pytermgui.ansi_interface
Various functions to interface with the terminal, using ANSI sequences.
Credits:
- https://wiki.bash-hackers.org/scripting/terminalcodes
- https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
1""" 2Various functions to interface with the terminal, using ANSI sequences. 3 4Credits: 5 6- https://wiki.bash-hackers.org/scripting/terminalcodes 7- https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 8""" 9 10# The entirety of Terminal will soon be moved over to a new submodule, so 11# this ignore is temporary. 12# pylint: disable=too-many-lines 13 14from __future__ import annotations 15 16import re 17from dataclasses import dataclass, fields 18from enum import Enum 19from enum import auto as _auto 20from os import name as _name 21from os import system 22from typing import Any, Optional, Pattern, Union 23 24from .input import getch 25from .terminal import terminal 26 27__all__ = [ 28 "save_screen", 29 "restore_screen", 30 "set_alt_buffer", 31 "unset_alt_buffer", 32 "clear", 33 "hide_cursor", 34 "show_cursor", 35 "save_cursor", 36 "restore_cursor", 37 "report_cursor", 38 "move_cursor", 39 "cursor_up", 40 "cursor_down", 41 "cursor_right", 42 "cursor_left", 43 "cursor_next_line", 44 "cursor_prev_line", 45 "cursor_column", 46 "cursor_home", 47 "set_echo", 48 "unset_echo", 49 "set_mode", 50 "MouseAction", 51 "MouseEvent", 52 "report_mouse", 53 "translate_mouse", 54 "print_to", 55 "reset", 56 "bold", 57 "dim", 58 "italic", 59 "underline", 60 "blink", 61 "inverse", 62 "invisible", 63 "strikethrough", 64 "overline", 65] 66 67 68RE_MOUSE: dict[str, Pattern] = { 69 "decimal_xterm": re.compile(r"<(\d{1,2})\;(\d{1,3})\;(\d{1,3})(\w)"), 70 "decimal_urxvt": re.compile(r"(\d{1,2})\;(\d{1,3})\;(\d{1,3})()"), 71} 72 73 74# screen commands 75def save_screen() -> None: 76 """Saves the contents of the screen, and wipes it. 77 78 Use `restore_screen()` to get them back. 79 """ 80 81 print("\x1b[?47h") 82 83 84def restore_screen() -> None: 85 """Restores the contents of the screen saved by `save_screen()`.""" 86 87 print("\x1b[?47l") 88 89 90def set_alt_buffer() -> None: 91 """Starts an alternate buffer.""" 92 93 print("\x1b[?1049h") 94 95 96def unset_alt_buffer() -> None: 97 """Returns to main buffer, restoring its original state.""" 98 99 print("\x1b[?1049l") 100 101 102def clear(what: str = "screen") -> None: 103 """Clears the specified screen region. 104 105 Args: 106 what: The specifier defining the screen area. 107 108 Available options: 109 * screen: clear whole screen and go to origin 110 * bos: clear screen from cursor backwards 111 * eos: clear screen from cursor forwards 112 * line: clear line and go to beginning 113 * bol: clear line from cursor backwards 114 * eol: clear line from cursor forwards 115 """ 116 117 commands = { 118 "eos": "\x1b[0J", 119 "bos": "\x1b[1J", 120 "screen": "\x1b[2J", 121 "eol": "\x1b[0K", 122 "bol": "\x1b[1K", 123 "line": "\x1b[2K", 124 } 125 126 terminal.write(commands[what]) 127 128 129# cursor commands 130def hide_cursor() -> None: 131 """Stops printing the cursor.""" 132 133 print("\x1b[?25l") 134 135 136def show_cursor() -> None: 137 """Starts printing the cursor.""" 138 139 print("\x1b[?25h") 140 141 142def save_cursor() -> None: 143 """Saves the current cursor position. 144 145 Use `restore_cursor()` to restore it. 146 """ 147 148 terminal.write("\x1b[s") 149 150 151def restore_cursor() -> None: 152 """Restore cursor position as saved by `save_cursor`.""" 153 154 terminal.write("\x1b[u") 155 156 157def report_cursor() -> tuple[int, int] | None: 158 """Gets position of cursor. 159 160 Returns: 161 A tuple of integers, (columns, rows), describing the 162 current (printing) cursor's position. Returns None if 163 this could not be determined. 164 165 Note that this position is **not** the mouse position. See 166 `report_mouse` if that is what you are interested in. 167 """ 168 169 print("\x1b[6n") 170 chars = getch() 171 posy, posx = chars[2:-1].split(";") 172 173 if not posx.isdigit() or not posy.isdigit(): 174 return None 175 176 return int(posx), int(posy) 177 178 179def move_cursor(pos: tuple[int, int]) -> None: 180 """Moves the cursor. 181 182 Args: 183 pos: Tuple of (columns, rows) that the cursor will be moved to. 184 185 This does not flush the terminal for performance reasons. You 186 can do it manually with `sys.stdout.flush()`. 187 """ 188 189 posx, posy = pos 190 terminal.write(f"\x1b[{posy};{posx}H") 191 192 193def cursor_up(num: int = 1) -> None: 194 """Moves the cursor up by `num` lines. 195 196 Args: 197 num: How many lines the cursor should move by. Must be positive, 198 to move in the opposite direction use `cursor_down`. 199 Note: 200 This does not flush the terminal for performance reasons. You 201 can do it manually with `sys.stdout.flush()`. 202 """ 203 204 terminal.write(f"\x1b[{num}A") 205 206 207def cursor_down(num: int = 1) -> None: 208 """Moves the cursor up by `num` lines. 209 210 Args: 211 num: How many lines the cursor should move by. Must be positive, 212 to move in the opposite direction use `cursor_up`. 213 Note: 214 This does not flush the terminal for performance reasons. You 215 can do it manually with `sys.stdout.flush()`. 216 """ 217 218 terminal.write(f"\x1b[{num}B") 219 220 221def cursor_right(num: int = 1) -> None: 222 """Moves the cursor right by `num` lines. 223 224 Args: 225 num: How many characters the cursor should move by. Must be positive, 226 to move in the opposite direction use `cursor_left`. 227 Note: 228 This does not flush the terminal for performance reasons. You 229 can do it manually with `sys.stdout.flush()`. 230 """ 231 232 terminal.write(f"\x1b[{num}C") 233 234 235def cursor_left(num: int = 1) -> None: 236 """Moves the cursor left by `num` lines. 237 238 Args: 239 num: How many characters the cursor should move by. Must be positive, 240 to move in the opposite direction use `cursor_right`. 241 Note: 242 This does not flush the terminal for performance reasons. You 243 can do it manually with `sys.stdout.flush()`. 244 """ 245 246 terminal.write(f"\x1b[{num}D") 247 248 249def cursor_next_line(num: int = 1) -> None: 250 """Moves the cursor to the beginning of the `num`-th line downwards. 251 252 Args: 253 num: The amount the cursor should move by. Must be positive, to move 254 in the opposite direction use `cursor_prev_line`. 255 Note: 256 This does not flush the terminal for performance reasons. You 257 can do it manually with `sys.stdout.flush()`. 258 """ 259 260 terminal.write(f"\x1b[{num}E") 261 262 263def cursor_prev_line(num: int = 1) -> None: 264 """Moves the cursor to the beginning of the `num`-th line upwards. 265 266 Args: 267 num: The amount the cursor should move by. Must be positive, to move 268 in the opposite direction use `cursor_next_line`. 269 Note: 270 This does not flush the terminal for performance reasons. You 271 can do it manually with `sys.stdout.flush()`. 272 """ 273 274 terminal.write(f"\x1b[{num}F") 275 276 277def cursor_column(num: int = 0) -> None: 278 """Moves the cursor to the `num`-th character of the current line. 279 280 Args: 281 num: The new cursor position. 282 283 Note: 284 This does not flush the terminal for performance reasons. You 285 can do it manually with `sys.stdout.flush()`. 286 """ 287 288 terminal.write(f"\x1b[{num}G") 289 290 291def cursor_home() -> None: 292 """Moves cursor to `terminal.origin`. 293 294 Note: 295 This does not flush the terminal for performance reasons. You 296 can do it manually with `sys.stdout.flush()`. 297 """ 298 299 terminal.write("\x1b[H") 300 301 302def set_mode(mode: Union[str, int], write: bool = True) -> str: 303 """Sets terminal display mode. 304 305 This is better left internal. To use these modes, you can call their 306 specific functions, such as `bold("text")` or `italic("text")`. 307 308 Args: 309 mode: One of the available modes. Strings and integers both work. 310 write: Boolean that determines whether the output should be written 311 to stdout. 312 313 Returns: 314 A string that sets the given mode. 315 316 Available modes: 317 - 0: reset 318 - 1: bold 319 - 2: dim 320 - 3: italic 321 - 4: underline 322 - 5: blink 323 - 7: inverse 324 - 8: invisible 325 - 9: strikethrough 326 - 53: overline 327 """ 328 329 options = { 330 "reset": 0, 331 "bold": 1, 332 "dim": 2, 333 "italic": 3, 334 "underline": 4, 335 "blink": 5, 336 "inverse": 7, 337 "invisible": 8, 338 "strikethrough": 9, 339 "overline": 53, 340 } 341 342 if not str(mode).isdigit(): 343 mode = options[str(mode)] 344 345 code = f"\x1b[{mode}m" 346 if write: 347 terminal.write(code) 348 349 return code 350 351 352def set_echo() -> None: 353 """Starts echoing of user input. 354 355 Note: 356 This is currently only available on POSIX. 357 """ 358 359 if not _name == "posix": 360 return 361 362 system("stty echo") 363 364 365def unset_echo() -> None: 366 """Stops echoing of user input. 367 368 Note: 369 This is currently only available on POSIX. 370 """ 371 372 if not _name == "posix": 373 return 374 375 system("stty -echo") 376 377 378class MouseAction(Enum): 379 """An enumeration of all the polled mouse actions""" 380 381 LEFT_CLICK = _auto() 382 """Start of a left button action sequence.""" 383 384 LEFT_DRAG = _auto() 385 """Mouse moved while left button was held down.""" 386 387 RIGHT_CLICK = _auto() 388 """Start of a right button action sequence.""" 389 390 RIGHT_DRAG = _auto() 391 """Mouse moved while right button was held down.""" 392 393 SCROLL_UP = _auto() 394 """Mouse wheel or touchpad scroll upwards.""" 395 396 SCROLL_DOWN = _auto() 397 """Mouse wheel or touchpad scroll downwards.""" 398 399 HOVER = _auto() 400 """Mouse moved without clicking.""" 401 402 # TODO: Support left & right mouse release separately, without breaking 403 # current API. 404 RELEASE = _auto() 405 """Mouse button released; end of any and all mouse action sequences.""" 406 407 408@dataclass 409class MouseEvent: 410 """A class to represent events created by mouse actions. 411 412 Its first argument is a `MouseAction` describing what happened, 413 and its second argument is a `tuple[int, int]` describing where 414 it happened. 415 416 This class mostly exists for readability & typing reasons. It also 417 implements the iterable protocol, so you can use the unpacking syntax, 418 such as: 419 420 ```python3 421 action, position = MouseEvent(...) 422 ``` 423 """ 424 425 action: MouseAction 426 position: tuple[int, int] 427 428 def __post_init__(self) -> None: 429 """Initialize iteration counter""" 430 431 self._iter_index = 0 432 433 def __next__(self) -> MouseAction | tuple[int, int]: 434 """Get next iteration item""" 435 436 data = fields(self) 437 438 if self._iter_index >= len(data): 439 self._iter_index = 0 440 raise StopIteration 441 442 self._iter_index += 1 443 return getattr(self, data[self._iter_index - 1].name) 444 445 def __iter__(self) -> MouseEvent: 446 """Start iteration""" 447 448 return self 449 450 def is_scroll(self) -> bool: 451 """Returns True if event.action is one of the scrolling actions.""" 452 453 return self.action in {MouseAction.SCROLL_DOWN, MouseAction.SCROLL_UP} 454 455 def is_primary(self) -> bool: 456 """Returns True if event.action is one of the primary (left-button) actions.""" 457 458 return self.action in {MouseAction.LEFT_CLICK, MouseAction.LEFT_DRAG} 459 460 def is_secondary(self) -> bool: 461 """Returns True if event.action is one of the secondary (secondary-button) actions.""" 462 463 return self.action in {MouseAction.RIGHT_CLICK, MouseAction.RIGHT_DRAG} 464 465 466def report_mouse( 467 event: str, method: Optional[str] = "decimal_xterm", stop: bool = False 468) -> None: 469 """Starts reporting of mouse events. 470 471 You can specify multiple events to report on. 472 473 Args: 474 event: The type of event to report on. See below for options. 475 method: The method of reporting to use. See below for options. 476 stop: If set to True, the stopping code is written to stdout. 477 478 Raises: 479 NotImplementedError: The given event is not supported. 480 481 Note: 482 If you need this functionality, you're probably better off using the wrapper 483 `pytermgui.context_managers.mouse_handler`, which allows listening on multiple 484 events, gives a translator method and handles exceptions. 485 486 Possible events: 487 - **press**: Report when the mouse is clicked, left or right button. 488 - **highlight**: Report highlighting. 489 - **press_hold**: Report with a left or right click, as well as both 490 left & right drag and release. 491 - **hover**: Report even when no active action is done, only the mouse 492 is moved. 493 494 Methods: 495 - **None**: Non-decimal xterm method. Limited in coordinates. 496 - **decimal_xterm**: The default setting. Most universally supported. 497 - **decimal_urxvt**: Older, less compatible, but useful on some systems. 498 - **decimal_utf8**: Apparently not too stable. 499 500 More information <a href='https://stackoverflow.com/a/5970472'>here</a>. 501 """ 502 503 if event == "press": 504 terminal.write("\x1b[?1000") 505 506 elif event == "highlight": 507 terminal.write("\x1b[?1001") 508 509 elif event == "press_hold": 510 terminal.write("\x1b[?1002") 511 512 elif event == "hover": 513 terminal.write("\x1b[?1003") 514 515 else: 516 raise NotImplementedError(f"Mouse report event {event} is not supported!") 517 518 terminal.write("l" if stop else "h") 519 520 if method == "decimal_utf8": 521 terminal.write("\x1b[?1005") 522 523 elif method == "decimal_xterm": 524 terminal.write("\x1b[?1006") 525 526 elif method == "decimal_urxvt": 527 terminal.write("\x1b[?1015") 528 529 elif method is None: 530 return 531 532 else: 533 raise NotImplementedError(f"Mouse report method {method} is not supported!") 534 535 terminal.write("l" if stop else "h", flush=True) 536 537 538def translate_mouse(code: str, method: str) -> list[MouseEvent | None] | None: 539 """Translates the output of produced by setting `report_mouse` into MouseEvents. 540 541 This method currently only supports `decimal_xterm` and `decimal_urxvt`. 542 543 Args: 544 code: The string of mouse code(s) to translate. 545 method: The reporting method to translate. One of [`decimal_xterm`, `decimal_urxvt`]. 546 547 Returns: 548 A list of optional mouse events obtained from the code argument. If the code was malformed, 549 and no codes could be determined None is returned. 550 """ 551 552 if code == "\x1b": 553 return None 554 555 mouse_codes = { 556 "decimal_xterm": { 557 "0M": MouseAction.LEFT_CLICK, 558 "0m": MouseAction.RELEASE, 559 "2M": MouseAction.RIGHT_CLICK, 560 "2m": MouseAction.RELEASE, 561 "32": MouseAction.LEFT_DRAG, 562 "34": MouseAction.RIGHT_DRAG, 563 "35": MouseAction.HOVER, 564 "64": MouseAction.SCROLL_UP, 565 "65": MouseAction.SCROLL_DOWN, 566 }, 567 "decimal_urxvt": { 568 "32": MouseAction.LEFT_CLICK, 569 "34": MouseAction.RIGHT_CLICK, 570 "35": MouseAction.RELEASE, 571 "64": MouseAction.LEFT_DRAG, 572 "66": MouseAction.RIGHT_DRAG, 573 "96": MouseAction.SCROLL_UP, 574 "97": MouseAction.SCROLL_DOWN, 575 }, 576 } 577 578 mapping = mouse_codes[method] 579 pattern: Pattern = RE_MOUSE[method] 580 581 events: list[MouseEvent | None] = [] 582 583 for sequence in code.split("\x1b"): 584 if len(sequence) == 0: 585 continue 586 587 matches = list(pattern.finditer(sequence)) 588 if len(matches) == 0: 589 return None 590 591 for match in matches: 592 identifier, *pos, release_code = match.groups() 593 594 # decimal_xterm uses the last character's 595 # capitalization to signify press/release state 596 if len(release_code) > 0 and identifier in ["0", "2"]: 597 identifier += release_code 598 599 if identifier in mapping: 600 action = mapping[identifier] 601 assert isinstance(action, MouseAction) 602 603 events.append(MouseEvent(action, (int(pos[0]), int(pos[1])))) 604 continue 605 606 events.append(None) 607 608 return events 609 610 611# shorthand functions 612def print_to(pos: tuple[int, int], *args: Any, **kwargs: Any) -> None: 613 """Prints text to given `pos`. 614 615 Note: 616 This method passes through all arguments (except for `pos`) to the `print` 617 method. 618 """ 619 620 move_cursor(pos) 621 print(*args, **kwargs, end="", flush=True) 622 623 624def reset() -> str: 625 """Resets printing mode.""" 626 627 return set_mode("reset", False) 628 629 630def bold(text: str, reset_style: Optional[bool] = True) -> str: 631 """Returns text in bold. 632 633 Args: 634 reset_style: Boolean that determines whether a reset character should 635 be appended to the end of the string. 636 """ 637 638 return set_mode("bold", False) + text + (reset() if reset_style else "") 639 640 641def dim(text: str, reset_style: Optional[bool] = True) -> str: 642 """Returns text in dim. 643 644 Args: 645 reset_style: Boolean that determines whether a reset character should 646 be appended to the end of the string. 647 """ 648 649 return set_mode("dim", False) + text + (reset() if reset_style else "") 650 651 652def italic(text: str, reset_style: Optional[bool] = True) -> str: 653 """Returns text in italic. 654 655 Args: 656 reset_style: Boolean that determines whether a reset character should 657 be appended to the end of the string. 658 """ 659 660 return set_mode("italic", False) + text + (reset() if reset_style else "") 661 662 663def underline(text: str, reset_style: Optional[bool] = True) -> str: 664 """Returns text underlined. 665 666 Args: 667 reset_style: Boolean that determines whether a reset character should 668 be appended to the end of the string. 669 """ 670 671 return set_mode("underline", False) + text + (reset() if reset_style else "") 672 673 674def blink(text: str, reset_style: Optional[bool] = True) -> str: 675 """Returns text blinking. 676 677 Args: 678 reset_style: Boolean that determines whether a reset character should 679 be appended to the end of the string. 680 """ 681 682 return set_mode("blink", False) + text + (reset() if reset_style else "") 683 684 685def inverse(text: str, reset_style: Optional[bool] = True) -> str: 686 """Returns text inverse-colored. 687 688 Args: 689 reset_style: Boolean that determines whether a reset character should 690 be appended to the end of the string. 691 """ 692 693 return set_mode("inverse", False) + text + (reset() if reset_style else "") 694 695 696def invisible(text: str, reset_style: Optional[bool] = True) -> str: 697 """Returns text as invisible. 698 699 Args: 700 reset_style: Boolean that determines whether a reset character should 701 be appended to the end of the string. 702 703 Note: 704 This isn't very widely supported. 705 """ 706 707 return set_mode("invisible", False) + text + (reset() if reset_style else "") 708 709 710def strikethrough(text: str, reset_style: Optional[bool] = True) -> str: 711 """Return text as strikethrough. 712 713 Args: 714 reset_style: Boolean that determines whether a reset character should 715 be appended to the end of the string. 716 """ 717 718 return set_mode("strikethrough", False) + text + (reset() if reset_style else "") 719 720 721def overline(text: str, reset_style: Optional[bool] = True) -> str: 722 """Return text overlined. 723 724 Args: 725 reset_style: Boolean that determines whether a reset character should 726 be appended to the end of the string. 727 728 Note: 729 This isnt' very widely supported. 730 """ 731 732 return set_mode("overline", False) + text + (reset() if reset_style else "")
76def save_screen() -> None: 77 """Saves the contents of the screen, and wipes it. 78 79 Use `restore_screen()` to get them back. 80 """ 81 82 print("\x1b[?47h")
Saves the contents of the screen, and wipes it.
Use restore_screen()
to get them back.
85def restore_screen() -> None: 86 """Restores the contents of the screen saved by `save_screen()`.""" 87 88 print("\x1b[?47l")
Restores the contents of the screen saved by save_screen()
.
Starts an alternate buffer.
97def unset_alt_buffer() -> None: 98 """Returns to main buffer, restoring its original state.""" 99 100 print("\x1b[?1049l")
Returns to main buffer, restoring its original state.
103def clear(what: str = "screen") -> None: 104 """Clears the specified screen region. 105 106 Args: 107 what: The specifier defining the screen area. 108 109 Available options: 110 * screen: clear whole screen and go to origin 111 * bos: clear screen from cursor backwards 112 * eos: clear screen from cursor forwards 113 * line: clear line and go to beginning 114 * bol: clear line from cursor backwards 115 * eol: clear line from cursor forwards 116 """ 117 118 commands = { 119 "eos": "\x1b[0J", 120 "bos": "\x1b[1J", 121 "screen": "\x1b[2J", 122 "eol": "\x1b[0K", 123 "bol": "\x1b[1K", 124 "line": "\x1b[2K", 125 } 126 127 terminal.write(commands[what])
Clears the specified screen region.
Args
- what: The specifier defining the screen area.
Available options:
- screen: clear whole screen and go to origin
- bos: clear screen from cursor backwards
- eos: clear screen from cursor forwards
- line: clear line and go to beginning
- bol: clear line from cursor backwards
- eol: clear line from cursor forwards
Stops printing the cursor.
Starts printing the cursor.
143def save_cursor() -> None: 144 """Saves the current cursor position. 145 146 Use `restore_cursor()` to restore it. 147 """ 148 149 terminal.write("\x1b[s")
Saves the current cursor position.
Use restore_cursor()
to restore it.
152def restore_cursor() -> None: 153 """Restore cursor position as saved by `save_cursor`.""" 154 155 terminal.write("\x1b[u")
Restore cursor position as saved by save_cursor
.
158def report_cursor() -> tuple[int, int] | None: 159 """Gets position of cursor. 160 161 Returns: 162 A tuple of integers, (columns, rows), describing the 163 current (printing) cursor's position. Returns None if 164 this could not be determined. 165 166 Note that this position is **not** the mouse position. See 167 `report_mouse` if that is what you are interested in. 168 """ 169 170 print("\x1b[6n") 171 chars = getch() 172 posy, posx = chars[2:-1].split(";") 173 174 if not posx.isdigit() or not posy.isdigit(): 175 return None 176 177 return int(posx), int(posy)
Gets position of cursor.
Returns
A tuple of integers, (columns, rows), describing the current (printing) cursor's position. Returns None if this could not be determined.
Note that this position is not the mouse position. See
report_mouse
if that is what you are interested in.
180def move_cursor(pos: tuple[int, int]) -> None: 181 """Moves the cursor. 182 183 Args: 184 pos: Tuple of (columns, rows) that the cursor will be moved to. 185 186 This does not flush the terminal for performance reasons. You 187 can do it manually with `sys.stdout.flush()`. 188 """ 189 190 posx, posy = pos 191 terminal.write(f"\x1b[{posy};{posx}H")
Moves the cursor.
Args
- pos: Tuple of (columns, rows) that the cursor will be moved to.
This does not flush the terminal for performance reasons. You
can do it manually with sys.stdout.flush()
.
194def cursor_up(num: int = 1) -> None: 195 """Moves the cursor up by `num` lines. 196 197 Args: 198 num: How many lines the cursor should move by. Must be positive, 199 to move in the opposite direction use `cursor_down`. 200 Note: 201 This does not flush the terminal for performance reasons. You 202 can do it manually with `sys.stdout.flush()`. 203 """ 204 205 terminal.write(f"\x1b[{num}A")
Moves the cursor up by num
lines.
Args
- num: How many lines the cursor should move by. Must be positive,
to move in the opposite direction use
cursor_down
.
Note
This does not flush the terminal for performance reasons. You can do it manually with
sys.stdout.flush()
.
208def cursor_down(num: int = 1) -> None: 209 """Moves the cursor up by `num` lines. 210 211 Args: 212 num: How many lines the cursor should move by. Must be positive, 213 to move in the opposite direction use `cursor_up`. 214 Note: 215 This does not flush the terminal for performance reasons. You 216 can do it manually with `sys.stdout.flush()`. 217 """ 218 219 terminal.write(f"\x1b[{num}B")
Moves the cursor up by num
lines.
Args
- num: How many lines the cursor should move by. Must be positive,
to move in the opposite direction use
cursor_up
.
Note
This does not flush the terminal for performance reasons. You can do it manually with
sys.stdout.flush()
.
222def cursor_right(num: int = 1) -> None: 223 """Moves the cursor right by `num` lines. 224 225 Args: 226 num: How many characters the cursor should move by. Must be positive, 227 to move in the opposite direction use `cursor_left`. 228 Note: 229 This does not flush the terminal for performance reasons. You 230 can do it manually with `sys.stdout.flush()`. 231 """ 232 233 terminal.write(f"\x1b[{num}C")
Moves the cursor right by num
lines.
Args
- num: How many characters the cursor should move by. Must be positive,
to move in the opposite direction use
cursor_left
.
Note
This does not flush the terminal for performance reasons. You can do it manually with
sys.stdout.flush()
.
236def cursor_left(num: int = 1) -> None: 237 """Moves the cursor left by `num` lines. 238 239 Args: 240 num: How many characters the cursor should move by. Must be positive, 241 to move in the opposite direction use `cursor_right`. 242 Note: 243 This does not flush the terminal for performance reasons. You 244 can do it manually with `sys.stdout.flush()`. 245 """ 246 247 terminal.write(f"\x1b[{num}D")
Moves the cursor left by num
lines.
Args
- num: How many characters the cursor should move by. Must be positive,
to move in the opposite direction use
cursor_right
.
Note
This does not flush the terminal for performance reasons. You can do it manually with
sys.stdout.flush()
.
250def cursor_next_line(num: int = 1) -> None: 251 """Moves the cursor to the beginning of the `num`-th line downwards. 252 253 Args: 254 num: The amount the cursor should move by. Must be positive, to move 255 in the opposite direction use `cursor_prev_line`. 256 Note: 257 This does not flush the terminal for performance reasons. You 258 can do it manually with `sys.stdout.flush()`. 259 """ 260 261 terminal.write(f"\x1b[{num}E")
Moves the cursor to the beginning of the num
-th line downwards.
Args
- num: The amount the cursor should move by. Must be positive, to move
in the opposite direction use
cursor_prev_line
.
Note
This does not flush the terminal for performance reasons. You can do it manually with
sys.stdout.flush()
.
264def cursor_prev_line(num: int = 1) -> None: 265 """Moves the cursor to the beginning of the `num`-th line upwards. 266 267 Args: 268 num: The amount the cursor should move by. Must be positive, to move 269 in the opposite direction use `cursor_next_line`. 270 Note: 271 This does not flush the terminal for performance reasons. You 272 can do it manually with `sys.stdout.flush()`. 273 """ 274 275 terminal.write(f"\x1b[{num}F")
Moves the cursor to the beginning of the num
-th line upwards.
Args
- num: The amount the cursor should move by. Must be positive, to move
in the opposite direction use
cursor_next_line
.
Note
This does not flush the terminal for performance reasons. You can do it manually with
sys.stdout.flush()
.
278def cursor_column(num: int = 0) -> None: 279 """Moves the cursor to the `num`-th character of the current line. 280 281 Args: 282 num: The new cursor position. 283 284 Note: 285 This does not flush the terminal for performance reasons. You 286 can do it manually with `sys.stdout.flush()`. 287 """ 288 289 terminal.write(f"\x1b[{num}G")
Moves the cursor to the num
-th character of the current line.
Args
- num: The new cursor position.
Note
This does not flush the terminal for performance reasons. You can do it manually with
sys.stdout.flush()
.
292def cursor_home() -> None: 293 """Moves cursor to `terminal.origin`. 294 295 Note: 296 This does not flush the terminal for performance reasons. You 297 can do it manually with `sys.stdout.flush()`. 298 """ 299 300 terminal.write("\x1b[H")
Moves cursor to terminal.origin
.
Note
This does not flush the terminal for performance reasons. You can do it manually with
sys.stdout.flush()
.
353def set_echo() -> None: 354 """Starts echoing of user input. 355 356 Note: 357 This is currently only available on POSIX. 358 """ 359 360 if not _name == "posix": 361 return 362 363 system("stty echo")
Starts echoing of user input.
Note
This is currently only available on POSIX.
366def unset_echo() -> None: 367 """Stops echoing of user input. 368 369 Note: 370 This is currently only available on POSIX. 371 """ 372 373 if not _name == "posix": 374 return 375 376 system("stty -echo")
Stops echoing of user input.
Note
This is currently only available on POSIX.
303def set_mode(mode: Union[str, int], write: bool = True) -> str: 304 """Sets terminal display mode. 305 306 This is better left internal. To use these modes, you can call their 307 specific functions, such as `bold("text")` or `italic("text")`. 308 309 Args: 310 mode: One of the available modes. Strings and integers both work. 311 write: Boolean that determines whether the output should be written 312 to stdout. 313 314 Returns: 315 A string that sets the given mode. 316 317 Available modes: 318 - 0: reset 319 - 1: bold 320 - 2: dim 321 - 3: italic 322 - 4: underline 323 - 5: blink 324 - 7: inverse 325 - 8: invisible 326 - 9: strikethrough 327 - 53: overline 328 """ 329 330 options = { 331 "reset": 0, 332 "bold": 1, 333 "dim": 2, 334 "italic": 3, 335 "underline": 4, 336 "blink": 5, 337 "inverse": 7, 338 "invisible": 8, 339 "strikethrough": 9, 340 "overline": 53, 341 } 342 343 if not str(mode).isdigit(): 344 mode = options[str(mode)] 345 346 code = f"\x1b[{mode}m" 347 if write: 348 terminal.write(code) 349 350 return code
Sets terminal display mode.
This is better left internal. To use these modes, you can call their
specific functions, such as bold("text")
or italic("text")
.
Args
- mode: One of the available modes. Strings and integers both work.
- write: Boolean that determines whether the output should be written to stdout.
Returns
A string that sets the given mode.
Available modes
- 0: reset
- 1: bold
- 2: dim
- 3: italic
- 4: underline
- 5: blink
- 7: inverse
- 8: invisible
- 9: strikethrough
- 53: overline
379class MouseAction(Enum): 380 """An enumeration of all the polled mouse actions""" 381 382 LEFT_CLICK = _auto() 383 """Start of a left button action sequence.""" 384 385 LEFT_DRAG = _auto() 386 """Mouse moved while left button was held down.""" 387 388 RIGHT_CLICK = _auto() 389 """Start of a right button action sequence.""" 390 391 RIGHT_DRAG = _auto() 392 """Mouse moved while right button was held down.""" 393 394 SCROLL_UP = _auto() 395 """Mouse wheel or touchpad scroll upwards.""" 396 397 SCROLL_DOWN = _auto() 398 """Mouse wheel or touchpad scroll downwards.""" 399 400 HOVER = _auto() 401 """Mouse moved without clicking.""" 402 403 # TODO: Support left & right mouse release separately, without breaking 404 # current API. 405 RELEASE = _auto() 406 """Mouse button released; end of any and all mouse action sequences."""
An enumeration of all the polled mouse actions
Mouse button released; end of any and all mouse action sequences.
Inherited Members
- enum.Enum
- name
- value
409@dataclass 410class MouseEvent: 411 """A class to represent events created by mouse actions. 412 413 Its first argument is a `MouseAction` describing what happened, 414 and its second argument is a `tuple[int, int]` describing where 415 it happened. 416 417 This class mostly exists for readability & typing reasons. It also 418 implements the iterable protocol, so you can use the unpacking syntax, 419 such as: 420 421 ```python3 422 action, position = MouseEvent(...) 423 ``` 424 """ 425 426 action: MouseAction 427 position: tuple[int, int] 428 429 def __post_init__(self) -> None: 430 """Initialize iteration counter""" 431 432 self._iter_index = 0 433 434 def __next__(self) -> MouseAction | tuple[int, int]: 435 """Get next iteration item""" 436 437 data = fields(self) 438 439 if self._iter_index >= len(data): 440 self._iter_index = 0 441 raise StopIteration 442 443 self._iter_index += 1 444 return getattr(self, data[self._iter_index - 1].name) 445 446 def __iter__(self) -> MouseEvent: 447 """Start iteration""" 448 449 return self 450 451 def is_scroll(self) -> bool: 452 """Returns True if event.action is one of the scrolling actions.""" 453 454 return self.action in {MouseAction.SCROLL_DOWN, MouseAction.SCROLL_UP} 455 456 def is_primary(self) -> bool: 457 """Returns True if event.action is one of the primary (left-button) actions.""" 458 459 return self.action in {MouseAction.LEFT_CLICK, MouseAction.LEFT_DRAG} 460 461 def is_secondary(self) -> bool: 462 """Returns True if event.action is one of the secondary (secondary-button) actions.""" 463 464 return self.action in {MouseAction.RIGHT_CLICK, MouseAction.RIGHT_DRAG}
A class to represent events created by mouse actions.
Its first argument is a MouseAction
describing what happened,
and its second argument is a tuple[int, int]
describing where
it happened.
This class mostly exists for readability & typing reasons. It also implements the iterable protocol, so you can use the unpacking syntax, such as:
action, position = MouseEvent(...)
451 def is_scroll(self) -> bool: 452 """Returns True if event.action is one of the scrolling actions.""" 453 454 return self.action in {MouseAction.SCROLL_DOWN, MouseAction.SCROLL_UP}
Returns True if event.action is one of the scrolling actions.
456 def is_primary(self) -> bool: 457 """Returns True if event.action is one of the primary (left-button) actions.""" 458 459 return self.action in {MouseAction.LEFT_CLICK, MouseAction.LEFT_DRAG}
Returns True if event.action is one of the primary (left-button) actions.
461 def is_secondary(self) -> bool: 462 """Returns True if event.action is one of the secondary (secondary-button) actions.""" 463 464 return self.action in {MouseAction.RIGHT_CLICK, MouseAction.RIGHT_DRAG}
Returns True if event.action is one of the secondary (secondary-button) actions.
467def report_mouse( 468 event: str, method: Optional[str] = "decimal_xterm", stop: bool = False 469) -> None: 470 """Starts reporting of mouse events. 471 472 You can specify multiple events to report on. 473 474 Args: 475 event: The type of event to report on. See below for options. 476 method: The method of reporting to use. See below for options. 477 stop: If set to True, the stopping code is written to stdout. 478 479 Raises: 480 NotImplementedError: The given event is not supported. 481 482 Note: 483 If you need this functionality, you're probably better off using the wrapper 484 `pytermgui.context_managers.mouse_handler`, which allows listening on multiple 485 events, gives a translator method and handles exceptions. 486 487 Possible events: 488 - **press**: Report when the mouse is clicked, left or right button. 489 - **highlight**: Report highlighting. 490 - **press_hold**: Report with a left or right click, as well as both 491 left & right drag and release. 492 - **hover**: Report even when no active action is done, only the mouse 493 is moved. 494 495 Methods: 496 - **None**: Non-decimal xterm method. Limited in coordinates. 497 - **decimal_xterm**: The default setting. Most universally supported. 498 - **decimal_urxvt**: Older, less compatible, but useful on some systems. 499 - **decimal_utf8**: Apparently not too stable. 500 501 More information <a href='https://stackoverflow.com/a/5970472'>here</a>. 502 """ 503 504 if event == "press": 505 terminal.write("\x1b[?1000") 506 507 elif event == "highlight": 508 terminal.write("\x1b[?1001") 509 510 elif event == "press_hold": 511 terminal.write("\x1b[?1002") 512 513 elif event == "hover": 514 terminal.write("\x1b[?1003") 515 516 else: 517 raise NotImplementedError(f"Mouse report event {event} is not supported!") 518 519 terminal.write("l" if stop else "h") 520 521 if method == "decimal_utf8": 522 terminal.write("\x1b[?1005") 523 524 elif method == "decimal_xterm": 525 terminal.write("\x1b[?1006") 526 527 elif method == "decimal_urxvt": 528 terminal.write("\x1b[?1015") 529 530 elif method is None: 531 return 532 533 else: 534 raise NotImplementedError(f"Mouse report method {method} is not supported!") 535 536 terminal.write("l" if stop else "h", flush=True)
Starts reporting of mouse events.
You can specify multiple events to report on.
Args
- event: The type of event to report on. See below for options.
- method: The method of reporting to use. See below for options.
- stop: If set to True, the stopping code is written to stdout.
Raises
- NotImplementedError: The given event is not supported.
Note
If you need this functionality, you're probably better off using the wrapper
pytermgui.context_managers.mouse_handler
, which allows listening on multiple events, gives a translator method and handles exceptions.
Possible events
- press: Report when the mouse is clicked, left or right button.
- highlight: Report highlighting.
- press_hold: Report with a left or right click, as well as both left & right drag and release.
- hover: Report even when no active action is done, only the mouse is moved.
Methods
- None: Non-decimal xterm method. Limited in coordinates.
- decimal_xterm: The default setting. Most universally supported.
- decimal_urxvt: Older, less compatible, but useful on some systems.
- decimal_utf8: Apparently not too stable.
More information here.
539def translate_mouse(code: str, method: str) -> list[MouseEvent | None] | None: 540 """Translates the output of produced by setting `report_mouse` into MouseEvents. 541 542 This method currently only supports `decimal_xterm` and `decimal_urxvt`. 543 544 Args: 545 code: The string of mouse code(s) to translate. 546 method: The reporting method to translate. One of [`decimal_xterm`, `decimal_urxvt`]. 547 548 Returns: 549 A list of optional mouse events obtained from the code argument. If the code was malformed, 550 and no codes could be determined None is returned. 551 """ 552 553 if code == "\x1b": 554 return None 555 556 mouse_codes = { 557 "decimal_xterm": { 558 "0M": MouseAction.LEFT_CLICK, 559 "0m": MouseAction.RELEASE, 560 "2M": MouseAction.RIGHT_CLICK, 561 "2m": MouseAction.RELEASE, 562 "32": MouseAction.LEFT_DRAG, 563 "34": MouseAction.RIGHT_DRAG, 564 "35": MouseAction.HOVER, 565 "64": MouseAction.SCROLL_UP, 566 "65": MouseAction.SCROLL_DOWN, 567 }, 568 "decimal_urxvt": { 569 "32": MouseAction.LEFT_CLICK, 570 "34": MouseAction.RIGHT_CLICK, 571 "35": MouseAction.RELEASE, 572 "64": MouseAction.LEFT_DRAG, 573 "66": MouseAction.RIGHT_DRAG, 574 "96": MouseAction.SCROLL_UP, 575 "97": MouseAction.SCROLL_DOWN, 576 }, 577 } 578 579 mapping = mouse_codes[method] 580 pattern: Pattern = RE_MOUSE[method] 581 582 events: list[MouseEvent | None] = [] 583 584 for sequence in code.split("\x1b"): 585 if len(sequence) == 0: 586 continue 587 588 matches = list(pattern.finditer(sequence)) 589 if len(matches) == 0: 590 return None 591 592 for match in matches: 593 identifier, *pos, release_code = match.groups() 594 595 # decimal_xterm uses the last character's 596 # capitalization to signify press/release state 597 if len(release_code) > 0 and identifier in ["0", "2"]: 598 identifier += release_code 599 600 if identifier in mapping: 601 action = mapping[identifier] 602 assert isinstance(action, MouseAction) 603 604 events.append(MouseEvent(action, (int(pos[0]), int(pos[1])))) 605 continue 606 607 events.append(None) 608 609 return events
Translates the output of produced by setting report_mouse
into MouseEvents.
This method currently only supports decimal_xterm
and decimal_urxvt
.
Args
- code: The string of mouse code(s) to translate.
- method: The reporting method to translate. One of [
decimal_xterm
,decimal_urxvt
].
Returns
A list of optional mouse events obtained from the code argument. If the code was malformed, and no codes could be determined None is returned.
613def print_to(pos: tuple[int, int], *args: Any, **kwargs: Any) -> None: 614 """Prints text to given `pos`. 615 616 Note: 617 This method passes through all arguments (except for `pos`) to the `print` 618 method. 619 """ 620 621 move_cursor(pos) 622 print(*args, **kwargs, end="", flush=True)
Prints text to given pos
.
Note
This method passes through all arguments (except for
pos
) to the
Resets printing mode.
631def bold(text: str, reset_style: Optional[bool] = True) -> str: 632 """Returns text in bold. 633 634 Args: 635 reset_style: Boolean that determines whether a reset character should 636 be appended to the end of the string. 637 """ 638 639 return set_mode("bold", False) + text + (reset() if reset_style else "")
Returns text in bold.
Args
- reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
642def dim(text: str, reset_style: Optional[bool] = True) -> str: 643 """Returns text in dim. 644 645 Args: 646 reset_style: Boolean that determines whether a reset character should 647 be appended to the end of the string. 648 """ 649 650 return set_mode("dim", False) + text + (reset() if reset_style else "")
Returns text in dim.
Args
- reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
653def italic(text: str, reset_style: Optional[bool] = True) -> str: 654 """Returns text in italic. 655 656 Args: 657 reset_style: Boolean that determines whether a reset character should 658 be appended to the end of the string. 659 """ 660 661 return set_mode("italic", False) + text + (reset() if reset_style else "")
Returns text in italic.
Args
- reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
664def underline(text: str, reset_style: Optional[bool] = True) -> str: 665 """Returns text underlined. 666 667 Args: 668 reset_style: Boolean that determines whether a reset character should 669 be appended to the end of the string. 670 """ 671 672 return set_mode("underline", False) + text + (reset() if reset_style else "")
Returns text underlined.
Args
- reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
675def blink(text: str, reset_style: Optional[bool] = True) -> str: 676 """Returns text blinking. 677 678 Args: 679 reset_style: Boolean that determines whether a reset character should 680 be appended to the end of the string. 681 """ 682 683 return set_mode("blink", False) + text + (reset() if reset_style else "")
Returns text blinking.
Args
- reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
686def inverse(text: str, reset_style: Optional[bool] = True) -> str: 687 """Returns text inverse-colored. 688 689 Args: 690 reset_style: Boolean that determines whether a reset character should 691 be appended to the end of the string. 692 """ 693 694 return set_mode("inverse", False) + text + (reset() if reset_style else "")
Returns text inverse-colored.
Args
- reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
697def invisible(text: str, reset_style: Optional[bool] = True) -> str: 698 """Returns text as invisible. 699 700 Args: 701 reset_style: Boolean that determines whether a reset character should 702 be appended to the end of the string. 703 704 Note: 705 This isn't very widely supported. 706 """ 707 708 return set_mode("invisible", False) + text + (reset() if reset_style else "")
Returns text as invisible.
Args
- reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
Note
This isn't very widely supported.
711def strikethrough(text: str, reset_style: Optional[bool] = True) -> str: 712 """Return text as strikethrough. 713 714 Args: 715 reset_style: Boolean that determines whether a reset character should 716 be appended to the end of the string. 717 """ 718 719 return set_mode("strikethrough", False) + text + (reset() if reset_style else "")
Return text as strikethrough.
Args
- reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
722def overline(text: str, reset_style: Optional[bool] = True) -> str: 723 """Return text overlined. 724 725 Args: 726 reset_style: Boolean that determines whether a reset character should 727 be appended to the end of the string. 728 729 Note: 730 This isnt' very widely supported. 731 """ 732 733 return set_mode("overline", False) + text + (reset() if reset_style else "")
Return text overlined.
Args
- reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
Note
This isnt' very widely supported.