pytermgui.ansi_interface

Various functions to interface with the terminal, using ANSI sequences.

Credits:

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

Saves the contents of the screen, and wipes it.

Use restore_screen() to get them back.

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

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

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

Starts an alternate buffer.

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

Returns to main buffer, restoring its original state.

def clear(what: str = 'screen') -> None:
103def clear(what: str = "screen") -> None:
104    """Clears the specified screen region.
105
106    Args:
107        what: The specifier defining the screen area.
108
109    Available options:
110    * screen: clear whole screen and go to origin
111    * bos: clear screen from cursor backwards
112    * eos: clear screen from cursor forwards
113    * line: clear line and go to beginning
114    * bol: clear line from cursor backwards
115    * eol: clear line from cursor forwards
116    """
117
118    commands = {
119        "eos": "\x1b[0J",
120        "bos": "\x1b[1J",
121        "screen": "\x1b[2J",
122        "eol": "\x1b[0K",
123        "bol": "\x1b[1K",
124        "line": "\x1b[2K",
125    }
126
127    terminal.write(commands[what])

Clears the specified screen region.

Args
  • what: The specifier defining the screen area.

Available options:

  • screen: clear whole screen and go to origin
  • bos: clear screen from cursor backwards
  • eos: clear screen from cursor forwards
  • line: clear line and go to beginning
  • bol: clear line from cursor backwards
  • eol: clear line from cursor forwards
def hide_cursor() -> None:
131def hide_cursor() -> None:
132    """Stops printing the cursor."""
133
134    print("\x1b[?25l")

Stops printing the cursor.

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

Starts printing the cursor.

def save_cursor() -> None:
143def save_cursor() -> None:
144    """Saves the current cursor position.
145
146    Use `restore_cursor()` to restore it.
147    """
148
149    terminal.write("\x1b[s")

Saves the current cursor position.

Use restore_cursor() to restore it.

def restore_cursor() -> None:
152def restore_cursor() -> None:
153    """Restore cursor position as saved by `save_cursor`."""
154
155    terminal.write("\x1b[u")

Restore cursor position as saved by save_cursor.

def report_cursor() -> tuple[int, int] | None:
158def report_cursor() -> tuple[int, int] | None:
159    """Gets position of cursor.
160
161    Returns:
162        A tuple of integers, (columns, rows), describing the
163        current (printing) cursor's position. Returns None if
164        this could not be determined.
165
166        Note that this position is **not** the mouse position. See
167        `report_mouse` if that is what you are interested in.
168    """
169
170    print("\x1b[6n")
171    chars = getch()
172    posy, posx = chars[2:-1].split(";")
173
174    if not posx.isdigit() or not posy.isdigit():
175        return None
176
177    return int(posx), int(posy)

Gets position of cursor.

Returns

A tuple of integers, (columns, rows), describing the current (printing) cursor's position. Returns None if this could not be determined.

Note that this position is not the mouse position. See report_mouse if that is what you are interested in.

def move_cursor(pos: tuple[int, int]) -> None:
180def move_cursor(pos: tuple[int, int]) -> None:
181    """Moves the cursor.
182
183    Args:
184        pos: Tuple of (columns, rows) that the cursor will be moved to.
185
186    This does not flush the terminal for performance reasons. You
187    can do it manually with `sys.stdout.flush()`.
188    """
189
190    posx, posy = pos
191    terminal.write(f"\x1b[{posy};{posx}H")

Moves the cursor.

Args
  • pos: Tuple of (columns, rows) that the cursor will be moved to.

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

def cursor_up(num: int = 1) -> None:
194def cursor_up(num: int = 1) -> None:
195    """Moves the cursor up by `num` lines.
196
197    Args:
198        num: How many lines the cursor should move by. Must be positive,
199            to move in the opposite direction use `cursor_down`.
200    Note:
201        This does not flush the terminal for performance reasons. You
202        can do it manually with `sys.stdout.flush()`.
203    """
204
205    terminal.write(f"\x1b[{num}A")

Moves the cursor up by num lines.

