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 CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev187'
2
- __revision__ = 'adae44733a5f69bcd3faa57f0ef2be7b47c9bb4d'
1
+ __version__ = '0.0.0.dev189'
2
+ __revision__ = 'a07761c6eb75b56d0c159dd576f686dec5845963'
3
3
 
4
4
 
5
5
  #
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
@@ -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
- buf: ta.Any = None
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 buf:
161
- buf = check.isinstance((yield None), bytes)
162
- if not buf:
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 = buf
167
- buf = None
169
+ i = check.not_none(rlb.read())
168
170
 
169
171
  elif isinstance(o, int):
170
- while len(buf) < o:
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
- buf += more
177
+ rlb.feed(more)
176
178
 
177
- i = buf[:o]
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 term
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, term.SGRs.FG] = {
11
- logging.WARNING: term.SGRs.FG.BRIGHT_YELLOW,
12
- logging.ERROR: term.SGRs.FG.BRIGHT_RED,
13
- logging.CRITICAL: term.SGRs.FG.BRIGHT_RED,
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 = term.SGR(c) + buf + term.SGR(term.SGRs.RESET)
23
+ buf = tc.SGR(c) + buf + tc.SGR(tc.SGRs.RESET)
24
24
  return buf
omlish/subprocesses.py CHANGED
@@ -1,4 +1,5 @@
1
1
  # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
2
3
  import abc
3
4
  import contextlib
4
5
  import logging
File without changes
@@ -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 . import lang
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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: omlish
3
- Version: 0.0.0.dev187
3
+ Version: 0.0.0.dev189
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -1,5 +1,5 @@
1
1
  omlish/.manifests.json,sha256=lRkBDFxlAbf6lN5upo3WSf-owW8YG1T21dfpbQL-XHM,7598
2
- omlish/__about__.py,sha256=uczpyoSCBa1D3dhUVJ18pnATINJxHB2nOI42fI9vOVg,3409
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=n6pk0nUaTFHzD_A6duyKNJ4ggncU7uNepfh_T90etHE,8671
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=6wsFKHwL41IW-F0Zu-H7_ovQqvW7q0CwtQFu2wdhlEE,5286
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=sl-3-hNVYi7qGYZjwBPHs0hKxmz7XkfDMosCXbhIYlE,5025
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=02feYPZm4A7qHeBABpiar2J2E6tf-vtw1pOQAsJs_1c,668
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.dev187.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
577
- omlish-0.0.0.dev187.dist-info/METADATA,sha256=ji6ytWK7_WKR3YDtI_fBXFAcEjfQ3huw6GPdX8h4wHs,4264
578
- omlish-0.0.0.dev187.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
579
- omlish-0.0.0.dev187.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
580
- omlish-0.0.0.dev187.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
581
- omlish-0.0.0.dev187.dist-info/RECORD,,
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,,