flexablelib-tui 3.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flexablelib_tui/__init__.py +27 -0
- flexablelib_tui/app.py +640 -0
- flexablelib_tui/keys.py +340 -0
- flexablelib_tui/py.typed +0 -0
- flexablelib_tui/screen.py +276 -0
- flexablelib_tui/terminal.py +239 -0
- flexablelib_tui/widgets.py +624 -0
- flexablelib_tui-3.3.0.dist-info/METADATA +41 -0
- flexablelib_tui-3.3.0.dist-info/RECORD +12 -0
- flexablelib_tui-3.3.0.dist-info/WHEEL +5 -0
- flexablelib_tui-3.3.0.dist-info/licenses/LICENSE +21 -0
- flexablelib_tui-3.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TERMINAL INPUT MODE
|
|
3
|
+
|
|
4
|
+
:class:`Terminal` puts the console in the mode an interactive interface
|
|
5
|
+
needs -- keys delivered one at a time, not echoed, Ctrl-C delivered as a key --
|
|
6
|
+
and restores it on exit, on Linux and macOS (``termios``) and on Windows
|
|
7
|
+
(``msvcrt`` plus virtual-terminal output through ``SetConsoleMode``).
|
|
8
|
+
|
|
9
|
+
>>> with Terminal() as term:
|
|
10
|
+
... for key in term.read_keys(timeout=0.1):
|
|
11
|
+
... print(key)
|
|
12
|
+
|
|
13
|
+
It also reports size changes: ``term.resized()`` is true once after the window
|
|
14
|
+
changed size (``SIGWINCH`` where there is one, polling elsewhere).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
from flexablelib.utils import get_window_size
|
|
22
|
+
|
|
23
|
+
from .keys import KeyParser, WindowsKeyParser
|
|
24
|
+
|
|
25
|
+
__all__ = ['Terminal', 'WINDOWS']
|
|
26
|
+
|
|
27
|
+
WINDOWS = os.name == 'nt'
|
|
28
|
+
|
|
29
|
+
# How long a lone Escape waits for the rest of a possible sequence.
|
|
30
|
+
ESCAPE_DELAY = 0.025
|
|
31
|
+
|
|
32
|
+
MOUSE_ON = '\x1b[?1000h\x1b[?1006h'
|
|
33
|
+
MOUSE_OFF = '\x1b[?1006l\x1b[?1000l'
|
|
34
|
+
PASTE_ON = '\x1b[?2004h'
|
|
35
|
+
PASTE_OFF = '\x1b[?2004l'
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Terminal(object):
|
|
39
|
+
"""
|
|
40
|
+
Raw keyboard input and resize detection for an interactive session.
|
|
41
|
+
|
|
42
|
+
``stdin`` / ``stdout`` default to the process's. ``mouse=True`` asks the
|
|
43
|
+
terminal for mouse reports (clicks and the wheel), ``paste=True`` (the
|
|
44
|
+
default) for bracketed paste, so pasted text arrives as one ``'paste'``
|
|
45
|
+
key instead of keystrokes.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, stdin=None, stdout=None, mouse=False, paste=True):
|
|
49
|
+
self.stdin = stdin if stdin is not None else sys.stdin
|
|
50
|
+
self.stdout = stdout if stdout is not None else sys.stdout
|
|
51
|
+
self.mouse = bool(mouse)
|
|
52
|
+
self.paste = bool(paste)
|
|
53
|
+
self.parser = WindowsKeyParser() if WINDOWS else KeyParser()
|
|
54
|
+
self._saved = None
|
|
55
|
+
self._old_winch = None
|
|
56
|
+
self._resized = False
|
|
57
|
+
self._size = None
|
|
58
|
+
self._active = False
|
|
59
|
+
self._win_mode = None
|
|
60
|
+
|
|
61
|
+
# -- mode ---------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
def _fileno(self):
|
|
64
|
+
try:
|
|
65
|
+
return self.stdin.fileno()
|
|
66
|
+
except (AttributeError, ValueError, OSError):
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
def __enter__(self):
|
|
70
|
+
self.start()
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def __exit__(self, *exc):
|
|
74
|
+
self.stop()
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
def start(self):
|
|
78
|
+
if self._active:
|
|
79
|
+
return self
|
|
80
|
+
self._active = True
|
|
81
|
+
self._size = self.size()
|
|
82
|
+
if WINDOWS:
|
|
83
|
+
self._enable_vt()
|
|
84
|
+
else:
|
|
85
|
+
self._enter_cbreak()
|
|
86
|
+
self._install_winch()
|
|
87
|
+
codes = (MOUSE_ON if self.mouse else '') + \
|
|
88
|
+
(PASTE_ON if self.paste else '')
|
|
89
|
+
if codes:
|
|
90
|
+
self._write(codes)
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
def stop(self):
|
|
94
|
+
if not self._active:
|
|
95
|
+
return
|
|
96
|
+
self._active = False
|
|
97
|
+
codes = (MOUSE_OFF if self.mouse else '') + \
|
|
98
|
+
(PASTE_OFF if self.paste else '')
|
|
99
|
+
if codes:
|
|
100
|
+
self._write(codes)
|
|
101
|
+
if WINDOWS:
|
|
102
|
+
self._restore_vt()
|
|
103
|
+
else:
|
|
104
|
+
self._restore_cbreak()
|
|
105
|
+
self._remove_winch()
|
|
106
|
+
|
|
107
|
+
def _write(self, text):
|
|
108
|
+
try:
|
|
109
|
+
self.stdout.write(text)
|
|
110
|
+
self.stdout.flush()
|
|
111
|
+
except (AttributeError, ValueError, OSError):
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
def _enter_cbreak(self):
|
|
115
|
+
fd = self._fileno()
|
|
116
|
+
if fd is None or not os.isatty(fd):
|
|
117
|
+
return
|
|
118
|
+
import termios
|
|
119
|
+
self._saved = termios.tcgetattr(fd)
|
|
120
|
+
mode = termios.tcgetattr(fd)
|
|
121
|
+
# Keys one at a time, no echo, Ctrl-C/Z/S/Q delivered as keys, CR not
|
|
122
|
+
# turned into LF. Output processing stays on so '\n' still returns
|
|
123
|
+
# the carriage.
|
|
124
|
+
mode[0] &= ~(termios.IXON | termios.ICRNL | termios.BRKINT |
|
|
125
|
+
termios.INPCK | termios.ISTRIP)
|
|
126
|
+
mode[3] &= ~(termios.ICANON | termios.ECHO | termios.ISIG |
|
|
127
|
+
termios.IEXTEN)
|
|
128
|
+
mode[6][termios.VMIN] = 0
|
|
129
|
+
mode[6][termios.VTIME] = 0
|
|
130
|
+
termios.tcsetattr(fd, termios.TCSANOW, mode)
|
|
131
|
+
|
|
132
|
+
def _restore_cbreak(self):
|
|
133
|
+
if self._saved is None:
|
|
134
|
+
return
|
|
135
|
+
import termios
|
|
136
|
+
termios.tcsetattr(self._fileno(), termios.TCSADRAIN, self._saved)
|
|
137
|
+
self._saved = None
|
|
138
|
+
|
|
139
|
+
def _install_winch(self):
|
|
140
|
+
import signal
|
|
141
|
+
if not hasattr(signal, 'SIGWINCH'): # pragma: no cover
|
|
142
|
+
return
|
|
143
|
+
try:
|
|
144
|
+
self._old_winch = signal.signal(signal.SIGWINCH, self._on_winch)
|
|
145
|
+
except ValueError: # not the main thread
|
|
146
|
+
self._old_winch = None
|
|
147
|
+
|
|
148
|
+
def _remove_winch(self):
|
|
149
|
+
import signal
|
|
150
|
+
if self._old_winch is not None:
|
|
151
|
+
signal.signal(signal.SIGWINCH, self._old_winch)
|
|
152
|
+
self._old_winch = None
|
|
153
|
+
|
|
154
|
+
def _on_winch(self, *args):
|
|
155
|
+
self._resized = True
|
|
156
|
+
|
|
157
|
+
def _enable_vt(self): # pragma: no cover - Windows only
|
|
158
|
+
try:
|
|
159
|
+
import ctypes
|
|
160
|
+
kernel32 = ctypes.windll.kernel32
|
|
161
|
+
handle = kernel32.GetStdHandle(-11) # STD_OUTPUT_HANDLE
|
|
162
|
+
mode = ctypes.c_uint32()
|
|
163
|
+
if kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
|
164
|
+
self._win_mode = (handle, mode.value)
|
|
165
|
+
# ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
166
|
+
kernel32.SetConsoleMode(handle, mode.value | 0x0001 | 0x0004)
|
|
167
|
+
except (AttributeError, OSError):
|
|
168
|
+
self._win_mode = None
|
|
169
|
+
|
|
170
|
+
def _restore_vt(self): # pragma: no cover - Windows only
|
|
171
|
+
if self._win_mode is None:
|
|
172
|
+
return
|
|
173
|
+
import ctypes
|
|
174
|
+
handle, mode = self._win_mode
|
|
175
|
+
ctypes.windll.kernel32.SetConsoleMode(handle, mode)
|
|
176
|
+
self._win_mode = None
|
|
177
|
+
|
|
178
|
+
# -- size ---------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
def size(self):
|
|
181
|
+
"""``(columns, lines)`` of the terminal."""
|
|
182
|
+
return get_window_size()
|
|
183
|
+
|
|
184
|
+
def resized(self):
|
|
185
|
+
"""True once after the window changed size."""
|
|
186
|
+
size = self.size()
|
|
187
|
+
changed = self._resized or size != self._size
|
|
188
|
+
self._resized = False
|
|
189
|
+
self._size = size
|
|
190
|
+
return changed
|
|
191
|
+
|
|
192
|
+
# -- input --------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
def _read_raw(self, timeout):
|
|
195
|
+
"""Whatever input is available within ``timeout`` seconds."""
|
|
196
|
+
if WINDOWS:
|
|
197
|
+
import msvcrt
|
|
198
|
+
deadline = time.monotonic() + max(0.0, timeout)
|
|
199
|
+
chars = []
|
|
200
|
+
while True:
|
|
201
|
+
while msvcrt.kbhit():
|
|
202
|
+
chars.append(msvcrt.getwch())
|
|
203
|
+
if chars or time.monotonic() >= deadline:
|
|
204
|
+
return ''.join(chars)
|
|
205
|
+
time.sleep(0.005)
|
|
206
|
+
fd = self._fileno()
|
|
207
|
+
if fd is None:
|
|
208
|
+
time.sleep(max(0.0, timeout))
|
|
209
|
+
return b''
|
|
210
|
+
import select
|
|
211
|
+
try:
|
|
212
|
+
ready, _, _ = select.select([fd], [], [], max(0.0, timeout))
|
|
213
|
+
except (InterruptedError, OSError, ValueError):
|
|
214
|
+
return b''
|
|
215
|
+
if not ready:
|
|
216
|
+
return b''
|
|
217
|
+
try:
|
|
218
|
+
return os.read(fd, 4096)
|
|
219
|
+
except (BlockingIOError, InterruptedError):
|
|
220
|
+
return b''
|
|
221
|
+
|
|
222
|
+
def read_keys(self, timeout=None):
|
|
223
|
+
"""
|
|
224
|
+
The keys pressed within ``timeout`` seconds (``None``: wait for one).
|
|
225
|
+
A lone Escape is reported after a short pause with no follow-up.
|
|
226
|
+
"""
|
|
227
|
+
keys = []
|
|
228
|
+
while True:
|
|
229
|
+
data = self._read_raw(0.05 if timeout is None else timeout)
|
|
230
|
+
if data:
|
|
231
|
+
keys.extend(self.parser.feed(data))
|
|
232
|
+
if self.parser.pending:
|
|
233
|
+
more = self._read_raw(ESCAPE_DELAY)
|
|
234
|
+
if more:
|
|
235
|
+
keys.extend(self.parser.feed(more))
|
|
236
|
+
continue
|
|
237
|
+
keys.extend(self.parser.flush())
|
|
238
|
+
if keys or timeout is not None or self._resized:
|
|
239
|
+
return keys
|