Source code for Karana.KUtils.MultibodyTUI.dialog

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Generic, TypeVar, Any
from collections.abc import Callable

from . import terminal
from .base import TUIBase
from . import constants as c


[docs] class EntryDialog: """Base class for a user text input TUI""" def __init__(self, prompt: list[str] | None = None, entry: str = ""): """EntryDialog constructor Parameters ---------- prompt: list[str] Static prompt text displayed above the entry line entry: str = "" Initial value of the entry text """ if prompt is None: prompt = [] self.prompt = prompt self.entry = entry
[docs] def run(self) -> str | None: """Run the TUI This blocks until the user entry is confirmed or canceled """ with terminal.tui_mode(on_resume=self.redraw): try: while True: self.redraw() while not (key := terminal.poll_key()): pass if key in ("\r", "\n"): if self.is_valid(): return self.entry elif key in ("\x7f", "\x08"): self.entry = self.entry[:-1] elif key.isprintable(): self.entry += key except KeyboardInterrupt: return None
[docs] def is_valid(self) -> bool: return True
[docs] def footer(self) -> list[str]: return []
[docs] def redraw(self): terminal.clear_screen() terminal.print_lines(self.prompt) entry_str = terminal.invert(self.entry) lines = [f" > {entry_str} ", ""] lines.extend(self.footer()) terminal.print_lines(lines)
[docs] class SearchDialog(EntryDialog): def __init__(self, universe: list[str], prompt=list[str] | None): self.universe = universe super().__init__(prompt=prompt)
[docs] def matches(self) -> list[str]: if not self.entry: return [] return [el for el in self.universe if self.entry in el]
[docs] def is_valid(self) -> bool: return bool(self.matches())
[docs] @staticmethod def bold_match(string: str, substr: str) -> str: if not substr: return string start = string.find(substr) if start == -1: return string end = start + len(substr) return string[:start] + terminal.bold(string[start:end]) + string[end:]
[docs] def footer(self) -> list[str]: if not self.entry: return ["Please type a search term (Ctrl-C to abort)"] matches = self.matches() if not matches: return ["No matches found (Ctrl-C to abort)"] max_display = 8 if len(matches) == 1: lines = ["1 match:"] else: lines = [f"{len(matches)} matches:"] displayed_matches = matches[:max_display] for match in displayed_matches: formatted = self.bold_match(match, self.entry) lines.append(f" {formatted}") remaining_matches = len(matches) - max_display if remaining_matches > 0: lines.append(f" ...{remaining_matches} more") lines.extend(["", "Press Enter to confirm"]) return lines
[docs] @dataclass class FlagData: brief: str enabled: bool
[docs] def toggle(self): self.enabled = not self.enabled
[docs] def __bool__(self): return self.enabled
[docs] class FlagsTUI(TUIBase): def __init__(self, header: list[str], flags: list[FlagData]): assert flags self._header = header self._flags = flags self._index = 0 self._shortcuts = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"] self._confirmed = False super().__init__() @property def confirmed(self): return self._confirmed
[docs] def redraw(self): lines = self._header[:] lines.append("") for i, flag in enumerate(self._flags): enabled_str = "*" if flag.enabled else " " shortcut_str = " " if i < len(self._shortcuts): shortcut_str = f"({self._shortcuts[i]})" line = f"{shortcut_str} [{enabled_str}] {flag.brief}" if i == self._index: line = terminal.invert(line) lines.append(line) terminal.clear_screen() terminal.print_lines(lines)
def _prev(self): self._index = (self._index - 1) % len(self._flags) def _next(self): self._index = (self._index + 1) % len(self._flags) def _post_update(self, flag: FlagData): pass def _toggle_current(self): flag = self._flags[self._index] flag.toggle() self._post_update(flag) def _toggle_shortcut(self, shortcut: str): try: index = self._shortcuts.index(shortcut) flag = self._flags[index] flag.toggle() self._post_update(flag) except IndexError: pass
[docs] def handle_key(self, key): if key == "q": return False elif key in ["\r", "\n"]: self._confirmed = True return False elif key in ["j", c.KEY_DOWN]: self._next() elif key in ["k", c.KEY_UP]: self._prev() elif key == " ": self._toggle_current() elif key in self._shortcuts: self._toggle_shortcut(key) return True
[docs] @dataclass class ChoiceData: brief: str value: Any description: str = ""
[docs] class ChoiceTUI(TUIBase): _cancel_choice = ChoiceData(brief="Cancel", value=None) def __init__(self, header: list[str], choices: list[ChoiceData], index: int = 0): assert choices choices = choices + [self._cancel_choice] self._header = header self._choices = choices self._index = index self._shortcuts = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"] self._confirmed = False super().__init__() @property def choice(self) -> ChoiceData | None: if not self._confirmed: return None return self._choices[self._index]
[docs] def redraw(self): lines = self._header[:] lines.append("") for i, choice in enumerate(self._choices): shortcut_str = " " if i < len(self._shortcuts): shortcut_str = f"({self._shortcuts[i]})" line = f"{shortcut_str} {choice.brief}" if i == self._index: line = terminal.invert(line) lines.append(line) lines.append("") choice = self._choices[self._index] lines.append(choice.description) terminal.clear_screen() terminal.print_lines(lines)
def _prev(self): self._index = (self._index - 1) % len(self._choices) self._post_update() def _next(self): self._index = (self._index + 1) % len(self._choices) self._post_update() def _select_current(self): choice = self._choices[self._index] if choice != self._cancel_choice: self._confirmed = True def _post_update(self): pass def _jump_shortcut(self, shortcut: str): try: index = self._shortcuts.index(shortcut) # Trigger an IndexError if not in bounds self._choices[index] self._index = index self._post_update() except IndexError: pass
[docs] def handle_key(self, key): if key == "q": return False elif key in ["\r", "\n"]: self._select_current() return False elif key in ["j", c.KEY_DOWN]: self._next() elif key in ["k", c.KEY_UP]: self._prev() elif key in self._shortcuts: self._jump_shortcut(key) return True