feat: implement item model and ncurses UI
This commit is contained in:
parent
934055205e
commit
25ec2bde43
8 changed files with 233 additions and 31 deletions
60
src/App.py
Normal file
60
src/App.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
from .items.Folder import Folder
|
||||||
|
from pathlib import Path
|
||||||
|
from .UI import UI
|
||||||
|
import curses
|
||||||
|
|
||||||
|
|
||||||
|
class App():
|
||||||
|
"""
|
||||||
|
Raise:
|
||||||
|
FileNotFoundError can append in __init__
|
||||||
|
"""
|
||||||
|
def __init__(self, path: Path) -> None:
|
||||||
|
self._history = []
|
||||||
|
self._items = []
|
||||||
|
self.history.append(path)
|
||||||
|
self._current = Folder(path)
|
||||||
|
self._items = self.current.children()
|
||||||
|
self._UI = UI(self)
|
||||||
|
|
||||||
|
def _run(self, stdscr: curses.window) -> None:
|
||||||
|
curses.curs_set(0)
|
||||||
|
curses.start_color()
|
||||||
|
curses.use_default_colors()
|
||||||
|
curses.init_pair(1, curses.COLOR_WHITE, -1)
|
||||||
|
curses.init_pair(2, curses.COLOR_BLUE, -1)
|
||||||
|
curses.init_pair(3, curses.COLOR_CYAN, -1)
|
||||||
|
curses.init_pair(4, curses.COLOR_YELLOW, -1)
|
||||||
|
stdscr.clear()
|
||||||
|
while (True):
|
||||||
|
try:
|
||||||
|
self._current = Folder(self.history[-1])
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass # doubt
|
||||||
|
stdscr.addstr(0, 0, str(self.history[-1]))
|
||||||
|
self.UI.render(stdscr)
|
||||||
|
stdscr.refresh()
|
||||||
|
key = stdscr.getkey()
|
||||||
|
self._items = self.current.children()
|
||||||
|
if 'q' == key:
|
||||||
|
break
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
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
|
||||||
|
|
||||||
|
@property
|
||||||
|
def UI(self) -> UI:
|
||||||
|
return self._UI
|
||||||
|
|
||||||
36
src/UI.py
Normal file
36
src/UI.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import stat
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
import curses
|
||||||
|
from .items.Symlink import Symlink
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .App import App
|
||||||
|
|
||||||
|
|
||||||
|
class UI:
|
||||||
|
def __init__(self, app: App):
|
||||||
|
self._app = app
|
||||||
|
|
||||||
|
def render(self, stdscr: curses.window):
|
||||||
|
height, width = stdscr.getmaxyx()
|
||||||
|
for i in range(min(height, len(self.app.items))):
|
||||||
|
item = self.app.items[i]
|
||||||
|
name, size, perm = item.fields()
|
||||||
|
|
||||||
|
stdscr.attron(curses.color_pair(item.color_pair))
|
||||||
|
stdscr.addstr(i, 0, name)
|
||||||
|
|
||||||
|
if type(item) is Symlink:
|
||||||
|
stdscr.attron(curses.color_pair(0))
|
||||||
|
stdscr.addstr(i, len(name), " -> ")
|
||||||
|
stdscr.attron(curses.color_pair(Symlink.target_color(item.point_to)))
|
||||||
|
stdscr.addstr(i, len(name) + 4, str(item.point_to))
|
||||||
|
|
||||||
|
stdscr.addstr(i, int(width / 3), str(size))
|
||||||
|
stdscr.addstr(i, int(width / 3) * 2, stat.filemode(perm))
|
||||||
|
stdscr.attroff(curses.color_pair(item.color_pair))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def app(self) -> App:
|
||||||
|
return self._app
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class AItem(ABC):
|
||||||
|
"""
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: if the given path does not exist on the FS
|
||||||
|
PermissionError: if the process lacks permission to read the file metadata
|
||||||
|
"""
|
||||||
|
def __init__(self, path: Path) -> None:
|
||||||
|
self._path = path
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
COLOR = 0
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
self._stat = self._path.lstat()
|
||||||
|
self._permissions = self._stat.st_mode
|
||||||
|
|
||||||
|
def fields(self) -> tuple[str, int, int]:
|
||||||
|
return (self.name, self.size, self.permissions)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.name}, {self.size}, {self.permissions}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self) -> Path:
|
||||||
|
return self._path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self._path.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self) -> int:
|
||||||
|
return self._stat.st_size
|
||||||
|
|
||||||
|
@property
|
||||||
|
def permissions(self) -> int:
|
||||||
|
return self._permissions
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_pair(self) -> int:
|
||||||
|
return self.COLOR
|
||||||
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from .AItems import AItem
|
||||||
|
|
||||||
|
|
||||||
|
class File(AItem):
|
||||||
|
COLOR = 1
|
||||||
|
|
||||||
|
def __init__(self, path: Path) -> None:
|
||||||
|
super().__init__(path)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from .AItems import AItem
|
||||||
|
|
||||||
|
|
||||||
|
class Folder(AItem):
|
||||||
|
COLOR = 2
|
||||||
|
|
||||||
|
def __init__(self, path: Path) -> None:
|
||||||
|
super().__init__(path)
|
||||||
|
|
||||||
|
def fields(self) -> tuple[str, int, int]:
|
||||||
|
return (f"{self.name}/", self.size, self.permissions)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.name}/, {self.size}, {self.permissions}"
|
||||||
|
|
||||||
|
def children(self) -> list[AItem]:
|
||||||
|
from .factory import make_item
|
||||||
|
items: list[AItem] = []
|
||||||
|
for entry in self.path.iterdir():
|
||||||
|
try:
|
||||||
|
items.append(make_item(entry))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from .AItems import AItem
|
||||||
|
from .File import File
|
||||||
|
from .Folder import Folder
|
||||||
|
|
||||||
|
class Symlink(AItem):
|
||||||
|
COLOR = 3
|
||||||
|
def __init__(self, path: Path) -> None:
|
||||||
|
super().__init__(path)
|
||||||
|
self._point_to = path.resolve()
|
||||||
|
if False == self._point_to.exists():
|
||||||
|
self._point_to = None
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.name} -> {'(broken)' if None == self.point_to else self.point_to.name}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def target_color(point_to: Path | None) -> int:
|
||||||
|
if point_to is None:
|
||||||
|
return 5
|
||||||
|
elif point_to.is_dir():
|
||||||
|
return Folder.COLOR
|
||||||
|
else:
|
||||||
|
return File.COLOR
|
||||||
|
|
||||||
|
@property
|
||||||
|
def point_to(self) -> Path:
|
||||||
|
return self._point_to
|
||||||
12
src/items/factory.py
Normal file
12
src/items/factory.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
from .AItems import AItem
|
||||||
|
from .File import File
|
||||||
|
from .Folder import Folder
|
||||||
|
from .Symlink import Symlink
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def make_item(path: Path) -> AItem:
|
||||||
|
if path.is_symlink():
|
||||||
|
return Symlink(path)
|
||||||
|
if path.is_file():
|
||||||
|
return File(path)
|
||||||
|
return Folder(path)
|
||||||
46
src/main.py
46
src/main.py
|
|
@ -1,35 +1,19 @@
|
||||||
import curses
|
from .App import App
|
||||||
from curses import wrapper
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
path: None | Path = None
|
||||||
|
try:
|
||||||
|
path = Path(sys.argv[1])
|
||||||
|
except IndexError:
|
||||||
|
print(f"Usage: {sys.argv[0]} <path>")
|
||||||
|
return 1
|
||||||
|
|
||||||
def init_curse() -> None:
|
pyExp = App(path)
|
||||||
curses.curs_set(0)
|
pyExp.run()
|
||||||
curses.start_color()
|
return 0
|
||||||
curses.use_default_colors()
|
|
||||||
|
|
||||||
curses.init_pair(1, curses.COLOR_YELLOW, -1) # fg, bg
|
if __name__ == "__main__":
|
||||||
curses.init_pair(2, curses.COLOR_CYAN, -1)
|
main()
|
||||||
curses.init_pair(3, curses.COLOR_GREEN, -1)
|
|
||||||
|
|
||||||
|
|
||||||
def print_folder(stdscr: curses.window, path: str) -> None:
|
|
||||||
stdscr.clear()
|
|
||||||
|
|
||||||
stdscr.attron(curses.color_pair(1))
|
|
||||||
stdscr.addstr(0, 1, path)
|
|
||||||
stdscr.attroff(curses.color_pair(1))
|
|
||||||
|
|
||||||
for i in range(1, 9):
|
|
||||||
stdscr.attron(curses.color_pair(2))
|
|
||||||
stdscr.addstr(i, 0, " f - a")
|
|
||||||
stdscr.attroff(curses.color_pair(2))
|
|
||||||
|
|
||||||
|
|
||||||
def main(stdscr):
|
|
||||||
init_curse()
|
|
||||||
print_folder(stdscr, "/tmp")
|
|
||||||
stdscr.refresh()
|
|
||||||
stdscr.getkey()
|
|
||||||
|
|
||||||
|
|
||||||
wrapper(main)
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue