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 @@
-
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_name: browsr
-nav:
--index.md
--Command Line Interface ⌨️: cli.md
--Contributing 🤝: contributing.md
--API Documentation 🤖: reference/
-theme:
-favicon: https://raw.githubusercontent.com/juftin/browsr/main/docs/_static/browsr_no_label.png
-logo: https://raw.githubusercontent.com/juftin/browsr/main/docs/_static/browsr_no_label.png
-name: material
-features:
--navigation.tracking
--content.code.annotate
--content.code.copy
-palette:
--media: "(prefers-color-scheme:light)"
-scheme: default
-accent: purple
-toggle:
-icon: material/weather-sunny
-name: Switch to dark mode
--media: "(prefers-color-scheme:dark)"
-scheme: slate
-primary: black
-toggle:
-icon: material/weather-night
-name: Switch to light mode
-repo_url: https://github.com/juftin/browsr
-repo_name: browsr
-edit_uri: blob/main/docs/
-site_author: Justin Flannery
-remote_branch: gh-pages
-copyright: Copyright © 2023 Justin Flannery
-extra:
-generator: false
-markdown_extensions:
--toc:
-permalink: "#"
--pymdownx.snippets
--pymdownx.magiclink
--attr_list
--md_in_html
--pymdownx.highlight:
+site_name: browsr
+nav:
+ - index.md
+ - Command Line Interface ⌨️: cli.md
+ - Contributing 🤝: contributing.md
+ - API Documentation 🤖: reference/
+theme:
+favicon: https://raw.githubusercontent.com/juftin/browsr/main/docs/_static/browsr_no_label.png
+logo: https://raw.githubusercontent.com/juftin/browsr/main/docs/_static/browsr_no_label.png
+name: material
+features:
+ - navigation.tracking
+ - content.code.annotate
+ - content.code.copy
+palette:
+ - media: "(prefers-color-scheme: light)"
+scheme: default
+accent: purple
+toggle:
+icon: material/weather-sunny
+name: Switch to dark mode
+ - media: "(prefers-color-scheme: dark)"
+scheme: slate
+primary: black
+toggle:
+icon: material/weather-night
+name: Switch to light mode
+repo_url: https://github.com/juftin/browsr
+repo_name: browsr
+edit_uri: blob/main/docs/
+site_author: Justin Flannery
+remote_branch: gh-pages
+copyright: Copyright © 2023 Justin Flannery
+extra:
+generator: false
+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 @@
+
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"