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.
@@ -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