feat: replace error banner and prompt line with centered modal system

This commit is contained in:
lohhiiccc 2026-07-02 20:04:04 +02:00
parent 4b721cb993
commit 51724a1aa6
4 changed files with 59 additions and 25 deletions

View file

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

View file

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

View file

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

View file

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