restiny 0.2.1__py3-none-any.whl → 0.6.1__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 +28 -14
- restiny/assets/style.tcss +56 -2
- restiny/consts.py +236 -0
- restiny/data/db.py +60 -0
- restiny/data/models.py +111 -0
- restiny/data/repos.py +455 -0
- restiny/data/sql/__init__.py +3 -0
- restiny/entities.py +438 -0
- restiny/enums.py +14 -5
- restiny/httpx_auths.py +52 -0
- restiny/ui/__init__.py +17 -0
- restiny/ui/app.py +586 -0
- restiny/ui/collections_area.py +594 -0
- restiny/ui/environments_screen.py +270 -0
- restiny/ui/request_area.py +602 -0
- restiny/{core → ui}/response_area.py +4 -1
- restiny/ui/settings_screen.py +73 -0
- restiny/ui/top_bar_area.py +60 -0
- restiny/{core → ui}/url_area.py +54 -38
- restiny/utils.py +52 -15
- restiny/widgets/__init__.py +15 -1
- restiny/widgets/collections_tree.py +74 -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.6.1.dist-info}/METADATA +7 -5
- restiny-0.6.1.dist-info/RECORD +38 -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-0.2.1.dist-info → restiny-0.6.1.dist-info}/WHEEL +0 -0
- {restiny-0.2.1.dist-info → restiny-0.6.1.dist-info}/entry_points.txt +0 -0
- {restiny-0.2.1.dist-info → restiny-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {restiny-0.2.1.dist-info → restiny-0.6.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import Horizontal
|
|
5
|
+
from textual.widget import Widget
|
|
6
|
+
from textual.widgets import Select
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from restiny.ui.app import RESTinyApp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TopBarArea(Widget):
|
|
13
|
+
app: 'RESTinyApp'
|
|
14
|
+
|
|
15
|
+
DEFAULT_CSS = """
|
|
16
|
+
TopBarArea {
|
|
17
|
+
width: 1fr;
|
|
18
|
+
height: auto;
|
|
19
|
+
align: right middle;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Select {
|
|
23
|
+
width: 24;
|
|
24
|
+
}
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def compose(self) -> ComposeResult:
|
|
28
|
+
with Horizontal(classes='w-auto h-auto'):
|
|
29
|
+
yield Select(
|
|
30
|
+
[],
|
|
31
|
+
prompt='No environment',
|
|
32
|
+
allow_blank=True,
|
|
33
|
+
compact=True,
|
|
34
|
+
id='environment',
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def on_mount(self) -> None:
|
|
38
|
+
self.environment_select = self.query_one('#environment', Select)
|
|
39
|
+
|
|
40
|
+
self.populate()
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def environment(self) -> str:
|
|
44
|
+
if self.environment_select.value == Select.BLANK:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
return self.environment_select.value
|
|
48
|
+
|
|
49
|
+
def populate(self) -> None:
|
|
50
|
+
prev_selected_environment = self.environment_select.value
|
|
51
|
+
environments = [
|
|
52
|
+
environment.name
|
|
53
|
+
for environment in self.app.environments_repo.list().data
|
|
54
|
+
if environment.name != 'global'
|
|
55
|
+
]
|
|
56
|
+
self.environment_select.set_options(
|
|
57
|
+
(environment, environment) for environment in environments
|
|
58
|
+
)
|
|
59
|
+
if prev_selected_environment in environments:
|
|
60
|
+
self.environment_select.value = prev_selected_environment
|
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,30 @@ 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
|
+
|
|
113
|
+
def clear(self) -> None:
|
|
114
|
+
self.method = HTTPMethod.GET
|
|
115
|
+
self.url = ''
|
|
116
|
+
|
|
101
117
|
@on(Button.Pressed, '#send-request')
|
|
102
|
-
@on(
|
|
118
|
+
@on(CustomInput.Submitted, '#url')
|
|
103
119
|
def _on_send_request(
|
|
104
|
-
self, message: Button.Pressed |
|
|
120
|
+
self, message: Button.Pressed | CustomInput.Submitted
|
|
105
121
|
) -> None:
|
|
106
122
|
if self.request_pending:
|
|
107
123
|
return
|
|
@@ -109,7 +125,7 @@ class URLArea(Static):
|
|
|
109
125
|
self.post_message(message=self.SendRequest())
|
|
110
126
|
|
|
111
127
|
@on(Button.Pressed, '#cancel-request')
|
|
112
|
-
@on(
|
|
128
|
+
@on(CustomInput.Submitted, '#url')
|
|
113
129
|
def _on_cancel_request(self, message: Button.Pressed) -> None:
|
|
114
130
|
if not self.request_pending:
|
|
115
131
|
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,74 @@
|
|
|
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
|
+
)
|
|
64
|
+
node.data = {
|
|
65
|
+
'method': method,
|
|
66
|
+
'name': name,
|
|
67
|
+
'id': id,
|
|
68
|
+
}
|
|
69
|
+
self.node_by_id[id] = node
|
|
70
|
+
return node
|
|
71
|
+
|
|
72
|
+
def remove(self, node: TreeNode) -> None:
|
|
73
|
+
del self.node_by_id[node.data['id']]
|
|
74
|
+
node.remove()
|
|
@@ -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)
|