data-sourcerer 0.3.0__py3-none-any.whl → 0.4.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.3.0.dist-info → data_sourcerer-0.4.0.dist-info}/METADATA +3 -1
- {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.4.0.dist-info}/RECORD +23 -21
- sourcerer/__init__.py +1 -1
- sourcerer/domain/storage_provider/entities.py +1 -1
- sourcerer/infrastructure/storage_provider/services/azure.py +1 -3
- sourcerer/infrastructure/storage_provider/services/gcp.py +1 -2
- sourcerer/infrastructure/storage_provider/services/s3.py +1 -2
- sourcerer/presentation/screens/file_system_finder/main.py +3 -9
- sourcerer/presentation/screens/main/main.py +27 -2
- sourcerer/presentation/screens/main/messages/preview_request.py +1 -0
- sourcerer/presentation/screens/main/widgets/storage_content.py +10 -3
- sourcerer/presentation/screens/preview_content/main.py +202 -15
- sourcerer/presentation/screens/preview_content/styles.tcss +39 -4
- sourcerer/presentation/screens/preview_content/text_area_style.py +60 -0
- sourcerer/presentation/screens/provider_creds_list/main.py +9 -5
- sourcerer/presentation/screens/provider_creds_registration/main.py +3 -3
- sourcerer/presentation/screens/shared/modal_screens.py +37 -0
- sourcerer/presentation/screens/storages_list/main.py +9 -5
- sourcerer/presentation/screens/storages_registration/main.py +3 -3
- sourcerer/settings.py +2 -0
- {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.4.0.dist-info}/WHEEL +0 -0
- {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.4.0.dist-info}/entry_points.txt +0 -0
- {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: data-sourcerer
|
3
|
-
Version: 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
|
@@ -1,5 +1,5 @@
|
|
1
|
-
sourcerer/__init__.py,sha256=
|
2
|
-
sourcerer/settings.py,sha256=
|
1
|
+
sourcerer/__init__.py,sha256=QELNZ0_i0crBFBzWe24aLrXu6z8WyDxq3mevvARdn2Q,585
|
2
|
+
sourcerer/settings.py,sha256=jIUcq9-_yYPxLS_8m1iKlkGCOg_pbhIjHXkR1LQmHQI,1463
|
3
3
|
sourcerer/utils.py,sha256=4jAlcofepAQMcD1cYDsC1ryGwBLxE9m7ckPS6CzDsCI,879
|
4
4
|
sourcerer/domain/__init__.py,sha256=rV21d-dD-e0q4EQ2LfWDSDLlrUOjnHnWBtWPujoue0o,556
|
5
5
|
sourcerer/domain/access_credentials/__init__.py,sha256=pFAwnr74uy09e7kubYsuaqzkVPkTA66dwjKzpIGQkAY,217
|
@@ -17,7 +17,7 @@ sourcerer/domain/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
17
17
|
sourcerer/domain/storage/entities.py,sha256=bSWVFx4Hh7_RLCHVLN_U2Wg6XxeYUyzDGLUpbFuWTLo,687
|
18
18
|
sourcerer/domain/storage/repositories.py,sha256=v7RU8_X_HgFT3wn7Yc5m07aN7DEbsBMh8sRZSRSCTxU,1003
|
19
19
|
sourcerer/domain/storage_provider/__init__.py,sha256=P3RUH9LFkWez3ehCczgVbnbp0tZZepPeOQf9spsC8FQ,192
|
20
|
-
sourcerer/domain/storage_provider/entities.py,sha256=
|
20
|
+
sourcerer/domain/storage_provider/entities.py,sha256=koaY4M4nWeie9kXJrHLJ85fEvaLxOQJEq8Zl3vmGwMM,1931
|
21
21
|
sourcerer/domain/storage_provider/exceptions.py,sha256=6xK5r62Bhedx3vV0_i7Eu5ZG5IExxeiuaGHG5sX17i4,508
|
22
22
|
sourcerer/domain/storage_provider/services.py,sha256=Zm6nrKqQJW-9ZaqTp9wQaQuwMeH6hkLJZCkhQ_sTRF0,3997
|
23
23
|
sourcerer/infrastructure/__init__.py,sha256=HQoqA8S9Vx2dr1Eua86wu_YxwXyY6jqa4IfEoZJcXcQ,616
|
@@ -40,9 +40,9 @@ sourcerer/infrastructure/storage_provider/__init__.py,sha256=GONjDCsTmd6f_fF3lzx
|
|
40
40
|
sourcerer/infrastructure/storage_provider/exceptions.py,sha256=acx3IIXD2yWlzLvD2asJBEpKEa6eJW31uKkzzr8MrR4,3336
|
41
41
|
sourcerer/infrastructure/storage_provider/registry.py,sha256=8dbRLOx1jLK_i18uuh_JnKvId9NJBECKg4nG9F_dFH4,2249
|
42
42
|
sourcerer/infrastructure/storage_provider/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
43
|
-
sourcerer/infrastructure/storage_provider/services/azure.py,sha256=
|
44
|
-
sourcerer/infrastructure/storage_provider/services/gcp.py,sha256=
|
45
|
-
sourcerer/infrastructure/storage_provider/services/s3.py,sha256=
|
43
|
+
sourcerer/infrastructure/storage_provider/services/azure.py,sha256=ZCglIQ0DQX1mYimsT4rS1tUvZtLbvpR_NZZzjGw7sB4,9757
|
44
|
+
sourcerer/infrastructure/storage_provider/services/gcp.py,sha256=vGqD7rrDZ4fepxIiY1owI4YTSCZWbRMmyrsuEssR24U,9059
|
45
|
+
sourcerer/infrastructure/storage_provider/services/s3.py,sha256=avRAtgZTobKnovfCG_lnW9xZOG77cnpDnZGazn85XHc,9239
|
46
46
|
sourcerer/presentation/__init__.py,sha256=kzOeaTpy9hm61MLl_nybdooRrawFUd1uEX4f3Y-84ZU,472
|
47
47
|
sourcerer/presentation/app.py,sha256=ROu3vSWzo6d8W30A9Zqi5zdLcVeHJsGLDJMLTKrthHE,1018
|
48
48
|
sourcerer/presentation/di_container.py,sha256=KTDSWEtR_YTqJTOsyhwBBHqB9gqIkX-nu5a8Ks9UlDc,2215
|
@@ -52,17 +52,17 @@ sourcerer/presentation/screens/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5
|
|
52
52
|
sourcerer/presentation/screens/critical_error/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
53
53
|
sourcerer/presentation/screens/critical_error/main.py,sha256=10Ip1InBzktwwM2ijKXBOkhvBXGorw1X8EGxdh75WZE,2208
|
54
54
|
sourcerer/presentation/screens/critical_error/styles.tcss,sha256=mURvbf0_npkRtzVBs2bVBybbyNK9cO_6Ar2Muk1Mpv8,604
|
55
|
-
sourcerer/presentation/screens/file_system_finder/main.py,sha256=
|
55
|
+
sourcerer/presentation/screens/file_system_finder/main.py,sha256=8V603I74iKoyMMChbl8XjpQAu614qpW0sbRlkCAxSjg,10221
|
56
56
|
sourcerer/presentation/screens/file_system_finder/styles.tcss,sha256=fZkdwXFsDkjXkaIskLxoQ_YHsLWKjgrn6hYseugg_68,718
|
57
57
|
sourcerer/presentation/screens/file_system_finder/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
58
58
|
sourcerer/presentation/screens/file_system_finder/widgets/file_system_navigator.py,sha256=giQXiNEsSAmBV-YWc1eXcJDFUWxpCppv0oNxgvrLZxg,31070
|
59
59
|
sourcerer/presentation/screens/main/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
60
|
-
sourcerer/presentation/screens/main/main.py,sha256=
|
60
|
+
sourcerer/presentation/screens/main/main.py,sha256=4eCKA7ZCy7DPfpK-uFnwSdY7HIELQSHr2TrmGdPX0u4,24038
|
61
61
|
sourcerer/presentation/screens/main/styles.tcss,sha256=Ruv2vBKzM8njH7OS2TCpZqCmRVEp7XQLeBN4XhVB5AU,381
|
62
62
|
sourcerer/presentation/screens/main/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
63
63
|
sourcerer/presentation/screens/main/messages/delete_request.py,sha256=puaU1UtbDErfYi8ViPEawhSbycnUpwdE81zZfzlslNE,203
|
64
64
|
sourcerer/presentation/screens/main/messages/download_request.py,sha256=3urSTrvNbod1FrXfu-C1UDZqOu5D0OC-NJsKJhhprXE,205
|
65
|
-
sourcerer/presentation/screens/main/messages/preview_request.py,sha256
|
65
|
+
sourcerer/presentation/screens/main/messages/preview_request.py,sha256=-UVWFVr4vsB0LGj97MTT8pLbJ3eNJClMWvT6vQ4lqw8,198
|
66
66
|
sourcerer/presentation/screens/main/messages/refresh_storages_list_request.py,sha256=NxOBcUf5oKF1cMCcHZny8qG2Jv6zTdPmYeFV9qORHh8,136
|
67
67
|
sourcerer/presentation/screens/main/messages/resizing_rule.py,sha256=ws7lzS08h6qqeihF66XV5FsX26YkjQOje_4vgCw2mqI,332
|
68
68
|
sourcerer/presentation/screens/main/messages/select_storage_item.py,sha256=iZOwzOwFLhsr58WV01-NtTFr4LXKykz1i76nayLkCLw,269
|
@@ -73,24 +73,26 @@ sourcerer/presentation/screens/main/mixins/resize_containers_watcher_mixin.py,sh
|
|
73
73
|
sourcerer/presentation/screens/main/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
74
74
|
sourcerer/presentation/screens/main/widgets/gradient.py,sha256=Ow6oaX0tsFNbUKRh_e9ITph20_tKoQgqzM7rujwKmxs,1263
|
75
75
|
sourcerer/presentation/screens/main/widgets/resizing_rule.py,sha256=W4tAbSZlmAIytP1BNlSvBeiMGA7cD6l13IpxG2JYzxk,2088
|
76
|
-
sourcerer/presentation/screens/main/widgets/storage_content.py,sha256=
|
76
|
+
sourcerer/presentation/screens/main/widgets/storage_content.py,sha256=7ngNzPSagFi2GzzYNzFQqq6DCppLil2taghJdkSube0,26278
|
77
77
|
sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py,sha256=83R7HximPBPIj-3J9oWnISv10RcCabTo5EVWmhO5lSk,7748
|
78
78
|
sourcerer/presentation/screens/preview_content/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
79
|
-
sourcerer/presentation/screens/preview_content/main.py,sha256=
|
80
|
-
sourcerer/presentation/screens/preview_content/styles.tcss,sha256=
|
79
|
+
sourcerer/presentation/screens/preview_content/main.py,sha256=UejOzmE6cl6HKTH_N0-spoByyTJRxZwI8-0nm3S8hi0,9111
|
80
|
+
sourcerer/presentation/screens/preview_content/styles.tcss,sha256=ESFpZgwZlehkrbCVhA45ODiXTpFuwqQPvQypsSjGxpQ,887
|
81
|
+
sourcerer/presentation/screens/preview_content/text_area_style.py,sha256=AOX8CG9A77gQIhF8GHTW45clPWrQcRKqu0KJcUzXrFU,2699
|
81
82
|
sourcerer/presentation/screens/provider_creds_list/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
82
|
-
sourcerer/presentation/screens/provider_creds_list/main.py,sha256=
|
83
|
+
sourcerer/presentation/screens/provider_creds_list/main.py,sha256=96BCZUi7ZLhZSCGeg8KaSeMdaQbh_BxwEZ1lW-9ZUp4,8822
|
83
84
|
sourcerer/presentation/screens/provider_creds_list/styles.tcss,sha256=_BXTWQw_LjPjbAG-V4YxxOeHn4GFShVjc4K1by-uVR0,956
|
84
85
|
sourcerer/presentation/screens/provider_creds_list/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
85
86
|
sourcerer/presentation/screens/provider_creds_list/messages/reload_credentials_request.py,sha256=zkyLFkXZHQMj5OsDU_IiInUnezHNWyvYn8yZLoJmJn0,134
|
86
87
|
sourcerer/presentation/screens/provider_creds_registration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
87
|
-
sourcerer/presentation/screens/provider_creds_registration/main.py,sha256=
|
88
|
+
sourcerer/presentation/screens/provider_creds_registration/main.py,sha256=CkGFef_JdMvcH0ve-XPGA_I8ZRTdzJNurvjUjZ0ZjLo,11422
|
88
89
|
sourcerer/presentation/screens/provider_creds_registration/styles.tcss,sha256=gd1SNeRoHTYwNzdGxK-2aDqNPeY5b2wFWajtoNn5--Y,612
|
89
90
|
sourcerer/presentation/screens/question/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
90
91
|
sourcerer/presentation/screens/question/main.py,sha256=7ogFbKrqP9deBYfgq7Bs6NkiSdy3whEu51mW_--TCIY,980
|
91
92
|
sourcerer/presentation/screens/question/styles.tcss,sha256=NT8Ty4opqYLG1-sal3m1us4M4LNhtTcQywlU-Al-v7o,396
|
92
93
|
sourcerer/presentation/screens/shared/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
93
94
|
sourcerer/presentation/screens/shared/containers.py,sha256=9Tkl5SbPNgycZlfp5Pq50pHnJqbP0EckmxayXPPuhFs,378
|
95
|
+
sourcerer/presentation/screens/shared/modal_screens.py,sha256=ycpqEy9M3Dh_q-EO44_YbIyDAb9lkF8ZCUUje5fh42c,1128
|
94
96
|
sourcerer/presentation/screens/shared/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
95
97
|
sourcerer/presentation/screens/shared/widgets/button.py,sha256=aGsJTwK04nTXarafkl8TgdeB9EGfRwn4gffoPvFFwiU,1580
|
96
98
|
sourcerer/presentation/screens/shared/widgets/labeled_input.py,sha256=OmaZjRJSg8LABvBvbN3LQ0s9M3-jp9X2i3yCVNbRhEk,2798
|
@@ -99,17 +101,17 @@ sourcerer/presentation/screens/storage_action_progress/__init__.py,sha256=47DEQp
|
|
99
101
|
sourcerer/presentation/screens/storage_action_progress/main.py,sha256=0nNSxKwZv-ly5uA1cpdZf6x-UO8xoF0poHwc9VcGMj4,17899
|
100
102
|
sourcerer/presentation/screens/storage_action_progress/styles.tcss,sha256=ffvDxRWhckE5tjEG4jwlhQNqeQsYpdF71104StWCGWU,818
|
101
103
|
sourcerer/presentation/screens/storages_list/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
102
|
-
sourcerer/presentation/screens/storages_list/main.py,sha256=
|
104
|
+
sourcerer/presentation/screens/storages_list/main.py,sha256=tzgCH-nJODW4wX1TO8Z7Zj5EwFXl5IX5JjEjxy6_Ky0,6481
|
103
105
|
sourcerer/presentation/screens/storages_list/styles.tcss,sha256=BlvlL5M-WGtMZuyqxT37gGlAIi1AFfILMaLiL-ZwvIc,766
|
104
106
|
sourcerer/presentation/screens/storages_list/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
105
107
|
sourcerer/presentation/screens/storages_list/messages/reload_storages_request.py,sha256=PtwlWgPUP_ZcAid535fd4Py6AvKUVhaXbc0WMka4QD0,131
|
106
108
|
sourcerer/presentation/screens/storages_registration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
107
|
-
sourcerer/presentation/screens/storages_registration/main.py,sha256=
|
109
|
+
sourcerer/presentation/screens/storages_registration/main.py,sha256=xam1qrUgmh7kA1y7EEbUAO1znZRpd1FQ7igN7Q_4j50,3802
|
108
110
|
sourcerer/presentation/screens/storages_registration/styles.tcss,sha256=Yd78pkiaaShb30r5s6qlYlLxyB62DJNeAQqs_gMcP6k,601
|
109
111
|
sourcerer/presentation/themes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
110
112
|
sourcerer/presentation/themes/github_dark.py,sha256=9E1mEOr701nU-ZDSKBccMl3GYchroCEsxEVelm5oI-E,497
|
111
|
-
data_sourcerer-0.
|
112
|
-
data_sourcerer-0.
|
113
|
-
data_sourcerer-0.
|
114
|
-
data_sourcerer-0.
|
115
|
-
data_sourcerer-0.
|
113
|
+
data_sourcerer-0.4.0.dist-info/METADATA,sha256=xanVKbREZlNIs0m1ecxFpYZS-OzCqb9S0BQ7YdE94jw,2595
|
114
|
+
data_sourcerer-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
115
|
+
data_sourcerer-0.4.0.dist-info/entry_points.txt,sha256=CyD02GehPW6QuhR5oDY5tLLRHQ9qbPXe0v3aT1pK3N8,62
|
116
|
+
data_sourcerer-0.4.0.dist-info/licenses/LICENSE,sha256=HjZ7RAG3i6izxvitGfY4feHfvW5F8DPj5eF0YBSf2rI,1073
|
117
|
+
data_sourcerer-0.4.0.dist-info/RECORD,,
|
sourcerer/__init__.py
CHANGED
@@ -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=
|
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=
|
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
|
-
|
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(
|
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.
|
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(
|
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.
|
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
|
)
|
@@ -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(
|
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=
|
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
|
|
@@ -1,52 +1,194 @@
|
|
1
|
+
import contextlib
|
2
|
+
import re
|
3
|
+
from dataclasses import dataclass
|
1
4
|
from pathlib import Path
|
5
|
+
from typing import ClassVar
|
2
6
|
|
7
|
+
import humanize
|
3
8
|
from dependency_injector.wiring import Provide
|
4
9
|
from rich.syntax import Syntax
|
5
|
-
from textual import on
|
10
|
+
from textual import events, on
|
6
11
|
from textual.app import ComposeResult
|
12
|
+
from textual.binding import Binding, BindingType
|
7
13
|
from textual.containers import Container, Horizontal
|
8
|
-
from textual.
|
9
|
-
from textual.
|
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
|
10
19
|
|
11
20
|
from sourcerer.infrastructure.access_credentials.services import CredentialsService
|
12
21
|
from sourcerer.infrastructure.storage_provider.exceptions import (
|
13
22
|
ReadStorageItemsError,
|
14
23
|
)
|
15
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
|
16
30
|
from sourcerer.presentation.screens.shared.widgets.button import Button
|
17
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()
|
18
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
|
+
]
|
19
114
|
|
20
|
-
|
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):
|
21
151
|
CSS_PATH = "styles.tcss"
|
22
152
|
|
153
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
154
|
+
Binding("escape", "cancel", "Close the screen"),
|
155
|
+
]
|
156
|
+
|
23
157
|
def __init__(
|
24
158
|
self,
|
25
159
|
storage_name,
|
26
160
|
key,
|
161
|
+
file_size,
|
27
162
|
access_credentials_uuid,
|
28
163
|
*args,
|
29
164
|
credentials_service: CredentialsService = Provide[
|
30
165
|
DiContainer.credentials_repository
|
31
166
|
],
|
32
|
-
**kwargs
|
167
|
+
**kwargs,
|
33
168
|
):
|
34
169
|
super().__init__(*args, **kwargs)
|
35
170
|
|
36
171
|
self.storage_name = storage_name
|
37
172
|
self.key = key
|
173
|
+
self.file_size = file_size
|
38
174
|
self.access_credentials_uuid = access_credentials_uuid
|
39
175
|
self.credentials_service = credentials_service
|
176
|
+
self.content = None
|
40
177
|
|
41
178
|
def compose(self) -> ComposeResult:
|
42
179
|
with Container(id="PreviewContentScreen"):
|
180
|
+
yield Search(id="search-bar")
|
43
181
|
yield LoadingIndicator(id="loading")
|
44
|
-
yield
|
182
|
+
yield TextArea(read_only=True, show_line_numbers=True)
|
45
183
|
with Horizontal(id="controls"):
|
46
184
|
yield Button("Close", name="cancel")
|
47
185
|
|
48
186
|
def on_mount(self) -> None:
|
49
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
|
50
192
|
|
51
193
|
provider_service = get_provider_service_by_access_uuid(
|
52
194
|
self.access_credentials_uuid, self.credentials_service
|
@@ -55,28 +197,73 @@ class PreviewContentScreen(ModalScreen):
|
|
55
197
|
self.notify("Could not read file :(", severity="error")
|
56
198
|
return
|
57
199
|
try:
|
58
|
-
content = provider_service.read_storage_item(
|
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
|
59
212
|
except ReadStorageItemsError:
|
60
213
|
self.notify("Could not read file :(", severity="error")
|
61
214
|
return
|
62
215
|
self.query_one("#loading").remove()
|
63
|
-
if content is None:
|
216
|
+
if self.content is None:
|
64
217
|
self.notify("Empty file", severity="warning")
|
65
218
|
return
|
66
219
|
|
67
|
-
text_log = self.query_one(RichLog)
|
68
|
-
|
69
220
|
extension = Path(self.key).suffix
|
70
221
|
|
71
222
|
lexer = (
|
72
|
-
"json"
|
223
|
+
"json"
|
224
|
+
if extension == ".tfstate"
|
225
|
+
else Syntax.guess_lexer(self.key, self.content)
|
73
226
|
)
|
74
|
-
|
75
|
-
|
76
|
-
|
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)
|
77
233
|
|
78
234
|
@on(Button.Click)
|
79
235
|
def on_button_click(self, event: Button.Click) -> None:
|
80
236
|
"""Handle button click events."""
|
81
237
|
if event.action == "cancel":
|
82
|
-
self.
|
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()
|
@@ -12,16 +12,51 @@ PreviewContentScreen {
|
|
12
12
|
& > #PreviewContentScreen {
|
13
13
|
padding: 1 2 0 2;
|
14
14
|
margin: 0 0;
|
15
|
-
width:
|
15
|
+
width: 70%;
|
16
16
|
height: 40;
|
17
17
|
border: solid $secondary-background;
|
18
18
|
border-title-color: $primary-lighten-2;
|
19
19
|
|
20
|
-
|
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;
|
21
50
|
background: transparent;
|
22
|
-
width:
|
51
|
+
width: 78%;
|
23
52
|
}
|
24
53
|
|
54
|
+
TextArea {
|
55
|
+
background-tint: $background;
|
56
|
+
|
57
|
+
&:focus {
|
58
|
+
border: tall $border-blurred;
|
59
|
+
}
|
60
|
+
}
|
25
61
|
}
|
26
62
|
}
|
27
|
-
|
@@ -0,0 +1,60 @@
|
|
1
|
+
from rich.style import Style
|
2
|
+
from textual._text_area_theme import TextAreaTheme
|
3
|
+
|
4
|
+
SOURCERER_THEME_NAME = "sourcerer"
|
5
|
+
# Terraform theme for the text area.
|
6
|
+
sourcerer_text_area_theme = TextAreaTheme(
|
7
|
+
name=SOURCERER_THEME_NAME,
|
8
|
+
cursor_style=Style(color="#1e1e1e", bgcolor="#f0f0f0"),
|
9
|
+
cursor_line_style=Style(bgcolor="#2b2b2b"),
|
10
|
+
bracket_matching_style=Style(bgcolor="#3a3a3a", bold=True),
|
11
|
+
cursor_line_gutter_style=Style(color="#CCCCCC", bgcolor="#2b2b2b"),
|
12
|
+
selection_style=Style(bgcolor="#FFA656"),
|
13
|
+
syntax_styles={
|
14
|
+
"string": Style(color="#79C0FF"),
|
15
|
+
"string.documentation": Style(color="#79C0FF"),
|
16
|
+
"comment": Style(color="#6A9955"),
|
17
|
+
"heading.marker": Style(color="#6E7681"),
|
18
|
+
"keyword": Style(color="#C586C0"),
|
19
|
+
"operator": Style(color="#CCCCCC"),
|
20
|
+
"conditional": Style(color="#569cd6"),
|
21
|
+
"keyword.function": Style(color="#F97970"),
|
22
|
+
"keyword.return": Style(color="#569cd6"),
|
23
|
+
"keyword.operator": Style(color="#569cd6"),
|
24
|
+
"repeat": Style(color="#569cd6"),
|
25
|
+
"exception": Style(color="#569cd6"),
|
26
|
+
"include": Style(color="#569cd6"),
|
27
|
+
"number": Style(color="#b5cea8"),
|
28
|
+
"float": Style(color="#b5cea8"),
|
29
|
+
"class": Style(color="#4EC9B0"),
|
30
|
+
"type": Style(color="#EFCB43"),
|
31
|
+
"type.class": Style(color="#FFA656"),
|
32
|
+
"type.builtin": Style(color="#CDD9E5"),
|
33
|
+
"function": Style(color="#CCB0EA"),
|
34
|
+
"function.call": Style(color="#CCB0EA"),
|
35
|
+
"method": Style(color="#CCB0EA"),
|
36
|
+
"method.call": Style(color="#CCB0EA"),
|
37
|
+
"constructor": Style(color="#DCBDFB"),
|
38
|
+
"boolean": Style(color="#7DAF9C"),
|
39
|
+
"constant.builtin": Style(color="#7DAF9C"),
|
40
|
+
"json.null": Style(color="#FDA556"),
|
41
|
+
"tag": Style(color="#EFCB43"),
|
42
|
+
"yaml.field": Style(color="#569cd6", bold=True),
|
43
|
+
"json.label": Style(color="#8EDB8C", bold=True),
|
44
|
+
"toml.type": Style(color="#569cd6"),
|
45
|
+
"toml.datetime": Style(color="#C586C0", italic=True),
|
46
|
+
"css.property": Style(color="#569cd6"),
|
47
|
+
"heading": Style(color="#569cd6", bold=True),
|
48
|
+
"bold": Style(bold=True),
|
49
|
+
"italic": Style(italic=True),
|
50
|
+
"strikethrough": Style(strike=True),
|
51
|
+
"link.uri": Style(color="#40A6FF", underline=True),
|
52
|
+
"link.label": Style(color="#569cd6"),
|
53
|
+
"list.marker": Style(color="#6E7681"),
|
54
|
+
"inline_code": Style(color="#ce9178"),
|
55
|
+
"info_string": Style(color="#ce9178", bold=True, italic=True),
|
56
|
+
"punctuation.bracket": Style(color="#CCCCCC"),
|
57
|
+
"punctuation.delimiter": Style(color="#CCCCCC"),
|
58
|
+
"punctuation.special": Style(color="#CCCCCC"),
|
59
|
+
},
|
60
|
+
)
|
@@ -7,7 +7,6 @@ from textual.app import ComposeResult
|
|
7
7
|
from textual.containers import Container, Horizontal, VerticalScroll
|
8
8
|
from textual.message import Message
|
9
9
|
from textual.reactive import reactive
|
10
|
-
from textual.screen import ModalScreen
|
11
10
|
from textual.widgets import Checkbox, Label
|
12
11
|
|
13
12
|
from sourcerer.domain.access_credentials.entities import Credentials
|
@@ -22,6 +21,9 @@ from sourcerer.presentation.screens.provider_creds_registration.main import (
|
|
22
21
|
ProviderCredsRegistrationScreen,
|
23
22
|
)
|
24
23
|
from sourcerer.presentation.screens.question.main import QuestionScreen
|
24
|
+
from sourcerer.presentation.screens.shared.modal_screens import (
|
25
|
+
RefreshTriggerableModalScreen,
|
26
|
+
)
|
25
27
|
from sourcerer.presentation.screens.shared.widgets.button import Button
|
26
28
|
|
27
29
|
|
@@ -98,7 +100,7 @@ class ProviderCredentialsRow(Horizontal):
|
|
98
100
|
self.post_message(ReloadCredentialsRequest())
|
99
101
|
|
100
102
|
|
101
|
-
class ProviderCredsListScreen(
|
103
|
+
class ProviderCredsListScreen(RefreshTriggerableModalScreen):
|
102
104
|
CSS_PATH = "styles.tcss"
|
103
105
|
|
104
106
|
MAIN_CONTAINER_ID = "ProviderCredsListScreen"
|
@@ -151,13 +153,15 @@ class ProviderCredsListScreen(ModalScreen):
|
|
151
153
|
"""
|
152
154
|
Initialize the screen by refreshing the credentials list when the screen is composed.
|
153
155
|
"""
|
154
|
-
self.refresh_credentials_list()
|
156
|
+
self.refresh_credentials_list(set_refresh_flag=False)
|
155
157
|
|
156
|
-
def refresh_credentials_list(self):
|
158
|
+
def refresh_credentials_list(self, set_refresh_flag: bool = True):
|
157
159
|
"""
|
158
160
|
Refresh the credentials list by retrieving the latest credentials from the credentials service.
|
159
161
|
"""
|
160
162
|
self.credentials_list = self.credentials_service.list()
|
163
|
+
if set_refresh_flag:
|
164
|
+
self._requires_storage_refresh = True
|
161
165
|
|
162
166
|
def create_provider_creds_registration(
|
163
167
|
self,
|
@@ -195,7 +199,7 @@ class ProviderCredsListScreen(ModalScreen):
|
|
195
199
|
event (Button.Click): The button click event.
|
196
200
|
"""
|
197
201
|
if event.action == ControlsEnum.CANCEL.name:
|
198
|
-
self.
|
202
|
+
self.action_cancel_screen()
|
199
203
|
if event.action == "add_registration":
|
200
204
|
self.app.push_screen(
|
201
205
|
ProviderCredsRegistrationScreen(),
|
@@ -5,7 +5,6 @@ from dependency_injector.wiring import Provide
|
|
5
5
|
from textual import on
|
6
6
|
from textual.app import ComposeResult
|
7
7
|
from textual.containers import Container, Horizontal, VerticalScroll
|
8
|
-
from textual.screen import ModalScreen
|
9
8
|
from textual.widgets import Label, Select
|
10
9
|
|
11
10
|
from sourcerer.domain.access_credentials.services import (
|
@@ -20,6 +19,7 @@ from sourcerer.infrastructure.access_credentials.registry import (
|
|
20
19
|
)
|
21
20
|
from sourcerer.infrastructure.utils import generate_unique_name
|
22
21
|
from sourcerer.presentation.di_container import DiContainer
|
22
|
+
from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
|
23
23
|
from sourcerer.presentation.screens.shared.widgets.button import Button
|
24
24
|
from sourcerer.presentation.screens.shared.widgets.labeled_input import LabeledInput
|
25
25
|
|
@@ -36,7 +36,7 @@ class ProviderCredentialsEntry:
|
|
36
36
|
fields: dict[str, str]
|
37
37
|
|
38
38
|
|
39
|
-
class ProviderCredsRegistrationScreen(
|
39
|
+
class ProviderCredsRegistrationScreen(ExitBoundModalScreen):
|
40
40
|
CSS_PATH = "styles.tcss"
|
41
41
|
|
42
42
|
MAIN_CONTAINER_ID = "ProviderCredsRegistrationScreen"
|
@@ -182,7 +182,7 @@ class ProviderCredsRegistrationScreen(ModalScreen):
|
|
182
182
|
collected authentication fields.
|
183
183
|
"""
|
184
184
|
if event.action == ControlsEnum.CANCEL.name:
|
185
|
-
self.
|
185
|
+
self.action_cancel_screen()
|
186
186
|
elif event.action == ControlsEnum.CREATE.name:
|
187
187
|
if not self.auth_method:
|
188
188
|
self.notify("Please select provider and auth method", severity="error")
|
@@ -0,0 +1,37 @@
|
|
1
|
+
from typing import ClassVar
|
2
|
+
|
3
|
+
from textual.binding import Binding, BindingType
|
4
|
+
from textual.screen import ModalScreen
|
5
|
+
|
6
|
+
|
7
|
+
class ExitBoundModalScreen(ModalScreen):
|
8
|
+
"""
|
9
|
+
A base class for modal screens that can be exited.
|
10
|
+
It provides a method to exit the screen and a flag to indicate if the screen should be exited.
|
11
|
+
"""
|
12
|
+
|
13
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
14
|
+
Binding("escape", "cancel_screen", "Pop screen"),
|
15
|
+
]
|
16
|
+
|
17
|
+
def action_cancel_screen(self):
|
18
|
+
"""
|
19
|
+
Action to exit the screen.
|
20
|
+
"""
|
21
|
+
self.dismiss()
|
22
|
+
|
23
|
+
|
24
|
+
class RefreshTriggerableModalScreen(ExitBoundModalScreen):
|
25
|
+
"""
|
26
|
+
A base class for modal screens that can be refreshed.
|
27
|
+
It provides a method to refresh the screen and a flag to indicate if the screen should be refreshed.
|
28
|
+
"""
|
29
|
+
|
30
|
+
def __init__(self, *args, **kwargs):
|
31
|
+
super().__init__(*args, **kwargs)
|
32
|
+
self._requires_storage_refresh = False
|
33
|
+
|
34
|
+
def action_cancel_screen(self):
|
35
|
+
requires_storage_refresh = self._requires_storage_refresh
|
36
|
+
self._requires_storage_refresh = False
|
37
|
+
self.dismiss(requires_storage_refresh)
|
@@ -7,7 +7,6 @@ from textual import on
|
|
7
7
|
from textual.app import ComposeResult
|
8
8
|
from textual.containers import Container, Horizontal, VerticalScroll
|
9
9
|
from textual.reactive import reactive
|
10
|
-
from textual.screen import ModalScreen
|
11
10
|
from textual.widgets import Label
|
12
11
|
|
13
12
|
from sourcerer.domain.storage.entities import Storage
|
@@ -15,6 +14,9 @@ from sourcerer.infrastructure.access_credentials.services import CredentialsServ
|
|
15
14
|
from sourcerer.infrastructure.storage.services import StoragesService
|
16
15
|
from sourcerer.presentation.di_container import DiContainer
|
17
16
|
from sourcerer.presentation.screens.question.main import QuestionScreen
|
17
|
+
from sourcerer.presentation.screens.shared.modal_screens import (
|
18
|
+
RefreshTriggerableModalScreen,
|
19
|
+
)
|
18
20
|
from sourcerer.presentation.screens.shared.widgets.button import Button
|
19
21
|
from sourcerer.presentation.screens.storages_list.messages.reload_storages_request import (
|
20
22
|
ReloadStoragesRequest,
|
@@ -74,7 +76,7 @@ class StorageRow(Horizontal):
|
|
74
76
|
self.post_message(ReloadStoragesRequest())
|
75
77
|
|
76
78
|
|
77
|
-
class StoragesListScreen(
|
79
|
+
class StoragesListScreen(RefreshTriggerableModalScreen):
|
78
80
|
CSS_PATH = "styles.tcss"
|
79
81
|
|
80
82
|
MAIN_CONTAINER_ID = "StoragesListScreen"
|
@@ -119,13 +121,15 @@ class StoragesListScreen(ModalScreen):
|
|
119
121
|
"""
|
120
122
|
Initialize the screen by refreshing the credentials list when the screen is composed.
|
121
123
|
"""
|
122
|
-
self.refresh_storages_list()
|
124
|
+
self.refresh_storages_list(set_refresh_flag=False)
|
123
125
|
|
124
|
-
def refresh_storages_list(self):
|
126
|
+
def refresh_storages_list(self, set_refresh_flag: bool = True):
|
125
127
|
"""
|
126
128
|
Refresh the storages list by retrieving the latest storages from the storage service.
|
127
129
|
"""
|
128
130
|
self.storages_list = self.storage_service.list()
|
131
|
+
if set_refresh_flag:
|
132
|
+
self._requires_storage_refresh = True
|
129
133
|
|
130
134
|
@on(ReloadStoragesRequest)
|
131
135
|
def on_reload_storages_request(self, _: ReloadStoragesRequest):
|
@@ -149,7 +153,7 @@ class StoragesListScreen(ModalScreen):
|
|
149
153
|
event (Button.Click): The button click event.
|
150
154
|
"""
|
151
155
|
if event.action == ControlsEnum.CANCEL.name:
|
152
|
-
self.
|
156
|
+
self.action_cancel_screen()
|
153
157
|
if event.action == ControlsEnum.ADD_STORAGE.name:
|
154
158
|
self.app.push_screen(
|
155
159
|
StoragesRegistrationScreen(),
|
@@ -5,11 +5,11 @@ from dependency_injector.wiring import Provide
|
|
5
5
|
from textual import on
|
6
6
|
from textual.app import ComposeResult
|
7
7
|
from textual.containers import Container, Horizontal, VerticalScroll
|
8
|
-
from textual.screen import ModalScreen
|
9
8
|
from textual.widgets import Label, Select
|
10
9
|
|
11
10
|
from sourcerer.infrastructure.access_credentials.services import CredentialsService
|
12
11
|
from sourcerer.presentation.di_container import DiContainer
|
12
|
+
from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
|
13
13
|
from sourcerer.presentation.screens.shared.widgets.button import Button
|
14
14
|
from sourcerer.presentation.screens.shared.widgets.labeled_input import LabeledInput
|
15
15
|
|
@@ -25,7 +25,7 @@ class StorageEntry:
|
|
25
25
|
credentials_uuid: str
|
26
26
|
|
27
27
|
|
28
|
-
class StoragesRegistrationScreen(
|
28
|
+
class StoragesRegistrationScreen(ExitBoundModalScreen):
|
29
29
|
CSS_PATH = "styles.tcss"
|
30
30
|
|
31
31
|
MAIN_CONTAINER_ID = "StoragesRegistrationScreen"
|
@@ -84,7 +84,7 @@ class StoragesRegistrationScreen(ModalScreen):
|
|
84
84
|
collected authentication fields.
|
85
85
|
"""
|
86
86
|
if event.action == ControlsEnum.CANCEL.name:
|
87
|
-
self.
|
87
|
+
self.action_cancel_screen()
|
88
88
|
elif event.action == ControlsEnum.CREATE.name:
|
89
89
|
storage_name = self.query_one("#storage_name", LabeledInput).get().value
|
90
90
|
if not storage_name:
|
sourcerer/settings.py
CHANGED
File without changes
|
File without changes
|
File without changes
|