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