pytermgui.terminal
This module houses the Terminal
class, and its provided instance.
View Source
0"""This module houses the `Terminal` class, and its provided instance.""" 1 2# pylint: disable=cyclic-import 3 4from __future__ import annotations 5 6import os 7import sys 8import time 9import errno 10import signal 11from enum import Enum 12from datetime import datetime 13from shutil import get_terminal_size 14from contextlib import contextmanager 15from typing import Any, Callable, TextIO, Generator 16 17from .input import getch_timeout 18from .regex import strip_ansi, real_length, RE_PIXEL_SIZE, has_open_sequence 19 20__all__ = [ 21 "terminal", 22 "set_global_terminal", 23 "get_terminal", 24 "Terminal", 25 "Recorder", 26 "ColorSystem", 27] 28 29 30class Recorder: 31 """A class that records & exports terminal content.""" 32 33 def __init__(self) -> None: 34 """Initializes the Recorder.""" 35 36 self.recording: list[tuple[str, float]] = [] 37 self._start_stamp = time.time() 38 39 @property 40 def _content(self) -> str: 41 """Returns the str part of self._recording""" 42 43 return "".join(data for data, _ in self.recording) 44 45 def write(self, data: str) -> None: 46 """Writes to the recorder.""" 47 48 self.recording.append((data, time.time() - self._start_stamp)) 49 50 def export_text(self) -> str: 51 """Exports current content as plain text.""" 52 53 return strip_ansi(self._content) 54 55 def export_html( 56 self, prefix: str | None = None, inline_styles: bool = False 57 ) -> str: 58 """Exports current content as HTML. 59 60 For help on the arguments, see `pytermgui.html.to_html`. 61 """ 62 63 from .exporters import to_html # pylint: disable=import-outside-toplevel 64 65 return to_html(self._content, prefix=prefix, inline_styles=inline_styles) 66 67 def export_svg( 68 self, 69 prefix: str | None = None, 70 inline_styles: bool = False, 71 title: str = "PyTermGUI", 72 ) -> str: 73 """Exports current content as SVG. 74 75 For help on the arguments, see `pytermgui.html.to_svg`. 76 """ 77 78 from .exporters import to_svg # pylint: disable=import-outside-toplevel 79 80 return to_svg( 81 self._content, prefix=prefix, inline_styles=inline_styles, title=title 82 ) 83 84 def save_plain(self, filename: str) -> None: 85 """Exports plain text content to the given file. 86 87 Args: 88 filename: The file to save to. 89 """ 90 91 with open(filename, "w", encoding="utf-8") as file: 92 file.write(self.export_text()) 93 94 def save_html( 95 self, 96 filename: str | None = None, 97 prefix: str | None = None, 98 inline_styles: bool = False, 99 ) -> None: 100 """Exports HTML content to the given file. 101 102 For help on the arguments, see `pytermgui.exporters.to_html`. 103 104 Args: 105 filename: The file to save to. If the filename does not contain the '.html' 106 extension it will be appended to the end. 107 """ 108 109 if filename is None: 110 filename = f"PTG_{time.time():%Y-%m-%d %H:%M:%S}.html" 111 112 if not filename.endswith(".html"): 113 filename += ".html" 114 115 with open(filename, "w", encoding="utf-8") as file: 116 file.write(self.export_html(prefix=prefix, inline_styles=inline_styles)) 117 118 def save_svg( 119 self, 120 filename: str | None = None, 121 prefix: str | None = None, 122 inline_styles: bool = False, 123 title: str = "PyTermGUI", 124 ) -> None: 125 """Exports SVG content to the given file. 126 127 For help on the arguments, see `pytermgui.exporters.to_svg`. 128 129 Args: 130 filename: The file to save to. If the filename does not contain the '.svg' 131 extension it will be appended to the end. 132 """ 133 134 if filename is None: 135 timeval = datetime.now() 136 filename = f"PTG_{timeval:%Y-%m-%d_%H:%M:%S}.svg" 137 138 if not filename.endswith(".svg"): 139 filename += ".svg" 140 141 with open(filename, "w", encoding="utf-8") as file: 142 file.write( 143 self.export_svg(prefix=prefix, inline_styles=inline_styles, title=title) 144 ) 145 146 147class ColorSystem(Enum): 148 """An enumeration of various terminal-supported colorsystems.""" 149 150 NO_COLOR = -1 151 """No-color terminal. See https://no-color.org/.""" 152 153 STANDARD = 0 154 """Standard 3-bit colorsystem of the basic 16 colors.""" 155 156 EIGHT_BIT = 1 157 """xterm 8-bit colors, 0-256.""" 158 159 TRUE = 2 160 """'True' color, a.k.a. 24-bit RGB colors.""" 161 162 def __ge__(self, other): 163 """Comparison: self >= other.""" 164 165 if self.__class__ is other.__class__: 166 return self.value >= other.value 167 168 return NotImplemented 169 170 def __gt__(self, other): 171 """Comparison: self > other.""" 172 173 if self.__class__ is other.__class__: 174 return self.value > other.value 175 176 return NotImplemented 177 178 def __le__(self, other): 179 """Comparison: self <= other.""" 180 181 if self.__class__ is other.__class__: 182 return self.value <= other.value 183 184 return NotImplemented 185 186 def __lt__(self, other): 187 """Comparison: self < other.""" 188 189 if self.__class__ is other.__class__: 190 return self.value < other.value 191 192 return NotImplemented 193 194 195def _get_env_colorsys() -> ColorSystem | None: 196 """Gets a colorsystem if the `PTG_COLORSYS` env var can be linked to one.""" 197 198 colorsys = os.getenv("PTG_COLORSYS") 199 if colorsys is None: 200 return None 201 202 try: 203 return ColorSystem[colorsys] 204 205 except NameError: 206 return None 207 208 209class Terminal: # pylint: disable=too-many-instance-attributes 210 """A class to store & access data about a terminal.""" 211 212 RESIZE = 0 213 """Event sent out when the terminal has been resized. 214 215 Arguments passed: 216 - New size: tuple[int, int] 217 """ 218 219 margins = [0, 0, 0, 0] 220 """Not quite sure what this does at the moment.""" 221 222 displayhook_installed: bool = False 223 """This is set to True when `pretty.install` is called.""" 224 225 origin: tuple[int, int] = (1, 1) 226 """Origin of the internal coordinate system.""" 227 228 def __init__( 229 self, 230 stream: TextIO | None = None, 231 *, 232 size: tuple[int, int] | None = None, 233 pixel_size: tuple[int, int] | None = None, 234 ) -> None: 235 """Initialize `_Terminal` class.""" 236 237 if stream is None: 238 stream = sys.stdout 239 240 self._size = size 241 self._pixel_size = pixel_size 242 self._stream = stream or sys.stdout 243 244 self._recorder: Recorder | None = None 245 246 self.size: tuple[int, int] = self._get_size() 247 self.forced_colorsystem: ColorSystem | None = _get_env_colorsys() 248 self.pixel_size: tuple[int, int] = self._get_pixel_size() 249 250 self._listeners: dict[int, list[Callable[..., Any]]] = {} 251 252 if hasattr(signal, "SIGWINCH"): 253 signal.signal(signal.SIGWINCH, self._update_size) 254 255 # TODO: Support SIGWINCH on Windows. 256 257 self._diff_buffer = [ 258 ["" for _ in range(self.width)] for y in range(self.height) 259 ] 260 261 def _get_pixel_size(self) -> tuple[int, int]: 262 """Gets the terminal's size, in pixels.""" 263 264 if self._pixel_size is not None: 265 return self._pixel_size 266 267 if self.isatty(): 268 sys.stdout.write("\x1b[14t") 269 sys.stdout.flush() 270 271 # Some terminals may not respond to a pixel size query, so we send 272 # a timed-out getch call with a default response of 1280x720. 273 output = getch_timeout(0.01, default="\x1b[4;720;1280t") 274 match = RE_PIXEL_SIZE.match(output) 275 276 if match is not None: 277 return (int(match[2]), int(match[1])) 278 279 return (0, 0) 280 281 def _call_listener(self, event: int, data: Any) -> None: 282 """Calls callbacks for event. 283 284 Args: 285 event: A terminal event. 286 data: Arbitrary data passed to the callback. 287 """ 288 289 if event in self._listeners: 290 for callback in self._listeners[event]: 291 callback(data) 292 293 def _get_size(self) -> tuple[int, int]: 294 """Gets the screen size with origin substracted.""" 295 296 if self._size is not None: 297 return self._size 298 299 size = get_terminal_size() 300 return (size[0], size[1]) 301 302 def _update_size(self, *_: Any) -> None: 303 """Resize terminal when SIGWINCH occurs, and call listeners.""" 304 305 self.size = self._get_size() 306 self.pixel_size = self._get_pixel_size() 307 self._call_listener(self.RESIZE, self.size) 308 309 # Wipe the screen in case anything got messed up 310 self.write("\x1b[2J") 311 312 @property 313 def width(self) -> int: 314 """Gets the current width of the terminal.""" 315 316 return self.size[0] 317 318 @property 319 def height(self) -> int: 320 """Gets the current height of the terminal.""" 321 322 return self.size[1] 323 324 @staticmethod 325 def is_interactive() -> bool: 326 """Determines whether shell is interactive. 327 328 A shell is interactive if it is run from `python3` or `python3 -i`. 329 """ 330 331 return hasattr(sys, "ps1") 332 333 @property 334 def forced_colorsystem(self) -> ColorSystem | None: 335 """Forces a color system type on this terminal.""" 336 337 return self._forced_colorsystem 338 339 @forced_colorsystem.setter 340 def forced_colorsystem(self, new: ColorSystem | None) -> None: 341 """Sets a colorsystem, clears colorsystem cache.""" 342 343 self._forced_colorsystem = new 344 345 @property 346 def colorsystem(self) -> ColorSystem: 347 """Gets the current terminal's supported color system.""" 348 349 if self.forced_colorsystem is not None: 350 return self.forced_colorsystem 351 352 if os.getenv("NO_COLOR") is not None: 353 return ColorSystem.NO_COLOR 354 355 term = os.getenv("TERM", "") 356 color_term = os.getenv("COLORTERM", "").strip().lower() 357 358 if color_term == "": 359 color_term = term.split("xterm-")[-1] 360 361 if color_term in ["24bit", "truecolor"]: 362 return ColorSystem.TRUE 363 364 if color_term == "256color": 365 return ColorSystem.EIGHT_BIT 366 367 return ColorSystem.STANDARD 368 369 @contextmanager 370 def record(self) -> Generator[Recorder, None, None]: 371 """Records the terminal's stream.""" 372 373 if self._recorder is not None: 374 raise RuntimeError(f"{self!r} is already recording.") 375 376 try: 377 self._recorder = Recorder() 378 yield self._recorder 379 380 finally: 381 self._recorder = None 382 383 @contextmanager 384 def no_record(self) -> Generator[None, None, None]: 385 """Pauses recording for the duration of the context.""" 386 387 recorder = self._recorder 388 389 try: 390 self._recorder = None 391 yield 392 393 finally: 394 self._recorder = recorder 395 396 @staticmethod 397 def isatty() -> bool: 398 """Returns whether sys.stdin is a tty.""" 399 400 return sys.stdin.isatty() 401 402 def replay(self, recorder: Recorder) -> None: 403 """Replays a recording.""" 404 405 last_time = 0.0 406 for data, delay in recorder.recording: 407 if last_time > 0.0: 408 time.sleep(delay - last_time) 409 410 self.write(data, flush=True) 411 last_time = delay 412 413 def subscribe(self, event: int, callback: Callable[..., Any]) -> None: 414 """Subcribes a callback to be called when event occurs. 415 416 Args: 417 event: The terminal event that calls callback. 418 callback: The callable to be called. The signature of this 419 callable is dependent on the event. See the documentation 420 of the specific event for more information. 421 """ 422 423 if not event in self._listeners: 424 self._listeners[event] = [] 425 426 self._listeners[event].append(callback) 427 428 def write( 429 self, 430 data: str, 431 pos: tuple[int, int] | None = None, 432 flush: bool = False, 433 slice_too_long: bool = True, 434 ) -> None: 435 """Writes the given data to the terminal's stream. 436 437 Args: 438 data: The data to write. 439 pos: Terminal-character space position to write the data to, (x, y). 440 flush: If set, `flush` will be called on the stream after reading. 441 slice_too_long: If set, lines that are outside of the terminal will be 442 sliced to fit. Involves a sizable performance hit. 443 """ 444 445 def _slice(line: str, maximum: int) -> str: 446 length = 0 447 sliced = "" 448 for char in line: 449 sliced += char 450 if char == "\x1b": 451 continue 452 453 if ( 454 length > maximum 455 and real_length(sliced) > maximum 456 and not has_open_sequence(sliced) 457 ): 458 break 459 460 length += 1 461 462 return sliced 463 464 if "\x1b[2J" in data: 465 self.clear_stream() 466 467 if pos is not None: 468 xpos, ypos = pos 469 470 if slice_too_long: 471 if not self.height + self.origin[1] + 1 > ypos >= 0: 472 return 473 474 maximum = self.width - xpos + self.origin[0] 475 476 if xpos < self.origin[0]: 477 xpos = self.origin[0] 478 479 sliced = _slice(data, maximum) if len(data) > maximum else data 480 481 data = f"\x1b[{ypos};{xpos}H{sliced}\x1b[0m" 482 483 else: 484 data = f"\x1b[{ypos};{xpos}H{data}" 485 486 self._stream.write(data) 487 488 if self._recorder is not None: 489 self._recorder.write(data) 490 491 if flush: 492 self._stream.flush() 493 494 def clear_stream(self) -> None: 495 """Clears (truncates) the terminal's stream.""" 496 497 try: 498 self._stream.truncate(0) 499 500 except OSError as error: 501 if error.errno != errno.EINVAL and os.name != "nt": 502 raise 503 504 self._stream.write("\x1b[2J") 505 506 def print( 507 self, 508 *items, 509 pos: tuple[int, int] | None = None, 510 sep: str = " ", 511 end="\n", 512 flush: bool = True, 513 ) -> None: 514 """Prints items to the stream. 515 516 All arguments not mentioned here are analogous to `print`. 517 518 Args: 519 pos: Terminal-character space position to write the data to, (x, y). 520 521 """ 522 523 self.write(sep.join(map(str, items)) + end, pos=pos, flush=flush) 524 525 def flush(self) -> None: 526 """Flushes self._stream.""" 527 528 self._stream.flush() 529 530 531terminal = Terminal() # pylint: disable=invalid-name 532"""Terminal instance that should be used pretty much always.""" 533 534 535def set_global_terminal(new: Terminal) -> None: 536 """Sets the terminal instance to be used by the module.""" 537 538 globals()["terminal"] = new 539 540 541def get_terminal() -> Terminal: 542 """Gets the default terminal instance used by the module.""" 543 544 return terminal
Terminal instance that should be used pretty much always.
View Source
Sets the terminal instance to be used by the module.
View Source
Gets the default terminal instance used by the module.
View Source
210class Terminal: # pylint: disable=too-many-instance-attributes 211 """A class to store & access data about a terminal.""" 212 213 RESIZE = 0 214 """Event sent out when the terminal has been resized. 215 216 Arguments passed: 217 - New size: tuple[int, int] 218 """ 219 220 margins = [0, 0, 0, 0] 221 """Not quite sure what this does at the moment.""" 222 223 displayhook_installed: bool = False 224 """This is set to True when `pretty.install` is called.""" 225 226 origin: tuple[int, int] = (1, 1) 227 """Origin of the internal coordinate system.""" 228 229 def __init__( 230 self, 231 stream: TextIO | None = None, 232 *, 233 size: tuple[int, int] | None = None, 234 pixel_size: tuple[int, int] | None = None, 235 ) -> None: 236 """Initialize `_Terminal` class.""" 237 238 if stream is None: 239 stream = sys.stdout 240 241 self._size = size 242 self._pixel_size = pixel_size 243 self._stream = stream or sys.stdout 244 245 self._recorder: Recorder | None = None 246 247 self.size: tuple[int, int] = self._get_size() 248 self.forced_colorsystem: ColorSystem | None = _get_env_colorsys() 249 self.pixel_size: tuple[int, int] = self._get_pixel_size() 250 251 self._listeners: dict[int, list[Callable[..., Any]]] = {} 252 253 if hasattr(signal, "SIGWINCH"): 254 signal.signal(signal.SIGWINCH, self._update_size) 255 256 # TODO: Support SIGWINCH on Windows. 257 258 self._diff_buffer = [ 259 ["" for _ in range(self.width)] for y in range(self.height) 260 ] 261 262 def _get_pixel_size(self) -> tuple[int, int]: 263 """Gets the terminal's size, in pixels.""" 264 265 if self._pixel_size is not None: 266 return self._pixel_size 267 268 if self.isatty(): 269 sys.stdout.write("\x1b[14t") 270 sys.stdout.flush() 271 272 # Some terminals may not respond to a pixel size query, so we send 273 # a timed-out getch call with a default response of 1280x720. 274 output = getch_timeout(0.01, default="\x1b[4;720;1280t") 275 match = RE_PIXEL_SIZE.match(output) 276 277 if match is not None: 278 return (int(match[2]), int(match[1])) 279 280 return (0, 0) 281 282 def _call_listener(self, event: int, data: Any) -> None: 283 """Calls callbacks for event. 284 285 Args: 286 event: A terminal event. 287 data: Arbitrary data passed to the callback. 288 """ 289 290 if event in self._listeners: 291 for callback in self._listeners[event]: 292 callback(data) 293 294 def _get_size(self) -> tuple[int, int]: 295 """Gets the screen size with origin substracted.""" 296 297 if self._size is not None: 298 return self._size 299 300 size = get_terminal_size() 301 return (size[0], size[1]) 302 303 def _update_size(self, *_: Any) -> None: 304 """Resize terminal when SIGWINCH occurs, and call listeners.""" 305 306 self.size = self._get_size() 307 self.pixel_size = self._get_pixel_size() 308 self._call_listener(self.RESIZE, self.size) 309 310 # Wipe the screen in case anything got messed up 311 self.write("\x1b[2J") 312 313 @property 314 def width(self) -> int: 315 """Gets the current width of the terminal.""" 316 317 return self.size[0] 318 319 @property 320 def height(self) -> int: 321 """Gets the current height of the terminal.""" 322 323 return self.size[1] 324 325 @staticmethod 326 def is_interactive() -> bool: 327 """Determines whether shell is interactive. 328 329 A shell is interactive if it is run from `python3` or `python3 -i`. 330 """ 331 332 return hasattr(sys, "ps1") 333 334 @property 335 def forced_colorsystem(self) -> ColorSystem | None: 336 """Forces a color system type on this terminal.""" 337 338 return self._forced_colorsystem 339 340 @forced_colorsystem.setter 341 def forced_colorsystem(self, new: ColorSystem | None) -> None: 342 """Sets a colorsystem, clears colorsystem cache.""" 343 344 self._forced_colorsystem = new 345 346 @property 347 def colorsystem(self) -> ColorSystem: 348 """Gets the current terminal's supported color system.""" 349 350 if self.forced_colorsystem is not None: 351 return self.forced_colorsystem 352 353 if os.getenv("NO_COLOR") is not None: 354 return ColorSystem.NO_COLOR 355 356 term = os.getenv("TERM", "") 357 color_term = os.getenv("COLORTERM", "").strip().lower() 358 359 if color_term == "": 360 color_term = term.split("xterm-")[-1] 361 362 if color_term in ["24bit", "truecolor"]: 363 return ColorSystem.TRUE 364 365 if color_term == "256color": 366 return ColorSystem.EIGHT_BIT 367 368 return ColorSystem.STANDARD 369 370 @contextmanager 371 def record(self) -> Generator[Recorder, None, None]: 372 """Records the terminal's stream.""" 373 374 if self._recorder is not None: 375 raise RuntimeError(f"{self!r} is already recording.") 376 377 try: 378 self._recorder = Recorder() 379 yield self._recorder 380 381 finally: 382 self._recorder = None 383 384 @contextmanager 385 def no_record(self) -> Generator[None, None, None]: 386 """Pauses recording for the duration of the context.""" 387 388 recorder = self._recorder 389 390 try: 391 self._recorder = None 392 yield 393 394 finally: 395 self._recorder = recorder 396 397 @staticmethod 398 def isatty() -> bool: 399 """Returns whether sys.stdin is a tty.""" 400 401 return sys.stdin.isatty() 402 403 def replay(self, recorder: Recorder) -> None: 404 """Replays a recording.""" 405 406 last_time = 0.0 407 for data, delay in recorder.recording: 408 if last_time > 0.0: 409 time.sleep(delay - last_time) 410 411 self.write(data, flush=True) 412 last_time = delay 413 414 def subscribe(self, event: int, callback: Callable[..., Any]) -> None: 415 """Subcribes a callback to be called when event occurs. 416 417 Args: 418 event: The terminal event that calls callback. 419 callback: The callable to be called. The signature of this 420 callable is dependent on the event. See the documentation 421 of the specific event for more information. 422 """ 423 424 if not event in self._listeners: 425 self._listeners[event] = [] 426 427 self._listeners[event].append(callback) 428 429 def write( 430 self, 431 data: str, 432 pos: tuple[int, int] | None = None, 433 flush: bool = False, 434 slice_too_long: bool = True, 435 ) -> None: 436 """Writes the given data to the terminal's stream. 437 438 Args: 439 data: The data to write. 440 pos: Terminal-character space position to write the data to, (x, y). 441 flush: If set, `flush` will be called on the stream after reading. 442 slice_too_long: If set, lines that are outside of the terminal will be 443 sliced to fit. Involves a sizable performance hit. 444 """ 445 446 def _slice(line: str, maximum: int) -> str: 447 length = 0 448 sliced = "" 449 for char in line: 450 sliced += char 451 if char == "\x1b": 452 continue 453 454 if ( 455 length > maximum 456 and real_length(sliced) > maximum 457 and not has_open_sequence(sliced) 458 ): 459 break 460 461 length += 1 462 463 return sliced 464 465 if "\x1b[2J" in data: 466 self.clear_stream() 467 468 if pos is not None: 469 xpos, ypos = pos 470 471 if slice_too_long: 472 if not self.height + self.origin[1] + 1 > ypos >= 0: 473 return 474 475 maximum = self.width - xpos + self.origin[0] 476 477 if xpos < self.origin[0]: 478 xpos = self.origin[0] 479 480 sliced = _slice(data, maximum) if len(data) > maximum else data 481 482 data = f"\x1b[{ypos};{xpos}H{sliced}\x1b[0m" 483 484 else: 485 data = f"\x1b[{ypos};{xpos}H{data}" 486 487 self._stream.write(data) 488 489 if self._recorder is not None: 490 self._recorder.write(data) 491 492 if flush: 493 self._stream.flush() 494 495 def clear_stream(self) -> None: 496 """Clears (truncates) the terminal's stream.""" 497 498 try: 499 self._stream.truncate(0) 500 501 except OSError as error: 502 if error.errno != errno.EINVAL and os.name != "nt": 503 raise 504 505 self._stream.write("\x1b[2J") 506 507 def print( 508 self, 509 *items, 510 pos: tuple[int, int] | None = None, 511 sep: str = " ", 512 end="\n", 513 flush: bool = True, 514 ) -> None: 515 """Prints items to the stream. 516 517 All arguments not mentioned here are analogous to `print`. 518 519 Args: 520 pos: Terminal-character space position to write the data to, (x, y). 521 522 """ 523 524 self.write(sep.join(map(str, items)) + end, pos=pos, flush=flush) 525 526 def flush(self) -> None: 527 """Flushes self._stream.""" 528 529 self._stream.flush()
A class to store & access data about a terminal.
View Source
229 def __init__( 230 self, 231 stream: TextIO | None = None, 232 *, 233 size: tuple[int, int] | None = None, 234 pixel_size: tuple[int, int] | None = None, 235 ) -> None: 236 """Initialize `_Terminal` class.""" 237 238 if stream is None: 239 stream = sys.stdout 240 241 self._size = size 242 self._pixel_size = pixel_size 243 self._stream = stream or sys.stdout 244 245 self._recorder: Recorder | None = None 246 247 self.size: tuple[int, int] = self._get_size() 248 self.forced_colorsystem: ColorSystem | None = _get_env_colorsys() 249 self.pixel_size: tuple[int, int] = self._get_pixel_size() 250 251 self._listeners: dict[int, list[Callable[..., Any]]] = {} 252 253 if hasattr(signal, "SIGWINCH"): 254 signal.signal(signal.SIGWINCH, self._update_size) 255 256 # TODO: Support SIGWINCH on Windows. 257 258 self._diff_buffer = [ 259 ["" for _ in range(self.width)] for y in range(self.height) 260 ]
Initialize _Terminal
class.
Event sent out when the terminal has been resized.
Arguments passed:
- New size: tuple[int, int]
Not quite sure what this does at the moment.
This is set to True when pretty.install
is called.
Origin of the internal coordinate system.
Forces a color system type on this terminal.
Gets the current width of the terminal.
Gets the current height of the terminal.
View Source
Determines whether shell is interactive.
A shell is interactive if it is run from python3
or python3 -i
.
Gets the current terminal's supported color system.
View Source
370 @contextmanager 371 def record(self) -> Generator[Recorder, None, None]: 372 """Records the terminal's stream.""" 373 374 if self._recorder is not None: 375 raise RuntimeError(f"{self!r} is already recording.") 376 377 try: 378 self._recorder = Recorder() 379 yield self._recorder 380 381 finally: 382 self._recorder = None
Records the terminal's stream.
View Source
Pauses recording for the duration of the context.
View Source
Returns whether sys.stdin is a tty.
View Source
Replays a recording.
View Source
414 def subscribe(self, event: int, callback: Callable[..., Any]) -> None: 415 """Subcribes a callback to be called when event occurs. 416 417 Args: 418 event: The terminal event that calls callback. 419 callback: The callable to be called. The signature of this 420 callable is dependent on the event. See the documentation 421 of the specific event for more information. 422 """ 423 424 if not event in self._listeners: 425 self._listeners[event] = [] 426 427 self._listeners[event].append(callback)
Subcribes a callback to be called when event occurs.
Args
- event: The terminal event that calls callback.
- callback: The callable to be called. The signature of this callable is dependent on the event. See the documentation of the specific event for more information.
View Source
429 def write( 430 self, 431 data: str, 432 pos: tuple[int, int] | None = None, 433 flush: bool = False, 434 slice_too_long: bool = True, 435 ) -> None: 436 """Writes the given data to the terminal's stream. 437 438 Args: 439 data: The data to write. 440 pos: Terminal-character space position to write the data to, (x, y). 441 flush: If set, `flush` will be called on the stream after reading. 442 slice_too_long: If set, lines that are outside of the terminal will be 443 sliced to fit. Involves a sizable performance hit. 444 """ 445 446 def _slice(line: str, maximum: int) -> str: 447 length = 0 448 sliced = "" 449 for char in line: 450 sliced += char 451 if char == "\x1b": 452 continue 453 454 if ( 455 length > maximum 456 and real_length(sliced) > maximum 457 and not has_open_sequence(sliced) 458 ): 459 break 460 461 length += 1 462 463 return sliced 464 465 if "\x1b[2J" in data: 466 self.clear_stream() 467 468 if pos is not None: 469 xpos, ypos = pos 470 471 if slice_too_long: 472 if not self.height + self.origin[1] + 1 > ypos >= 0: 473 return 474 475 maximum = self.width - xpos + self.origin[0] 476 477 if xpos < self.origin[0]: 478 xpos = self.origin[0] 479 480 sliced = _slice(data, maximum) if len(data) > maximum else data 481 482 data = f"\x1b[{ypos};{xpos}H{sliced}\x1b[0m" 483 484 else: 485 data = f"\x1b[{ypos};{xpos}H{data}" 486 487 self._stream.write(data) 488 489 if self._recorder is not None: 490 self._recorder.write(data) 491 492 if flush: 493 self._stream.flush()
Writes the given data to the terminal's stream.
Args
- data: The data to write.
- pos: Terminal-character space position to write the data to, (x, y).
- flush: If set,
flush
will be called on the stream after reading. - slice_too_long: If set, lines that are outside of the terminal will be sliced to fit. Involves a sizable performance hit.
View Source
Clears (truncates) the terminal's stream.
View Source
507 def print( 508 self, 509 *items, 510 pos: tuple[int, int] | None = None, 511 sep: str = " ", 512 end="\n", 513 flush: bool = True, 514 ) -> None: 515 """Prints items to the stream. 516 517 All arguments not mentioned here are analogous to `print`. 518 519 Args: 520 pos: Terminal-character space position to write the data to, (x, y). 521 522 """ 523 524 self.write(sep.join(map(str, items)) + end, pos=pos, flush=flush)
Prints items to the stream.
All arguments not mentioned here are analogous to print
.
Args
- pos: Terminal-character space position to write the data to, (x, y).
View Source
31class Recorder: 32 """A class that records & exports terminal content.""" 33 34 def __init__(self) -> None: 35 """Initializes the Recorder.""" 36 37 self.recording: list[tuple[str, float]] = [] 38 self._start_stamp = time.time() 39 40 @property 41 def _content(self) -> str: 42 """Returns the str part of self._recording""" 43 44 return "".join(data for data, _ in self.recording) 45 46 def write(self, data: str) -> None: 47 """Writes to the recorder.""" 48 49 self.recording.append((data, time.time() - self._start_stamp)) 50 51 def export_text(self) -> str: 52 """Exports current content as plain text.""" 53 54 return strip_ansi(self._content) 55 56 def export_html( 57 self, prefix: str | None = None, inline_styles: bool = False 58 ) -> str: 59 """Exports current content as HTML. 60 61 For help on the arguments, see `pytermgui.html.to_html`. 62 """ 63 64 from .exporters import to_html # pylint: disable=import-outside-toplevel 65 66 return to_html(self._content, prefix=prefix, inline_styles=inline_styles) 67 68 def export_svg( 69 self, 70 prefix: str | None = None, 71 inline_styles: bool = False, 72 title: str = "PyTermGUI", 73 ) -> str: 74 """Exports current content as SVG. 75 76 For help on the arguments, see `pytermgui.html.to_svg`. 77 """ 78 79 from .exporters import to_svg # pylint: disable=import-outside-toplevel 80 81 return to_svg( 82 self._content, prefix=prefix, inline_styles=inline_styles, title=title 83 ) 84 85 def save_plain(self, filename: str) -> None: 86 """Exports plain text content to the given file. 87 88 Args: 89 filename: The file to save to. 90 """ 91 92 with open(filename, "w", encoding="utf-8") as file: 93 file.write(self.export_text()) 94 95 def save_html( 96 self, 97 filename: str | None = None, 98 prefix: str | None = None, 99 inline_styles: bool = False, 100 ) -> None: 101 """Exports HTML content to the given file. 102 103 For help on the arguments, see `pytermgui.exporters.to_html`. 104 105 Args: 106 filename: The file to save to. If the filename does not contain the '.html' 107 extension it will be appended to the end. 108 """ 109 110 if filename is None: 111 filename = f"PTG_{time.time():%Y-%m-%d %H:%M:%S}.html" 112 113 if not filename.endswith(".html"): 114 filename += ".html" 115 116 with open(filename, "w", encoding="utf-8") as file: 117 file.write(self.export_html(prefix=prefix, inline_styles=inline_styles)) 118 119 def save_svg( 120 self, 121 filename: str | None = None, 122 prefix: str | None = None, 123 inline_styles: bool = False, 124 title: str = "PyTermGUI", 125 ) -> None: 126 """Exports SVG content to the given file. 127 128 For help on the arguments, see `pytermgui.exporters.to_svg`. 129 130 Args: 131 filename: The file to save to. If the filename does not contain the '.svg' 132 extension it will be appended to the end. 133 """ 134 135 if filename is None: 136 timeval = datetime.now() 137 filename = f"PTG_{timeval:%Y-%m-%d_%H:%M:%S}.svg" 138 139 if not filename.endswith(".svg"): 140 filename += ".svg" 141 142 with open(filename, "w", encoding="utf-8") as file: 143 file.write( 144 self.export_svg(prefix=prefix, inline_styles=inline_styles, title=title) 145 )
A class that records & exports terminal content.
View Source
Initializes the Recorder.
View Source
Writes to the recorder.
View Source
Exports current content as plain text.
View Source
56 def export_html( 57 self, prefix: str | None = None, inline_styles: bool = False 58 ) -> str: 59 """Exports current content as HTML. 60 61 For help on the arguments, see `pytermgui.html.to_html`. 62 """ 63 64 from .exporters import to_html # pylint: disable=import-outside-toplevel 65 66 return to_html(self._content, prefix=prefix, inline_styles=inline_styles)
Exports current content as HTML.
For help on the arguments, see pytermgui.html.to_html
.
View Source
68 def export_svg( 69 self, 70 prefix: str | None = None, 71 inline_styles: bool = False, 72 title: str = "PyTermGUI", 73 ) -> str: 74 """Exports current content as SVG. 75 76 For help on the arguments, see `pytermgui.html.to_svg`. 77 """ 78 79 from .exporters import to_svg # pylint: disable=import-outside-toplevel 80 81 return to_svg( 82 self._content, prefix=prefix, inline_styles=inline_styles, title=title 83 )
Exports current content as SVG.
For help on the arguments, see pytermgui.html.to_svg
.
View Source
Exports plain text content to the given file.
Args
- filename: The file to save to.
View Source
95 def save_html( 96 self, 97 filename: str | None = None, 98 prefix: str | None = None, 99 inline_styles: bool = False, 100 ) -> None: 101 """Exports HTML content to the given file. 102 103 For help on the arguments, see `pytermgui.exporters.to_html`. 104 105 Args: 106 filename: The file to save to. If the filename does not contain the '.html' 107 extension it will be appended to the end. 108 """ 109 110 if filename is None: 111 filename = f"PTG_{time.time():%Y-%m-%d %H:%M:%S}.html" 112 113 if not filename.endswith(".html"): 114 filename += ".html" 115 116 with open(filename, "w", encoding="utf-8") as file: 117 file.write(self.export_html(prefix=prefix, inline_styles=inline_styles))
Exports HTML content to the given file.
For help on the arguments, see pytermgui.exporters.to_html
.
Args
- filename: The file to save to. If the filename does not contain the '.html' extension it will be appended to the end.
View Source
119 def save_svg( 120 self, 121 filename: str | None = None, 122 prefix: str | None = None, 123 inline_styles: bool = False, 124 title: str = "PyTermGUI", 125 ) -> None: 126 """Exports SVG content to the given file. 127 128 For help on the arguments, see `pytermgui.exporters.to_svg`. 129 130 Args: 131 filename: The file to save to. If the filename does not contain the '.svg' 132 extension it will be appended to the end. 133 """ 134 135 if filename is None: 136 timeval = datetime.now() 137 filename = f"PTG_{timeval:%Y-%m-%d_%H:%M:%S}.svg" 138 139 if not filename.endswith(".svg"): 140 filename += ".svg" 141 142 with open(filename, "w", encoding="utf-8") as file: 143 file.write( 144 self.export_svg(prefix=prefix, inline_styles=inline_styles, title=title) 145 )
Exports SVG content to the given file.
For help on the arguments, see pytermgui.exporters.to_svg
.
Args
- filename: The file to save to. If the filename does not contain the '.svg' extension it will be appended to the end.
View Source
148class ColorSystem(Enum): 149 """An enumeration of various terminal-supported colorsystems.""" 150 151 NO_COLOR = -1 152 """No-color terminal. See https://no-color.org/.""" 153 154 STANDARD = 0 155 """Standard 3-bit colorsystem of the basic 16 colors.""" 156 157 EIGHT_BIT = 1 158 """xterm 8-bit colors, 0-256.""" 159 160 TRUE = 2 161 """'True' color, a.k.a. 24-bit RGB colors.""" 162 163 def __ge__(self, other): 164 """Comparison: self >= other.""" 165 166 if self.__class__ is other.__class__: 167 return self.value >= other.value 168 169 return NotImplemented 170 171 def __gt__(self, other): 172 """Comparison: self > other.""" 173 174 if self.__class__ is other.__class__: 175 return self.value > other.value 176 177 return NotImplemented 178 179 def __le__(self, other): 180 """Comparison: self <= other.""" 181 182 if self.__class__ is other.__class__: 183 return self.value <= other.value 184 185 return NotImplemented 186 187 def __lt__(self, other): 188 """Comparison: self < other.""" 189 190 if self.__class__ is other.__class__: 191 return self.value < other.value 192 193 return NotImplemented
An enumeration of various terminal-supported colorsystems.
No-color terminal. See https://no-color.org/.
Standard 3-bit colorsystem of the basic 16 colors.
xterm 8-bit colors, 0-256.
'True' color, a.k.a. 24-bit RGB colors.
Inherited Members
- enum.Enum
- name
- value