data-sourcerer 0.3.0__py3-none-any.whl → 0.5.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 (55) hide show
  1. {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.5.0.dist-info}/METADATA +11 -8
  2. {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.5.0.dist-info}/RECORD +55 -34
  3. sourcerer/__init__.py +3 -1
  4. sourcerer/domain/package_meta/__init__.py +0 -0
  5. sourcerer/domain/package_meta/entities.py +9 -0
  6. sourcerer/domain/package_meta/services.py +9 -0
  7. sourcerer/domain/settings/__init__.py +0 -0
  8. sourcerer/domain/settings/entities.py +11 -0
  9. sourcerer/domain/settings/repositories.py +20 -0
  10. sourcerer/domain/settings/services.py +19 -0
  11. sourcerer/domain/storage_provider/entities.py +1 -1
  12. sourcerer/infrastructure/db/models.py +23 -0
  13. sourcerer/infrastructure/package_meta/__init__.py +0 -0
  14. sourcerer/infrastructure/package_meta/services.py +26 -0
  15. sourcerer/infrastructure/settings/__init__.py +0 -0
  16. sourcerer/infrastructure/settings/repositories.py +59 -0
  17. sourcerer/infrastructure/settings/services.py +16 -0
  18. sourcerer/infrastructure/storage_provider/services/azure.py +1 -3
  19. sourcerer/infrastructure/storage_provider/services/gcp.py +1 -3
  20. sourcerer/infrastructure/storage_provider/services/s3.py +1 -2
  21. sourcerer/infrastructure/utils.py +1 -0
  22. sourcerer/presentation/di_container.py +13 -0
  23. sourcerer/presentation/screens/about/__init__.py +0 -0
  24. sourcerer/presentation/screens/about/main.py +60 -0
  25. sourcerer/presentation/screens/about/styles.tcss +32 -0
  26. sourcerer/presentation/screens/file_system_finder/__init__.py +0 -0
  27. sourcerer/presentation/screens/file_system_finder/main.py +3 -9
  28. sourcerer/presentation/screens/main/main.py +116 -8
  29. sourcerer/presentation/screens/main/messages/preview_request.py +1 -0
  30. sourcerer/presentation/screens/main/styles.tcss +13 -4
  31. sourcerer/presentation/screens/main/widgets/storage_content.py +10 -3
  32. sourcerer/presentation/screens/main/widgets/storage_list_sidebar.py +102 -18
  33. sourcerer/presentation/screens/preview_content/main.py +202 -15
  34. sourcerer/presentation/screens/preview_content/styles.tcss +39 -4
  35. sourcerer/presentation/screens/preview_content/text_area_style.py +60 -0
  36. sourcerer/presentation/screens/provider_creds_list/main.py +23 -9
  37. sourcerer/presentation/screens/provider_creds_list/styles.tcss +9 -0
  38. sourcerer/presentation/screens/provider_creds_registration/main.py +3 -3
  39. sourcerer/presentation/screens/settings/__init__.py +0 -0
  40. sourcerer/presentation/screens/settings/main.py +70 -0
  41. sourcerer/presentation/screens/settings/styles.tcss +44 -0
  42. sourcerer/presentation/screens/shared/modal_screens.py +37 -0
  43. sourcerer/presentation/screens/shared/widgets/button.py +11 -0
  44. sourcerer/presentation/screens/shared/widgets/labeled_input.py +1 -3
  45. sourcerer/presentation/screens/storage_action_progress/main.py +1 -2
  46. sourcerer/presentation/screens/storages_list/main.py +24 -9
  47. sourcerer/presentation/screens/storages_list/styles.tcss +7 -0
  48. sourcerer/presentation/screens/storages_registration/main.py +3 -3
  49. sourcerer/presentation/settings.py +1 -0
  50. sourcerer/presentation/utils.py +1 -0
  51. sourcerer/settings.py +2 -0
  52. sourcerer/utils.py +19 -1
  53. {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.5.0.dist-info}/WHEEL +0 -0
  54. {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.5.0.dist-info}/entry_points.txt +0 -0
  55. {data_sourcerer-0.3.0.dist-info → data_sourcerer-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -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.screen import ModalScreen
9
- from textual.widgets import LoadingIndicator, RichLog
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
- class PreviewContentScreen(ModalScreen):
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 RichLog(highlight=True, markup=True, auto_scroll=False)
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(self.storage_name, self.key)
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" if extension == ".tfstate" else Syntax.guess_lexer(self.key, content)
223
+ "json"
224
+ if extension == ".tfstate"
225
+ else Syntax.guess_lexer(self.key, self.content)
73
226
  )
