# 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 dataclasses import dataclass
from typing import 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