refactor: extract Navigator to own view state and add navigation

This commit is contained in:
lohhiiccc 2026-07-02 18:27:57 +02:00
parent 37b14de04d
commit abd3e2e98d
6 changed files with 124 additions and 45 deletions

View file

@ -10,12 +10,21 @@ class Input:
self._app = app self._app = app
def handle(self, key: str) -> bool: def handle(self, key: str) -> bool:
navigator = self.app.navigator
if 'q' == key: if 'q' == key:
return False return False
elif key in ('j', 'KEY_DOWN'): elif key in ('j', 'KEY_DOWN'):
self.app.cursor.down(len(self.app.items)) navigator.cursor.down(len(navigator.items))
elif key in ('k', 'KEY_UP'): elif key in ('k', 'KEY_UP'):
self.app.cursor.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()
return True return True
@property @property

View file

@ -13,39 +13,54 @@ class UI:
def __init__(self, app: App): def __init__(self, app: App):
self._app = app self._app = app
def _get_attr(self, item: any, i: int) -> str: HEADER_ROWS = 2
ERROR_COLOR = 5
def _get_attr(self, item: any, index: int) -> str:
attr = curses.color_pair(item.color_pair) attr = curses.color_pair(item.color_pair)
if i == self.app.cursor.pos: if index == self.app.navigator.cursor.pos:
attr |= curses.A_REVERSE attr |= curses.A_REVERSE
return attr return attr
def render(self, stdscr: curses.window) -> None: def render(self, stdscr: curses.window) -> None:
stdscr.erase()
height, width = stdscr.getmaxyx() height, width = stdscr.getmaxyx()
for i in range(min(height, len(self.app.items))): navigator = self.app.navigator
item = self.app.items[i]
self._render_item(stdscr, item, i, width) stdscr.addstr(0, 0, str(navigator.history[-1])[:width - 1])
if navigator.error:
stdscr.addstr(1, 0, navigator.error[:width - 1],
curses.color_pair(self.ERROR_COLOR))
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)
ARROW = " -> " ARROW = " -> "
ARROW_LEN = len(ARROW) ARROW_LEN = len(ARROW)
def _render_item(self, stdscr: curses.window, item: AItem, i: int, width: int) -> None:
def _render_item(self, stdscr: curses.window, item: AItem, index: int,
row: int, width: int) -> None:
name, size, perm = item.fields() name, size, perm = item.fields()
name_len = len(name) name_len = len(name)
col_width = int(width / 3) col_width = int(width / 3)
attr = self._get_attr(item, i) attr = self._get_attr(item, index)
stdscr.addstr(i, 0, name.ljust(col_width), attr) stdscr.addstr(row, 0, name.ljust(col_width), attr)
if type(item) is Symlink: if type(item) is Symlink:
attr_arrow = curses.color_pair(0) attr_arrow = curses.color_pair(0)
attr_target = curses.color_pair(Symlink.target_color(item.point_to)) attr_target = curses.color_pair(Symlink.target_color(item.point_to))
if i == self.app.cursor.pos: if index == self.app.navigator.cursor.pos:
attr_arrow |= curses.A_REVERSE attr_arrow |= curses.A_REVERSE
attr_target |= curses.A_REVERSE attr_target |= curses.A_REVERSE
stdscr.addstr(i, name_len, self.ARROW, attr_arrow) stdscr.addstr(row, name_len, self.ARROW, attr_arrow)
stdscr.addstr(i, name_len + self.ARROW_LEN, str(item.point_to), attr_target) stdscr.addstr(row, name_len + self.ARROW_LEN, str(item.point_to), attr_target)
stdscr.addstr(i, col_width, str(size).ljust(col_width * 2), attr) stdscr.addstr(row, col_width, str(size).ljust(col_width * 2), attr)
stdscr.addstr(i, col_width * 2, stat.filemode(perm), attr) stdscr.addstr(row, col_width * 2, stat.filemode(perm), attr)
@property @property
def app(self) -> App: def app(self) -> App:

View file

