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

Saves the contents of the screen, and wipes it.

Use restore_screen() to get them back.

#   def restore_screen() -> None:
View Source
83def restore_screen() -> None:
84    """Restores the contents of the screen saved by `save_screen()`."""
85
86    print("\x1b[?47l")

Restores the contents of the screen saved by save_screen().

#   def set_alt_buffer() -> None:
View Source
89def set_alt_buffer() -> None:
90    """Starts an alternate buffer."""
91
92    print("\x1b[?1049h")

Starts an alternate buffer.

#   def unset_alt_buffer() -> None:
View Source
95def unset_alt_buffer() -> None:
96    """Returns to main buffer, restoring its original state."""
97
98    print("\x1b[?1049l")

Returns to main buffer, restoring its original state.

#   def clear(what: str = 'screen') -> None:
View Source
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])

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
#   def hide_cursor() -> None:
View Source
129def hide_cursor() -> None:
130    """Stops printing the cursor."""
131
132    print("\x1b[?25l")

Stops printing the cursor.

#   def show_cursor() -> None:
View Source
135def show_cursor() -> None:
136    """Starts printing the cursor."""
137
138    print("\x1b[?25h")

Starts printing the cursor.

#   def save_cursor() -> None:
View Source
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")

Saves the current cursor position.

Use restore_cursor() to restore it.

#   def restore_cursor() -> None:
View Source
150def restore_cursor() -> None:
151    """Restore cursor position as saved by `save_cursor`."""
152
153    terminal.write("\x1b[u")

Restore cursor position as saved by save_cursor.

#   def report_cursor() -> tuple[int, int] | None:
View Source
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)

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.

#   def move_cursor(pos: tuple[int, int]) -> None:
View Source
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")

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().

#   def cursor_up(num: int = 1) -> None:
View Source
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")

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().

#   def cursor_down(num: int = 1) -> None:
View Source
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")

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().

#   def cursor_right(num: int = 1) -> None:
View Source
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")

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().

#   def cursor_left(num: int = 1) -> None:
View Source
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")

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().

#   def cursor_next_line(num: int = 1) -> None:
View Source
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")

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().

#   def cursor_prev_line(num: int = 1) -> None:
View Source
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")

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().

#   def cursor_column(num: int = 0) -> None:
View Source
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")

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().

#   def cursor_home() -> None:
View Source
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")

Moves cursor to terminal.origin.

Note

This does not flush the terminal for performance reasons. You can do it manually with sys.stdout.flush().

#   def set_echo() -> None:
View Source
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")

Starts echoing of user input.

Note

This is currently only available on POSIX.

#   def unset_echo() -> None:
View Source
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")

Stops echoing of user input.

Note

This is currently only available on POSIX.

#   def set_mode(mode: Union[str, int], write: bool = True) -> str:
View Source
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

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
#   class MouseAction(enum.Enum):
View Source
377class MouseAction(Enum):
378    """An enumeration of all the polled mouse actions"""
379
380    LEFT_CLICK = _auto()
381    """Start of a left button action sequence."""
382
383    LEFT_DRAG = _auto()
384    """Mouse moved while left button was held down."""
385
386    RIGHT_CLICK = _auto()
387    """Start of a right button action sequence."""
388
389    RIGHT_DRAG = _auto()
390    """Mouse moved while right button was held down."""
391
392    SCROLL_UP = _auto()
393    """Mouse wheel or touchpad scroll upwards."""
394
395    SCROLL_DOWN = _auto()
396    """Mouse wheel or touchpad scroll downwards."""
397
398    HOVER = _auto()
399    """Mouse moved without clicking."""
400
401    # TODO: Support left & right mouse release separately, without breaking
402    #       current API.
403    RELEASE = _auto()
404    """Mouse button released; end of any and all mouse action sequences."""

An enumeration of all the polled mouse actions

#   LEFT_CLICK = <MouseAction.LEFT_CLICK: 1>

Start of a left button action sequence.

#   LEFT_DRAG = <MouseAction.LEFT_DRAG: 2>

Mouse moved while left button was held down.

#   RIGHT_CLICK = <MouseAction.RIGHT_CLICK: 3>

Start of a right button action sequence.

#   RIGHT_DRAG = <MouseAction.RIGHT_DRAG: 4>

Mouse moved while right button was held down.

#   SCROLL_UP = <MouseAction.SCROLL_UP: 5>

Mouse wheel or touchpad scroll upwards.

#   SCROLL_DOWN = <MouseAction.SCROLL_DOWN: 6>

Mouse wheel or touchpad scroll downwards.

#   HOVER = <MouseAction.HOVER: 7>

Mouse moved without clicking.

#   RELEASE = <MouseAction.RELEASE: 8>

Mouse button released; end of any and all mouse action sequences.

Inherited Members
enum.Enum
name
value
#  
@dataclass
class MouseEvent:
View Source
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}

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(...)
#   MouseEvent( action: pytermgui.ansi_interface.MouseAction, position: tuple[int, int] )
#   def is_scroll(self) -> bool:
View Source
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}

Returns True if event.action is one of the scrolling actions.

#   def is_primary(self) -> bool:
View Source
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}

Returns True if event.action is one of the primary (left-button) actions.

#   def is_secondary(self) -> bool:
View Source
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}

Returns True if event.action is one of the secondary (secondary-button) actions.

#   def report_mouse( event: str, method: Optional[str] = 'decimal_xterm', stop: bool = False ) -> None:
View Source
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)

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.

#   def translate_mouse( code: str, method: str ) -> list[pytermgui.ansi_interface.MouseEvent | None] | None:
View Source
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

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.

#   def reset() -> str:
View Source
623def reset() -> str:
624    """Resets printing mode."""
625
626    return set_mode("reset", False)

Resets printing mode.

#   def bold(text: str, reset_style: Optional[bool] = True) -> str:
View Source
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 "")

Returns text in bold.

Args
  • reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
#   def dim(text: str, reset_style: Optional[bool] = True) -> str:
View Source
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 "")

Returns text in dim.

Args
  • reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
#   def italic(text: str, reset_style: Optional[bool] = True) -> str:
View Source
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 "")

Returns text in italic.

Args
  • reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
#   def underline(text: str, reset_style: Optional[bool] = True) -> str:
View Source
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 "")

Returns text underlined.

Args
  • reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
#   def inverse(text: str, reset_style: Optional[bool] = True) -> str:
View Source
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 "")

Returns text inverse-colored.

Args
  • reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
#   def invisible(text: str, reset_style: Optional[bool] = True) -> str:
View Source
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 "")

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.

#   def strikethrough(text: str, reset_style: Optional[bool] = True) -> str:
View Source
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 "")

Return text as strikethrough.

Args
  • reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
#   def overline(text: str, reset_style: Optional[bool] = True) -> str:
View Source
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 "")

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.