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 "")
def save_screen() -> None:
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.

def restore_screen() -> None:
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().

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

Starts an alternate buffer.

def unset_alt_buffer() -> None:
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.

def clear(what: str = 'screen') -> None:
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
def hide_cursor() -> None:
130def hide_cursor() -> None:
131    """Stops printing the cursor."""
132
133    print("\x1b[?25l")

Stops printing the cursor.

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

Starts printing the cursor.

def save_cursor() -> None:
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.

def restore_cursor() -> None:
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.

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

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

def cursor_up(num: int = 1) -> None:
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().

def cursor_down(num: int = 1) -> None:
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().

def cursor_right(num: int = 1) -> None:
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().

def cursor_left(num: int = 1) -> None:
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().

def cursor_next_line(num: int = 1) -> None:
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().

def cursor_prev_line(num: int = 1) -> None:
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().

def cursor_column(num: int = 0) -> None:
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().

def cursor_home() -> None:
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().

def set_echo() -> None:
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.

def unset_echo() -> None:
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.

def set_mode(mode: Union[str, int], write: bool = True) -> str:
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
class MouseAction(enum.Enum):
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

LEFT_CLICK = <MouseAction.LEFT_CLICK: 'left_click'>

Start of a left button action sequence.

LEFT_DRAG = <MouseAction.LEFT_DRAG: 'left_drag'>

Mouse moved while left button was held down.

RIGHT_CLICK = <MouseAction.RIGHT_CLICK: 'right_click'>

Start of a right button action sequence.

RIGHT_DRAG = <MouseAction.RIGHT_DRAG: 'right_drag'>

Mouse moved while right button was held down.

SCROLL_UP = <MouseAction.SCROLL_UP: 'scroll_up'>

Mouse wheel or touchpad scroll upwards.

SCROLL_DOWN = <MouseAction.SCROLL_DOWN: 'scroll_down'>

Mouse wheel or touchpad scroll downwards.

HOVER = <MouseAction.HOVER: 'hover'>

Mouse moved without clicking.

RELEASE = <MouseAction.RELEASE: 'release'>

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

Inherited Members
enum.Enum
name
value
@dataclass
class MouseEvent:
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(...)
MouseEvent( action: pytermgui.ansi_interface.MouseAction, position: tuple[int, int])
def is_scroll(self) -> bool:
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.

def is_primary(self) -> bool:
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.

def is_secondary(self) -> bool:
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.

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

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

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

Resets printing mode.

def bold(text: str, reset_style: Optional[bool] = True) -> str:
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.
def dim(text: str, reset_style: Optional[bool] = True) -> str:
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.
def italic(text: str, reset_style: Optional[bool] = True) -> str:
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.
def underline(text: str, reset_style: Optional[bool] = True) -> str:
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.
def inverse(text: str, reset_style: Optional[bool] = True) -> str:
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.
def invisible(text: str, reset_style: Optional[bool] = True) -> str:
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.

def strikethrough(text: str, reset_style: Optional[bool] = True) -> str:
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.
def overline(text: str, reset_style: Optional[bool] = True) -> str:
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.