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