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.
@@ -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,2 @@
1
+ # forgetui
2
+ 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
+ libAnsiScreen
@@ -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
+