@ -1,8 +1,7 @@
from ..items.Folder import Folder
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 .Cursor import Cursor from .Navigator import Navigator
import curses import curses
@ -14,14 +13,9 @@ class App():
""" """
def __init__(self, path: Path) -> None: def __init__(self, path: Path) -> None:
self._history = [] self._navigator = Navigator(path)
self._items = []
self.history.append(path)
self._current = Folder(path)
self._items = self.current.children()
self._UI = UI(self) self._UI = UI(self)
self._input = Input(self) self._input = Input(self)
self._cursor = Cursor()
def _run(self, stdscr: curses.window) -> None: def _run(self, stdscr: curses.window) -> None:
curses.curs_set(0) curses.curs_set(0)
@ -31,17 +25,13 @@ class App():
curses.init_pair(2, curses.COLOR_BLUE, -1) curses.init_pair(2, curses.COLOR_BLUE, -1)
curses.init_pair(3, curses.COLOR_CYAN, -1) curses.init_pair(3, curses.COLOR_CYAN, -1)
curses.init_pair(4, curses.COLOR_YELLOW, -1) curses.init_pair(4, curses.COLOR_YELLOW, -1)
curses.init_pair(5, curses.COLOR_RED, -1)
stdscr.clear() stdscr.clear()
while (True): while (True):
try: self.navigator.refresh()
self._current = Folder(self.history[-1])
except FileNotFoundError:
pass
stdscr.addstr(0, 0, str(self.history[-1]))
self.UI.render(stdscr) self.UI.render(stdscr)
stdscr.refresh() stdscr.refresh()
key = stdscr.getkey() key = stdscr.getkey()
self._items = self.current.children()
if False is self.input.handle(key): if False is self.input.handle(key):
break break
@ -49,16 +39,8 @@ class App():
curses.wrapper(self._run) curses.wrapper(self._run)
@property @property
def history(self) -> list: def navigator(self) -> Navigator:
return self._history return self._navigator
@property
def items(self) -> list:
return self._items
@property
def current(self) -> Folder:
return self._current
@property @property
def UI(self) -> UI: def UI(self) -> UI:
@ -67,7 +49,3 @@ class App():
@property @property
def input(self) -> Input: def input(self) -> Input:
return self._input return self._input
@property
def cursor(self) -> Cursor:
return self._cursor

View file

@ -4,7 +4,7 @@ class Cursor:
self._pos = 0 self._pos = 0
def resete(self) -> None: def resete(self) -> None:
set._pos = 0 self._pos = 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

77
src/core/Navigator.py Normal file
View file

@ -0,0 +1,77 @@
from pathlib import Path
from ..items.AItems import AItem
from ..items.Folder import Folder
from ..items.Symlink import Symlink
from .Cursor import Cursor
class Navigator:
def __init__(self, path: Path) -> None:
self._history = [path]
self._cursor = Cursor()
self._current: Folder | None = None
self._items: list[AItem] = []
self._error: str | None = None
self._load()
def _load(self) -> None:
self._error = None
try:
self._current = Folder(self._history[-1])
self._items = self._current.children()
except FileNotFoundError:
self._items = []
except PermissionError:
self._items = []
self._error = f"Permission denied: {self._history[-1]}"
def refresh(self) -> None:
self._load()
def _navigate_to(self, path: Path) -> None:
self._history.append(path)
self._load()
self._cursor.resete()
def enter(self, item: AItem) -> bool:
if not item.is_navigable():
return False
path = item.point_to if isinstance(item, Symlink) else item.path
self._navigate_to(path)
return True
def up(self) -> bool:
parent = self._history[-1].parent
if parent == self._history[-1]:
return False
self._navigate_to(parent)
return True
def back(self) -> bool:
if len(self._history) <= 1:
return False
self._history.pop()
self._load()
self._cursor.resete()
return True
@property
def current(self) -> Folder:
return self._current
@property
def items(self) -> list[AItem]:
return self._items
@property
def history(self) -> list[Path]:
return self._history
@property
def cursor(self) -> Cursor:
return self._cursor
@property
def error(self) -> str | None:
return self._error

View file

@ -20,7 +20,7 @@ class Folder(AItem):
for entry in self.path.iterdir(): for entry in self.path.iterdir():
try: try:
items.append(make_item(entry)) items.append(make_item(entry))
except FileNotFoundError: except (FileNotFoundError, PermissionError):
pass pass
return items return items