116 lines
4 KiB
Python
116 lines
4 KiB
Python
from __future__ import annotations
|
|
import stat
|
|
from typing import TYPE_CHECKING
|
|
import curses
|
|
from ..items.Symlink import Symlink
|
|
from ..items.AItems import AItem
|
|
|
|
if TYPE_CHECKING:
|
|
from .App import App
|
|
|
|
|
|
class UI:
|
|
def __init__(self, app: App):
|
|
self._app = app
|
|
|
|
HEADER_ROWS = 1
|
|
|
|
def _get_attr(self, item: any, index: int) -> str:
|
|
pair = self.app.config.theme.pair_for(item.color_role)
|
|
attr = curses.color_pair(pair)
|
|
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()
|
|
navigator = self.app.navigator
|
|
|
|
stdscr.addstr(0, 0, str(navigator.history[-1])[:width - 1])
|
|
|
|
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)
|
|
|
|
prompt = self.app.prompt
|
|
if prompt:
|
|
self._render_modal(stdscr, prompt, height, width)
|
|
|
|
MODAL_MAX_WIDTH = 60
|
|
MODAL_PADDING = 2
|
|
|
|
def _modal_lines(self, prompt) -> list[str]:
|
|
if prompt.kind == "text":
|
|
return [prompt.message + prompt.buffer]
|
|
if prompt.kind == "confirm":
|
|
return [prompt.message]
|
|
return [prompt.message, "(press any key)"]
|
|
|
|
def _render_modal(self, stdscr: curses.window, prompt,
|
|
screen_height: int, screen_width: int) -> None:
|
|
lines = self._modal_lines(prompt)
|
|
max_line = max(len(line) for line in lines)
|
|
inner_width = min(max_line, self.MODAL_MAX_WIDTH)
|
|
box_width = min(inner_width + 2 * self.MODAL_PADDING + 2,
|
|
screen_width - 2)
|
|
inner_width = box_width - 2 * self.MODAL_PADDING - 2
|
|
box_height = len(lines) + 2
|
|
|
|
top = max(0, (screen_height - box_height) // 2)
|
|
left = max(0, (screen_width - box_width) // 2)
|
|
|
|
attr = curses.A_NORMAL
|
|
if prompt.kind == "error":
|
|
attr = curses.color_pair(self.app.config.theme.pair_for("error"))
|
|
|
|
border = "+" + "-" * (box_width - 2) + "+"
|
|
pad = " " * self.MODAL_PADDING
|
|
stdscr.addstr(top, left, border, attr)
|
|
for i, line in enumerate(lines):
|
|
text = line[:inner_width].ljust(inner_width)
|
|
stdscr.addstr(top + 1 + i, left, "|" + pad + text + pad + "|", attr)
|
|
stdscr.addstr(top + box_height - 1, left, border, attr)
|
|
|
|
SIZE_UNITS = ("", "K", "M", "G", "T", "P")
|
|
|
|
@staticmethod
|
|
def _format_size(size: int) -> str:
|
|
value = float(size)
|
|
for unit in UI.SIZE_UNITS:
|
|
if value < 1024:
|
|
return str(int(value)) if unit == "" else f"{value:.1f}{unit}"
|
|
value /= 1024
|
|
return f"{value:.1f}E"
|
|
|
|
ARROW = " -> "
|
|
ARROW_LEN = len(ARROW)
|
|
|
|
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, index)
|
|
stdscr.addstr(row, 0, name.ljust(col_width), attr)
|
|
|
|
if type(item) is Symlink:
|
|
target_pair = self.app.config.theme.pair_for(
|
|
Symlink.target_role(item.point_to))
|
|
attr_arrow = curses.color_pair(0)
|
|
attr_target = curses.color_pair(target_pair)
|
|
if index == self.app.navigator.cursor.pos:
|
|
attr_arrow |= curses.A_REVERSE
|
|
attr_target |= curses.A_REVERSE
|
|
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(row, col_width, self._format_size(size).ljust(col_width * 2), attr)
|
|
stdscr.addstr(row, col_width * 2, stat.filemode(perm), attr)
|
|
|
|
@property
|
|
def app(self) -> App:
|
|
return self._app
|