diff --git a/browsr/browsr.css b/browsr/browsr.css index fed6d02..591c7ce 100644 --- a/browsr/browsr.css +++ b/browsr/browsr.css @@ -55,6 +55,11 @@ StaticWindow { width: auto; } +TextWindow { + border: none; + padding: 0; +} + DataTableWindow { overflow: auto scroll; min-width: 100%; @@ -84,3 +89,40 @@ ConfirmationPopUp Button { margin-top: 1; width: 100%; } + +/* -- ShortcutsPopUp -- */ + +ShortcutsWindow { + width: 100%; + height: 100%; + align: center middle; +} + +ShortcutsPopUp { + background: $boost; + height: auto; + max-width: 100; + min-width: 40; + border: wide $primary; + padding: 1 2; + margin: 1 2; + box-sizing: border-box; +} + +#shortcuts-header { + width: 100%; + content-align: center middle; + text-style: bold; + margin-bottom: 1; +} + +#shortcuts-table { + height: auto; + max-height: 20; + border: none; +} + +ShortcutsPopUp Button { + margin-top: 1; + width: 100%; +} diff --git a/browsr/browsr.py b/browsr/browsr.py index e5e84b1..85ba5ad 100644 --- a/browsr/browsr.py +++ b/browsr/browsr.py @@ -74,6 +74,12 @@ def action_download_file(self) -> None: """ self.code_browser_screen.code_browser.download_file_workflow() + def action_copy_text(self) -> None: + """ + An action to copy text. + """ + self.code_browser_screen.code_browser.window_switcher.text_window.copy_selected_text() + app = Browsr( config_object=TextualAppContext( diff --git a/browsr/cli.py b/browsr/cli.py index c9232a4..76ebdfd 100644 --- a/browsr/cli.py +++ b/browsr/cli.py @@ -189,15 +189,18 @@ def browsr( ``` ## Key Bindings - - **`Q`** - Quit the application - - **`F`** - Toggle the file tree sidebar - - **`T`** - Toggle the rich theme for code formatting - - **`N`** - Toggle line numbers for code formatting - - **`D`** - Toggle dark mode for the application + - **`q`** - Quit the application + - **`f`** - Toggle the file tree sidebar + - **`t`** - Toggle the rich theme for code formatting + - **`n`** - Toggle line numbers for code formatting + - **`d`** - Toggle dark mode for the application - **`.`** - Parent Directory - go up one directory - - **`R`** - Reload the current directory - - **`C`** - Copy the current file or directory path to the clipboard - - **`X`** - Download the file from cloud storage + - **`r`** - Reload the current directory + - **`w`** - Toggle word wrap for code files + - **`c`** - Copy the current file or directory path to the clipboard + - **`shift+c`** - Copy the selected text to the clipboard + - **`x`** - Download the file from cloud storage + - **`?`** - View keyboard shortcuts """ extra_kwargs = {} if kwargs: diff --git a/browsr/config.py b/browsr/config.py index 5dc5562..5acac12 100644 --- a/browsr/config.py +++ b/browsr/config.py @@ -2,6 +2,7 @@ browsr configuration file """ +from collections import OrderedDict from os import getenv favorite_themes: list[str] = [ @@ -57,3 +58,63 @@ ".xv", ".pdf", ] + +textarea_default_theme = "vscode_dark" + +textarea_theme_map = OrderedDict( + [ + ("monokai", "monokai"), + ("dracula", "dracula"), + ("github-dark", "vscode_dark"), + ("solarized-light", "github_light"), + ("material", "vscode_dark"), + ("one-dark", "vscode_dark"), + ("solarized-dark", "vscode_dark"), + ("native", "vscode_dark"), + ("emacs", "vscode_dark"), + ("vim", "vscode_dark"), + ("paraiso-dark", "vscode_dark"), + ] +) + +language_map = { + "py": "python", + "pyi": "python", + "pyw": "python", + "md": "markdown", + "markdown": "markdown", + "json": "json", + "toml": "toml", + "yaml": "yaml", + "yml": "yaml", + "html": "html", + "htm": "html", + "css": "css", + "js": "javascript", + "mjs": "javascript", + "cjs": "javascript", + "rs": "rust", + "go": "go", + "sql": "sql", + "java": "java", + "sh": "bash", + "bash": "bash", + "zsh": "bash", + "xml": "xml", + "rss": "xml", + "svg": "xml", + "xsd": "xml", + "xslt": "xml", +} + +filename_map = { + "uv.lock": "toml", + "pyproject.toml": "toml", + "cargo.lock": "toml", + "cargo.toml": "toml", + "makefile": "bash", + "dockerfile": "bash", + "procfile": "yaml", + ".gitignore": "bash", + ".env": "bash", +} diff --git a/browsr/screens/code_browser.py b/browsr/screens/code_browser.py index bb74038..3027e81 100644 --- a/browsr/screens/code_browser.py +++ b/browsr/screens/code_browser.py @@ -27,25 +27,40 @@ class CodeBrowserScreen(SortedBindingsScreen): Code Browser Screen """ + LAYERS: ClassVar[list[str]] = ["default", "overlay"] + BINDINGS: ClassVar[list[BindingType]] = [ Binding(key="f", action="toggle_files", description="Files"), Binding(key="t", action="theme", description="Theme"), Binding(key="n", action="linenos", description="Line Numbers"), - Binding(key="r", action="reload", description="Reload"), - Binding(key=".", action="parent_dir", description="Parent Directory"), + Binding(key="r", action="reload", description="Reload", show=False), + Binding( + key=".", + action="parent_dir", + description="Parent Directory", + key_display=".", + show=False, + ), + Binding(key="w", action="toggle_wrap", description="Toggle Wrap", show=False), + Binding( + key="?", action="toggle_shortcuts", description="Shortcuts", key_display="?" + ), ] BINDING_WEIGHTS: ClassVar[dict[str, int]] = { - "ctrl+c": 1, - "q": 2, - "f": 3, - "t": 4, - "n": 5, - "d": 6, - "r": 995, - ".": 996, - "c": 997, - "x": 998, + "ctrl+c": 5, + "q": 10, + "f": 15, + "t": 20, + "n": 25, + "d": 30, + "r": 905, + ".": 910, + "c": 920, + "x": 925, + "w": 930, + "C": 935, + "?": 940, } def __init__( @@ -157,8 +172,8 @@ def action_linenos(self) -> None: """ if self.code_browser.selected_file_path is None: return - self.code_browser.static_window.linenos = ( - not self.code_browser.static_window.linenos + self.code_browser.window_switcher.linenos = ( + not self.code_browser.window_switcher.linenos ) def action_reload(self) -> None: @@ -189,3 +204,17 @@ def action_reload(self) -> None: severity="information", timeout=1, ) + + def action_toggle_wrap(self) -> None: + """ + Toggle soft wrap for the text area. + """ + self.code_browser.window_switcher.text_window.soft_wrap = ( + not self.code_browser.window_switcher.text_window.soft_wrap + ) + + def action_toggle_shortcuts(self) -> None: + """ + Toggle the shortcuts window + """ + self.code_browser.toggle_shortcuts() diff --git a/browsr/utils.py b/browsr/utils.py index d8cab93..f8b7f24 100644 --- a/browsr/utils.py +++ b/browsr/utils.py @@ -39,8 +39,8 @@ def open_image(document: UPath, screen_width: float) -> Pixels: image_width = image.width image_height = image.height size_ratio = image_width / screen_width - new_width = min(int(image_width / size_ratio), image_width) - new_height = min(int(image_height / size_ratio), image_height) + new_width = int(image_width / size_ratio) + new_height = int(image_height / size_ratio) resized = image.resize((new_width, new_height)) return Pixels.from_image(resized) diff --git a/browsr/widgets/base.py b/browsr/widgets/base.py new file mode 100644 index 0000000..677ede1 --- /dev/null +++ b/browsr/widgets/base.py @@ -0,0 +1,84 @@ +""" +Base classes for widgets +""" + +from __future__ import annotations + +from typing import ClassVar + +from textual import on +from textual.binding import Binding, BindingType +from textual.containers import Container +from textual.message import Message + + +class BasePopUp(Container): + """ + Base class for popup widgets + """ + + can_focus = True + + BINDINGS: ClassVar[list[BindingType]] = [ + Binding("escape", "close", "Close", show=False), + ] + + class Toggle(Message): + """ + Toggle the popup visibility + """ + + def __init__(self, display: bool | None = None) -> None: + self.display = display + super().__init__() + + def action_close(self) -> None: + """ + Close the popup + """ + self.post_message(self.Toggle(display=False)) + + +class BaseOverlay(Container): + """ + Base class for overlay containers + """ + + can_focus = True + + BINDINGS: ClassVar[list[BindingType]] = [ + Binding("escape", "close", "Close", show=False), + ] + + def action_close(self) -> None: + """ + Close the overlay + """ + self.display = False + + def on_mount(self) -> None: + """ + On Mount + """ + self.display = False + + def watch_display(self, display: bool) -> None: + """ + Focus the overlay when it is displayed + """ + if display: + self.focus() + + @on(BasePopUp.Toggle) + def handle_toggle(self, message: BasePopUp.Toggle) -> None: + """ + Handle the toggle message from the popup + """ + if message.display is False: + self.action_close() + elif message.display is True: + self.display = True + elif self.display: + self.action_close() + else: + self.display = True diff --git a/browsr/widgets/code_browser.py b/browsr/widgets/code_browser.py index ccd824f..c0f37d2 100644 --- a/browsr/widgets/code_browser.py +++ b/browsr/widgets/code_browser.py @@ -7,16 +7,15 @@ import inspect import pathlib import shutil -from textwrap import dedent from typing import Any import pyperclip -from rich.markdown import Markdown from textual import on, work from textual.app import ComposeResult from textual.containers import Container from textual.events import Mount from textual.reactive import var +from textual.widget import Widget from textual.widgets import DirectoryTree from textual_universal_directorytree import ( UPath, @@ -27,14 +26,15 @@ TextualAppContext, ) from browsr.config import favorite_themes -from browsr.exceptions import FileSizeError from browsr.utils import ( get_file_info, handle_duplicate_filenames, ) +from browsr.widgets.base import BaseOverlay, BasePopUp from browsr.widgets.confirmation import ConfirmationPopUp, ConfirmationWindow from browsr.widgets.double_click_directory_tree import DoubleClickDirectoryTree from browsr.widgets.files import CurrentFileInfoBar +from browsr.widgets.shortcuts import ShortcutsPopUp, ShortcutsWindow from browsr.widgets.universal_directory_tree import BrowsrDirectoryTree from browsr.widgets.windows import DataTableWindow, StaticWindow, WindowSwitcher @@ -59,10 +59,6 @@ class CodeBrowser(Container): force_show_tree = var(False) selected_file_path: UPath | None | var[None] = var(None) - hidden_table_view = var(False) - table_view_status = var(False) - static_window_status = var(False) - def __init__( self, config_object: TextualAppContext, @@ -93,6 +89,17 @@ def __init__( self.confirmation, id="confirmation-container" ) self.confirmation_window.display = False + self.shortcuts = ShortcutsPopUp() + self.shortcuts_window = ShortcutsWindow( + self.shortcuts, id="shortcuts-container" + ) + self.shortcuts_window.display = False + self._content_display_state: dict[Widget, bool] = {} + self._overlay_history: list[BaseOverlay] = [] + self._overlay_popup_map: dict[BaseOverlay, BasePopUp] = { + self.confirmation_window: self.confirmation, + self.shortcuts_window: self.shortcuts, + } # Copy Pasting self._copy_function = pyperclip.determine_clipboard()[0] self._copy_supported = inspect.isfunction(self._copy_function) @@ -118,6 +125,7 @@ def compose(self) -> ComposeResult: yield self.directory_tree yield self.window_switcher yield self.confirmation_window + yield self.shortcuts_window @on(Mount) def bind_keys(self) -> None: @@ -126,11 +134,19 @@ def bind_keys(self) -> None: """ if self._copy_supported: self.app.bind( - keys="c", action="copy_file_path", description="Copy Path", show=True + keys="c", action="copy_file_path", description="Copy Path", show=False ) + self.app.bind( + keys="C", + action="copy_text", + description="Copy Text", + show=False, + key_display="shift+c", + ) + if is_remote_path(self.initial_file_path): # type: ignore[arg-type] self.app.bind( - keys="x", action="download_file", description="Download File", show=True + keys="x", action="download_file", description="Download", show=True ) def watch_show_tree(self, show_tree: bool) -> None: @@ -162,14 +178,100 @@ def handle_download_confirmation( self.download_selected_file() @on(ConfirmationPopUp.DisplayToggle) - def handle_table_view_display_toggle( + def handle_confirmation_window_display_toggle( self, _: ConfirmationPopUp.DisplayToggle ) -> None: """ - Handle the table view display toggle. + Handle the confirmation window display toggle. + """ + self._close_overlay(self.confirmation_window) + + @on(ShortcutsPopUp.DisplayToggle) + def handle_shortcuts_window_display_toggle( + self, _: ShortcutsPopUp.DisplayToggle + ) -> None: + """ + Handle the shortcuts window display toggle. + """ + self._close_overlay(self.shortcuts_window) + + def _get_content_window_display_state(self) -> dict[Widget, bool]: + """ + Capture the current content window visibility state. + """ + return { + self.window_switcher.datatable_window: ( + self.window_switcher.datatable_window.display + ), + self.window_switcher.text_window: self.window_switcher.text_window.display, + self.window_switcher.vim_scroll: self.window_switcher.vim_scroll.display, + } + + def _hide_content_windows(self) -> None: + """ + Hide the content windows while an overlay is active. """ - self.datatable_window.display = self.table_view_status - self.window_switcher.vim_scroll.display = self.static_window_status + self.window_switcher.datatable_window.display = False + self.window_switcher.text_window.display = False + self.window_switcher.vim_scroll.display = False + + def _restore_content_windows(self) -> None: + """ + Restore the content windows after all overlays are closed. + """ + for widget, state in self._content_display_state.items(): + widget.display = state + active_widget = self.window_switcher.get_active_widget() + if active_widget is not None: + active_widget.focus() + + def _get_active_overlay(self) -> BaseOverlay | None: + """ + Get the currently active overlay. + """ + if not self._overlay_history: + return None + return self._overlay_history[-1] + + def _focus_overlay(self, overlay: BaseOverlay) -> None: + """ + Focus the popup inside the active overlay. + """ + self._overlay_popup_map[overlay].focus() + + def _show_overlay(self, overlay: BaseOverlay) -> None: + """ + Display an overlay while preserving the last content window state. + """ + active_overlay = self._get_active_overlay() + if active_overlay is None: + self._content_display_state = self._get_content_window_display_state() + self._hide_content_windows() + elif active_overlay is not overlay: + active_overlay.display = False + + if overlay in self._overlay_history: + self._overlay_history.remove(overlay) + self._overlay_history.append(overlay) + overlay.display = True + self._focus_overlay(overlay) + + def _close_overlay(self, overlay: BaseOverlay) -> None: + """ + Close an overlay and restore the previous overlay or content window. + """ + if overlay in self._overlay_history: + was_active_overlay = self._overlay_history[-1] is overlay + self._overlay_history.remove(overlay) + overlay.display = False + if was_active_overlay and self._overlay_history: + previous_overlay = self._overlay_history[-1] + previous_overlay.display = True + self._focus_overlay(previous_overlay) + elif was_active_overlay: + self._restore_content_windows() + else: + overlay.display = False @on(DirectoryTree.FileSelected) def handle_file_selected(self, message: DirectoryTree.FileSelected) -> None: @@ -178,19 +280,7 @@ def handle_file_selected(self, message: DirectoryTree.FileSelected) -> None: """ self.selected_file_path = message.path # type: ignore[assignment] file_info = get_file_info(file_path=self.selected_file_path) # type: ignore[arg-type] - try: - self.static_window.handle_file_size( - file_info=file_info, max_file_size=self.config_object.max_file_size - ) - self.window_switcher.render_file(file_path=self.selected_file_path) # type: ignore[arg-type] - except FileSizeError as e: - error_message = self.static_window.handle_exception(exception=e) - error_syntax = self.static_window.text_to_syntax( - text=error_message, - file_path=self.selected_file_path, # type: ignore[arg-type] - ) - self.static_window.update(error_syntax) - self.window_switcher.switch_window(self.static_window) + self.window_switcher.render_file(file_path=self.selected_file_path) # type: ignore[arg-type] self.post_message(CurrentFileInfoBar.FileInfoUpdate(new_file=file_info)) @on(DoubleClickDirectoryTree.DirectoryDoubleClicked) @@ -233,25 +323,25 @@ def download_file_workflow(self) -> None: elif self.selected_file_path.is_dir(): return elif is_remote_path(self.selected_file_path): + if self._get_active_overlay() is self.confirmation_window: + self._close_overlay(self.confirmation_window) + return handled_download_path = self._get_download_file_name() - prompt_message: str = dedent( - f""" - ## File Download - - **Are you sure you want to download that file?** - - **File:** `{self.selected_file_path}` - - **Path:** `{handled_download_path}` - """ + self.confirmation.prompt_download( + file_path=str(self.selected_file_path), + download_path=str(handled_download_path), ) - self.confirmation.download_message.update(Markdown(prompt_message)) - self.confirmation.refresh() - self.table_view_status = self.datatable_window.display - self.static_window_status = self.window_switcher.vim_scroll.display - self.datatable_window.display = False - self.window_switcher.vim_scroll.display = False - self.confirmation_window.display = True + self._show_overlay(self.confirmation_window) + + def toggle_shortcuts(self) -> None: + """ + Toggle the shortcuts window. + """ + if self._get_active_overlay() is self.shortcuts_window: + self._close_overlay(self.shortcuts_window) + else: + self.shortcuts.update_shortcuts() + self._show_overlay(self.shortcuts_window) @work(thread=True) def download_selected_file(self) -> None: diff --git a/browsr/widgets/confirmation.py b/browsr/widgets/confirmation.py index 6118570..c0d6aef 100644 --- a/browsr/widgets/confirmation.py +++ b/browsr/widgets/confirmation.py @@ -1,14 +1,22 @@ +""" +Confirmation Widget +""" + +from __future__ import annotations + from textwrap import dedent from rich.markdown import Markdown from textual import on from textual.app import ComposeResult -from textual.containers import Container +from textual.events import Key from textual.message import Message from textual.widgets import Button, Static +from browsr.widgets.base import BaseOverlay, BasePopUp + -class ConfirmationPopUp(Container): +class ConfirmationPopUp(BasePopUp): """ A Pop Up that asks for confirmation """ @@ -26,19 +34,17 @@ class ConfirmationWindowDownload(Message): Confirmation Window """ - class ConfirmationWindowDisplay(Message): + class DisplayToggle(Message): """ - Confirmation Window + TableView Display """ - def __init__(self, display: bool) -> None: - self.display = display - super().__init__() - - class DisplayToggle(Message): + def action_close(self) -> None: """ - TableView Display + Close the popup and restore the previous display state. """ + super().action_close() + self.post_message(self.DisplayToggle()) def compose(self) -> ComposeResult: """ @@ -46,30 +52,56 @@ def compose(self) -> ComposeResult: """ self.download_message = Static(Markdown("")) yield self.download_message - yield Button("Yes", variant="success") - yield Button("No", variant="error") + yield Button("Yes (y)", variant="success", id="confirm-yes") + yield Button("No (n)", variant="error", id="confirm-no") + + def prompt_download(self, file_path: str, download_path: str) -> None: + """ + Prompt the user to download a file + """ + prompt_message: str = dedent( + f""" + ## File Download + + **Are you sure you want to download that file?** + + **File:** `{file_path}` + + **Path:** `{download_path}` + """ + ) + self.download_message.update(Markdown(prompt_message)) + self.refresh() @on(Button.Pressed) def handle_download_selection(self, message: Button.Pressed) -> None: """ Handle Button Presses """ - self.post_message(self.ConfirmationWindowDisplay(display=False)) + self.action_close() if message.button.variant == "success": self.post_message(self.ConfirmationWindowDownload()) - self.post_message(self.DisplayToggle()) + + @on(Key) + def handle_key_press(self, message: Key) -> None: + """ + Handle Key Presses + """ + if message.key.lower() == "y": + self.post_message(self.ConfirmationWindowDownload()) + self.action_close() + elif message.key.lower() == "n": + self.action_close() -class ConfirmationWindow(Container): +class ConfirmationWindow(BaseOverlay): """ Window containing the Confirmation Pop Up """ - @on(ConfirmationPopUp.ConfirmationWindowDisplay) - def handle_confirmation_window_display( - self, message: ConfirmationPopUp.ConfirmationWindowDisplay - ) -> None: + def action_close(self) -> None: """ - Handle Confirmation Window Display + Close the overlay and restore the previous display state. """ - self.display = message.display + super().action_close() + self.post_message(ConfirmationPopUp.DisplayToggle()) diff --git a/browsr/widgets/shortcuts.py b/browsr/widgets/shortcuts.py new file mode 100644 index 0000000..c59078d --- /dev/null +++ b/browsr/widgets/shortcuts.py @@ -0,0 +1,92 @@ +""" +Shortcuts Widget +""" + +from __future__ import annotations + +from typing import ClassVar + +from textual.app import ComposeResult +from textual.binding import Binding +from textual.message import Message +from textual.widgets import DataTable, Static + +from browsr.widgets.base import BaseOverlay, BasePopUp + + +class ShortcutsPopUp(BasePopUp): + """A Pop Up that displays keyboard shortcuts""" + + class DisplayToggle(Message): + """Shortcuts window display""" + + TRUSTED_ACTIONS: ClassVar[list[str]] = [ + "copy_file_path", + "copy_text", + "download_file", + "toggle_files", + "parent_dir", + "quit", + "reload", + "toggle_shortcuts", + "toggle_dark", + "linenos", + "theme", + "toggle_wrap", + ] + DESCRIPTION_MAPPINGS: ClassVar[dict[str, str]] = { + "toggle_dark": "Toggle Dark Mode", + "linenos": "Toggle Line Numbers", + "theme": "Toggle Theme", + "download_file": "Download File", + "toggle_shortcuts": "Show/Hide Shortcuts", + } + + IGNORED_KEYS: ClassVar[list[str]] = ["ctrl+q"] + + def compose(self) -> ComposeResult: + """Compose the Shortcuts Pop Up""" + yield Static("Keyboard Shortcuts", id="shortcuts-header") + yield DataTable(id="shortcuts-table", show_cursor=False, zebra_stripes=True) + + def action_close(self) -> None: + """Close the popup and restore the previous display state.""" + super().action_close() + self.post_message(self.DisplayToggle()) + + def on_mount(self) -> None: + """Called when the widget is mounted""" + table = self.query_one(DataTable) + table.add_columns("Key", "Description") + table.cursor_type = "row" + self.update_shortcuts() + + def update_shortcuts(self) -> None: + """Update the shortcuts displayed in the table""" + table = self.query_one(DataTable) + table.clear() + rows = [] + for active_binding in self.app.active_bindings.values(): + binding = active_binding.binding + if not isinstance(binding, Binding): + continue + key = binding.key_display or binding.key + if binding.action not in self.TRUSTED_ACTIONS or key in self.IGNORED_KEYS: + continue + description = self.DESCRIPTION_MAPPINGS.get( + binding.action, binding.description + ) + cells = [key, description] + rows.append(cells) + sorted_rows = sorted(rows, key=lambda x: x[1]) + for row in sorted_rows: + table.add_row(*row) + + +class ShortcutsWindow(BaseOverlay): + """Window containing the Shortcuts Pop Up""" + + def action_close(self) -> None: + """Close the overlay and restore the previous display state.""" + super().action_close() + self.post_message(ShortcutsPopUp.DisplayToggle()) diff --git a/browsr/widgets/windows.py b/browsr/widgets/windows.py index 303e611..270cbda 100644 --- a/browsr/widgets/windows.py +++ b/browsr/widgets/windows.py @@ -4,35 +4,67 @@ from __future__ import annotations +import contextlib from json import JSONDecodeError -from typing import Any, ClassVar +from typing import Any, ClassVar, NamedTuple import orjson import pandas as pd +import pyperclip from art import text2art from numpy import nan from rich.markdown import Markdown from rich.syntax import Syntax from rich_pixels import Pixels from textual.app import ComposeResult +from textual.binding import Binding from textual.containers import Container from textual.message import Message from textual.reactive import Reactive, reactive from textual.widget import Widget -from textual.widgets import Static +from textual.widgets import Static, TextArea from textual_universal_directorytree import UPath from browsr.base import TextualAppContext -from browsr.config import favorite_themes, image_file_extensions +from browsr.config import ( + favorite_themes, + filename_map, + image_file_extensions, + language_map, + textarea_default_theme, + textarea_theme_map, +) from browsr.exceptions import FileSizeError from browsr.utils import ( ArchiveFileError, FileInfo, + get_file_info, open_image, ) from browsr.widgets.vim import VimDataTable, VimScroll +class FileToStringResult(NamedTuple): + result: str + error_occurred: bool + + +class ThemeVisibleMixin: + """ + Mixin for widgets with a theme + """ + + theme: Reactive[str] = reactive(favorite_themes[0]) + + +class LinenosVisibleMixin: + """ + Mixin for widgets with line numbers + """ + + linenos: Reactive[bool] = reactive(False) + + class BaseCodeWindow(Widget): """ Base code view widget @@ -50,10 +82,15 @@ def __init__(self, window: type[BaseCodeWindow], scroll_home: bool = False): self.scroll_home: bool = scroll_home super().__init__() - def file_to_string(self, file_path: UPath, max_lines: int | None = None) -> str: + def file_to_string( + self, file_path: UPath, max_lines: int | None = None + ) -> FileToStringResult: """ Load a file into a string + + Returns a tuple of the string and a boolean indicating if an exception occurred. """ + error_occurred = False try: if file_path.suffix in self.archive_extensions: message = f"Cannot render archive file {file_path}." @@ -61,9 +98,10 @@ def file_to_string(self, file_path: UPath, max_lines: int | None = None) -> str: text = file_path.read_text(encoding="utf-8") except Exception as e: text = self.handle_exception(exception=e) + error_occurred = True if max_lines: text = "\n".join(text.split("\n")[:max_lines]) - return text + return FileToStringResult(result=text, error_occurred=error_occurred) def file_to_image(self, file_path: UPath) -> Pixels: """ @@ -77,7 +115,7 @@ def file_to_json(self, file_path: UPath, max_lines: int | None = None) -> str: """ Load a file into a JSON object """ - code_str = self.file_to_string(file_path=file_path) + code_str = self.file_to_string(file_path=file_path).result try: code_obj = orjson.loads(code_str) code_str = orjson.dumps(code_obj, option=orjson.OPT_INDENT_2).decode( @@ -103,61 +141,26 @@ def handle_file_size(cls, file_info: FileInfo, max_file_size: int = 5) -> None: def handle_exception(cls, exception: Exception) -> str: """ Handle an exception - - This method is used to handle exceptions that occur when rendering a file. - When an uncommon exception occurs, the method will raise the exception. - - Parameters - ---------- - exception: Exception - The exception that occurred. - - Raises - ------ - Exception - If the exception is not one of the expected exceptions. - - Returns - ------- - str - The error message to display. """ font = "univers" - if isinstance(exception, ArchiveFileError): - error_message = ( - text2art("ARCHIVE", font=font) + "\n\n" + text2art("FILE", font=font) - ) - elif isinstance(exception, FileSizeError): - error_message = ( - text2art("FILE TOO", font=font) + "\n\n" + text2art("LARGE", font=font) - ) - elif isinstance(exception, PermissionError): - error_message = ( - text2art("PERMISSION", font=font) - + "\n\n" - + text2art("ERROR", font=font) - ) - elif isinstance(exception, UnicodeError): - error_message = ( - text2art("ENCODING", font=font) + "\n\n" + text2art("ERROR", font=font) - ) - elif isinstance(exception, FileNotFoundError): - error_message = ( - text2art("FILE NOT", font=font) + "\n\n" + text2art("FOUND", font=font) - ) - else: - raise exception from exception - return error_message + exception_map = { + ArchiveFileError: ("ARCHIVE", "FILE"), + FileSizeError: ("FILE TOO", "LARGE"), + PermissionError: ("PERMISSION", "ERROR"), + UnicodeError: ("ENCODING", "ERROR"), + FileNotFoundError: ("FILE NOT", "FOUND"), + } + for exc_type, (line1, line2) in exception_map.items(): + if isinstance(exception, exc_type): + return text2art(line1, font=font) + "\n\n" + text2art(line2, font=font) + raise exception from exception -class StaticWindow(Static, BaseCodeWindow): +class StaticWindow(Static, BaseCodeWindow, ThemeVisibleMixin, LinenosVisibleMixin): """ A static widget for displaying code. """ - linenos: Reactive[bool] = reactive(False) - theme: Reactive[str] = reactive(favorite_themes[0]) - rich_themes: ClassVar[list[str]] = favorite_themes def __init__( @@ -173,7 +176,7 @@ def file_to_markdown( Load a file into a Markdown """ return Markdown( - self.file_to_string(file_path, max_lines=max_lines), + self.file_to_string(file_path, max_lines=max_lines).result, code_theme=self.theme, hyperlinks=True, ) @@ -198,6 +201,7 @@ def watch_linenos(self, linenos: bool) -> None: """ if isinstance(self.content, Syntax): self.content.line_numbers = linenos + self.refresh() def watch_theme(self, theme: str) -> None: """ @@ -216,16 +220,93 @@ def watch_theme(self, theme: str) -> None: elif isinstance(self.content, Markdown): self.content.code_theme = self.theme - def next_theme(self) -> str | None: + +class TextWindow(TextArea, BaseCodeWindow, ThemeVisibleMixin, LinenosVisibleMixin): + """ + A window that displays text using a TextArea. + """ + + theme: Reactive[str] = reactive(textarea_default_theme) + default_theme: ClassVar[str] = textarea_default_theme + + BINDINGS: ClassVar[list[Binding | tuple[str, str] | tuple[str, str, str]]] = [ + Binding("j", "cursor_down", "Down", show=False), + Binding("k", "cursor_up", "Up", show=False), + Binding("l", "cursor_right", "Right", show=False), + Binding("h", "cursor_left", "Left", show=False), + ] + + def __init__(self, **kwargs: Any) -> None: + super().__init__(read_only=True, **kwargs) + self.theme = self.default_theme + self.soft_wrap = False + self.display = False + + def watch_linenos(self, linenos: bool) -> None: """ - Switch to the next theme + Called when linenos is modified. """ - if not isinstance(self.content, (Syntax, Markdown)): - return None - current_index = favorite_themes.index(self.theme) - next_theme = favorite_themes[(current_index + 1) % len(favorite_themes)] - self.theme = next_theme - return next_theme + before_scroll = self.scroll_x, self.scroll_y + self.show_line_numbers = linenos + self.scroll_to(x=before_scroll[0], y=before_scroll[1], animate=False) + + def copy_selected_text(self) -> None: + """ + Copy the selected text to the clipboard. + """ + if self.selected_text: + pyperclip.copy(self.selected_text) + self.app.notify( + title="Copied", + message="Selected text copied to clipboard", + severity="information", + timeout=1, + ) + else: + self.app.notify( + title="No Selection", + message="No text selected to copy", + severity="warning", + timeout=1, + ) + + def apply_smart_theme(self, rich_theme: str) -> None: + """ + Apply a theme to the TextArea + """ + with contextlib.suppress(RuntimeError, AttributeError): + if not getattr(self.app, "dark", True): + if self.theme != "github_light": + self.theme = "github_light" + return + target = textarea_theme_map.get(rich_theme, self.default_theme) + if target in self.available_themes and self.theme != target: + self.theme = target + + def load_file(self, text: str, file_path: UPath) -> None: + """ + Load text and detect language + """ + self.load_text(text) + self.detect_language(file_path) + + def detect_language(self, file_path: str | UPath) -> None: + """ + Detect the language from the file path + """ + if isinstance(file_path, str): + file_path = UPath(file_path) + file_name = file_path.name.lower() + if file_name in filename_map: + self.language = filename_map[file_name] + return + ext = file_path.suffix.lstrip(".").lower() + if ext in language_map: + self.language = language_map[ext] + elif ext in self.available_languages: + self.language = ext + else: + self.language = None class DataTableWindow(VimDataTable, BaseCodeWindow): @@ -285,7 +366,7 @@ def refresh_from_df( self.add_row(*row) -class WindowSwitcher(Container): +class WindowSwitcher(Container, ThemeVisibleMixin, LinenosVisibleMixin): """ A container that contains the file content windows """ @@ -307,20 +388,62 @@ def __init__( self, config_object: TextualAppContext, *args: Any, **kwargs: Any ) -> None: super().__init__(*args, **kwargs) + self.rendered_file: UPath | None = None self.config_object = config_object self.static_window = StaticWindow(expand=True, config_object=config_object) + self.text_window = TextWindow() self.datatable_window = DataTableWindow( zebra_stripes=True, show_header=True, show_cursor=True, id="table-view" ) self.datatable_window.display = False self.vim_scroll = VimScroll(self.static_window) - self.rendered_file: UPath | None = None + + def watch_linenos(self, linenos: bool) -> None: + """ + Called when linenos is modified. + """ + self.static_window.linenos = linenos + self.text_window.linenos = linenos + + def watch_theme(self, theme: str) -> None: + """ + Called when theme is modified. + """ + self.static_window.theme = theme + if self.text_window.display: + self.text_window.apply_smart_theme(theme) + self._update_subtitle() + + def _update_subtitle(self) -> None: + """ + Update the app subtitle + """ + if self.rendered_file is None: + return + active_widget = self.get_active_widget() + if active_widget is self.text_window: + display_theme = self.text_window.theme.replace("_", "-") + elif active_widget is self.vim_scroll: + display_theme = self.static_window.theme + else: + self.app.sub_title = str(self.rendered_file) + return + self.app.sub_title = str(self.rendered_file) + f" [{display_theme}]" + + def watch_dark(self, _dark: bool) -> None: + """ + Called when dark mode is modified. + """ + self.text_window.apply_smart_theme(self.theme) + self.static_window.refresh() + self._update_subtitle() def compose(self) -> ComposeResult: """ Compose the widget """ yield self.vim_scroll + yield self.text_window yield self.datatable_window def get_active_widget(self) -> Widget: # type: ignore[return] @@ -329,6 +452,8 @@ def get_active_widget(self) -> Widget: # type: ignore[return] """ if self.vim_scroll.display: return self.vim_scroll + elif self.text_window.display: + return self.text_window elif self.datatable_window.display: return self.datatable_window @@ -336,51 +461,44 @@ def switch_window(self, window: BaseCodeWindow) -> None: """ Switch to the window """ - screens: dict[Widget, Widget] = { + window_map: dict[Widget, Widget] = { self.static_window: self.vim_scroll, + self.text_window: self.text_window, self.datatable_window: self.datatable_window, } - for window_screen, _ in screens.items(): - if window is window_screen: - screens[window_screen].display = True - else: - screens[window_screen].display = False + for window_widget, container_widget in window_map.items(): + container_widget.display = window is window_widget + self._update_subtitle() def render_file(self, file_path: UPath, scroll_home: bool = True) -> None: """ Render a file """ - switch_window = self.static_window - joined_suffixes = "".join(file_path.suffixes).lower() - if joined_suffixes in self.datatable_extensions: - self.datatable_window.refresh_from_file( - file_path=file_path, max_lines=self.config_object.max_lines - ) - switch_window = self.datatable_window # type: ignore[assignment] - elif file_path.suffix.lower() in self.image_extensions: - image = self.static_window.file_to_image(file_path=file_path) - self.static_window.update(image) - elif file_path.suffix.lower() in self.markdown_extensions: - markdown = self.static_window.file_to_markdown( - file_path=file_path, max_lines=self.config_object.max_lines - ) - self.static_window.update(markdown) - elif file_path.suffix.lower() in self.json_extensions: - json_str = self.static_window.file_to_json( - file_path=file_path, max_lines=self.config_object.max_lines - ) - json_syntax = self.static_window.text_to_syntax( - text=json_str, file_path=file_path + try: + file_info = get_file_info(file_path=file_path) + self.static_window.handle_file_size( + file_info=file_info, max_file_size=self.config_object.max_file_size ) - self.static_window.update(json_syntax) - switch_window = self.static_window - else: - string = self.static_window.file_to_string( - file_path=file_path, max_lines=self.config_object.max_lines + joined_suffixes = "".join(file_path.suffixes).lower() + if joined_suffixes in self.datatable_extensions: + switch_window = self._render_datatable(file_path) + elif file_path.suffix.lower() in self.image_extensions: + switch_window = self._render_image(file_path) + elif file_path.suffix.lower() in self.markdown_extensions: + switch_window = self._render_markdown(file_path) + elif file_path.suffix.lower() in self.json_extensions: + switch_window = self._render_json(file_path) + else: + switch_window = self._render_text(file_path) + except Exception as e: + error_message = self.static_window.handle_exception(exception=e) + error_syntax = self.static_window.text_to_syntax( + text=error_message, + file_path=file_path, ) - syntax = self.static_window.text_to_syntax(text=string, file_path=file_path) - self.static_window.update(syntax) + self.static_window.update(error_syntax) switch_window = self.static_window + self.switch_window(switch_window) active_widget = self.get_active_widget() if scroll_home: @@ -388,22 +506,82 @@ def render_file(self, file_path: UPath, scroll_home: bool = True) -> None: self.vim_scroll.scroll_home(animate=False) else: switch_window.scroll_home(animate=False) - if active_widget is self.vim_scroll: - self.app.sub_title = str(file_path) + f" [{self.static_window.theme}]" - else: - self.app.sub_title = str(file_path) self.rendered_file = file_path + self._update_subtitle() + + def _render_datatable(self, file_path: UPath) -> BaseCodeWindow: + """Render a datatable file""" + self.datatable_window.refresh_from_file( + file_path=file_path, max_lines=self.config_object.max_lines + ) + return self.datatable_window + + def _render_image(self, file_path: UPath) -> BaseCodeWindow: + """Render an image file""" + image = self.static_window.file_to_image(file_path=file_path) + self.static_window.update(image) + return self.static_window + + def _render_markdown(self, file_path: UPath) -> BaseCodeWindow: + """Render a markdown file""" + markdown = self.static_window.file_to_markdown( + file_path=file_path, max_lines=self.config_object.max_lines + ) + self.static_window.update(markdown) + return self.static_window + + def _render_json(self, file_path: UPath) -> BaseCodeWindow: + """Render a JSON file""" + json_str = self.static_window.file_to_json( + file_path=file_path, max_lines=self.config_object.max_lines + ) + self.text_window.load_file(json_str, file_path) + return self.text_window + + def _render_text(self, file_path: UPath) -> BaseCodeWindow: + """Render a text file""" + result = self.static_window.file_to_string( + file_path=file_path, max_lines=self.config_object.max_lines + ) + if result.error_occurred: + self.static_window.update(result.result) + return self.static_window + else: + self.text_window.load_file(result.result, file_path) + return self.text_window def next_theme(self) -> str | None: """ Switch to the next theme """ - if self.get_active_widget() is not self.vim_scroll: - return None - current_index = favorite_themes.index(self.static_window.theme) + active_widget = self.get_active_widget() + if active_widget is self.text_window: + return self._next_textarea_theme() + elif active_widget is self.vim_scroll: + return self._next_rich_theme() + return None + + def _next_textarea_theme(self) -> str: + """Switch to the next TextArea theme""" + themes = list(textarea_theme_map.values()) + unique_themes = list(dict.fromkeys(themes)) + try: + current_index = unique_themes.index(self.text_window.theme) + except ValueError: + current_index = -1 + next_theme = unique_themes[(current_index + 1) % len(unique_themes)] + self.text_window.theme = next_theme + self._update_subtitle() + return next_theme + + def _next_rich_theme(self) -> str: + """Switch to the next Rich theme""" + try: + current_index = favorite_themes.index(self.theme) + except ValueError: + current_index = -1 next_theme = favorite_themes[(current_index + 1) % len(favorite_themes)] - self.static_window.theme = next_theme - self.app.sub_title = str(self.rendered_file) + f" [{self.static_window.theme}]" + self.theme = next_theme return next_theme def action_toggle_files(self) -> None: diff --git a/pyproject.toml b/pyproject.toml index ec603de..ea29f23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ "rich>=14,<15", "rich-click~=1.9.7", "rich-pixels~=3.0.1", - "textual>=8,<9", + "textual[syntax]>=8,<9", "textual-universal-directorytree~=1.6.0", "universal-pathlib~=0.2.6", "Pillow>=12.1.1", diff --git a/tests/__snapshots__/test_screenshots/test_github_screenshot.svg b/tests/__snapshots__/test_screenshots/test_github_screenshot.svg deleted file mode 100644 index f7b3566..0000000 --- a/tests/__snapshots__/test_screenshots/test_github_screenshot.svg +++ /dev/null @@ -1,267 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - browsr - - - - - - - - - - browsr — github://juftin:browsr@v1.6.0/README.md [monokai] -📂 browsrbrowsr -├── 📁 .github -├── 📁 browsr -├── 📁 docs🌆 browsr Version 🌆 PyPI 🌆 Testing Status 🌆 GitHub License -├── 📁 requirements -├── 📁 testsbrowsr is a TUI (text-based user interface) file browser for your terminal. It's a simple way to browse your files and take a  -├── 📄 .gitignorepeek at their contents. Plus it works on local and remote file systems.                                                        -├── 📄 .pre-commit-config.yaml -├── 📄 .releaserc.js -├── 📄 LICENSE -├── 📄 mkdocs.yaml -├── 📄 pyproject.tomlInstallation -└── 📄 README.md -The below command recommends pipx instead of pip. pipx installs the package in an isolated environment and makes it easy to    -uninstall. If you'd like to use pip instead, just replace pipx with pip in the below command.                                  - - -pipx install browsr - - -Extra Installation - -If you're looking to use browsr on remote file systems, like AWS S3, you'll need to install the remote extra. If you'd like to -browse parquet files, you'll need to install the parquet extra. Or, even simpler, you can install the all extra to get all the -extras.                                                                                                                        - - -pipx install "browsr[all]" - - -Usage - - -browsr ~/Downloads/ - - -Simply give browsr a path to a file/directory and it will open a browser window with a file browser. You can also give it a    -URL to a remote file system, like AWS S3.                                                                                      - - -browsr s3://my-bucket/my-file.parquet - -▆▆ -Check out the Documentation for more - -🗂️  GitHub  🗄️️  3KB  📂  browsr  💾  README.md - q Quit  f Files  t Theme  n Line Numbers  d Dark Mode  . Parent Directory  r Reload  c Copy Path  x Download File ^p palette - - - diff --git a/tests/__snapshots__/test_screenshots/test_github_screenshot_license.svg b/tests/__snapshots__/test_screenshots/test_github_screenshot_license.svg index 02925c6..8c933f1 100644 --- a/tests/__snapshots__/test_screenshots/test_github_screenshot_license.svg +++ b/tests/__snapshots__/test_screenshots/test_github_screenshot_license.svg @@ -35,13 +35,12 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #e0e0e0 } .terminal-r3 { fill: #a0a3a6 } -.terminal-r4 { fill: #f8f8f2 } -.terminal-r5 { fill: #dfdfdf } -.terminal-r6 { fill: #0d0d0d } -.terminal-r7 { fill: #003054 } -.terminal-r8 { fill: #e0bf91 } -.terminal-r9 { fill: #ffa62b;font-weight: bold } -.terminal-r10 { fill: #495259 } +.terminal-r4 { fill: #cccccc } +.terminal-r5 { fill: #1f1f1f } +.terminal-r6 { fill: #003054 } +.terminal-r7 { fill: #e0bf91 } +.terminal-r8 { fill: #ffa62b;font-weight: bold } +.terminal-r9 { fill: #495259 } @@ -199,18 +198,18 @@ - + - browsr — github://juftin:browsr@v1.6.0/LICENSE [monokai] -MIT License + browsr — github://juftin:browsr@v1.6.0/LICENSE [vscode-dark] +MIT License                                                                                                                                                      -Copyright (c) 2023-present Justin Flannery <justin.flannery@juftin.com> +Copyright (c) 2023-present Justin Flannery <justin.flannery@juftin.com>                                                                                          -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in  +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in th -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.                                   -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FO +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR  @@ -246,9 +245,9 @@ - -🗂️  GitHub  🗄️️  1KB  📂  browsr  💾  LICENSE - q Quit  f Files  t Theme  n Line Numbers  d Dark Mode  . Parent Directory  r Reload  c Copy Path  x Download File ^p palette + +🗂️  GitHub  🗄️️  1KB  📂  browsr  💾  LICENSE + q Quit  f Files  t Theme  n Line Numbers  d Dark Mode  ? Shortcuts  x Download ^p palette diff --git a/tests/__snapshots__/test_screenshots/test_mkdocs_screenshot.svg b/tests/__snapshots__/test_screenshots/test_mkdocs_screenshot.svg index 152f09d..ed7c3d7 100644 --- a/tests/__snapshots__/test_screenshots/test_mkdocs_screenshot.svg +++ b/tests/__snapshots__/test_screenshots/test_mkdocs_screenshot.svg @@ -35,12 +35,12 @@ .terminal-r1 { fill: #c5c8c6 } .terminal-r2 { fill: #e0e0e0 } .terminal-r3 { fill: #a0a3a6 } -.terminal-r4 { fill: #959077 } -.terminal-r5 { fill: #dfdfdf } -.terminal-r6 { fill: #0d0d0d } -.terminal-r7 { fill: #ff4689 } -.terminal-r8 { fill: #f8f8f2 } -.terminal-r9 { fill: #e6db74 } +.terminal-r4 { fill: #6a9955 } +.terminal-r5 { fill: #cccccc } +.terminal-r6 { fill: #1f1f1f } +.terminal-r7 { fill: #569cd6;font-weight: bold } +.terminal-r8 { fill: #ce9178 } +.terminal-r9 { fill: #7daf9c } .terminal-r10 { fill: #e0bf91 } .terminal-r11 { fill: #ffa62b;font-weight: bold } .terminal-r12 { fill: #495259 } @@ -201,56 +201,56 @@ - + - browsr — github://juftin:browsr@v1.6.0/mkdocs.yaml [monokai] + browsr — github://juftin:browsr@v1.6.0/mkdocs.yaml [vscode-dark] # schema: https://squidfunk.github.io/mkdocs-material/schema.json -site_namebrowsr                                                                                                                                              -nav: --index.md                                                                                                                                                 --Command Line Interface ⌨️cli.md                                                                                                                        --Contributing 🤝contributing.md                                                                                                                         --API Documentation 🤖reference/                                                                                                                         -theme: -faviconhttps://raw.githubusercontent.com/juftin/browsr/main/docs/_static/browsr_no_label.png                                                             -logohttps://raw.githubusercontent.com/juftin/browsr/main/docs/_static/browsr_no_label.png                                                                -namematerial                                                                                                                                             -features: --navigation.tracking                                                                                                                                  --content.code.annotate                                                                                                                                --content.code.copy                                                                                                                                    -palette: --media"(prefers-color-scheme:light)" -schemedefault                                                                                                                                      -accentpurple                                                                                                                                       -toggle: -iconmaterial/weather-sunny                                                                                                                     -nameSwitch to dark mode                                                                                                                        --media"(prefers-color-scheme:dark)" -schemeslate                                                                                                                                        -primaryblack                                                                                                                                       -toggle: -iconmaterial/weather-night                                                                                                                     -nameSwitch to light mode                                                                                                                       -repo_urlhttps://github.com/juftin/browsr                                                                                                                     -repo_namebrowsr                                                                                                                                              -edit_uriblob/main/docs/                                                                                                                                      -site_authorJustin Flannery                                                                                                                                   -remote_branchgh-pages                                                                                                                                        -copyrightCopyright © 2023 Justin Flannery                                                                                                                    -extra: -generatorfalse                                                                                                                                           -markdown_extensions: --toc: -permalink"#" --pymdownx.snippets                                                                                                                                        --pymdownx.magiclink                                                                                                                                       --attr_list                                                                                                                                                --md_in_html                                                                                                                                               --pymdownx.highlight: +site_namebrowsr +nav:                                                                                                                                                           +    - index.md +    - Command Line Interface ⌨️cli.md +    - Contributing 🤝contributing.md +    - API Documentation 🤖reference/ +theme:                                                                                                                                                         +faviconhttps://raw.githubusercontent.com/juftin/browsr/main/docs/_static/browsr_no_label.png +logohttps://raw.githubusercontent.com/juftin/browsr/main/docs/_static/browsr_no_label.png +namematerial +features:                                                                                                                                                  +        - navigation.tracking +        - content.code.annotate +        - content.code.copy +palette:                                                                                                                                                   +        - media"(prefers-color-scheme: light)" +schemedefault +accentpurple +toggle:                                                                                                                                              +iconmaterial/weather-sunny +nameSwitch to dark mode +        - media"(prefers-color-scheme: dark)" +schemeslate +primaryblack +toggle:                                                                                                                                              +iconmaterial/weather-night +nameSwitch to light mode +repo_urlhttps://github.com/juftin/browsr +repo_namebrowsr +edit_uriblob/main/docs/ +site_authorJustin Flannery +remote_branchgh-pages +copyrightCopyright © 2023 Justin Flannery +extra:                                                                                                                                                         +generatorfalse +markdown_extensions:                                                                                                                                           +    - toc:                                                                                                                                                     +permalink"#" +    - pymdownx.snippets +    - pymdownx.magiclink +    - attr_list +    - md_in_html +    - pymdownx.highlight:                                                                                                                                      🗂️  GitHub  🗄️️  2KB  📂  browsr  💾  mkdocs.yaml - q Quit  f Files  t Theme  n Line Numbers  d Dark Mode  . Parent Directory  r Reload  c Copy Path  x Download File ^p palette + q Quit  f Files  t Theme  n Line Numbers  d Dark Mode  ? Shortcuts  x Download ^p palette diff --git a/tests/__snapshots__/test_screenshots/test_shortcuts_screenshot.svg b/tests/__snapshots__/test_screenshots/test_shortcuts_screenshot.svg new file mode 100644 index 0000000..e819eb6 --- /dev/null +++ b/tests/__snapshots__/test_screenshots/test_shortcuts_screenshot.svg @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + browsr + + + + + + + + + + browsr — github://juftin:browsr@v1.6.0/README.md [monokai] +📂 browsr +├── 📁 .github +├── 📁 browsr +├── 📁 docs +├── 📁 requirements +├── 📁 tests +├── 📄 .gitignore +├── 📄 .pre-commit-config.yaml +├── 📄 .releaserc.js +├── 📄 LICENSE +├── 📄 mkdocs.yaml +├── 📄 pyproject.toml +└── 📄 README.md + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +Keyboard Shortcuts + + Key      Description          + c        Copy Path            + shift+c  Copy Text            + x        Download File        + f        Files                + .        Parent Directory     + q        Quit                 + r        Reload               + ?        Show/Hide Shortcuts  + d        Toggle Dark Mode     + n        Toggle Line Numbers  + t        Toggle Theme         + w        Toggle Wrap          + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + + + + + + + + + + +🗂️  GitHub  🗄️️  3KB  📂  browsr  💾  README.md + q Quit  f Files  t Theme  n Line Numbers  d Dark Mode  ? Shortcuts  x Download ^p palette + + + diff --git a/tests/test_screenshots.py b/tests/test_screenshots.py index cc0b56a..e48ba02 100644 --- a/tests/test_screenshots.py +++ b/tests/test_screenshots.py @@ -30,7 +30,7 @@ def terminal_size() -> tuple[int, int]: @cassette -def test_github_screenshot( +def test_shortcuts_screenshot( snap_compare: Callable[..., bool], tmp_path: UPath, app_file: str, @@ -38,11 +38,13 @@ def test_github_screenshot( terminal_size: tuple[int, int], ) -> None: """ - Snapshot a release of this repo + Snapshot the shortcuts window """ app_path = tmp_path / "app.py" app_path.write_text(app_file.format(file_path=str(github_release_path))) - assert snap_compare(app=app_path, terminal_size=terminal_size) + assert snap_compare( + app=app_path, terminal_size=terminal_size, press=["question_mark"] + ) @cassette diff --git a/tests/test_shortcuts.py b/tests/test_shortcuts.py new file mode 100644 index 0000000..23cd37d --- /dev/null +++ b/tests/test_shortcuts.py @@ -0,0 +1,256 @@ +from typing import ClassVar +from unittest.mock import MagicMock + +import pytest +from textual.app import App +from textual.binding import Binding +from textual_universal_directorytree import UPath + +from browsr.base import TextualAppContext +from browsr.browsr import Browsr +from browsr.widgets.confirmation import ConfirmationPopUp +from browsr.widgets.shortcuts import ShortcutsPopUp + + +class MockApp(App): + BINDINGS: ClassVar[list[Binding]] = [Binding("q", "quit", "Quit")] + + +@pytest.mark.asyncio +async def test_shortcut_discovery(): + app = MockApp() + async with app.run_test(): + popup = ShortcutsPopUp() + await app.mount(popup) + # Note: the widget calls update_shortcuts on mount + table = popup.query_one("#shortcuts-table") + assert table.row_count > 0 + + +@pytest.mark.asyncio +async def test_shortcuts_overlay_is_mounted_in_code_browser(repo_dir: UPath): + app = Browsr(config_object=TextualAppContext(file_path=repo_dir)) + + async with app.run_test() as pilot: + await pilot.press("question_mark") + + shortcuts_window = app.code_browser_screen.code_browser.shortcuts_window + assert shortcuts_window.display is True + assert shortcuts_window.parent is app.code_browser_screen.code_browser + + +@pytest.mark.asyncio +async def test_shortcuts_popup_stays_within_viewport( + github_release_path: UPath, +): + app = Browsr(config_object=TextualAppContext(file_path=github_release_path)) + + async with app.run_test(size=(30, 15)) as pilot: + await pilot.press("question_mark") + await pilot.pause() + + popup = app.code_browser_screen.code_browser.shortcuts + shortcuts_window = app.code_browser_screen.code_browser.shortcuts_window + viewport = app.size + + assert shortcuts_window.display is True + assert shortcuts_window.region.x >= 0 + assert shortcuts_window.region.y >= 0 + assert shortcuts_window.region.right <= viewport.width + assert shortcuts_window.region.bottom <= viewport.height + assert popup.region.x >= 0 + assert popup.region.y >= 0 + + +@pytest.mark.asyncio +async def test_shortcuts_escape_restores_text_window(repo_dir: UPath): + app = Browsr(config_object=TextualAppContext(file_path=repo_dir)) + + async with app.run_test() as pilot: + code_browser = app.code_browser_screen.code_browser + code_browser.window_switcher.vim_scroll.display = False + code_browser.window_switcher.datatable_window.display = False + code_browser.window_switcher.text_window.display = True + + code_browser.toggle_shortcuts() + + assert code_browser.shortcuts_window.display is True + assert code_browser.window_switcher.text_window.display is False + + await pilot.press("escape") + + assert code_browser.shortcuts_window.display is False + assert code_browser.window_switcher.text_window.display is True + assert code_browser.window_switcher.vim_scroll.display is False + assert code_browser.window_switcher.datatable_window.display is False + + +@pytest.mark.asyncio +async def test_download_confirmation_restores_text_window( + repo_dir: UPath, monkeypatch: pytest.MonkeyPatch +): + app = Browsr(config_object=TextualAppContext(file_path=repo_dir)) + + async with app.run_test(): + code_browser = app.code_browser_screen.code_browser + selected_file_path = MagicMock() + selected_file_path.is_dir.return_value = False + selected_file_path.name = "example.txt" + selected_file_path.__str__.return_value = "github://owner:repo/example.txt" + + monkeypatch.setattr( + "browsr.widgets.code_browser.is_remote_path", lambda _: True + ) + monkeypatch.setattr( + code_browser, + "_get_download_file_name", + lambda: repo_dir / "downloaded-example.txt", + ) + + code_browser.selected_file_path = selected_file_path + code_browser.window_switcher.vim_scroll.display = False + code_browser.window_switcher.datatable_window.display = False + code_browser.window_switcher.text_window.display = True + + code_browser.download_file_workflow() + + assert code_browser.confirmation_window.display is True + assert code_browser.window_switcher.text_window.display is False + + code_browser.handle_confirmation_window_display_toggle( + ConfirmationPopUp.DisplayToggle() + ) + + assert code_browser.window_switcher.text_window.display is True + assert code_browser.window_switcher.vim_scroll.display is False + assert code_browser.window_switcher.datatable_window.display is False + + +@pytest.mark.asyncio +async def test_shortcuts_and_confirmation_can_switch_back_and_forth( + repo_dir: UPath, monkeypatch: pytest.MonkeyPatch +): + app = Browsr(config_object=TextualAppContext(file_path=repo_dir)) + + async with app.run_test(): + code_browser = app.code_browser_screen.code_browser + selected_file_path = MagicMock() + selected_file_path.is_dir.return_value = False + selected_file_path.name = "example.txt" + selected_file_path.__str__.return_value = "github://owner:repo/example.txt" + + monkeypatch.setattr( + "browsr.widgets.code_browser.is_remote_path", lambda _: True + ) + monkeypatch.setattr( + code_browser, + "_get_download_file_name", + lambda: repo_dir / "downloaded-example.txt", + ) + + code_browser.selected_file_path = selected_file_path + code_browser.window_switcher.vim_scroll.display = False + code_browser.window_switcher.datatable_window.display = False + code_browser.window_switcher.text_window.display = True + + code_browser.toggle_shortcuts() + assert code_browser.shortcuts_window.display is True + assert code_browser.confirmation_window.display is False + + code_browser.download_file_workflow() + assert code_browser.shortcuts_window.display is False + assert code_browser.confirmation_window.display is True + assert code_browser.window_switcher.text_window.display is False + + code_browser.toggle_shortcuts() + assert code_browser.shortcuts_window.display is True + assert code_browser.confirmation_window.display is False + assert code_browser.window_switcher.text_window.display is False + + code_browser.toggle_shortcuts() + assert code_browser.shortcuts_window.display is False + assert code_browser.confirmation_window.display is True + + code_browser.download_file_workflow() + assert code_browser.shortcuts_window.display is False + assert code_browser.confirmation_window.display is False + assert code_browser.window_switcher.text_window.display is True + + +@pytest.mark.asyncio +async def test_download_confirmation_popup_stays_within_viewport( + repo_dir: UPath, monkeypatch: pytest.MonkeyPatch +): + app = Browsr(config_object=TextualAppContext(file_path=repo_dir)) + + async with app.run_test(size=(30, 15)) as pilot: + code_browser = app.code_browser_screen.code_browser + selected_file_path = MagicMock() + selected_file_path.is_dir.return_value = False + selected_file_path.name = "example.txt" + selected_file_path.__str__.return_value = "github://owner:repo/example.txt" + + monkeypatch.setattr( + "browsr.widgets.code_browser.is_remote_path", lambda _: True + ) + monkeypatch.setattr( + code_browser, + "_get_download_file_name", + lambda: repo_dir / "downloaded-example.txt", + ) + + code_browser.selected_file_path = selected_file_path + code_browser.download_file_workflow() + await pilot.pause() + + popup = code_browser.confirmation + confirmation_window = code_browser.confirmation_window + viewport = app.size + + assert confirmation_window.display is True + assert confirmation_window.region.x >= 0 + assert confirmation_window.region.y >= 0 + assert confirmation_window.region.right <= viewport.width + assert confirmation_window.region.bottom <= viewport.height + assert popup.region.x >= 0 + assert popup.region.y >= 0 + + +@pytest.mark.asyncio +async def test_download_confirmation_escape_restores_text_window( + repo_dir: UPath, monkeypatch: pytest.MonkeyPatch +): + app = Browsr(config_object=TextualAppContext(file_path=repo_dir)) + + async with app.run_test() as pilot: + code_browser = app.code_browser_screen.code_browser + selected_file_path = MagicMock() + selected_file_path.is_dir.return_value = False + selected_file_path.name = "example.txt" + selected_file_path.__str__.return_value = "github://owner:repo/example.txt" + + monkeypatch.setattr( + "browsr.widgets.code_browser.is_remote_path", lambda _: True + ) + monkeypatch.setattr( + code_browser, + "_get_download_file_name", + lambda: repo_dir / "downloaded-example.txt", + ) + + code_browser.selected_file_path = selected_file_path + code_browser.window_switcher.vim_scroll.display = False + code_browser.window_switcher.datatable_window.display = False + code_browser.window_switcher.text_window.display = True + + code_browser.download_file_workflow() + + assert code_browser.confirmation_window.display is True + assert code_browser.window_switcher.text_window.display is False + + await pilot.press("escape") + + assert code_browser.confirmation_window.display is False + assert code_browser.window_switcher.text_window.display is True + assert code_browser.window_switcher.vim_scroll.display is False + assert code_browser.window_switcher.datatable_window.display is False diff --git a/tests/test_windows.py b/tests/test_windows.py new file mode 100644 index 0000000..f9f46cc --- /dev/null +++ b/tests/test_windows.py @@ -0,0 +1,179 @@ +from unittest.mock import MagicMock, patch + +import pytest +from textual.widgets import TextArea +from textual_universal_directorytree import UPath + +from browsr.base import TextualAppContext +from browsr.widgets.windows import FileToStringResult, TextWindow, WindowSwitcher + + +def test_text_window_inheritance(): + window = TextWindow() + assert isinstance(window, TextArea) + assert window.read_only is True + assert window.theme == window.default_theme + + +def test_text_window_theme_mapping(): + window = TextWindow() + # Test with dark mode + mock_app = MagicMock() + mock_app.dark = True + with pytest.MonkeyPatch.context() as mp: + mp.setattr("textual.widget.Widget.app", mock_app, raising=False) + window.apply_smart_theme("monokai") + assert window.theme == "monokai" + window.apply_smart_theme("invalid-theme") + assert window.theme == "vscode_dark" # Default + + # Test with light mode + mock_app.dark = False + window.apply_smart_theme("monokai") + assert window.theme == "github_light" + + +def test_text_window_language_detection(): + window = TextWindow() + window.detect_language("test.py") + assert window.language == "python" + window.detect_language("test.json") + assert window.language == "json" + window.detect_language("test.yml") + assert window.language == "yaml" + window.detect_language("test.sh") + assert window.language == "bash" + window.detect_language("uv.lock") + assert window.language == "toml" + window.detect_language("file.something.json") + assert window.language == "json" + + +def test_text_window_copy_text(): + window = TextWindow() + window.text = "Hello World" + mock_app = MagicMock() + + with pytest.MonkeyPatch.context() as mp: + mp.setattr("textual.widget.Widget.app", mock_app, raising=False) + with patch("pyperclip.copy") as mock_copy: + # Test no selection + window.copy_selected_text() + mock_copy.assert_not_called() + mock_app.notify.assert_called_with( + title="No Selection", + message="No text selected to copy", + severity="warning", + timeout=1, + ) + + # Test selection + window.selection = ((0, 0), (0, 5)) # "Hello" + window.copy_selected_text() + mock_copy.assert_called_with("Hello") + mock_app.notify.assert_called_with( + title="Copied", + message="Selected text copied to clipboard", + severity="information", + timeout=1, + ) + + +@pytest.mark.asyncio +async def test_window_switcher_routing(): + mock_app = MagicMock() + # Patch the property on the class for the duration of the test + with pytest.MonkeyPatch.context() as mp: + mp.setattr("textual.widget.Widget.app", mock_app, raising=False) + context = TextualAppContext() + switcher = WindowSwitcher(config_object=context) + + # Ensure default theme + assert switcher.text_window.theme == switcher.text_window.default_theme + + # Mock UPath for a JSON file + mock_json = MagicMock(spec=UPath) + mock_json.suffix = ".json" + mock_json.suffixes = [".json"] + mock_json.read_text.return_value = '{"key": "value"}' + mock_json.__str__.return_value = "test.json" + mock_json.stat.return_value.st_size = 100 + + # Mock UPath for a Python file + mock_py = MagicMock(spec=UPath) + mock_py.suffix = ".py" + mock_py.suffixes = [".py"] + mock_py.read_text.return_value = "print('hello')" + mock_py.__str__.return_value = "test.py" + mock_py.stat.return_value.st_size = 100 + + # We need to patch various things to avoid NoActiveAppError and other issues + switcher.static_window.file_to_json = MagicMock( + return_value='{\n "key": "value"\n}' + ) + switcher.static_window.file_to_string = MagicMock( + return_value=FileToStringResult( + result="print('hello')", error_occurred=False + ) + ) + switcher.text_window.scroll_home = MagicMock() + switcher.vim_scroll.scroll_home = MagicMock() + + # Render JSON + switcher.render_file(mock_json) + # After rendering JSON, the theme should remain at the default theme + assert switcher.text_window.theme == switcher.text_window.default_theme + + # Manually change theme + switcher.text_window.theme = "monokai" + + # Render Python - should PERSIST theme + switcher.render_file(mock_py) + assert switcher.text_window.theme == "monokai" + + +def test_window_switcher_linenos_sync(): + mock_app = MagicMock() + with pytest.MonkeyPatch.context() as mp: + mp.setattr("textual.widget.Widget.app", mock_app, raising=False) + context = TextualAppContext() + switcher = WindowSwitcher(config_object=context) + switcher.linenos = True + assert switcher.static_window.linenos is True + assert switcher.text_window.linenos is True + switcher.linenos = False + assert switcher.static_window.linenos is False + assert switcher.text_window.linenos is False + + +def test_window_switcher_theme_sync(): + mock_app = MagicMock() + mock_app.dark = True + mock_app.sub_title = "" + + with pytest.MonkeyPatch.context() as mp: + mp.setattr("textual.widget.Widget.app", mock_app, raising=False) + context = TextualAppContext() + switcher = WindowSwitcher(config_object=context) + switcher.rendered_file = "test.py" + + # Test StaticWindow theme cycling + switcher.switch_window(switcher.static_window) + initial_theme = switcher.theme + switcher.next_theme() + assert switcher.theme != initial_theme + assert switcher.static_window.theme == switcher.theme + + # Test TextWindow theme cycling + switcher.switch_window(switcher.text_window) + initial_text_theme = switcher.text_window.theme + switcher.next_theme() + assert switcher.text_window.theme != initial_text_theme + # Global switcher theme should NOT have changed + assert switcher.theme == switcher.static_window.theme + + mock_app.dark = False + # Trigger the watch_dark + switcher.watch_dark(False) + # TextWindow forces light theme when app is light + assert switcher.text_window.theme == "github_light" diff --git a/uv.lock b/uv.lock index 5bd2bbf..42b3206 100644 --- a/uv.lock +++ b/uv.lock @@ -314,7 +314,7 @@ dependencies = [ { name = "rich" }, { name = "rich-click" }, { name = "rich-pixels" }, - { name = "textual" }, + { name = "textual", extra = ["syntax"] }, { name = "textual-universal-directorytree" }, { name = "universal-pathlib" }, ] @@ -377,7 +377,7 @@ requires-dist = [ { name = "rich", specifier = ">=14,<15" }, { name = "rich-click", specifier = "~=1.9.7" }, { name = "rich-pixels", specifier = "~=3.0.1" }, - { name = "textual", specifier = ">=8,<9" }, + { name = "textual", extras = ["syntax"], specifier = ">=8,<9" }, { name = "textual-universal-directorytree", specifier = "~=1.6.0" }, { name = "textual-universal-directorytree", extras = ["remote"], marker = "extra == 'all'", specifier = "~=1.6.0" }, { name = "textual-universal-directorytree", extras = ["remote"], marker = "extra == 'remote'", specifier = "~=1.6.0" }, @@ -2626,6 +2626,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/09/c6f000c2e3702036e593803319af02feee58a662528d0d5728a37e1cf81b/textual-8.2.1-py3-none-any.whl", hash = "sha256:746cbf947a8ca875afc09779ef38cadbc7b9f15ac886a5090f7099fef5ade990", size = 723871, upload-time = "2026-03-29T03:57:34.334Z" }, ] +[package.optional-dependencies] +syntax = [ + { name = "tree-sitter" }, + { name = "tree-sitter-bash" }, + { name = "tree-sitter-css" }, + { name = "tree-sitter-go" }, + { name = "tree-sitter-html" }, + { name = "tree-sitter-java" }, + { name = "tree-sitter-javascript" }, + { name = "tree-sitter-json" }, + { name = "tree-sitter-markdown" }, + { name = "tree-sitter-python" }, + { name = "tree-sitter-regex" }, + { name = "tree-sitter-rust" }, + { name = "tree-sitter-sql" }, + { name = "tree-sitter-toml" }, + { name = "tree-sitter-xml" }, + { name = "tree-sitter-yaml" }, +] + [[package]] name = "textual-dev" version = "1.4.0" @@ -2704,6 +2724,284 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] +[[package]] +name = "tree-sitter" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/7c/0350cfc47faadc0d3cf7d8237a4e34032b3014ddf4a12ded9933e1648b55/tree-sitter-0.25.2.tar.gz", hash = "sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20", size = 177961, upload-time = "2025-09-25T17:37:59.751Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/d4/f7ffb855cb039b7568aba4911fbe42e4c39c0e4398387c8e0d8251489992/tree_sitter-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72a510931c3c25f134aac2daf4eb4feca99ffe37a35896d7150e50ac3eee06c7", size = 146749, upload-time = "2025-09-25T17:37:16.475Z" }, + { url = "https://files.pythonhosted.org/packages/9a/58/f8a107f9f89700c0ab2930f1315e63bdedccbb5fd1b10fcbc5ebadd54ac8/tree_sitter-0.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:44488e0e78146f87baaa009736886516779253d6d6bac3ef636ede72bc6a8234", size = 137766, upload-time = "2025-09-25T17:37:18.138Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/357158d39f01699faea466e8fd5a849f5a30252c68414bddc20357a9ac79/tree_sitter-0.25.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2f8e7d6b2f8489d4a9885e3adcaef4bc5ff0a275acd990f120e29c4ab3395c5", size = 599809, upload-time = "2025-09-25T17:37:19.169Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a4/68ae301626f2393a62119481cb660eb93504a524fc741a6f1528a4568cf6/tree_sitter-0.25.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b570690f87f1da424cd690e51cc56728d21d63f4abd4b326d382a30353acc7", size = 627676, upload-time = "2025-09-25T17:37:20.715Z" }, + { url = "https://files.pythonhosted.org/packages/69/fe/4c1bef37db5ca8b17ca0b3070f2dff509468a50b3af18f17665adcab42b9/tree_sitter-0.25.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a0ec41b895da717bc218a42a3a7a0bfcfe9a213d7afaa4255353901e0e21f696", size = 624281, upload-time = "2025-09-25T17:37:21.823Z" }, + { url = "https://files.pythonhosted.org/packages/d4/30/3283cb7fa251cae2a0bf8661658021a789810db3ab1b0569482d4a3671fd/tree_sitter-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:7712335855b2307a21ae86efe949c76be36c6068d76df34faa27ce9ee40ff444", size = 127295, upload-time = "2025-09-25T17:37:22.977Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/ceb05e6de281aebe82b68662890619580d4ffe09283ebd2ceabcf5df7b4a/tree_sitter-0.25.2-cp310-cp310-win_arm64.whl", hash = "sha256:a925364eb7fbb9cdce55a9868f7525a1905af512a559303bd54ef468fd88cb37", size = 113991, upload-time = "2025-09-25T17:37:23.854Z" }, + { url = "https://files.pythonhosted.org/packages/7c/22/88a1e00b906d26fa8a075dd19c6c3116997cb884bf1b3c023deb065a344d/tree_sitter-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ca72d841215b6573ed0655b3a5cd1133f9b69a6fa561aecad40dca9029d75b", size = 146752, upload-time = "2025-09-25T17:37:24.775Z" }, + { url = "https://files.pythonhosted.org/packages/57/1c/22cc14f3910017b7a76d7358df5cd315a84fe0c7f6f7b443b49db2e2790d/tree_sitter-0.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc0351cfe5022cec5a77645f647f92a936b38850346ed3f6d6babfbeeeca4d26", size = 137765, upload-time = "2025-09-25T17:37:26.103Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0c/d0de46ded7d5b34631e0f630d9866dab22d3183195bf0f3b81de406d6622/tree_sitter-0.25.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1799609636c0193e16c38f366bda5af15b1ce476df79ddaae7dd274df9e44266", size = 604643, upload-time = "2025-09-25T17:37:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/34/38/b735a58c1c2f60a168a678ca27b4c1a9df725d0bf2d1a8a1c571c033111e/tree_sitter-0.25.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e65ae456ad0d210ee71a89ee112ac7e72e6c2e5aac1b95846ecc7afa68a194c", size = 632229, upload-time = "2025-09-25T17:37:28.463Z" }, + { url = "https://files.pythonhosted.org/packages/32/f6/cda1e1e6cbff5e28d8433578e2556d7ba0b0209d95a796128155b97e7693/tree_sitter-0.25.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:49ee3c348caa459244ec437ccc7ff3831f35977d143f65311572b8ba0a5f265f", size = 629861, upload-time = "2025-09-25T17:37:29.593Z" }, + { url = "https://files.pythonhosted.org/packages/f9/19/427e5943b276a0dd74c2a1f1d7a7393443f13d1ee47dedb3f8127903c080/tree_sitter-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:56ac6602c7d09c2c507c55e58dc7026b8988e0475bd0002f8a386cce5e8e8adc", size = 127304, upload-time = "2025-09-25T17:37:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/eef856dc15f784d85d1397a17f3ee0f82df7778efce9e1961203abfe376a/tree_sitter-0.25.2-cp311-cp311-win_arm64.whl", hash = "sha256:b3d11a3a3ac89bb8a2543d75597f905a9926f9c806f40fcca8242922d1cc6ad5", size = 113990, upload-time = "2025-09-25T17:37:31.852Z" }, + { url = "https://files.pythonhosted.org/packages/3c/9e/20c2a00a862f1c2897a436b17edb774e831b22218083b459d0d081c9db33/tree_sitter-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ddabfff809ffc983fc9963455ba1cecc90295803e06e140a4c83e94c1fa3d960", size = 146941, upload-time = "2025-09-25T17:37:34.813Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/8512e2062e652a1016e840ce36ba1cc33258b0dcc4e500d8089b4054afec/tree_sitter-0.25.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c0c0ab5f94938a23fe81928a21cc0fac44143133ccc4eb7eeb1b92f84748331c", size = 137699, upload-time = "2025-09-25T17:37:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/47/8a/d48c0414db19307b0fb3bb10d76a3a0cbe275bb293f145ee7fba2abd668e/tree_sitter-0.25.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd12d80d91d4114ca097626eb82714618dcdfacd6a5e0955216c6485c350ef99", size = 607125, upload-time = "2025-09-25T17:37:37.725Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/b95f545e9fc5001b8a78636ef942a4e4e536580caa6a99e73dd0a02e87aa/tree_sitter-0.25.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b43a9e4c89d4d0839de27cd4d6902d33396de700e9ff4c5ab7631f277a85ead9", size = 635418, upload-time = "2025-09-25T17:37:38.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/4d/b734bde3fb6f3513a010fa91f1f2875442cdc0382d6a949005cd84563d8f/tree_sitter-0.25.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbb1706407c0e451c4f8cc016fec27d72d4b211fdd3173320b1ada7a6c74c3ac", size = 631250, upload-time = "2025-09-25T17:37:40.039Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/5f654994f36d10c64d50a192239599fcae46677491c8dd53e7579c35a3e3/tree_sitter-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:6d0302550bbe4620a5dc7649517c4409d74ef18558276ce758419cf09e578897", size = 127156, upload-time = "2025-09-25T17:37:41.132Z" }, + { url = "https://files.pythonhosted.org/packages/67/23/148c468d410efcf0a9535272d81c258d840c27b34781d625f1f627e2e27d/tree_sitter-0.25.2-cp312-cp312-win_arm64.whl", hash = "sha256:0c8b6682cac77e37cfe5cf7ec388844957f48b7bd8d6321d0ca2d852994e10d5", size = 113984, upload-time = "2025-09-25T17:37:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/8c/67/67492014ce32729b63d7ef318a19f9cfedd855d677de5773476caf771e96/tree_sitter-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0628671f0de69bb279558ef6b640bcfc97864fe0026d840f872728a86cd6b6cd", size = 146926, upload-time = "2025-09-25T17:37:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/a278b15e6b263e86c5e301c82a60923fa7c59d44f78d7a110a89a413e640/tree_sitter-0.25.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f5ddcd3e291a749b62521f71fc953f66f5fd9743973fd6dd962b092773569601", size = 137712, upload-time = "2025-09-25T17:37:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/423bba15d2bf6473ba67846ba5244b988cd97a4b1ea2b146822162256794/tree_sitter-0.25.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd88fbb0f6c3a0f28f0a68d72df88e9755cf5215bae146f5a1bdc8362b772053", size = 607873, upload-time = "2025-09-25T17:37:45.477Z" }, + { url = "https://files.pythonhosted.org/packages/ed/4c/b430d2cb43f8badfb3a3fa9d6cd7c8247698187b5674008c9d67b2a90c8e/tree_sitter-0.25.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b878e296e63661c8e124177cc3084b041ba3f5936b43076d57c487822426f614", size = 636313, upload-time = "2025-09-25T17:37:46.68Z" }, + { url = "https://files.pythonhosted.org/packages/9d/27/5f97098dbba807331d666a0997662e82d066e84b17d92efab575d283822f/tree_sitter-0.25.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d77605e0d353ba3fe5627e5490f0fbfe44141bafa4478d88ef7954a61a848dae", size = 631370, upload-time = "2025-09-25T17:37:47.993Z" }, + { url = "https://files.pythonhosted.org/packages/d4/3c/87caaed663fabc35e18dc704cd0e9800a0ee2f22bd18b9cbe7c10799895d/tree_sitter-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:463c032bd02052d934daa5f45d183e0521ceb783c2548501cf034b0beba92c9b", size = 127157, upload-time = "2025-09-25T17:37:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/d5/23/f8467b408b7988aff4ea40946a4bd1a2c1a73d17156a9d039bbaff1e2ceb/tree_sitter-0.25.2-cp313-cp313-win_arm64.whl", hash = "sha256:b3f63a1796886249bd22c559a5944d64d05d43f2be72961624278eff0dcc5cb8", size = 113975, upload-time = "2025-09-25T17:37:49.922Z" }, + { url = "https://files.pythonhosted.org/packages/07/e3/d9526ba71dfbbe4eba5e51d89432b4b333a49a1e70712aa5590cd22fc74f/tree_sitter-0.25.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65d3c931013ea798b502782acab986bbf47ba2c452610ab0776cf4a8ef150fc0", size = 146776, upload-time = "2025-09-25T17:37:50.898Z" }, + { url = "https://files.pythonhosted.org/packages/42/97/4bd4ad97f85a23011dd8a535534bb1035c4e0bac1234d58f438e15cff51f/tree_sitter-0.25.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bda059af9d621918efb813b22fb06b3fe00c3e94079c6143fcb2c565eb44cb87", size = 137732, upload-time = "2025-09-25T17:37:51.877Z" }, + { url = "https://files.pythonhosted.org/packages/b6/19/1e968aa0b1b567988ed522f836498a6a9529a74aab15f09dd9ac1e41f505/tree_sitter-0.25.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eac4e8e4c7060c75f395feec46421eb61212cb73998dbe004b7384724f3682ab", size = 609456, upload-time = "2025-09-25T17:37:52.925Z" }, + { url = "https://files.pythonhosted.org/packages/48/b6/cf08f4f20f4c9094006ef8828555484e842fc468827ad6e56011ab668dbd/tree_sitter-0.25.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:260586381b23be33b6191a07cea3d44ecbd6c01aa4c6b027a0439145fcbc3358", size = 636772, upload-time = "2025-09-25T17:37:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/57/e2/d42d55bf56360987c32bc7b16adb06744e425670b823fb8a5786a1cea991/tree_sitter-0.25.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d2ee1acbacebe50ba0f85fff1bc05e65d877958f00880f49f9b2af38dce1af0", size = 631522, upload-time = "2025-09-25T17:37:55.833Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/af9604ebe275a9345d88c3ace0cf2a1341aa3f8ef49dd9fc11662132df8a/tree_sitter-0.25.2-cp314-cp314-win_amd64.whl", hash = "sha256:4973b718fcadfb04e59e746abfbb0288694159c6aeecd2add59320c03368c721", size = 130864, upload-time = "2025-09-25T17:37:57.453Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6e/e64621037357acb83d912276ffd30a859ef117f9c680f2e3cb955f47c680/tree_sitter-0.25.2-cp314-cp314-win_arm64.whl", hash = "sha256:b8d4429954a3beb3e844e2872610d2a4800ba4eb42bb1990c6a4b1949b18459f", size = 117470, upload-time = "2025-09-25T17:37:58.431Z" }, +] + +[[package]] +name = "tree-sitter-bash" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/0e/f0108be910f1eef6499eabce517e79fe3b12057280ed398da67ce2426cba/tree_sitter_bash-0.25.1.tar.gz", hash = "sha256:bfc0bdaa77bc1e86e3c6652e5a6e140c40c0a16b84185c2b63ad7cd809b88f14", size = 419703, upload-time = "2025-12-02T17:01:08.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/8e/37e7364d9c9c58da89e05c510671d8c45818afd7b31c6939ab72f8dc6c04/tree_sitter_bash-0.25.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:0e6235f59e366d220dde7d830196bed597d01e853e44d8ccd1a82c5dd2500acf", size = 194160, upload-time = "2025-12-02T17:00:59.047Z" }, + { url = "https://files.pythonhosted.org/packages/23/bb/2d2cfbb1f89aaeb1ec892624f069d92d058d06bb66f16b9ec9fb5873ab60/tree_sitter_bash-0.25.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f4a34a6504c7c5b2a9b8c5c4065531dea19ca2c35026e706cf2eeeebe2c92512", size = 202659, upload-time = "2025-12-02T17:01:00.275Z" }, + { url = "https://files.pythonhosted.org/packages/25/f0/1bb25519be27460255d3899db677313cfa1e6306988fbf456a3d7e211bbb/tree_sitter_bash-0.25.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e76c4cfb20b076552406782b7f8c2a3946835993df0a44df006de54b7030c7dc", size = 230596, upload-time = "2025-12-02T17:01:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/d7/22/9f70bc3d3b942ab9fc0f89c1dc9e087519a3a94f64ae6b7377aae3a7a0f0/tree_sitter_bash-0.25.1-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f484c4bb8796cde7a87ca351e6116f09653edac0eb3c6d238566359dd28b117", size = 231981, upload-time = "2025-12-02T17:01:02.859Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c3/f1540e42cd41b323c6821e45e52e1aed6ed386209aad52db996f05703963/tree_sitter_bash-0.25.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5e76af6df46d958c7f5b6d5884c9743218e3902a00ccb493ec92728b1084430b", size = 228364, upload-time = "2025-12-02T17:01:03.997Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a0/c3050a6277dfcac8c480f514dc4fe49f3f65f0eac68b4702cbaca2584e85/tree_sitter_bash-0.25.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a3332d71c7b7d5f78259b19d02d0ea111fcb82b72712ee4a93aaa5b226d3f0a8", size = 230074, upload-time = "2025-12-02T17:01:05.05Z" }, + { url = "https://files.pythonhosted.org/packages/71/0f/203fe6b27211387f4b9ba8c4a321567ca4ded2624dae6ccdbd2b6e940e17/tree_sitter_bash-0.25.1-cp310-abi3-win_amd64.whl", hash = "sha256:52a6802d9218f86278aa3e8b459c3abdad67eed0fde1f9f13aca5b6c634217a6", size = 195574, upload-time = "2025-12-02T17:01:06.412Z" }, + { url = "https://files.pythonhosted.org/packages/47/75/4ca1a9fabd8fb5aea78cea70f7837ce4dbf2afae115f62051e5fa99cba1c/tree_sitter_bash-0.25.1-cp310-abi3-win_arm64.whl", hash = "sha256:59115057ec2bae319e8082ff29559861045002964c3431ccb0fc92aa4bc9bccb", size = 191196, upload-time = "2025-12-02T17:01:07.486Z" }, +] + +[[package]] +name = "tree-sitter-css" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/37/7d60171240d4c5ba330f05b725dfb5e5fd5b7cbe0aa98ef9e77f77f868f5/tree_sitter_css-0.25.0.tar.gz", hash = "sha256:2fc996bf05b04e06061e88ee4c60837783dc4e62a695205acbc262ee30454138", size = 43232, upload-time = "2025-09-28T11:37:13.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/a9/69e556f15ca774638bd79005369213dfbd41995bf032ce81cf3ffe086b8a/tree_sitter_css-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ddce6f84eeb0bb2877b4587b07bffb0753040c44d811ed9ab2af978c313beda8", size = 29933, upload-time = "2025-09-28T11:37:07.703Z" }, + { url = "https://files.pythonhosted.org/packages/4d/28/ebcbcbba812d3e407f2f393747330eb8843e0c69d159024e33460b622aab/tree_sitter_css-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:5a2a9c875037ef5f9da57697fb8075086476d42a49d25a88dcca60dfc09bd092", size = 31097, upload-time = "2025-09-28T11:37:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/86/a2/6f9658c723f3a857367c198bd4f50d854aa9468783b418407492c9634a44/tree_sitter_css-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4f5e1135bfd01bce24e2fc7bca1381f52bdd6c6282ee28f7aa77185340bcd135", size = 41713, upload-time = "2025-09-28T11:37:09.101Z" }, + { url = "https://files.pythonhosted.org/packages/85/bb/f74eea6839cb1ff6b5851c6ed33b18e65309eb347bbbe027c93e70e6c691/tree_sitter_css-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b6d0084536828c733a66524a43c9df89f335971d5b1b973e9d1c42ba9dd426b", size = 42312, upload-time = "2025-09-28T11:37:09.757Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fd/031ef1a5938441c98342faf70bb30998683b2130d4b55c282d76b2083f4a/tree_sitter_css-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8a83825daf538656cb88f4f7a0dd9963e3f204e83e7f8d92131f17e5bd712a77", size = 41585, upload-time = "2025-09-28T11:37:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/96/74/9f269bb3644a0511c1c263135e32d38a7f2af39cbba24d59a1633a5ebbc1/tree_sitter_css-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b486c097d250a598fba5f1f46f62697c7f4428252c8bdaad696a907ee913421d", size = 41490, upload-time = "2025-09-28T11:37:11.134Z" }, + { url = "https://files.pythonhosted.org/packages/04/9f/d4f1d3164b692b97266274dad6437586e0614f75080b7795fc7bfa5bf8ff/tree_sitter_css-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:fe319e4ad1b8327afbd9758b3ae22b09226d6c28dc9b022bcadabdaf6ea3716c", size = 32416, upload-time = "2025-09-28T11:37:11.808Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/fa62d70cb324788bcced741b5e19864ccf4c51ca31766a9f56a6b46a5cf6/tree_sitter_css-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:4fc2c82645cd593f1c695b4d6b678d71e633212ca030f26dedee4f92434bfe21", size = 31057, upload-time = "2025-09-28T11:37:12.734Z" }, +] + +[[package]] +name = "tree-sitter-go" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/05/727308adbbc79bcb1c92fc0ea10556a735f9d0f0a5435a18f59d40f7fd77/tree_sitter_go-0.25.0.tar.gz", hash = "sha256:a7466e9b8d94dda94cae8d91629f26edb2d26166fd454d4831c3bf6dfa2e8d68", size = 93890, upload-time = "2025-08-29T06:20:25.044Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/aa/0984707acc2b9bb461fe4a41e7e0fc5b2b1e245c32820f0c83b3c602957c/tree_sitter_go-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b852993063a3429a443e7bd0aa376dd7dd329d595819fabf56ac4cf9d7257b54", size = 47117, upload-time = "2025-08-29T06:20:14.286Z" }, + { url = "https://files.pythonhosted.org/packages/32/16/dd4cb124b35e99239ab3624225da07d4cb8da4d8564ed81d03fcb3a6ba9f/tree_sitter_go-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:503b81a2b4c31e302869a1de3a352ad0912ccab3df9ac9950197b0a9ceeabd8f", size = 48674, upload-time = "2025-08-29T06:20:17.557Z" }, + { url = "https://files.pythonhosted.org/packages/86/fb/b30d63a08044115d8b8bd196c6c2ab4325fb8db5757249a4ef0563966e2e/tree_sitter_go-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04b3b3cb4aff18e74e28d49b716c6f24cb71ddfdd66768987e26e4d0fa812f74", size = 66418, upload-time = "2025-08-29T06:20:18.345Z" }, + { url = "https://files.pythonhosted.org/packages/26/21/d3d88a30ad007419b2c97b3baeeef7431407faf9f686195b6f1cad0aedf9/tree_sitter_go-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:148255aca2f54b90d48c48a9dbb4c7faad6cad310a980b2c5a5a9822057ed145", size = 72006, upload-time = "2025-08-29T06:20:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d0/0dd6442353ced8a88bbda9e546f4ea29e381b59b5a40b122e5abb586bb6c/tree_sitter_go-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4d338116cdf8a6c6ff990d2441929b41323ef17c710407abe0993c13417d6aad", size = 70603, upload-time = "2025-08-29T06:20:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/01/e2/ee5e09f63504fc286539535d374d2eaa0e7d489b80f8f744bb3962aff22a/tree_sitter_go-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5608e089d2a29fa8d2b327abeb2ad1cdb8e223c440a6b0ceab0d3fa80bdeebae", size = 66088, upload-time = "2025-08-29T06:20:22.336Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b6/d9142583374720e79aca9ccb394b3795149a54c012e1dfd80738df2d984e/tree_sitter_go-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:30d4ada57a223dfc2c32d942f44d284d40f3d1215ddcf108f96807fd36d53022", size = 48152, upload-time = "2025-08-29T06:20:23.089Z" }, + { url = "https://files.pythonhosted.org/packages/9e/00/9a2638e7339236f5b01622952a4d71c1474dd3783d1982a89555fc1f03b1/tree_sitter_go-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:d5d62362059bf79997340773d47cc7e7e002883b527a05cca829c46e40b70ded", size = 46752, upload-time = "2025-08-29T06:20:24.235Z" }, +] + +[[package]] +name = "tree-sitter-html" +version = "0.23.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/06/ad1c53c79da15bef85939aa022d72301e12a9773e9bb9a5e6a6f65b7753a/tree_sitter_html-0.23.2.tar.gz", hash = "sha256:bc9922defe23144d9146bc1509fcd00d361bf6b3303f9effee6532c6a0296961", size = 13977, upload-time = "2024-11-11T05:58:07.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/27/b846852b567601c4df765bcb4636085a3260e9f03ae21e0ef2e7c7f957fc/tree_sitter_html-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e1641d5edf5568a246c6c47b947ed524b5bf944664e6473b21d4ae568e28ee9", size = 14787, upload-time = "2024-11-11T05:57:58.684Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/827c315deb156bb8cac541da800c4bd62878f50a28b7498fbb722bddd225/tree_sitter_html-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:3d0a83dd6cd1c7d4bcf6287b5145c92140f0194f8516f329ae8b9e952fbfa8ff", size = 15232, upload-time = "2024-11-11T05:58:00.139Z" }, + { url = "https://files.pythonhosted.org/packages/91/cb/2028fe446d0e18edf3737d91edcb6430f2c97f2296b8cd760702dfa13d90/tree_sitter_html-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b3775732fffc0abd275a419ef018fd4c1ad4044b2a2e422f3378d93c30eded", size = 39109, upload-time = "2024-11-11T05:58:00.986Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/b24f5e66be51447cf7e9bcce3d9440a6b4f17021da85779a51566646a7c7/tree_sitter_html-0.23.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bdaa7ac5030d416aea0c512d4810ef847bbbd62d61e3d213f370b64ce147293", size = 39630, upload-time = "2024-11-11T05:58:02.424Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d5/31b46cb362ad9679af21ff8b75d846fb7522ecf949beea4fddc86e97815d/tree_sitter_html-0.23.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d2e9631b66041a4fd792d7f79a0c4128adb3bfc71f3dcb7e1a3eab5dbee77d67", size = 37440, upload-time = "2024-11-11T05:58:03.819Z" }, + { url = "https://files.pythonhosted.org/packages/28/30/03910b7c037105f33166439f0518dd0aa4f1b7ef8c9d7367c6e9cc6b5681/tree_sitter_html-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:85095f49f9e57f0ac9087a3e830783352c8447fdda55b1c1139aa47e5eaa0e21", size = 17765, upload-time = "2024-11-11T05:58:05.163Z" }, + { url = "https://files.pythonhosted.org/packages/20/32/63761055b03c69202a0e67b6e9a5cb3578da23aeefb62ee3e7ec2c1b0ff2/tree_sitter_html-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:0f65ed9e877144d0f04ade5644e5b0e88bf98a9e60bce65235c99905623e2f1a", size = 15576, upload-time = "2024-11-11T05:58:06.577Z" }, +] + +[[package]] +name = "tree-sitter-java" +version = "0.23.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/dc/eb9c8f96304e5d8ae1663126d89967a622a80937ad2909903569ccb7ec8f/tree_sitter_java-0.23.5.tar.gz", hash = "sha256:f5cd57b8f1270a7f0438878750d02ccc79421d45cca65ff284f1527e9ef02e38", size = 138121, upload-time = "2024-12-21T18:24:26.936Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/21/b3399780b440e1567a11d384d0ebb1aea9b642d0d98becf30fa55c0e3a3b/tree_sitter_java-0.23.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:355ce0308672d6f7013ec913dee4a0613666f4cda9044a7824240d17f38209df", size = 58926, upload-time = "2024-12-21T18:24:12.53Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/6406b444e2a93bc72a04e802f4107e9ecf04b8de4a5528830726d210599c/tree_sitter_java-0.23.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:24acd59c4720dedad80d548fe4237e43ef2b7a4e94c8549b0ca6e4c4d7bf6e69", size = 62288, upload-time = "2024-12-21T18:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6c/74b1c150d4f69c291ab0b78d5dd1b59712559bbe7e7daf6d8466d483463f/tree_sitter_java-0.23.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9401e7271f0b333df39fc8a8336a0caf1b891d9a2b89ddee99fae66b794fc5b7", size = 85533, upload-time = "2024-12-21T18:24:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/29/09/e0d08f5c212062fd046db35c1015a2621c2631bc8b4aae5740d7adb276ad/tree_sitter_java-0.23.5-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:370b204b9500b847f6d0c5ad584045831cee69e9a3e4d878535d39e4a7e4c4f1", size = 84033, upload-time = "2024-12-21T18:24:18.758Z" }, + { url = "https://files.pythonhosted.org/packages/43/56/7d06b23ddd09bde816a131aa504ee11a1bbe87c6b62ab9b2ed23849a3382/tree_sitter_java-0.23.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:aae84449e330363b55b14a2af0585e4e0dae75eb64ea509b7e5b0e1de536846a", size = 82564, upload-time = "2024-12-21T18:24:20.493Z" }, + { url = "https://files.pythonhosted.org/packages/da/d6/0528c7e1e88a18221dbd8ccee3825bf274b1fa300f745fd74eb343878043/tree_sitter_java-0.23.5-cp39-abi3-win_amd64.whl", hash = "sha256:1ee45e790f8d31d416bc84a09dac2e2c6bc343e89b8a2e1d550513498eedfde7", size = 60650, upload-time = "2024-12-21T18:24:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/72/57/5bab54d23179350356515526fff3cc0f3ac23bfbc1a1d518a15978d4880e/tree_sitter_java-0.23.5-cp39-abi3-win_arm64.whl", hash = "sha256:402efe136104c5603b429dc26c7e75ae14faaca54cfd319ecc41c8f2534750f4", size = 59059, upload-time = "2024-12-21T18:24:24.934Z" }, +] + +[[package]] +name = "tree-sitter-javascript" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/e0/e63103c72a9d3dfd89a31e02e660263ad84b7438e5f44ee82e443e65bbde/tree_sitter_javascript-0.25.0.tar.gz", hash = "sha256:329b5414874f0588a98f1c291f1b28138286617aa907746ffe55adfdcf963f38", size = 132338, upload-time = "2025-09-01T07:13:44.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/df/5106ac250cd03661ebc3cc75da6b3d9f6800a3606393a0122eca58038104/tree_sitter_javascript-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b70f887fb269d6e58c349d683f59fa647140c410cfe2bee44a883b20ec92e3dc", size = 64052, upload-time = "2025-09-01T07:13:36.865Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/6b4b2bc90d8ab3955856ce852cc9d1e82c81d7ab9646385f0e75ffd5b5d3/tree_sitter_javascript-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:8264a996b8845cfce06965152a013b5d9cbb7d199bc3503e12b5682e62bb1de1", size = 66440, upload-time = "2025-09-01T07:13:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c4/7da74ecdcd8a398f88bd003a87c65403b5fe0e958cdd43fbd5fd4a398fcf/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9dc04ba91fc8583344e57c1f1ed5b2c97ecaaf47480011b92fbeab8dda96db75", size = 99728, upload-time = "2025-09-01T07:13:38.755Z" }, + { url = "https://files.pythonhosted.org/packages/96/c8/97da3af4796495e46421e9344738addb3602fa6426ea695be3fcbadbee37/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:199d09985190852e0912da2b8d26c932159be314bc04952cf917ed0e4c633e6b", size = 106072, upload-time = "2025-09-01T07:13:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/13/be/c964e8130be08cc9bd6627d845f0e4460945b158429d39510953bbcb8fcc/tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dfcf789064c58dc13c0a4edb550acacfc6f0f280577f1e7a00de3e89fc7f8ddc", size = 104388, upload-time = "2025-09-01T07:13:40.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/89/9b773dee0f8961d1bb8d7baf0a204ab587618df19897c1ef260916f318ec/tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b852d3aee8a36186dbcc32c798b11b4869f9b5041743b63b65c2ef793db7a54", size = 98377, upload-time = "2025-09-01T07:13:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/d90cb1790f8cec9b4878d278ad9faf7c8f893189ce0f855304fd704fc274/tree_sitter_javascript-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:e5ed840f5bd4a3f0272e441d19429b26eedc257abe5574c8546da6b556865e3c", size = 62975, upload-time = "2025-09-01T07:13:42.828Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1f/f9eba1038b7d4394410f3c0a6ec2122b590cd7acb03f196e52fa57ebbe72/tree_sitter_javascript-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:622a69d677aa7f6ee2931d8c77c981a33f0ebb6d275aa9d43d3397c879a9bb0b", size = 61668, upload-time = "2025-09-01T07:13:43.803Z" }, +] + +[[package]] +name = "tree-sitter-json" +version = "0.24.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/29/e92df6dca3a6b2ab1c179978be398059817e1173fbacd47e832aaff3446b/tree_sitter_json-0.24.8.tar.gz", hash = "sha256:ca8486e52e2d261819311d35cf98656123d59008c3b7dcf91e61d2c0c6f3120e", size = 8155, upload-time = "2024-11-11T06:05:00.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/41/84866232980fb3cf0cff46f5af2dbb9bfa3324b32614c6a9af3d08926b72/tree_sitter_json-0.24.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:59ac06c6db1877d0e2076bce54a5fddcdd2fc38ca778905662e80fa9ffcea2ab", size = 8718, upload-time = "2024-11-11T06:04:49.779Z" }, + { url = "https://files.pythonhosted.org/packages/5c/31/102c15948d97b135611d6a995c97a3933c0e9745f25737723977f58e142c/tree_sitter_json-0.24.8-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:62b4c45b561db31436a81a3f037f71ec29049f4fc9bf5269b6ec3ebaaa35a1cd", size = 9163, upload-time = "2024-11-11T06:04:51.275Z" }, + { url = "https://files.pythonhosted.org/packages/28/64/aa44ea2f3d2e76ec086ce83902eb26b2ed0a92d3fd5e2714c9cb007e90d1/tree_sitter_json-0.24.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8627f7d375fda9fc193ebee368c453f374f65c2f25c58b6fea4e6b49a7fccbc", size = 17726, upload-time = "2024-11-11T06:04:52.732Z" }, + { url = "https://files.pythonhosted.org/packages/77/08/10001992526670e0d6f24c571b179f0ece90e5e014a4b98a3ce076884f32/tree_sitter_json-0.24.8-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85cca779872f7278f3a74eb38533d34b9c4de4fd548615e3361fa64fe350ad0a", size = 17236, upload-time = "2024-11-11T06:04:54.189Z" }, + { url = "https://files.pythonhosted.org/packages/92/64/908e9e0bd84fe3c81c564115d3bbe0e49b0e152784bbaf153d749d00bbe6/tree_sitter_json-0.24.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:deeb45850dcc52990fbb52c80196492a099e3fa3512d928a390a91cf061068cc", size = 16071, upload-time = "2024-11-11T06:04:55.628Z" }, + { url = "https://files.pythonhosted.org/packages/53/df/31daab1eedb445bef208a04fc35428de3afe2b37075fec84d7737e1c69de/tree_sitter_json-0.24.8-cp39-abi3-win_amd64.whl", hash = "sha256:e4849a03cd7197267b2688a4506a90a13568a8e0e8588080bd0212fcb38974e3", size = 11457, upload-time = "2024-11-11T06:04:57.698Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3d/902d2f3125b6b90cebf404b63ca775bc6d82071ccc76c0d10fabfeb2febe/tree_sitter_json-0.24.8-cp39-abi3-win_arm64.whl", hash = "sha256:591e0096c882d12668b88f30d3ca6f85b9db3406910eaaab6afb6b17d65367dd", size = 10174, upload-time = "2024-11-11T06:04:59.309Z" }, +] + +[[package]] +name = "tree-sitter-markdown" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/87/8f705d8f99337c8a691bcc8c22d89ddd323eb2b860a78ae2e894b9f7ade1/tree_sitter_markdown-0.5.1.tar.gz", hash = "sha256:6c69d7270a7e09be8988ced44584c09a6a4f541cea0dc394dd1c1a5ac3b5601d", size = 250138, upload-time = "2025-09-16T17:12:11.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/73/b5f88217a526f61080ddd71d554cff6a01ea23fffa584ad9de41ee8d1fe5/tree_sitter_markdown-0.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:f00ce3f48f127377983859fcb93caf0693cbc7970f8c41f1e2bd21e4d56bdfd8", size = 139706, upload-time = "2025-09-16T17:12:03.738Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9b/65eb5e6a8d7791174644854437d35849d9b4e4ed034d54d2c78810eaf1a6/tree_sitter_markdown-0.5.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1ec4cc5d7b0d188bad22247501ab13663bb1bf1a60c2c020a22877fabce8daa9", size = 147540, upload-time = "2025-09-16T17:12:04.955Z" }, + { url = "https://files.pythonhosted.org/packages/24/d5/4152d00829c8643243f65b67a5485248661824f15e1868e14e54f03c2069/tree_sitter_markdown-0.5.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727242a70c46222092eba86c102301646f21ba32aee221f4b1f70e2020755e81", size = 187851, upload-time = "2025-09-16T17:12:05.813Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c1/994001c5a51d09e9da7236e01a855d3d49437a47fa8669f1d5e9ed60e64f/tree_sitter_markdown-0.5.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0b2fde19e692bb90e300d9788887528c624b659c794de6337f8193396de4399", size = 187563, upload-time = "2025-09-16T17:12:06.929Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d1/1f2ba1ae11568639f133c45c7a697e4e9277d6cc26a66c0caee62c11d1c2/tree_sitter_markdown-0.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:13da82db04cec7910b6afd4a67d02da9ef402df8d56fc6ed85e00584af1730ee", size = 185478, upload-time = "2025-09-16T17:12:08.126Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c8/8218482d56b78755cdc20816a28754145cb1767e1e7e0ddde5988547ab86/tree_sitter_markdown-0.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b8a8a04a5d942c177cc590ec40074fcf3658f3a7c0a3388a8575990003665d8c", size = 184922, upload-time = "2025-09-16T17:12:08.937Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ca/423600960b91c3aba6f2202ad4c430b5401e652d51a73a59769375c2b4ea/tree_sitter_markdown-0.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:b1b0e4cbcf5a7b85005f1e9266fc2ed9b649b41a6048f3b1abae3612368d97a6", size = 142519, upload-time = "2025-09-16T17:12:10.027Z" }, + { url = "https://files.pythonhosted.org/packages/93/f5/327dd7fa42ae39796a8853685c40a8ac968585260094c581047270cbc851/tree_sitter_markdown-0.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:2296ef53a757d8f5b848616706d0518e04d487bc7748bd05755d4a3a65711542", size = 137166, upload-time = "2025-09-16T17:12:10.858Z" }, +] + +[[package]] +name = "tree-sitter-python" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/8b/c992ff0e768cb6768d5c96234579bf8842b3a633db641455d86dd30d5dac/tree_sitter_python-0.25.0.tar.gz", hash = "sha256:b13e090f725f5b9c86aa455a268553c65cadf325471ad5b65cd29cac8a1a68ac", size = 159845, upload-time = "2025-09-11T06:47:58.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/64/a4e503c78a4eb3ac46d8e72a29c1b1237fa85238d8e972b063e0751f5a94/tree_sitter_python-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:14a79a47ddef72f987d5a2c122d148a812169d7484ff5c75a3db9609d419f361", size = 73790, upload-time = "2025-09-11T06:47:47.652Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/60d8c2a0cc63d6ec4ba4e99ce61b802d2e39ef9db799bdf2a8f932a6cd4b/tree_sitter_python-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:480c21dbd995b7fe44813e741d71fed10ba695e7caab627fb034e3828469d762", size = 76691, upload-time = "2025-09-11T06:47:49.038Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/d9b0b67d037922d60cbe0359e0c86457c2da721bc714381a63e2c8e35eba/tree_sitter_python-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86f118e5eecad616ecdb81d171a36dde9bef5a0b21ed71ea9c3e390813c3baf5", size = 108133, upload-time = "2025-09-11T06:47:50.499Z" }, + { url = "https://files.pythonhosted.org/packages/40/bd/bf4787f57e6b2860f3f1c8c62f045b39fb32d6bac4b53d7a9e66de968440/tree_sitter_python-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be71650ca2b93b6e9649e5d65c6811aad87a7614c8c1003246b303f6b150f61b", size = 110603, upload-time = "2025-09-11T06:47:51.985Z" }, + { url = "https://files.pythonhosted.org/packages/5d/25/feff09f5c2f32484fbce15db8b49455c7572346ce61a699a41972dea7318/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6d5b5799628cc0f24691ab2a172a8e676f668fe90dc60468bee14084a35c16d", size = 108998, upload-time = "2025-09-11T06:47:53.046Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/4946da3d6c0df316ccb938316ce007fb565d08f89d02d854f2d308f0309f/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:71959832fc5d9642e52c11f2f7d79ae520b461e63334927e93ca46cd61cd9683", size = 107268, upload-time = "2025-09-11T06:47:54.388Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a2/996fc2dfa1076dc460d3e2f3c75974ea4b8f02f6bc925383aaae519920e8/tree_sitter_python-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:9bcde33f18792de54ee579b00e1b4fe186b7926825444766f849bf7181793a76", size = 76073, upload-time = "2025-09-11T06:47:55.773Z" }, + { url = "https://files.pythonhosted.org/packages/07/19/4b5569d9b1ebebb5907d11554a96ef3fa09364a30fcfabeff587495b512f/tree_sitter_python-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:0fbf6a3774ad7e89ee891851204c2e2c47e12b63a5edbe2e9156997731c128bb", size = 74169, upload-time = "2025-09-11T06:47:56.747Z" }, +] + +[[package]] +name = "tree-sitter-regex" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/92/1767b833518d731b97c07cf616ea15495dcc0af584aa0381657be4ec446d/tree_sitter_regex-0.25.0.tar.gz", hash = "sha256:5d29111b3f27d4afb31496476d392d1f562fe0bfe954e8968f1d8683424fc331", size = 22156, upload-time = "2025-09-13T05:00:18.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/b4/12e9ba02bab4ce13d1875f6585c3f2a5816233104d1507ea118950a4f7eb/tree_sitter_regex-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3fa11bbd76b29ac8ca2dbf85ad082f9b18ae6352251d805eb2d4191e1706a9d5", size = 13267, upload-time = "2025-09-13T05:00:10.847Z" }, + { url = "https://files.pythonhosted.org/packages/71/06/6b4f995f61952572a94bcfce12d43fc580226551fab9dd0aac4e94465f38/tree_sitter_regex-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:df5713649b89c5758649398053c306c41565f22a6f267cb5ec25596504bcf012", size = 13646, upload-time = "2025-09-13T05:00:12.149Z" }, + { url = "https://files.pythonhosted.org/packages/43/61/d94d889ee415805e5d64fc5163e7e2996975bb2c40d13f547efae3e7e37d/tree_sitter_regex-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cdd92400fd9d8229e584c55e12410251561f0d47eea49db17805e2f64a8b2490", size = 24691, upload-time = "2025-09-13T05:00:13.037Z" }, + { url = "https://files.pythonhosted.org/packages/00/a8/09dd698a9ac2b3d3139a936742b41ec1263f0b86d32ad68f4695871c8860/tree_sitter_regex-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cceab1c14deeec9c5899babcb2b7942f0607b4355e66eab4083514f644f1bd52", size = 26741, upload-time = "2025-09-13T05:00:14.182Z" }, + { url = "https://files.pythonhosted.org/packages/d7/bf/985e226c9a9f5ae895ff1a2cbc69531589a7d74acac49b2710ec89d53d80/tree_sitter_regex-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:253436be178150ca4a0603720e0c246e08b5bdd2dc6df313667d97e6c0fce846", size = 25758, upload-time = "2025-09-13T05:00:14.994Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/c6a6817e94a7deb61770a21e590a46791778ceed053ba4afbfb095488a23/tree_sitter_regex-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:883eacc46fd7eaffc328efd5865f1fe8825711892d3a89fccc2c414b061e806d", size = 24575, upload-time = "2025-09-13T05:00:16.081Z" }, + { url = "https://files.pythonhosted.org/packages/d0/5e/04e87eb155875f27355703ac7ab703090e30ad9aac6e003ef5c40820ee98/tree_sitter_regex-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:f0f2ebf9a6bb5d0d0da2a8ac51d7e5a985b87cdb24d86db5ddc6a58baf115d5d", size = 15684, upload-time = "2025-09-13T05:00:16.865Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d3/1f37c79dc18cc3c7521fdb51b614d29a36628d2afdc2cac2680967e703a6/tree_sitter_regex-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:d5a36150daa452f8aec1c2d6d1f2d26255dc05d1490f9618b14c12a6a648cda4", size = 14525, upload-time = "2025-09-13T05:00:17.673Z" }, +] + +[[package]] +name = "tree-sitter-rust" +version = "0.24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/87/75cbd22b927267d310f76cca1ab3c1d9d41035dfa3eb9cc95f96ee199440/tree_sitter_rust-0.24.2.tar.gz", hash = "sha256:54fb02a5911e345308b405174465112479f56dc39e3f1e7744d7568595f00db9", size = 339341, upload-time = "2026-03-27T21:08:55.629Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/24/2b2d33af5e27c84a4fde4e8cd2594bb4ab1e1cf48756a9f40dadc84956cc/tree_sitter_rust-0.24.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3620cfd12340efa43082d45df76349ff511893a9c361da2f8d6d51e307020a59", size = 129507, upload-time = "2026-03-27T21:08:47.585Z" }, + { url = "https://files.pythonhosted.org/packages/78/2a/cf39f881a545360b5a86bb1accba1f4acc713daab01fb9edd35b6e84f473/tree_sitter_rust-0.24.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:01a46622735498493f29f3e628a90de95c96a07bfbeb88996243eb986b1cee36", size = 136812, upload-time = "2026-03-27T21:08:48.761Z" }, + { url = "https://files.pythonhosted.org/packages/ca/45/a051bbd3045a61182dde25b93ae9a33d2677c935b16952283e12eaf46051/tree_sitter_rust-0.24.2-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e033c5a93b57c88e0a835880de39fc802909ff69f57aaff6000211c196ea5190", size = 164706, upload-time = "2026-03-27T21:08:49.605Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f6/a5a146df5c0a5daea3ffcd5d7245775fe7f084357770d5a313dd6245ae78/tree_sitter_rust-0.24.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d76d1208c3638b871236090759dfc13d478921320653a6c9da5336e7c58f65a", size = 170310, upload-time = "2026-03-27T21:08:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/95/a8/f85b1ca75e01361ca5f92d226593ca4857cea49551b9f6c8fa6fc08ea917/tree_sitter_rust-0.24.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87930163a462408c49ab62c667e74029bc26b4cc7123dd1bdc7352215786c64a", size = 168668, upload-time = "2026-03-27T21:08:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e1/3519f866a4679ca36acd9f5a06a779ecb8a92b18887c5546458d521df557/tree_sitter_rust-0.24.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:da2b86099028fd42c6cd32878b7b16b01f8aac0f7b0e98742b7fa6bc3cf09b89", size = 162403, upload-time = "2026-03-27T21:08:52.588Z" }, + { url = "https://files.pythonhosted.org/packages/34/71/7ef609894dbfe5699eb16f7471f9b8af1d958d8ba3e29c238d7607e8cb47/tree_sitter_rust-0.24.2-cp39-abi3-win_amd64.whl", hash = "sha256:4529c125d928882ddfb879fdc6bc0704913261ecc078b6fa7902559e0daf200d", size = 129422, upload-time = "2026-03-27T21:08:54.031Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d8/050a781172745bc345f98abb7c56e72022ea0790f8e793de981c83c2ef15/tree_sitter_rust-0.24.2-cp39-abi3-win_arm64.whl", hash = "sha256:66ba90f61bd54f4c4f5d30434957daf64507c16b0313df76becb37d63f70a227", size = 128245, upload-time = "2026-03-27T21:08:54.803Z" }, +] + +[[package]] +name = "tree-sitter-sql" +version = "0.3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/5c/3d10387f779f36835486167253682f61d5f4fd8336b7001da1ac7d78f31c/tree_sitter_sql-0.3.11.tar.gz", hash = "sha256:700b93be2174c3c83d174ec3e10b682f72a4fb451f0076c7ce5012f1d5a76cbc", size = 834454, upload-time = "2025-10-01T13:44:15.913Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/68/bb80073915dfe1b38935451bc0d65528666c126b2d5878e7140ef9bf9f8a/tree_sitter_sql-0.3.11-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cf1b0c401756940bf47544ad7c4cc97373fc0dac118f821820953e7015a115e3", size = 322035, upload-time = "2025-10-01T13:44:07.497Z" }, + { url = "https://files.pythonhosted.org/packages/05/45/b2bd5f9919ea15c4ae90a156999101ebd4caa4036babe54efaf9d3e77d55/tree_sitter_sql-0.3.11-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a33cd6880ab2debef036f80365c32becb740ec79946805598488732b6c515fff", size = 341635, upload-time = "2025-10-01T13:44:08.961Z" }, + { url = "https://files.pythonhosted.org/packages/8e/96/7cee5661aa897e5d1a67499944ea5cf8a148953c1dc07a3059a50db8cb56/tree_sitter_sql-0.3.11-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:344e99b59c8c8d72f7154041e9d054400f4a3fccc16c2c96ac106dde0e7f8d0c", size = 381217, upload-time = "2025-10-01T13:44:10.211Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c1/eec7c09a9c94436ea4c56d096feba815e42b209b3d41a17532f99ecf0c67/tree_sitter_sql-0.3.11-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5128b12f71ac0f5ebcc607f67a62cdc56a187c1a5ba7553feeb9c5f6f9bc3c72", size = 380606, upload-time = "2025-10-01T13:44:11.135Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/06e9598799bd119e56f6e431d42c2f3a5c6dee858a5b6ad7633cc4d670aa/tree_sitter_sql-0.3.11-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:03cc164fcf7b1f711e7d939aeb4d1f62c76f4162e081c70b860b4fcd91806a38", size = 380862, upload-time = "2025-10-01T13:44:12.072Z" }, + { url = "https://files.pythonhosted.org/packages/52/e9/a7afd7f68ce165c040ce50e67bb05553784a8e17f37e057405d693fc869d/tree_sitter_sql-0.3.11-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:0e22ea8de690dd9960d8c0c36c4cd25417b084e1e29c91ac0235fbdb3abb4664", size = 379447, upload-time = "2025-10-01T13:44:13.062Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b3/57ff42dadd33c06fabe6c725de50e1625e1060f1571cc21a9260febadc1f/tree_sitter_sql-0.3.11-cp310-abi3-win_amd64.whl", hash = "sha256:c57b877702d218c0856592d33320c02b2dc8411d8820b3bf7b81be86c54fa0bb", size = 343550, upload-time = "2025-10-01T13:44:13.988Z" }, + { url = "https://files.pythonhosted.org/packages/77/60/f10b8551f435d57a4748820ee30e66df2682820b2972375c2b89d2e5fb10/tree_sitter_sql-0.3.11-cp310-abi3-win_arm64.whl", hash = "sha256:8a1e42f0a2c9b01b23074708ecf5b8d21b9a0440e3dff279d8cf466cdf1a877e", size = 333547, upload-time = "2025-10-01T13:44:14.893Z" }, +] + +[[package]] +name = "tree-sitter-toml" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/b9/03ee757ac375e77186ea112c14fcf31e0ca70b27b6388d93dcceef61f029/tree_sitter_toml-0.7.0.tar.gz", hash = "sha256:29e257612fa8f0c1fcbc4e7e08ddc561169f1725265302e64d81086354144a70", size = 16803, upload-time = "2024-12-03T05:03:46.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/4d/1e00a5cd8dba09e340b25aa60a3eaeae584ff5bc5d93b0777169d6741ee5/tree_sitter_toml-0.7.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b9ae5c3e7c5b6bb05299dd73452ceafa7fa0687d5af3012332afa7757653b676", size = 14755, upload-time = "2024-12-03T05:03:39.973Z" }, + { url = "https://files.pythonhosted.org/packages/92/20/ac8a20805339105fe0bbb6beaa99dbbd1159647760ddd786142364e0b7f2/tree_sitter_toml-0.7.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:18be09538e9775cddc0290392c4e2739de2201260af361473ca60b5c21f7bd22", size = 15201, upload-time = "2024-12-03T05:03:40.871Z" }, + { url = "https://files.pythonhosted.org/packages/36/cf/7bae8e20310e7cc763ae407599e6130b819f91ad5197e210a56f697f15d8/tree_sitter_toml-0.7.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a045e0acfcf91b7065066f7e51ea038ed7385c1e35e7e8fae18f252d3f8adb8c", size = 30855, upload-time = "2024-12-03T05:03:41.83Z" }, + { url = "https://files.pythonhosted.org/packages/7d/49/51f2fa25a3ff4d45af1be8cbf7a3d733fb6a390b2763cfa00892fffe90bf/tree_sitter_toml-0.7.0-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a2f8cf9d73f07b6628093b35e5c5fbac039247e32cb075eaa5289a5914e73af", size = 29741, upload-time = "2024-12-03T05:03:42.577Z" }, + { url = "https://files.pythonhosted.org/packages/4d/30/dd94ed1ab0bc3198e16ed2140a6f4d2474c1cd561d8c6847ab269af73654/tree_sitter_toml-0.7.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:860ffa4513b2dc3083d8e412bd815a350b0a9490624b37e7c8f6ed5c6f9ce63c", size = 30498, upload-time = "2024-12-03T05:03:43.447Z" }, + { url = "https://files.pythonhosted.org/packages/a2/dd/0681d43aa09dd161565858bcfdd4402c8d10259f142de734448f5ce17418/tree_sitter_toml-0.7.0-cp39-abi3-win_amd64.whl", hash = "sha256:2760a04f06937b01b1562a2135cd7e8207e399e73ef75bbebc77e37b1ad3b15d", size = 16756, upload-time = "2024-12-03T05:03:44.227Z" }, + { url = "https://files.pythonhosted.org/packages/17/e4/cce587001e620f1972e70aeabc1b38893a85681be9ec5a64e4be9ce17410/tree_sitter_toml-0.7.0-cp39-abi3-win_arm64.whl", hash = "sha256:fd00fd8a51c65aa19c40539431cb1773d87c30af5757b4041fa6c229058420b4", size = 15651, upload-time = "2024-12-03T05:03:45.261Z" }, +] + +[[package]] +name = "tree-sitter-xml" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/ba/77a92dbb4dfb374fb99863a07f938de7509ceeaa74139933ac2bd306eeb1/tree_sitter_xml-0.7.0.tar.gz", hash = "sha256:ab0ff396f20230ad8483d968151ce0c35abe193eb023b20fbd8b8ce4cf9e9f61", size = 54635, upload-time = "2024-11-13T17:27:01.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/1d/6b8974c493973c0c9df2bbf220a1f0a96fa785da81a5a13461faafd1441c/tree_sitter_xml-0.7.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cc3e516d4c1e0860fb22172c172148debb825ba638971bc48bad15b22e5b0bae", size = 35404, upload-time = "2024-11-13T17:26:51.989Z" }, + { url = "https://files.pythonhosted.org/packages/75/f5/31013d04c4e3b9a55e90168cc222a601c84235ba4953a5a06b5cdf8353c4/tree_sitter_xml-0.7.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0674fdf4cc386e4d323cb287d3b072663de0f20a9e9af5d5e09821aae56a9e5c", size = 35488, upload-time = "2024-11-13T17:26:53.526Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e6/e7493217f950a7c5969e3f3f057664142fa948abefd2dba5acea25719d55/tree_sitter_xml-0.7.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c0fe5f2d6cc09974c8375c8ea9b24909f493b5bf04aacdc4c694b5d2ae6b040", size = 74199, upload-time = "2024-11-13T17:26:55.069Z" }, + { url = "https://files.pythonhosted.org/packages/94/27/1dd6815592489de51fa7b5fffc1160cd385ade7fa06f07b998742ac18020/tree_sitter_xml-0.7.0-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd3209516a4d84dff90bc91d2ad2ce246de8504cede4358849687fa8e71536e7", size = 76244, upload-time = "2024-11-13T17:26:56.655Z" }, + { url = "https://files.pythonhosted.org/packages/20/10/2e4e84c50b2175cb53d255ef154aa893cb82cc9d035d7a1a73be9d2d2db4/tree_sitter_xml-0.7.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:87578e15fa55f44ecd9f331233b6f8a2cbde3546b354c830ecb862a632379455", size = 75112, upload-time = "2024-11-13T17:26:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/ae/91/77c348568bccb179eca21062c923f6f54026900b09fe0cf1aae89d78a0c8/tree_sitter_xml-0.7.0-cp39-abi3-win_amd64.whl", hash = "sha256:9ba2dafc6ce9feaf4ccc617d3aeea57f8e0ca05edad34953e788001ebff79133", size = 36558, upload-time = "2024-11-13T17:26:58.702Z" }, + { url = "https://files.pythonhosted.org/packages/be/cc/6b4de230770d7be87b2a415583121ac565ce1ff7d9a1ad7fec11f8e613fc/tree_sitter_xml-0.7.0-cp39-abi3-win_arm64.whl", hash = "sha256:fc759f710a8fd7a01c23e2d7cb013679199045bea3dc0e5151650a11322aaf40", size = 34610, upload-time = "2024-11-13T17:27:00.187Z" }, +] + +[[package]] +name = "tree-sitter-yaml" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/b6/941d356ac70c90b9d2927375259e3a4204f38f7499ec6e7e8a95b9664689/tree_sitter_yaml-0.7.2.tar.gz", hash = "sha256:756db4c09c9d9e97c81699e8f941cb8ce4e51104927f6090eefe638ee567d32c", size = 84882, upload-time = "2025-10-07T14:40:36.071Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/29/c0b8dbff302c49ff4284666ffb6f2f21145006843bb4c3a9a85d0ec0b7ae/tree_sitter_yaml-0.7.2-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:7e269ddcfcab8edb14fbb1f1d34eed1e1e26888f78f94eedfe7cc98c60f8bc9f", size = 43898, upload-time = "2025-10-07T14:40:29.486Z" }, + { url = "https://files.pythonhosted.org/packages/18/0d/15a5add06b3932b5e4ce5f5e8e179197097decfe82a0ef000952c8b98216/tree_sitter_yaml-0.7.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:0807b7966e23ddf7dddc4545216e28b5a58cdadedcecca86b8d8c74271a07870", size = 44691, upload-time = "2025-10-07T14:40:30.369Z" }, + { url = "https://files.pythonhosted.org/packages/72/92/c4b896c90d08deb8308fadbad2210fdcc4c66c44ab4292eac4e80acb4b61/tree_sitter_yaml-0.7.2-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f1a5c60c98b6c4c037aae023569f020d0c489fad8dc26fdfd5510363c9c29a41", size = 91430, upload-time = "2025-10-07T14:40:31.16Z" }, + { url = "https://files.pythonhosted.org/packages/89/59/61f1fed31eb6d46ff080b8c0d53658cf29e10263f41ef5fe34768908037a/tree_sitter_yaml-0.7.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88636d19d0654fd24f4f242eaaafa90f6f5ebdba8a62e4b32d251ed156c51a2a", size = 92428, upload-time = "2025-10-07T14:40:31.954Z" }, + { url = "https://files.pythonhosted.org/packages/e3/62/a33a04d19b7f9a0ded780b9c9fcc6279e37c5d00b89b00425bb807a22cc2/tree_sitter_yaml-0.7.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1d2e8f0bb14aa4537320952d0f9607eef3021d5aada8383c34ebeece17db1e06", size = 90580, upload-time = "2025-10-07T14:40:33.037Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e7/9525defa7b30792623f56b1fba9bbba361752348875b165b8975b87398fd/tree_sitter_yaml-0.7.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:74ca712c50fc9d7dbc68cb36b4a7811d6e67a5466b5a789f19bf8dd6084ef752", size = 90455, upload-time = "2025-10-07T14:40:33.778Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d6/8d1e1ace03db3b02e64e91daf21d1347941d1bbecc606a5473a1a605250d/tree_sitter_yaml-0.7.2-cp310-abi3-win_amd64.whl", hash = "sha256:7587b5ca00fc4f9a548eff649697a3b395370b2304b399ceefa2087d8a6c9186", size = 45514, upload-time = "2025-10-07T14:40:34.562Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c7/dcf3ea1c4f5da9b10353b9af4455d756c92d728a8f58f03c480d3ef0ead5/tree_sitter_yaml-0.7.2-cp310-abi3-win_arm64.whl", hash = "sha256:f63c227b18e7ce7587bce124578f0bbf1f890ac63d3e3cd027417574273642c4", size = 44065, upload-time = "2025-10-07T14:40:35.337Z" }, +] + [[package]] name = "typing-extensions" version = "4.14.1"