data-sourcerer 0.3.0__tar.gz → 0.4.0__tar.gz

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 (119) hide show
  1. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/PKG-INFO +3 -1
  2. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/README.md +2 -0
  3. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/pyproject.toml +1 -1
  4. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/__init__.py +1 -1
  5. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/storage_provider/entities.py +1 -1
  6. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage_provider/services/azure.py +1 -3
  7. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage_provider/services/gcp.py +1 -2
  8. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage_provider/services/s3.py +1 -2
  9. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/file_system_finder/main.py +3 -9
  10. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/main.py +27 -2
  11. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/preview_request.py +1 -0
  12. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/widgets/storage_content.py +10 -3
  13. data_sourcerer-0.4.0/sourcerer/presentation/screens/preview_content/main.py +269 -0
  14. data_sourcerer-0.4.0/sourcerer/presentation/screens/preview_content/styles.tcss +62 -0
  15. data_sourcerer-0.4.0/sourcerer/presentation/screens/preview_content/text_area_style.py +60 -0
  16. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/provider_creds_list/main.py +9 -5
  17. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/provider_creds_registration/main.py +3 -3
  18. data_sourcerer-0.4.0/sourcerer/presentation/screens/shared/modal_screens.py +37 -0
  19. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storages_list/main.py +9 -5
  20. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storages_registration/main.py +3 -3
  21. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/settings.py +2 -0
  22. data_sourcerer-0.3.0/sourcerer/presentation/screens/preview_content/main.py +0 -82
  23. data_sourcerer-0.3.0/sourcerer/presentation/screens/preview_content/styles.tcss +0 -27
  24. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/.gitignore +0 -0
  25. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/LICENSE +0 -0
  26. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/__init__.py +0 -0
  27. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/access_credentials/__init__.py +0 -0
  28. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/access_credentials/entities.py +0 -0
  29. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/access_credentials/exceptions.py +0 -0
  30. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/access_credentials/repositories.py +0 -0
  31. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/access_credentials/services.py +0 -0
  32. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/file_system/__init__.py +0 -0
  33. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/file_system/entities.py +0 -0
  34. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/file_system/exceptions.py +0 -0
  35. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/file_system/services.py +0 -0
  36. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/shared/__init__.py +0 -0
  37. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/shared/entities.py +0 -0
  38. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/storage/__init__.py +0 -0
  39. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/storage/entities.py +0 -0
  40. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/storage/repositories.py +0 -0
  41. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/storage_provider/__init__.py +0 -0
  42. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/storage_provider/exceptions.py +0 -0
  43. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/domain/storage_provider/services.py +0 -0
  44. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/__init__.py +0 -0
  45. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/access_credentials/__init__.py +0 -0
  46. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/access_credentials/exceptions.py +0 -0
  47. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/access_credentials/registry.py +0 -0
  48. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/access_credentials/repositories.py +0 -0
  49. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/access_credentials/services.py +0 -0
  50. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/db/__init__.py +0 -0
  51. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/db/config.py +0 -0
  52. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/db/models.py +0 -0
  53. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/file_system/__init__.py +0 -0
  54. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/file_system/exceptions.py +0 -0
  55. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/file_system/services.py +0 -0
  56. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage/__init__.py +0 -0
  57. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage/repositories.py +0 -0
  58. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage/services.py +0 -0
  59. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage_provider/__init__.py +0 -0
  60. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage_provider/exceptions.py +0 -0
  61. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage_provider/registry.py +0 -0
  62. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/storage_provider/services/__init__.py +0 -0
  63. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/infrastructure/utils.py +0 -0
  64. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/__init__.py +0 -0
  65. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/app.py +0 -0
  66. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/di_container.py +0 -0
  67. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/__init__.py +0 -0
  68. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/critical_error/__init__.py +0 -0
  69. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/critical_error/main.py +0 -0
  70. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/critical_error/styles.tcss +0 -0
  71. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/file_system_finder/styles.tcss +0 -0
  72. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/file_system_finder/widgets/__init__.py +0 -0
  73. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py +0 -0
  74. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/__init__.py +0 -0
  75. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/__init__.py +0 -0
  76. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/delete_request.py +0 -0
  77. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/download_request.py +0 -0
  78. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/refresh_storages_list_request.py +0 -0
  79. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/resizing_rule.py +0 -0
  80. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/select_storage_item.py +0 -0
  81. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/uncheck_files_request.py +0 -0
  82. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/messages/upload_request.py +0 -0
  83. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/mixins/__init__.py +0 -0
  84. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py +0 -0
  85. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/styles.tcss +0 -0
  86. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/widgets/__init__.py +0 -0
  87. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/widgets/gradient.py +0 -0
  88. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/widgets/resizing_rule.py +0 -0
  89. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +0 -0
  90. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/preview_content/__init__.py +0 -0
  91. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/provider_creds_list/__init__.py +0 -0
  92. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/provider_creds_list/messages/__init__.py +0 -0
  93. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/provider_creds_list/messages/reload_credentials_request.py +0 -0
  94. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/provider_creds_list/styles.tcss +0 -0
  95. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/provider_creds_registration/__init__.py +0 -0
  96. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/provider_creds_registration/styles.tcss +0 -0
  97. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/question/__init__.py +0 -0
  98. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/question/main.py +0 -0
  99. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/question/styles.tcss +0 -0
  100. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/shared/__init__.py +0 -0
  101. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/shared/containers.py +0 -0
  102. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/shared/widgets/__init__.py +0 -0
  103. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/shared/widgets/button.py +0 -0
  104. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/shared/widgets/labeled_input.py +0 -0
  105. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/shared/widgets/spinner.py +0 -0
  106. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storage_action_progress/__init__.py +0 -0
  107. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storage_action_progress/main.py +0 -0
  108. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storage_action_progress/styles.tcss +0 -0
  109. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storages_list/__init__.py +0 -0
  110. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storages_list/messages/__init__.py +0 -0
  111. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storages_list/messages/reload_storages_request.py +0 -0
  112. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storages_list/styles.tcss +0 -0
  113. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storages_registration/__init__.py +0 -0
  114. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/screens/storages_registration/styles.tcss +0 -0
  115. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/settings.py +0 -0
  116. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/themes/__init__.py +0 -0
  117. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/themes/github_dark.py +0 -0
  118. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/presentation/utils.py +0 -0
  119. {data_sourcerer-0.3.0 → data_sourcerer-0.4.0}/sourcerer/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-sourcerer
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Sourcerer is a terminal cloud storage navigator.
5
5
  Author-email: Bohdana Kuzmenko <bohdana.kuzmenko.dev@gmail.com>
