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:
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:
navigator = self.app.navigator
if 'q' == key:
action = self.app.config.action_for(key)
handler = self._actions.get(action)
return handler() if handler else True
def _quit(self) -> bool:
return False
elif key in ('j', 'KEY_DOWN'):
def _down(self) -> bool:
navigator = self.app.navigator
navigator.cursor.down(len(navigator.items))
elif key in ('k', 'KEY_UP'):
navigator.cursor.up()
elif key in ('l', 'KEY_RIGHT', '\n', 'KEY_ENTER'):
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])
elif key in ('h', 'KEY_LEFT', 'KEY_BACKSPACE', '\x7f', '\x08'):
navigator.back()
elif key == '-':
navigator.up()
return True
def _back(self) -> bool:
self.app.navigator.back()
return True
def _parent(self) -> bool:
self.app.navigator.up()
return True
@property

View file

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

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

View file

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