omdev 0.0.0.dev462__py3-none-any.whl → 0.0.0.dev464__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.
@@ -8,15 +8,28 @@ with _lang.auto_proxy_init(globals()):
8
8
 
9
9
  from textual import app # noqa
10
10
  from textual import binding # noqa
11
+ from textual import constants # noqa
11
12
  from textual import containers # noqa
13
+ from textual import content # noqa
14
+ from textual import driver # noqa
12
15
  from textual import events # noqa
13
- from textual import events # noqa
16
+ from textual import geometry # noqa
17
+ from textual import markup # noqa
14
18
  from textual import message # noqa
19
+ from textual import messages # noqa
20
+ from textual import on # noqa
21
+ from textual import pad # noqa
15
22
  from textual import reactive # noqa
23
+ from textual import screen # noqa
24
+ from textual import style # noqa
25
+ from textual import suggester # noqa
26
+ from textual import suggestions # noqa
27
+ from textual import timer # noqa
28
+ from textual import widget # noqa
16
29
  from textual import widgets # noqa
17
30
  from textual.app import ActionError # noqa
18
31
  from textual.app import ActiveModeError # noqa
19
- from textual.app import App # noqa
32
+ from textual.app import App as App_ # noqa
20
33
  from textual.app import AppError # noqa
21
34
  from textual.app import AutopilotCallbackType # noqa
22
35
  from textual.app import CallThreadReturnType # noqa
@@ -59,6 +72,11 @@ with _lang.auto_proxy_init(globals()):
59
72
  from textual.containers import Vertical # noqa
60
73
  from textual.containers import VerticalGroup # noqa
61
74
  from textual.containers import VerticalScroll # noqa
75
+ from textual.content import Content # noqa
76
+ from textual.content import ContentText # noqa
77
+ from textual.content import ContentType # noqa
78
+ from textual.content import EMPTY_CONTENT # noqa
79
+ from textual.content import Span # noqa
62
80
  from textual.driver import Driver # noqa
63
81
  from textual.events import Action # noqa
64
82
  from textual.events import AppBlur # noqa
@@ -99,10 +117,55 @@ with _lang.auto_proxy_init(globals()):
99
117
  from textual.events import ScreenResume # noqa
100
118
  from textual.events import ScreenSuspend # noqa
101
119
  from textual.events import Show # noqa
102
- from textual.events import Timer # noqa
120
+ from textual.events import Timer as TimerEvent # noqa
103
121
  from textual.events import Unmount # noqa
122
+ from textual.geometry import NULL_OFFSET # noqa
123
+ from textual.geometry import NULL_REGION # noqa
124
+ from textual.geometry import NULL_SIZE # noqa
125
+ from textual.geometry import NULL_SPACING # noqa
126
+ from textual.geometry import Offset # noqa
127
+ from textual.geometry import Region # noqa
128
+ from textual.geometry import Size # noqa
129
+ from textual.geometry import Spacing # noqa
130
+ from textual.geometry import SpacingDimensions # noqa
131
+ from textual.geometry import clamp # noqa
132
+ from textual.markup import MarkupError # noqa
133
+ from textual.markup import MarkupTokenizer # noqa
134
+ from textual.markup import StyleTokenizer # noqa
135
+ from textual.markup import escape # noqa
136
+ from textual.markup import parse_style # noqa
137
+ from textual.markup import to_content # noqa
104
138
  from textual.message import Message # noqa
139
+ from textual.messages import CloseMessages # noqa
140
+ from textual.messages import ExitApp # noqa
141
+ from textual.messages import InBandWindowResize # noqa
142
+ from textual.messages import InvokeLater # noqa
143
+ from textual.messages import Layout # noqa
144
+ from textual.messages import Prompt # noqa
145
+ from textual.messages import Prune # noqa
146
+ from textual.messages import ScrollToRegion # noqa
147
+ from textual.messages import TerminalSupportsSynchronizedOutput # noqa
148
+ from textual.messages import Update # noqa
149
+ from textual.messages import UpdateScroll # noqa
150
+ from textual.pad import HorizontalPad # noqa
151
+ from textual.reactive import Initialize # noqa
105
152
  from textual.reactive import Reactive # noqa