Args
  • num: How many lines the cursor should move by. Must be positive, to move in the opposite direction use cursor_down.
Note

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

def cursor_down(num: int = 1) -> None:
208def cursor_down(num: int = 1) -> None:
209    """Moves the cursor up by `num` lines.
210
211    Args:
212        num: How many lines the cursor should move by. Must be positive,
213            to move in the opposite direction use `cursor_up`.
214    Note:
215        This does not flush the terminal for performance reasons. You
216        can do it manually with `sys.stdout.flush()`.
217    """
218
219    terminal.write(f"\x1b[{num}B")

Moves the cursor up by num lines.

Args
  • num: How many lines the cursor should move by. Must be positive, to move in the opposite direction use cursor_up.
Note

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

def cursor_right(num: int = 1) -> None:
222def cursor_right(num: int = 1) -> None:
223    """Moves the cursor right by `num` lines.
224
225    Args:
226        num: How many characters the cursor should move by. Must be positive,
227            to move in the opposite direction use `cursor_left`.
228    Note:
229        This does not flush the terminal for performance reasons. You
230        can do it manually with `sys.stdout.flush()`.
231    """
232
233    terminal.write(f"\x1b[{num}C")

Moves the cursor right by num lines.

Args
  • num: How many characters the cursor should move by. Must be positive, to move in the opposite direction use cursor_left.
Note

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

def cursor_left(num: int = 1) -> None:
236def cursor_left(num: int = 1) -> None:
237    """Moves the cursor left by `num` lines.
238
239    Args:
240        num: How many characters the cursor should move by. Must be positive,
241            to move in the opposite direction use `cursor_right`.
242    Note:
243        This does not flush the terminal for performance reasons. You
244        can do it manually with `sys.stdout.flush()`.
245    """
246
247    terminal.write(f"\x1b[{num}D")

Moves the cursor left by num lines.

Args
  • num: How many characters the cursor should move by. Must be positive, to move in the opposite direction use cursor_right.
Note

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

def cursor_next_line(num: int = 1) -> None:
250def cursor_next_line(num: int = 1) -> None:
251    """Moves the cursor to the beginning of the `num`-th line downwards.
252
253    Args:
254        num: The amount the cursor should move by. Must be positive, to move
255            in the opposite direction use `cursor_prev_line`.
256    Note:
257        This does not flush the terminal for performance reasons. You
258        can do it manually with `sys.stdout.flush()`.
259    """
260
261    terminal.write(f"\x1b[{num}E")

Moves the cursor to the beginning of the num-th line downwards.

Args
  • num: The amount the cursor should move by. Must be positive, to move in the opposite direction use cursor_prev_line.
Note

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

def cursor_prev_line(num: int = 1) -> None:
264def cursor_prev_line(num: int = 1) -> None:
265    """Moves the cursor to the beginning of the `num`-th line upwards.
266
267    Args:
268        num: The amount the cursor should move by. Must be positive, to move
269            in the opposite direction use `cursor_next_line`.
270    Note:
271        This does not flush the terminal for performance reasons. You
272        can do it manually with `sys.stdout.flush()`.
273    """
274
275    terminal.write(f"\x1b[{num}F")

Moves the cursor to the beginning of the num-th line upwards.

Args
  • num: The amount the cursor should move by. Must be positive, to move in the opposite direction use cursor_next_line.
Note

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

def cursor_column(num: int = 0) -> None:
278def cursor_column(num: int = 0) -> None:
279    """Moves the cursor to the `num`-th character of the current line.
280
281    Args:
282        num: The new cursor position.
283
284    Note:
285        This does not flush the terminal for performance reasons. You
286        can do it manually with `sys.stdout.flush()`.
287    """
288
289    terminal.write(f"\x1b[{num}G")

Moves the cursor to the num-th character of the current line.

Args
  • num: The new cursor position.
Note

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

def cursor_home() -> None:
292def cursor_home() -> None:
293    """Moves cursor to `terminal.origin`.
294
295    Note:
296        This does not flush the terminal for performance reasons. You
297        can do it manually with `sys.stdout.flush()`.
298    """
299
300    terminal.write("\x1b[H")