74
-
75
- content = Syntax(content, lexer, line_numbers=True, theme="ansi_dark")
76
- text_log.write(content)
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.dismiss()
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: 100;
15
+ width: 70%;
16
16
  height: 40;
17
17
  border: solid $secondary-background;
18
18
  border-title-color: $primary-lighten-2;
19
19
 
20
- RichLog {
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: 95;
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
+ )
@@ -1,13 +1,14 @@
1
1
  from dataclasses import dataclass
2
2
  from enum import Enum
3
+ from typing import ClassVar
3
4
 
4
5
  from dependency_injector.wiring import Provide
5
6
  from textual import on
6
7
  from textual.app import ComposeResult
8
+ from textual.binding import Binding, BindingType
7
9
  from textual.containers import Container, Horizontal, VerticalScroll
8
10
  from textual.message import Message
9
11
  from textual.reactive import reactive
10
- from textual.screen import ModalScreen
11
12
  from textual.widgets import Checkbox, Label
12
13
 
13
14
  from sourcerer.domain.access_credentials.entities import Credentials
@@ -22,6 +23,9 @@ from sourcerer.presentation.screens.provider_creds_registration.main import (
22
23
  ProviderCredsRegistrationScreen,
23
24
  )
24
25
  from sourcerer.presentation.screens.question.main import QuestionScreen
26
+ from sourcerer.presentation.screens.shared.modal_screens import (
27
+ RefreshTriggerableModalScreen,
28
+ )
25
29
  from sourcerer.presentation.screens.shared.widgets.button import Button
26
30
 
27
31
 
@@ -98,7 +102,7 @@ class ProviderCredentialsRow(Horizontal):
98
102
  self.post_message(ReloadCredentialsRequest())
99
103
 
100
104
 
101
- class ProviderCredsListScreen(ModalScreen):
105
+ class ProviderCredsListScreen(RefreshTriggerableModalScreen):
102
106
  CSS_PATH = "styles.tcss"
103
107
 
104
108
  MAIN_CONTAINER_ID = "ProviderCredsListScreen"
@@ -110,6 +114,11 @@ class ProviderCredsListScreen(ModalScreen):
110
114
  PROVIDERS_NAME = "providers"
111
115
  AUTH_METHODS_NAME = "auth_methods"
112
116
 
117
+ BINDINGS: ClassVar[list[BindingType]] = [
118
+ *RefreshTriggerableModalScreen.BINDINGS,
119
+ Binding("ctrl+n", "add_credentials", "Add new credentials"),
120
+ ]
121
+
113
122
  credentials_list = reactive([], recompose=True)
114
123
 