153
+ from textual.reactive import ReactiveError # noqa
154
+ from textual.reactive import await_watcher # noqa
155
+ from textual.reactive import invoke_watcher # noqa
156
+ from textual.reactive import reactive as reactive_ # noqa
157
+ from textual.reactive import var # noqa
158
+ from textual.screen import ModalScreen # noqa
159
+ from textual.screen import Screen # noqa
160
+ from textual.screen import SystemModalScreen # noqa
161
+ from textual.style import Style # noqa
162
+ from textual.suggester import SuggestFromList # noqa
163
+ from textual.suggester import Suggester # noqa
164
+ from textual.suggester import SuggestionReady # noqa
165
+ from textual.suggestions import get_suggestion # noqa
166
+ from textual.suggestions import get_suggestions # noqa
167
+ from textual.timer import Timer # noqa
168
+ from textual.timer import TimerCallback # noqa
106
169
  from textual.widget import Widget # noqa
107
170
  from textual.widgets import Button # noqa
108
171
  from textual.widgets import Checkbox # noqa
@@ -147,9 +210,16 @@ with _lang.auto_proxy_init(globals()):
147
210
  from textual.widgets import Tooltip # noqa
148
211
  from textual.widgets import Tree # noqa
149
212
  from textual.widgets import Welcome # noqa
213
+ from textual.widgets.option_list import OptionDoesNotExist # noqa
214
+ from textual.widgets.option_list import Option # noqa
215
+ from textual.widgets.option_list import DuplicateID # noqa
150
216
 
151
217
  ##
152
218
 