Moves cursor to terminal.origin.

Note

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

def set_echo() -> None:
353def set_echo() -> None:
354    """Starts echoing of user input.
355
356    Note:
357        This is currently only available on POSIX.
358    """
359
360    if not _name == "posix":
361        return
362
363    system("stty echo")

Starts echoing of user input.

Note

This is currently only available on POSIX.

def unset_echo() -> None:
366def unset_echo() -> None:
367    """Stops echoing of user input.
368
369    Note:
370        This is currently only available on POSIX.
371    """
372
373    if not _name == "posix":
374        return
375
376    system("stty -echo")

Stops echoing of user input.

Note

This is currently only available on POSIX.

def set_mode(mode: Union[str, int], write: bool = True) -> str:
303def set_mode(mode: Union[str, int], write: bool = True) -> str:
304    """Sets terminal display mode.
305
306    This is better left internal. To use these modes, you can call their
307    specific functions, such as `bold("text")` or `italic("text")`.
308
309    Args:
310        mode: One of the available modes. Strings and integers both work.
311        write: Boolean that determines whether the output should be written
312            to stdout.
313
314    Returns:
315        A string that sets the given mode.
316
317    Available modes:
318        - 0: reset
319        - 1: bold
320        - 2: dim
321        - 3: italic
322        - 4: underline
323        - 5: blink
324        - 7: inverse
325        - 8: invisible
326        - 9: strikethrough
327        - 53: overline
328    """
329
330    options = {
331        "reset": 0,
332        "bold": 1,
333        "dim": 2,
334        "italic": 3,
335        "underline": 4,
336        "blink": 5,
337        "inverse": 7,
338        "invisible": 8,
339        "strikethrough": 9,
340        "overline": 53,
341    }
342
343    if not str(mode).isdigit():
344        mode = options[str(mode)]
345
346    code = f"\x1b[{mode}m"
347    if write:
348        terminal.write(code)
349
350    return code

Sets terminal display mode.

This is better left internal. To use these modes, you can call their specific functions, such as bold("text") or italic("text").

Args
  • mode: One of the available modes. Strings and integers both work.
  • write: Boolean that determines whether the output should be written to stdout.
Returns

A string that sets the given mode.

Available modes
  • 0: reset
  • 1: bold
  • 2: dim
  • 3: italic
  • 4: underline
  • 5: blink
  • 7: inverse
  • 8: invisible
  • 9: strikethrough
  • 53: overline
class MouseAction(enum.Enum):
379class MouseAction(Enum):
380    """An enumeration of all the polled mouse actions"""
381
382    LEFT_CLICK = _auto()
383    """Start of a left button action sequence."""
384
385    LEFT_DRAG = _auto()
386    """Mouse moved while left button was held down."""
387
388    RIGHT_CLICK = _auto()
389    """Start of a right button action sequence."""
390
391    RIGHT_DRAG = _auto()
392    """Mouse moved while right button was held down."""
393
394    SCROLL_UP = _auto()
395    """Mouse wheel or touchpad scroll upwards."""
396
397    SCROLL_DOWN = _auto()
398    """Mouse wheel or touchpad scroll downwards."""
399
400    HOVER = _auto()
401    """Mouse moved without clicking."""
402
403    # TODO: Support left & right mouse release separately, without breaking
404    #       current API.
405    RELEASE = _auto()
406    """Mouse button released; end of any and all mouse action sequences."""

