forge-terminal-ui 0.0.4__tar.gz
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.
- forge_terminal_ui-0.0.4/PKG-INFO +19 -0
- forge_terminal_ui-0.0.4/README.md +2 -0
- forge_terminal_ui-0.0.4/forge_terminal_ui.egg-info/PKG-INFO +19 -0
- forge_terminal_ui-0.0.4/forge_terminal_ui.egg-info/SOURCES.txt +16 -0
- forge_terminal_ui-0.0.4/forge_terminal_ui.egg-info/dependency_links.txt +1 -0
- forge_terminal_ui-0.0.4/forge_terminal_ui.egg-info/requires.txt +1 -0
- forge_terminal_ui-0.0.4/forge_terminal_ui.egg-info/top_level.txt +1 -0
- forge_terminal_ui-0.0.4/forgetui/termcontrol.py +362 -0
- forge_terminal_ui-0.0.4/forgetui/terminput.py +121 -0
- forge_terminal_ui-0.0.4/forgetui/termkeymap.py +70 -0
- forge_terminal_ui-0.0.4/forgetui/theme.py +235 -0
- forge_terminal_ui-0.0.4/forgetui/widget.py +584 -0
- forge_terminal_ui-0.0.4/forgetui/widget_container.py +314 -0
- forge_terminal_ui-0.0.4/forgetui/widget_input.py +303 -0
- forge_terminal_ui-0.0.4/forgetui/widget_output.py +102 -0
- forge_terminal_ui-0.0.4/forgetui/widget_terminal.py +80 -0
- forge_terminal_ui-0.0.4/pyproject.toml +32 -0
- forge_terminal_ui-0.0.4/setup.cfg +4 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forge-terminal-ui
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: Python TUI framework and terminal control
|
|
5
|
+
Author-email: Gretchen Maculo <gretchen.maculo@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/gretchycat/ForgeTUI
|
|
7
|
+
Project-URL: Repository, https://github.com/gretchycat/ForgeTUI
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
14
|
+
Requires-Python: >=3.7
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: libAnsiScreen
|
|
17
|
+
|
|
18
|
+
# forgetui
|
|
19
|
+
python library to control an ansi terminal
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forge-terminal-ui
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: Python TUI framework and terminal control
|
|
5
|
+
Author-email: Gretchen Maculo <gretchen.maculo@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/gretchycat/ForgeTUI
|
|
7
|
+
Project-URL: Repository, https://github.com/gretchycat/ForgeTUI
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
14
|
+
Requires-Python: >=3.7
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: libAnsiScreen
|
|
17
|
+
|
|
18
|
+
# forgetui
|
|
19
|
+
python library to control an ansi terminal
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
forge_terminal_ui.egg-info/PKG-INFO
|
|
4
|
+
forge_terminal_ui.egg-info/SOURCES.txt
|
|
5
|
+
forge_terminal_ui.egg-info/dependency_links.txt
|
|
6
|
+
forge_terminal_ui.egg-info/requires.txt
|
|
7
|
+
forge_terminal_ui.egg-info/top_level.txt
|
|
8
|
+
forgetui/termcontrol.py
|
|
9
|
+
forgetui/terminput.py
|
|
10
|
+
forgetui/termkeymap.py
|
|
11
|
+
forgetui/theme.py
|
|
12
|
+
forgetui/widget.py
|
|
13
|
+
forgetui/widget_container.py
|
|
14
|
+
forgetui/widget_input.py
|
|
15
|
+
forgetui/widget_output.py
|
|
16
|
+
forgetui/widget_terminal.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
libAnsiScreen
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
forgetui
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import sys, os, fcntl, select
|
|
4
|
+
from libansiscreen.screen import Screen
|
|
5
|
+
from libansiscreen.renderer.ansi_emitter import ANSIEmitter, Box
|
|
6
|
+
from forgetui.termkeymap import gen_keymap
|
|
7
|
+
|
|
8
|
+
rgb_file_path = '/usr/share/X11/rgb.txt'
|
|
9
|
+
|
|
10
|
+
class termcontrol:
|
|
11
|
+
def __init__(self):
|
|
12
|
+
#self.x11_colors = self.parse_rgb_file(rgb_file_path)
|
|
13
|
+
self.image_support=[]
|
|
14
|
+
self.img_cache={}
|
|
15
|
+
term=os.environ.get('TERM', '')
|
|
16
|
+
konsole_ver=os.environ.get('KONSOLE_VERSION', '')
|
|
17
|
+
if 'kitty' in term:
|
|
18
|
+
self.image_support.append('kitty')
|
|
19
|
+
if 'vt340' in term or len(konsole_ver or '')>0:
|
|
20
|
+
self.image_support.append('sixel')
|
|
21
|
+
|
|
22
|
+
def output(self, data, fd=sys.stdout.fileno()):
|
|
23
|
+
CHUNK_SIZE = 4096 # safe per-write chunk
|
|
24
|
+
"""Queue a string or bytes for non-blocking terminal output."""
|
|
25
|
+
outbuf = bytearray() # persistent buffer
|
|
26
|
+
if isinstance(data, str):
|
|
27
|
+
data = data.encode()
|
|
28
|
+
outbuf.extend(data)
|
|
29
|
+
# drain in chunks
|
|
30
|
+
while outbuf:
|
|
31
|
+
chunk = outbuf[:CHUNK_SIZE]
|
|
32
|
+
try:
|
|
33
|
+
n = os.write(fd, chunk)
|
|
34
|
+
del outbuf[:n]
|
|
35
|
+
except BlockingIOError:
|
|
36
|
+
# wait until fd is writable
|
|
37
|
+
select.select([], [fd], [])
|
|
38
|
+
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
# Mouse control
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
def enable_mouse(self, utf8=True, all_motion=False):
|
|
43
|
+
"""
|
|
44
|
+
Enable mouse reporting.
|
|
45
|
+
Parameters:
|
|
46
|
+
- utf8: Use UTF-8 or SGR encoding for coordinates (1005 / 1006)
|
|
47
|
+
- all_motion: Track all mouse motion events, even without buttons (1003)
|
|
48
|
+
"""
|
|
49
|
+
codes = []
|
|
50
|
+
# Base tracking
|
|
51
|
+
codes.append("\x1b[?1002h") # Button-event tracking (press/release + drag)
|
|
52
|
+
# Optional: all motion reporting
|
|
53
|
+
if all_motion:
|
|
54
|
+
codes.append("\x1b[?1003h") # Any-motion tracking (hover)
|
|
55
|
+
# Coordinate encoding
|
|
56
|
+
if utf8:
|
|
57
|
+
codes.append("\x1b[?1005h") # UTF-8 coordinates
|
|
58
|
+
codes.append("\x1b[?1006h") # SGR coordinates (decimal)
|
|
59
|
+
return "".join(codes)
|
|
60
|
+
|
|
61
|
+
def disable_mouse(self, utf8=True, all_motion=False):
|
|
62
|
+
"""
|
|
63
|
+
Disable mouse reporting.
|
|
64
|
+
"""
|
|
65
|
+
codes = []
|
|
66
|
+
# Base tracking
|
|
67
|
+
codes.append("\x1b[?1002l") # Disable button-event tracking
|
|
68
|
+
# Optional: disable all-motion reporting
|
|
69
|
+
if all_motion:
|
|
70
|
+
codes.append("\x1b[?1003l") # Disable any-motion tracking
|
|
71
|
+
# Coordinate encoding
|
|
72
|
+
if utf8:
|
|
73
|
+
codes.append("\x1b[?1005l") # Disable UTF-8 coordinates
|
|
74
|
+
codes.append("\x1b[?1006l") # Disable SGR coordinates
|
|
75
|
+
return "".join(codes)
|
|
76
|
+
|
|
77
|
+
def start_sync(self):
|
|
78
|
+
return "\x1b[?2026h"
|
|
79
|
+
|
|
80
|
+
def end_sync(self):
|
|
81
|
+
return "\x1b[?2026l"
|
|
82
|
+
|
|
83
|
+
def enable_cursor(self):
|
|
84
|
+
return "\x1b[?25h"
|
|
85
|
+
|
|
86
|
+
def disable_cursor(self):
|
|
87
|
+
return "\x1b[?25l"
|
|
88
|
+
|
|
89
|
+
def normal_screen(self):
|
|
90
|
+
return "\x1b[?1049l"
|
|
91
|
+
|
|
92
|
+
def alt_screen(self):
|
|
93
|
+
return "\x1b[?1049h"
|
|
94
|
+
|
|
95
|
+
def set_title(self, title):
|
|
96
|
+
return f"\x1b]0;{title}\\a"
|
|
97
|
+
|
|
98
|
+
def pause_terminal_output(self):
|
|
99
|
+
sys.stdout.flush()
|
|
100
|
+
os.system('stty -icanon -echo')
|
|
101
|
+
|
|
102
|
+
def resume_terminal_output(self):
|
|
103
|
+
sys.stdout.flush()
|
|
104
|
+
os.system('stty icanon echo')
|
|
105
|
+
|
|
106
|
+
def parse_rgb_file(self, file_path):
|
|
107
|
+
colors = {}
|
|
108
|
+
if not os.path.isfile(file_path):
|
|
109
|
+
return colors
|
|
110
|
+
with open(file_path, 'r') as file:
|
|
111
|
+
for line in file:
|
|
112
|
+
if not line.startswith('!'):
|
|
113
|
+
parts = line.strip().split('\t')
|
|
114
|
+
if len(parts) >= 4:
|
|
115
|
+
name = parts[3].lower()
|
|
116
|
+
red, green, blue = int(parts[0]), int(parts[1]), int(parts[2])
|
|
117
|
+
colors[name] = {'red':red, 'green':green, 'blue':blue}
|
|
118
|
+
return colors
|
|
119
|
+
|
|
120
|
+
def pause(self):
|
|
121
|
+
print ('[pause]')
|
|
122
|
+
return sys.stdin.readline()
|
|
123
|
+
|
|
124
|
+
def get_terminal_size(self):
|
|
125
|
+
import array, fcntl, sys, termios
|
|
126
|
+
buf = array.array('H', [0, 0, 0, 0])
|
|
127
|
+
fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, buf)
|
|
128
|
+
# Create a dictionary with meaningful keys
|
|
129
|
+
window_info = {
|
|
130
|
+
"rows": buf[0],
|
|
131
|
+
"columns": buf[1],
|
|
132
|
+
"width": buf[2],
|
|
133
|
+
"height": buf[3]
|
|
134
|
+
}
|
|
135
|
+
return window_info
|
|
136
|
+
|
|
137
|
+
def color(self, color):
|
|
138
|
+
if type(color)==int:
|
|
139
|
+
return color
|
|
140
|
+
co={
|
|
141
|
+
'black' : 0,
|
|
142
|
+
'red' : 1,
|
|
143
|
+
'green' : 2,
|
|
144
|
+
'yellow' : 3,
|
|
145
|
+
'brown' : 3,
|
|
146
|
+
'blue' : 4,
|
|
147
|
+
'magenta': 5,
|
|
148
|
+
'cyan' : 6,
|
|
149
|
+
'white' : 7,
|
|
150
|
+
'brightblack' : 8,
|
|
151
|
+
'brightred' : 9,
|
|
152
|
+
'brightgreen' : 10,
|
|
153
|
+
'brightbrown' : 11,
|
|
154
|
+
'brightyellow' : 11,
|
|
155
|
+
'brightblue' : 12,
|
|
156
|
+
'brightmagenta': 13,
|
|
157
|
+
'brightcyan' : 14,
|
|
158
|
+
'brightwhite' : 15,
|
|
159
|
+
}
|
|
160
|
+
if type(color)==str:
|
|
161
|
+
regex = r'^([A-Fa-f0-9]{6})$'
|
|
162
|
+
if re.match(regex, color) is not None:
|
|
163
|
+
color={
|
|
164
|
+
'red' :int(color[0:2], 16),
|
|
165
|
+
'green':int(color[2:4], 16),
|
|
166
|
+
'blue' :int(color[4:6], 16),
|
|
167
|
+
}
|
|
168
|
+
return color
|
|
169
|
+
regex = r'^([A-Fa-f0-9]{3})$'
|
|
170
|
+
if re.match(regex, color) is not None:
|
|
171
|
+
color={
|
|
172
|
+
'red' :int(color[0:1], 16)*16,
|
|
173
|
+
'green':int(color[1:2], 16)*16,
|
|
174
|
+
'blue' :int(color[2:3], 16)*16,
|
|
175
|
+
}
|
|
176
|
+
return color
|
|
177
|
+
regex = r'^#([A-Fa-f0-9]{6})$'
|
|
178
|
+
if re.match(regex, color) is not None:
|
|
179
|
+
color={
|
|
180
|
+
'red' :int(color[1:3], 16),
|
|
181
|
+
'green':int(color[3:5], 16),
|
|
182
|
+
'blue' :int(color[5:7], 16),
|
|
183
|
+
}
|
|
184
|
+
return color
|
|
185
|
+
regex = r'^#([A-Fa-f0-9]{3})$'
|
|
186
|
+
if re.match(regex, color) is not None:
|
|
187
|
+
color={
|
|
188
|
+
'red' :int(color[1:2], 16)*16,
|
|
189
|
+
'green':int(color[2:3], 16)*16,
|
|
190
|
+
'blue' :int(color[3:4], 16)*16,
|
|
191
|
+
}
|
|
192
|
+
return color
|
|
193
|
+
if co.get(color):
|
|
194
|
+
return co.get(color.lower())
|
|
195
|
+
return self.x11_colors.get(color.lower())
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
def getRGB(self, c):
|
|
199
|
+
color=self.color(c)
|
|
200
|
+
if type(color)==dict:
|
|
201
|
+
return color
|
|
202
|
+
if type(color)==int:
|
|
203
|
+
pass #FIXME
|
|
204
|
+
return {'red':127, 'green':127, 'blue':127}
|
|
205
|
+
|
|
206
|
+
def ansicolor(self, fg='default', bg='default',
|
|
207
|
+
bold=False, dim=False, italic=False, underline=False,
|
|
208
|
+
strike=False, blink=False, blink2=False, reverse=False,
|
|
209
|
+
bold_is_bright=False):
|
|
210
|
+
if fg=='default':
|
|
211
|
+
fg=7
|
|
212
|
+
if bg=='default':
|
|
213
|
+
bg=None
|
|
214
|
+
fg=self.color(fg)
|
|
215
|
+
bg=self.color(bg)
|
|
216
|
+
fgs=""
|
|
217
|
+
bgs=""
|
|
218
|
+
if type(fg)==int:
|
|
219
|
+
if fg<16:
|
|
220
|
+
if fg<8:
|
|
221
|
+
if bold_is_bright:
|
|
222
|
+
fgs=f"{fg+90}"
|
|
223
|
+
else:
|
|
224
|
+
fgs=f"{fg+30}"
|
|
225
|
+
else:
|
|
226
|
+
fgs=f"{(fg-8)+90}"
|
|
227
|
+
else:
|
|
228
|
+
fgs=f'38;5;{fg}'
|
|
229
|
+
elif type(fg)==dict:
|
|
230
|
+
fgs=f'38;2;{fg["red"]};{fg["green"]};{fg["blue"]}'
|
|
231
|
+
if type(bg)==int:
|
|
232
|
+
if bg<16:
|
|
233
|
+
if bg<8:
|
|
234
|
+
bgs=f"{bg+40}"
|
|
235
|
+
else:
|
|
236
|
+
bgs=f"{(bg-8)+100}"
|
|
237
|
+
else:
|
|
238
|
+
bgs=f'48;5;{bg}'
|
|
239
|
+
elif type(bg)==dict:
|
|
240
|
+
bgs=f'48;2;{bg["red"]};{bg["green"]};{bg["blue"]}'
|
|
241
|
+
bo, bl, bl2, dm, it, ul, st, rv="","","","","","","", ""
|
|
242
|
+
if bold:bo='1;'
|
|
243
|
+
if dim:bdm='2;'
|
|
244
|
+
if italic:it='3;'
|
|
245
|
+
if underline:ul='4;'
|
|
246
|
+
if blink:bl='5;'
|
|
247
|
+
if blink2:bl2='6;'
|
|
248
|
+
if reverse:rv='7;'
|
|
249
|
+
ansi=""
|
|
250
|
+
if len(bgs) and len(fgs):
|
|
251
|
+
ansi=f'{fgs};{bgs}'
|
|
252
|
+
elif len(bgs):
|
|
253
|
+
ansi=bgs
|
|
254
|
+
elif len(fgs):
|
|
255
|
+
ansi=fgs
|
|
256
|
+
if len(ansi)>0:
|
|
257
|
+
return f"\x1b[{bo}{dm}{it}{ul}{bl}{bl2}{rv}{ansi}m"
|
|
258
|
+
return ""
|
|
259
|
+
|
|
260
|
+
def drawRuler(self,w,h):
|
|
261
|
+
buffer=''
|
|
262
|
+
for y in range(0,h-2):
|
|
263
|
+
for x in range(0,int((w)/10)):
|
|
264
|
+
buffer+=self.gotoxy(x*10+1,y+1)
|
|
265
|
+
buffer+=f'({x*10},{y})'
|
|
266
|
+
return buffer
|
|
267
|
+
|
|
268
|
+
def pyte_render(self, x, y, screen, line=1,
|
|
269
|
+
fg='default', bg='default',
|
|
270
|
+
fg0='default', bg0='default',
|
|
271
|
+
bold_is_bright=False):
|
|
272
|
+
bold=False
|
|
273
|
+
blink=False
|
|
274
|
+
w=screen.columns
|
|
275
|
+
h=screen.screen_lines
|
|
276
|
+
start_line=line-1
|
|
277
|
+
if start_line<0:
|
|
278
|
+
start_line=int(screen.cursor.y-h+2)
|
|
279
|
+
if start_line<0:
|
|
280
|
+
start_line=0
|
|
281
|
+
if start_line>int(screen.cursor.y-h+2):
|
|
282
|
+
start_line=int(screen.cursor.y-h+2)
|
|
283
|
+
buffer = self.ansicolor(fg, bg, bold=bold, blink=blink)
|
|
284
|
+
screen.cursor_position(screen.screen_lines+start_line, 1)
|
|
285
|
+
for yy in range(start_line, start_line+h):
|
|
286
|
+
buffer += self.gotoxy(x, y+yy-(start_line))
|
|
287
|
+
buffer+=self.ansicolor(fg, bg)
|
|
288
|
+
for xx in range(w):
|
|
289
|
+
if screen.buffer[yy][xx].fg!=fg or screen.buffer[yy][xx].bold!=bold:
|
|
290
|
+
fg=screen.buffer[yy][xx].fg
|
|
291
|
+
bold=screen.buffer[yy][xx].bold
|
|
292
|
+
buffer += self.ansicolor(fg, None, bold=bold, bold_is_bright=bold_is_bright)
|
|
293
|
+
if screen.buffer[yy][xx].bg!=bg or screen.buffer[yy][xx].blink!=blink:
|
|
294
|
+
bg=screen.buffer[yy][xx].bg
|
|
295
|
+
blink=screen.buffer[yy][xx].blink
|
|
296
|
+
buffer += self.ansicolor(None, bg, blink=blink)
|
|
297
|
+
buffer += screen.buffer[yy][xx].data
|
|
298
|
+
buffer+=self.ansicolor(fg0, bg0)
|
|
299
|
+
return buffer
|
|
300
|
+
|
|
301
|
+
def gotoxy(self, x, y):
|
|
302
|
+
return f'\x1b[{int(y)};{int(x)}f'
|
|
303
|
+
|
|
304
|
+
def clear(self):
|
|
305
|
+
return '\x1b[2J'
|
|
306
|
+
|
|
307
|
+
def reset(self):
|
|
308
|
+
return '\x1b[0m'
|
|
309
|
+
|
|
310
|
+
def setbg(self, c):
|
|
311
|
+
return self.ansicolor(None, c)
|
|
312
|
+
|
|
313
|
+
def setfg(self, c):
|
|
314
|
+
return self.ansicolor(c, None)
|
|
315
|
+
|
|
316
|
+
def up(self, n):
|
|
317
|
+
return f'\x1b[{n}A'
|
|
318
|
+
|
|
319
|
+
def down(self, n):
|
|
320
|
+
return f'\x1b[{n}B'
|
|
321
|
+
|
|
322
|
+
def left(self, n):
|
|
323
|
+
return f'\x1b[{n}D'
|
|
324
|
+
|
|
325
|
+
def right(self, n):
|
|
326
|
+
return f'\x1b[{n}C'
|
|
327
|
+
|
|
328
|
+
def clear_images(self):
|
|
329
|
+
out=''
|
|
330
|
+
if 'kitty' in self.image_support:
|
|
331
|
+
out+='\x1b_Ga=d\x1b\\'
|
|
332
|
+
if 'sixel' in self.image_support:
|
|
333
|
+
pass
|
|
334
|
+
return out
|
|
335
|
+
|
|
336
|
+
def showImage(self, image, x=0, y=0, w=30, h=15, showInfo=False, mode='auto', charset='utf8'):
|
|
337
|
+
desc=""
|
|
338
|
+
imgX,imgY=0,0
|
|
339
|
+
if(showInfo):
|
|
340
|
+
try:
|
|
341
|
+
img = Image.open(image)
|
|
342
|
+
imgX,imgY=img.size
|
|
343
|
+
img.close()
|
|
344
|
+
except:
|
|
345
|
+
pass
|
|
346
|
+
filename=os.path.basename(image)
|
|
347
|
+
desc=f'({imgX}x{imgY}) {filename}'[:w]
|
|
348
|
+
descX=int(x+(w/2)-(len(desc)/2))+1
|
|
349
|
+
descY=int(y+h)-1
|
|
350
|
+
desc=f'\x1b[s\x1b[48;5;245;30m\x1b[{descY};{descX}H{desc}\n'
|
|
351
|
+
start_pos = f'\x1b[{y};{x+1}H'
|
|
352
|
+
if not self.img_cache.get(image):
|
|
353
|
+
ic=ICat(w=int(w), h=int(h), zoom='aspect', f=True, x=int(0), y=int(0), place=True, mode=mode, charset=charset)
|
|
354
|
+
self.img_cache[image]=ic.print(image)
|
|
355
|
+
return f'{start_pos}{self.img_cache[image]}{desc}'
|
|
356
|
+
|
|
357
|
+
def clean(input_string):
|
|
358
|
+
# Use regular expressions to replace consecutive whitespace characters with a single space
|
|
359
|
+
cleaned_string = re.sub(r'\s+', ' ', input_string)
|
|
360
|
+
# Remove leading and trailing spaces
|
|
361
|
+
return cleaned_string.strip()
|
|
362
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import sys, os, fcntl, select, asyncio, time, termios, tty, re
|
|
4
|
+
|
|
5
|
+
from .termkeymap import gen_keymap
|
|
6
|
+
|
|
7
|
+
class termInput:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
fps=30
|
|
10
|
+
self.timeout=1/fps
|
|
11
|
+
self.start=0
|
|
12
|
+
self.keymap=gen_keymap()
|
|
13
|
+
self.buffer=''
|
|
14
|
+
self.flags = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
|
|
15
|
+
self.filedescriptors = termios.tcgetattr(sys.stdin)
|
|
16
|
+
self.raw=False
|
|
17
|
+
self.__enter__()
|
|
18
|
+
|
|
19
|
+
def __enter__(self):
|
|
20
|
+
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, self.flags | os.O_NONBLOCK)
|
|
21
|
+
if self.raw:
|
|
22
|
+
tty.setraw(sys.stdin)
|
|
23
|
+
else:
|
|
24
|
+
tty.setcbreak(sys.stdin)
|
|
25
|
+
|
|
26
|
+
def __del__(self):
|
|
27
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.filedescriptors)
|
|
28
|
+
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, self.flags & ~os.O_NONBLOCK)
|
|
29
|
+
|
|
30
|
+
def __exit__(self):
|
|
31
|
+
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.filedescriptors)
|
|
32
|
+
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, self.flags & ~os.O_NONBLOCK)
|
|
33
|
+
|
|
34
|
+
def disable_keyboard_echo(self): # Get the current terminal attributes
|
|
35
|
+
attributes = termios.tcgetattr(sys.stdin)
|
|
36
|
+
# Disable echo flag
|
|
37
|
+
attributes[3] = attributes[3] & ~termios.ECHO
|
|
38
|
+
# Apply the modified attributes
|
|
39
|
+
termios.tcsetattr(sys.stdin, termios.TCSANOW, attributes)
|
|
40
|
+
|
|
41
|
+
def enable_keyboard_echo(self): # Get the current terminal attributes
|
|
42
|
+
attributes = termios.tcgetattr(sys.stdin)
|
|
43
|
+
# Enable echo flag
|
|
44
|
+
attributes[3] = attributes[3] | termios.ECHO
|
|
45
|
+
# Apply the modified attributes
|
|
46
|
+
termios.tcsetattr(sys.stdin, termios.TCSANOW, attributes)
|
|
47
|
+
|
|
48
|
+
def read(self, bin=False):
|
|
49
|
+
self.buffer = ''
|
|
50
|
+
try:
|
|
51
|
+
rlist, _, _ = select.select([sys.stdin], [], [], self.timeout)
|
|
52
|
+
except (InterruptedError, OSError) as e:
|
|
53
|
+
if isinstance(e, InterruptedError) or getattr(e, "errno", None) == errno.EINTR:
|
|
54
|
+
return self.buffer
|
|
55
|
+
raise
|
|
56
|
+
if rlist:
|
|
57
|
+
if not bin:
|
|
58
|
+
try:
|
|
59
|
+
# Read only a chunk of available buffer (e.g., 4096 bytes) instead of EOF
|
|
60
|
+
self.buffer += sys.stdin.read(4096)
|
|
61
|
+
except Exception:
|
|
62
|
+
self.buffer += sys.stdin.buffer.read(4096).decode('utf-8', errors='ignore')
|
|
63
|
+
else:
|
|
64
|
+
self.buffer += sys.stdin.buffer.read(4096)
|
|
65
|
+
return self.buffer
|
|
66
|
+
|
|
67
|
+
def ord(self, d):
|
|
68
|
+
if(type(d)==int):
|
|
69
|
+
return d
|
|
70
|
+
if(type(d)==str):
|
|
71
|
+
return ord(d[0])
|
|
72
|
+
if(type(d)==bytes):
|
|
73
|
+
return int.from_bytes(d)
|
|
74
|
+
return int(d)
|
|
75
|
+
|
|
76
|
+
def split_codes(self, buffer): #TODO split by all of the key map values and then every character individually
|
|
77
|
+
inputlist=buffer.split('\x1b')
|
|
78
|
+
for k,i in enumerate(inputlist[1:], start=1):
|
|
79
|
+
inputlist[k]='\x1b'+i
|
|
80
|
+
if inputlist[0]=='':
|
|
81
|
+
inputlist.pop(0)
|
|
82
|
+
return inputlist
|
|
83
|
+
|
|
84
|
+
def mouse_input(self, buffer):
|
|
85
|
+
m = re.match(r"\x1b\[<(\d+);(\d+);(\d+)([Mm])", buffer)
|
|
86
|
+
scroll_dir={64:'up', 65:'down',66:'left',67:'right'}
|
|
87
|
+
if m:
|
|
88
|
+
button, column, row, event = m.groups()
|
|
89
|
+
detail=''
|
|
90
|
+
button = int(button)
|
|
91
|
+
action = "button down" if event == "M" else "button up" if event == "m" else "Unknown"
|
|
92
|
+
if button>=64:
|
|
93
|
+
if button<=67:
|
|
94
|
+
action='scroll '+scroll_dir[button]
|
|
95
|
+
else:
|
|
96
|
+
action='scroll'
|
|
97
|
+
elif button >= 32:
|
|
98
|
+
button=button%32
|
|
99
|
+
action='drag'
|
|
100
|
+
column = int(column)-1
|
|
101
|
+
row = int(row)-1
|
|
102
|
+
return {'button':button, 'x':column, 'y':row,
|
|
103
|
+
'action':action}
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
def read_input(self): # Get the current settings of the terminal
|
|
107
|
+
buffer = self.read()
|
|
108
|
+
inputs=self.split_codes(buffer)
|
|
109
|
+
all_inputs=[]
|
|
110
|
+
for i in inputs:
|
|
111
|
+
key=self.keymap.get(i)
|
|
112
|
+
if key:
|
|
113
|
+
all_inputs.append(key)
|
|
114
|
+
continue
|
|
115
|
+
mouse=self.mouse_input(i)
|
|
116
|
+
if mouse:
|
|
117
|
+
all_inputs.append(mouse)
|
|
118
|
+
continue
|
|
119
|
+
all_inputs.append(i)
|
|
120
|
+
return all_inputs
|
|
121
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
def gen_keymap():
|
|
2
|
+
term_keymap = {
|
|
3
|
+
# --- Manual Standard Overrides ---
|
|
4
|
+
"\x7f":"Backspace",
|
|
5
|
+
"\x1b":"Esc",
|
|
6
|
+
"\x00":"Ctrl ",
|
|
7
|
+
"\x09":"Tab",
|
|
8
|
+
"\x0a":"Enter",
|
|
9
|
+
"\x0d":"CR",
|
|
10
|
+
"\x1b[Z":"Shift Tab",
|
|
11
|
+
"\x1b\n":"Alt Enter",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
special = {28: "\\", 29: "] 5", 30: "^ 6", 31: "/ 7"}
|
|
15
|
+
# --- 1. C0 Control Codes (Ctrl+A to Ctrl+Z) ---
|
|
16
|
+
for i in range(1, 32):
|
|
17
|
+
# Handle the Information Separators
|
|
18
|
+
char = special.get(i, chr(i + 64))
|
|
19
|
+
if term_keymap.get(chr(i)) is None:
|
|
20
|
+
term_keymap[chr(i)] = f"Ctrl {char}"
|
|
21
|
+
|
|
22
|
+
# --- 2. XTerm Modified Sequences ---
|
|
23
|
+
modifiers = {
|
|
24
|
+
"2": "Shift", "3": "Alt", "4": "Shift Alt", "5": "Ctrl",
|
|
25
|
+
"6": "Ctrl Shift", "7": "Ctrl Alt", "8": "Ctrl Shift Alt",
|
|
26
|
+
"9": "Meta", "10": "Meta Shift", "12": "Meta Shift Alt",
|
|
27
|
+
"13": "Meta Ctrl","14": "Meta Ctrl Shift","15": "Meta Ctrl Alt",
|
|
28
|
+
"16": "Meta Ctrl Shift Alt",
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# CSI Suffixes (1;modX)
|
|
33
|
+
nav_f_codes = {
|
|
34
|
+
"A": "Up", "B": "Down", "C": "Right", "D": "Left",
|
|
35
|
+
"H": "Home", "F": "End", "P": "F1", "Q": "F2", "R": "F3", "S": "F4"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Tilde Codes (code;mod~)
|
|
39
|
+
tilde_codes = {
|
|
40
|
+
"1": "Home", "2": "Ins", "3": "Del", "4": 'End',
|
|
41
|
+
"5": "PgUp", "6": "PgDn",
|
|
42
|
+
"15": "F5", "17": "F6", "18": "F7", "19": "F8",
|
|
43
|
+
"20": "F9", "21": "F10", "23": "F11", "24": "F12",
|
|
44
|
+
"25": "F13", "26": "F14", "28": "F15", "29": "F16",
|
|
45
|
+
"31": "F17", "32": "F18", "33": "F19", "34": "F20",
|
|
46
|
+
"35": "F21", "36": "F22", "37": "F23", "38": "F24",
|
|
47
|
+
"32": "SysRq", "34": "Break",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#keys with no modifiers
|
|
51
|
+
for code, name in nav_f_codes.items():
|
|
52
|
+
term_keymap[f"\x1bO{code}"] = f"{name}"
|
|
53
|
+
term_keymap[f"\x1b[{code}"] = f"{name}"
|
|
54
|
+
for code, name in tilde_codes.items():
|
|
55
|
+
term_keymap[f"\x1b[{code}~"] = f"{name}"
|
|
56
|
+
|
|
57
|
+
#keys for all the modifiers
|
|
58
|
+
for mod_val, mod_name in modifiers.items():
|
|
59
|
+
for code, name in nav_f_codes.items():
|
|
60
|
+
term_keymap[f"\x1b[1;{mod_val}{code}"] = f"{mod_name} {name}"
|
|
61
|
+
for code, name in tilde_codes.items():
|
|
62
|
+
term_keymap[f"\x1b[{code};{mod_val}~"] = f"{mod_name} {name}"
|
|
63
|
+
|
|
64
|
+
# --- 3. Alt + Keys (Meta) ---
|
|
65
|
+
for i in range(32, 127): # Space through ~
|
|
66
|
+
term_keymap[f"\x1b{chr(i)}"] = f"Alt {chr(i)}"
|
|
67
|
+
for m in [ 9,10,12,13,14,15,16]:
|
|
68
|
+
term_keymap[f"\x1b[{str(i)};{str(m)}u" ] = f"{modifiers[str(m)]} {chr(i)}"
|
|
69
|
+
return term_keymap
|
|
70
|
+
|