"""Classes for controlling dialog in TUIs."""
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 = ""):
"""Create an EntryDialog instance.
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.isValid():
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 isValid(self) -> bool:
"""Check if the user entered content is valid.
Returns
-------
bool
Always returns True.
"""
return True
[docs]
def redraw(self):
"""Redraw the TUI."""
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):
"""Diaglog for searching."""
def __init__(self, universe: list[str], prompt=list[str] | None):
"""Create an instance of SearchDialog.
Parameters
----------
prompt : list[str] | None
The search prompt.
universe : list[str]
The list of possible things to search for.
"""
self.universe = universe
super().__init__(prompt=prompt)
[docs]
def matches(self) -> list[str]:
"""Return a list of matches.
Returns
-------
list[str]
A list of matches from the users search.
"""
if not self.entry:
return []
return [el for el in self.universe if self.entry in el]
[docs]
def isValid(self) -> bool:
"""Check if matches were found.
Returns
-------
bool
True if matches were found, False otherwise.
"""
return bool(self.matches())
[docs]
@staticmethod
def boldMatch(string: str, substr: str) -> str:
"""Make part of a string bold.
Parameters
----------
string : str
The full string.
substr : str
The substring to make bold.
Returns
-------
str
A string with the substr bolded.
"""
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]
@dataclass
class FlagData:
"""Simple class to track the value of a flag."""
brief: str
enabled: bool
[docs]
def toggle(self):
"""Toggle the flag."""
self.enabled = not self.enabled
[docs]
def __bool__(self):
"""Return true if the flag is true.
Returns
-------
bool:
True if the flag is True, False otherwise.
"""
return self.enabled
[docs]
class FlagsTUI(TUIBase):
"""TUI for controlling a set of flags."""
def __init__(self, header: list[str], flags: list[FlagData]):
"""Create an instance of FlagsTUI.
Parameters
----------
header : list[str]
The headers to put before the flags.
flags : list[FlagData]
The the flags to control.
"""
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 true if confirmed.
Returns
-------
bool
True if confirmed, False otherwise.
"""
return self._confirmed
[docs]
def redraw(self):
"""Redraw the TUI."""
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 _postUpdate(self, flag: FlagData):
pass
def _toggleCurrent(self):
flag = self._flags[self._index]
flag.toggle()
self._postUpdate(flag)
def _toggleShortcut(self, shortcut: str):
try:
index = self._shortcuts.index(shortcut)
flag = self._flags[index]
flag.toggle()
self._postUpdate(flag)
except IndexError:
pass
[docs]
def handleKey(self, key):
"""Handle user key presses.
Parameters
----------
key :
The key pressed.
Returns
-------
bool
True to continue with this TUI, False to exit.
"""
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._toggleCurrent()
elif key in self._shortcuts:
self._toggleShortcut(key)
return True
[docs]
@dataclass
class ChoiceData:
"""Data associated with a ChoiceTUI."""
brief: str
value: Any
description: str = ""
[docs]
class ChoiceTUI(TUIBase):
"""A TUI for controlling choices."""
_cancel_choice = ChoiceData(brief="Cancel", value=None)
def __init__(self, header: list[str], choices: list[ChoiceData], index: int = 0):
"""Create an instance of ChoiceTUI.
Parameters
----------
header : list[str]
The strings to list before the coices.
choices : list[ChoiceData]
The choices.
index : int
The currently selected choice.
"""
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:
"""The currently selected choice.
Returns
-------
ChoiceData | None
The currently selected choice.
"""
if not self._confirmed:
return None
return self._choices[self._index]
[docs]
def redraw(self):
"""Redraw the TUI."""
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._postUpdate()
def _next(self):
self._index = (self._index + 1) % len(self._choices)
self._postUpdate()
def _selectCurrent(self):
choice = self._choices[self._index]
if choice != self._cancel_choice:
self._confirmed = True
def _postUpdate(self):
pass
def _jumpShortcut(self, shortcut: str):
try:
index = self._shortcuts.index(shortcut)
# Trigger an IndexError if not in bounds
self._choices[index]
self._index = index
self._postUpdate()
except IndexError:
pass
[docs]
def handleKey(self, key):
"""Handle user key presses.
Parameters
----------
key :
The key pressed.
Returns
-------
bool
True to continue with this TUI, False to exit.
"""
if key == "q":
return False
elif key in ["\r", "\n"]:
self._selectCurrent()
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._jumpShortcut(key)
return True