An enumeration of all the polled mouse actions

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:
409@dataclass
410class MouseEvent:
411    """A class to represent events created by mouse actions.
412
413    Its first argument is a `MouseAction` describing what happened,
414    and its second argument is a `tuple[int, int]` describing where
415    it happened.
416
417    This class mostly exists for readability & typing reasons. It also
418    implements the iterable protocol, so you can use the unpacking syntax,
419    such as:
420
421    ```python3
422    action, position = MouseEvent(...)
423    ```
424    """
425
426    action: MouseAction
427    position: tuple[int, int]
428
429    def __post_init__(self) -> None:
430        """Initialize iteration counter"""
431
432        self._iter_index = 0
433
434    def __next__(self) -> MouseAction | tuple[int, int]:
435        """Get next iteration item"""
436
437        data = fields(self)
438
439        if self._iter_index >= len(data):
440            self._iter_index = 0
441            raise StopIteration
442
443        self._iter_index += 1
444        return getattr(self, data[self._iter_index - 1].name)
445
446    def __iter__(self) -> MouseEvent:
447        """Start iteration"""
448
449        return self
450
451    def is_scroll(self) -> bool:
452        """Returns True if event.action is one of the scrolling actions."""
453
454        return self.action in {MouseAction.SCROLL_DOWN, MouseAction.SCROLL_UP}
455
456    def is_primary(self) -> bool:
457        """Returns True if event.action is one of the primary (left-button) actions."""
458
459        return self.action in {MouseAction.LEFT_CLICK, MouseAction.LEFT_DRAG}
460
461    def is_secondary(self) -> bool:
462        """Returns True if event.action is one of the secondary (secondary-button) actions."""
463
464        return self.action in {MouseAction.RIGHT_CLICK, MouseAction.RIGHT_DRAG}

A class to represent events created by mouse actions.

Its first argument is a MouseAction describing what happened, and its second argument is a tuple[int, int] describing where it happened.

This class mostly exists for readability & typing reasons. It also implements the iterable protocol, so you can use the unpacking syntax, such as:

action, position = MouseEvent(...)
MouseEvent( action: pytermgui.ansi_interface.MouseAction, position: tuple[int, int])
def is_scroll(self) -> bool:
451    def is_scroll(self) -> bool:
452        """Returns True if event.action is one of the scrolling actions."""
453
454        return self.action in {MouseAction.SCROLL_DOWN, MouseAction.SCROLL_UP}

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

def is_primary(self) -> bool:
456    def is_primary(self) -> bool:
457        """Returns True if event.action is one of the primary (left-button) actions."""
458
459        return self.action in {MouseAction.LEFT_CLICK, MouseAction.LEFT_DRAG}

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

def is_secondary(self) -> bool:
461    def is_secondary(self) -> bool:
462        """Returns True if event.action is one of the secondary (secondary-button) actions."""
463
464        return self.action in {MouseAction.RIGHT_CLICK, MouseAction.RIGHT_DRAG}

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

def report_mouse( event: str, method: Optional[str] = 'decimal_xterm', stop: bool = False) -> None:
467def report_mouse(
468    event: str, method: Optional[str] = "decimal_xterm", stop: bool = False
469) -> None:
470    """Starts reporting of mouse events.
471
472    You can specify multiple events to report on.
473
474    Args:
475        event: The type of event to report on. See below for options.
476        method: The method of reporting to use. See below for options.
477        stop: If set to True, the stopping code is written to stdout.
478
479    Raises:
480        NotImplementedError: The given event is not supported.
481
482    Note:
483        If you need this functionality, you're probably better off using the wrapper
484        `pytermgui.context_managers.mouse_handler`, which allows listening on multiple
485        events, gives a translator method and handles exceptions.
486
487    Possible events:
488        - **press**: Report when the mouse is clicked, left or right button.
489        - **highlight**: Report highlighting.
490        - **press_hold**: Report with a left or right click, as well as both
491            left & right drag and release.
492        - **hover**: Report even when no active action is done, only the mouse
493          is moved.
494
495    Methods:
496        - **None**: Non-decimal xterm method. Limited in coordinates.
497        - **decimal_xterm**: The default setting. Most universally supported.
498        - **decimal_urxvt**: Older, less compatible, but useful on some systems.
499        - **decimal_utf8**:  Apparently not too stable.
500
501    More information <a href='https://stackoverflow.com/a/5970472'>here</a>.
502    """
503
504    if event == "press":
505        terminal.write("\x1b[?1000")
506
507    elif event == "highlight":
508        terminal.write("\x1b[?1001")
509
510    elif event == "press_hold":
511        terminal.write("\x1b[?1002")
512
513    elif event == "hover":
514        terminal.write("\x1b[?1003")
515
516    else:
517        raise NotImplementedError(f"Mouse report event {event} is not supported!")
518
519    terminal.write("l" if stop else "h")
520
521    if method == "decimal_utf8":
522        terminal.write("\x1b[?1005")
523
524    elif method == "decimal_xterm":
525        terminal.write("\x1b[?1006")
526
527    elif method == "decimal_urxvt":
528        terminal.write("\x1b[?1015")
529
530    elif method is None:
531        return
532
533    else:
534        raise NotImplementedError(f"Mouse report method {method} is not supported!")
535
536    terminal.write("l" if stop else "h", flush=True)

