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.
"""Functions for controlling the text in the terminal."""
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 tuiMode(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 tuiMode
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 _handleSigcont(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, _handleSigcont)
tty.setcbreak(fd)
yield
finally:
# Restore previous settings
signal.signal(signal.SIGCONT, old_handler)
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
[docs]
@contextmanager
def normalMode(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 _handleSigcont(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, _handleSigcont)
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 clearScreen():
"""Clear screen and move cursor to top-left."""
sys.stdout.write("\x1b[2J\x1b[H")
sys.stdout.flush()
[docs]
def printLines(lines: list[str]):
"""Print the provided lines in the terminal.
Parameters
----------
lines : list[str]
The lines to print.
"""
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 pollKey(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 ""