diff --git a/src/IO/Input.py b/src/IO/Input.py index 0146576..af696da 100644 --- a/src/IO/Input.py +++ b/src/IO/Input.py @@ -8,23 +8,45 @@ if TYPE_CHECKING: class Input: def __init__(self, app: App) -> None: self._app = app + self._actions = { + "quit": self._quit, + "down": self._down, + "up": self._up, + "enter": self._enter, + "back": self._back, + "parent": self._parent, + } def handle(self, key: str) -> bool: + action = self.app.config.action_for(key) + handler = self._actions.get(action) + return handler() if handler else True + + def _quit(self) -> bool: + return False + + def _down(self) -> bool: navigator = self.app.navigator - if 'q' == key: - return False - elif key in ('j', 'KEY_DOWN'): - navigator.cursor.down(len(navigator.items)) - elif key in ('k', 'KEY_UP'): - navigator.cursor.up() - elif key in ('l', 'KEY_RIGHT', '\n', 'KEY_ENTER'): - items = navigator.items - if items: - navigator.enter(items[navigator.cursor.pos]) - elif key in ('h', 'KEY_LEFT', 'KEY_BACKSPACE', '\x7f', '\x08'): - navigator.back() - elif key == '-': - navigator.up() + navigator.cursor.down(len(navigator.items)) + return True + + def _up(self) -> bool: + self.app.navigator.cursor.up() + return True + + def _enter(self) -> bool: + navigator = self.app.navigator + items = navigator.items + if items: + navigator.enter(items[navigator.cursor.pos]) + return True + + def _back(self) -> bool: + self.app.navigator.back() + return True + + def _parent(self) -> bool: + self.app.navigator.up() return True @property diff --git a/src/core/App.py b/src/core/App.py index 590dd79..3f18b4d 100644 --- a/src/core/App.py +++ b/src/core/App.py @@ -1,6 +1,7 @@ from pathlib import Path from ..IO.UI import UI from ..IO.Input import Input +from .Config import Config from .Navigator import Navigator import curses @@ -13,7 +14,8 @@ class App(): """ def __init__(self, path: Path) -> None: - self._navigator = Navigator(path) + self._config = Config.load() + self._navigator = Navigator(path, self._config) self._UI = UI(self) self._input = Input(self) @@ -38,6 +40,10 @@ class App(): def run(self) -> None: curses.wrapper(self._run) + @property + def config(self) -> Config: + return self._config + @property def navigator(self) -> Navigator: return self._navigator diff --git a/src/core/Config.py b/src/core/Config.py new file mode 100644 index 0000000..621eca1 --- /dev/null +++ b/src/core/Config.py @@ -0,0 +1,60 @@ +from __future__ import annotations +import os +import sys +import tomllib +from pathlib import Path + +DEFAULT_KEYBINDS: dict[str, list[str]] = { + "quit": ["q"], + "down": ["j", "KEY_DOWN"], + "up": ["k", "KEY_UP"], + "enter": ["l", "KEY_RIGHT", "\n", "KEY_ENTER"], + "back": ["h", "KEY_LEFT", "KEY_BACKSPACE", "\x7f", "\x08"], + "parent": ["-"], +} + +DEFAULT_CURSOR_RESET = "top" + + +class Config: + def __init__(self, keybinds: dict[str, list[str]], cursor_reset: str) -> None: + self._keybinds = keybinds + self._cursor_reset = cursor_reset + self._key_to_action: dict[str, str] = { + key: action + for action, keys in keybinds.items() + for key in keys + } + + @classmethod + def load(cls, path: Path | None = None) -> Config: + path = path or cls._default_path() + data: dict = {} + if path.is_file(): + try: + with path.open("rb") as f: + data = tomllib.load(f) + except tomllib.TOMLDecodeError as e: + print(f"pyexplorer: ignoring invalid config {path}: {e}", + file=sys.stderr) + data = {} + + keybinds = dict(DEFAULT_KEYBINDS) + keybinds.update(data.get("keybinds", {})) + cursor_reset = data.get("cursor", {}).get("reset", DEFAULT_CURSOR_RESET) + return cls(keybinds, cursor_reset) + + @staticmethod + def _default_path() -> Path: + base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config") + return Path(base) / "pyexplorer" / "config.toml" + + def keys_for(self, action: str) -> list[str]: + return self._keybinds.get(action, []) + + def action_for(self, key: str) -> str | None: + return self._key_to_action.get(key) + + @property + def cursor_reset(self) -> str: + return self._cursor_reset diff --git a/src/core/Cursor.py b/src/core/Cursor.py index 8c1fc4f..471110a 100644 --- a/src/core/Cursor.py +++ b/src/core/Cursor.py @@ -6,6 +6,9 @@ class Cursor: def resete(self) -> None: self._pos = 0 + def clamp(self, count: int) -> None: + self._pos = min(self._pos, max(count - 1, 0)) + def up(self) -> None: self._pos -= 1 if self.pos >= 1 else 0 diff --git a/src/core/Navigator.py b/src/core/Navigator.py index 88d9499..ccde0a1 100644 --- a/src/core/Navigator.py +++ b/src/core/Navigator.py @@ -2,12 +2,14 @@ from pathlib import Path from ..items.AItems import AItem from ..items.Folder import Folder from ..items.Symlink import Symlink +from .Config import Config from .Cursor import Cursor class Navigator: - def __init__(self, path: Path) -> None: + def __init__(self, path: Path, config: Config) -> None: + self._config = config self._history = [path] self._cursor = Cursor() self._current: Folder | None = None @@ -29,10 +31,16 @@ class Navigator: def refresh(self) -> None: self._load() + def _reset_cursor(self) -> None: + if self._config.cursor_reset == "keep": + self._cursor.clamp(len(self._items)) + else: + self._cursor.resete() + def _navigate_to(self, path: Path) -> None: self._history.append(path) self._load() - self._cursor.resete() + self._reset_cursor() def enter(self, item: AItem) -> bool: if not item.is_navigable(): @@ -53,7 +61,7 @@ class Navigator: return False self._history.pop() self._load() - self._cursor.resete() + self._reset_cursor() return True @property