Starts reporting of mouse events.

You can specify multiple events to report on.

Args
  • event: The type of event to report on. See below for options.
  • method: The method of reporting to use. See below for options.
  • stop: If set to True, the stopping code is written to stdout.
Raises
  • NotImplementedError: The given event is not supported.
Note

If you need this functionality, you're probably better off using the wrapper pytermgui.context_managers.mouse_handler, which allows listening on multiple events, gives a translator method and handles exceptions.

Possible events
  • press: Report when the mouse is clicked, left or right button.
  • highlight: Report highlighting.
  • press_hold: Report with a left or right click, as well as both left & right drag and release.
  • hover: Report even when no active action is done, only the mouse is moved.
Methods
  • None: Non-decimal xterm method. Limited in coordinates.
  • decimal_xterm: The default setting. Most universally supported.
  • decimal_urxvt: Older, less compatible, but useful on some systems.
  • decimal_utf8: Apparently not too stable.

More information here.

def translate_mouse( code: str, method: str) -> list[pytermgui.ansi_interface.MouseEvent | None] | None:
539def translate_mouse(code: str, method: str) -> list[MouseEvent | None] | None:
540    """Translates the output of produced by setting `report_mouse` into MouseEvents.
541
542    This method currently only supports `decimal_xterm` and `decimal_urxvt`.
543
544    Args:
545        code: The string of mouse code(s) to translate.
546        method: The reporting method to translate. One of [`decimal_xterm`, `decimal_urxvt`].
547
548    Returns:
549        A list of optional mouse events obtained from the code argument. If the code was malformed,
550        and no codes could be determined None is returned.
551    """
552
553    if code == "\x1b":
554        return None
555
556    mouse_codes = {
557        "decimal_xterm": {
558            "0M": MouseAction.LEFT_CLICK,
559            "0m": MouseAction.RELEASE,
560            "2M": MouseAction.RIGHT_CLICK,
561            "2m": MouseAction.RELEASE,
562            "32": MouseAction.LEFT_DRAG,
563            "34": MouseAction.RIGHT_DRAG,
564            "35": MouseAction.HOVER,
565            "64": MouseAction.SCROLL_UP,
566            "65": MouseAction.SCROLL_DOWN,
567        },
568        "decimal_urxvt": {
569            "32": MouseAction.LEFT_CLICK,
570            "34": MouseAction.RIGHT_CLICK,
571            "35": MouseAction.RELEASE,
572            "64": MouseAction.LEFT_DRAG,
573            "66": MouseAction.RIGHT_DRAG,
574            "96": MouseAction.SCROLL_UP,
575            "97": MouseAction.SCROLL_DOWN,
576        },
577    }
578
579    mapping = mouse_codes[method]
580    pattern: Pattern = RE_MOUSE[method]
581
582    events: list[MouseEvent | None] = []
583
584    for sequence in code.split("\x1b"):
585        if len(sequence) == 0:
586            continue
587
588        matches = list(pattern.finditer(sequence))
589        if len(matches) == 0:
590            return None
591
592        for match in matches:
593            identifier, *pos, release_code = match.groups()
594
595            # decimal_xterm uses the last character's
596            # capitalization to signify press/release state
597            if len(release_code) > 0 and identifier in ["0", "2"]:
598                identifier += release_code
599
600            if identifier in mapping:
601                action = mapping[identifier]
602                assert isinstance(action, MouseAction)
603
604                events.append(MouseEvent(action, (int(pos[0]), int(pos[1]))))
605                continue
606
607            events.append(None)
608
609    return events

