feat: implement item model and ncurses UI

This commit is contained in:
lohhiiccc 2026-06-30 22:27:29 +02:00
parent 934055205e
commit 25ec2bde43
8 changed files with 233 additions and 31 deletions

60
src/App.py Normal file
View 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
View 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

View file

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

View file

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

View file

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

View file

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

View file

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