refactor: extract Navigator to own view state and add navigation
This commit is contained in:
parent
37b14de04d
commit
abd3e2e98d
6 changed files with 124 additions and 45 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
41
src/IO/UI.py
41
src/IO/UI.py
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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
77
src/core/Navigator.py
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue