From 87c9a6f641c036f2f3ba2710e53a31ed6513bbc0 Mon Sep 17 00:00:00 2001 From: lohhiiccc <96543753+lohhiiccc@users.noreply.github.com> Date: Thu, 2 Jul 2026 20:06:30 +0200 Subject: [PATCH] feat: add scrolling with configurable scrolloff --- src/IO/UI.py | 32 +++++++++++++++++++++++++++++--- src/core/Config.py | 16 ++++++++++++++-- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/IO/UI.py b/src/IO/UI.py index 9fb5b68..a11d6bf 100644 --- a/src/IO/UI.py +++ b/src/IO/UI.py @@ -12,6 +12,8 @@ if TYPE_CHECKING: class UI: def __init__(self, app: App): self._app = app + self._scroll_top = 0 + self._scroll_path = None HEADER_ROWS = 1 @@ -31,14 +33,38 @@ class UI: items = navigator.items available = max(0, height - self.HEADER_ROWS) - for index in range(min(available, len(items))): - item = items[index] - self._render_item(stdscr, item, index, index + self.HEADER_ROWS, width) + top = self._update_scroll(navigator, available) + for offset, item in enumerate(items[top:top + available]): + index = top + offset + self._render_item(stdscr, item, index, offset + self.HEADER_ROWS, width) prompt = self.app.prompt if prompt: self._render_modal(stdscr, prompt, height, width) + def _update_scroll(self, navigator, available: int) -> int: + current_path = navigator.history[-1] + if current_path != self._scroll_path: + self._scroll_path = current_path + self._scroll_top = 0 + + total = len(navigator.items) + if available <= 0 or total <= available: + self._scroll_top = 0 + return self._scroll_top + + offset = min(self.app.config.scroll_offset, (available - 1) // 2) + pos = navigator.cursor.pos + max_top = total - available + + if pos < self._scroll_top + offset: + self._scroll_top = pos - offset + elif pos > self._scroll_top + available - 1 - offset: + self._scroll_top = pos - available + 1 + offset + + self._scroll_top = max(0, min(self._scroll_top, max_top)) + return self._scroll_top + MODAL_MAX_WIDTH = 60 MODAL_PADDING = 2 diff --git a/src/core/Config.py b/src/core/Config.py index 4d67432..d9cd0b1 100644 --- a/src/core/Config.py +++ b/src/core/Config.py @@ -20,15 +20,17 @@ DEFAULT_KEYBINDS: dict[str, list[str]] = { } DEFAULT_CURSOR_RESET = "top" +DEFAULT_SCROLL_OFFSET = 4 class Config: def __init__(self, keybinds: dict[str, list[str]], cursor_reset: str, - sort_mode: str, theme: Theme) -> None: + sort_mode: str, theme: Theme, scroll_offset: int) -> None: self._keybinds = keybinds self._cursor_reset = cursor_reset self._sort_mode = sort_mode self._theme = theme + self._scroll_offset = scroll_offset self._key_to_action: dict[str, str] = { key: action for action, keys in keybinds.items() @@ -57,7 +59,13 @@ class Config: f"falling back to {Sort.DEFAULT_MODE!r}", file=sys.stderr) sort_mode = Sort.DEFAULT_MODE theme = Theme.load(data) - return cls(keybinds, cursor_reset, sort_mode, theme) + scroll_offset = data.get("scroll", {}).get("offset", + DEFAULT_SCROLL_OFFSET) + if not isinstance(scroll_offset, int) or scroll_offset < 0: + print(f"pyexplorer: invalid scroll offset {scroll_offset!r}, " + f"falling back to {DEFAULT_SCROLL_OFFSET}", file=sys.stderr) + scroll_offset = DEFAULT_SCROLL_OFFSET + return cls(keybinds, cursor_reset, sort_mode, theme, scroll_offset) @staticmethod def _default_path() -> Path: @@ -81,3 +89,7 @@ class Config: @property def theme(self) -> Theme: return self._theme + + @property + def scroll_offset(self) -> int: + return self._scroll_offset