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 curses import wrapper
|
||||
from .App import App
|
||||
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:
|
||||
curses.curs_set(0)
|
||||
curses.start_color()
|
||||
curses.use_default_colors()
|
||||
pyExp = App(path)
|
||||
pyExp.run()
|
||||
return 0
|
||||
|
||||
curses.init_pair(1, curses.COLOR_YELLOW, -1) # fg, bg
|
||||
curses.init_pair(2, curses.COLOR_CYAN, -1)
|
||||
curses.init_pair(3, curses.COLOR_GREEN, -1)
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
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