restiny 0.2.1__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.
- restiny/__about__.py +1 -1
- restiny/__main__.py +26 -14
- restiny/assets/style.tcss +47 -1
- restiny/consts.py +236 -0
- restiny/data/db.py +60 -0
- restiny/data/models.py +90 -0
- restiny/data/repos.py +351 -0
- restiny/data/sql/__init__.py +3 -0
- restiny/entities.py +320 -0
- restiny/enums.py +14 -5
- restiny/httpx_auths.py +52 -0
- restiny/ui/__init__.py +15 -0
- restiny/ui/app.py +500 -0
- restiny/ui/collections_area.py +569 -0
- restiny/ui/request_area.py +575 -0
- restiny/ui/settings_screen.py +80 -0
- restiny/{core → ui}/url_area.py +50 -38
- restiny/utils.py +52 -15
- restiny/widgets/__init__.py +15 -1
- restiny/widgets/collections_tree.py +70 -0
- restiny/widgets/confirm_prompt.py +76 -0
- restiny/widgets/custom_input.py +20 -0
- restiny/widgets/dynamic_fields.py +65 -70
- restiny/widgets/password_input.py +161 -0
- restiny/widgets/path_chooser.py +12 -12
- {restiny-0.2.1.dist-info → restiny-0.5.0.dist-info}/METADATA +7 -5
- restiny-0.5.0.dist-info/RECORD +36 -0
- restiny/core/__init__.py +0 -15
- restiny/core/app.py +0 -348
- restiny/core/request_area.py +0 -337
- restiny-0.2.1.dist-info/RECORD +0 -24
- /restiny/{core → ui}/response_area.py +0 -0
- {restiny-0.2.1.dist-info → restiny-0.5.0.dist-info}/WHEEL +0 -0
- {restiny-0.2.1.dist-info → restiny-0.5.0.dist-info}/entry_points.txt +0 -0
- {restiny-0.2.1.dist-info → restiny-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {restiny-0.2.1.dist-info → restiny-0.5.0.dist-info}/top_level.txt +0 -0
restiny/{core → ui}/url_area.py
RENAMED
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
|
|
3
1
|
from textual import on
|
|
4
2
|
from textual.app import ComposeResult
|
|
3
|
+
from textual.containers import Horizontal
|
|
5
4
|
from textual.message import Message
|
|
6
|
-
from textual.widgets import Button, ContentSwitcher,
|
|
5
|
+
from textual.widgets import Button, ContentSwitcher, Select, Static
|
|
7
6
|
|
|
8
7
|
from restiny.enums import HTTPMethod
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@dataclass
|
|
12
|
-
class URLAreaData:
|
|
13
|
-
method: str
|
|
14
|
-
url: str
|
|
8
|
+
from restiny.widgets import CustomInput
|
|
15
9
|
|
|
16
10
|
|
|
17
11
|
class URLArea(Static):
|
|
@@ -20,9 +14,8 @@ class URLArea(Static):
|
|
|
20
14
|
BORDER_TITLE = 'URL'
|
|
21
15
|
DEFAULT_CSS = """
|
|
22
16
|
URLArea {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
grid-columns: 1fr 6fr 1fr;
|
|
17
|
+
width: 1fr;
|
|
18
|
+
height: auto;
|
|
26
19
|
border: heavy black;
|
|
27
20
|
border-title-color: gray;
|
|
28
21
|
}
|
|
@@ -49,25 +42,34 @@ class URLArea(Static):
|
|
|
49
42
|
self._request_pending = False
|
|
50
43
|
|
|
51
44
|
def compose(self) -> ComposeResult:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
with ContentSwitcher(
|
|
57
|
-
id='request-button-switcher', initial='send-request'
|
|
58
|
-
):
|
|
59
|
-
yield Button(
|
|
60
|
-
label='Send Request',
|
|
61
|
-
id='send-request',
|
|
45
|
+
with Horizontal(classes='h-auto'):
|
|
46
|
+
yield Select.from_values(
|
|
47
|
+
values=[method.value for method in HTTPMethod],
|
|
48
|
+
allow_blank=False,
|
|
62
49
|
classes='w-1fr',
|
|
63
|
-
|
|
50
|
+
id='method',
|
|
64
51
|
)
|
|
65
|
-
yield
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
classes='w-
|
|
69
|
-
|
|
52
|
+
yield CustomInput(
|
|
53
|
+
placeholder='Enter URL',
|
|
54
|
+
select_on_focus=False,
|
|
55
|
+
classes='w-5fr',
|
|
56
|
+
id='url',
|
|
70
57
|
)
|
|
58
|
+
with ContentSwitcher(
|
|
59
|
+
id='request-button-switcher', initial='send-request'
|
|
60
|
+
):
|
|
61
|
+
yield Button(
|
|
62
|
+
label='Send Request',
|
|
63
|
+
id='send-request',
|
|
64
|
+
classes='w-1fr',
|
|
65
|
+
variant='default',
|
|
66
|
+
)
|
|
67
|
+
yield Button(
|
|
68
|
+
label='Cancel Request',
|
|
69
|
+
id='cancel-request',
|
|
70
|
+
classes='w-1fr',
|
|
71
|
+
variant='error',
|
|
72
|
+
)
|
|
71
73
|
|
|
72
74
|
def on_mount(self) -> None:
|
|
73
75
|
self._request_button_switcher = self.query_one(
|
|
@@ -75,16 +77,10 @@ class URLArea(Static):
|
|
|
75
77
|
)
|
|
76
78
|
|
|
77
79
|
self.method_select = self.query_one('#method', Select)
|
|
78
|
-
self.url_input = self.query_one('#url',
|
|
80
|
+
self.url_input = self.query_one('#url', CustomInput)
|
|
79
81
|
self.send_request_button = self.query_one('#send-request', Button)
|
|
80
82
|
self.cancel_request_button = self.query_one('#cancel-request', Button)
|
|
81
83
|
|
|
82
|
-
def get_data(self) -> URLAreaData:
|
|
83
|
-
return URLAreaData(
|
|
84
|
-
method=self.method_select.value,
|
|
85
|
-
url=self.url_input.value,
|
|
86
|
-
)
|
|
87
|
-
|
|
88
84
|
@property
|
|
89
85
|
def request_pending(self) -> bool:
|
|
90
86
|
return self._request_pending
|
|
@@ -98,10 +94,26 @@ class URLArea(Static):
|
|
|
98
94
|
|
|
99
95
|
self._request_pending = value
|
|
100
96
|
|
|
97
|
+
@property
|
|
98
|
+
def method(self) -> HTTPMethod:
|
|
99
|
+
return self.method_select.value
|
|
100
|
+
|
|
101
|
+
@method.setter
|
|
102
|
+
def method(self, value: HTTPMethod) -> None:
|
|
103
|
+
self.method_select.value = value
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def url(self) -> str:
|
|
107
|
+
return self.url_input.value
|
|
108
|
+
|
|
109
|
+
@url.setter
|
|
110
|
+
def url(self, value: str) -> None:
|
|
111
|
+
self.url_input.value = value
|
|
112
|
+
|
|
101
113
|
@on(Button.Pressed, '#send-request')
|
|
102
|
-
@on(
|
|
114
|
+
@on(CustomInput.Submitted, '#url')
|
|
103
115
|
def _on_send_request(
|
|
104
|
-
self, message: Button.Pressed |
|
|
116
|
+
self, message: Button.Pressed | CustomInput.Submitted
|
|
105
117
|
) -> None:
|
|
106
118
|
if self.request_pending:
|
|
107
119
|
return
|
|
@@ -109,7 +121,7 @@ class URLArea(Static):
|
|
|
109
121
|
self.post_message(message=self.SendRequest())
|
|
110
122
|
|
|
111
123
|
@on(Button.Pressed, '#cancel-request')
|
|
112
|
-
@on(
|
|
124
|
+
@on(CustomInput.Submitted, '#url')
|
|
113
125
|
def _on_cancel_request(self, message: Button.Pressed) -> None:
|
|
114
126
|
if not self.request_pending:
|
|
115
127
|
return
|
restiny/utils.py
CHANGED
|
@@ -8,32 +8,43 @@ import httpx
|
|
|
8
8
|
def build_curl_cmd(
|
|
9
9
|
method: str,
|
|
10
10
|
url: str,
|
|
11
|
-
headers: dict[str, str] =
|
|
12
|
-
params: dict[str, str] =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
headers: dict[str, str] | None = None,
|
|
12
|
+
params: dict[str, str] | None = None,
|
|
13
|
+
body_raw: str | None = None,
|
|
14
|
+
body_form_urlencoded: dict[str, str] | None = None,
|
|
15
|
+
body_form_multipart: dict[str, str | Path] | None = None,
|
|
16
|
+
body_files: list[Path] | None = None,
|
|
17
|
+
auth_basic: tuple[str, str] | None = None,
|
|
18
|
+
auth_bearer: str | None = None,
|
|
19
|
+
auth_api_key_header: tuple[str, str] | None = None,
|
|
20
|
+
auth_api_key_param: tuple[str, str] | None = None,
|
|
21
|
+
auth_digest: tuple[str, str] | None = None,
|
|
17
22
|
) -> str:
|
|
18
23
|
cmd_parts = ['curl']
|
|
24
|
+
|
|
25
|
+
# Method
|
|
19
26
|
cmd_parts.extend(['--request', method])
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
# URL + Params
|
|
29
|
+
if params:
|
|
30
|
+
url = str(httpx.URL(url).copy_merge_params(params))
|
|
22
31
|
cmd_parts.extend(['--url', shlex.quote(url)])
|
|
23
32
|
|
|
33
|
+
# Headers
|
|
24
34
|
for header_key, header_value in headers.items():
|
|
25
35
|
header = f'{header_key}: {header_value}'
|
|
26
36
|
cmd_parts.extend(['--header', shlex.quote(header)])
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
# Body
|
|
39
|
+
if body_raw:
|
|
40
|
+
cmd_parts.extend(['--data', shlex.quote(body_raw)])
|
|
41
|
+
elif body_form_urlencoded:
|
|
42
|
+
for form_key, form_value in body_form_urlencoded.items():
|
|
32
43
|
cmd_parts.extend(
|
|
33
44
|
['--data', shlex.quote(f'{form_key}={form_value}')]
|
|
34
45
|
)
|
|
35
|
-
elif
|
|
36
|
-
for form_key, form_value in
|
|
46
|
+
elif body_form_multipart:
|
|
47
|
+
for form_key, form_value in body_form_multipart.items():
|
|
37
48
|
if isinstance(form_value, str):
|
|
38
49
|
cmd_parts.extend(
|
|
39
50
|
['--form', shlex.quote(f'{form_key}={form_value}')]
|
|
@@ -42,10 +53,29 @@ def build_curl_cmd(
|
|
|
42
53
|
cmd_parts.extend(
|
|
43
54
|
['--form', shlex.quote(f'{form_key}=@{form_value}')]
|
|
44
55
|
)
|
|
45
|
-
elif
|
|
46
|
-
for file in
|
|
56
|
+
elif body_files:
|
|
57
|
+
for file in body_files:
|
|
47
58
|
cmd_parts.extend(['--data', shlex.quote(f'@{file}')])
|
|
48
59
|
|
|
60
|
+
# Auth
|
|
61
|
+
if auth_basic:
|
|
62
|
+
user, pwd = auth_basic
|
|
63
|
+
cmd_parts.extend(['--user', shlex.quote(f'{user}:{pwd}')])
|
|
64
|
+
elif auth_bearer:
|
|
65
|
+
token = auth_bearer
|
|
66
|
+
cmd_parts.extend(['--header', shlex.quote(f'Authorization: {token}')])
|
|
67
|
+
elif auth_api_key_header:
|
|
68
|
+
key, value = auth_api_key_header
|
|
69
|
+
cmd_parts.extend(['--header', shlex.quote(f'{key}: {value}')])
|
|
70
|
+
elif auth_api_key_param:
|
|
71
|
+
key, value = auth_api_key_param
|
|
72
|
+
url_arg_index = cmd_parts.index('--url')
|
|
73
|
+
new_url = str(httpx.URL(url).copy_merge_params({key: value}))
|
|
74
|
+
cmd_parts[url_arg_index + 1] = shlex.quote(new_url)
|
|
75
|
+
elif auth_digest:
|
|
76
|
+
user, pwd = auth_digest
|
|
77
|
+
cmd_parts.extend(['--digest', '--user', shlex.quote(f'{user}:{pwd}')])
|
|
78
|
+
|
|
49
79
|
return ' '.join(cmd_parts)
|
|
50
80
|
|
|
51
81
|
|
|
@@ -102,3 +132,10 @@ def first_char_non_empty(text: str) -> int | None:
|
|
|
102
132
|
|
|
103
133
|
def seconds_to_milliseconds(seconds: int | float) -> int:
|
|
104
134
|
return round(seconds * 1000)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def shorten_string(value: str, max_lenght: int, elipsis: str = '..') -> str:
|
|
138
|
+
if len(value) <= max_lenght:
|
|
139
|
+
return value
|
|
140
|
+
|
|
141
|
+
return value[: max_lenght - len(elipsis)] + elipsis
|
restiny/widgets/__init__.py
CHANGED
|
@@ -2,15 +2,29 @@
|
|
|
2
2
|
This module contains reusable widgets used in the DataFox interface.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from restiny.widgets.collections_tree import CollectionsTree
|
|
6
|
+
from restiny.widgets.confirm_prompt import ConfirmPrompt, ConfirmPromptResult
|
|
5
7
|
from restiny.widgets.custom_directory_tree import CustomDirectoryTree
|
|
8
|
+
from restiny.widgets.custom_input import CustomInput
|
|
6
9
|
from restiny.widgets.custom_text_area import CustomTextArea
|
|
7
|
-
from restiny.widgets.dynamic_fields import
|
|
10
|
+
from restiny.widgets.dynamic_fields import (
|
|
11
|
+
DynamicFields,
|
|
12
|
+
TextDynamicField,
|
|
13
|
+
TextOrFileDynamicField,
|
|
14
|
+
)
|
|
15
|
+
from restiny.widgets.password_input import PasswordInput
|
|
8
16
|
from restiny.widgets.path_chooser import PathChooser
|
|
9
17
|
|
|
10
18
|
__all__ = [
|
|
11
19
|
'TextDynamicField',
|
|
20
|
+
'TextOrFileDynamicField',
|
|
12
21
|
'DynamicFields',
|
|
13
22
|
'CustomDirectoryTree',
|
|
14
23
|
'CustomTextArea',
|
|
15
24
|
'PathChooser',
|
|
25
|
+
'PasswordInput',
|
|
26
|
+
'CustomInput',
|
|
27
|
+
'CollectionsTree',
|
|
28
|
+
'ConfirmPrompt',
|
|
29
|
+
'ConfirmPromptResult',
|
|
16
30
|
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from textual.widgets import Tree
|
|
2
|
+
from textual.widgets.tree import TreeNode
|
|
3
|
+
|
|
4
|
+
from restiny.enums import HTTPMethod
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CollectionsTree(Tree):
|
|
8
|
+
show_root = False
|
|
9
|
+
|
|
10
|
+
def on_mount(self) -> None:
|
|
11
|
+
self.node_by_id: dict[int | None, TreeNode] = {}
|
|
12
|
+
self.node_by_id[None] = self.root
|
|
13
|
+
self.root.data = {'name': '/', 'id': None}
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def current_parent_folder(self) -> TreeNode:
|
|
17
|
+
if not self.cursor_node:
|
|
18
|
+
return self.root
|
|
19
|
+
|
|
20
|
+
return self.cursor_node.parent
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def current_folder(self) -> TreeNode:
|
|
24
|
+
if not self.cursor_node:
|
|
25
|
+
return self.root
|
|
26
|
+
|
|
27
|
+
if self.cursor_node.allow_expand:
|
|
28
|
+
return self.cursor_node
|
|
29
|
+
else:
|
|
30
|
+
return self.cursor_node.parent
|
|
31
|
+
|
|
32
|
+
def add_folder(
|
|
33
|
+
self, parent_node: TreeNode | None, name: str, id: int
|
|
34
|
+
) -> TreeNode:
|
|
35
|
+
parent_node = parent_node or self.root
|
|
36
|
+
|
|
37
|
+
node = parent_node.add(label=name)
|
|
38
|
+
node.data = {
|
|
39
|
+
'name': name,
|
|
40
|
+
'id': id,
|
|
41
|
+
}
|
|
42
|
+
self.node_by_id[id] = node
|
|
43
|
+
return node
|
|
44
|
+
|
|
45
|
+
def add_request(
|
|
46
|
+
self, parent_node: TreeNode | None, method: str, name: str, id: int
|
|
47
|
+
) -> TreeNode:
|
|
48
|
+
parent_node = parent_node or self.root
|
|
49
|
+
|
|
50
|
+
method_to_color = {
|
|
51
|
+
HTTPMethod.GET: '#00cc66', # green
|
|
52
|
+
HTTPMethod.POST: '#ffcc00', # yellow
|
|
53
|
+
HTTPMethod.PUT: '#3388ff', # blue
|
|
54
|
+
HTTPMethod.PATCH: '#00b3b3', # teal
|
|
55
|
+
HTTPMethod.DELETE: '#ff3333', # red
|
|
56
|
+
HTTPMethod.HEAD: '#808080', # gray
|
|
57
|
+
HTTPMethod.OPTIONS: '#cc66ff', # magenta
|
|
58
|
+
HTTPMethod.CONNECT: '#ff9966', # orange
|
|
59
|
+
HTTPMethod.TRACE: '#6666ff', # violet
|
|
60
|
+
}
|
|
61
|
+
node = parent_node.add_leaf(
|
|
62
|
+
label=f'[{method_to_color[method]}]{method}[/] {name}'
|
|
63
|
+
) # noqa
|
|
64
|
+
node.data = {
|
|
65
|
+
'method': method,
|
|
66
|
+
'name': name,
|
|
67
|
+
'id': id,
|
|
68
|
+
}
|
|
69
|
+
self.node_by_id[id] = node
|
|
70
|
+
return node
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from textual import on
|
|
4
|
+
from textual.app import ComposeResult
|
|
5
|
+
from textual.binding import Binding
|
|
6
|
+
from textual.containers import Horizontal, Vertical
|
|
7
|
+
from textual.screen import ModalScreen
|
|
8
|
+
from textual.widgets import Button, Label
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ConfirmPromptResult:
|
|
13
|
+
confirmed: bool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConfirmPrompt(ModalScreen):
|
|
17
|
+
DEFAULT_CSS = """
|
|
18
|
+
ConfirmPrompt {
|
|
19
|
+
align: center middle;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#modal-content {
|
|
23
|
+
border: heavy black;
|
|
24
|
+
border-title-color: gray;
|
|
25
|
+
background: $surface;
|
|
26
|
+
width: auto;
|
|
27
|
+
height: auto;
|
|
28
|
+
max-width: 40%;
|
|
29
|
+
text-align: center;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Horizontal {
|
|
33
|
+
align: center middle;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#message {
|
|
37
|
+
}
|
|
38
|
+
"""
|
|
39
|
+
AUTO_FOCUS = '#confirm'
|
|
40
|
+
|
|
41
|
+
BINDINGS = [
|
|
42
|
+
Binding(
|
|
43
|
+
key='escape',
|
|
44
|
+
action='dismiss',
|
|
45
|
+
description='Quit the screen',
|
|
46
|
+
show=False,
|
|
47
|
+
),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def __init__(self, message: str = 'Are you sure?') -> None:
|
|
51
|
+
super().__init__()
|
|
52
|
+
self._message = message
|
|
53
|
+
|
|
54
|
+
def compose(self) -> ComposeResult:
|
|
55
|
+
with Vertical(id='modal-content'):
|
|
56
|
+
with Horizontal(classes='w-1fr h-auto mt-1'):
|
|
57
|
+
yield Label(self._message, id='message')
|
|
58
|
+
with Horizontal(classes='w-1fr h-auto mt-1'):
|
|
59
|
+
yield Button(label='Cancel', classes='w-1fr', id='cancel')
|
|
60
|
+
yield Button(label='Confirm', classes='w-1fr', id='confirm')
|
|
61
|
+
|
|
62
|
+
def on_mount(self) -> None:
|
|
63
|
+
self.modal_content = self.query_one('#modal-content', Vertical)
|
|
64
|
+
self.message_label = self.query_one('#message', Label)
|
|
65
|
+
self.cancel_button = self.query_one('#cancel', Button)
|
|
66
|
+
self.confirm_button = self.query_one('#confirm', Button)
|
|
67
|
+
|
|
68
|
+
self.modal_content.border_title = 'Confirm'
|
|
69
|
+
|
|
70
|
+
@on(Button.Pressed, '#cancel')
|
|
71
|
+
def _on_cancel(self, message: Button.Pressed) -> None:
|
|
72
|
+
self.dismiss(result=ConfirmPromptResult(confirmed=False))
|
|
73
|
+
|
|
74
|
+
@on(Button.Pressed, '#confirm')
|
|
75
|
+
def _on_confirm(self, message: Button.Pressed) -> None:
|
|
76
|
+
self.dismiss(result=ConfirmPromptResult(confirmed=True))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from textual import on
|
|
2
|
+
from textual.binding import Binding
|
|
3
|
+
from textual.events import Blur
|
|
4
|
+
from textual.widgets import Input
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CustomInput(Input):
|
|
8
|
+
BINDINGS = [
|
|
9
|
+
Binding(
|
|
10
|
+
key='ctrl+a',
|
|
11
|
+
action='select_all',
|
|
12
|
+
description='Select all text',
|
|
13
|
+
show=False,
|
|
14
|
+
),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
@on(Blur)
|
|
18
|
+
def on_blur(self, event: Blur) -> None:
|
|
19
|
+
self.selection = 0, 0
|
|
20
|
+
self.cursor_position = len(self.value)
|
|
@@ -10,12 +10,12 @@ from textual.widget import Widget
|
|
|
10
10
|
from textual.widgets import (
|
|
11
11
|
Button,
|
|
12
12
|
ContentSwitcher,
|
|
13
|
-
Input,
|
|
14
13
|
RadioButton,
|
|
15
14
|
RadioSet,
|
|
16
15
|
Switch,
|
|
17
16
|
)
|
|
18
17
|
|
|
18
|
+
from restiny.widgets import CustomInput
|
|
19
19
|
from restiny.widgets.path_chooser import PathChooser
|
|
20
20
|
|
|
21
21
|
|
|
@@ -142,24 +142,24 @@ class TextDynamicField(DynamicField):
|
|
|
142
142
|
self, enabled: bool, key: str, value: str, *args, **kwargs
|
|
143
143
|
) -> None:
|
|
144
144
|
super().__init__(*args, **kwargs)
|
|
145
|
-
self.
|
|
146
|
-
self.
|
|
147
|
-
self.
|
|
145
|
+
self._enabled = enabled
|
|
146
|
+
self._key = key
|
|
147
|
+
self._value = value
|
|
148
148
|
|
|
149
149
|
def compose(self) -> ComposeResult:
|
|
150
150
|
yield Switch(
|
|
151
|
-
value=self.
|
|
151
|
+
value=self._enabled,
|
|
152
152
|
tooltip='Send this field?',
|
|
153
153
|
id='enabled',
|
|
154
154
|
)
|
|
155
|
-
yield
|
|
156
|
-
value=self.
|
|
155
|
+
yield CustomInput(
|
|
156
|
+
value=self._key,
|
|
157
157
|
placeholder='Key',
|
|
158
158
|
select_on_focus=False,
|
|
159
159
|
id='key',
|
|
160
160
|
)
|
|
161
|
-
yield
|
|
162
|
-
value=self.
|
|
161
|
+
yield CustomInput(
|
|
162
|
+
value=self._value,
|
|
163
163
|
placeholder='Value',
|
|
164
164
|
select_on_focus=False,
|
|
165
165
|
id='value',
|
|
@@ -168,8 +168,8 @@ class TextDynamicField(DynamicField):
|
|
|
168
168
|
|
|
169
169
|
async def on_mount(self) -> None:
|
|
170
170
|
self.enabled_switch = self.query_one('#enabled', Switch)
|
|
171
|
-
self.key_input = self.query_one('#key',
|
|
172
|
-
self.value_input = self.query_one('#value',
|
|
171
|
+
self.key_input = self.query_one('#key', CustomInput)
|
|
172
|
+
self.value_input = self.query_one('#value', CustomInput)
|
|
173
173
|
self.remove_button = self.query_one('#remove', Button)
|
|
174
174
|
|
|
175
175
|
@property
|
|
@@ -211,9 +211,9 @@ class TextDynamicField(DynamicField):
|
|
|
211
211
|
elif message.value is False:
|
|
212
212
|
self.post_message(message=self.Disabled(field=self))
|
|
213
213
|
|
|
214
|
-
@on(
|
|
215
|
-
@on(
|
|
216
|
-
def on_input_changed(self, message:
|
|
214
|
+
@on(CustomInput.Changed, '#key')
|
|
215
|
+
@on(CustomInput.Changed, '#value')
|
|
216
|
+
def on_input_changed(self, message: CustomInput.Changed) -> None:
|
|
217
217
|
self.enabled_switch.value = True
|
|
218
218
|
|
|
219
219
|
if self.is_empty:
|
|
@@ -260,51 +260,51 @@ class TextOrFileDynamicField(DynamicField):
|
|
|
260
260
|
**kwargs,
|
|
261
261
|
) -> None:
|
|
262
262
|
super().__init__(*args, **kwargs)
|
|
263
|
-
self.
|
|
264
|
-
self.
|
|
265
|
-
self.
|
|
266
|
-
self.
|
|
263
|
+
self._enabled = enabled
|
|
264
|
+
self._key = key
|
|
265
|
+
self._value = value
|
|
266
|
+
self._value_kind = value_kind
|
|
267
267
|
|
|
268
268
|
def compose(self) -> ComposeResult:
|
|
269
269
|
with RadioSet(id='value-kind', compact=True):
|
|
270
270
|
yield RadioButton(
|
|
271
271
|
label=_ValueKind.TEXT,
|
|
272
|
-
value=bool(self.
|
|
272
|
+
value=bool(self._value_kind == _ValueKind.TEXT),
|
|
273
273
|
id='value-kind-text',
|
|
274
274
|
)
|
|
275
275
|
yield RadioButton(
|
|
276
276
|
label=_ValueKind.FILE,
|
|
277
|
-
value=bool(self.
|
|
277
|
+
value=bool(self._value_kind == _ValueKind.FILE),
|
|
278
278
|
id='value-kind-file',
|
|
279
279
|
)
|
|
280
280
|
yield Switch(
|
|
281
|
-
value=self.
|
|
281
|
+
value=self._enabled,
|
|
282
282
|
tooltip='Send this field?',
|
|
283
283
|
id='enabled',
|
|
284
284
|
)
|
|
285
|
-
yield
|
|
286
|
-
value=self.
|
|
285
|
+
yield CustomInput(
|
|
286
|
+
value=self._key,
|
|
287
287
|
placeholder='Key',
|
|
288
288
|
select_on_focus=False,
|
|
289
289
|
id='key',
|
|
290
290
|
)
|
|
291
291
|
with ContentSwitcher(
|
|
292
292
|
initial='value-text'
|
|
293
|
-
if self.
|
|
293
|
+
if self._value_kind == _ValueKind.TEXT
|
|
294
294
|
else 'value-file',
|
|
295
295
|
id='value-kind-switcher',
|
|
296
296
|
):
|
|
297
|
-
yield
|
|
298
|
-
value=self.
|
|
299
|
-
if self.
|
|
297
|
+
yield CustomInput(
|
|
298
|
+
value=self._value
|
|
299
|
+
if self._value_kind == _ValueKind.TEXT
|
|
300
300
|
else '',
|
|
301
301
|
placeholder='Value',
|
|
302
302
|
select_on_focus=False,
|
|
303
303
|
id='value-text',
|
|
304
304
|
)
|
|
305
305
|
yield PathChooser.file(
|
|
306
|
-
path=self.
|
|
307
|
-
if self.
|
|
306
|
+
path=self._value
|
|
307
|
+
if self._value_kind == _ValueKind.FILE
|
|
308
308
|
else None,
|
|
309
309
|
id='value-file',
|
|
310
310
|
)
|
|
@@ -323,8 +323,8 @@ class TextOrFileDynamicField(DynamicField):
|
|
|
323
323
|
'#value-kind-file', RadioButton
|
|
324
324
|
)
|
|
325
325
|
self.enabled_switch = self.query_one('#enabled', Switch)
|
|
326
|
-
self.key_input = self.query_one('#key',
|
|
327
|
-
self.value_text_input = self.query_one('#value-text',
|
|
326
|
+
self.key_input = self.query_one('#key', CustomInput)
|
|
327
|
+
self.value_text_input = self.query_one('#value-text', CustomInput)
|
|
328
328
|
self.value_file_input = self.query_one('#value-file', PathChooser)
|
|
329
329
|
self.remove_button = self.query_one('#remove', Button)
|
|
330
330
|
|
|
@@ -403,11 +403,11 @@ class TextOrFileDynamicField(DynamicField):
|
|
|
403
403
|
elif message.value is False:
|
|
404
404
|
self.post_message(message=self.Disabled(field=self))
|
|
405
405
|
|
|
406
|
-
@on(
|
|
407
|
-
@on(
|
|
406
|
+
@on(CustomInput.Changed, '#key')
|
|
407
|
+
@on(CustomInput.Changed, '#value-text')
|
|
408
408
|
@on(PathChooser.Changed, '#value-file')
|
|
409
409
|
def on_input_changed(
|
|
410
|
-
self, message:
|
|
410
|
+
self, message: CustomInput.Changed | PathChooser.Changed
|
|
411
411
|
) -> None:
|
|
412
412
|
self.enabled_switch.value = True
|
|
413
413
|
|
|
@@ -493,27 +493,41 @@ class DynamicFields(Widget):
|
|
|
493
493
|
def filled_fields(self) -> list[DynamicField]:
|
|
494
494
|
return [field for field in self.fields if field.is_filled]
|
|
495
495
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
496
|
+
async def add_field(
|
|
497
|
+
self, field: DynamicField, before_last: bool = False
|
|
498
|
+
) -> None:
|
|
499
|
+
if before_last:
|
|
500
|
+
await self.fields_container.mount(field, before=self.fields[-1])
|
|
501
|
+
else:
|
|
502
|
+
await self.fields_container.mount(field)
|
|
503
|
+
|
|
504
|
+
def remove_field(
|
|
505
|
+
self, field: DynamicField, focus_neighbor: bool = False
|
|
506
|
+
) -> None:
|
|
507
|
+
if len(self.fields) == 1:
|
|
508
|
+
self.app.bell()
|
|
509
|
+
return
|
|
510
|
+
elif field is self.fields[-1]:
|
|
511
|
+
self.app.bell()
|
|
512
|
+
return
|
|
513
|
+
|
|
514
|
+
if focus_neighbor:
|
|
515
|
+
field_index = self.fields.index(field)
|
|
516
|
+
|
|
517
|
+
neighbor_field = None
|
|
518
|
+
if field_index == 0:
|
|
519
|
+
neighbor_field = self.fields[field_index + 1]
|
|
520
|
+
else:
|
|
521
|
+
neighbor_field = self.fields[field_index - 1]
|
|
522
|
+
|
|
523
|
+
self.app.set_focus(neighbor_field.query_one(CustomInput))
|
|
524
|
+
|
|
511
525
|
field.add_class('hidden')
|
|
512
526
|
field.remove()
|
|
513
527
|
|
|
514
528
|
@on(DynamicField.Empty)
|
|
515
529
|
def _on_field_is_empty(self, message: DynamicField.Empty) -> None:
|
|
516
|
-
self.
|
|
530
|
+
self.remove_field(field=message.field, focus_neighbor=True)
|
|
517
531
|
self.post_message(
|
|
518
532
|
message=self.FieldEmpty(fields=self, field=message.field)
|
|
519
533
|
)
|
|
@@ -544,23 +558,4 @@ class DynamicFields(Widget):
|
|
|
544
558
|
def _on_field_remove_requested(
|
|
545
559
|
self, message: DynamicField.RemoveRequested
|
|
546
560
|
) -> None:
|
|
547
|
-
self.
|
|
548
|
-
|
|
549
|
-
def _focus_neighbor_field_then_remove(self, field: DynamicField) -> None:
|
|
550
|
-
if len(self.fields) == 1:
|
|
551
|
-
self.app.bell()
|
|
552
|
-
return
|
|
553
|
-
elif field is self.fields[-1]:
|
|
554
|
-
self.app.bell()
|
|
555
|
-
return
|
|
556
|
-
|
|
557
|
-
field_index = self.fields.index(field)
|
|
558
|
-
|
|
559
|
-
neighbor_field = None
|
|
560
|
-
if field_index == 0:
|
|
561
|
-
neighbor_field = self.fields[field_index + 1]
|
|
562
|
-
else:
|
|
563
|
-
neighbor_field = self.fields[field_index - 1]
|
|
564
|
-
|
|
565
|
-
self.app.set_focus(neighbor_field.query_one(Input))
|
|
566
|
-
self.remove_field(field=field)
|
|
561
|
+
self.remove_field(field=message.field, focus_neighbor=True)
|