219
+ from .app2 import ( # noqa
220
+ App,
221
+ )
222
+
153
223
  from .drivers2 import ( # noqa
154
224
  PendingWritesDriverMixin,
155
225
  get_pending_writes_driver_class,
@@ -0,0 +1,11 @@
1
+ import typing as ta
2
+
3
+ from textual.app import App as App_
4
+ from textual.binding import BindingType # noqa
5
+
6
+
7
+ ##
8
+
9
+
10
+ class App(App_):
11
+ BINDINGS: ta.ClassVar[ta.Sequence[BindingType]] = App_.BINDINGS # type: ignore[assignment]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Darren Burns
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,33 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2023 Darren Burns
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ # documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ # persons to whom the Software is furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ # Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17
+ #
18
+ # https://github.com/darrenburns/textual-autocomplete/tree/0344cd3eb3383cbbd80e01b035ed808ce53cef4d
19
+ from .matching import ( # noqa
20
+ FuzzySearch,
21
+ FuzzyMatcher,
22
+ )
23
+
24
+ from .paths import ( # noqa
25
+ PathAutoComplete,
26
+ )
27
+
28
+ from .widget import ( # noqa
29
+ AutoCompleteItem,
30
+ AutoCompleteItemHit,
31
+ AutoCompleteList,
32
+ AutoComplete,
33
+ )
@@ -0,0 +1,226 @@
1
+ import functools
2
+ import operator
3
+ import re
4
+ import typing as ta
5
+
6
+ import rich.repr
7
+ from textual.cache import LRUCache
8
+ from textual.content import Content
9
+ from textual.style import Style
10
+
11
+
12
+ ##
13
+
14
+
15
+ class FuzzySearch:
16
+ """
17
+ Performs a fuzzy search.
18
+
19
+ Unlike a regex solution, this will finds all possible matches.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ case_sensitive: bool = False,
25
+ *,
26
+ cache_size: int = 1024 * 4,
27
+ ) -> None:
28
+ """
29
+ Initialize fuzzy search.
30
+
31
+ Args:
32
+ case_sensitive: Is the match case sensitive?
33
+ cache_size: Number of queries to cache.
34
+ """
35
+
36
+ self.case_sensitive = case_sensitive
37
+ self.cache: LRUCache[tuple[str, str], tuple[float, ta.Sequence[int]]] = LRUCache(cache_size)
38
+
39
+ def match(self, query: str, candidate: str) -> tuple[float, ta.Sequence[int]]:
40
+ """
41
+ Match against a query.
42
+
43
+ Args:
44
+ query: The fuzzy query.
45
+ candidate: A candidate to check,.
46
+
47
+ Returns:
48
+ A pair of (score, tuple of offsets). `(0, ())` for no result.
49
+ """
50
+
51
+ cache_key = (query, candidate)
52
+ if cache_key in self.cache:
53
+ return self.cache[cache_key]
54
+ default: tuple[float, ta.Sequence[int]] = (0.0, [])
55
+ result = max(self._match(query, candidate), key=operator.itemgetter(0), default=default)
56
+ self.cache[cache_key] = result
57
+ return result
58
+
59
+ @classmethod
60
+ @functools.lru_cache(maxsize=1024)
61
+ def get_first_letters(cls, candidate: str) -> frozenset[int]:
62
+ return frozenset({match.start() for match in re.finditer(r'\w+', candidate)})
63
+
64
+ def score(self, candidate: str, positions: ta.Sequence[int]) -> float:
65
+ """
66
+ Score a search.
67
+
68
+ Args:
69
+ search: Search object.
70
+
71
+ Returns:
72
+ Score.
73
+ """
74
+
75
+ first_letters = self.get_first_letters(candidate)
76
+ # This is a heuristic, and can be tweaked for better results.
77
+ # Boost first letter matches.
78
+ offset_count = len(positions)
79
+ score: float = offset_count + len(first_letters.intersection(positions))
80
+
81
+ groups = 1
82
+ last_offset, *offsets = positions
83
+ for offset in offsets:
84
+ if offset != last_offset + 1:
85
+ groups += 1
86
+ last_offset = offset
87
+
88
+ # Boost to favor less groups
89
+ normalized_groups = (offset_count - (groups - 1)) / offset_count
90
+ score *= 1 + (normalized_groups * normalized_groups)
91
+ return score
92
+
93
+ def _match(self, query: str, candidate: str) -> ta.Iterable[tuple[float, ta.Sequence[int]]]:
94
+ letter_positions: list[list[int]] = []
95
+ position = 0
96
+
97
+ if not self.case_sensitive:
98
+ candidate = candidate.lower()
99
+ query = query.lower()
100
+ score = self.score
101
+ if query in candidate:
102
+ # Quick exit when the query exists as a substring
103
+ query_location = candidate.rfind(query)
104
+ offsets = list(range(query_location, query_location + len(query)))
105
+ yield (
106
+ score(candidate, offsets) * (2.0 if candidate == query else 1.5),
107
+ offsets,
108
+ )
109
+ return
110
+
111
+ for offset, letter in enumerate(query):
112
+ last_index = len(candidate) - offset
113
+ positions: list[int] = []
114
+ letter_positions.append(positions)
115
+ index = position
116
+ while (location := candidate.find(letter, index)) != -1:
117
+ positions.append(location)
118
+ index = location + 1
119
+ if index >= last_index:
120
+ break
121
+ if not positions:
122
+ yield (0.0, ())
123
+ return
124
+ position = positions[0] + 1
125
+
126
+ possible_offsets: list[list[int]] = []
127
+ query_length = len(query)
128
+
129
+ def get_offsets(offsets: list[int], positions_index: int) -> None:
130
+ """
131
+ Recursively match offsets.
132
+
133
+ Args:
134
+ offsets: A list of offsets.
135
+ positions_index: Index of query letter.
136
+ """
137
+
138
+ for offset in letter_positions[positions_index]:
139
+ if not offsets or offset > offsets[-1]:
140
+ new_offsets = [*offsets, offset]
141
+ if len(new_offsets) == query_length:
142
+ possible_offsets.append(new_offsets)
143
+ else:
144
+ get_offsets(new_offsets, positions_index + 1)
145
+
146
+ get_offsets([], 0)
147
+
148
+ for offsets in possible_offsets:
149
+ yield score(candidate, offsets), offsets
150
+
151
+
152
+ @rich.repr.auto
153
+ class FuzzyMatcher:
154
+ """A fuzzy matcher."""
155
+
156
+ def __init__(
157
+ self,
158
+ query: str,
159
+ *,
160
+ match_style: Style | None = None,
161
+ case_sensitive: bool = False,
162
+ ) -> None:
163
+ """
164
+ Initialize the fuzzy matching object.
165
+
166
+ Args:
167
+ query: A query as typed in by the user.
168
+ match_style: The style to use to highlight matched portions of a string.
169
+ case_sensitive: Should matching be case sensitive?
170
+ """
171
+
172
+ self._query = query
173
+ self._match_style = Style(reverse=True) if match_style is None else match_style
174
+ self._case_sensitive = case_sensitive
175
+ self.fuzzy_search = FuzzySearch()
176
+
177
+ @property
178
+ def query(self) -> str:
179
+ """The query string to look for."""
180
+
181
+ return self._query
182
+
183
+ @property
184
+ def match_style(self) -> Style:
185
+ """The style that will be used to highlight hits in the matched text."""
186
+
187
+ return self._match_style
188
+
189
+ @property
190
+ def case_sensitive(self) -> bool:
191
+ """Is this matcher case sensitive?"""
192
+
193
+ return self._case_sensitive
194
+
195
+ def match(self, candidate: str) -> float:
196
+ """
197
+ Match the candidate against the query.
198
+
199
+ Args:
200
+ candidate: Candidate string to match against the query.
201
+
202
+ Returns:
203
+ Strength of the match from 0 to 1.
204
+ """
205
+
206
+ return self.fuzzy_search.match(self.query, candidate)[0]
207
+
208
+ def highlight(self, candidate: str) -> Content:
209
+ """
210
+ Highlight the candidate with the fuzzy match.
211
+
212
+ Args:
213
+ candidate: The candidate string to match against the query.
214
+
215
+ Returns:
216
+ A [`Text`][rich.text.Text] object with highlighted matches.
217
+ """
218
+
219
+ content = Content.from_markup(candidate)
220
+ score, offsets = self.fuzzy_search.match(self.query, candidate)
221
+ if not score:
222
+ return content
223
+ for offset in offsets:
224
+ if not candidate[offset].isspace():
225
+ content = content.stylize(self._match_style, offset, offset + 1)
226
+ return content
@@ -0,0 +1,202 @@
1
+ import os
2
+ import pathlib
3
+ import typing as ta
4
+
5
+ from textual.cache import LRUCache
6
+ from textual.content import Content
7
+ from textual.widgets import Input
8
+
9
+ from .widget import AutoComplete
10
+ from .widget import AutoCompleteItem
11
+
12
+
13
+ ##
14
+
15
+
16
+ class PathAutoCompleteItem(AutoCompleteItem):
17
+ def __init__(self, completion: str, path: pathlib.Path) -> None:
18
+ super().__init__(completion)
19
+
20
+ self.path = path
21
+
22
+
23
+ def default_path_input_sort_key(item: PathAutoCompleteItem) -> tuple[bool, bool, str]:
24
+ """
25
+ Sort key function for results within the dropdown.
26
+
27
+ Args:
28
+ item: The PathAutoCompleteItem to get a sort key for.
29
+
30
+ Returns:
31
+ A tuple of (is_dotfile, is_file, lowercase_name) for sorting.
32
+ """
33
+
34
+ name = item.path.name
35
+ is_dotfile = name.startswith('.')
36
+ return (not item.path.is_dir(), not is_dotfile, name.lower())
37
+
38
+
39
+ class PathAutoComplete(AutoComplete):
40
+ def __init__(
41
+ self,
42
+ target: Input | str,
43
+ path: str | pathlib.Path = '.',
44
+ *,
45
+ show_dotfiles: bool = True,
46
+ sort_key: ta.Callable[[PathAutoCompleteItem], ta.Any] = default_path_input_sort_key,
47
+ folder_prefix: Content = Content('📂'),
48
+ file_prefix: Content = Content('📄'),
49
+ prevent_default_enter: bool = True,
50
+ prevent_default_tab: bool = True,
51
+ cache_size: int = 100,
52
+ name: str | None = None,
53
+ id: str | None = None, # noqa
54
+ classes: str | None = None,
55
+ disabled: bool = False,
56
+ ) -> None:
57
+ """
58
+ An autocomplete widget for filesystem paths.
59
+
60
+ Args:
61
+ target: The target input widget to autocomplete.
62
+ path: The base path to autocomplete from.
63
+ show_dotfiles: Whether to show dotfiles (files/dirs starting with ".").
64
+ sort_key: Function to sort the dropdown items.
65
+ folder_prefix: The prefix for folder items (e.g. 📂).
66
+ file_prefix: The prefix for file items (e.g. 📄).
67
+ prevent_default_enter: Whether to prevent the default enter behavior.
68
+ prevent_default_tab: Whether to prevent the default tab behavior.
69
+ cache_size: The number of directories to cache.
70
+ name: The name of the widget.
71
+ id: The DOM node id of the widget.
72
+ classes: The CSS classes of the widget.
73
+ disabled: Whether the widget is disabled.
74
+ """
75
+
76
+ super().__init__(
77
+ target,
78
+ None,
79
+ prevent_default_enter=prevent_default_enter,
80
+ prevent_default_tab=prevent_default_tab,
81
+ name=name,
82
+ id=id,
83
+ classes=classes,
84
+ disabled=disabled,
85
+ )
86
+
87
+ self.path = pathlib.Path(path) if isinstance(path, str) else path
88
+ self.show_dotfiles = show_dotfiles
89
+ self.sort_key = sort_key
90
+ self.folder_prefix = folder_prefix
91
+ self.file_prefix = file_prefix
92
+ self._directory_cache: LRUCache[str, list[os.DirEntry[str]]] = LRUCache(cache_size)
93
+
94
+ def get_candidates(self, target_state: AutoComplete.TargetState) -> list[AutoCompleteItem]:
95
+ """
96
+ Get the candidates for the current path segment.
97
+
98
+ This is called each time the input changes or the cursor position changes/
99
+ """
100
+
101
+ current_input = target_state.text[: target_state.cursor_position]
102
+
103
+ if '/' in current_input:
104
+ last_slash_index = current_input.rindex('/')
105
+ path_segment = current_input[:last_slash_index] or '/'
106
+ directory = self.path / path_segment if path_segment != '/' else self.path
107
+ else:
108
+ directory = self.path
109
+
110
+ # Use the directory path as the cache key
111
+ cache_key = str(directory)
112
+ cached_entries = self._directory_cache.get(cache_key)
113
+
114
+ if cached_entries is not None:
115
+ entries = cached_entries
116
+ else:
117
+ try:
118
+ entries = list(os.scandir(directory))
119
+ self._directory_cache[cache_key] = entries
120
+ except OSError:
121
+ return []
122
+
123
+ results: list[PathAutoCompleteItem] = []
124
+ for entry in entries:
125
+ # Only include the entry name, not the full path
126
+ completion = entry.name
127
+ if not self.show_dotfiles and completion.startswith('.'):
128
+ continue
129
+ if entry.is_dir():
130
+ completion += '/'
131
+ results.append(PathAutoCompleteItem(completion, path=pathlib.Path(entry.path)))
132
+
133
+ results.sort(key=self.sort_key)
134
+ folder_prefix = self.folder_prefix
135
+ file_prefix = self.file_prefix
136
+ return [
137
+ AutoCompleteItem(
138
+ item.main,
139
+ prefix=folder_prefix if item.path.is_dir() else file_prefix,
140
+ )
141
+ for item in results
142
+ ]
143
+
144
+ def get_search_string(self, target_state: AutoComplete.TargetState) -> str:
145
+ """Return only the current path segment for searching in the dropdown."""
146
+
147
+ current_input = target_state.text[: target_state.cursor_position]
148
+
149
+ if '/' in current_input:
150
+ last_slash_index = current_input.rindex('/')
151
+ search_string = current_input[last_slash_index + 1:]
152
+ return search_string
153
+ else:
154
+ return current_input
155
+
156
+ def apply_completion(self, value: str, state: AutoComplete.TargetState) -> None:
157
+ """Apply the completion by replacing only the current path segment."""
158
+
159
+ target = self.target
160
+ current_input = state.text
161
+ cursor_position = state.cursor_position
162
+
163
+ # There's a slash before the cursor, so we only want to replace the text after the last slash with the selected
164
+ # value
165
+ try:
166
+ replace_start_index = current_input.rindex('/', 0, cursor_position)
167
+ except ValueError:
168
+ # No slashes, so we do a full replacement
169
+ new_value = value
170
+ new_cursor_position = len(value)
171
+ else:
172
+ # Keep everything before and including the slash before the cursor.
173
+ path_prefix = current_input[: replace_start_index + 1]
174
+ new_value = path_prefix + value
175
+ new_cursor_position = len(path_prefix) + len(value)
176
+
177
+ with self.prevent(Input.Changed):
178
+ target.value = new_value
179
+ target.cursor_position = new_cursor_position
180
+
181
+ def post_completion(self) -> None:
182
+ if not self.target.value.endswith('/'):
183
+ self.action_hide()
184
+
185
+ def should_show_dropdown(self, search_string: str) -> bool:
186
+ return (
187
+ super().should_show_dropdown(search_string) or
188
+ (
189
+ (search_string == '' and self.target.value != '') and
190
+ self.option_list.option_count > 1
191
+ )
192
+ )
193
+
194
+ def clear_directory_cache(self) -> None:
195
+ """
196
+ Clear the directory cache. If you know that the contents of the directory have changed, you can call this method
197
+ to invalidate the cache.
198
+ """
199
+
200
+ self._directory_cache.clear()
201
+ target_state = self._get_target_state()
202
+ self._rebuild_options(target_state, self.get_search_string(target_state))