6
6
  License: MIT
@@ -42,6 +42,8 @@ engineers to view and manage files across multiple cloud providers like
42
42
 
43
43
  > Your terminal. Your storages. Your control.
44
44
 
45
+ [Demo page](https://the-impact-craft.github.io/sourcerer/)
46
+
45
47
  ---
46
48
 
47
49
  ## ✨ Features
@@ -6,6 +6,8 @@ engineers to view and manage files across multiple cloud providers like
6
6
 
7
7
  > Your terminal. Your storages. Your control.
8
8
 
9
+ [Demo page](https://the-impact-craft.github.io/sourcerer/)
10
+
9
11
  ---
10
12
 
11
13
  ## ✨ Features
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
 
3
3
  name = "data-sourcerer"
4
- version = "0.3.0"
4
+ version = "0.4.0"
5
5
  description = "Sourcerer is a terminal cloud storage navigator."
6
6
  requires-python = ">=3.9"
7
7
 
@@ -12,4 +12,4 @@ The application is structured using a clean architecture approach with:
12
12
  - Presentation layer: User interface components
13
13
  """
14
14
 
15
- __version__ = "0.3.0"
15
+ __version__ = "0.4.0"
@@ -63,7 +63,7 @@ class File(Struct):
63
63
 
64
64
  uuid: str
65
65
  key: str
66
- size: str
66
+ size: int
67
67
  is_text: bool
68
68
  date_modified: datetime | None = None
69
69
 
@@ -10,7 +10,6 @@ from collections.abc import Callable
10
10
  from pathlib import Path
11
11
  from typing import Any
12
12
 
13
- import humanize
14
13
  from azure.mgmt.storage import StorageManagementClient
15
14
  from azure.storage.blob import BlobServiceClient
16
15
  from platformdirs import user_downloads_dir
@@ -38,7 +37,6 @@ from sourcerer.infrastructure.utils import generate_uuid, is_text_file
38
37
 
39
38
  @storage_provider(StorageProvider.AzureStorage)
40
39
  class AzureStorageProviderService(BaseStorageProviderService):
41
-
42
40
  def __init__(self, credentials: Any):
43
41
  """
44
42
  Initialize the service with Azure credentials.
@@ -137,7 +135,7 @@ class AzureStorageProviderService(BaseStorageProviderService):
137
135
  File(
138
136
  generate_uuid(),
139
137
  remaining_path,
140
- size=humanize.naturalsize(blob.size),
138
+ size=blob.size,
141
139
  date_modified=blob.last_modified,
142
140
  is_text=is_text_file(blob.name),
143
141
  )
@@ -9,7 +9,6 @@ from collections.abc import Callable
9
9
  from pathlib import Path
10
10
  from typing import Any
11
11
 
12
- import humanize
13
12
  from platformdirs import user_downloads_dir
14
13
 
15
14
  from sourcerer.domain.shared.entities import StorageProvider
@@ -136,7 +135,7 @@ class GCPStorageProviderService(BaseStorageProviderService):
136
135
  File(
137
136
  generate_uuid(),
138
137
  blob.name[len(path) :],
139
- size=humanize.naturalsize(blob.size),
138
+ size=blob.size,
140
139
  date_modified=blob.updated.date(),
141
140
  is_text=is_text_file(blob.name),
142
141
  )
@@ -10,7 +10,6 @@ from itertools import groupby
10
10
  from pathlib import Path
11
11
  from typing import Any
12
12
 
13
- import humanize
14
13
  from platformdirs import user_downloads_dir
15
14
 
16
15
  from sourcerer.domain.shared.entities import StorageProvider
@@ -173,7 +172,7 @@ class S3ProviderService(BaseStorageProviderService):
173
172
  File(
174
173
  generate_uuid(),
175
174
  i.get("Key").replace(path, ""),
176
- humanize.naturalsize(i.get("Size")),
175
+ i.get("Size"),
177
176
  is_text_file(i.get("Key")),
178
177
  i.get("LastModified"),
179
178
  )
@@ -1,16 +1,13 @@
1
1
  from collections.abc import Callable
2
2
  from dataclasses import dataclass
3
3
  from pathlib import Path
4
- from typing import ClassVar
5
4
 
6
5
  from dependency_injector.wiring import Provide
7
6
  from textual import on
8
7
  from textual.app import ComposeResult
9
- from textual.binding import Binding, BindingType
10
8
  from textual.containers import Container, Horizontal
11
9
  from textual.css.query import NoMatches
12
10
  from textual.reactive import reactive
13
- from textual.screen import ModalScreen
14
11
  from textual.widgets import Static
15
12
 
16
13
  from sourcerer.infrastructure.file_system.services import FileSystemService
@@ -18,6 +15,7 @@ from sourcerer.presentation.di_container import DiContainer
18
15
  from sourcerer.presentation.screens.file_system_finder.widgets.file_system_navigator import (
19
16
  FileSystemNavigator,
20
17
  )
18
+ from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
21
19
  from sourcerer.presentation.screens.shared.widgets.button import Button
22
20
 
23
21
 
@@ -27,14 +25,10 @@ class FileSystemSelectionValidationRule:
27
25
  error_message: str
28
26
 
29
27
 
30
- class FileSystemNavigationModal(ModalScreen):
28
+ class FileSystemNavigationModal(ExitBoundModalScreen):
31
29
  CONTAINER_ID = "file_system_view_container"
32
30
  CSS_PATH = "styles.tcss"
33
31
 
34
- BINDINGS: ClassVar[list[BindingType]] = [
35
- Binding("escape", "app.pop_screen", "Pop screen"),
36
- ]
37
-
38
32
  active_path: reactive[Path] = reactive(Path())
39
33
 
40
34
  def __init__(
@@ -126,7 +120,7 @@ class FileSystemNavigationModal(ModalScreen):
126
120
  event (Button.Click): The event containing the button that was clicked.
127
121
  """
128
122
  if event.action == "close":
129
- self.on_close()
123
+ self.action_cancel_screen()
130
124
  else:
131
125
  self.on_apply()
132
126
 
@@ -1,3 +1,4 @@
1
+ import contextlib
1
2
  import time
2
3
  import traceback
3
4
  from concurrent.futures import ThreadPoolExecutor
@@ -9,6 +10,7 @@ from textual import on, work
9
10
  from textual.app import App, ComposeResult
10
11
  from textual.binding import Binding, BindingType
11
12
  from textual.containers import Horizontal
13
+ from textual.css.query import NoMatches
12
14
  from textual.reactive import reactive
13
15
  from textual.widgets import Footer
14
16
 
@@ -102,6 +104,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
102
104
  BINDINGS: ClassVar[list[BindingType]] = [
103
105
  Binding("ctrl+r", "registrations", "Registrations list"),
104
106
  Binding("ctrl+s", "storages", "Storages list"),
107
+ Binding("ctrl+f", "find", show=False),
105
108
  Binding(
106
109
  KeyBindings.ARROW_LEFT.value, "focus_sidebar", "Focus sidebar", show=False
107
110
  ),
@@ -152,6 +155,13 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
152
155
  self.theme = "github-dark"
153
156
  self.init_storages_list()
154
157
 
158
+ def action_find(self):
159
+ """
160
+ Focus search input.
161
+ """
162
+ with contextlib.suppress(NoMatches):
163
+ self.query_one(f"#{self.storage_content.search_input_id}").focus()
164
+
155
165
  def action_focus_content(self):
156
166
  """
157
167
  Focuses the storage content container.
@@ -174,10 +184,23 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
174
184
  This method is typically used to allow users to add their
175
185
  cloud storage credentials, which will then be reflected in the storage
176
186
  """
177
- self.app.push_screen(ProviderCredsListScreen(), callback=self.refresh_storages)
187
+ self.app.push_screen(
188
+ ProviderCredsListScreen(), callback=self.modal_screen_callback
189
+ )
178
190
 
179
191
  def action_storages(self):
180
- self.app.push_screen(StoragesListScreen(), callback=self.refresh_storages)
192
+ self.app.push_screen(StoragesListScreen(), callback=self.modal_screen_callback)
193
+
194
+ def modal_screen_callback(self, requires_storage_refresh: bool | None = True):
195
+ """
196
+ Callback for modal screens to refresh the storage list if required.
197
+
198
+ This method is called when a modal screen is closed. If the
199
+ `requires_storage_refresh` flag is set to True, it refreshes the
200
+ storage list by calling the `refresh_storages` method.
201
+ """
202
+ if requires_storage_refresh:
203
+ self.refresh_storages()
181
204
 
182
205
  def refresh_storages(self, *args, **kwargs):
183
206
  """
@@ -187,6 +210,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
187
210
  configurations.
188
211
  """
189
212
  self.storage_list_sidebar.storages = {}
213
+ self.storage_list_sidebar.last_update_timestamp = time.time()
190
214
  self.init_storages_list()
191
215
 
192
216
  @work(thread=True)
@@ -384,6 +408,7 @@ class Sourcerer(App, ResizeContainersWatcherMixin):
384
408
  PreviewContentScreen(
385
409
  storage_name=event.storage_name,
386
410
  key=event.path,
411
+ file_size=event.size,
387
412
  access_credentials_uuid=event.access_credentials_uuid,
388
413
  )
389
414
  )
@@ -8,3 +8,4 @@ class PreviewRequest(Message):
8
8
  storage_name: str
9
9
  access_credentials_uuid: str
10
10
  path: str
11
+ size: int
@@ -12,6 +12,7 @@ from dataclasses import dataclass
12
12
  from enum import Enum, auto
13
13
  from typing import ClassVar, Self
14
14
 
15
+ import humanize
15
16
  from textual import events, on
16
17
  from textual.app import ComposeResult
17
18
  from textual.binding import Binding, BindingType
@@ -308,6 +309,7 @@ class FileItem(StorageContentItem):
308
309
  """Message sent when a file preview is selected."""
309
310
 
310
311
  name: str
312
+ size: int
311
313
 
312
314
  @dataclass
313
315
  class Unselect(Message):
@@ -333,7 +335,9 @@ class FileItem(StorageContentItem):
333
335
  yield FileMetaLabel(
334
336
  f"{FILE_ICON} {self.file.key}", classes="file_name", markup=False
335
337
  )
336
- yield FileMetaLabel(f"{self.file.size}", classes="file_size", markup=False)
338
+ yield FileMetaLabel(
339
+ f"{humanize.naturalsize(self.file.size)}", classes="file_size", markup=False
340
+ )
337
341
  yield FileMetaLabel(
338
342
  str(self.file.date_modified), classes="file_date", markup=False
339
343
  )
@@ -360,7 +364,7 @@ class FileItem(StorageContentItem):
360
364
  preview_button = self.query_one(Button)
361
365
 
362
366
  if widget is preview_button:
363
- self.post_message(self.Preview(self.file.key))
367
+ self.post_message(self.Preview(self.file.key, self.file.size))
364
368
  return
365
369
 
366
370
  checkbox = self.query_one(UnfocusableCheckbox)
@@ -561,6 +565,8 @@ class StorageContentContainer(Vertical):
561
565
  }
562
566
  """
563
567
 
568
+ search_input_id: ClassVar[str] = "search_input"
569
+
564
570
  def compose(self) -> ComposeResult:
565
571
  if not self.storage:
566
572
  return
@@ -584,7 +590,7 @@ class StorageContentContainer(Vertical):
584
590
  with Horizontal():
585
591
  yield Label("Search:")
586
592
  yield Input(
587
- id="search_input",
593
+ id=self.search_input_id,
588
594
  placeholder="input path prefix here...",
589
595
  value=self.search_prefix,
590
596
  )
@@ -663,6 +669,7 @@ class StorageContentContainer(Vertical):
663
669
  self.storage,
664
670
  self.access_credentials_uuid,
665
671
  os.path.join(self.path, event.name) if self.path else event.name,
672
+ event.size,
666
673
  )
667
674
  )
668
675
 
@@ -0,0 +1,269 @@
1
+ import contextlib
2
+ import re
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import ClassVar
6
+
7
+ import humanize
8
+ from dependency_injector.wiring import Provide
9
+ from rich.syntax import Syntax
10
+ from textual import events, on
11
+ from textual.app import ComposeResult
12
+ from textual.binding import Binding, BindingType
13
+ from textual.containers import Container, Horizontal
14
+ from textual.css.query import NoMatches
15
+ from textual.document._document import Selection
16
+ from textual.message import Message
17
+ from textual.reactive import reactive
18
+ from textual.widgets import Input, Label, LoadingIndicator, Rule, TextArea
19
+
20
+ from sourcerer.infrastructure.access_credentials.services import CredentialsService
21
+ from sourcerer.infrastructure.storage_provider.exceptions import (
22
+ ReadStorageItemsError,
23
+ )
24
+ from sourcerer.presentation.di_container import DiContainer
25
+ from sourcerer.presentation.screens.preview_content.text_area_style import (
26
+ SOURCERER_THEME_NAME,
27
+ sourcerer_text_area_theme,
28
+ )
29
+ from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
30
+ from sourcerer.presentation.screens.shared.widgets.button import Button
31
+ from sourcerer.presentation.utils import get_provider_service_by_access_uuid
32
+ from sourcerer.settings import PREVIEW_LENGTH_LIMIT, PREVIEW_LIMIT_SIZE
33
+
34
+
35
+ @dataclass
36
+ class HighlightResult(Message):
37
+ line: int
38
+ start: int
39
+ end: int
40
+
41
+
42
+ @dataclass
43
+ class HideSearchBar(Message):
44
+ pass
45
+
46
+
47
+ class ClickableLabel(Label):
48
+ @dataclass
49
+ class Click(Message):
50
+ name: str
51
+
52
+ def __init__(self, *args, **kwargs):
53
+ super().__init__(*args, **kwargs)
54
+
55
+ def on_click(self, _: events.Click) -> None:
56
+ self.post_message(self.Click(name=self.name)) # type: ignore
57
+
58
+
59
+ class Search(Container):
60
+ total = reactive(0, recompose=False)
61
+ current = reactive(0, recompose=False)
62
+ content = reactive("", recompose=False)
63
+
64
+ def __init__(self, *args, **kwargs):
65
+ super().__init__(*args, **kwargs)
66
+ self.search_result_lines = []
67
+ self.search_value = ""
68
+
69
+ def compose(self) -> ComposeResult:
70
+ with Horizontal():
71
+ with Horizontal(id="left"):
72
+ yield Label("Search:")
73
+ yield Input(placeholder="...")
74
+
75
+ with Horizontal(id="right"):
76
+ yield ClickableLabel(
77
+ "◀", id="previous", name="previous", classes="search-button"
78
+ )
79
+ yield Label(f"{self.current}/{self.total}", id="search-result")
80
+ yield ClickableLabel(
81
+ "▶", id="next", name="next", classes="search-button"
82
+ )
83
+ yield ClickableLabel(
84
+ "❌", id="hide", name="hide", classes="search-button"
85
+ )
86
+ yield Rule()
87
+
88
+ @on(Input.Submitted)
89
+ def on_input_submitted(self, event: Input.Submitted) -> None:
90
+ """Handle input submitted events."""
91
+ if not event.value or not self.content:
92
+ self.total = 0
93
+ self.current = 0
94
+ self.search_value = ""
95
+ self.search_result_lines = []
96
+ return
97
+ if event.value == self.search_value:
98
+ self._increment_current()
99
+ return
100
+
101
+ self.search_value = event.value
102
+ lines = self.content.split("\n")
103
+ search_pattern = event.value.lower()
104
+
105
+ self.search_result_lines = [
106
+ (line_n, index)
107
+ for line_n, line in enumerate(lines)
108
+ if search_pattern in line.lower()
109
+ for index in [
110
+ match.start()
111
+ for match in re.finditer(rf"(?i){re.escape(search_pattern)}", line)
112
+ ]
113
+ ]
114
+
115
+ if not self.search_result_lines:
116
+ self.notify("No matches found", severity="warning")
117
+ self.total, self.current = 0, 0
118
+ return
119
+
120
+ self.total = len(self.search_result_lines)
121
+ self.current = 1
122
+
123
+ @on(ClickableLabel.Click)
124
+ def on_click(self, event: ClickableLabel.Click) -> None:
125
+ if event.name == "next":
126
+ self._increment_current()
127
+ elif event.name == "previous":
128
+ self._decrement_current()
129
+ elif event.name == "hide":
130
+ self.post_message(HideSearchBar())
131
+
132
+ def _increment_current(self):
133
+ self.current = self.current + 1 if self.current < self.total else 1
134
+
135
+ def _decrement_current(self):
136
+ self.current = self.current - 1 if self.current > 1 else self.total
137
+
138
+ def watch_current(self):
139
+ with contextlib.suppress(NoMatches):
140
+ search_result = self.query_one("#search-result", Label)
141
+ search_result.update(f"{self.current}/{self.total}")
142
+ if not self.search_result_lines:
143
+ return
144
+ line, start = self.search_result_lines[self.current - 1]
145
+ self.post_message(
146
+ HighlightResult(line, start=start, end=start + len(self.search_value))
147
+ )
148
+
149
+
150
+ class PreviewContentScreen(ExitBoundModalScreen):
151
+ CSS_PATH = "styles.tcss"
152
+
153
+ BINDINGS: ClassVar[list[BindingType]] = [
154
+ Binding("escape", "cancel", "Close the screen"),
155
+ ]
156
+
157
+ def __init__(
158
+ self,
159
+ storage_name,
160
+ key,
161
+ file_size,
162
+ access_credentials_uuid,
163
+ *args,
164
+ credentials_service: CredentialsService = Provide[
165
+ DiContainer.credentials_repository
166
+ ],
167
+ **kwargs,
168
+ ):
169
+ super().__init__(*args, **kwargs)
170
+
171
+ self.storage_name = storage_name
172
+ self.key = key
173
+ self.file_size = file_size
174
+ self.access_credentials_uuid = access_credentials_uuid
175
+ self.credentials_service = credentials_service
176
+ self.content = None
177
+
178
+ def compose(self) -> ComposeResult:
179
+ with Container(id="PreviewContentScreen"):
180
+ yield Search(id="search-bar")
181
+ yield LoadingIndicator(id="loading")
182
+ yield TextArea(read_only=True, show_line_numbers=True)
183
+ with Horizontal(id="controls"):
184
+ yield Button("Close", name="cancel")
185
+
186
+ def on_mount(self) -> None:
187
+ """Called when the DOM is ready."""
188
+ search = self.query_one(Search)
189
+ text_log = self.query_one(TextArea)
190
+ text_log.register_theme(sourcerer_text_area_theme)
191
+ text_log.theme = SOURCERER_THEME_NAME
192
+
193
+ provider_service = get_provider_service_by_access_uuid(
194
+ self.access_credentials_uuid, self.credentials_service
195
+ )
196
+ if not provider_service:
197
+ self.notify("Could not read file :(", severity="error")
198
+ return
199
+ try:
200
+ self.content = provider_service.read_storage_item(
201
+ self.storage_name, self.key
202
+ )
203
+ if self.file_size > PREVIEW_LIMIT_SIZE:
204
+ self.content = self.content[:PREVIEW_LENGTH_LIMIT]
205
+ self.notify(
206
+ f"The file size {humanize.naturalsize(self.file_size)} "
207
+ f"exceeds {humanize.naturalsize(PREVIEW_LIMIT_SIZE)} preview limit. "
208
+ f"The content is truncated to {PREVIEW_LENGTH_LIMIT} characters.",
209
+ severity="warning",
210
+ )
211
+ search.content = self.content
212
+ except ReadStorageItemsError:
213
+ self.notify("Could not read file :(", severity="error")
214
+ return
215
+ self.query_one("#loading").remove()
216
+ if self.content is None:
217
+ self.notify("Empty file", severity="warning")
218
+ return
219
+
220
+ extension = Path(self.key).suffix
221
+
222
+ lexer = (
223
+ "json"
224
+ if extension == ".tfstate"
225
+ else Syntax.guess_lexer(self.key, self.content)
226
+ )
227
+ if lexer in text_log.available_languages:
228
+ text_log.language = lexer
229
+ else:
230
+ text_log.language = "python"
231
+ text_log.blur()
232
+ text_log.load_text(self.content)
233
+
234
+ @on(Button.Click)
235
+ def on_button_click(self, event: Button.Click) -> None:
236
+ """Handle button click events."""
237
+ if event.action == "cancel":
238
+ self.action_cancel_screen()
239
+
240
+ @on(HideSearchBar)
241
+ def on_hide_search_bar(self, _: HideSearchBar) -> None:
242
+ """Handle hide search bar events."""
243
+ search_bar = self.query_one("#search-bar", Search)
244
+ search_bar.remove_class("-visible")
245
+ search_bar.query_one(Input).value = ""
246
+ search_bar.total = 0
247
+ search_bar.current = 0
248
+ search_bar.search_result_lines = []
249
+ search_bar.search_value = ""
250
+
251
+ @on(HighlightResult)
252
+ def on_highlight_result(self, event: HighlightResult) -> None:
253
+ """Handle highlight result events."""
254
+
255
+ text_area = self.query_one(TextArea)
256
+ text_area.selection = Selection(
257
+ start=(event.line, event.start), end=(event.line, event.end)
258
+ )
259
+
260
+ def action_find(self):
261
+ self.query_one("#search-bar").add_class("-visible")
262
+ self.query_one(Input).focus()
263
+
264
+ def action_cancel(self):
265
+ self.action_cancel_screen()
266
+
267
+ def on_key(self, event: events.Key) -> None:
268
+ if event.key in ("ctrl+f", "super+f"):
269
+ self.action_find()
@@ -0,0 +1,62 @@
1
+
2
+ Container {
3
+ height: auto;
4
+ }
5
+
6
+
7
+ PreviewContentScreen {
8
+ align: center middle;
9
+ content-align: center top;
10
+
11
+
12
+ & > #PreviewContentScreen {
13
+ padding: 1 2 0 2;
14
+ margin: 0 0;
15
+ width: 70%;
16
+ height: 40;
17
+ border: solid $secondary-background;
18
+ border-title-color: $primary-lighten-2;
19
+
20
+
21
+ #search-bar {
22
+ height: auto;
23
+ display: none;
24
+
25
+ &.-visible {
26
+ display: block;
27
+ }
28
+ }
29
+
30
+ Horizontal{
31
+ height: auto;
32
+
33
+ & > Static {
34
+ width: auto;
35
+ }
36
+
37
+ #left {
38
+ align: left middle;
39
+ width: 90%;
40
+ }
41
+ #right {
42
+ align: right middle;
43
+ width: 10%;
44
+ }
45
+ }
46
+
47
+ Input {
48
+ height: 1;
49
+ border: none;
50
+ background: transparent;
51
+ width: 78%;
52
+ }
53
+
54
+ TextArea {
55
+ background-tint: $background;
56
+
57
+ &:focus {
58
+ border: tall $border-blurred;
59
+ }
60
+ }
61
+ }
62
+ }