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.
Files changed (95) hide show
  1. data_sourcerer-0.1.0.dist-info/METADATA +52 -0
  2. data_sourcerer-0.1.0.dist-info/RECORD +95 -0
  3. data_sourcerer-0.1.0.dist-info/WHEEL +5 -0
  4. data_sourcerer-0.1.0.dist-info/entry_points.txt +2 -0
  5. data_sourcerer-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. data_sourcerer-0.1.0.dist-info/top_level.txt +1 -0
  7. sourcerer/__init__.py +15 -0
  8. sourcerer/domain/__init__.py +13 -0
  9. sourcerer/domain/access_credentials/__init__.py +7 -0
  10. sourcerer/domain/access_credentials/entities.py +53 -0
  11. sourcerer/domain/access_credentials/exceptions.py +17 -0
  12. sourcerer/domain/access_credentials/repositories.py +84 -0
  13. sourcerer/domain/access_credentials/services.py +91 -0
  14. sourcerer/domain/file_system/__init__.py +7 -0
  15. sourcerer/domain/file_system/entities.py +70 -0
  16. sourcerer/domain/file_system/exceptions.py +17 -0
  17. sourcerer/domain/file_system/services.py +64 -0
  18. sourcerer/domain/shared/__init__.py +6 -0
  19. sourcerer/domain/shared/entities.py +18 -0
  20. sourcerer/domain/storage_provider/__init__.py +7 -0
  21. sourcerer/domain/storage_provider/entities.py +86 -0
  22. sourcerer/domain/storage_provider/exceptions.py +17 -0
  23. sourcerer/domain/storage_provider/services.py +130 -0
  24. sourcerer/infrastructure/__init__.py +13 -0
  25. sourcerer/infrastructure/access_credentials/__init__.py +7 -0
  26. sourcerer/infrastructure/access_credentials/exceptions.py +16 -0
  27. sourcerer/infrastructure/access_credentials/registry.py +120 -0
  28. sourcerer/infrastructure/access_credentials/repositories.py +119 -0
  29. sourcerer/infrastructure/access_credentials/services.py +396 -0
  30. sourcerer/infrastructure/db/__init__.py +6 -0
  31. sourcerer/infrastructure/db/config.py +73 -0
  32. sourcerer/infrastructure/db/models.py +47 -0
  33. sourcerer/infrastructure/file_system/__init__.py +7 -0
  34. sourcerer/infrastructure/file_system/exceptions.py +89 -0
  35. sourcerer/infrastructure/file_system/services.py +147 -0
  36. sourcerer/infrastructure/storage_provider/__init__.py +7 -0
  37. sourcerer/infrastructure/storage_provider/exceptions.py +78 -0
  38. sourcerer/infrastructure/storage_provider/registry.py +84 -0
  39. sourcerer/infrastructure/storage_provider/services.py +509 -0
  40. sourcerer/infrastructure/utils.py +106 -0
  41. sourcerer/presentation/__init__.py +12 -0
  42. sourcerer/presentation/app.py +36 -0
  43. sourcerer/presentation/di_container.py +46 -0
  44. sourcerer/presentation/screens/__init__.py +0 -0
  45. sourcerer/presentation/screens/critical_error/__init__.py +0 -0
  46. sourcerer/presentation/screens/critical_error/main.py +78 -0
  47. sourcerer/presentation/screens/critical_error/styles.tcss +41 -0
  48. sourcerer/presentation/screens/file_system_finder/main.py +248 -0
  49. sourcerer/presentation/screens/file_system_finder/styles.tcss +44 -0
  50. sourcerer/presentation/screens/file_system_finder/widgets/__init__.py +0 -0
  51. sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +810 -0
  52. sourcerer/presentation/screens/main/__init__.py +0 -0
  53. sourcerer/presentation/screens/main/main.py +469 -0
  54. sourcerer/presentation/screens/main/messages/__init__.py +0 -0
  55. sourcerer/presentation/screens/main/messages/delete_request.py +12 -0
  56. sourcerer/presentation/screens/main/messages/download_request.py +12 -0
  57. sourcerer/presentation/screens/main/messages/preview_request.py +10 -0
  58. sourcerer/presentation/screens/main/messages/resizing_rule.py +21 -0
  59. sourcerer/presentation/screens/main/messages/select_storage_item.py +11 -0
  60. sourcerer/presentation/screens/main/messages/uncheck_files_request.py +8 -0
  61. sourcerer/presentation/screens/main/messages/upload_request.py +10 -0
  62. sourcerer/presentation/screens/main/mixins/__init__.py +0 -0
  63. sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +144 -0
  64. sourcerer/presentation/screens/main/styles.tcss +32 -0
  65. sourcerer/presentation/screens/main/widgets/__init__.py +0 -0
  66. sourcerer/presentation/screens/main/widgets/gradient.py +45 -0
  67. sourcerer/presentation/screens/main/widgets/resizing_rule.py +67 -0
  68. sourcerer/presentation/screens/main/widgets/storage_content.py +691 -0
  69. sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +143 -0
  70. sourcerer/presentation/screens/preview_content/__init__.py +0 -0
  71. sourcerer/presentation/screens/preview_content/main.py +59 -0
  72. sourcerer/presentation/screens/preview_content/styles.tcss +26 -0
  73. sourcerer/presentation/screens/provider_creds_list/__init__.py +0 -0
  74. sourcerer/presentation/screens/provider_creds_list/main.py +164 -0
  75. sourcerer/presentation/screens/provider_creds_list/styles.tcss +65 -0
  76. sourcerer/presentation/screens/provider_creds_registration/__init__.py +0 -0
  77. sourcerer/presentation/screens/provider_creds_registration/main.py +264 -0
  78. sourcerer/presentation/screens/provider_creds_registration/styles.tcss +42 -0
  79. sourcerer/presentation/screens/question/__init__.py +0 -0
  80. sourcerer/presentation/screens/question/main.py +31 -0
  81. sourcerer/presentation/screens/question/styles.tcss +33 -0
  82. sourcerer/presentation/screens/shared/__init__.py +0 -0
  83. sourcerer/presentation/screens/shared/containers.py +13 -0
  84. sourcerer/presentation/screens/shared/widgets/__init__.py +0 -0
  85. sourcerer/presentation/screens/shared/widgets/button.py +54 -0
  86. sourcerer/presentation/screens/shared/widgets/labeled_input.py +80 -0
  87. sourcerer/presentation/screens/storage_action_progress/__init__.py +0 -0
  88. sourcerer/presentation/screens/storage_action_progress/main.py +476 -0
  89. sourcerer/presentation/screens/storage_action_progress/styles.tcss +43 -0
  90. sourcerer/presentation/settings.py +31 -0
  91. sourcerer/presentation/themes/__init__.py +0 -0
  92. sourcerer/presentation/themes/github_dark.py +21 -0
  93. sourcerer/presentation/utils.py +69 -0
  94. sourcerer/settings.py +72 -0
  95. 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()