diff --git a/src/IO/Input.py b/src/IO/Input.py index c304058..0146576 100644 --- a/src/IO/Input.py +++ b/src/IO/Input.py @@ -10,12 +10,21 @@ class Input: self._app = app def handle(self, key: str) -> bool: + navigator = self.app.navigator if 'q' == key: return False 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'): - 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 @property diff --git a/src/IO/UI.py b/src/IO/UI.py index 7076e05..57e8b21 100644 --- a/src/IO/UI.py +++ b/src/IO/UI.py @@ -13,39 +13,54 @@ class UI: def __init__(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) - if i == self.app.cursor.pos: + if index == self.app.navigator.cursor.pos: attr |= curses.A_REVERSE return attr def render(self, stdscr: curses.window) -> None: + stdscr.erase() height, width = stdscr.getmaxyx() - for i in range(min(height, len(self.app.items))): - item = self.app.items[i] - self._render_item(stdscr, item, i, width) + navigator = self.app.navigator + + 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_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_len = len(name) col_width = int(width / 3) - attr = self._get_attr(item, i) - stdscr.addstr(i, 0, name.ljust(col_width), attr) + attr = self._get_attr(item, index) + stdscr.addstr(row, 0, name.ljust(col_width), attr) if type(item) is Symlink: attr_arrow = curses.color_pair(0) 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_target |= curses.A_REVERSE - stdscr.addstr(i, 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, attr_arrow) + 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(i, col_width * 2, stat.filemode(perm), attr) + stdscr.addstr(row, col_width, str(size).ljust(col_width * 2), attr) + stdscr.addstr(row, col_width * 2, stat.filemode(perm), attr) @property def app(self) -> App: diff --git a/src/core/App.py b/src/core/App.py index a06b509..590dd79 100644 --- a/src/core/App.py +++ b/src/core/App.py @@ -1,8 +1,7 @@ -from ..items.Folder import Folder from pathlib import Path from ..IO.UI import UI from ..IO.Input import Input -from .Cursor import Cursor +from .Navigator import Navigator import curses @@ -14,14 +13,9 @@ class App(): """ def __init__(self, path: Path) -> None: - self._history = [] - self._items = [] - self.history.append(path) - self._current = Folder(path) - self._items = self.current.children() + self._navigator = Navigator(path) self._UI = UI(self) self._input = Input(self) - self._cursor = Cursor() def _run(self, stdscr: curses.window) -> None: curses.curs_set(0) @@ -31,17 +25,13 @@ class App(): curses.init_pair(2, curses.COLOR_BLUE, -1) curses.init_pair(3, curses.COLOR_CYAN, -1) curses.init_pair(4, curses.COLOR_YELLOW, -1) + curses.init_pair(5, curses.COLOR_RED, -1) stdscr.clear() while (True): - try: - self._current = Folder(self.history[-1]) - except FileNotFoundError: - pass - stdscr.addstr(0, 0, str(self.history[-1])) + self.navigator.refresh() self.UI.render(stdscr) stdscr.refresh() key = stdscr.getkey() - self._items = self.current.children() if False is self.input.handle(key): break @@ -49,16 +39,8 @@ class App(): curses.wrapper(self._run) @property - def history(self) -> list: - return self._history - - @property - def items(self) -> list: - return self._items - - @property - def current(self) -> Folder: - return self._current + def navigator(self) -> Navigator: + return self._navigator @property def UI(self) -> UI: @@ -67,7 +49,3 @@ class App(): @property def input(self) -> Input: return self._input - - @property - def cursor(self) -> Cursor: - return self._cursor diff --git a/src/core/Cursor.py b/src/core/Cursor.py index c79d7a6..8c1fc4f 100644 --- a/src/core/Cursor.py +++ b/src/core/Cursor.py @@ -4,7 +4,7 @@ class Cursor: self._pos = 0 def resete(self) -> None: - set._pos = 0 + self._pos = 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 new file mode 100644 index 0000000..88d9499 --- /dev/null +++ b/src/core/Navigator.py @@ -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 diff --git a/src/items/Folder.py b/src/items/Folder.py index 2f6bb71..414494e 100644 --- a/src/items/Folder.py +++ b/src/items/Folder.py @@ -20,7 +20,7 @@ class Folder(AItem): for entry in self.path.iterdir(): try: items.append(make_item(entry)) - except FileNotFoundError: + except (FileNotFoundError, PermissionError): pass return items