summaryrefslogtreecommitdiff
path: root/cmd2/rl_utils.py
blob: 634f0c5eec221e76448b1d2b23b6aefdc6fd4262 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# coding=utf-8
"""
Imports the proper readline for the platform and provides utility functions for it
"""
from enum import Enum
import sys

# Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit)
try:
    import gnureadline as readline
except ImportError:
    # Try to import readline, but allow failure for convenience in Windows unit testing
    # Note: If this actually fails, you should install readline on Linux or Mac or pyreadline on Windows
    try:
        # noinspection PyUnresolvedReferences
        import readline
    except ImportError:  # pragma: no cover
        pass


class RlType(Enum):
    """Readline library types we recognize"""
    GNU = 1
    PYREADLINE = 2
    NONE = 3


# Check what implementation of readline we are using
rl_type = RlType.NONE

# The order of this check matters since importing pyreadline will also show readline in the modules list
if 'pyreadline' in sys.modules:
    rl_type = RlType.PYREADLINE

    from ctypes import byref
    from ctypes.wintypes import DWORD, HANDLE
    import atexit

    # noinspection PyPep8Naming
    def enable_win_vt100(handle: HANDLE) -> None:
        """
        Enables VT100 character sequences in a Windows console
        This only works on Windows 10 and up
        """
        ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004

        # Get the current mode for this handle in the console
        cur_mode = DWORD(0)
        readline.rl.console.GetConsoleMode(handle, byref(cur_mode))

        # If ENABLE_VIRTUAL_TERMINAL_PROCESSING is not enabled, then enable it now
        if (cur_mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0:
            readline.rl.console.SetConsoleMode(handle, cur_mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING)

            # Restore the original mode when we exit
            atexit.register(readline.rl.console.SetConsoleMode, handle, cur_mode)

    # Enable VT100 sequences for stdout and stderr
    STD_OUT_HANDLE = -11
    STD_ERROR_HANDLE = -12
    enable_win_vt100(readline.rl.console.GetStdHandle(STD_OUT_HANDLE))
    enable_win_vt100(readline.rl.console.GetStdHandle(STD_ERROR_HANDLE))

    ############################################################################################################
    # pyreadline is incomplete in terms of the Python readline API. Add the missing functions we need.
    ############################################################################################################
    # readline.redisplay()
    try:
        getattr(readline, 'redisplay')
    except AttributeError:
        # noinspection PyProtectedMember
        readline.redisplay = readline.rl.mode._update_line

    # readline.remove_history_item()
    try:
        getattr(readline, 'remove_history_item')
    except AttributeError:
        # noinspection PyProtectedMember
        def pyreadline_remove_history_item(pos: int) -> None:
            """
            An implementation of remove_history_item() for pyreadline
            :param pos: The 0-based position in history to remove
            """
            # Save of the current location of the history cursor
            saved_cursor = readline.rl.mode._history.history_cursor

            # Delete the history item
            del(readline.rl.mode._history.history[pos])

            # Update the cursor if needed
            if saved_cursor > pos:
                readline.rl.mode._history.history_cursor -= 1

        readline.remove_history_item = pyreadline_remove_history_item

elif 'gnureadline' in sys.modules or 'readline' in sys.modules:
    # We don't support libedit
    if 'libedit' not in readline.__doc__:
        rl_type = RlType.GNU

        # Load the readline lib so we can access members of it
        import ctypes
        readline_lib = ctypes.CDLL(readline.__file__)


# noinspection PyProtectedMember
def rl_force_redisplay() -> None:
    """
    Causes readline to display the prompt and input text wherever the cursor is and start
    reading input from this location. This is the proper way to restore the input line after
    printing to the screen
    """
    if not sys.stdout.isatty():
        return

    if rl_type == RlType.GNU:  # pragma: no cover
        readline_lib.rl_forced_update_display()

        # After manually updating the display, readline asks that rl_display_fixed be set to 1 for efficiency
        display_fixed = ctypes.c_int.in_dll(readline_lib, "rl_display_fixed")
        display_fixed.value = 1

    elif rl_type == RlType.PYREADLINE:  # pragma: no cover
        # Call _print_prompt() first to set the new location of the prompt
        readline.rl.mode._print_prompt()
        readline.rl.mode._update_line()


# noinspection PyProtectedMember
def rl_get_point() -> int:
    """
    Returns the offset of the current cursor position in rl_line_buffer
    """
    if rl_type == RlType.GNU:  # pragma: no cover
        return ctypes.c_int.in_dll(readline_lib, "rl_point").value

    elif rl_type == RlType.PYREADLINE:  # pragma: no cover
        return readline.rl.mode.l_buffer.point

    else:  # pragma: no cover
        return 0