Translates the output of produced by setting report_mouse into MouseEvents.

This method currently only supports decimal_xterm and decimal_urxvt.

Args
  • code: The string of mouse code(s) to translate.
  • method: The reporting method to translate. One of [decimal_xterm, decimal_urxvt].
Returns

A list of optional mouse events obtained from the code argument. If the code was malformed, and no codes could be determined None is returned.

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

Resets printing mode.

def bold(text: str, reset_style: Optional[bool] = True) -> str:
631def bold(text: str, reset_style: Optional[bool] = True) -> str:
632    """Returns text in bold.
633
634    Args:
635        reset_style: Boolean that determines whether a reset character should
636            be appended to the end of the string.
637    """
638
639    return set_mode("bold", False) + text + (reset() if reset_style else "")

Returns text in bold.

Args
  • reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
def dim(text: str, reset_style: Optional[bool] = True) -> str:
642def dim(text: str, reset_style: Optional[bool] = True) -> str:
643    """Returns text in dim.
644
645    Args:
646        reset_style: Boolean that determines whether a reset character should
647            be appended to the end of the string.
648    """
649
650    return set_mode("dim", False) + text + (reset() if reset_style else "")

Returns text in dim.

Args
  • reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
def italic(text: str, reset_style: Optional[bool] = True) -> str:
653def italic(text: str, reset_style: Optional[bool] = True) -> str:
654    """Returns text in italic.
655
656    Args:
657        reset_style: Boolean that determines whether a reset character should
658            be appended to the end of the string.
659    """
660
661    return set_mode("italic", False) + text + (reset() if reset_style else "")

Returns text in italic.

Args
  • reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
def underline(text: str, reset_style: Optional[bool] = True) -> str:
664def underline(text: str, reset_style: Optional[bool] = True) -> str:
665    """Returns text underlined.
666
667    Args:
668        reset_style: Boolean that determines whether a reset character should
669            be appended to the end of the string.
670    """
671
672    return set_mode("underline", False) + text + (reset() if reset_style else "")

Returns text underlined.

Args
  • reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
def inverse(text: str, reset_style: Optional[bool] = True) -> str:
686def inverse(text: str, reset_style: Optional[bool] = True) -> str:
687    """Returns text inverse-colored.
688
689    Args:
690        reset_style: Boolean that determines whether a reset character should
691            be appended to the end of the string.
692    """
693
694    return set_mode("inverse", False) + text + (reset() if reset_style else "")

Returns text inverse-colored.

Args
  • reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
def invisible(text: str, reset_style: Optional[bool] = True) -> str:
697def invisible(text: str, reset_style: Optional[bool] = True) -> str:
698    """Returns text as invisible.
699
700    Args:
701        reset_style: Boolean that determines whether a reset character should
702            be appended to the end of the string.
703
704    Note:
705        This isn't very widely supported.
706    """
707
708    return set_mode("invisible", False) + text + (reset() if reset_style else "")

Returns text as invisible.

Args
  • reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
Note

This isn't very widely supported.

def strikethrough(text: str, reset_style: Optional[bool] = True) -> str:
711def strikethrough(text: str, reset_style: Optional[bool] = True) -> str:
712    """Return text as strikethrough.
713
714    Args:
715        reset_style: Boolean that determines whether a reset character should
716            be appended to the end of the string.
717    """
718
719    return set_mode("strikethrough", False) + text + (reset() if reset_style else "")

Return text as strikethrough.

Args
  • reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
def overline(text: str, reset_style: Optional[bool] = True) -> str:
722def overline(text: str, reset_style: Optional[bool] = True) -> str:
723    """Return text overlined.
724
725    Args:
726        reset_style: Boolean that determines whether a reset character should
727            be appended to the end of the string.
728
729    Note:
730        This isnt' very widely supported.
731    """
732
733    return set_mode("overline", False) + text + (reset() if reset_style else "")

Return text overlined.

Args
  • reset_style: Boolean that determines whether a reset character should be appended to the end of the string.
Note

This isnt' very widely supported.