115
124
  def __init__(
@@ -151,13 +160,15 @@ class ProviderCredsListScreen(ModalScreen):
151
160
  """
152
161
  Initialize the screen by refreshing the credentials list when the screen is composed.
153
162
  """
154
- self.refresh_credentials_list()
163
+ self.refresh_credentials_list(set_refresh_flag=False)
155
164
 
156
- def refresh_credentials_list(self):
165
+ def refresh_credentials_list(self, set_refresh_flag: bool = True):
157
166
  """
158
167
  Refresh the credentials list by retrieving the latest credentials from the credentials service.
159
168
  """
160
169
  self.credentials_list = self.credentials_service.list()
170
+ if set_refresh_flag:
171
+ self._requires_storage_refresh = True
161
172
 
162
173
  def create_provider_creds_registration(
163
174
  self,
@@ -195,12 +206,9 @@ class ProviderCredsListScreen(ModalScreen):
195
206
  event (Button.Click): The button click event.
196
207
  """
197
208
  if event.action == ControlsEnum.CANCEL.name:
198
- self.dismiss()
209
+ self.action_cancel_screen()
199
210
  if event.action == "add_registration":
200
- self.app.push_screen(
201
- ProviderCredsRegistrationScreen(),
202
- callback=self.create_provider_creds_registration, # type: ignore
203
- )
211
+ self.action_add_credentials()
204
212
 
205
213
  @on(ProviderCredentialsRow.ChangeActiveStatus)
206
214
  def on_change_active_status(self, event: ProviderCredentialsRow.ChangeActiveStatus):
@@ -227,3 +235,9 @@ class ProviderCredsListScreen(ModalScreen):
227
235
  _ (ReloadCredentialsRequest): The reload credentials request event.
228
236
  """
229
237
  self.refresh_credentials_list()
238
+
239
+ def action_add_credentials(self):
240
+ self.app.push_screen(
241
+ ProviderCredsRegistrationScreen(),
242
+ callback=self.create_provider_creds_registration, # type: ignore
243
+ )
@@ -23,6 +23,14 @@ ProviderCredsListScreen {
23
23
 
24
24
  .add_registration_button {
25
25
  color: $success-darken-2;
26
+
27
+ &:hover {
28
+ color: $primary;
29
+ }
30
+ &:focus {
31
+ color: $primary;
32
+ }
33
+
26
34
  }
27
35
 
28
36
  margin-bottom: 1;
@@ -30,6 +38,7 @@ ProviderCredsListScreen {
30
38
 
31
39
  ProviderCredentialsRow.active {
32
40
  background: $primary-lighten-2;
41
+ color: $background-darken-3;
33
42
  }
34
43
 
35
44
  ProviderCredentialsRow, Horizontal {
@@ -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(ModalScreen):
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.dismiss()
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")
File without changes
@@ -0,0 +1,70 @@
1
+ from textual import on
2
+ from textual.app import ComposeResult
3
+ from textual.containers import Container, Horizontal
4
+ from textual.widgets import Checkbox, Rule, Select, Static
5
+
6
+ from sourcerer.domain.settings.entities import Settings, SettingsFields
7
+ from sourcerer.presentation.screens.shared.modal_screens import ExitBoundModalScreen
8
+ from sourcerer.presentation.screens.shared.widgets.button import Button
9
+
10
+
11
+ class SettingsScreen(ExitBoundModalScreen):
12
+ """Screen with a parameter."""
13
+
14
+ CSS_PATH = "styles.tcss"
15
+
16
+ def __init__(self, settings: Settings) -> None:
17
+ super().__init__()
18
+ self.settings = settings
19
+
20
+ def compose(self) -> ComposeResult:
21
+ with Container():
22
+ with Horizontal():
23
+ yield Static("Theme:")
24
+ yield Select(
25
+ ((theme, theme) for theme in self.app._registered_themes),
26
+ id="theme",
27
+ value=self.settings.theme,
28
+ allow_blank=False,
29
+ )
30
+
31
+ yield Rule()
32
+ with Horizontal():
33
+ yield Checkbox(
34
+ "Group storage by access credentials",
35
+ value=self.settings.group_by_access_credentials,
36
+ )
37
+
38
+ yield Rule()
39
+ with Horizontal(id="controls"):
40
+ yield Button("Save", name="save")
41
+ yield Button("Close", name="close")
42
+
43
+ @on(Button.Click)
44
+ def on_button_clicked(self, event: Button.Click) -> None:
45
+ """Handle button clicked events."""
46
+ if event.action == "close":
47
+ self.action_cancel_screen()
48
+ elif event.action == "save":
49
+ self.dismiss(
50
+ {
51
+ SettingsFields.theme: self.query_one("Select#theme", Select).value,
52
+ SettingsFields.group_by_access_credentials: self.query_one(
53
+ Checkbox
54
+ ).value,
55
+ }
56
+ )
57
+
58
+ def action_cancel_screen(self):
59
+ self.dismiss(
60
+ {
61
+ SettingsFields.theme: self.settings.theme,
62
+ SettingsFields.group_by_access_credentials: self.settings.group_by_access_credentials,
63
+ }
64
+ )
65
+
66
+ @on(Select.Changed)
67
+ def on_select_changed(self, event: Select.Changed) -> None:
68
+ """Handle select changed events."""
69
+ if event.select.id == "theme":
70
+ self.app.theme = event.value # type: ignore[assignment]
@@ -0,0 +1,44 @@
1
+ SettingsScreen {
2
+ align: center middle;
3
+ content-align: center top;
4
+
5
+ & > Container {
6
+ padding: 1 2 0 2;
7
+ margin: 0 0;
8
+ width: 70%;
9
+ height: auto;
10
+ border: solid $border;
11
+
12
+ & > Static {
13
+ text-align: center;
14
+ text-wrap: wrap;
15
+ }
16
+
17
+ & > Horizontal {
18
+ align: center bottom;
19
+ height: auto;
20
+
21
+ & > Static {
22
+ width: 10%;
23
+ padding-top: 1;
24
+ }
25
+
26
+ & > Checkbox {
27
+ width: 100%;
28
+ border: none;
29
+ background: transparent;
30
+
31
+ &:focus {
32
+ background: transparent;
33
+
34
+ & > .toggle--label {
35
+ color: $text-secondary;
36
+ background: transparent;
37
+ }
38
+ }
39
+
40
+ }
41
+ }
42
+ }
43
+
44
+ }