feat: replace error banner and prompt line with centered modal system
This commit is contained in:
parent
4b721cb993
commit
51724a1aa6
4 changed files with 59 additions and 25 deletions
|
|
@ -37,6 +37,11 @@ class Input:
|
||||||
def _handle_prompt(self, key: str) -> bool:
|
def _handle_prompt(self, key: str) -> bool:
|
||||||
prompt = self.app.prompt
|
prompt = self.app.prompt
|
||||||
|
|
||||||
|
if prompt.kind == "error":
|
||||||
|
self.app.close_prompt()
|
||||||
|
prompt.on_submit(None)
|
||||||
|
return True
|
||||||
|
|
||||||
if prompt.kind == "confirm":
|
if prompt.kind == "confirm":
|
||||||
if key in ("y", "Y"):
|
if key in ("y", "Y"):
|
||||||
self.app.close_prompt()
|
self.app.close_prompt()
|
||||||
|
|
@ -68,19 +73,27 @@ class Input:
|
||||||
self.app.navigator.cursor.up()
|
self.app.navigator.cursor.up()
|
||||||
return True
|
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:
|
def _enter(self) -> bool:
|
||||||
navigator = self.app.navigator
|
navigator = self.app.navigator
|
||||||
items = navigator.items
|
items = navigator.items
|
||||||
if items:
|
if items:
|
||||||
navigator.enter(items[navigator.cursor.pos])
|
navigator.enter(items[navigator.cursor.pos])
|
||||||
|
self._maybe_show_error()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _back(self) -> bool:
|
def _back(self) -> bool:
|
||||||
self.app.navigator.back()
|
self.app.navigator.back()
|
||||||
|
self._maybe_show_error()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _parent(self) -> bool:
|
def _parent(self) -> bool:
|
||||||
self.app.navigator.up()
|
self.app.navigator.up()
|
||||||
|
self._maybe_show_error()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _sort(self) -> bool:
|
def _sort(self) -> bool:
|
||||||
|
|
@ -97,7 +110,7 @@ class Input:
|
||||||
self.app.file_actions.create_folder(navigator.current.path,
|
self.app.file_actions.create_folder(navigator.current.path,
|
||||||
name)
|
name)
|
||||||
except OSError as e:
|
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))
|
self.app.open_prompt(Prompt("New folder name: ", "text", on_submit))
|
||||||
return True
|
return True
|
||||||
|
|
@ -117,7 +130,7 @@ class Input:
|
||||||
try:
|
try:
|
||||||
self.app.file_actions.delete_folder(item.path)
|
self.app.file_actions.delete_folder(item.path)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
navigator.set_action_error(str(e))
|
self.app.open_prompt(Prompt.error(str(e)))
|
||||||
|
|
||||||
self.app.open_prompt(
|
self.app.open_prompt(
|
||||||
Prompt(f"Delete '{item.name}'? (y/n) ", "confirm", on_confirm))
|
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,
|
self.app.file_actions.create_file(navigator.current.path,
|
||||||
name)
|
name)
|
||||||
except OSError as e:
|
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))
|
self.app.open_prompt(Prompt("New file name: ", "text", on_submit))
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
51
src/IO/UI.py
51
src/IO/UI.py
|
|
@ -13,7 +13,7 @@ class UI:
|
||||||
def __init__(self, app: App):
|
def __init__(self, app: App):
|
||||||
self._app = app
|
self._app = app
|
||||||
|
|
||||||
HEADER_ROWS = 2
|
HEADER_ROWS = 1
|
||||||
|
|
||||||
def _get_attr(self, item: any, index: int) -> str:
|
def _get_attr(self, item: any, index: int) -> str:
|
||||||
pair = self.app.config.theme.pair_for(item.color_role)
|
pair = self.app.config.theme.pair_for(item.color_role)
|
||||||
|
|
@ -26,30 +26,53 @@ class UI:
|
||||||
stdscr.erase()
|
stdscr.erase()
|
||||||
height, width = stdscr.getmaxyx()
|
height, width = stdscr.getmaxyx()
|
||||||
navigator = self.app.navigator
|
navigator = self.app.navigator
|
||||||
prompt = self.app.prompt
|
|
||||||
|
|
||||||
stdscr.addstr(0, 0, str(navigator.history[-1])[:width - 1])
|
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
|
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))):
|
for index in range(min(available, len(items))):
|
||||||
item = items[index]
|
item = items[index]
|
||||||
self._render_item(stdscr, item, index, index + self.HEADER_ROWS, width)
|
self._render_item(stdscr, item, index, index + self.HEADER_ROWS, width)
|
||||||
|
|
||||||
|
prompt = self.app.prompt
|
||||||
if 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,
|
MODAL_MAX_WIDTH = 60
|
||||||
width: int) -> None:
|
MODAL_PADDING = 2
|
||||||
text = prompt.message
|
|
||||||
|
def _modal_lines(self, prompt) -> list[str]:
|
||||||
if prompt.kind == "text":
|
if prompt.kind == "text":
|
||||||
text += prompt.buffer
|
return [prompt.message + prompt.buffer]
|
||||||
stdscr.addstr(row, 0, text[:width - 1])
|
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")
|
SIZE_UNITS = ("", "K", "M", "G", "T", "P")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ class Navigator:
|
||||||
self._current: Folder | None = None
|
self._current: Folder | None = None
|
||||||
self._items: list[AItem] = []
|
self._items: list[AItem] = []
|
||||||
self._error: str | None = None
|
self._error: str | None = None
|
||||||
self._action_error: str | None = None
|
|
||||||
self._load()
|
self._load()
|
||||||
|
|
||||||
def _load(self) -> None:
|
def _load(self) -> None:
|
||||||
|
|
@ -45,12 +44,8 @@ class Navigator:
|
||||||
else:
|
else:
|
||||||
self._cursor.resete()
|
self._cursor.resete()
|
||||||
|
|
||||||
def set_action_error(self, message: str) -> None:
|
|
||||||
self._action_error = message
|
|
||||||
|
|
||||||
def _navigate_to(self, path: Path) -> None:
|
def _navigate_to(self, path: Path) -> None:
|
||||||
self._history.append(path)
|
self._history.append(path)
|
||||||
self._action_error = None
|
|
||||||
self._load()
|
self._load()
|
||||||
self._reset_cursor()
|
self._reset_cursor()
|
||||||
|
|
||||||
|
|
@ -72,7 +67,6 @@ class Navigator:
|
||||||
if len(self._history) <= 1:
|
if len(self._history) <= 1:
|
||||||
return False
|
return False
|
||||||
self._history.pop()
|
self._history.pop()
|
||||||
self._action_error = None
|
|
||||||
self._load()
|
self._load()
|
||||||
self._reset_cursor()
|
self._reset_cursor()
|
||||||
return True
|
return True
|
||||||
|
|
@ -95,4 +89,4 @@ class Navigator:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def error(self) -> str | None:
|
def error(self) -> str | None:
|
||||||
return self._action_error or self._error
|
return self._error
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,12 @@ from typing import Callable
|
||||||
|
|
||||||
class Prompt:
|
class Prompt:
|
||||||
def __init__(self, message: str, kind: str,
|
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.message = message
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.on_submit = on_submit
|
self.on_submit = on_submit
|
||||||
self.buffer = ""
|
self.buffer = ""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def error(cls, message: str) -> Prompt:
|
||||||
|
return cls(message, "error")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue