omdev 0.0.0.dev463__py3-none-any.whl → 0.0.0.dev465__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.

Potentially problematic release.


This version of omdev might be problematic. Click here for more details.

@@ -0,0 +1,612 @@
1
+ import dataclasses as dc
2
+ import operator
3
+ import typing as ta
4
+
5
+ from rich.text import Text
6
+ from textual import events
7
+ from textual import on
8
+ from textual.app import ComposeResult
9
+ from textual.binding import Binding
10
+ from textual.binding import BindingType
11
+ from textual.content import Content
12
+ from textual.css.query import NoMatches
13
+ from textual.geometry import Offset
14
+ from textual.geometry import Region
15
+ from textual.geometry import Spacing
16
+ from textual.style import Style
17
+ from textual.widget import Widget
18
+ from textual.widgets import Input
19
+ from textual.widgets import OptionList
20
+ from textual.widgets.option_list import Option
21
+
22
+ from omlish import check
23
+
24
+ from .matching import FuzzySearch
25
+
26
+
27
+ ##
28
+
29
+
30
+ class AutoCompleteItem(Option):
31
+ def __init__(
32
+ self,
33
+ main: str | Content,
34
+ prefix: str | Content | None = None,
35
+ id: str | None = None, # noqa
36
+ disabled: bool = False,
37
+ ) -> None:
38
+ """
39
+ A single option appearing in the autocompletion dropdown. Each option has up to 3 columns. Note that this is not
40
+ a widget, it's simply a data structure for describing dropdown items.
41
+
42
+ Args:
43
+ prefix: The prefix will often contain an icon/symbol, the main (middle) column contains the text that
44
+ represents this option.
45
+ main: The main text representing this option - this will be highlighted by default. In an IDE, the `main`
46
+ (middle) column might contain the name of a function or method.
47
+ """
48
+
49
+ self.main = Content(main) if isinstance(main, str) else main
50
+ self.prefix = Content(prefix) if isinstance(prefix, str) else prefix
51
+ left = self.prefix
52
+ prompt = self.main
53
+ if left:
54
+ prompt = Content.assemble(left, self.main)
55
+
56
+ super().__init__(prompt, id, disabled)
57
+
58
+ @property
59
+ def value(self) -> str:
60
+ return self.main.plain
61
+
62
+
63
+ class AutoCompleteItemHit(AutoCompleteItem):
64
+ """
65
+ A dropdown item which matches the current search string - in other words AutoComplete.match has returned a score
66
+ greater than 0 for this item.
67
+ """
68
+
69
+
70
+ class AutoCompleteList(OptionList):
71
+ pass
72
+
73
+
74
+ class AutoComplete(Widget):
75
+ BINDINGS: ta.ClassVar[list[BindingType]] = [
76
+ Binding('escape', 'hide', 'Hide dropdown', show=False),
77
+ ]
78
+
79
+ DEFAULT_CSS = """
80
+ AutoComplete {
81
+ height: auto;
82
+ width: auto;
83
+ max-height: 12;
84
+ display: none;
85
+ background: $surface;
86
+ overlay: screen;
87
+
88
+ & AutoCompleteList {
89
+ width: auto;
90
+ height: auto;
91
+ border: none;
92
+ padding: 0;
93
+ margin: 0;
94
+ scrollbar-size-vertical: 1;
95
+ text-wrap: nowrap;
96
+ color: $foreground;
97
+ background: transparent;
98
+ }
99
+
100
+ & .autocomplete--highlight-match {
101
+ text-style: bold;
102
+ }
103
+
104
+ }
105
+ """
106
+
107
+ COMPONENT_CLASSES: ta.ClassVar[set[str]] = {
108
+ 'autocomplete--highlight-match',
109
+ }
110
+
111
+ class Matcher(ta.Protocol):
112
+ def __call__(self, query: str, candidate: str) -> tuple[float, ta.Sequence[int]]:
113
+ """
114
+ Match a query (search string) against a candidate (dropdown item value).
115
+
116
+ Returns a tuple of (score, offsets) where score is a float between 0 and 1, used for sorting the matches,
117
+ and offsets is a tuple of integers representing the indices of the characters in the candidate string that
118
+ match the query.
119
+ """
120
+
121
+ ...
122
+
123
+ @dc.dataclass()
124
+ class TargetState:
125
+ text: str
126
+ """The content in the target widget."""
127
+
128
+ cursor_position: int
129
+ """The cursor position in the target widget."""
130
+
131
+ def __init__(
132
+ self,
133
+ target: Input | str,
134
+ candidates: ta.Sequence[AutoCompleteItem | str] | ta.Callable[[TargetState], list[AutoCompleteItem]] | None = None, # noqa
135
+ *,
136
+ prevent_default_enter: bool = True,
137
+ prevent_default_tab: bool = True,
138
+ name: str | None = None,
139
+ id: str | None = None, # noqa
140
+ classes: str | None = None,
141
+ disabled: bool = False,
142
+ matcher: Matcher | None = None,
143
+ ) -> None:
144
+ """
145
+ An autocomplete widget.
146
+
147
+ Args:
148
+ target: An Input instance or a selector string used to query an Input instance. If a selector is used,
149
+ remember that widgets are not available until the widget has been mounted (don't use the selector in
150
+ `compose` - use it in `on_mount` instead).
151
+ candidates: The candidates to match on, or a function which returns the candidates to match on. If set to
152
+ None, the candidates will be fetched by directly calling the `get_candidates` method, which is what
153
+ you'll probably want to do if you're subclassing AutoComplete and supplying your own custom
154
+ `get_candidates` method.
155
+ prevent_default_enter: Prevent the default enter behavior. If True, when you select a dropdown option using
156
+ the enter key, the default behavior (e.g. submitting an Input) will be prevented.
157
+ prevent_default_tab: Prevent the default tab behavior. If True, when you select a dropdown option using the
158
+ tab key, the default behavior (e.g. moving focus to the next widget) will be prevented.
159
+ """
160
+
161
+ super().__init__(name=name, id=id, classes=classes, disabled=disabled)
162
+
163
+ self._target = target
164
+
165
+ # Users can supply strings as a convenience for the simplest cases, so let's convert them to DropdownItems.
166
+ if isinstance(candidates, ta.Sequence):
167
+ self.candidates = [
168
+ candidate
169
+ if isinstance(candidate, AutoCompleteItem)
170
+ else AutoCompleteItem(main=candidate)
171
+ for candidate in candidates
172
+ ]
173
+ else:
174
+ self.candidates = candidates
175
+
176
+ self.prevent_default_enter = prevent_default_enter
177
+ self.prevent_default_tab = prevent_default_tab
178
+ self._target_state = AutoComplete.TargetState('', 0)
179
+ self._previous_terminal_cursor_position = (0, 0)
180
+ if matcher is None:
181
+ matcher = FuzzySearch().match
182
+ self._matcher = matcher
183
+
184
+ candidates: list[AutoCompleteItem] | ta.Callable[[TargetState], list[AutoCompleteItem]] | None
185
+ """The candidates to match on, or a function which returns the candidates to match on."""
186
+
187
+ prevent_default_enter: bool
188
+ """
189
+ Prevent the default enter behavior. If True, when you select a dropdown option using the enter key, the default
190
+ behavior (e.g. submitting an Input) will be prevented.
191
+ """
192
+
193
+ prevent_default_tab: bool
194
+ """
195
+ Prevent the default tab behavior. If True, when you select a dropdown option using the tab key, the default
196
+ behavior (e.g. moving focus to the next widget) will be prevented.
197
+ """
198
+
199
+ _target_state: TargetState
200
+ """Cached state of the target Input."""
201
+
202
+ _matcher: Matcher
203
+ """The default implementation used by AutoComplete.match."""
204
+
205
+ _previous_terminal_cursor_position: tuple[int, int]
206
+ """Tracks the last known cursor position in the terminal."""
207
+
208
+ def compose(self) -> ComposeResult:
209
+ option_list = AutoCompleteList()
210
+ option_list.can_focus = False
211
+ yield option_list
212
+
213
+ def on_mount(self) -> None:
214
+ # Subscribe to the target widget's reactive attributes.
215
+ self.target.message_signal.subscribe(self, self._listen_to_messages) # type: ignore
216
+ self._subscribe_to_target()
217
+ self._handle_target_update()
218
+
219
+ def _realign(_=None) -> None:
220
+ # Only realign if the cursor position has changed (the cursor position is in screen-space, so if it remains
221
+ # the same, the autocomplete does not need to be realigned).
222
+ if (
223
+ self.is_attached and
224
+ self._previous_terminal_cursor_position != self.app.cursor_position
225
+ ):
226
+ self._align_to_target()
227
+ self._previous_terminal_cursor_position = self.app.cursor_position
228
+
229
+ self.screen.screen_layout_refresh_signal.subscribe(self, _realign)
230
+
231
+ def _listen_to_messages(self, event: events.Event) -> None:
232
+ """Listen to some events of the target widget."""
233
+
234
+ try:
235
+ option_list = self.option_list
236
+ except NoMatches:
237
+ # This can happen if the event is an Unmount event during application shutdown.
238
+ return
239
+
240
+ if isinstance(event, events.Key) and option_list.option_count:
241
+ displayed = self.display
242
+ highlighted = option_list.highlighted or 0
243
+ if event.key == 'down':
244
+ # Check if there's only one item and it matches the search string
245
+ if option_list.option_count == 1:
246
+ search_string = self.get_search_string(self._get_target_state())
247
+ first_option = option_list.get_option_at_index(0).prompt
248
+ text_from_option = (
249
+ first_option.plain
250
+ if isinstance(first_option, Text)
251
+ else first_option
252
+ )
253
+ if text_from_option == search_string:
254
+ # Don't prevent default behavior in this case
255
+ return
256
+
257
+ # If you press `down` while in an Input and the autocomplete is currently hidden, then we should show
258
+ # the dropdown.
259
+ event.prevent_default()
260
+ event.stop()
261
+ if displayed:
262
+ highlighted = (highlighted + 1) % option_list.option_count
263
+ else:
264
+ self.display = True
265
+ highlighted = 0
266
+
267
+ option_list.highlighted = highlighted
268
+
269
+ elif event.key == 'up':
270
+ if displayed:
271
+ event.prevent_default()
272
+ event.stop()
273
+ highlighted = (highlighted - 1) % option_list.option_count
274
+ option_list.highlighted = highlighted
275
+
276
+ elif event.key == 'enter':
277
+ if self.prevent_default_enter and displayed:
278
+ event.prevent_default()
279
+ event.stop()
280
+ self._complete(option_index=highlighted)
281
+
282
+ elif event.key == 'tab':
283
+ if self.prevent_default_tab and displayed:
284
+ event.prevent_default()
285
+ event.stop()
286
+ self._complete(option_index=highlighted)
287
+
288
+ elif event.key == 'escape':
289
+ if displayed:
290
+ event.prevent_default()
291
+ event.stop()
292
+ self.action_hide()
293
+
294
+ if isinstance(event, Input.Changed):
295
+ # We suppress Changed events from the target widget, so that we don't handle change events as a result of
296
+ # performing a completion.
297
+ self._handle_target_update()
298
+
299
+ def action_hide(self) -> None:
300
+ self.styles.display = 'none'
301
+
302
+ def action_show(self) -> None:
303
+ self.styles.display = 'block'
304
+
305
+ def _complete(self, option_index: int) -> None:
306
+ """
307
+ Do the completion (i.e. insert the selected item into the target input).
308
+
309
+ This is when the user highlights an option in the dropdown and presses tab or enter.
310
+ """
311
+
312
+ if not self.display or self.option_list.option_count == 0:
313
+ return
314
+
315
+ option_list = self.option_list
316
+ highlighted = option_index
317
+ option = ta.cast(AutoCompleteItem, option_list.get_option_at_index(highlighted))
318
+ highlighted_value = option.value
319
+ with self.prevent(Input.Changed):
320
+ self.apply_completion(highlighted_value, self._get_target_state())
321
+ self.post_completion()
322
+
323
+ def post_completion(self) -> None:
324
+ """This method is called after a completion is applied. By default, it simply hides the dropdown."""
325
+
326
+ self.action_hide()
327
+
328
+ def apply_completion(self, value: str, state: TargetState) -> None:
329
+ """
330
+ Apply the completion to the target widget.
331
+
332
+ This method updates the state of the target widget to the reflect the value the user has chosen from the
333
+ dropdown list.
334
+ """
335
+
336
+ target = self.target
337
+ target.value = ''
338
+ target.insert_text_at_cursor(value)
339
+
340
+ # We need to rebuild here because we've prevented the Changed events from being sent to the target widget,
341
+ # meaning AutoComplete won't spot intercept that message, and would not trigger a rebuild like it normally does
342
+ # when a Changed event is received.
343
+ new_target_state = self._get_target_state()
344
+ self._rebuild_options(new_target_state, self.get_search_string(new_target_state))
345
+
346
+ @property
347
+ def target(self) -> Input:
348
+ """The resolved target widget."""
349
+
350
+ if isinstance(self._target, Input):
351
+ return self._target
352
+
353
+ else:
354
+ target = self.screen.query_one(self._target)
355
+ return check.isinstance(target, Input)
356
+
357
+ def _subscribe_to_target(self) -> None:
358
+ """Attempt to subscribe to the target widget, if it's available."""
359
+
360
+ target = self.target
361
+ self.watch(target, 'has_focus', self._handle_focus_change)
362
+ self.watch(target, 'selection', self._align_and_rebuild)
363
+
364
+ def _align_and_rebuild(self) -> None:
365
+ self._align_to_target()
366
+ self._target_state = self._get_target_state()
367
+ search_string = self.get_search_string(self._target_state)
368
+ self._rebuild_options(self._target_state, search_string)
369
+
370
+ def _align_to_target(self) -> None:
371
+ """
372
+ Align the dropdown to the position of the cursor within the target widget, and constrain it to be within the
373
+ screen.
374
+ """
375
+
376
+ x, y = self.target.cursor_screen_offset
377
+ dropdown = self.option_list
378
+ width, height = dropdown.outer_size
379
+
380
+ # Constrain the dropdown within the screen.
381
+ x, y, _width, _height = Region(x - 1, y + 1, width, height).constrain(
382
+ 'inside',
383
+ 'none',
384
+ Spacing.all(0),
385
+ self.screen.scrollable_content_region,
386
+ )
387
+ self.absolute_offset = Offset(x, y)
388
+
389
+ def _get_target_state(self) -> TargetState:
390
+ """Get the state of the target widget."""
391
+
392
+ target = self.target
393
+ return AutoComplete.TargetState(
394
+ text=target.value,
395
+ cursor_position=target.cursor_position,
396
+ )
397
+
398
+ def _handle_focus_change(self, has_focus: bool) -> None:
399
+ """Called when the focus of the target widget changes."""
400
+
401
+ if not has_focus:
402
+ self.action_hide()
403
+ else:
404
+ target_state = self._get_target_state()
405
+ search_string = self.get_search_string(target_state)
406
+ self._rebuild_options(target_state, search_string)
407
+
408
+ def _handle_target_update(self) -> None:
409
+ """
410
+ Called when the state (text or cursor position) of the target is updated.
411
+
412
+ Here we align the dropdown to the target, determine if it should be visible, and rebuild the options in it.
413
+ """
414
+
415
+ self._target_state = self._get_target_state()
416
+ search_string = self.get_search_string(self._target_state)
417
+
418
+ # Determine visibility after the user makes a change in the target widget (e.g. typing in a character in the
419
+ # Input).
420
+ self._rebuild_options(self._target_state, search_string)
421
+ self._align_to_target()
422
+
423
+ if self.should_show_dropdown(search_string):
424
+ self.action_show()
425
+ else:
426
+ self.action_hide()
427
+
428
+ def should_show_dropdown(self, search_string: str) -> bool:
429
+ """
430
+ Determine whether to show or hide the dropdown based on the current state.
431
+
432
+ This method can be overridden to customize the visibility behavior.
433
+
434
+ Args:
435
+ search_string: The current search string.
436
+
437
+ Returns:
438
+ bool: True if the dropdown should be shown, False otherwise.
439
+ """
440
+
441
+ option_list = self.option_list
442
+ option_count = option_list.option_count
443
+
444
+ if len(search_string) == 0 or option_count == 0:
445
+ return False
446
+ elif option_count == 1:
447
+ first_option = option_list.get_option_at_index(0).prompt
448
+ text_from_option = (
449
+ first_option.plain if isinstance(first_option, Text) else first_option
450
+ )
451
+ return text_from_option != search_string
452
+ else:
453
+ return True
454
+
455
+ def _rebuild_options(self, target_state: TargetState, search_string: str) -> None:
456
+ """
457
+ Rebuild the options in the dropdown.
458
+
459
+ Args:
460
+ target_state: The state of the target widget.
461
+ """
462
+
463
+ option_list = self.option_list
464
+ option_list.clear_options()
465
+ if self.target.has_focus:
466
+ matches = self._compute_matches(target_state, search_string)
467
+ if matches:
468
+ option_list.add_options(matches)
469
+ option_list.highlighted = 0
470
+
471
+ def get_search_string(self, target_state: TargetState) -> str:
472
+ """
473
+ This value will be passed to the match function.
474
+
475
+ This could be, for example, the text in the target widget, or a substring of that text.
476
+
477
+ Returns:
478
+ The search string that will be used to filter the dropdown options.
479
+ """
480
+
481
+ return target_state.text[: target_state.cursor_position]
482
+
483
+ def _compute_matches(self, target_state: TargetState, search_string: str) -> list[AutoCompleteItem]:
484
+ """
485
+ Compute the matches based on the target state.
486
+
487
+ Args:
488
+ target_state: The state of the target widget.
489
+
490
+ Returns:
491
+ The matches to display in the dropdown.
492
+ """
493
+
494
+ # If items is a callable, then it's a factory function that returns the candidates. Otherwise, it's a list of
495
+ # candidates.
496
+ candidates = self.get_candidates(target_state)
497
+ matches = self.get_matches(target_state, candidates, search_string)
498
+ return matches
499
+
500
+ def get_candidates(self, target_state: TargetState) -> list[AutoCompleteItem]:
501
+ """Get the candidates to match against."""
502
+
503
+ candidates = self.candidates
504
+ if isinstance(candidates, ta.Sequence):
505
+ return list(candidates)
506
+ elif candidates is None:
507
+ raise NotImplementedError(
508
+ 'You must implement get_candidates in your AutoComplete subclass, because candidates is None',
509
+ )
510
+ else:
511
+ # candidates is a callable
512
+ return candidates(target_state)
513
+
514
+ def get_matches(
515
+ self,
516
+ target_state: TargetState,
517
+ candidates: list[AutoCompleteItem],
518
+ search_string: str,
519
+ ) -> list[AutoCompleteItem]:
520
+ """
521
+ Given the state of the target widget, return the DropdownItems which match the query string and should be appear
522
+ in the dropdown.
523
+
524
+ Args:
525
+ target_state: The state of the target widget.
526
+ candidates: The candidates to match against.
527
+ search_string: The search string to match against.
528
+
529
+ Returns:
530
+ The matches to display in the dropdown.
531
+ """
532
+
533
+ if not search_string:
534
+ return candidates
535
+
536
+ matches_and_scores: list[tuple[AutoCompleteItem, float]] = []
537
+ append_score = matches_and_scores.append
538
+ match = self.match
539
+
540
+ for candidate in candidates:
541
+ candidate_string = candidate.value
542
+ score, offsets = match(search_string, candidate_string)
543
+ if score > 0:
544
+ highlighted = self.apply_highlights(candidate.main, offsets)
545
+ highlighted_item = AutoCompleteItemHit(
546
+ main=highlighted,
547
+ prefix=candidate.prefix,
548
+ id=candidate.id,
549
+ disabled=candidate.disabled,
550
+ )
551
+ append_score((highlighted_item, score))
552
+
553
+ matches_and_scores.sort(key=operator.itemgetter(1), reverse=True)
554
+ matches = [match for match, _ in matches_and_scores]
555
+ return matches
556
+
557
+ def match(self, query: str, candidate: str) -> tuple[float, ta.Sequence[int]]:
558
+ """
559
+ Match a query (search string) against a candidate (dropdown item value).
560
+
561
+ Returns a tuple of (score, offsets) where score is a float between 0 and 1, used for sorting the matches, and
562
+ offsets is a tuple of integers representing the indices of the characters in the candidate string that match the
563
+ query.
564
+
565
+ So, if the query is "hello" and the candidate is "hello world", and the offsets will be (0,1,2,3,4). The score
566
+ can be anything you want - and the highest score will be at the top of the list by default.
567
+
568
+ The offsets will be highlighted in the dropdown list.
569
+
570
+ A score of 0 means no match, and such candidates will not be shown in the dropdown.
571
+
572
+ Args:
573
+ query: The search string.
574
+ candidate: The candidate string (dropdown item value).
575
+
576
+ Returns:
577
+ A tuple of (score, offsets).
578
+ """
579
+
580
+ return self._matcher(query, candidate)
581
+
582
+ def apply_highlights(self, candidate: Content, offsets: ta.Sequence[int]) -> Content:
583
+ """
584
+ Highlight the candidate with the fuzzy match offsets.
585
+
586
+ Args:
587
+ candidate: The candidate which matched the query. Note that this may already have its own styling applied.
588
+ offsets: The offsets to highlight.
589
+ Returns:
590
+ A [rich.text.Text][`Text`] object with highlighted matches.
591
+ """
592
+
593
+ # TODO - let's have styles which account for the cursor too
594
+ match_style = Style.from_rich_style(
595
+ self.get_component_rich_style('autocomplete--highlight-match', partial=True),
596
+ )
597
+
598
+ plain = candidate.plain
599
+ for offset in offsets:
600
+ if not plain[offset].isspace():
601
+ candidate = candidate.stylize(match_style, offset, offset + 1)
602
+
603
+ return candidate
604
+
605
+ @property
606
+ def option_list(self) -> AutoCompleteList:
607
+ return self.query_one(AutoCompleteList)
608
+
609
+ @on(OptionList.OptionSelected, 'AutoCompleteList')
610
+ def _apply_completion(self, event: OptionList.OptionSelected) -> None:
611
+ # Handles click events on dropdown items.
612
+ self._complete(event.option_index)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omdev
3
- Version: 0.0.0.dev463
3
+ Version: 0.0.0.dev465
4
4
  Summary: omdev
5
5
  Author: wrmsr
6
6
  License-Expression: BSD-3-Clause
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.13
14
14
  Requires-Python: >=3.13
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
- Requires-Dist: omlish==0.0.0.dev463
17
+ Requires-Dist: omlish==0.0.0.dev465
18
18
  Provides-Extra: all
19
19
  Requires-Dist: black~=25.9; extra == "all"
20
20
  Requires-Dist: pycparser~=2.23; extra == "all"
@@ -1,5 +1,5 @@
1
1
  omdev/.omlish-manifests.json,sha256=cMWrMB14_AFp9mQiTEeoTYL1uaC3zq7ER7-XHGCXY8I,11671
2
- omdev/__about__.py,sha256=TwPTq6a1_K0K-oDBQf6x2VmkiD5hi6W8Q7CvmTW9Aic,1223
2
+ omdev/__about__.py,sha256=9oG5_aX1pKiZ-pxcYppjvatDSNyKOwEcT5zG7-H_bSY,1236
3
3
  omdev/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  omdev/cmake.py,sha256=gu49t10_syXh_TUJs4POsxeFs8we8Y3XTOOPgIXmGvg,4608
5
5
  omdev/imgur.py,sha256=oqei705LhSnLWQTOMHMHwRecRXcpSEP90Sg4SVINPQ0,3133
@@ -272,20 +272,20 @@ omdev/py/tools/importscan.py,sha256=OYPZBg__u6On416tcmYfRTMkjr1ZlFcOAy9AafJ8iuo,
272
272
  omdev/py/tools/mkrelimp.py,sha256=40gE1p_w-f_oWptUiS8TqODhso6ow46UCEptw4VbH0c,4040
273
273
  omdev/pyproject/__init__.py,sha256=Y3l4WY4JRi2uLG6kgbGp93fuGfkxkKwZDvhsa0Rwgtk,15
274
274
  omdev/pyproject/__main__.py,sha256=gn3Rl1aYPYdiTtEqa9ifi0t-e4ZwPY0vhJ4UXvYdJDY,165
275
- omdev/pyproject/cexts.py,sha256=GLD4fe61M_fHhdMcKlcQNUoCb7MeVXY6Fw-grKH4hTU,4264
276
275
  omdev/pyproject/cli.py,sha256=Umsu2bcJUYeeVXICaZFhKckUBT6VWuYDL4htgCGGQIs,8749
277
276
  omdev/pyproject/configs.py,sha256=baNRwHtUW8S8DKCxuKlMbV3Gduujd1PyNURxQ48Nnxk,2813
278
277
  omdev/pyproject/inject.py,sha256=Von8_8ofkITLoCEwDHNRAwY0AEdFQg7r2ILS8kcTMuY,325
279
- omdev/pyproject/pkg.py,sha256=pyOiotKT1g522mcwertS0VeGCarJmQZ8gAm8FVgqo4A,14965
278
+ omdev/pyproject/pkg.py,sha256=eo8g-EaKEfEFH2urERDlYC8ojKOsWBUZ00mQGcs5Ujg,19255
280
279
  omdev/pyproject/reqs.py,sha256=DU7NBNpFTjU06VgqRYZj5jZRQxpIWbzL9q9Vm13_o0o,3317
281
280
  omdev/pyproject/venvs.py,sha256=PNgfVrGlw9NFKJgUyzyWH5H5nAIzUDPTHRVUNBM0bKs,2187
282
281
  omdev/pyproject/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
283
282
  omdev/pyproject/resources/docker-dev.sh,sha256=DHkz5D18jok_oDolfg2mqrvGRWFoCe9GQo04dR1czcc,838
284
283
  omdev/pyproject/resources/python.sh,sha256=rFaN4SiJ9hdLDXXsDTwugI6zsw6EPkgYMmtacZeTbvw,749
284
+ omdev/rs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
285
285
  omdev/scripts/__init__.py,sha256=MKCvUAEQwsIvwLixwtPlpBqmkMXLCnjjXyAXvVpDwVk,91
286
286
  omdev/scripts/ci.py,sha256=dmpJXF0mS37K6psivRDukXEBF8Z6CAuK_PQERDBzFgE,427466
287
287
  omdev/scripts/interp.py,sha256=GELa1TPg0tStMbofHpEYetMrAl3YnInehwler2gdE2I,168564
288
- omdev/scripts/pyproject.py,sha256=DNIVc0GALqIW2lM7YUQCmil-60FblcHgAV8Sil06fWM,349535
288
+ omdev/scripts/pyproject.py,sha256=J6X4Izk-mdGLxPw-HplF3fwPQdmSEo7jQB--f1Q3xac,353810
289
289
  omdev/scripts/slowcat.py,sha256=PwdT-pg62imEEb6kcOozl9_YUi-4KopvjvzWT1OmGb0,2717
290
290
  omdev/scripts/tmpexec.py,sha256=t0nErDRALjTk7H0X8ADjZUIDFjlPNzOOokmjCjBHdzs,1431
291
291
  omdev/scripts/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -344,15 +344,20 @@ omdev/tui/apps/irc/main.py,sha256=ptsSjKE3LDmPERCExivcjTN7SOfYY7WVnIlDXSL_XQA,51
344
344
  omdev/tui/apps/markdown/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
345
345
  omdev/tui/apps/markdown/__main__.py,sha256=Xy-G2-8Ymx8QMBbRzA4LoiAMZqvtC944mMjFEWd69CA,182
346
346
  omdev/tui/apps/markdown/cli.py,sha256=K1vH7f3ZqLv4xTPluhJBEZH8nx8n42_vXIALEV07Q50,469
347
- omdev/tui/rich/__init__.py,sha256=_PcNDlzl7FIa071qni4AlBAf0oXXFHUjEaPxumnuXWs,775
347
+ omdev/tui/rich/__init__.py,sha256=ZmStqeMrES5FiHLCysKRcg3vvmrIfksIZknDIGD1o0E,809
348
348
  omdev/tui/rich/console2.py,sha256=BYYLbbD65If9TvfPI6qUcMQKUWJbuWwykEzPplvkf6A,342
349
349
  omdev/tui/rich/markdown2.py,sha256=fBcjG_34XzUf4WclBL_MxvBj5NUwvLCANhHCx3R0akw,6139
350
- omdev/tui/textual/__init__.py,sha256=l4lDY7g_spyUpCMYlzmM2jFz92xv4-VzOM88vFlV6zA,9801
350
+ omdev/tui/textual/__init__.py,sha256=2InVQxcIgL5-JcfsQWp_FoQcTLm6TJdZAd-WSwuoIYo,10621
351
351
  omdev/tui/textual/app2.py,sha256=QNh8dX9lXtvWkUOLX5x6ucJCYqDoKD78VDpaX4ZcGS8,225
352
352
  omdev/tui/textual/drivers2.py,sha256=ZVxI9n6cyczjrdjqKOAjE51pF0yppACJOVmqLaWuJuM,1402
353
- omdev-0.0.0.dev463.dist-info/licenses/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
354
- omdev-0.0.0.dev463.dist-info/METADATA,sha256=BZD4CAxJpONI1QQgQF1aYu6dr7WvhH_OXomlZy-_guA,5170
355
- omdev-0.0.0.dev463.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
356
- omdev-0.0.0.dev463.dist-info/entry_points.txt,sha256=dHLXFmq5D9B8qUyhRtFqTGWGxlbx3t5ejedjrnXNYLU,33
357
- omdev-0.0.0.dev463.dist-info/top_level.txt,sha256=1nr7j30fEWgLYHW3lGR9pkdHkb7knv1U1ES1XRNVQ6k,6
358
- omdev-0.0.0.dev463.dist-info/RECORD,,
353
+ omdev/tui/textual/autocomplete/LICENSE,sha256=E4XIgwSRB-UmqQi5b3HvRUfmpR9vfi99rvxGjnkS6BI,1069
354
+ omdev/tui/textual/autocomplete/__init__.py,sha256=m0EGewct7SoATrTcsCSmeRQyPucP5Sqew5qZOIQUgKI,1442
355
+ omdev/tui/textual/autocomplete/matching.py,sha256=joxUxF4jfs47E4JK0DAo_l0lwoNe9mU6iJzxI2FlVYI,6801
356
+ omdev/tui/textual/autocomplete/paths.py,sha256=Z3ZlTkPZezKBaFgB23d6IFJXanJc8OueBm-S0kExdRM,7242
357
+ omdev/tui/textual/autocomplete/widget.py,sha256=1UgWqDT0d9wD6w7MNaZBjgj0o9FohYXydifocPErdks,22629
358
+ omdev-0.0.0.dev465.dist-info/licenses/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
359
+ omdev-0.0.0.dev465.dist-info/METADATA,sha256=YcjjqPR-_yHV6suja0JJvqNBnDRViw3bBQBSL_cje2Y,5170
360
+ omdev-0.0.0.dev465.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
361
+ omdev-0.0.0.dev465.dist-info/entry_points.txt,sha256=dHLXFmq5D9B8qUyhRtFqTGWGxlbx3t5ejedjrnXNYLU,33
362
+ omdev-0.0.0.dev465.dist-info/top_level.txt,sha256=1nr7j30fEWgLYHW3lGR9pkdHkb7knv1U1ES1XRNVQ6k,6
363
+ omdev-0.0.0.dev465.dist-info/RECORD,,