omlish 0.0.0.dev187__py3-none-any.whl → 0.0.0.dev189__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.
- omlish/__about__.py +2 -2
- omlish/io/buffers.py +11 -0
- omlish/io/generators/stepped.py +11 -10
- omlish/logs/color.py +6 -6
- omlish/subprocesses.py +1 -0
- omlish/term/__init__.py +0 -0
- omlish/{term.py → term/codes.py} +1 -139
- omlish/term/progressbar.py +139 -0
- omlish/term/vt100/__init__.py +0 -0
- omlish/term/vt100/c.py +106 -0
- omlish/term/vt100/states.py +271 -0
- omlish/term/vt100/terminal.py +271 -0
- {omlish-0.0.0.dev187.dist-info → omlish-0.0.0.dev189.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev187.dist-info → omlish-0.0.0.dev189.dist-info}/RECORD +18 -12
- {omlish-0.0.0.dev187.dist-info → omlish-0.0.0.dev189.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev187.dist-info → omlish-0.0.0.dev189.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev187.dist-info → omlish-0.0.0.dev189.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev187.dist-info → omlish-0.0.0.dev189.dist-info}/top_level.txt +0 -0
omlish/__about__.py
CHANGED
omlish/io/buffers.py
CHANGED
@@ -129,10 +129,15 @@ class DelimitingBuffer:
|
|
129
129
|
|
130
130
|
|
131
131
|
class ReadableListBuffer:
|
132
|
+
# FIXME: merge with PrependableGeneratorReader
|
133
|
+
|
132
134
|
def __init__(self) -> None:
|
133
135
|
super().__init__()
|
134
136
|
self._lst: list[bytes] = []
|
135
137
|
|
138
|
+
def __len__(self) -> int:
|
139
|
+
return sum(map(len, self._lst))
|
140
|
+
|
136
141
|
def feed(self, d: bytes) -> None:
|
137
142
|
if d:
|
138
143
|
self._lst.append(d)
|
@@ -171,6 +176,12 @@ class ReadableListBuffer:
|
|
171
176
|
|
172
177
|
return None
|
173
178
|
|
179
|
+
def read_exact(self, sz: int) -> bytes:
|
180
|
+
d = self.read(sz)
|
181
|
+
if d is None or len(d) != sz:
|
182
|
+
raise EOFError(f'ReadableListBuffer got {"no" if d is None else len(d)}, expected {sz}')
|
183
|
+
return d
|
184
|
+
|
174
185
|
def read_until(self, delim: bytes = b'\n') -> ta.Optional[bytes]:
|
175
186
|
if not (lst := self._lst):
|
176
187
|
return None
|
omlish/io/generators/stepped.py
CHANGED
@@ -2,6 +2,7 @@ import typing as ta
|
|
2
2
|
|
3
3
|
from ... import check
|
4
4
|
from ... import lang
|
5
|
+
from ..buffers import ReadableListBuffer
|
5
6
|
from .consts import DEFAULT_BUFFER_SIZE
|
6
7
|
from .direct import BytesDirectGenerator
|
7
8
|
from .direct import StrDirectGenerator
|
@@ -149,33 +150,33 @@ def read_into_str_stepped_generator(
|
|
149
150
|
|
150
151
|
@lang.autostart
|
151
152
|
def buffer_bytes_stepped_reader_generator(g: BytesSteppedReaderGenerator) -> BytesSteppedGenerator:
|
153
|
+
i: bytes | None
|
152
154
|
o = g.send(None)
|
153
|
-
|
155
|
+
rlb = ReadableListBuffer()
|
154
156
|
eof = False
|
155
157
|
|
156
158
|
while True:
|
157
159
|
if eof:
|
158
160
|
raise EOFError
|
159
161
|
|
160
|
-
if not
|
161
|
-
|
162
|
-
|
162
|
+
if not len(rlb):
|
163
|
+
if (more := check.isinstance((yield None), bytes)):
|
164
|
+
rlb.feed(more)
|
165
|
+
else:
|
163
166
|
eof = True
|
164
167
|
|
165
168
|
if o is None:
|
166
|
-
i =
|
167
|
-
buf = None
|
169
|
+
i = check.not_none(rlb.read())
|
168
170
|
|
169
171
|
elif isinstance(o, int):
|
170
|
-
while len(
|
172
|
+
while len(rlb) < o:
|
171
173
|
more = check.isinstance((yield None), bytes)
|
172
174
|
if not more:
|
173
175
|
raise EOFError
|
174
176
|
# FIXME: lol - share guts with readers
|
175
|
-
|
177
|
+
rlb.feed(more)
|
176
178
|
|
177
|
-
i =
|
178
|
-
buf = buf[o:]
|
179
|
+
i = check.not_none(rlb.read(o))
|
179
180
|
|
180
181
|
else:
|
181
182
|
raise TypeError(o)
|
omlish/logs/color.py
CHANGED
@@ -2,15 +2,15 @@
|
|
2
2
|
import logging
|
3
3
|
import typing as ta
|
4
4
|
|
5
|
-
from .. import
|
5
|
+
from ..term import codes as tc
|
6
6
|
from .standard import StandardLogFormatter
|
7
7
|
|
8
8
|
|
9
9
|
class ColorLogFormatter(StandardLogFormatter):
|
10
|
-
LEVEL_COLORS: ta.Mapping[int,
|
11
|
-
logging.WARNING:
|
12
|
-
logging.ERROR:
|
13
|
-
logging.CRITICAL:
|
10
|
+
LEVEL_COLORS: ta.Mapping[int, tc.SGRs.FG] = {
|
11
|
+
logging.WARNING: tc.SGRs.FG.BRIGHT_YELLOW,
|
12
|
+
logging.ERROR: tc.SGRs.FG.BRIGHT_RED,
|
13
|
+
logging.CRITICAL: tc.SGRs.FG.BRIGHT_RED,
|
14
14
|
}
|
15
15
|
|
16
16
|
def formatMessage(self, record):
|
@@ -20,5 +20,5 @@ class ColorLogFormatter(StandardLogFormatter):
|
|
20
20
|
except KeyError:
|
21
21
|
pass
|
22
22
|
else:
|
23
|
-
buf =
|
23
|
+
buf = tc.SGR(c) + buf + tc.SGR(tc.SGRs.RESET)
|
24
24
|
return buf
|
omlish/subprocesses.py
CHANGED
omlish/term/__init__.py
ADDED
File without changes
|
omlish/{term.py → term/codes.py}
RENAMED
@@ -1,13 +1,8 @@
|
|
1
1
|
import enum
|
2
2
|
import re
|
3
|
-
import sys
|
4
|
-
import time
|
5
3
|
import typing as ta
|
6
4
|
|
7
|
-
from
|
8
|
-
|
9
|
-
|
10
|
-
T = ta.TypeVar('T')
|
5
|
+
from .. import lang
|
11
6
|
|
12
7
|
|
13
8
|
##
|
@@ -233,139 +228,6 @@ BG24_RGB = ControlSequence(
|
|
233
228
|
##
|
234
229
|
|
235
230
|
|
236
|
-
class ProgressBar:
|
237
|
-
"""
|
238
|
-
TODO:
|
239
|
-
- ProgressBarRenderer
|
240
|
-
- right-justify
|
241
|
-
- ProgressBarGroup
|
242
|
-
- animate
|
243
|
-
"""
|
244
|
-
|
245
|
-
def __init__(
|
246
|
-
self,
|
247
|
-
total: int | None = None,
|
248
|
-
*,
|
249
|
-
length: int = 40,
|
250
|
-
interval: float = .2,
|
251
|
-
start_time: float | None = None,
|
252
|
-
out: ta.TextIO | None = None,
|
253
|
-
) -> None:
|
254
|
-
super().__init__()
|
255
|
-
|
256
|
-
self._total = total
|
257
|
-
self._length = length
|
258
|
-
self._interval = interval
|
259
|
-
if start_time is None:
|
260
|
-
start_time = time.time()
|
261
|
-
self._start_time = start_time
|
262
|
-
if out is None:
|
263
|
-
out = sys.stdout
|
264
|
-
self._out = out
|
265
|
-
|
266
|
-
self._i = 0
|
267
|
-
self._elapsed = 0.
|
268
|
-
self._last_print = 0.
|
269
|
-
|
270
|
-
def render(
|
271
|
-
self,
|
272
|
-
*,
|
273
|
-
complete: bool = False,
|
274
|
-
) -> str:
|
275
|
-
iter_per_sec = self._i / self._elapsed if self._elapsed > 0 else 0
|
276
|
-
|
277
|
-
if self._total is not None:
|
278
|
-
remaining = (self._total - self._i) / iter_per_sec if iter_per_sec > 0 else 0
|
279
|
-
done = int(self._length * self._i / self._total)
|
280
|
-
|
281
|
-
bar = f'[{"█" * done}{"." * (self._length - done)}]'
|
282
|
-
info_parts = [
|
283
|
-
f'{self._i}/{self._total}',
|
284
|
-
f'{iter_per_sec:.2f} it/s',
|
285
|
-
f'{self._elapsed:.2f}s elapsed',
|
286
|
-
f'{remaining:.2f}s left',
|
287
|
-
]
|
288
|
-
|
289
|
-
else:
|
290
|
-
bar = f'[{("█" if complete else "?") * self._length}]'
|
291
|
-
info_parts = [
|
292
|
-
f'{self._i}',
|
293
|
-
f'{iter_per_sec:.2f} it/s',
|
294
|
-
f'{self._elapsed:.2f}s elapsed',
|
295
|
-
]
|
296
|
-
|
297
|
-
info = ' | '.join(info_parts)
|
298
|
-
return f'{bar} {info}'
|
299
|
-
|
300
|
-
def print(
|
301
|
-
self,
|
302
|
-
*,
|
303
|
-
now: float | None = None,
|
304
|
-
**kwargs: ta.Any,
|
305
|
-
) -> None:
|
306
|
-
if now is None:
|
307
|
-
now = time.time()
|
308
|
-
|
309
|
-
line = self.render(**kwargs)
|
310
|
-
self._out.write(f'\033[2K\033[G{line}')
|
311
|
-
self._out.flush()
|
312
|
-
|
313
|
-
self._last_print = now
|
314
|
-
|
315
|
-
def update(
|
316
|
-
self,
|
317
|
-
n: int = 1,
|
318
|
-
*,
|
319
|
-
now: float | None = None,
|
320
|
-
silent: bool = False,
|
321
|
-
) -> None:
|
322
|
-
if now is None:
|
323
|
-
now = time.time()
|
324
|
-
|
325
|
-
self._i += n
|
326
|
-
self._elapsed = now - self._start_time
|
327
|
-
|
328
|
-
if not silent:
|
329
|
-
if now - self._last_print >= self._interval:
|
330
|
-
self.print(now=now)
|
331
|
-
|
332
|
-
|
333
|
-
def progress_bar(
|
334
|
-
seq: ta.Iterable[T],
|
335
|
-
*,
|
336
|
-
no_tty_check: bool = False,
|
337
|
-
total: int | None = None,
|
338
|
-
out: ta.TextIO | None = None,
|
339
|
-
**kwargs: ta.Any,
|
340
|
-
) -> ta.Generator[T, None, None]:
|
341
|
-
if out is None:
|
342
|
-
out = sys.stdout
|
343
|
-
|
344
|
-
if not no_tty_check and not out.isatty():
|
345
|
-
yield from seq
|
346
|
-
return
|
347
|
-
|
348
|
-
if total is None:
|
349
|
-
if isinstance(seq, ta.Sized):
|
350
|
-
total = len(seq)
|
351
|
-
|
352
|
-
pb = ProgressBar(
|
353
|
-
total=total,
|
354
|
-
out=out,
|
355
|
-
**kwargs,
|
356
|
-
)
|
357
|
-
|
358
|
-
for item in seq:
|
359
|
-
pb.update()
|
360
|
-
yield item
|
361
|
-
|
362
|
-
pb.print(complete=True)
|
363
|
-
out.write('\n')
|
364
|
-
|
365
|
-
|
366
|
-
##
|
367
|
-
|
368
|
-
|
369
231
|
def main() -> None:
|
370
232
|
import sys
|
371
233
|
|
@@ -0,0 +1,139 @@
|
|
1
|
+
import sys
|
2
|
+
import time
|
3
|
+
import typing as ta
|
4
|
+
|
5
|
+
|
6
|
+
T = ta.TypeVar('T')
|
7
|
+
|
8
|
+
|
9
|
+
##
|
10
|
+
|
11
|
+
|
12
|
+
class ProgressBar:
|
13
|
+
"""
|
14
|
+
TODO:
|
15
|
+
- ProgressBarRenderer
|
16
|
+
- right-justify
|
17
|
+
- ProgressBarGroup
|
18
|
+
- animate
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
total: int | None = None,
|
24
|
+
*,
|
25
|
+
length: int = 40,
|
26
|
+
interval: float = .2,
|
27
|
+
start_time: float | None = None,
|
28
|
+
out: ta.TextIO | None = None,
|
29
|
+
) -> None:
|
30
|
+
super().__init__()
|
31
|
+
|
32
|
+
self._total = total
|
33
|
+
self._length = length
|
34
|
+
self._interval = interval
|
35
|
+
if start_time is None:
|
36
|
+
start_time = time.time()
|
37
|
+
self._start_time = start_time
|
38
|
+
if out is None:
|
39
|
+
out = sys.stdout
|
40
|
+
self._out = out
|
41
|
+
|
42
|
+
self._i = 0
|
43
|
+
self._elapsed = 0.
|
44
|
+
self._last_print = 0.
|
45
|
+
|
46
|
+
def render(
|
47
|
+
self,
|
48
|
+
*,
|
49
|
+
complete: bool = False,
|
50
|
+
) -> str:
|
51
|
+
iter_per_sec = self._i / self._elapsed if self._elapsed > 0 else 0
|
52
|
+
|
53
|
+
if self._total is not None:
|
54
|
+
remaining = (self._total - self._i) / iter_per_sec if iter_per_sec > 0 else 0
|
55
|
+
done = int(self._length * self._i / self._total)
|
56
|
+
|
57
|
+
bar = f'[{"█" * done}{"." * (self._length - done)}]'
|
58
|
+
info_parts = [
|
59
|
+
f'{self._i}/{self._total}',
|
60
|
+
f'{iter_per_sec:.2f} it/s',
|
61
|
+
f'{self._elapsed:.2f}s elapsed',
|
62
|
+
f'{remaining:.2f}s left',
|
63
|
+
]
|
64
|
+
|
65
|
+
else:
|
66
|
+
bar = f'[{("█" if complete else "?") * self._length}]'
|
67
|
+
info_parts = [
|
68
|
+
f'{self._i}',
|
69
|
+
f'{iter_per_sec:.2f} it/s',
|
70
|
+
f'{self._elapsed:.2f}s elapsed',
|
71
|
+
]
|
72
|
+
|
73
|
+
info = ' | '.join(info_parts)
|
74
|
+
return f'{bar} {info}'
|
75
|
+
|
76
|
+
def print(
|
77
|
+
self,
|
78
|
+
*,
|
79
|
+
now: float | None = None,
|
80
|
+
**kwargs: ta.Any,
|
81
|
+
) -> None:
|
82
|
+
if now is None:
|
83
|
+
now = time.time()
|
84
|
+
|
85
|
+
line = self.render(**kwargs)
|
86
|
+
self._out.write(f'\033[2K\033[G{line}')
|
87
|
+
self._out.flush()
|
88
|
+
|
89
|
+
self._last_print = now
|
90
|
+
|
91
|
+
def update(
|
92
|
+
self,
|
93
|
+
n: int = 1,
|
94
|
+
*,
|
95
|
+
now: float | None = None,
|
96
|
+
silent: bool = False,
|
97
|
+
) -> None:
|
98
|
+
if now is None:
|
99
|
+
now = time.time()
|
100
|
+
|
101
|
+
self._i += n
|
102
|
+
self._elapsed = now - self._start_time
|
103
|
+
|
104
|
+
if not silent:
|
105
|
+
if now - self._last_print >= self._interval:
|
106
|
+
self.print(now=now)
|
107
|
+
|
108
|
+
|
109
|
+
def progress_bar(
|
110
|
+
seq: ta.Iterable[T],
|
111
|
+
*,
|
112
|
+
no_tty_check: bool = False,
|
113
|
+
total: int | None = None,
|
114
|
+
out: ta.TextIO | None = None,
|
115
|
+
**kwargs: ta.Any,
|
116
|
+
) -> ta.Generator[T, None, None]:
|
117
|
+
if out is None:
|
118
|
+
out = sys.stdout
|
119
|
+
|
120
|
+
if not no_tty_check and not out.isatty():
|
121
|
+
yield from seq
|
122
|
+
return
|
123
|
+
|
124
|
+
if total is None:
|
125
|
+
if isinstance(seq, ta.Sized):
|
126
|
+
total = len(seq)
|
127
|
+
|
128
|
+
pb = ProgressBar(
|
129
|
+
total=total,
|
130
|
+
out=out,
|
131
|
+
**kwargs,
|
132
|
+
)
|
133
|
+
|
134
|
+
for item in seq:
|
135
|
+
pb.update()
|
136
|
+
yield item
|
137
|
+
|
138
|
+
pb.print(complete=True)
|
139
|
+
out.write('\n')
|
File without changes
|
omlish/term/vt100/c.py
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
import io
|
2
|
+
|
3
|
+
from ... import check
|
4
|
+
from .states import ACTIONS_IN_ORDER
|
5
|
+
from .states import STATE_TABLES
|
6
|
+
from .states import STATES
|
7
|
+
from .states import STATES_IN_ORDER
|
8
|
+
from .states import StateTransition
|
9
|
+
from .states import check_state_tables
|
10
|
+
|
11
|
+
|
12
|
+
def _pad(s: str, length: int) -> str:
|
13
|
+
return s + ' ' * (length - len(s))
|
14
|
+
|
15
|
+
|
16
|
+
def _generate_c() -> dict[str, str]:
|
17
|
+
out = {}
|
18
|
+
|
19
|
+
#
|
20
|
+
|
21
|
+
f = io.StringIO()
|
22
|
+
|
23
|
+
f.write('typedef enum {\n')
|
24
|
+
for i, state in enumerate(STATES_IN_ORDER):
|
25
|
+
f.write(f' VTPARSE_STATE_{state.upper()} = {i + 1},\n')
|
26
|
+
f.write('} vtparse_state_t;\n\n')
|
27
|
+
|
28
|
+
f.write('typedef enum {\n')
|
29
|
+
for i, action_str in enumerate(ACTIONS_IN_ORDER):
|
30
|
+
f.write(f' VTPARSE_ACTION_{action_str.upper()} = {i + 1},\n')
|
31
|
+
f.write('} vtparse_action_t;\n\n')
|
32
|
+
|
33
|
+
f.write('typedef unsigned char state_change_t;\n')
|
34
|
+
f.write(f'extern state_change_t STATE_TABLE[{len(STATES_IN_ORDER)}][256];\n')
|
35
|
+
f.write(f'extern vtparse_action_t ENTRY_ACTIONS[{len(STATES_IN_ORDER)}];\n')
|
36
|
+
f.write(f'extern vtparse_action_t EXIT_ACTIONS[{len(STATES_IN_ORDER)}];\n')
|
37
|
+
f.write(f'extern char *ACTION_NAMES[{len(ACTIONS_IN_ORDER) + 1}];\n')
|
38
|
+
f.write(f'extern char *STATE_NAMES[{len(STATES_IN_ORDER) + 1}];\n\n')
|
39
|
+
|
40
|
+
out['vtparse_table.h'] = f.getvalue()
|
41
|
+
|
42
|
+
#
|
43
|
+
|
44
|
+
f = io.StringIO()
|
45
|
+
|
46
|
+
f.write('#include "vtparse_table.h"\n\n')
|
47
|
+
|
48
|
+
f.write('char *ACTION_NAMES[] = {\n')
|
49
|
+
f.write(' "<no action>",\n')
|
50
|
+
for action_str in ACTIONS_IN_ORDER:
|
51
|
+
f.write(f' "{action_str.upper()}",\n')
|
52
|
+
f.write('};\n\n')
|
53
|
+
|
54
|
+
f.write('char *STATE_NAMES[] = {\n')
|
55
|
+
f.write(' "<no state>",\n')
|
56
|
+
for state in STATES_IN_ORDER:
|
57
|
+
f.write(f' "{state}",\n')
|
58
|
+
f.write('};\n\n')
|
59
|
+
|
60
|
+
f.write(f'state_change_t STATE_TABLE[{len(STATES_IN_ORDER)}][256] = {{\n')
|
61
|
+
for i, state in enumerate(STATES_IN_ORDER):
|
62
|
+
f.write(f' {{ /* VTPARSE_STATE_{state.upper()} = {i} */\n')
|
63
|
+
for j, state_change in enumerate(STATE_TABLES[state]):
|
64
|
+
if not state_change:
|
65
|
+
f.write(' 0,\n')
|
66
|
+
else:
|
67
|
+
action = next((s for s in state_change if isinstance(s, str)), None)
|
68
|
+
state_trans = next((s for s in state_change if isinstance(s, StateTransition)), None)
|
69
|
+
action_str = f'VTPARSE_ACTION_{action.upper()}' if action else '0'
|
70
|
+
state_str = f'VTPARSE_STATE_{state_trans.to_state}' if state_trans else '0'
|
71
|
+
f.write(
|
72
|
+
f'/*{str(j).rjust(3)}*/ {_pad(action_str, 33)} | ({_pad(state_str, 33)} << 4),\n')
|
73
|
+
f.write(' },\n')
|
74
|
+
f.write('};\n\n')
|
75
|
+
|
76
|
+
f.write('vtparse_action_t ENTRY_ACTIONS[] = {\n')
|
77
|
+
for state in STATES_IN_ORDER:
|
78
|
+
actions = STATES[state]
|
79
|
+
if 'on_entry' in actions:
|
80
|
+
f.write(f" VTPARSE_ACTION_{check.isinstance(actions['on_entry'], str).upper()}, /* {state} */\n")
|
81
|
+
else:
|
82
|
+
f.write(f' 0 /* none for {state} */,\n')
|
83
|
+
f.write('};\n\n')
|
84
|
+
|
85
|
+
f.write('vtparse_action_t EXIT_ACTIONS[] = {\n')
|
86
|
+
for state in STATES_IN_ORDER:
|
87
|
+
actions = STATES[state]
|
88
|
+
if 'on_exit' in actions:
|
89
|
+
f.write(f" VTPARSE_ACTION_{check.isinstance(actions['on_exit'], str).upper()}, /* {state} */\n")
|
90
|
+
else:
|
91
|
+
f.write(f' 0 /* none for {state} */,\n')
|
92
|
+
f.write('};\n\n')
|
93
|
+
|
94
|
+
out['vtparse_table.c'] = f.getvalue()
|
95
|
+
|
96
|
+
#
|
97
|
+
|
98
|
+
return out
|
99
|
+
|
100
|
+
|
101
|
+
if __name__ == '__main__':
|
102
|
+
check_state_tables(STATE_TABLES)
|
103
|
+
for f, c in _generate_c().items():
|
104
|
+
print(f)
|
105
|
+
print(c)
|
106
|
+
print()
|
@@ -0,0 +1,271 @@
|
|
1
|
+
"""
|
2
|
+
Original author: Joshua Haberman
|
3
|
+
https://github.com/haberman/vtparse/blob/198ea4382f824dbb3f0e5b5553a9eb3290764694/vtparse_tables.rb
|
4
|
+
|
5
|
+
==
|
6
|
+
|
7
|
+
https://vt100.net/emu/dec_ansi_parser
|
8
|
+
https://github.com/haberman/vtparse
|
9
|
+
"""
|
10
|
+
import typing as ta
|
11
|
+
|
12
|
+
|
13
|
+
##
|
14
|
+
|
15
|
+
|
16
|
+
class StateTransition(ta.NamedTuple):
|
17
|
+
to_state: str
|
18
|
+
|
19
|
+
|
20
|
+
TransitionMap: ta.TypeAlias = ta.Mapping[
|
21
|
+
int | range | str,
|
22
|
+
str | StateTransition | tuple[str | StateTransition, ...],
|
23
|
+
]
|
24
|
+
|
25
|
+
|
26
|
+
##
|
27
|
+
|
28
|
+
|
29
|
+
# Define the anywhere transitions
|
30
|
+
ANYWHERE_TRANSITIONS: TransitionMap = {
|
31
|
+
0x18: ('execute', StateTransition('ground')),
|
32
|
+
0x1a: ('execute', StateTransition('ground')),
|
33
|
+
range(0x80, 0x90): ('execute', StateTransition('ground')),
|
34
|
+
range(0x91, 0x98): ('execute', StateTransition('ground')),
|
35
|
+
0x99: ('execute', StateTransition('ground')),
|
36
|
+
0x9a: ('execute', StateTransition('ground')),
|
37
|
+
0x9c: StateTransition('ground'),
|
38
|
+
0x1b: StateTransition('escape'),
|
39
|
+
0x98: StateTransition('sos_pm_apc_string'),
|
40
|
+
0x9e: StateTransition('sos_pm_apc_string'),
|
41
|
+
0x9f: StateTransition('sos_pm_apc_string'),
|
42
|
+
0x90: StateTransition('dcs_entry'),
|
43
|
+
0x9d: StateTransition('osc_string'),
|
44
|
+
0x9b: StateTransition('csi_entry'),
|
45
|
+
}
|
46
|
+
|
47
|
+
|
48
|
+
# Global states dictionary
|
49
|
+
STATES: ta.Mapping[str, TransitionMap] = {
|
50
|
+
# Define state transitions
|
51
|
+
'ground': {
|
52
|
+
range(0x18): 'execute',
|
53
|
+
0x19: 'execute',
|
54
|
+
range(0x1c, 0x20): 'execute',
|
55
|
+
range(0x20, 0x80): 'print',
|
56
|
+
},
|
57
|
+
|
58
|
+
'escape': {
|
59
|
+
'on_entry': 'clear',
|
60
|
+
range(0x18): 'execute',
|
61
|
+
0x19: 'execute',
|
62
|
+
range(0x1c, 0x20): 'execute',
|
63
|
+
0x7f: 'ignore',
|
64
|
+
range(0x20, 0x30): ('collect', StateTransition('escape_intermediate')),
|
65
|
+
range(0x30, 0x50): ('esc_dispatch', StateTransition('ground')),
|
66
|
+
range(0x51, 0x58): ('esc_dispatch', StateTransition('ground')),
|
67
|
+
0x59: ('esc_dispatch', StateTransition('ground')),
|
68
|
+
0x5a: ('esc_dispatch', StateTransition('ground')),
|
69
|
+
0x5c: ('esc_dispatch', StateTransition('ground')),
|
70
|
+
range(0x60, 0x7f): ('esc_dispatch', StateTransition('ground')),
|
71
|
+
0x5b: StateTransition('csi_entry'),
|
72
|
+
0x5d: StateTransition('osc_string'),
|
73
|
+
0x50: StateTransition('dcs_entry'),
|
74
|
+
0x58: StateTransition('sos_pm_apc_string'),
|
75
|
+
0x5e: StateTransition('sos_pm_apc_string'),
|
76
|
+
0x5f: StateTransition('sos_pm_apc_string'),
|
77
|
+
},
|
78
|
+
|
79
|
+
'escape_intermediate': {
|
80
|
+
range(0x18): 'execute',
|
81
|
+
0x19: 'execute',
|
82
|
+
range(0x1c, 0x20): 'execute',
|
83
|
+
range(0x20, 0x30): 'collect',
|
84
|
+
0x7f: 'ignore',
|
85
|
+
range(0x30, 0x7f): ('esc_dispatch', StateTransition('ground')),
|
86
|
+
},
|
87
|
+
|
88
|
+
'csi_entry': {
|
89
|
+
'on_entry': 'clear',
|
90
|
+
range(0x18): 'execute',
|
91
|
+
0x19: 'execute',
|
92
|
+
range(0x1c, 0x20): 'execute',
|
93
|
+
0x7f: 'ignore',
|
94
|
+
range(0x20, 0x30): ('collect', StateTransition('csi_intermediate')),
|
95
|
+
0x3a: StateTransition('csi_ignore'),
|
96
|
+
range(0x30, 0x3a): ('param', StateTransition('csi_param')),
|
97
|
+
0x3b: ('param', StateTransition('csi_param')),
|
98
|
+
range(0x3c, 0x40): ('collect', StateTransition('csi_param')),
|
99
|
+
range(0x40, 0x7f): ('csi_dispatch', StateTransition('ground')),
|
100
|
+
},
|
101
|
+
|
102
|
+
'csi_ignore': {
|
103
|
+
range(0x18): 'execute',
|
104
|
+
0x19: 'execute',
|
105
|
+
range(0x1c, 0x20): 'execute',
|
106
|
+
range(0x20, 0x40): 'ignore',
|
107
|
+
0x7f: 'ignore',
|
108
|
+
range(0x40, 0x7f): StateTransition('ground'),
|
109
|
+
},
|
110
|
+
|
111
|
+
'csi_param': {
|
112
|
+
range(0x18): 'execute',
|
113
|
+
0x19: 'execute',
|
114
|
+
range(0x1c, 0x20): 'execute',
|
115
|
+
range(0x30, 0x3a): 'param',
|
116
|
+
0x3b: 'param',
|
117
|
+
0x7f: 'ignore',
|
118
|
+
0x3a: StateTransition('csi_ignore'),
|
119
|
+
range(0x3c, 0x40): StateTransition('csi_ignore'),
|
120
|
+
range(0x20, 0x30): ('collect', StateTransition('csi_intermediate')),
|
121
|
+
range(0x40, 0x7f): ('csi_dispatch', StateTransition('ground')),
|
122
|
+
},
|
123
|
+
|
124
|
+
'csi_intermediate': {
|
125
|
+
range(0x18): 'execute',
|
126
|
+
0x19: 'execute',
|
127
|
+
range(0x1c, 0x20): 'execute',
|
128
|
+
range(0x20, 0x30): 'collect',
|
129
|
+
0x7f: 'ignore',
|
130
|
+
range(0x30, 0x40): StateTransition('csi_ignore'),
|
131
|
+
range(0x40, 0x7f): ('csi_dispatch', StateTransition('ground')),
|
132
|
+
},
|
133
|
+
|
134
|
+
'dcs_entry': {
|
135
|
+
'on_entry': 'clear',
|
136
|
+
range(0x18): 'ignore',
|
137
|
+
0x19: 'ignore',
|
138
|
+
range(0x1c, 0x20): 'ignore',
|
139
|
+
0x7f: 'ignore',
|
140
|
+
0x3a: StateTransition('dcs_ignore'),
|
141
|
+
range(0x20, 0x30): ('collect', StateTransition('dcs_intermediate')),
|
142
|
+
range(0x30, 0x3a): ('param', StateTransition('dcs_param')),
|
143
|
+
0x3b: ('param', StateTransition('dcs_param')),
|
144
|
+
range(0x3c, 0x40): ('collect', StateTransition('dcs_param')),
|
145
|
+
range(0x40, 0x7f): (StateTransition('dcs_passthrough')),
|
146
|
+
},
|
147
|
+
|
148
|
+
'dcs_intermediate': {
|
149
|
+
range(0x18): 'ignore',
|
150
|
+
0x19: 'ignore',
|
151
|
+
range(0x1c, 0x20): 'ignore',
|
152
|
+
range(0x20, 0x30): 'collect',
|
153
|
+
0x7f: 'ignore',
|
154
|
+
range(0x30, 0x40): StateTransition('dcs_ignore'),
|
155
|
+
range(0x40, 0x7f): StateTransition('dcs_passthrough'),
|
156
|
+
},
|
157
|
+
|
158
|
+
'dcs_ignore': {
|
159
|
+
range(0x18): 'ignore',
|
160
|
+
0x19: 'ignore',
|
161
|
+
range(0x1c, 0x80): 'ignore',
|
162
|
+
},
|
163
|
+
|
164
|
+
'dcs_param': {
|
165
|
+
range(0x18): 'ignore',
|
166
|
+
0x19: 'ignore',
|
167
|
+
range(0x1c, 0x20): 'ignore',
|
168
|
+
range(0x30, 0x3a): 'param',
|
169
|
+
0x3b: 'param',
|
170
|
+
0x7f: 'ignore',
|
171
|
+
0x3a: StateTransition('dcs_ignore'),
|
172
|
+
range(0x3c, 0x40): StateTransition('dcs_ignore'),
|
173
|
+
range(0x20, 0x30): ('collect', StateTransition('dcs_intermediate')),
|
174
|
+
range(0x40, 0x7f): StateTransition('dcs_passthrough'),
|
175
|
+
},
|
176
|
+
|
177
|
+
'dcs_passthrough': {
|
178
|
+
'on_entry': 'hook',
|
179
|
+
range(0x18): 'put',
|
180
|
+
0x19: 'put',
|
181
|
+
range(0x1c, 0x20): 'put',
|
182
|
+
range(0x20, 0x7f): 'put',
|
183
|
+
0x7f: 'ignore',
|
184
|
+
'on_exit': 'unhook',
|
185
|
+
},
|
186
|
+
|
187
|
+
'sos_pm_apc_string': {
|
188
|
+
range(0x18): 'ignore',
|
189
|
+
0x19: 'ignore',
|
190
|
+
range(0x1c, 0x80): 'ignore',
|
191
|
+
},
|
192
|
+
|
193
|
+
'osc_string': {
|
194
|
+
'on_entry': 'osc_start',
|
195
|
+
range(0x18): 'ignore',
|
196
|
+
0x19: 'ignore',
|
197
|
+
range(0x1c, 0x20): 'ignore',
|
198
|
+
range(0x20, 0x80): 'osc_put',
|
199
|
+
'on_exit': 'osc_end',
|
200
|
+
},
|
201
|
+
}
|
202
|
+
|
203
|
+
|
204
|
+
##
|
205
|
+
|
206
|
+
|
207
|
+
ACTIONS_IN_ORDER = sorted(
|
208
|
+
action
|
209
|
+
for state, transitions in STATES.items()
|
210
|
+
for keys, actions in transitions.items()
|
211
|
+
for action in ([actions] if not isinstance(actions, tuple) else actions)
|
212
|
+
if isinstance(action, str)
|
213
|
+
)
|
214
|
+
|
215
|
+
STATES_IN_ORDER = sorted(STATES)
|
216
|
+
|
217
|
+
|
218
|
+
##
|
219
|
+
|
220
|
+
|
221
|
+
TransitionTable: ta.TypeAlias = tuple[tuple[str | StateTransition, ...], ...]
|
222
|
+
|
223
|
+
|
224
|
+
def _build_state_tables() -> ta.Mapping[str, TransitionTable]:
|
225
|
+
# Expand the range-based data structures into fully expanded tables
|
226
|
+
state_tables: dict[str, list] = {}
|
227
|
+
|
228
|
+
def expand_ranges(dct):
|
229
|
+
array = [None] * 256
|
230
|
+
for k, v in dct.items():
|
231
|
+
if isinstance(k, range):
|
232
|
+
for i in k:
|
233
|
+
array[i] = v
|
234
|
+
elif isinstance(k, int):
|
235
|
+
array[k] = v
|
236
|
+
return array
|
237
|
+
|
238
|
+
for s, t in STATES.items():
|
239
|
+
state_tables[s] = expand_ranges(t)
|
240
|
+
|
241
|
+
# Seed all the states with the anywhere transitions
|
242
|
+
anywhere_transitions_expanded = expand_ranges(ANYWHERE_TRANSITIONS)
|
243
|
+
|
244
|
+
for state, transitions in state_tables.items():
|
245
|
+
for i, transition in enumerate(anywhere_transitions_expanded):
|
246
|
+
if transition is not None:
|
247
|
+
if transitions[i] is not None:
|
248
|
+
raise ValueError(
|
249
|
+
f'State {state} already had a transition defined for 0x{i:02x}, but that transition is also an '
|
250
|
+
f'anywhere transition!',
|
251
|
+
)
|
252
|
+
transitions[i] = transition
|
253
|
+
|
254
|
+
# For consistency, make all transitions tuples of actions
|
255
|
+
return {
|
256
|
+
state: tuple(tuple(t if isinstance(t, (list, tuple)) else [t]) for t in transitions)
|
257
|
+
for state, transitions in state_tables.items()
|
258
|
+
}
|
259
|
+
|
260
|
+
|
261
|
+
STATE_TABLES = _build_state_tables()
|
262
|
+
|
263
|
+
|
264
|
+
##
|
265
|
+
|
266
|
+
|
267
|
+
def check_state_tables(state_tables: ta.Mapping[str, TransitionTable]) -> None:
|
268
|
+
for state, transitions in state_tables.items():
|
269
|
+
for i, val in enumerate(transitions):
|
270
|
+
if not val:
|
271
|
+
raise ValueError(f'No transition defined from state {state}, char 0x{i:02x}!')
|
@@ -0,0 +1,271 @@
|
|
1
|
+
import dataclasses as dc
|
2
|
+
import string
|
3
|
+
import typing as ta
|
4
|
+
|
5
|
+
from omlish.lite.check import check
|
6
|
+
|
7
|
+
|
8
|
+
T = ta.TypeVar('T')
|
9
|
+
|
10
|
+
|
11
|
+
##
|
12
|
+
|
13
|
+
|
14
|
+
@dc.dataclass()
|
15
|
+
class Cell:
|
16
|
+
"""Represents a single character cell in the terminal with attributes."""
|
17
|
+
|
18
|
+
char: str = ' '
|
19
|
+
|
20
|
+
fg: str = 'default'
|
21
|
+
bg: str = 'default'
|
22
|
+
|
23
|
+
bold: bool = False
|
24
|
+
underline: bool = False
|
25
|
+
reverse: bool = False
|
26
|
+
|
27
|
+
def __repr__(self):
|
28
|
+
return f'Cell({self.char!r}, bold={self.bold}, underline={self.underline})'
|
29
|
+
|
30
|
+
|
31
|
+
class Vt100Terminal:
|
32
|
+
def __init__(
|
33
|
+
self,
|
34
|
+
rows: int = 24,
|
35
|
+
cols: int = 80,
|
36
|
+
) -> None:
|
37
|
+
super().__init__()
|
38
|
+
|
39
|
+
self._rows = rows
|
40
|
+
self._cols = cols
|
41
|
+
|
42
|
+
# 2D array of Cell objects
|
43
|
+
self._screen = [
|
44
|
+
[Cell() for _ in range(cols)]
|
45
|
+
for _ in range(rows)
|
46
|
+
]
|
47
|
+
|
48
|
+
# Current cursor position (row, col), 0-based internally
|
49
|
+
self._cursor_row = 0
|
50
|
+
self._cursor_col = 0
|
51
|
+
|
52
|
+
# Current text attributes
|
53
|
+
self._current_fg = 'default'
|
54
|
+
self._current_bg = 'default'
|
55
|
+
self._current_bold = False
|
56
|
+
self._current_underline = False
|
57
|
+
self._current_reverse = False
|
58
|
+
|
59
|
+
# Parser state
|
60
|
+
self._state: ta.Literal['normal', 'esc', 'csi'] = 'normal'
|
61
|
+
self._escape_buffer: list[str] = []
|
62
|
+
|
63
|
+
def parse_byte(self, byte: int | str) -> None:
|
64
|
+
"""Parse a single byte of input (as an integer or a single-character string)."""
|
65
|
+
|
66
|
+
if isinstance(byte, int):
|
67
|
+
byte = chr(byte)
|
68
|
+
|
69
|
+
if self._state == 'normal':
|
70
|
+
if byte == '\x1b':
|
71
|
+
# Start of escape sequence
|
72
|
+
self._state = 'esc'
|
73
|
+
self._escape_buffer = [byte]
|
74
|
+
elif byte == '\r':
|
75
|
+
# Carriage return
|
76
|
+
self._cursor_col = 0
|
77
|
+
elif byte == '\n':
|
78
|
+
# Line feed
|
79
|
+
self._cursor_row = min(self._cursor_row + 1, self._rows - 1)
|
80
|
+
elif byte == '\b':
|
81
|
+
# Backspace
|
82
|
+
self._cursor_col = max(self._cursor_col - 1, 0)
|
83
|
+
elif byte in string.printable and byte not in ['\x0b', '\x0c']:
|
84
|
+
# Printable ASCII (excluding form feeds, vertical tabs, etc.)
|
85
|
+
self._put_char(byte)
|
86
|
+
else:
|
87
|
+
# Ignore other control characters
|
88
|
+
pass
|
89
|
+
|
90
|
+
elif self._state == 'esc':
|
91
|
+
self._escape_buffer.append(byte)
|
92
|
+
if byte == '[':
|
93
|
+
# Move to CSI state (Control Sequence Introducer)
|
94
|
+
self._state = 'csi'
|
95
|
+
# Some escape codes like ESCc, ESC7, etc. are possible but we'll ignore or handle them in a minimal way. If
|
96
|
+
# no further instructions, revert to normal
|
97
|
+
elif len(self._escape_buffer) == 2:
|
98
|
+
# We only support ESC + [ in this example, so revert
|
99
|
+
self._state = 'normal'
|
100
|
+
|
101
|
+
elif self._state == 'csi':
|
102
|
+
self._escape_buffer.append(byte)
|
103
|
+
# Check if this byte ends the sequence (typical final bytes are A-Z, @, etc.)
|
104
|
+
if byte.isalpha() or byte in '@`~':
|
105
|
+
# We have a complete CSI sequence: parse it
|
106
|
+
self._parse_csi(''.join(self._escape_buffer))
|
107
|
+
# Reset state
|
108
|
+
self._state = 'normal'
|
109
|
+
self._escape_buffer = []
|
110
|
+
|
111
|
+
def _put_char(self, ch: str) -> None:
|
112
|
+
"""Write a character at the current cursor position, and advance the cursor."""
|
113
|
+
|
114
|
+
if 0 <= self._cursor_row < self._rows and 0 <= self._cursor_col < self._cols:
|
115
|
+
cell = self._screen[self._cursor_row][self._cursor_col]
|
116
|
+
cell.char = ch
|
117
|
+
cell.fg = self._current_fg
|
118
|
+
cell.bg = self._current_bg
|
119
|
+
cell.bold = self._current_bold
|
120
|
+
cell.underline = self._current_underline
|
121
|
+
cell.reverse = self._current_reverse
|
122
|
+
|
123
|
+
self._cursor_col += 1
|
124
|
+
if self._cursor_col >= self._cols:
|
125
|
+
self._cursor_col = 0
|
126
|
+
self._cursor_row = min(self._cursor_row + 1, self._rows - 1)
|
127
|
+
|
128
|
+
def _parse_csi(self, seq: str) -> None:
|
129
|
+
"""
|
130
|
+
Parse a CSI (Control Sequence Introducer) escape sequence. Typically looks like: ESC [ parameters letter For
|
131
|
+
example: ESC [ 2 J, ESC [ 10 ; 20 H, etc.
|
132
|
+
"""
|
133
|
+
|
134
|
+
# seq includes the initial ESC[. e.g. "\x1b[10;20H"
|
135
|
+
# We'll strip ESC[ and parse what remains.
|
136
|
+
check.state(seq.startswith('\x1b['))
|
137
|
+
body = seq[2:] # everything after ESC[
|
138
|
+
|
139
|
+
# Find final character that determines the command (e.g. 'A', 'B', 'H', 'J', 'K', 'm', etc.)
|
140
|
+
final = body[-1]
|
141
|
+
params = body[:-1]
|
142
|
+
|
143
|
+
# Split params by semicolons, handle empty as '0'
|
144
|
+
if params.strip() == '':
|
145
|
+
numbers = [0]
|
146
|
+
else:
|
147
|
+
numbers = [int(x) if x.isdigit() else 0 for x in params.split(';')]
|
148
|
+
|
149
|
+
if final in 'ABCD':
|
150
|
+
# Cursor movement
|
151
|
+
n = numbers[0] if numbers else 1
|
152
|
+
if final == 'A':
|
153
|
+
# Up
|
154
|
+
self._cursor_row = max(self._cursor_row - n, 0)
|
155
|
+
elif final == 'B':
|
156
|
+
# Down
|
157
|
+
self._cursor_row = min(self._cursor_row + n, self._rows - 1)
|
158
|
+
elif final == 'C':
|
159
|
+
# Right
|
160
|
+
self._cursor_col = min(self._cursor_col + n, self._cols - 1)
|
161
|
+
elif final == 'D':
|
162
|
+
# Left
|
163
|
+
self._cursor_col = max(self._cursor_col - n, 0)
|
164
|
+
|
165
|
+
elif final in 'Hf':
|
166
|
+
# Cursor position
|
167
|
+
# CSI row;colH or CSI row;colf (1-based coords)
|
168
|
+
row = numbers[0] if len(numbers) > 0 else 1
|
169
|
+
col = numbers[1] if len(numbers) > 1 else 1
|
170
|
+
self._cursor_row = min(max(row - 1, 0), self._rows - 1)
|
171
|
+
self._cursor_col = min(max(col - 1, 0), self._cols - 1)
|
172
|
+
|
173
|
+
elif final == 'J':
|
174
|
+
# Erase display
|
175
|
+
# n=0 -> clear from cursor down,
|
176
|
+
# n=1 -> clear from cursor up,
|
177
|
+
# n=2 -> clear entire screen
|
178
|
+
n = numbers[0] if numbers else 0
|
179
|
+
if n == 2:
|
180
|
+
self._clear_screen()
|
181
|
+
elif n == 0:
|
182
|
+
self._clear_down()
|
183
|
+
elif n == 1:
|
184
|
+
self._clear_up()
|
185
|
+
# else: unsupported J mode, ignore
|
186
|
+
|
187
|
+
elif final == 'K':
|
188
|
+
# Erase line
|
189
|
+
# n=0 -> clear from cursor right
|
190
|
+
# n=1 -> clear from cursor left
|
191
|
+
# n=2 -> clear entire line
|
192
|
+
n = numbers[0] if numbers else 0
|
193
|
+
if n == 2:
|
194
|
+
self._clear_line(self._cursor_row)
|
195
|
+
elif n == 0:
|
196
|
+
self._clear_right(self._cursor_row, self._cursor_col)
|
197
|
+
elif n == 1:
|
198
|
+
self._clear_left(self._cursor_row, self._cursor_col)
|
199
|
+
# else: ignore
|
200
|
+
|
201
|
+
elif final == 'm':
|
202
|
+
# SGR - Select Graphic Rendition
|
203
|
+
# We handle a subset: 0 (reset), 1 (bold), 4 (underline), 7 (reverse)
|
204
|
+
# Colors could be extended, but here we keep it minimal
|
205
|
+
for code in numbers:
|
206
|
+
if code == 0:
|
207
|
+
self._current_fg = 'default'
|
208
|
+
self._current_bg = 'default'
|
209
|
+
self._current_bold = False
|
210
|
+
self._current_underline = False
|
211
|
+
self._current_reverse = False
|
212
|
+
elif code == 1:
|
213
|
+
self._current_bold = True
|
214
|
+
elif code == 4:
|
215
|
+
self._current_underline = True
|
216
|
+
elif code == 7:
|
217
|
+
self._current_reverse = True
|
218
|
+
# You can add more codes for color, etc.
|
219
|
+
else:
|
220
|
+
# Unsupported SGR code - ignore gracefully
|
221
|
+
pass
|
222
|
+
|
223
|
+
else:
|
224
|
+
# Unsupported final - ignore gracefully
|
225
|
+
pass
|
226
|
+
|
227
|
+
# hods for Erase Operations
|
228
|
+
|
229
|
+
def _clear_screen(self) -> None:
|
230
|
+
for r in range(self._rows):
|
231
|
+
for c in range(self._cols):
|
232
|
+
self._screen[r][c] = Cell()
|
233
|
+
|
234
|
+
def _clear_down(self) -> None:
|
235
|
+
"""Clear from cursor to the end of the screen."""
|
236
|
+
|
237
|
+
# Clear current line from cursor forward
|
238
|
+
self._clear_right(self._cursor_row, self._cursor_col)
|
239
|
+
|
240
|
+
# Clear all lines below cursor
|
241
|
+
for r in range(self._cursor_row + 1, self._rows):
|
242
|
+
self._clear_line(r)
|
243
|
+
|
244
|
+
def _clear_up(self) -> None:
|
245
|
+
"""Clear from the start of the screen up to the cursor."""
|
246
|
+
|
247
|
+
# Clear current line from start to cursor
|
248
|
+
self._clear_left(self._cursor_row, self._cursor_col)
|
249
|
+
|
250
|
+
# Clear all lines above cursor
|
251
|
+
for r in range(self._cursor_row):
|
252
|
+
self._clear_line(r)
|
253
|
+
|
254
|
+
def _clear_line(self, row: int) -> None:
|
255
|
+
for c in range(self._cols):
|
256
|
+
self._screen[row][c] = Cell()
|
257
|
+
|
258
|
+
def _clear_right(self, row: int, col: int) -> None:
|
259
|
+
for c in range(col, self._cols):
|
260
|
+
self._screen[row][c] = Cell()
|
261
|
+
|
262
|
+
def _clear_left(self, row: int, col: int) -> None:
|
263
|
+
for c in range(col + 1):
|
264
|
+
self._screen[row][c] = Cell()
|
265
|
+
|
266
|
+
# Debug/Utility Methods
|
267
|
+
|
268
|
+
def get_screen_as_strings(self) -> list[str]:
|
269
|
+
"""Return a list of strings representing each row (ignoring attributes). Useful for debugging/testing."""
|
270
|
+
|
271
|
+
return [''.join(cell.char for cell in row) for row in self._screen]
|
@@ -1,5 +1,5 @@
|
|
1
1
|
omlish/.manifests.json,sha256=lRkBDFxlAbf6lN5upo3WSf-owW8YG1T21dfpbQL-XHM,7598
|
2
|
-
omlish/__about__.py,sha256=
|
2
|
+
omlish/__about__.py,sha256=Gs9hXW7PiNiRdGtxDHSxJf4Q0yutVUp9BalCHX8po30,3409
|
3
3
|
omlish/__init__.py,sha256=SsyiITTuK0v74XpKV8dqNaCmjOlan1JZKrHQv5rWKPA,253
|
4
4
|
omlish/c3.py,sha256=ubu7lHwss5V4UznbejAI0qXhXahrU01MysuHOZI9C4U,8116
|
5
5
|
omlish/cached.py,sha256=UI-XTFBwA6YXWJJJeBn-WkwBkfzDjLBBaZf4nIJA9y0,510
|
@@ -11,9 +11,8 @@ omlish/libc.py,sha256=8r7Ejyhttk9ruCfBkxNTrlzir5WPbDE2vmY7VPlceMA,15362
|
|
11
11
|
omlish/multiprocessing.py,sha256=QZT4C7I-uThCAjaEY3xgUYb-5GagUlnE4etN01LDyU4,5186
|
12
12
|
omlish/runmodule.py,sha256=PWvuAaJ9wQQn6bx9ftEL3_d04DyotNn8dR_twm2pgw0,700
|
13
13
|
omlish/shlex.py,sha256=bsW2XUD8GiMTUTDefJejZ5AyqT1pTgWMPD0BMoF02jE,248
|
14
|
-
omlish/subprocesses.py,sha256=
|
14
|
+
omlish/subprocesses.py,sha256=KOvt5gvpq2uisjYKyU_XUPZyM6yq8ywgbfWjz-lx9CQ,8686
|
15
15
|
omlish/sync.py,sha256=QJ79kxmIqDP9SeHDoZAf--DpFIhDQe1jACy8H4N0yZI,2928
|
16
|
-
omlish/term.py,sha256=EVHm3lEEIc9hT4f8BPmzbNUwlqZ8nrRpCwyQMN7LBm0,9313
|
17
16
|
omlish/antlr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
17
|
omlish/antlr/delimit.py,sha256=3Byvh9_Ip8ftM_SeSEmMbnNo1jrxk-xm8HnHDp_nDaI,3466
|
19
18
|
omlish/antlr/dot.py,sha256=uH2X7-8xNLYDQNJ30uW8ssv1MLkZSm07GsalcRuunYI,817
|
@@ -311,7 +310,7 @@ omlish/inject/impl/proxy.py,sha256=1ko0VaKqzu9UG8bIldp9xtUrAVUOFTKWKTjOCqIGr4s,1
|
|
311
310
|
omlish/inject/impl/scopes.py,sha256=hKnzNieB-fJSFEXDP_QG1mCfIKoVFIfFlf9LiIt5tk4,5920
|
312
311
|
omlish/io/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
313
312
|
omlish/io/abc.py,sha256=Cxs8KB1B_69rxpUYxI-MTsilAmNooJJn3w07DKqYKkE,1255
|
314
|
-
omlish/io/buffers.py,sha256=
|
313
|
+
omlish/io/buffers.py,sha256=qo1hCqTfKvlbSmddneporqCtW0rZJ_Mv2GrQTI1Hbk0,5636
|
315
314
|
omlish/io/pyio.py,sha256=q4RBFVpBE5PYjnGPGT-_4pcZb7dFJmLJ4LtI8OoDRQY,95433
|
316
315
|
omlish/io/trampoline.py,sha256=oUKTQg1F5xQS1431Kt7MbK-NZpX509ubcXU-s86xJr8,7171
|
317
316
|
omlish/io/compress/__init__.py,sha256=qV-aDfPWykTMYcoQmE8THZ4KFDRzqwN3QPPNEJVarXY,86
|
@@ -336,7 +335,7 @@ omlish/io/generators/__init__.py,sha256=YsSLJY9uw72eX3iXd_A0pM69g7EvEqMFdCdR_BBD
|
|
336
335
|
omlish/io/generators/consts.py,sha256=4r6IMLBMic6MJHVn9UiORIkkPAuxsqtzFT3KV0fatC0,33
|
337
336
|
omlish/io/generators/direct.py,sha256=A9VJB1rNKU3l-NatpYIwyCLI3R_ybGglmdx6sAtoTo4,324
|
338
337
|
omlish/io/generators/readers.py,sha256=MolTFCzcnD5XoP0su0YUNHJ0xlHC3KTihvWAi75y8Bo,4336
|
339
|
-
omlish/io/generators/stepped.py,sha256=
|
338
|
+
omlish/io/generators/stepped.py,sha256=WZnLpCzv5pA6jLdb1lplXoKRPbREw9wO586Dew5EzV4,5129
|
340
339
|
omlish/iterators/__init__.py,sha256=yMavf5FofiS1EU4UFuWPXiFZ03W0H-y7MuMxW8FUaEE,358
|
341
340
|
omlish/iterators/iterators.py,sha256=ghI4dO6WPyyFOLTIIMaHQ_IOy2xXaFpGPqveZ5YGIBU,3158
|
342
341
|
omlish/iterators/recipes.py,sha256=53mkexitMhkwXQZbL6DrhpT0WePQ_56uXd5Jaw3DfzI,467
|
@@ -395,7 +394,7 @@ omlish/lite/typing.py,sha256=U3-JaEnkDSYxK4tsu_MzUn3RP6qALBe5FXQXpD-licE,1090
|
|
395
394
|
omlish/logs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
396
395
|
omlish/logs/abc.py,sha256=ho4ABKYMKX-V7g4sp1BByuOLzslYzLlQ0MESmjEpT-o,8005
|
397
396
|
omlish/logs/all.py,sha256=4Z6cNB0E1xbX0IOQWZEWLA0Jw-yyjhXa3PD_Nvfewu8,556
|
398
|
-
omlish/logs/color.py,sha256=
|
397
|
+
omlish/logs/color.py,sha256=CM-ceoPXs0j5_klnZDJkFCFRgHOToFzJyLjC6LsnPEk,665
|
399
398
|
omlish/logs/configs.py,sha256=XOc8rWxfPpPMxJESVD2mLCUoLtbQnGnZwvYhhqe7DD8,772
|
400
399
|
omlish/logs/filters.py,sha256=2noFRyBez3y519fpfsDSt1vo8wX-85b8sMXZi5o_xyE,208
|
401
400
|
omlish/logs/handlers.py,sha256=zgSnKQA5q9Fu7T0Nkd7twog9H1Wg9-bDCzz4_F1TOBo,319
|
@@ -545,6 +544,13 @@ omlish/sql/tabledefs/elements.py,sha256=lP_Ch19hKmiGYPQVeC8HpFaKdTYnXi2FfpfwKMxZ
|
|
545
544
|
omlish/sql/tabledefs/lower.py,sha256=YQf8gl1kxD5Fm-vOxV6G0Feh_D9PP1pYwz_vz6XjTPQ,1405
|
546
545
|
omlish/sql/tabledefs/marshal.py,sha256=j-Rz1HsiXmABv39-2VoJdzSSB3kbxqaVevbdkZWMyG8,504
|
547
546
|
omlish/sql/tabledefs/tabledefs.py,sha256=lIhvlt0pk6G7RZAtDFsFXm5j0l9BvRfnP7vNGeydHtE,816
|
547
|
+
omlish/term/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
548
|
+
omlish/term/codes.py,sha256=gqouA7KDyGzyCmmu8ejGhT_8XJGsejdTQh2pSCMbWAQ,6150
|
549
|
+
omlish/term/progressbar.py,sha256=TiwdmPSMa5jQj35i1NQURTWQGy4eWUNx_XiPM38JtvQ,3184
|
550
|
+
omlish/term/vt100/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
551
|
+
omlish/term/vt100/c.py,sha256=cAhDKXI81PZRtFmTotfad3HZGREP1QnOlWYoAw6v-Fw,3532
|
552
|
+
omlish/term/vt100/states.py,sha256=OxPUxfFTcfz56MhtDgIigEApChOtN6XO1g6R2H08mu4,8303
|
553
|
+
omlish/term/vt100/terminal.py,sha256=sYLddUSrubCJPnI6f2t5Pzp0tFGLLBKyYdDfKmkwREM,9363
|
548
554
|
omlish/testing/__init__.py,sha256=M_BQrcCHkoL-ZvE-UpQ8XxXNYRRawhjUz4rCJnAqM2A,152
|
549
555
|
omlish/testing/testing.py,sha256=TT2wwSzPZ_KhIvKxpM1qc1yHKD-LHDNgGrcr_h8vs7c,2895
|
550
556
|
omlish/testing/pytest/__init__.py,sha256=B2nyJrjIoNcEopbg0IZ5UUDs4OHmQ8qqElFJfGcDdas,257
|
@@ -573,9 +579,9 @@ omlish/text/indent.py,sha256=6Jj6TFY9unaPa4xPzrnZemJ-fHsV53IamP93XGjSUHs,1274
|
|
573
579
|
omlish/text/minja.py,sha256=KAmZ2POcLcxwF4DPKxdWa16uWxXmVz1UnJXLSwt4oZo,5761
|
574
580
|
omlish/text/parts.py,sha256=7vPF1aTZdvLVYJ4EwBZVzRSy8XB3YqPd7JwEnNGGAOo,6495
|
575
581
|
omlish/text/random.py,sha256=jNWpqiaKjKyTdMXC-pWAsSC10AAP-cmRRPVhm59ZWLk,194
|
576
|
-
omlish-0.0.0.
|
577
|
-
omlish-0.0.0.
|
578
|
-
omlish-0.0.0.
|
579
|
-
omlish-0.0.0.
|
580
|
-
omlish-0.0.0.
|
581
|
-
omlish-0.0.0.
|
582
|
+
omlish-0.0.0.dev189.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
|
583
|
+
omlish-0.0.0.dev189.dist-info/METADATA,sha256=JRj7SgIl1vGF733weElFasb6_IEBNT8cTHUdlW4Aq98,4264
|
584
|
+
omlish-0.0.0.dev189.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
585
|
+
omlish-0.0.0.dev189.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
|
586
|
+
omlish-0.0.0.dev189.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
|
587
|
+
omlish-0.0.0.dev189.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|