data-sourcerer 0.1.0__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.
- data_sourcerer-0.1.0.dist-info/METADATA +52 -0
- data_sourcerer-0.1.0.dist-info/RECORD +95 -0
- data_sourcerer-0.1.0.dist-info/WHEEL +5 -0
- data_sourcerer-0.1.0.dist-info/entry_points.txt +2 -0
- data_sourcerer-0.1.0.dist-info/licenses/LICENSE +21 -0
- data_sourcerer-0.1.0.dist-info/top_level.txt +1 -0
- sourcerer/__init__.py +15 -0
- sourcerer/domain/__init__.py +13 -0
- sourcerer/domain/access_credentials/__init__.py +7 -0
- sourcerer/domain/access_credentials/entities.py +53 -0
- sourcerer/domain/access_credentials/exceptions.py +17 -0
- sourcerer/domain/access_credentials/repositories.py +84 -0
- sourcerer/domain/access_credentials/services.py +91 -0
- sourcerer/domain/file_system/__init__.py +7 -0
- sourcerer/domain/file_system/entities.py +70 -0
- sourcerer/domain/file_system/exceptions.py +17 -0
- sourcerer/domain/file_system/services.py +64 -0
- sourcerer/domain/shared/__init__.py +6 -0
- sourcerer/domain/shared/entities.py +18 -0
- sourcerer/domain/storage_provider/__init__.py +7 -0
- sourcerer/domain/storage_provider/entities.py +86 -0
- sourcerer/domain/storage_provider/exceptions.py +17 -0
- sourcerer/domain/storage_provider/services.py +130 -0
- sourcerer/infrastructure/__init__.py +13 -0
- sourcerer/infrastructure/access_credentials/__init__.py +7 -0
- sourcerer/infrastructure/access_credentials/exceptions.py +16 -0
- sourcerer/infrastructure/access_credentials/registry.py +120 -0
- sourcerer/infrastructure/access_credentials/repositories.py +119 -0
- sourcerer/infrastructure/access_credentials/services.py +396 -0
- sourcerer/infrastructure/db/__init__.py +6 -0
- sourcerer/infrastructure/db/config.py +73 -0
- sourcerer/infrastructure/db/models.py +47 -0
- sourcerer/infrastructure/file_system/__init__.py +7 -0
- sourcerer/infrastructure/file_system/exceptions.py +89 -0
- sourcerer/infrastructure/file_system/services.py +147 -0
- sourcerer/infrastructure/storage_provider/__init__.py +7 -0
- sourcerer/infrastructure/storage_provider/exceptions.py +78 -0
- sourcerer/infrastructure/storage_provider/registry.py +84 -0
- sourcerer/infrastructure/storage_provider/services.py +509 -0
- sourcerer/infrastructure/utils.py +106 -0
- sourcerer/presentation/__init__.py +12 -0
- sourcerer/presentation/app.py +36 -0
- sourcerer/presentation/di_container.py +46 -0
- sourcerer/presentation/screens/__init__.py +0 -0
- sourcerer/presentation/screens/critical_error/__init__.py +0 -0
- sourcerer/presentation/screens/critical_error/main.py +78 -0
- sourcerer/presentation/screens/critical_error/styles.tcss +41 -0
- sourcerer/presentation/screens/file_system_finder/main.py +248 -0
- sourcerer/presentation/screens/file_system_finder/styles.tcss +44 -0
- sourcerer/presentation/screens/file_system_finder/widgets/__init__.py +0 -0
- sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +810 -0
- sourcerer/presentation/screens/main/__init__.py +0 -0
- sourcerer/presentation/screens/main/main.py +469 -0
- sourcerer/presentation/screens/main/messages/__init__.py +0 -0
- sourcerer/presentation/screens/main/messages/delete_request.py +12 -0
- sourcerer/presentation/screens/main/messages/download_request.py +12 -0
- sourcerer/presentation/screens/main/messages/preview_request.py +10 -0
- sourcerer/presentation/screens/main/messages/resizing_rule.py +21 -0
- sourcerer/presentation/screens/main/messages/select_storage_item.py +11 -0
- sourcerer/presentation/screens/main/messages/uncheck_files_request.py +8 -0
- sourcerer/presentation/screens/main/messages/upload_request.py +10 -0
- sourcerer/presentation/screens/main/mixins/__init__.py +0 -0
- sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +144 -0
- sourcerer/presentation/screens/main/styles.tcss +32 -0
- sourcerer/presentation/screens/main/widgets/__init__.py +0 -0
- sourcerer/presentation/screens/main/widgets/gradient.py +45 -0
- sourcerer/presentation/screens/main/widgets/resizing_rule.py +67 -0
- sourcerer/presentation/screens/main/widgets/storage_content.py +691 -0
- sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +143 -0
- sourcerer/presentation/screens/preview_content/__init__.py +0 -0
- sourcerer/presentation/screens/preview_content/main.py +59 -0
- sourcerer/presentation/screens/preview_content/styles.tcss +26 -0
- sourcerer/presentation/screens/provider_creds_list/__init__.py +0 -0
- sourcerer/presentation/screens/provider_creds_list/main.py +164 -0
- sourcerer/presentation/screens/provider_creds_list/styles.tcss +65 -0
- sourcerer/presentation/screens/provider_creds_registration/__init__.py +0 -0
- sourcerer/presentation/screens/provider_creds_registration/main.py +264 -0
- sourcerer/presentation/screens/provider_creds_registration/styles.tcss +42 -0
- sourcerer/presentation/screens/question/__init__.py +0 -0
- sourcerer/presentation/screens/question/main.py +31 -0
- sourcerer/presentation/screens/question/styles.tcss +33 -0
- sourcerer/presentation/screens/shared/__init__.py +0 -0
- sourcerer/presentation/screens/shared/containers.py +13 -0
- sourcerer/presentation/screens/shared/widgets/__init__.py +0 -0
- sourcerer/presentation/screens/shared/widgets/button.py +54 -0
- sourcerer/presentation/screens/shared/widgets/labeled_input.py +80 -0
- sourcerer/presentation/screens/storage_action_progress/__init__.py +0 -0
- sourcerer/presentation/screens/storage_action_progress/main.py +476 -0
- sourcerer/presentation/screens/storage_action_progress/styles.tcss +43 -0
- sourcerer/presentation/settings.py +31 -0
- sourcerer/presentation/themes/__init__.py +0 -0
- sourcerer/presentation/themes/github_dark.py +21 -0
- sourcerer/presentation/utils.py +69 -0
- sourcerer/settings.py +72 -0
- sourcerer/utils.py +32 -0
@@ -0,0 +1,810 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from enum import Enum
|
3
|
+
from pathlib import Path
|
4
|
+
from time import time
|
5
|
+
from typing import ClassVar, Sequence, Literal, List, Callable, Tuple, Type
|
6
|
+
|
7
|
+
from textual import events, on
|
8
|
+
from textual.app import ComposeResult
|
9
|
+
from textual.binding import Binding, BindingType
|
10
|
+
from textual.css.query import NoMatches
|
11
|
+
from textual.message import Message
|
12
|
+
from textual.reactive import reactive
|
13
|
+
from textual.widget import Widget
|
14
|
+
from textual.widgets import Rule, Label
|
15
|
+
|
16
|
+
from sourcerer.infrastructure.file_system.exceptions import ListDirException
|
17
|
+
from sourcerer.infrastructure.file_system.services import FileSystemService
|
18
|
+
from sourcerer.presentation.screens.shared.containers import (
|
19
|
+
ScrollHorizontalContainerWithNoBindings,
|
20
|
+
ScrollVerticalContainerWithNoBindings,
|
21
|
+
)
|
22
|
+
|
23
|
+
from sourcerer.infrastructure.utils import generate_uuid
|
24
|
+
from sourcerer.settings import DOUBLE_CLICK_THRESHOLD, FILE_ICON, DIRECTORY_ICON
|
25
|
+
|
26
|
+
|
27
|
+
class PathListingContainer(ScrollVerticalContainerWithNoBindings):
|
28
|
+
can_focus = False
|
29
|
+
|
30
|
+
def on_mount(self, _: events.Mount) -> None:
|
31
|
+
"""
|
32
|
+
Ensure the scroll is visible when the container is mounted.
|
33
|
+
|
34
|
+
This method is called automatically when the container is mounted, and it makes the scroll bar visible,
|
35
|
+
improving the user's ability to navigate through content that may exceed the container's visible area.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
_ (events.Mount): The mount event triggered when the container is added to the UI.
|
39
|
+
"""
|
40
|
+
self.scroll_visible()
|
41
|
+
|
42
|
+
|
43
|
+
class FileSystemWidget(Widget):
|
44
|
+
DEFAULT_CSS = """
|
45
|
+
FileSystemWidget {
|
46
|
+
&:hover, &:focus, &:focus-within, &:ansi {
|
47
|
+
color: $block-cursor-blurred-foreground;
|
48
|
+
background: $block-cursor-blurred-background;
|
49
|
+
text-style: $block-cursor-blurred-text-style;
|
50
|
+
}
|
51
|
+
|
52
|
+
.folder-name {
|
53
|
+
text-overflow: ellipsis;
|
54
|
+
text-wrap: nowrap;
|
55
|
+
}
|
56
|
+
}
|
57
|
+
"""
|
58
|
+
can_focus = True
|
59
|
+
|
60
|
+
@dataclass
|
61
|
+
class FileClick(Message):
|
62
|
+
name: Path
|
63
|
+
|
64
|
+
@dataclass
|
65
|
+
class FileDoubleClick(Message):
|
66
|
+
name: Path
|
67
|
+
|
68
|
+
@dataclass
|
69
|
+
class FolderClick(Message):
|
70
|
+
name: Path
|
71
|
+
|
72
|
+
@dataclass
|
73
|
+
class FolderDoubleClick(Message):
|
74
|
+
name: Path
|
75
|
+
|
76
|
+
@dataclass
|
77
|
+
class Focus(Message):
|
78
|
+
name: Path
|
79
|
+
|
80
|
+
def __init__(self, entity_name: Path, icon: str, *args, **kwargs):
|
81
|
+
"""
|
82
|
+
Initialize a FileSystemWidget representing a file or directory.
|
83
|
+
|
84
|
+
Parameters:
|
85
|
+
entity_name (Path): The path of the file or directory being represented
|
86
|
+
icon (str): A visual icon representing the file or directory type
|
87
|
+
*args: Variable positional arguments passed to the parent Widget constructor
|
88
|
+
**kwargs: Variable keyword arguments passed to the parent Widget constructor
|
89
|
+
|
90
|
+
Attributes:
|
91
|
+
entity_name (Path): Stores the path of the current file or directory
|
92
|
+
icon (str): Stores the icon for visual representation
|
93
|
+
last_file_click (Tuple[float, Path | None]): Tracks the timestamp and path of the last file click
|
94
|
+
to enable double-click detection, initialized with a timestamp two seconds in the past
|
95
|
+
"""
|
96
|
+
self.entity_name = entity_name
|
97
|
+
self.icon = icon
|
98
|
+
self.last_file_click: Tuple[float, Path | None] = (
|
99
|
+
time() - 2,
|
100
|
+
None,
|
101
|
+
)
|
102
|
+
super().__init__(*args, **kwargs)
|
103
|
+
|
104
|
+
def compose(self) -> ComposeResult:
|
105
|
+
"""
|
106
|
+
Compose the visual representation of a file system entity.
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
Label: A label displaying the entity's icon and name with the 'folder-name' CSS class.
|
110
|
+
|
111
|
+
The method yields a Label widget that combines the predefined icon and the name of the entity
|
112
|
+
(file or folder) for display in the user interface.
|
113
|
+
"""
|
114
|
+
yield Label(
|
115
|
+
f"{self.icon} {self.entity_name.name}", classes="folder-name"
|
116
|
+
).with_tooltip(self.entity_name.name)
|
117
|
+
|
118
|
+
def on_click(self, _: events.Click) -> None:
|
119
|
+
"""
|
120
|
+
Handle click events for file system entities, distinguishing between single and double clicks.
|
121
|
+
|
122
|
+
This method processes user clicks on files and folders, posting appropriate messages based on the entity type:
|
123
|
+
- For directories, it posts a FolderClick message
|
124
|
+
- For files, it posts a FileClick message
|
125
|
+
- For files clicked twice within a short time threshold, it posts a FileDoubleClick message
|
126
|
+
|
127
|
+
Parameters:
|
128
|
+
_ (events.Click): The click event (unused parameter)
|
129
|
+
|
130
|
+
Side Effects:
|
131
|
+
- Posts FileSystemWidget messages for folder and file interactions
|
132
|
+
- Updates the last file click timestamp and entity
|
133
|
+
"""
|
134
|
+
self.on_file_select()
|
135
|
+
|
136
|
+
def on_focus(self, _: events.Focus) -> None:
|
137
|
+
"""
|
138
|
+
Handle focus event for the file system widget.
|
139
|
+
|
140
|
+
When the widget receives focus, it posts a Focus message with the entity's name.
|
141
|
+
|
142
|
+
Parameters:
|
143
|
+
_ (events.Focus): The focus event (unused, hence the underscore)
|
144
|
+
|
145
|
+
Emits:
|
146
|
+
Focus message containing the name of the focused entity
|
147
|
+
"""
|
148
|
+
self.post_message(self.Focus(self.entity_name))
|
149
|
+
|
150
|
+
def on_key(self, event: events.Key) -> None:
|
151
|
+
"""
|
152
|
+
Handle key press events for the widget, specifically the "enter" key.
|
153
|
+
|
154
|
+
When the "enter" key is pressed, this method triggers the same behavior as a click event,
|
155
|
+
simulating a user interaction with the current widget.
|
156
|
+
|
157
|
+
Parameters:
|
158
|
+
event (events.Key): The key press event containing details about the key that was pressed.
|
159
|
+
|
160
|
+
Side Effects:
|
161
|
+
Calls the `on_click` method if the pressed key is "enter", which may trigger
|
162
|
+
folder navigation or file opening depending on the widget's context.
|
163
|
+
"""
|
164
|
+
if event.key == "enter":
|
165
|
+
self.on_file_select()
|
166
|
+
|
167
|
+
def on_file_select(self):
|
168
|
+
"""
|
169
|
+
Handle a file selection event by updating the selected file.
|
170
|
+
|
171
|
+
This method is triggered when a file is selected in the file system navigator. It updates the selected file
|
172
|
+
based on the event's file path.
|
173
|
+
|
174
|
+
"""
|
175
|
+
current_click = (time(), self.entity_name)
|
176
|
+
|
177
|
+
self.send_event(
|
178
|
+
self.FolderClick if self.entity_name.is_dir() else self.FileClick
|
179
|
+
)
|
180
|
+
if (
|
181
|
+
current_click[0] - self.last_file_click[0] < DOUBLE_CLICK_THRESHOLD
|
182
|
+
and current_click[1] == self.last_file_click[1]
|
183
|
+
):
|
184
|
+
self.send_event(
|
185
|
+
self.FolderDoubleClick
|
186
|
+
if self.entity_name.is_dir()
|
187
|
+
else self.FileDoubleClick
|
188
|
+
)
|
189
|
+
self.last_file_click = current_click
|
190
|
+
|
191
|
+
def send_event(self, event_name):
|
192
|
+
"""
|
193
|
+
Send an event with the current file system entity's name.
|
194
|
+
|
195
|
+
Parameters:
|
196
|
+
event_name: The event class to be sent, which should be a dataclass with a 'name' field.
|
197
|
+
|
198
|
+
This method is used to post an event related to the current file system entity, such as a click or double-click.
|
199
|
+
It dynamically creates an instance of the provided event class with the entity's name and posts it.
|
200
|
+
"""
|
201
|
+
self.post_message(event_name(self.entity_name))
|
202
|
+
|
203
|
+
|
204
|
+
class FileSystemNavigatorClasses(Enum):
|
205
|
+
MAIN_CONTAINER = "main_container"
|
206
|
+
PATH_LISTING_CONTAINER = "path_listing_container"
|
207
|
+
DIRECTORY_LISTING_FOLDER = "dir-listing-folder"
|
208
|
+
DIRECTORY_LISTING_FILE = "dir-listing-file"
|
209
|
+
|
210
|
+
|
211
|
+
class FileSystemNavigator(ScrollHorizontalContainerWithNoBindings):
|
212
|
+
DEFAULT_CSS = """
|
213
|
+
FileSystemNavigator {
|
214
|
+
width: 100%;
|
215
|
+
height: 11;
|
216
|
+
layout: horizontal;
|
217
|
+
overflow-y: hidden;
|
218
|
+
overflow-x: auto;
|
219
|
+
overflow-x: auto;
|
220
|
+
& > .dir_listing {
|
221
|
+
overflow-y: auto;
|
222
|
+
overflow-x: auto;
|
223
|
+
width: 40%;
|
224
|
+
}
|
225
|
+
|
226
|
+
& > Rule.-vertical {
|
227
|
+
margin: 0;
|
228
|
+
color: $block-cursor-blurred-background;
|
229
|
+
}
|
230
|
+
}
|
231
|
+
"""
|
232
|
+
|
233
|
+
# Consolidate key binding data
|
234
|
+
BINDINGS: ClassVar[List[BindingType]] = [
|
235
|
+
Binding("enter", "select_cursor", "Select", show=False),
|
236
|
+
Binding("up", "cursor_up", "Cursor up", show=False),
|
237
|
+
Binding("down", "cursor_down", "Cursor down", show=False),
|
238
|
+
Binding("left", "cursor_left", "Cursor left", show=False),
|
239
|
+
Binding("right", "cursor_right", "Cursor right", show=False),
|
240
|
+
]
|
241
|
+
|
242
|
+
MAIN_CONTAINER_ID: ClassVar[str] = "dirs_content"
|
243
|
+
|
244
|
+
# Direction constants for better readability
|
245
|
+
DIRECTION_UP: Literal["up"] = "up"
|
246
|
+
DIRECTION_DOWN: Literal["down"] = "down"
|
247
|
+
|
248
|
+
can_focus = True
|
249
|
+
active_path = reactive(None)
|
250
|
+
|
251
|
+
@dataclass
|
252
|
+
class ActivePathChanged(Message):
|
253
|
+
path: Path | None
|
254
|
+
|
255
|
+
@dataclass
|
256
|
+
class ActivePathFileDoubleClicked(Message):
|
257
|
+
path: Path
|
258
|
+
|
259
|
+
def __init__(
|
260
|
+
self, work_dir: Path, file_system_service: FileSystemService, *args, **kwargs
|
261
|
+
):
|
262
|
+
"""
|
263
|
+
Initialize a FileSystemNavigator with a working directory and file system service.
|
264
|
+
|
265
|
+
Parameters:
|
266
|
+
work_dir (Path): The initial directory path to start file system navigation
|
267
|
+
file_system_service (FileSystemService): Service responsible for file system operations
|
268
|
+
*args: Variable positional arguments for parent class initialization
|
269
|
+
**kwargs: Variable keyword arguments for parent class initialization
|
270
|
+
|
271
|
+
Attributes:
|
272
|
+
work_dir (Path): The current working directory
|
273
|
+
file_system_service (object): Service for performing file system operations
|
274
|
+
path_listing_containers_uuids (dict): Mapping of path containers to their unique identifiers
|
275
|
+
focus_path (Path): The currently focused path, initially set to the working directory
|
276
|
+
"""
|
277
|
+
self.work_dir = work_dir
|
278
|
+
self.file_system_service = file_system_service
|
279
|
+
self.path_listing_containers_uuids = {}
|
280
|
+
self.focus_path = work_dir
|
281
|
+
super().__init__(*args, **kwargs)
|
282
|
+
|
283
|
+
async def on_mount(self, _: events.Mount) -> None:
|
284
|
+
"""
|
285
|
+
Mount the initial path listing container for the current working directory.
|
286
|
+
|
287
|
+
This asynchronous method is called when the FileSystemNavigator is mounted. It retrieves the path listing
|
288
|
+
container for the current working directory and mounts it without a divider. The container's UUID is stored in
|
289
|
+
the path_listing_containers_uuids dictionary for tracking and future reference.
|
290
|
+
|
291
|
+
If no path listing container can be created for the current working directory, the method silently returns.
|
292
|
+
|
293
|
+
Args:
|
294
|
+
_ (events.Mount): The mount event (unused in this method)
|
295
|
+
|
296
|
+
Side Effects:
|
297
|
+
- Mounts a path listing container for the current working directory
|
298
|
+
- Stores the container's UUID in path_listing_containers_uuids
|
299
|
+
"""
|
300
|
+
path_listing_container = self._get_path_listing_container(self.work_dir)
|
301
|
+
if not path_listing_container:
|
302
|
+
return
|
303
|
+
|
304
|
+
await self._mount_path_listing_container(
|
305
|
+
path_listing_container, mount_divider=False
|
306
|
+
)
|
307
|
+
self._focus_first_child(path_listing_container)
|
308
|
+
|
309
|
+
self.path_listing_containers_uuids[str(self.work_dir)] = (
|
310
|
+
path_listing_container.id
|
311
|
+
)
|
312
|
+
|
313
|
+
def action_cursor_down(self) -> None:
|
314
|
+
"""
|
315
|
+
Move the cursor down to the next item in the current path listing container.
|
316
|
+
|
317
|
+
This method navigates downward through the children of the currently focused path listing container. If a next
|
318
|
+
focusable element is found, it receives focus.
|
319
|
+
|
320
|
+
Behavior:
|
321
|
+
- If no path listing container is currently focused, the method returns without action
|
322
|
+
- Searches for the next focusable element below the current focused item
|
323
|
+
- If no next element is found, the method returns without action
|
324
|
+
- Focuses the next available element when found
|
325
|
+
|
326
|
+
Returns:
|
327
|
+
None
|
328
|
+
"""
|
329
|
+
path_listing_container = self._get_focused_path_listing_container()
|
330
|
+
if not path_listing_container:
|
331
|
+
return
|
332
|
+
|
333
|
+
next_focus = self._get_next_element(
|
334
|
+
path_listing_container.children, self.DIRECTION_DOWN, self._has_focus
|
335
|
+
)
|
336
|
+
if not next_focus:
|
337
|
+
return
|
338
|
+
next_focus.focus()
|
339
|
+
|
340
|
+
def action_cursor_up(self) -> None:
|
341
|
+
"""
|
342
|
+
Navigate to the previous item in the current path listing container.
|
343
|
+
|
344
|
+
This method moves the focus upward within the currently focused path listing container.
|
345
|
+
If no container is focused or no previous item exists, the method does nothing.
|
346
|
+
|
347
|
+
Raises:
|
348
|
+
No explicit exceptions are raised.
|
349
|
+
|
350
|
+
Side Effects:
|
351
|
+
- Changes the focused widget to the previous item in the container
|
352
|
+
- Calls the `focus()` method on the selected widget
|
353
|
+
|
354
|
+
Example:
|
355
|
+
# When multiple items exist in a path listing container
|
356
|
+
# Pressing the up arrow key will move focus to the previous item
|
357
|
+
"""
|
358
|
+
focused_container = self._get_focused_path_listing_container()
|
359
|
+
if not focused_container:
|
360
|
+
return
|
361
|
+
|
362
|
+
next_focus = self._get_next_element(
|
363
|
+
focused_container.children, self.DIRECTION_UP, self._has_focus
|
364
|
+
)
|
365
|
+
if not next_focus:
|
366
|
+
return
|
367
|
+
next_focus.focus()
|
368
|
+
|
369
|
+
def action_cursor_left(self) -> None:
|
370
|
+
"""
|
371
|
+
Navigate to the parent directory's container and focus on the first child.
|
372
|
+
|
373
|
+
This method handles left cursor navigation in the file system navigator. If the current focus is at the root
|
374
|
+
working directory, no action is taken. Otherwise, it attempts to find and focus on the first child in the
|
375
|
+
parent directory's container.
|
376
|
+
|
377
|
+
Parameters:
|
378
|
+
None
|
379
|
+
|
380
|
+
Returns:
|
381
|
+
None
|
382
|
+
|
383
|
+
Raises:
|
384
|
+
NoMatches: If the parent directory's container cannot be found in the query.
|
385
|
+
|
386
|
+
Notes:
|
387
|
+
- Skips navigation if the current focus is at the root working directory
|
388
|
+
- Retrieves the UUID of the parent directory's container
|
389
|
+
- Focuses on the first child of the parent directory's container
|
390
|
+
"""
|
391
|
+
if self.focus_path == self.work_dir:
|
392
|
+
return
|
393
|
+
active_path_container_uuid = self.path_listing_containers_uuids.get(
|
394
|
+
str(self.focus_path.parent)
|
395
|
+
)
|
396
|
+
if not active_path_container_uuid:
|
397
|
+
return
|
398
|
+
|
399
|
+
try:
|
400
|
+
active_path_container = self.query_one(f"#{active_path_container_uuid}")
|
401
|
+
except NoMatches:
|
402
|
+
return
|
403
|
+
|
404
|
+
self._focus_first_child(active_path_container)
|
405
|
+
|
406
|
+
def action_cursor_right(self) -> None:
|
407
|
+
"""
|
408
|
+
Navigate to the next directory column to the right in the file system navigator.
|
409
|
+
|
410
|
+
This method handles cursor movement to the right within the file system navigation interface.
|
411
|
+
It performs the following steps:
|
412
|
+
- Retrieves the UUID of the currently focused path's container
|
413
|
+
- If no container is found for the current path, exits early
|
414
|
+
- Finds the next directory column to the right using the `_get_next_element` method
|
415
|
+
- If a next column is found, focuses on the first child of that column
|
416
|
+
|
417
|
+
Parameters:
|
418
|
+
self (FileSystemNavigator): The current file system navigator instance
|
419
|
+
|
420
|
+
Returns:
|
421
|
+
None: Moves the cursor focus without returning a value
|
422
|
+
"""
|
423
|
+
active_path_container_uuid = self.path_listing_containers_uuids.get(
|
424
|
+
str(self.focus_path)
|
425
|
+
)
|
426
|
+
|
427
|
+
if not active_path_container_uuid:
|
428
|
+
return
|
429
|
+
|
430
|
+
next_dir_column = self._get_next_element(
|
431
|
+
self.children,
|
432
|
+
"down",
|
433
|
+
lambda x: x.id == active_path_container_uuid,
|
434
|
+
of_type=PathListingContainer,
|
435
|
+
)
|
436
|
+
if not next_dir_column:
|
437
|
+
return
|
438
|
+
self._focus_first_child(next_dir_column)
|
439
|
+
|
440
|
+
def _get_focused_path_listing_container(self) -> PathListingContainer | None:
|
441
|
+
"""
|
442
|
+
Retrieve the currently focused path listing container based on the current focus path.
|
443
|
+
|
444
|
+
This method checks if the current focus path has an associated container UUID in the
|
445
|
+
path_listing_containers_uuids mapping. If found, it retrieves and returns the corresponding
|
446
|
+
PathListingContainer instance.
|
447
|
+
|
448
|
+
Returns:
|
449
|
+
PathListingContainer or None: The container associated with the current focus path,
|
450
|
+
or None if no matching container is found.
|
451
|
+
"""
|
452
|
+
focus_path = str(self.focus_path)
|
453
|
+
if focus_path not in self.path_listing_containers_uuids:
|
454
|
+
return
|
455
|
+
container_uuid = self.path_listing_containers_uuids[focus_path]
|
456
|
+
return self._get_container_by_uuid(container_uuid)
|
457
|
+
|
458
|
+
def _get_container_by_uuid(
|
459
|
+
self, container_uuid: str
|
460
|
+
) -> PathListingContainer | None:
|
461
|
+
"""
|
462
|
+
Safely retrieve a PathListingContainer by its unique identifier.
|
463
|
+
|
464
|
+
This method attempts to find a PathListingContainer widget within the current widget tree using its UUID. If
|
465
|
+
no matching container is found, it returns None instead of raising an exception.
|
466
|
+
|
467
|
+
Parameters:
|
468
|
+
container_uuid (str): The unique identifier of the container to retrieve.
|
469
|
+
|
470
|
+
Returns:
|
471
|
+
PathListingContainer | None: The container with the specified UUID, or None if no matching container exists.
|
472
|
+
|
473
|
+
Raises:
|
474
|
+
NoMatches: Silently caught internally if no widget matches the UUID.
|
475
|
+
"""
|
476
|
+
try:
|
477
|
+
return self.query_one(f"#{container_uuid}") # type: ignore
|
478
|
+
except NoMatches:
|
479
|
+
return None
|
480
|
+
|
481
|
+
def _get_path_listing_container(self, path: Path) -> PathListingContainer | None:
|
482
|
+
"""
|
483
|
+
Create a path listing container for a given directory path.
|
484
|
+
|
485
|
+
Attempts to list directory contents using the file system service. If successful and the directory is not empty,
|
486
|
+
returns a PathListingContainer with folder and file widgets.
|
487
|
+
|
488
|
+
Parameters:
|
489
|
+
path (Path): The directory path to list contents for
|
490
|
+
|
491
|
+
Returns:
|
492
|
+
PathListingContainer | None: A container with folder and file widgets, or None if directory is empty or
|
493
|
+
listing fails
|
494
|
+
|
495
|
+
Raises:
|
496
|
+
Notifies user via self.notify() if directory listing encounters an error
|
497
|
+
"""
|
498
|
+
try:
|
499
|
+
dir_list = self.file_system_service.list_dir(path, relative_paths=False)
|
500
|
+
except ListDirException as e:
|
501
|
+
self.notify(str(e), markup=False)
|
502
|
+
return
|
503
|
+
|
504
|
+
if not dir_list.files and not dir_list.directories:
|
505
|
+
return
|
506
|
+
|
507
|
+
return PathListingContainer(
|
508
|
+
*self.create_folder_widgets(dir_list.directories),
|
509
|
+
*self.create_file_widgets(dir_list.files),
|
510
|
+
id=generate_uuid(),
|
511
|
+
classes="dir_listing",
|
512
|
+
name=str(path),
|
513
|
+
)
|
514
|
+
|
515
|
+
async def _mount_path_listing_container(
|
516
|
+
self, path_listing_container, mount_divider=True
|
517
|
+
):
|
518
|
+
"""
|
519
|
+
Mount a new path listing container with an optional vertical divider.
|
520
|
+
|
521
|
+
This asynchronous method mounts a path listing container into the current navigator,
|
522
|
+
optionally adding a vertical divider before the container. After mounting, it focuses
|
523
|
+
on the first child of the newly mounted container.
|
524
|
+
|
525
|
+
Parameters:
|
526
|
+
path_listing_container (PathListingContainer): The container to be mounted.
|
527
|
+
mount_divider (bool, optional): Whether to add a vertical divider before the container.
|
528
|
+
Defaults to True.
|
529
|
+
|
530
|
+
Notes:
|
531
|
+
- Uses asynchronous mounting to integrate with Textual's async UI framework
|
532
|
+
- Automatically focuses the first child of the mounted container
|
533
|
+
- Generates a unique divider ID based on the container's ID when a divider is mounted
|
534
|
+
"""
|
535
|
+
if mount_divider:
|
536
|
+
divider = Rule(
|
537
|
+
orientation="vertical", id=f"{path_listing_container.id}-divider"
|
538
|
+
)
|
539
|
+
await self.mount(divider)
|
540
|
+
|
541
|
+
await self.mount(path_listing_container)
|
542
|
+
|
543
|
+
async def _clean_up_outdated_path_listing_containers(self, path):
|
544
|
+
"""
|
545
|
+
Clean up path listing containers that are no longer part of the current navigation path.
|
546
|
+
|
547
|
+
This asynchronous method removes UI containers and tracking entries for directories
|
548
|
+
that are not parent directories of the current path. It helps manage memory and
|
549
|
+
UI complexity by removing unnecessary navigation containers.
|
550
|
+
|
551
|
+
Parameters:
|
552
|
+
path (Path): The current active path used to determine which containers are outdated.
|
553
|
+
|
554
|
+
Side Effects:
|
555
|
+
- Removes child elements from the DOM for outdated containers
|
556
|
+
- Removes entries from the path_listing_containers_uuids dictionary
|
557
|
+
"""
|
558
|
+
outdated_containers = [
|
559
|
+
(uuid, dir_name)
|
560
|
+
for dir_name, uuid in self.path_listing_containers_uuids.items()
|
561
|
+
if Path(dir_name) not in path.parents
|
562
|
+
]
|
563
|
+
|
564
|
+
for uuid, dir_name in outdated_containers:
|
565
|
+
await self.remove_children(f"#{uuid}")
|
566
|
+
await self.remove_children(f"#{uuid}-divider")
|
567
|
+
del self.path_listing_containers_uuids[dir_name]
|
568
|
+
|
569
|
+
def _get_main_container(self):
|
570
|
+
"""
|
571
|
+
Returns the main container for the current widget.
|
572
|
+
|
573
|
+
This method is typically used to retrieve the primary container of the widget,
|
574
|
+
which in this case is the widget itself. It provides a simple way to access
|
575
|
+
the root container for further manipulation or reference.
|
576
|
+
|
577
|
+
Returns:
|
578
|
+
Widget: The current widget instance, serving as its own main container.
|
579
|
+
"""
|
580
|
+
return self
|
581
|
+
|
582
|
+
def watch_active_path(self):
|
583
|
+
"""
|
584
|
+
Reactively post an ActivePathChanged message when the active path is updated.
|
585
|
+
|
586
|
+
This method is a reactive watcher that sends a message with the current active path whenever it changes.
|
587
|
+
It allows other components to be notified and respond to path navigation events.
|
588
|
+
|
589
|
+
Attributes:
|
590
|
+
active_path (Path): The currently selected path in the file system navigator.
|
591
|
+
|
592
|
+
Emits:
|
593
|
+
ActivePathChanged: A message containing the updated active path.
|
594
|
+
"""
|
595
|
+
self.post_message(self.ActivePathChanged(self.active_path))
|
596
|
+
|
597
|
+
@on(FileSystemWidget.FolderClick)
|
598
|
+
async def on_folder_click(self, event: FileSystemWidget.FolderClick):
|
599
|
+
"""
|
600
|
+
Handle a folder click event in the file system navigator.
|
601
|
+
|
602
|
+
This asynchronous method is triggered when a user clicks on a folder, managing the navigation
|
603
|
+
and display of the folder's contents. It performs the following key actions:
|
604
|
+
- Retrieves the path listing container for the clicked folder
|
605
|
+
- Cleans up any outdated path listing containers
|
606
|
+
- Mounts the new path listing container
|
607
|
+
- Tracks the UUID of the newly mounted container
|
608
|
+
|
609
|
+
Args:
|
610
|
+
event (FileSystemWidget.FolderClick): The folder click event containing the folder path
|
611
|
+
|
612
|
+
Side Effects:
|
613
|
+
- Mounts a new path listing container for the clicked folder
|
614
|
+
- Removes outdated path listing containers
|
615
|
+
- Updates the path_listing_containers_uuids dictionary
|
616
|
+
|
617
|
+
Raises:
|
618
|
+
Any exceptions from _clean_up_outdated_path_listing_containers or _mount_path_listing_container
|
619
|
+
"""
|
620
|
+
folder_path = event.name
|
621
|
+
path_listing_container = self._get_path_listing_container(folder_path)
|
622
|
+
if not path_listing_container:
|
623
|
+
return
|
624
|
+
|
625
|
+
await self._clean_up_outdated_path_listing_containers(folder_path)
|
626
|
+
await self._mount_path_listing_container(path_listing_container)
|
627
|
+
|
628
|
+
if str(folder_path) not in self.path_listing_containers_uuids:
|
629
|
+
self.path_listing_containers_uuids[str(folder_path)] = (
|
630
|
+
path_listing_container.id
|
631
|
+
)
|
632
|
+
|
633
|
+
@on(FileSystemWidget.Focus)
|
634
|
+
def on_folder_focus(self, event: FileSystemWidget.Focus):
|
635
|
+
"""
|
636
|
+
Handle focus event for a folder in the file system navigator.
|
637
|
+
|
638
|
+
This method updates the navigator's focus and active path when a folder widget receives focus.
|
639
|
+
|
640
|
+
Parameters:
|
641
|
+
event (FileSystemWidget.Focus): Focus event containing the path of the focused folder widget
|
642
|
+
|
643
|
+
Side Effects:
|
644
|
+
- Sets `self.focus_path` to the parent directory of the focused folder
|
645
|
+
- Sets `self.active_path` to the path of the focused folder
|
646
|
+
"""
|
647
|
+
self.focus_path = event.name.parent
|
648
|
+
self.active_path = event.name
|
649
|
+
|
650
|
+
@on(FileSystemWidget.FileDoubleClick)
|
651
|
+
def on_file_doubleclick(self, event: FileSystemWidget.FileDoubleClick):
|
652
|
+
"""
|
653
|
+
Handle a file double-click event by posting an ActivePathFileDoubleClicked message.
|
654
|
+
|
655
|
+
This method is triggered when a user double-clicks on a file in the file system navigator. It forwards the file
|
656
|
+
name to the parent component through a custom message.
|
657
|
+
|
658
|
+
Parameters:
|
659
|
+
event (FileSystemWidget.FileDoubleClick): The file double-click event containing the name of the clicked
|
660
|
+
file.
|
661
|
+
|
662
|
+
Raises:
|
663
|
+
No explicit exceptions are raised by this method.
|
664
|
+
"""
|
665
|
+
self.post_message(self.ActivePathFileDoubleClicked(event.name))
|
666
|
+
|
667
|
+
@on(FileSystemWidget.FolderDoubleClick)
|
668
|
+
def on_folder_doubleclick(self, event: FileSystemWidget.FolderDoubleClick):
|
669
|
+
"""
|
670
|
+
Handle a folder double-click event by posting an ActivePathChanged message.
|
671
|
+
|
672
|
+
This method is triggered when a user double-clicks on a folder in the file system navigator. It forwards the
|
673
|
+
folder name to the parent component through a custom message.
|
674
|
+
|
675
|
+
Parameters:
|
676
|
+
event (FileSystemWidget.FolderDoubleClick): The folder double-click event containing the name of the
|
677
|
+
clicked folder.
|
678
|
+
|
679
|
+
Raises:
|
680
|
+
No explicit exceptions are raised by this method.
|
681
|
+
"""
|
682
|
+
self.post_message(self.ActivePathFileDoubleClicked(event.name))
|
683
|
+
|
684
|
+
@staticmethod
|
685
|
+
def _get_next_element(
|
686
|
+
elements: Sequence[Widget],
|
687
|
+
direction: Literal["up", "down"],
|
688
|
+
selector: Callable,
|
689
|
+
of_type: Type | None = None,
|
690
|
+
) -> Widget | None:
|
691
|
+
"""
|
692
|
+
Determine the next element in a sequence based on navigation direction and selection criteria.
|
693
|
+
|
694
|
+
This method helps with navigating through a sequence of widgets, supporting circular navigation
|
695
|
+
and optional type filtering.
|
696
|
+
|
697
|
+
Parameters:
|
698
|
+
elements (Sequence[Widget]): A sequence of widgets to navigate through.
|
699
|
+
direction (Literal["up", "down"]): The navigation direction, either upward or downward.
|
700
|
+
selector (Callable): A function to identify the currently focused element.
|
701
|
+
of_type (Type, optional): A specific widget type to filter the elements. Defaults to None.
|
702
|
+
|
703
|
+
Returns:
|
704
|
+
Widget | None: The next widget in the sequence, or None if no valid elements exist.
|
705
|
+
If no element is currently focused, returns the first element.
|
706
|
+
Supports circular navigation, wrapping around to the start/end of the sequence.
|
707
|
+
|
708
|
+
Behavior:
|
709
|
+
- If no elements are provided, returns None
|
710
|
+
- If type filtering is specified, filters elements by the given type
|
711
|
+
- Finds the currently focused element using the provided selector
|
712
|
+
- Determines the next element based on the navigation direction
|
713
|
+
- Implements circular navigation (wraps around when reaching sequence boundaries)
|
714
|
+
"""
|
715
|
+
if not elements:
|
716
|
+
return
|
717
|
+
|
718
|
+
if of_type:
|
719
|
+
elements = [i for i in elements if isinstance(i, of_type)]
|
720
|
+
|
721
|
+
if not elements:
|
722
|
+
return
|
723
|
+
|
724
|
+
focused_children = [
|
725
|
+
index for index, child in enumerate(elements) if selector(child)
|
726
|
+
]
|
727
|
+
if not focused_children:
|
728
|
+
return elements[0]
|
729
|
+
|
730
|
+
focused_index = focused_children[0]
|
731
|
+
|
732
|
+
if direction == "down":
|
733
|
+
focused_index += 1
|
734
|
+
if focused_index == len(elements):
|
735
|
+
focused_index = 0
|
736
|
+
elif direction == "up":
|
737
|
+
if focused_index == 0:
|
738
|
+
focused_index = len(elements)
|
739
|
+
focused_index -= 1
|
740
|
+
return elements[focused_index]
|
741
|
+
|
742
|
+
@staticmethod
|
743
|
+
def create_folder_widgets(folders: list[Path]) -> list[FileSystemWidget]:
|
744
|
+
"""
|
745
|
+
Create a list of FileSystemWidget instances representing folders.
|
746
|
+
|
747
|
+
Parameters:
|
748
|
+
folders (list[Path]): A list of directory paths to convert into widgets.
|
749
|
+
|
750
|
+
Returns:
|
751
|
+
list[FileSystemWidget]: A list of FileSystemWidget instances, each representing a folder
|
752
|
+
with a predefined directory icon and CSS class for styling.
|
753
|
+
"""
|
754
|
+
return [
|
755
|
+
FileSystemWidget(
|
756
|
+
folder,
|
757
|
+
icon=DIRECTORY_ICON,
|
758
|
+
classes=FileSystemNavigatorClasses.DIRECTORY_LISTING_FOLDER.value,
|
759
|
+
)
|
760
|
+
for folder in folders
|
761
|
+
]
|
762
|
+
|
763
|
+
@staticmethod
|
764
|
+
def create_file_widgets(files: list[Path]) -> list[FileSystemWidget]:
|
765
|
+
"""
|
766
|
+
Create a list of FileSystemWidget instances for the given files.
|
767
|
+
|
768
|
+
Parameters:
|
769
|
+
files (list[Path]): A list of file paths to convert into FileSystemWidget instances.
|
770
|
+
|
771
|
+
Returns:
|
772
|
+
list[FileSystemWidget]: A list of FileSystemWidget objects representing the input files,
|
773
|
+
each configured with a file icon and the appropriate CSS class for file listing.
|
774
|
+
"""
|
775
|
+
return [
|
776
|
+
FileSystemWidget(
|
777
|
+
file,
|
778
|
+
icon=FILE_ICON,
|
779
|
+
classes=FileSystemNavigatorClasses.DIRECTORY_LISTING_FILE.value,
|
780
|
+
)
|
781
|
+
for file in files
|
782
|
+
]
|
783
|
+
|
784
|
+
@staticmethod
|
785
|
+
def _has_focus(widget: Widget) -> bool:
|
786
|
+
"""
|
787
|
+
Check if a given Textual widget currently has focus.
|
788
|
+
|
789
|
+
Parameters:
|
790
|
+
widget (Widget): The Textual widget to check for focus status.
|
791
|
+
|
792
|
+
Returns:
|
793
|
+
bool: True if the widget has focus, False otherwise.
|
794
|
+
"""
|
795
|
+
return widget.has_focus
|
796
|
+
|
797
|
+
@staticmethod
|
798
|
+
def _focus_first_child(container):
|
799
|
+
"""
|
800
|
+
Focus the first child of a given container.
|
801
|
+
|
802
|
+
If the container has no children, this method does nothing. Otherwise, it sets focus
|
803
|
+
to the first child widget in the container.
|
804
|
+
|
805
|
+
Args:
|
806
|
+
container (Widget): The container whose first child should receive focus.
|
807
|
+
"""
|
808
|
+
if not container.children:
|
809
|
+
return
|
810
|
+
container.children[0].focus()
|