# Copyright (c) 2024-2026 Karana Dynamics Pty Ltd. All rights reserved.
#
# NOTICE TO USER:
#
# This source code and/or documentation (the "Licensed Materials") is
# the confidential and proprietary information of Karana Dynamics Inc.
# Use of these Licensed Materials is governed by the terms and conditions
# of a separate software license agreement between Karana Dynamics and the
# Licensee ("License Agreement"). Unless expressly permitted under that
# agreement, any reproduction, modification, distribution, or disclosure
# of the Licensed Materials, in whole or in part, to any third party
# without the prior written consent of Karana Dynamics is strictly prohibited.
#
# THE LICENSED MATERIALS ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
# KARANA DYNAMICS DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
# BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, AND
# FITNESS FOR A PARTICULAR PURPOSE.
#
# IN NO EVENT SHALL KARANA DYNAMICS BE LIABLE FOR ANY DAMAGES WHATSOEVER,
# INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS, DATA, OR USE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, WHETHER IN CONTRACT, TORT,
# OR OTHERWISE ARISING OUT OF OR IN CONNECTION WITH THE LICENSED MATERIALS.
#
# U.S. Government End Users: The Licensed Materials are a "commercial item"
# as defined at 48 C.F.R. 2.101, and are provided to the U.S. Government
# only as a commercial end item under the terms of this license.
#
# Any use of the Licensed Materials in individual or commercial software must
# include, in the user documentation and internal source code comments,
# this Notice, Disclaimer, and U.S. Government Use Provision.
"""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.tuiMode(on_resume=self.redraw):
try:
while True:
self.redraw()
while not (key := terminal.pollKey()):
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.clearScreen()
terminal.printLines(self.prompt)
entry_str = terminal.invert(self.entry)
lines = [f" > {entry_str} ", ""]
lines.extend(self.footer())
terminal.printLines(lines)
[docs]
class SearchDialog(EntryDialog):
"""Dialog 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.clearScreen()
terminal.printLines(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.clearScreen()
terminal.printLines(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