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 (e.g., 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 (e.g., 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 printWithPager(text: str): """Display text in a pager, respecting the PAGER variable. Parameters ---------- text: str The text to display in the pager. """ pager_command = os.environ.get("PAGER", "less") if pager_command == "less": # Use -R to handle ANSI color codes if your text has them pager_command = "less -R" try: process = subprocess.Popen(pager_command, shell=True, stdin=subprocess.PIPE) try: process.stdin.write(text.encode(sys.getdefaultencoding(), "replace")) process.stdin.close() except (IOError, KeyboardInterrupt): pass # Pager quit before all text was written process.wait() except (IOError, OSError): # Pager command not found or other error print(text)
[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 ""