feat: add TOML config for keybinds and cursor reset strategy

This commit is contained in:
lohhiiccc 2026-07-02 18:49:34 +02:00
parent abd3e2e98d
commit c0ff8ae70b
5 changed files with 117 additions and 18 deletions

View file

@ -8,23 +8,45 @@ if TYPE_CHECKING:
class Input: class Input:
def __init__(self, app: App) -> None: def __init__(self, app: App) -> None:
self._app = app 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: def handle(self, key: str) -> bool:
navigator = self.app.navigator action = self.app.config.action_for(key)
if 'q' == key: handler = self._actions.get(action)
return handler() if handler else True
def _quit(self) -> bool:
return False return False
elif key in ('j', 'KEY_DOWN'):
def _down(self) -> bool:
navigator = self.app.navigator
navigator.cursor.down(len(navigator.items)) navigator.cursor.down(len(navigator.items))
elif key in ('k', 'KEY_UP'): return True
navigator.cursor.up()
elif key in ('l', 'KEY_RIGHT', '\n', 'KEY_ENTER'): def _up(self) -> bool:
self.app.navigator.cursor.up()
return True
def _enter(self) -> bool:
navigator = self.app.navigator
items = navigator.items items = navigator.items
if items: if items:
navigator.enter(items[navigator.cursor.pos]) navigator.enter(items[navigator.cursor.pos])
elif key in ('h', 'KEY_LEFT', 'KEY_BACKSPACE', '\x7f', '\x08'): return True
navigator.back()
elif key == '-': def _back(self) -> bool:
navigator.up() self.app.navigator.back()
return True
def _parent(self) -> bool:
self.app.navigator.up()
return True return True
@property @property

View file

@ -1,6 +1,7 @@
from pathlib import Path from pathlib import Path
from ..IO.UI import UI from ..IO.UI import UI
from ..IO.Input import Input from ..IO.Input import Input
from .Config import Config
from .Navigator import Navigator from .Navigator import Navigator
import curses import curses
@ -13,7 +14,8 @@ class App():
""" """
def __init__(self, path: Path) -> None: 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._UI = UI(self)
self._input = Input(self) self._input = Input(self)
@ -38,6 +40,10 @@ class App():
def run(self) -> None: def run(self) -> None:
curses.wrapper(self._run) curses.wrapper(self._run)
@property
def config(self) -> Config:
return self._config
@property @property
def navigator(self) -> Navigator: def navigator(self) -> Navigator:
return self._navigator return self._navigator

60
src/core/Config.py Normal file
View file

@ -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

View file

@ -6,6 +6,9 @@ class Cursor:
def resete(self) -> None: def resete(self) -> None:
self._pos = 0 self._pos = 0
def clamp(self, count: int) -> None:
self._pos = min(self._pos, max(count - 1, 0))
def up(self) -> None: def up(self) -> None:
self._pos -= 1 if self.pos >= 1 else 0 self._pos -= 1 if self.pos >= 1 else 0

View file

@ -2,12 +2,14 @@ from pathlib import Path
from ..items.AItems import AItem from ..items.AItems import AItem
from ..items.Folder import Folder from ..items.Folder import Folder
from ..items.Symlink import Symlink from ..items.Symlink import Symlink
from .Config import Config
from .Cursor import Cursor from .Cursor import Cursor
class Navigator: class Navigator:
def __init__(self, path: Path) -> None: def __init__(self, path: Path, config: Config) -> None:
self._config = config
self._history = [path] self._history = [path]
self._cursor = Cursor() self._cursor = Cursor()
self._current: Folder | None = None self._current: Folder | None = None
@ -29,10 +31,16 @@ class Navigator:
def refresh(self) -> None: def refresh(self) -> None:
self._load() 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: def _navigate_to(self, path: Path) -> None:
self._history.append(path) self._history.append(path)
self._load() self._load()
self._cursor.resete() self._reset_cursor()
def enter(self, item: AItem) -> bool: def enter(self, item: AItem) -> bool:
if not item.is_navigable(): if not item.is_navigable():
@ -53,7 +61,7 @@ class Navigator:
return False return False
self._history.pop() self._history.pop()
self._load() self._load()
self._cursor.resete() self._reset_cursor()
return True return True
@property @property