PyTermint 0.0.1__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.
PyTermint/PyTerm.py ADDED
@@ -0,0 +1,702 @@
1
+ #PyTerm.py
2
+ """
3
+ This is a terminal engine which is desined to be imported by other python files to use its features.\
4
+ init(screen); return bg_buffer, {vars dict}, "optional command string"
5
+ tick(screen, vars, keys); return None
6
+
7
+ """
8
+ import sys, time, os, random, collections, re, ctypes
9
+ import copy, math, csv
10
+ from collections import deque
11
+ from time import sleep
12
+ import select
13
+
14
+ valid_run = True
15
+ if not os.name == 'nt':
16
+ if not 'pyodide' in sys.modules:
17
+ try: import tty, termios
18
+ except:
19
+ print("Needed packages not present")
20
+ valid_run = input("do you wish to continue (y/n): ")
21
+ if "y" in valid_run: valid_run = True
22
+ else: valid_run = False
23
+
24
+ def get_keys_linux_fallback():
25
+ """Reads characters from stdin without blocking."""
26
+ keys = set()
27
+ # Check if there is data waiting in the buffer
28
+ while select.select([sys.stdin], [], [], 0)[0]:
29
+ char = sys.stdin.read(1)
30
+ if char == '\x1b': # Escape sequence (Arrows/Alt)
31
+ next_char = sys.stdin.read(1)
32
+ if next_char == '[':
33
+ code = sys.stdin.read(1)
34
+ mapping = {'A': 'up', 'B': 'down', 'C': 'right', 'D': 'left'}
35
+ if code in mapping:
36
+ keys.add(mapping[code])
37
+ elif char == '\n' or char == '\r':
38
+ keys.add('enter')
39
+ elif char == ' ':
40
+ keys.add(' ')
41
+ elif char == '\x7f':
42
+ keys.add('backspace')
43
+ else:
44
+ keys.add(char.lower())
45
+ return keys
46
+
47
+ # IMPORTANT
48
+ """
49
+ Never every change somthing without a direct request including all parts of this code comments ect. That includes this comment.
50
+ """
51
+ # --- Input ---
52
+ # --- Key Tracking State ---
53
+ class KeyTracker:
54
+ def __init__(self):
55
+ self.press_times = {} # {key: start_time}
56
+ self.REPEAT_DELAY = 0.5 # Seconds before auto-repeat starts
57
+
58
+ def update(self, active_keys):
59
+ """Generates the 'p' (pulse) and 't' (time) versions of keys."""
60
+ now = time.time()
61
+ final_keys = set(active_keys)
62
+
63
+ # Remove keys no longer pressed
64
+ for k in list(self.press_times.keys()):
65
+ if k not in active_keys:
66
+ del self.press_times[k]
67
+
68
+ for k in active_keys:
69
+ if k not in self.press_times:
70
+ # First frame: Add 'p' version
71
+ self.press_times[k] = now
72
+ final_keys.add(f'p{k}')
73
+ final_keys.add(f't{k}:0')
74
+ else:
75
+ elapsed = now - self.press_times[k]
76
+ # Add 't' version with milliseconds
77
+ final_keys.add(f't{k}:{int(elapsed * 1000)}')
78
+
79
+ # After delay, 'p' version shows every frame
80
+ if elapsed >= self.REPEAT_DELAY:
81
+ final_keys.add(f'p{k}')
82
+
83
+ return final_keys
84
+
85
+ tracker = KeyTracker()
86
+
87
+ # Shared shift mapping for filtering logic
88
+ SHIFT_MAP = {
89
+ '1':'!', '2':'@', '3':'#', '4':'$', '5':'%', '6':'^', '7':'&', '8':'*', '9':'(', '0':')',
90
+ '-':'_', '=':'+', '[':'{', ']':'}', '\\':'|', ';':':', "'":'"', ',':'<', '.':'>', '/':'?', '`':'~'
91
+ }
92
+
93
+ tab_size = 4
94
+
95
+ def filter_shift_keys(keys):
96
+ """If shift is held, replaces base keys with symbols (e.g., '1' becomes '!')"""
97
+ if 'shift' in keys:
98
+ to_remove = set()
99
+ to_add = set()
100
+ for base, shifted in SHIFT_MAP.items():
101
+ if base in keys:
102
+ to_add.add(shifted)
103
+ to_remove.add(base)
104
+ return (keys | to_add) - to_remove
105
+ return keys
106
+
107
+ # --- CROSS-PLATFORM COMPATIBILITY LAYER ---
108
+ if os.name == 'nt':
109
+ user32 = ctypes.windll.user32
110
+ kernel32 = ctypes.windll.kernel32
111
+ kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
112
+
113
+ def get_keys():
114
+ keys = set()
115
+ is_pressed = lambda vk: user32.GetAsyncKeyState(vk) & 0x8000
116
+
117
+ # Modifiers
118
+ if is_pressed(0x10): keys.add('shift')
119
+ if is_pressed(0x11): keys.add('ctrl')
120
+ if is_pressed(0x12): keys.add('alt')
121
+ if is_pressed(0x09): keys.add('tab')
122
+ if is_pressed(0x1B): keys.add('escape')
123
+
124
+ # Navigation
125
+ nav = {0x26:'up', 0x28:'down', 0x25:'left', 0x27:'right', 0x2D:'insert', 0x2E:'delete'}
126
+ for vk, name in nav.items():
127
+ if is_pressed(vk): keys.add(name)
128
+
129
+ # Alpha-Numeric & Symbols
130
+ for i in range(0x30, 0x3A):
131
+ if is_pressed(i): keys.add(chr(i).lower())
132
+ for i in range(0x41, 0x5B):
133
+ if is_pressed(i): keys.add(chr(i).lower())
134
+
135
+ symbols = {0xBA:';', 0xBB:'=', 0xBC:',', 0xBD:'-', 0xBE:'.', 0xBF:'/', 0xC0:'`', 0xDB:'[', 0xDC:'\\', 0xDD:']', 0xDE:"'", 0x20:' ', 0x0D:'enter', 0x08:'backspace'}
136
+ for vk, name in symbols.items():
137
+ if is_pressed(vk): keys.add(name)
138
+
139
+ keys = filter_shift_keys(keys)
140
+ return tracker.update(keys)
141
+
142
+ elif 'pyodide' in sys.modules:
143
+ def get_keys(): return set()
144
+
145
+ else: # Linux
146
+ try:
147
+ from evdev import InputDevice, ecodes, list_devices
148
+ devices = [InputDevice(path) for path in list_devices()]
149
+ kbd = next((d for d in devices if "key" in d.name.lower()), None)
150
+
151
+ def get_keys():
152
+ if not kbd: return set()
153
+ keys = set()
154
+ active = kbd.active_keys()
155
+ mapping = {ecodes.KEY_UP:'up', ecodes.KEY_DOWN:'down', ecodes.KEY_LEFT:'left', ecodes.KEY_RIGHT:'right', ecodes.KEY_ENTER:'enter', ecodes.KEY_LEFTSHIFT:'shift', ecodes.KEY_RIGHTSHIFT:'shift', ecodes.KEY_SPACE:' '}
156
+
157
+ for code in active:
158
+ if code in mapping: keys.add(mapping[code])
159
+ key_name = ecodes.KEY[code].replace('KEY_', '').lower()
160
+ if len(key_name) == 1: keys.add(key_name)
161
+
162
+ keys = filter_shift_keys(keys)
163
+ return tracker.update(keys)
164
+
165
+ except (ImportError, PermissionError):
166
+ def get_keys():
167
+ keys = get_keys_linux_fallback()
168
+ keys = filter_shift_keys(keys)
169
+ return tracker.update(keys)
170
+
171
+ def pressed(keys, k):
172
+ if f"p{k}" in keys: return True
173
+ else: return False
174
+
175
+ def get_typed_chars(keys):
176
+ """
177
+ Returns a string of all characters that 'pulsed' this frame.
178
+ Handles 'pspace' as ' ' and ignores modifier pulses like 'pshift'.
179
+ """
180
+ typed_string = ""
181
+ for k in keys:
182
+ if k.startswith('p') and len(k) > 1:
183
+ char = k[1:]
184
+ if char == 'space': typed_string += ' '
185
+ elif char == 'tab': typed_string += ' ' * tab_size
186
+ elif len(char) == 1: typed_string += char
187
+ return typed_string
188
+ # --- UI Logic ---
189
+
190
+ def handle_input(v, keys, multiline=True, cursor_pos_name="cpos", use_text_from_vars=True, text="", text_name="text"):
191
+ if use_text_from_vars:lines = v[text_name].split('\n') if v[text_name] else [""]
192
+ else: lines = text.split('\n') if text else [""]
193
+ cx, _ = v[cursor_pos_name]
194
+ if multiline: _, ly = v[cursor_pos_name]
195
+ else: ly = 0
196
+
197
+ # --- Navigation ---
198
+ if multiline:
199
+ if pressed(keys, 'up') and ly > 0:
200
+ ly -= 1
201
+ cx = min(cx, len(lines[ly]))
202
+
203
+ if pressed(keys, 'down') and ly < len(lines) - 1:
204
+ ly += 1
205
+ if cx > len(lines[ly]): cx = min(cx, len(lines[ly]))
206
+
207
+ if pressed(keys, 'left'):
208
+ if cx > 0: cx -= 1
209
+ elif ly > 0:
210
+ ly -= 1
211
+ cx = len(lines[ly])
212
+
213
+ if pressed(keys, 'right'):
214
+ if cx < len(lines[ly]): cx += 1
215
+ elif ly < len(lines) - 1:
216
+ ly += 1
217
+ cx = 0
218
+
219
+ # --- Editing ---
220
+ if pressed(keys, 'backspace'):
221
+ if cx > 0:
222
+ lines[ly] = lines[ly][:cx-1] + lines[ly][cx:]
223
+ cx -= 1
224
+ elif ly > 0:
225
+ target_line = ly - 1
226
+ new_cx = len(lines[target_line])
227
+ lines[target_line] += lines[ly]
228
+ lines.pop(ly)
229
+ ly = target_line
230
+ cx = new_cx
231
+
232
+ if pressed(keys, 'delete'):
233
+ if cx < len(lines[ly]):
234
+ # Remove character at current cursor position
235
+ lines[ly] = lines[ly][:cx] + lines[ly][cx+1:]
236
+ elif ly < len(lines) - 1:
237
+ # At end of line: pull the line below up to this line
238
+ lines[ly] += lines[ly+1]
239
+ lines.pop(ly + 1)
240
+
241
+ if pressed(keys, 'enter') and multiline:
242
+ left_part = lines[ly][:cx]
243
+ right_part = lines[ly][cx:]
244
+ lines[ly] = left_part
245
+ lines.insert(ly + 1, right_part)
246
+ ly += 1
247
+ cx = 0
248
+
249
+ # --- Typing ---
250
+ typed = get_typed_chars(keys)
251
+ lines[ly] = lines[ly][:cx] + typed + lines[ly][cx:]
252
+ cx += len(typed)
253
+
254
+ # Update State
255
+ if multiline: v[cursor_pos_name] = cx, ly
256
+ else: v[cursor_pos_name] = cx, 0
257
+ if use_text_from_vars: v[text_name] = "\n".join(lines)
258
+ else: return "\n".join(lines)
259
+
260
+ def draw_window(arr, x, y, sx, sy, color_offset=0, char=None):
261
+ if isinstance(char, int):
262
+ for xi in range(x, sx):
263
+ for yi in range(y, sy):
264
+ if inside(arr, (xi, yi)):
265
+ arr[yi][xi] = char
266
+ for i in range(x, x + sx):
267
+ if inside(arr, (i, y)):
268
+ arr[y][i] = 101 + color_offset
269
+ for i in range(y, y + sy):
270
+ if inside(arr, (x, i)):
271
+ arr[i][x] = 100 + color_offset
272
+ for i in range(x, x + sx):
273
+ if inside(arr, (i, y + sy)):
274
+ arr[y + sy][i] = 101 + color_offset
275
+ for i in range(y, y + sy):
276
+ if inside(arr, (x + sx, i)):
277
+ arr[i][x + sx] = 100 + color_offset
278
+ if inside(arr, (x, y)):
279
+ arr[y][x] = 96 + color_offset
280
+ if inside(arr, (x + sx, y)):
281
+ arr[y][x + sx] = 99 + color_offset
282
+ if inside(arr, (x + sx, y + sy)):
283
+ arr[y + sy][x + sx] = 97 + color_offset
284
+ if inside(arr, (x, y + sy)):
285
+ arr[y + sy][x] = 98 + color_offset
286
+
287
+
288
+ # --- sound ---
289
+
290
+ if os.name == 'nt':
291
+ import winsound
292
+ def beep(freq, duration):
293
+ """Windows uses the built-in winsound module."""
294
+ try:
295
+ winsound.Beep(int(freq), int(duration))
296
+ except: pass
297
+ else:
298
+ def beep(freq, duration):
299
+ """Linux/macOS fallback using the ASCII Bell or system beep."""
300
+ # Note: This requires the 'beep' package on many Linux distros
301
+ # Alternatively, we can use the terminal bell, though it lacks frequency control
302
+ os.system(f'play -n synth {duration/1000} square {freq} > /dev/null 2>&1')
303
+
304
+ def play_note(note_index, duration=100):
305
+ """
306
+ Converts a MIDI note index to frequency.
307
+ Example: 60 is Middle C.
308
+ """
309
+ # Formula: f = 440 * 2^((n-69)/12)
310
+ freq = 440 * (2 ** ((note_index - 69) / 12))
311
+ beep(freq, duration)
312
+
313
+ # --- binary utilities ---
314
+ def export_raw_data(data, path, data_type="hex"):
315
+ """Writes binary files from hex or bit strings."""
316
+ try:
317
+ path = path if '.'in path else f"{path}.bin"
318
+ clean = data.replace(" ", "").lower()
319
+ is_hex = data_type == "hex" or clean.startswith("0x") or any(c in clean for c in "23456789abcdef")
320
+ if is_hex:
321
+ clean = clean.replace("0x", "")
322
+ if len(clean) % 2: clean = "0" + clean
323
+ rw_byt = bytes.fromhex(clean)
324
+ else:
325
+ clean = clean.replace("0b", "")
326
+ pad = (8 - len(clean) % 8) % 8
327
+ clean = clean + ("0" * pad)
328
+ byte_list = []
329
+ for i in range(0, len(clean), 8):
330
+ byte_list.append(int(clean[i:i+8], 2))
331
+ rw_byt = bytes(byte_list)
332
+ with open(path, "wb") as f: f.write(rw_byt)
333
+ except Exception as e: print(f"Export Error: {e}")
334
+
335
+ def load_raw_data(path, return_type="byt"):
336
+ """Loads a binary file. Appends .bin if needed."""
337
+ try:
338
+ if not os.path.splitext(path)[1]: path += ".bin"
339
+ with open(path, "rb") as f:content = f.read()
340
+ return content.hex() if return_type == "hex" else content
341
+ except Exception as e:print(f"Load Error: {e}"); return None
342
+
343
+ # --- Binary Map Format ---
344
+
345
+ def load_csv_array(file):
346
+ """Loads a CSV file into a 2D list (list[y][x])."""
347
+ file_path=file.strip('"').strip("'")
348
+ try:
349
+ with open(file_path, mode='r', encoding='utf-8') as f:
350
+ reader = list(csv.reader(f))
351
+ result = []
352
+ for line in reader:
353
+ result.append([int(v) for v in line])
354
+ return result
355
+ except FileNotFoundError:
356
+ print(f"Error: The file at {file_path} was not found.")
357
+ return []
358
+
359
+ def csv_to_bin(csv_path, output_path, force_zero_rle=False):
360
+ grid = load_csv_array(csv_path)
361
+ if not grid: return
362
+ h, w = len(grid), len(grid[0])
363
+ f_d = [item + 1 if item >= -1 else -1 for sublist in grid for item in sublist]
364
+
365
+ unique_tls = sorted(list(set(f_d)))
366
+ lut_size, lut_vw = len(unique_tls), max(1, math.ceil(math.log2(max(max(f_d), 1) + 1)))
367
+ data_b_width = max(1, math.ceil(math.log2(lut_size)))
368
+ tl_to_idx = {tl: i for i, tl in enumerate(unique_tls)}
369
+ if force_zero_rle: best_lbw = 0
370
+ else:
371
+ runs = []; curr_idx, count = tl_to_idx[f_d[0]], 1
372
+ for x in f_d[1:]:
373
+ idx = tl_to_idx[x]
374
+ if idx == curr_idx: count += 1
375
+ else: runs.append((curr_idx, count)); curr_idx, count = idx, 1
376
+ runs.append((curr_idx, count))
377
+ best_lbw, min_bits = 1, float('inf')
378
+ for lbw in range(1, 16):
379
+ total = sum(math.ceil(c / (2**lbw - 1)) * (data_b_width + lbw) for _, c in runs)
380
+ if total < min_bits: min_bits, best_lbw = total, lbw
381
+ best_lbw -= 1
382
+ b_s = format(data_b_width, '08b') + format(lut_vw, '08b') + format(lut_size, '024b')
383
+ b_s += "".join(format(tl, f'0{lut_vw}b') for tl in unique_tls)
384
+ b_s += format(w, '012b') + format(h, '012b') + format(best_lbw, '04b')
385
+ if best_lbw == 0:b_s += "".join(format(tl_to_idx[tl], f'0{data_b_width}b') for tl in f_d)
386
+ else:
387
+ for idx, count in runs:
388
+ m_r = (2**best_lbw) - 1
389
+ while count > 0:
390
+ cur = min(count, m_r); b_s += format(idx, f'0{data_b_width}b') + format(cur, f'0{best_lbw}b'); count -= cur
391
+ output_path += ".map.bin"
392
+ export_raw_data(b_s, output_path, data_type="in")
393
+
394
+ def bin_to_csv(bin_path):
395
+ raw_bytes = load_raw_data(bin_path, return_type="byt")
396
+ if not raw_bytes: return
397
+ b_s = "".join(format(b, '08b') for b in raw_bytes); p = 0
398
+ dbw, lvw = int(b_s[p:p+8], 2), int(b_s[p+8:p+16], 2); p += 16
399
+ ls = int(b_s[p:p+24], 2); p += 24
400
+ lut = [int(b_s[p+i*lvw:p+(i+1)*lvw],2) for i in range(ls)]; p += ls * lvw
401
+ w, h = int(b_s[p:p+12], 2), int(b_s[p+12:p+24], 2); p += 24
402
+ lbw = int(b_s[p:p+4], 2); p += 4;flat = []
403
+ while len(flat) < w * h:
404
+ idx = int(b_s[p:p+dbw], 2); p += dbw
405
+ if lbw == 0: flat.append(lut[idx] - 1)
406
+ else:
407
+ length = int(b_s[p:p+lbw], 2); p += lbw; flat.extend([lut[idx] - 1] * length)
408
+ return [flat[i*w : (i+1)*w] for i in range(h)]
409
+
410
+ # Dim: gets the dimensions of an array
411
+ def dim(arr):
412
+ try: return len(arr[0]), len(arr)
413
+ except: return (0, 0)
414
+
415
+ WIDTH=128
416
+ HEIGHT=32
417
+
418
+ low_res = {
419
+ ((0, 0), (0, 0)): 0,
420
+ ((1, 0), (0, 0)): 69,
421
+ ((0, 1), (0, 0)): 68,
422
+ ((0, 0), (1, 0)): 66,
423
+ ((0, 0), (0, 1)): 67,
424
+ ((1, 1), (0, 0)): 78,
425
+ ((0, 0), (1, 1)): 65,
426
+ ((1, 0), (1, 0)): 76,
427
+ ((0, 1), (0, 1)): 77,
428
+ ((1, 0), (0, 1)): 71,
429
+ ((0, 1), (1, 0)): 75,
430
+ ((1, 1), (1, 0)): 72,
431
+ ((1, 1), (0, 1)): 73,
432
+ ((1, 0), (1, 1)): 70,
433
+ ((0, 1), (1, 1)): 74,
434
+ ((1, 1), (1, 1)): 64
435
+ }
436
+
437
+ ilow_res = {v: k for k, v in low_res.items()}
438
+ # Engine character set
439
+ TILE_SET = r" []/\|_‾@#*%$=.^-+&<>?{}ABCDEFGHIJKLMNOPQRSTUVWXYZ!0123456789░▒▓█▄▖▗▝▘▙▚▛▜▟▞▌▐▀ ':;~`,()╱╲╳╋┻┳┫┣┏┛┗┓┃━⌷"
440
+ CHAR_MAP = {}
441
+
442
+ for index, char in enumerate(TILE_SET):
443
+ if char not in CHAR_MAP:
444
+ CHAR_MAP[char] = index
445
+
446
+ # ANSI Colors (Foreground/background): Defualt 0, Black 1, White 2, Red 3, Green 4, Yellow 5, Blue 6, Magenta 7, Cyan 8
447
+ ANSI_COLORS = ["0","30","37","31","32","33","34","35","36"]
448
+
449
+ def color(tile, fg=2 , bg=1, invert=False):
450
+ if isinstance(tile, str):
451
+ temp_tile = []
452
+ for char in list(tile):
453
+ if char in TILE_SET: temp_tile.append(TILE_SET.find(char))
454
+ tile = temp_tile
455
+ def sub_color(char, fg_c, bg_c, inv):
456
+ # Mask to 4 bits to ensure they stay in their 'slots'
457
+ f = fg_c & 0xF;b = bg_c & 0xF
458
+ if inv: color_bits = (b << 4) | f
459
+ else: color_bits = (f << 4) | b
460
+ return char + (len(TILE_SET) * color_bits)
461
+ if isinstance(tile, list):return [sub_color(x, fg, bg, invert) for x in tile]
462
+ else:return sub_color(tile, fg, bg, invert)
463
+
464
+ def str_to_arr(string, color=0):
465
+ string = string.upper()
466
+ lines = [line.replace('"', "'") for line in string.split('\n') if line]
467
+ max_len = max(len(line) for line in lines) if lines else 0
468
+ result = []
469
+ for line in lines:
470
+ row = [CHAR_MAP[c] + color for c in line]
471
+ while len(row) < max_len:
472
+ row.append(0 + color)
473
+ result.append(row)
474
+ return result
475
+
476
+ def plot(screen, char, x, y, color=0):
477
+ screen[y][x] = TILE_SET.find(char) + color
478
+
479
+ def gplot(screen, x, y, state, color_idx):
480
+ tx, ty = x // 2, y // 2
481
+ if not inside(screen, (tx, ty)): return
482
+ t_len = len(TILE_SET);current_val = screen[ty][tx];char_idx = current_val % t_len
483
+ color_bits = current_val // t_len;curr_fg = (color_bits >> 4) & 0xF
484
+ curr_bg = color_bits & 0xF
485
+ raw_matrix = ilow_res.get(char_idx, ((0,0),(0,0)))
486
+ matrix = [list(row) for row in raw_matrix]
487
+ if state:
488
+ if matrix == [[0,0],[0,0]]:
489
+ curr_fg = color_idx
490
+ elif color_idx == curr_bg and color_idx != curr_fg:
491
+ curr_fg, curr_bg = curr_bg, curr_fg
492
+ for row in range(2):
493
+ for col in range(2): matrix[row][col] = 1 - matrix[row][col]
494
+ elif color_idx != curr_fg: curr_fg = color_idx
495
+ matrix[y % 2][x % 2] = 1 if state else 0
496
+ lookup_matrix = tuple(tuple(row) for row in matrix)
497
+ new_char_idx = low_res.get(lookup_matrix, 0)
498
+ new_color_bits = (curr_fg << 4) | curr_bg
499
+ screen[ty][tx] = new_char_idx + (t_len * new_color_bits)
500
+
501
+ def draw_box(arr, start_x, start_y, width, height, fg=0, bg=1, invert=False):
502
+ """Draws a box."""
503
+ for y in range(start_y, start_y + height):
504
+ if 0 <= y < len(arr):
505
+ if 0 <= start_x < len(arr[y]): arr[y][start_x] = color(5, fg, bg, invert)
506
+ if 0 <= start_x + 1 < len(arr[y]): arr[y][start_x + 1] = color(5, fg, bg, invert)
507
+ if 0 <= start_x + width - 2 < len(arr[y]): arr[y][start_x + width - 2] = color(5, fg, bg, invert)
508
+ if 0 <= start_x + width - 1 < len(arr[y]): arr[y][start_x + width - 1] = color(5, fg, bg, invert)
509
+ for x in range(start_x, start_x + width):
510
+ if 0 <= start_y < len(arr) and 0 <= x < len(arr[0]): arr[start_y][x] = color(13, fg, bg, invert)
511
+ if 0 <= start_y + height - 1 < len(arr) and 0 <= x < len(arr[0]): arr[start_y + height - 1][x] = color(13, fg, bg, invert)
512
+
513
+ def draw_tri(arr, sx, sy, ph, color_ = 0):
514
+ for o in range(ph):
515
+ if inside(arr, (sx, sy)): arr[sy][sx] = 3 + color_
516
+ sx += 1
517
+ sy -= 1
518
+
519
+ if inside(arr, (sx, sy)): arr[sy][sx] = 3 + color_
520
+ sx += 1
521
+ for o in range(ph):
522
+ if inside(arr, (sx, sy)): arr[sy][sx] = 4 + color_
523
+ sx += 1
524
+ sy += 1
525
+ if inside(arr, (sx, sy)): arr[sy][sx] = 4 + color_
526
+ def blit(arr0, arr1, sx, sy):
527
+ w, h = dim(arr1)
528
+ for y in range(h):
529
+ for x in range(w):
530
+ if inside(arr0, (x + sx, y + sy)):
531
+ t = arr1[y][x]
532
+ if t >= 0: arr0[y + sy][x + sx] = t
533
+
534
+ def getfg(arr, xy):
535
+ """Returns the foreground color index of a tile at (x, y)."""
536
+ x, y = xy
537
+ if not inside(arr, xy):return 0
538
+ val = arr[y][x]; t_len = len(TILE_SET)
539
+ if val < t_len: return 0
540
+ color_bits = val // t_len;fg_idx = (color_bits >> 4) & 0xF
541
+ return fg_idx
542
+
543
+ def getbg(arr, xy):
544
+ """Returns the background color index of a tile at (x, y)."""
545
+ x, y = xy
546
+ if not inside(arr, xy): return 1
547
+ val = arr[y][x];t_len = len(TILE_SET)
548
+ if val < t_len:return 1
549
+ color_bits = val // t_len;bg_idx = color_bits & 0xF
550
+ return bg_idx
551
+
552
+ def clear(screen, char=0):
553
+ h, w = len(screen), len(screen[0])
554
+ if isinstance(char, list):
555
+ c_len = len(char)
556
+ for y in range(h):
557
+ for x in range(w):
558
+ screen[y][x] = char[x % c_len]
559
+ else:
560
+ for y in range(h):
561
+ for x in range(w): screen[y][x] = char
562
+
563
+ def print_screen(screen, color_suppress=False):
564
+ # \033[H moves cursor to top-left.
565
+ # We add \033[J to clear from cursor to end of screen to prevent ghosting.
566
+ sys.stdout.write("\033[H")
567
+ full_tile_set = TILE_SET
568
+ t_len = len(full_tile_set)
569
+ c_len = len(ANSI_COLORS)
570
+ output = []
571
+ q_pos = TILE_SET.find("'")
572
+ for y in range(HEIGHT):
573
+ line = screen[y]
574
+ row_str = []
575
+ last_color_idx = -1
576
+ for val in line:
577
+
578
+ if val >= t_len and not color_suppress:
579
+ color_val = (val // t_len)
580
+ fg_idx = (color_val >> 4) & 0xF
581
+ bg_idx = color_val & 0xF
582
+ fg_idx %= c_len
583
+ bg_idx %= c_len
584
+ char_idx = val % t_len
585
+
586
+ if color_val != last_color_idx:
587
+ fg_code = ANSI_COLORS[fg_idx]
588
+ bg_raw = int(ANSI_COLORS[bg_idx])
589
+ bg_code = str(bg_raw + 10) if bg_raw != 0 else "0"
590
+ row_str.append(f"\033[{fg_code};{bg_code}m")
591
+ last_color_idx = color_val
592
+ if char_idx == q_pos: row_str.append('"')
593
+ else: row_str.append(full_tile_set[char_idx])
594
+ else:
595
+ if last_color_idx >= 0:
596
+ row_str.append("\033[0m")
597
+ last_color_idx = -1
598
+ if val % t_len == q_pos: row_str.append('"')
599
+ else:row_str.append(full_tile_set[val % t_len])
600
+
601
+ # We join the row and ensure no extra trailing spaces are added by the terminal
602
+ output.append("".join(row_str))
603
+
604
+ # IMPORTANT: Use join with a literal newline, but ensure the final string
605
+ # ends with a reset code to prevent the last line from "bleeding" into the prompt.
606
+ sys.stdout.write("\n".join(output) + "\033[0m")
607
+ sys.stdout.flush()
608
+
609
+ def inside(screen, xy, wrap=False, inch=False):
610
+ x, y = xy; h = len(screen); w = len(screen[0]) if h > 0 else 0
611
+ if not wrap:
612
+ return 0 <= x < w and 0 <= y < h
613
+ wx, wy = xy
614
+ if inch:
615
+ wy = (wy + (x // w)) % h; wx = (wx + (y // h)) % w
616
+ else: wx, wy = x % w, y % h
617
+ return wx, wy
618
+
619
+ """ --- example program -- """
620
+ def init(screen):
621
+ bg_buffer = [[0 for _ in range(WIDTH)] for _ in range(HEIGHT)]
622
+ clear(bg_buffer, color(0, bg=2))
623
+ clear(screen, color(0, bg=2))
624
+ for tx in range(len(TILE_SET)):
625
+ screen[0][tx % WIDTH] = color(tx, 6, 2)
626
+ screen[1][tx % WIDTH] = color(tx, 3, 2)
627
+ screen[2][tx % WIDTH] = color(tx, 4, 2)
628
+ return bg_buffer, {
629
+ 'px': 15, 'py': 15,
630
+ 'debug': copy.deepcopy(screen),
631
+ 'mint': -1
632
+ }
633
+
634
+ def render_background(arr):
635
+ draw_box(arr, 10, 10, 20, 10, 2, 3, True);arr[12][29] = color(0, bg=2);arr[12][28] = color(0, bg=2)
636
+
637
+ def tick(screen, vars, keys):
638
+ last_p = vars['px'], vars['py']
639
+ vars['mint'] *= -1
640
+ nx, ny = vars['px'], vars['py']
641
+ if 'w' in keys: ny -= 1
642
+ if 's' in keys: ny += 1
643
+ if 'a' in keys: nx -= 1
644
+ if 'd' in keys: nx += 1
645
+
646
+ # Minimal validation to prevent IndexError
647
+ if inside(screen, (nx, ny)):
648
+ if screen[ny][nx] == color(0, bg=2):
649
+ vars['px'], vars['py'] = nx, ny
650
+
651
+ clear(screen, color(8, 0, bg=0));blit(screen, vars['debug'], 0, 0)
652
+ render_background(screen);screen[vars['py']][vars['px']] = color(8, 7, 2)
653
+
654
+ # --- end ---
655
+ def prosses_result(res, data_list):
656
+ if res == None: return
657
+ command = res.split()
658
+ cl = len(command)
659
+ if cl >= 3:
660
+ if command[0] == "att":
661
+ if command[1] == "frame_speed" and cl >=3: data_list[0] = int(command[2])
662
+ if command[1] == "color_supp":
663
+ if command[2].lower() == "true": data_list[1] = True
664
+ elif command[2].lower() == "false": data_list[1] = False
665
+
666
+ def run(tick_func, init_func, quit_on_q=True):
667
+ if not valid_run: return
668
+ screen = [[0 for _ in range(WIDTH)] for _ in range(HEIGHT)]
669
+ init_output = init_func(screen); color_suppress = False; frame = 30
670
+ data = [frame, color_suppress]
671
+ if len(init_output) == 3: # while not used in the init in this file it can be used by other files that provide their own init and tick functions
672
+ state_bg, state_vars, res = init_output
673
+ prosses_result(res, data)
674
+ else: state_bg, state_vars = init_output
675
+ old_settings = None
676
+ if os.name != 'nt':
677
+ old_settings = termios.tcgetattr(sys.stdin); tty.setcbreak(sys.stdin.fileno())
678
+ os.system('cls' if os.name == 'nt' else 'clear')
679
+ sys.stdout.write("\033[?25l")
680
+ elapses = 0
681
+ try:
682
+ while True:
683
+ start = time.time()
684
+ keys = get_keys()
685
+ if 'q' in keys and quit_on_q: break
686
+ res = tick_func(screen, state_vars, keys)
687
+ if res != None and "quit" in res: break
688
+ if 'alt' in keys and 'delete' in keys:
689
+ sys.stdout.write("\033[H\033[J")
690
+ sys.stdout.flush()
691
+ prosses_result(res, data)
692
+ frame, color_suppress = data
693
+ print_screen(screen, color_suppress)
694
+ elapsed = time.time() - start
695
+ time.sleep(max(0, 1/frame - elapsed))
696
+ elapses += 1
697
+ finally:
698
+ if old_settings: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
699
+ sys.stdout.write("\033[?25h\033[0m"); print("Engine Stopped.")
700
+
701
+ if __name__ == "__main__":
702
+ run(tick, init)
PyTermint/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ # pyterm/__init__.py
2
+ """
3
+ This is a terminal engine which is desined to be imported by other python files to use its features.
4
+ init(screen); return arr, {vars_dict}, "optional command string"
5
+ tick(screen, vars, keys): return None
6
+
7
+ """
8
+
9
+ from .PyTerm import *
10
+ __version__ = "0.0.1"
11
+
12
+ __all__ = [name for name, obj in globals().items()
13
+ if not name.startswith('_')
14
+ and getattr(obj, '__module__', '').startswith('PyTerm')]
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyTermint
3
+ Version: 0.0.1
4
+ Summary: A terminal interface that abstracts it into a tilemap.
5
+ Project-URL: Homepage, https://github.com/Antlar256/PyTerm
6
+ Project-URL: Issues, https://github.com/Antlar256/PyTerm/issues
7
+ Author-email: Antlar256 <antonio0granell2@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+
15
+ # PyTerm
16
+ This is a cross-platform terminal abstraction that converts a tile map to a non-flickering terminal output with 3 bit rgb support using escape codes.
17
+ [GitHub Home page link](https://github.com/Antlar256/PyTerm)
@@ -0,0 +1,6 @@
1
+ PyTermint/PyTerm.py,sha256=w78GX2SDgP5pmNx1DA_fCXF_OXNLJOEU3FNZvNNMQuE,26699
2
+ PyTermint/__init__.py,sha256=nVp18eFqXBUVpxDg9UdpyZ1la5U-WWXWHLXxCnvuV1Y,452
3
+ pytermint-0.0.1.dist-info/METADATA,sha256=jmI789EOgXGshNYOBfPb_5Dv-PrIp_6POmKUViBH7Y0,717
4
+ pytermint-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
5
+ pytermint-0.0.1.dist-info/licenses/LICENSE,sha256=jSbqmWviKsyY9I9tmZQiNiROmeuA6TxqIz-BdZc1m0M,1079
6
+ pytermint-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright 2026 PyTerm Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.