PyTermint 0.0.1__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.
- pytermint-0.0.1/LICENSE +9 -0
- pytermint-0.0.1/PKG-INFO +17 -0
- pytermint-0.0.1/README.md +3 -0
- pytermint-0.0.1/pyproject.toml +23 -0
- pytermint-0.0.1/src/PyTermint/PyTerm.py +702 -0
- pytermint-0.0.1/src/PyTermint/__init__.py +14 -0
pytermint-0.0.1/LICENSE
ADDED
|
@@ -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.
|
pytermint-0.0.1/PKG-INFO
ADDED
|
@@ -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,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "PyTermint"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name="Antlar256", email="antonio0granell2@gmail.com" },
|
|
6
|
+
]
|
|
7
|
+
description = "A terminal interface that abstracts it into a tilemap."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
]
|
|
14
|
+
license = "MIT"
|
|
15
|
+
license-files = ["LICEN[CS]E*"]
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["hatchling >= 1.26"]
|
|
19
|
+
build-backend = "hatchling.build"
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/Antlar256/PyTerm"
|
|
23
|
+
Issues = "https://github.com/Antlar256/PyTerm/issues"
|
|
@@ -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)
|
|
@@ -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')]
|