Source code for Karana.KUtils.MultibodyTUI.terminal
# Copyright (c) 2024-2025 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.
from contextlib import contextmanager
import sys
import select
import os
import subprocess
import termios
import signal
import tty
from collections.abc import Callable
[docs]
def bold(text: str) -> str:
"""Apply the bold effect
Parameters
----------
text: str
The given text
Returns
-------
str
The bolded text
"""
return f"\033[1m{text}\033[0m"
[docs]
def invert(text: str) -> str:
"""Swap background and foreground colors
Parameters
----------
text: str
The given text
Returns
-------
str
The inverted text
"""
return f"\033[7m{text}\033[0m"
[docs]
def critical(text: str) -> str:
"""Style the text for a critical error
Parameters
----------
text: str
The given text
Returns
-------
str
The stylized text
"""
# Bold, inverted background and foreground
return f"\033[1;7;31m{text}\033[0m"
[docs]
def error(text: str) -> str:
"""Style the text for an error
Parameters
----------
text: str
The given text
Returns
-------
str
The stylized text
"""
# Bold red
return f"\033[1;31m{text}\033[0m"
[docs]
def warn(text: str) -> str:
"""Style the text for a warning
Parameters
----------
text: str
The given text
Returns
-------
str
The stylized text
"""
# Yellow
return f"\033[33m{text}\033[0m"
[docs]
def info(text: str) -> str:
"""Style the text for important info
Parameters
----------
text: str
The given text
Returns
-------
str
The stylized text
"""
# Cyan
return f"\033[36m{text}\033[0m"
[docs]
@contextmanager
def tui_mode(on_resume: Callable | None = None):
"""Context manager to configure the terminal for a TUI
This primarily handles setting cbreak mode for immediate key handling and
restoring the previous mode upon exiting the context. Multiple tui_mode
contexts may be nested.
Parameters
----------
on_resume: collections.abc.Callable | None
Optional callable triggered upon SIGCONT (eg when resuming from
Ctrl-Z).
"""
fd = sys.stdin.fileno()
def _handle_sigcont(signum, frame):
tty.setcbreak(fd)
if on_resume:
on_resume()
old_settings = termios.tcgetattr(fd)
old_handler = signal.getsignal(signal.SIGCONT)
try:
signal.signal(signal.SIGCONT, _handle_sigcont)
tty.setcbreak(fd)
yield
finally:
# Restore previous settings
signal.signal(signal.SIGCONT, old_handler)
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
[docs]
@contextmanager
def normal_mode(on_resume: Callable | None = None):
"""Context manager to configure the terminal for regular input
This can be used to return the terminal to regular interaction. This is
could be used for example to return to normal behavior when entering a
Python REPL from within a TUI.
Parameters
----------
on_resume: collections.abc.Callable | None
Optional callable triggered upon SIGCONT (eg when resuming from
Ctrl-Z).
"""
fd = sys.stdin.fileno()
def _handle_sigcont(signum, frame):
tty.setcbreak(fd)
if on_resume:
on_resume()
old_settings = termios.tcgetattr(fd)
old_handler = signal.getsignal(signal.SIGCONT)
try:
signal.signal(signal.SIGCONT, _handle_sigcont)
attrs = termios.tcgetattr(fd)
attrs[3] |= termios.ICANON | termios.ECHO
termios.tcsetattr(fd, termios.TCSADRAIN, attrs)
yield
finally:
# Restore previous settings
signal.signal(signal.SIGCONT, old_handler)
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
[docs]
def clear_screen():
# ANSI: Clear screen and move cursor to top-left
sys.stdout.write("\x1b[2J\x1b[H")
sys.stdout.flush()
[docs]
def print_lines(lines: list[str]):
split_lines = []
for line in lines:
split_lines.extend(line.split("\n"))
split_lines = ["\r" + line for line in split_lines]
print("\n".join(split_lines))
[docs]
def poll_key(timeout: float = 0.1) -> str:
"""Poll for a pending key press
Parameters
----------
timeout: float
Amount of time in seconds to wait for a key press
Returns
-------
str
The pressed key or the empty string if no key was pressed.
"""
rlist, _, _ = select.select([sys.stdin], [], [], timeout)
if rlist:
ch1 = sys.stdin.read(1)
if ch1 == "\x1b":
# Possibly an escape sequence (e.g., arrow key)
ch2 = sys.stdin.read(1)
if ch2 == "[":
ch3 = sys.stdin.read(1)
return f"\x1b[{ch3}"
return ch1
return ""