From 51724a1aa6b278c38b018523e7e46f35cdb6e922 Mon Sep 17 00:00:00 2001 From: lohhiiccc <96543753+lohhiiccc@users.noreply.github.com> Date: Thu, 2 Jul 2026 20:04:04 +0200 Subject: [PATCH] feat: replace error banner and prompt line with centered modal system --- src/IO/Input.py | 19 +++++++++++++--- src/IO/UI.py | 51 +++++++++++++++++++++++++++++++------------ src/core/Navigator.py | 8 +------ src/core/Prompt.py | 6 ++++- 4 files changed, 59 insertions(+), 25 deletions(-) diff --git a/src/IO/Input.py b/src/IO/Input.py index 24b2a5b..bbac296 100644 --- a/src/IO/Input.py +++ b/src/IO/Input.py @@ -37,6 +37,11 @@ class Input: def _handle_prompt(self, key: str) -> bool: prompt = self.app.prompt + if prompt.kind == "error": + self.app.close_prompt() + prompt.on_submit(None) + return True + if prompt.kind == "confirm": if key in ("y", "Y"): self.app.close_prompt() @@ -68,19 +73,27 @@ class Input: self.app.navigator.cursor.up() return True + def _maybe_show_error(self) -> None: + error = self.app.navigator.error + if error: + self.app.open_prompt(Prompt.error(error)) + def _enter(self) -> bool: navigator = self.app.navigator items = navigator.items if items: navigator.enter(items[navigator.cursor.pos]) + self._maybe_show_error() return True def _back(self) -> bool: self.app.navigator.back() + self._maybe_show_error() return True def _parent(self) -> bool: self.app.navigator.up() + self._maybe_show_error() return True def _sort(self) -> bool: @@ -97,7 +110,7 @@ class Input: self.app.file_actions.create_folder(navigator.current.path, name) except OSError as e: - navigator.set_action_error(str(e)) + self.app.open_prompt(Prompt.error(str(e))) self.app.open_prompt(Prompt("New folder name: ", "text", on_submit)) return True @@ -117,7 +130,7 @@ class Input: try: self.app.file_actions.delete_folder(item.path) except OSError as e: - navigator.set_action_error(str(e)) + self.app.open_prompt(Prompt.error(str(e))) self.app.open_prompt( Prompt(f"Delete '{item.name}'? (y/n) ", "confirm", on_confirm)) @@ -133,7 +146,7 @@ class Input: self.app.file_actions.create_file(navigator.current.path, name) except OSError as e: - navigator.set_action_error(str(e)) + self.app.open_prompt(Prompt.error(str(e))) self.app.open_prompt(Prompt("New file name: ", "text", on_submit)) return True diff --git a/src/IO/UI.py b/src/IO/UI.py index 368b967..9fb5b68 100644 --- a/src/IO/UI.py +++ b/src/IO/UI.py @@ -13,7 +13,7 @@ class UI: def __init__(self, app: App): self._app = app - HEADER_ROWS = 2 + HEADER_ROWS = 1 def _get_attr(self, item: any, index: int) -> str: pair = self.app.config.theme.pair_for(item.color_role) @@ -26,30 +26,53 @@ class UI: stdscr.erase() height, width = stdscr.getmaxyx() navigator = self.app.navigator - prompt = self.app.prompt stdscr.addstr(0, 0, str(navigator.history[-1])[:width - 1]) - if navigator.error: - error_pair = self.app.config.theme.pair_for("error") - stdscr.addstr(1, 0, navigator.error[:width - 1], - curses.color_pair(error_pair)) - footer_rows = 1 if prompt else 0 items = navigator.items - available = max(0, height - self.HEADER_ROWS - footer_rows) + 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_prompt(stdscr, prompt, height - 1, width) + self._render_modal(stdscr, prompt, height, width) - def _render_prompt(self, stdscr: curses.window, prompt, row: int, - width: int) -> None: - text = prompt.message + MODAL_MAX_WIDTH = 60 + MODAL_PADDING = 2 + + def _modal_lines(self, prompt) -> list[str]: if prompt.kind == "text": - text += prompt.buffer - stdscr.addstr(row, 0, text[:width - 1]) + 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") diff --git a/src/core/Navigator.py b/src/core/Navigator.py index 993f522..8d446e3 100644 --- a/src/core/Navigator.py +++ b/src/core/Navigator.py @@ -17,7 +17,6 @@ class Navigator: self._current: Folder | None = None self._items: list[AItem] = [] self._error: str | None = None - self._action_error: str | None = None self._load() def _load(self) -> None: @@ -45,12 +44,8 @@ class Navigator: else: self._cursor.resete() - def set_action_error(self, message: str) -> None: - self._action_error = message - def _navigate_to(self, path: Path) -> None: self._history.append(path) - self._action_error = None self._load() self._reset_cursor() @@ -72,7 +67,6 @@ class Navigator: if len(self._history) <= 1: return False self._history.pop() - self._action_error = None self._load() self._reset_cursor() return True @@ -95,4 +89,4 @@ class Navigator: @property def error(self) -> str | None: - return self._action_error or self._error + return self._error diff --git a/src/core/Prompt.py b/src/core/Prompt.py index bb121f5..b50b9d4 100644 --- a/src/core/Prompt.py +++ b/src/core/Prompt.py @@ -4,8 +4,12 @@ from typing import Callable class Prompt: def __init__(self, message: str, kind: str, - on_submit: Callable[[object], None]) -> None: + on_submit: Callable[[object], None] = lambda _: None) -> None: self.message = message self.kind = kind self.on_submit = on_submit self.buffer = "" + + @classmethod + def error(cls, message: str) -> Prompt: + return cls(message, "error")