inquirer-textual 0.2.0__py3-none-any.whl → 0.3.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.
- inquirer_textual/InquirerApp.py +23 -17
- inquirer_textual/common/AppConfig.py +9 -0
- inquirer_textual/common/Choice.py +3 -0
- inquirer_textual/common/ChoiceLabel.py +14 -5
- inquirer_textual/common/InquirerResult.py +24 -0
- inquirer_textual/common/StandardTheme.py +10 -0
- inquirer_textual/prompts.py +57 -45
- inquirer_textual/version.py +1 -1
- inquirer_textual/widgets/InquirerCheckbox.py +29 -13
- inquirer_textual/widgets/InquirerConfirm.py +9 -5
- inquirer_textual/widgets/InquirerEditor.py +72 -0
- inquirer_textual/widgets/InquirerMulti.py +7 -6
- inquirer_textual/widgets/InquirerNumber.py +18 -9
- inquirer_textual/widgets/InquirerPath.py +93 -0
- inquirer_textual/widgets/InquirerPattern.py +169 -0
- inquirer_textual/widgets/InquirerSecret.py +2 -2
- inquirer_textual/widgets/InquirerSelect.py +5 -5
- inquirer_textual/widgets/InquirerText.py +7 -3
- inquirer_textual/widgets/InquirerWidget.py +8 -2
- {inquirer_textual-0.2.0.dist-info → inquirer_textual-0.3.0.dist-info}/METADATA +42 -3
- inquirer_textual-0.3.0.dist-info/RECORD +30 -0
- inquirer_textual/common/Result.py +0 -15
- inquirer_textual-0.2.0.dist-info/RECORD +0 -25
- {inquirer_textual-0.2.0.dist-info → inquirer_textual-0.3.0.dist-info}/WHEEL +0 -0
- {inquirer_textual-0.2.0.dist-info → inquirer_textual-0.3.0.dist-info}/licenses/LICENSE +0 -0
inquirer_textual/InquirerApp.py
CHANGED
|
@@ -10,14 +10,15 @@ from textual.binding import Binding, BindingsMap
|
|
|
10
10
|
from textual.widgets import Footer
|
|
11
11
|
|
|
12
12
|
from inquirer_textual.common.InquirerHeader import InquirerHeader
|
|
13
|
-
from inquirer_textual.common.
|
|
13
|
+
from inquirer_textual.common.InquirerResult import InquirerResult
|
|
14
14
|
from inquirer_textual.common.Shortcut import Shortcut
|
|
15
|
+
from inquirer_textual.common.StandardTheme import StandardTheme
|
|
15
16
|
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
16
17
|
|
|
17
18
|
T = TypeVar('T')
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
class InquirerApp(App[
|
|
21
|
+
class InquirerApp(App[InquirerResult[T]], inherit_bindings=False): # type: ignore[call-arg]
|
|
21
22
|
CSS = """
|
|
22
23
|
App {
|
|
23
24
|
background: black;
|
|
@@ -40,7 +41,7 @@ class InquirerApp(App[Result[T]], inherit_bindings=False): # type: ignore[call-
|
|
|
40
41
|
self.shortcuts: list[Shortcut] | None = None
|
|
41
42
|
self.header: str | list[str] | None = None
|
|
42
43
|
self.show_footer: bool = False
|
|
43
|
-
self.result:
|
|
44
|
+
self.result: InquirerResult[T] | None = None
|
|
44
45
|
self.result_ready: Event | None = None
|
|
45
46
|
self.inquiry_func: Callable[[InquirerApp[T]], None] | None = None
|
|
46
47
|
self.inquiry_func_stop: bool = False
|
|
@@ -63,21 +64,24 @@ class InquirerApp(App[Result[T]], inherit_bindings=False): # type: ignore[call-
|
|
|
63
64
|
if self.inquiry_func:
|
|
64
65
|
self.inquiry_func(self)
|
|
65
66
|
|
|
66
|
-
def action_shortcut(self, command: str):
|
|
67
|
+
async def action_shortcut(self, command: str):
|
|
67
68
|
value = self.widget.current_value() if self.widget else None
|
|
68
|
-
self._handle_result(command, value)
|
|
69
|
+
await self._handle_result(command, value)
|
|
69
70
|
|
|
70
71
|
async def action_quit(self):
|
|
71
|
-
self._handle_result('quit', None)
|
|
72
|
+
await self._handle_result('quit', None)
|
|
72
73
|
|
|
73
|
-
def on_inquirer_widget_submit(self, event: InquirerWidget.Submit) -> None:
|
|
74
|
-
self._handle_result(event.command, event.value)
|
|
74
|
+
async def on_inquirer_widget_submit(self, event: InquirerWidget.Submit) -> None:
|
|
75
|
+
await self._handle_result(event.command, event.value)
|
|
75
76
|
|
|
76
|
-
def _handle_result(self, command: str | None, value: Any | None):
|
|
77
|
+
async def _handle_result(self, command: str | None, value: Any | None):
|
|
77
78
|
if self.result_ready is not None:
|
|
78
|
-
self.result =
|
|
79
|
+
self.result = InquirerResult(self.widget.name if self.widget else None, value, # type: ignore[arg-type]
|
|
80
|
+
command)
|
|
79
81
|
self.result_ready.set()
|
|
80
82
|
else:
|
|
83
|
+
if self.widget:
|
|
84
|
+
await self.widget.set_selected_value(value)
|
|
81
85
|
self.call_after_refresh(lambda: self._terminate(command, value))
|
|
82
86
|
|
|
83
87
|
def _terminate(self, command: str | None = None, value: Any | None = None):
|
|
@@ -85,9 +89,9 @@ class InquirerApp(App[Result[T]], inherit_bindings=False): # type: ignore[call-
|
|
|
85
89
|
if self.result_ready:
|
|
86
90
|
self.result_ready.set()
|
|
87
91
|
if command is not None:
|
|
88
|
-
self.app.exit(
|
|
92
|
+
self.app.exit(InquirerResult(self.widget.name if self.widget else None, value, command))
|
|
89
93
|
else:
|
|
90
|
-
self.exit(value)
|
|
94
|
+
self.exit(InquirerResult(None, value, None))
|
|
91
95
|
|
|
92
96
|
def compose(self) -> ComposeResult:
|
|
93
97
|
if self.header is not None:
|
|
@@ -102,7 +106,7 @@ class InquirerApp(App[Result[T]], inherit_bindings=False): # type: ignore[call-
|
|
|
102
106
|
if self.widget:
|
|
103
107
|
self.call_after_refresh(self.widget.focus)
|
|
104
108
|
|
|
105
|
-
def prompt(self, widget: InquirerWidget, shortcuts: list[Shortcut] | None = None) ->
|
|
109
|
+
def prompt(self, widget: InquirerWidget, shortcuts: list[Shortcut] | None = None) -> InquirerResult[T]:
|
|
106
110
|
if shortcuts:
|
|
107
111
|
self.shortcuts = shortcuts
|
|
108
112
|
self.show_footer = True
|
|
@@ -135,7 +139,7 @@ class InquirerApp(App[Result[T]], inherit_bindings=False): # type: ignore[call-
|
|
|
135
139
|
auto_pilot: AutopilotCallbackType | None = None,
|
|
136
140
|
loop: AbstractEventLoop | None = None,
|
|
137
141
|
inquiry_func: Callable[[InquirerApp[T]], None] | None = None,
|
|
138
|
-
) ->
|
|
142
|
+
) -> InquirerResult[T]:
|
|
139
143
|
if not self.inquiry_func:
|
|
140
144
|
self.inquiry_func = inquiry_func
|
|
141
145
|
return super().run(
|
|
@@ -150,7 +154,9 @@ class InquirerApp(App[Result[T]], inherit_bindings=False): # type: ignore[call-
|
|
|
150
154
|
|
|
151
155
|
def get_theme_variable_defaults(self) -> dict[str, str]:
|
|
152
156
|
return {
|
|
153
|
-
'
|
|
154
|
-
'
|
|
155
|
-
'
|
|
157
|
+
'input-color': StandardTheme.input_color,
|
|
158
|
+
'prompt-color': StandardTheme.prompt_color,
|
|
159
|
+
'error-color': StandardTheme.error_color,
|
|
160
|
+
'select-list-item-highlight-foreground': StandardTheme.select_list_item_highlight_foreground,
|
|
161
|
+
'select-question-mark': StandardTheme.select_question_mark,
|
|
156
162
|
}
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from rich.text import Text
|
|
3
4
|
from textual.widgets import Label
|
|
4
5
|
|
|
5
6
|
from inquirer_textual.common.Choice import Choice
|
|
7
|
+
from inquirer_textual.common.StandardTheme import StandardTheme
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class ChoiceLabel(Label):
|
|
9
|
-
def __init__(self, item: str | Choice):
|
|
10
|
-
|
|
11
|
-
self._text
|
|
11
|
+
def __init__(self, item: str | Choice, pattern: str | None = None):
|
|
12
|
+
self._text = ChoiceLabel._get_text(item, pattern)
|
|
13
|
+
super().__init__(Text(' ').append_text(self._text))
|
|
12
14
|
self.item = item
|
|
13
15
|
|
|
16
|
+
@classmethod
|
|
17
|
+
def _get_text(cls, item: str | Choice, pattern: str | None = None) -> Text:
|
|
18
|
+
result = Text(item if isinstance(item, str) else item.name)
|
|
19
|
+
if pattern:
|
|
20
|
+
result.highlight_words([pattern], style=StandardTheme.prompt_color, case_sensitive=False)
|
|
21
|
+
return result
|
|
22
|
+
|
|
14
23
|
def add_pointer(self):
|
|
15
|
-
self.update(f'
|
|
24
|
+
self.update(Text(f'{StandardTheme.pointer_character} ').append_text(self._text))
|
|
16
25
|
|
|
17
26
|
def remove_pointer(self):
|
|
18
|
-
self.update(
|
|
27
|
+
self.update(Text(' ').append_text(self._text))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TypeVar, Generic
|
|
6
|
+
|
|
7
|
+
T = TypeVar('T')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class InquirerResult(Generic[T]):
|
|
12
|
+
name: str | None
|
|
13
|
+
value: T
|
|
14
|
+
command: str | None
|
|
15
|
+
|
|
16
|
+
def json(self):
|
|
17
|
+
d = {}
|
|
18
|
+
if self.name:
|
|
19
|
+
d[self.name] = self.value
|
|
20
|
+
else:
|
|
21
|
+
d['value'] = self.value
|
|
22
|
+
if self.command:
|
|
23
|
+
d['command'] = self.command
|
|
24
|
+
return json.dumps(d)
|
inquirer_textual/prompts.py
CHANGED
|
@@ -4,73 +4,85 @@ from textual.validation import Validator
|
|
|
4
4
|
|
|
5
5
|
from inquirer_textual.InquirerApp import InquirerApp
|
|
6
6
|
from inquirer_textual.common.Choice import Choice
|
|
7
|
-
from inquirer_textual.common.Result import Result
|
|
8
|
-
from inquirer_textual.common.Shortcut import Shortcut
|
|
9
7
|
from inquirer_textual.widgets.InquirerCheckbox import InquirerCheckbox
|
|
10
8
|
from inquirer_textual.widgets.InquirerConfirm import InquirerConfirm
|
|
9
|
+
from inquirer_textual.widgets.InquirerEditor import InquirerEditor
|
|
11
10
|
from inquirer_textual.widgets.InquirerMulti import InquirerMulti
|
|
12
11
|
from inquirer_textual.widgets.InquirerNumber import InquirerNumber
|
|
12
|
+
from inquirer_textual.widgets.InquirerPath import InquirerPath, PathType
|
|
13
|
+
from inquirer_textual.widgets.InquirerPattern import InquirerPattern
|
|
13
14
|
from inquirer_textual.widgets.InquirerSecret import InquirerSecret
|
|
14
15
|
from inquirer_textual.widgets.InquirerSelect import InquirerSelect
|
|
15
16
|
from inquirer_textual.widgets.InquirerText import InquirerText
|
|
16
17
|
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
app: InquirerApp[str] = InquirerApp()
|
|
22
|
-
app.widget =
|
|
23
|
-
app.
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
def checkbox(message: str, choices: list[str | Choice], enabled: list[str | Choice] | None = None,
|
|
21
|
+
mandatory: bool = False, clear: bool = False) -> list[str | Choice]:
|
|
22
|
+
app: InquirerApp[list[str | Choice]] = InquirerApp()
|
|
23
|
+
app.widget = InquirerCheckbox(message, choices, enabled=enabled, mandatory=mandatory)
|
|
24
|
+
return app.run(inline=True, inline_no_clear=not clear).value
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def confirm(message: str, default: bool = False, mandatory: bool = False, clear: bool = False) -> bool:
|
|
28
|
+
app: InquirerApp[bool] = InquirerApp()
|
|
29
|
+
app.widget = InquirerConfirm(message, default=default, mandatory=mandatory)
|
|
30
|
+
return app.run(inline=True, inline_no_clear=not clear).value
|
|
26
31
|
|
|
27
32
|
|
|
28
|
-
def
|
|
33
|
+
def editor(message: str, clear: bool = False) -> str:
|
|
29
34
|
app: InquirerApp[str] = InquirerApp()
|
|
30
|
-
app.widget =
|
|
31
|
-
app.
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
app.widget = InquirerEditor(message)
|
|
36
|
+
return app.run(inline=True, inline_no_clear=not clear).value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def external(widget: InquirerWidget, clear: bool = False) -> Any:
|
|
40
|
+
app: InquirerApp[Any] = InquirerApp()
|
|
41
|
+
app.widget = widget
|
|
42
|
+
return app.run(inline=True, inline_no_clear=not clear).value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def multi(widgets: list[tuple[str, InquirerWidget]], clear: bool = False) -> dict[str, Any]:
|
|
46
|
+
app: InquirerApp[dict[str, Any]] = InquirerApp()
|
|
47
|
+
app.widget = InquirerMulti(widgets)
|
|
48
|
+
return app.run(inline=True, inline_no_clear=not clear).value
|
|
34
49
|
|
|
35
50
|
|
|
36
|
-
def number(message: str,
|
|
51
|
+
def number(message: str, mandatory: bool = False, clear: bool = False) -> int:
|
|
37
52
|
app: InquirerApp[int] = InquirerApp()
|
|
38
|
-
app.widget = InquirerNumber(message)
|
|
39
|
-
app.
|
|
40
|
-
app.show_footer = bool(shortcuts)
|
|
41
|
-
return app.run(inline=True)
|
|
53
|
+
app.widget = InquirerNumber(message, mandatory=mandatory)
|
|
54
|
+
return app.run(inline=True, inline_no_clear=not clear).value
|
|
42
55
|
|
|
43
56
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
app: InquirerApp[
|
|
47
|
-
app.widget =
|
|
48
|
-
app.
|
|
49
|
-
app.show_footer = bool(shortcuts)
|
|
50
|
-
return app.run(inline=True)
|
|
57
|
+
def path(message: str, exists: bool = False, path_type: PathType = PathType.ANY, mandatory: bool = False,
|
|
58
|
+
clear: bool = False) -> str:
|
|
59
|
+
app: InquirerApp[str] = InquirerApp()
|
|
60
|
+
app.widget = InquirerPath(message, exists=exists, path_type=path_type, mandatory=mandatory)
|
|
61
|
+
return app.run(inline=True, inline_no_clear=not clear).value
|
|
51
62
|
|
|
52
63
|
|
|
53
|
-
def
|
|
54
|
-
|
|
64
|
+
def pattern(message: str, choices: list[str | Choice], default: str | Choice | None = None,
|
|
65
|
+
mandatory: bool = False, clear: bool = False) -> str | Choice:
|
|
55
66
|
app: InquirerApp[str | Choice] = InquirerApp()
|
|
56
|
-
app.widget =
|
|
57
|
-
app.
|
|
58
|
-
app.show_footer = bool(shortcuts)
|
|
59
|
-
return app.run(inline=True)
|
|
67
|
+
app.widget = InquirerPattern(message, choices, default=default, mandatory=mandatory)
|
|
68
|
+
return app.run(inline=True, inline_no_clear=not clear).value
|
|
60
69
|
|
|
61
70
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
app
|
|
65
|
-
app.
|
|
66
|
-
app.shortcuts = shortcuts
|
|
67
|
-
app.show_footer = bool(shortcuts)
|
|
68
|
-
return app.run(inline=True)
|
|
71
|
+
def secret(message: str, mandatory: bool = False, clear: bool = False) -> str:
|
|
72
|
+
app: InquirerApp[str] = InquirerApp()
|
|
73
|
+
app.widget = InquirerSecret(message, mandatory=mandatory)
|
|
74
|
+
return app.run(inline=True, inline_no_clear=not clear).value
|
|
69
75
|
|
|
70
76
|
|
|
71
|
-
def
|
|
72
|
-
|
|
73
|
-
app
|
|
74
|
-
app.
|
|
75
|
-
app.
|
|
76
|
-
|
|
77
|
+
def select(message: str, choices: list[str | Choice], default: str | Choice | None = None,
|
|
78
|
+
mandatory: bool = False, clear: bool = False) -> str | Choice:
|
|
79
|
+
app: InquirerApp[str | Choice] = InquirerApp()
|
|
80
|
+
app.widget = InquirerSelect(message, choices, default=default, mandatory=mandatory)
|
|
81
|
+
return app.run(inline=True, inline_no_clear=not clear).value
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def text(message: str, default: str = '', validators: Validator | Iterable[Validator] | None = None,
|
|
85
|
+
mandatory: bool = False, clear: bool = False) -> str:
|
|
86
|
+
app: InquirerApp[str] = InquirerApp()
|
|
87
|
+
app.widget = InquirerText(message, default=default, validators=validators, mandatory=mandatory)
|
|
88
|
+
return app.run(inline=True, inline_no_clear=not clear).value
|
inquirer_textual/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
version = "0.
|
|
1
|
+
version = "0.3.0"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from textual.app import ComposeResult
|
|
4
|
-
from textual.containers import VerticalGroup
|
|
5
|
-
from textual.widgets import ListItem, ListView
|
|
4
|
+
from textual.containers import VerticalGroup, HorizontalGroup
|
|
5
|
+
from textual.widgets import ListItem, ListView, Static
|
|
6
6
|
from typing_extensions import Self
|
|
7
7
|
|
|
8
8
|
from inquirer_textual.common.Choice import Choice
|
|
@@ -27,20 +27,25 @@ class InquirerCheckbox(InquirerWidget):
|
|
|
27
27
|
("space", "toggle_selected", "Toggle selection"),
|
|
28
28
|
]
|
|
29
29
|
|
|
30
|
-
def __init__(self, message: str, choices: list[str | Choice],
|
|
30
|
+
def __init__(self, message: str, choices: list[str | Choice], name: str | None = None,
|
|
31
|
+
enabled: list[str | Choice] | None = None, mandatory: bool = False):
|
|
31
32
|
"""
|
|
32
33
|
Args:
|
|
33
34
|
message (str): The prompt message to display.
|
|
34
35
|
choices (list[str | Choice]): A list of choices to present to the user.
|
|
36
|
+
name (str | None): The name of the input field.
|
|
35
37
|
enabled (list[str | Choice] | None): A list of choices that should be pre-selected.
|
|
38
|
+
mandatory (bool): Whether at least one selection is mandatory.
|
|
36
39
|
"""
|
|
37
|
-
super().__init__()
|
|
40
|
+
super().__init__(name=name, mandatory=mandatory)
|
|
38
41
|
self.message = message
|
|
39
42
|
self.choices = choices
|
|
40
43
|
self.enabled = enabled
|
|
41
44
|
self.list_view: ListView | None = None
|
|
42
45
|
self.selected_label: ChoiceCheckboxLabel | None = None
|
|
43
46
|
self.selected_item: str | Choice | None = None
|
|
47
|
+
self.selected_value: list[str | Choice] | None = None
|
|
48
|
+
self.show_selected_value: bool = False
|
|
44
49
|
|
|
45
50
|
def on_mount(self):
|
|
46
51
|
self.styles.height = min(10, len(self.choices) + 1)
|
|
@@ -55,7 +60,7 @@ class InquirerCheckbox(InquirerWidget):
|
|
|
55
60
|
self.selected_item = label.item
|
|
56
61
|
|
|
57
62
|
def on_list_view_selected(self, _: ListView.Selected) -> None:
|
|
58
|
-
self.
|
|
63
|
+
self.submit_current_value()
|
|
59
64
|
|
|
60
65
|
def focus(self, scroll_visible: bool = True) -> Self:
|
|
61
66
|
if self.list_view:
|
|
@@ -67,12 +72,23 @@ class InquirerCheckbox(InquirerWidget):
|
|
|
67
72
|
labels = self.query(ChoiceCheckboxLabel)
|
|
68
73
|
return [label.item for label in labels if label.checked]
|
|
69
74
|
|
|
75
|
+
async def set_selected_value(self, value: list[str | Choice]) -> None:
|
|
76
|
+
self.selected_value = value
|
|
77
|
+
self.styles.height = 1
|
|
78
|
+
self.show_selected_value = True
|
|
79
|
+
await self.recompose()
|
|
80
|
+
|
|
70
81
|
def compose(self) -> ComposeResult:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
if self.show_selected_value:
|
|
83
|
+
with HorizontalGroup():
|
|
84
|
+
yield PromptMessage(self.message)
|
|
85
|
+
yield Static(str(self.selected_value))
|
|
86
|
+
else:
|
|
87
|
+
with VerticalGroup():
|
|
88
|
+
items: list[ListItem] = []
|
|
89
|
+
for idx, choice in enumerate(self.choices):
|
|
90
|
+
list_item = ListItem(ChoiceCheckboxLabel(choice))
|
|
91
|
+
items.append(list_item)
|
|
92
|
+
self.list_view = ListView(*items, id='inquirer-checkbox-list-view')
|
|
93
|
+
yield PromptMessage(self.message)
|
|
94
|
+
yield self.list_view
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
3
|
from textual import events
|
|
4
4
|
from textual.app import ComposeResult
|
|
5
5
|
from textual.containers import HorizontalGroup
|
|
6
6
|
from textual.widgets import Label
|
|
7
7
|
|
|
8
|
+
from inquirer_textual.common.PromptMessage import PromptMessage
|
|
9
|
+
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
class InquirerConfirm(InquirerWidget):
|
|
10
13
|
"""A confirmation prompt that allows the user to confirm or reject."""
|
|
@@ -16,17 +19,18 @@ class InquirerConfirm(InquirerWidget):
|
|
|
16
19
|
"""
|
|
17
20
|
can_focus = True
|
|
18
21
|
|
|
19
|
-
def __init__(self, message: str, confirm_character: str = 'y', reject_character: str = 'n',
|
|
20
|
-
mandatory: bool =
|
|
22
|
+
def __init__(self, message: str, confirm_character: str = 'y', reject_character: str = 'n', name: str | None = None,
|
|
23
|
+
default=False, mandatory: bool = False):
|
|
21
24
|
"""
|
|
22
25
|
Args:
|
|
23
26
|
message (str): The prompt message to display.
|
|
24
27
|
confirm_character (str): The character to use for confirmation.
|
|
25
28
|
reject_character (str): The character to use for rejection.
|
|
29
|
+
name (str | None): The name of the prompt.
|
|
26
30
|
default (bool): The default value if the user presses Enter without input.
|
|
27
31
|
mandatory (bool): Whether a response is mandatory.
|
|
28
32
|
"""
|
|
29
|
-
super().__init__(mandatory)
|
|
33
|
+
super().__init__(name=name, mandatory=mandatory)
|
|
30
34
|
if len(confirm_character) != 1 or len(reject_character) != 1:
|
|
31
35
|
raise ValueError("confirm_character and reject_character must be a single character")
|
|
32
36
|
if confirm_character.lower() == reject_character.lower():
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
6
|
+
from textual import events
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.containers import HorizontalGroup
|
|
9
|
+
from textual.widgets import Static
|
|
10
|
+
|
|
11
|
+
from inquirer_textual.common.PromptMessage import PromptMessage
|
|
12
|
+
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InquirerEditor(InquirerWidget):
|
|
16
|
+
"""An input widget that uses an external editor."""
|
|
17
|
+
|
|
18
|
+
DEFAULT_CSS = """
|
|
19
|
+
InquirerEditor {
|
|
20
|
+
height: auto;
|
|
21
|
+
}
|
|
22
|
+
"""
|
|
23
|
+
can_focus = True
|
|
24
|
+
|
|
25
|
+
def __init__(self, message: str | None = None, name: str | None = None):
|
|
26
|
+
"""
|
|
27
|
+
Args:
|
|
28
|
+
message (str): The prompt message to display.
|
|
29
|
+
name (str): The name of the prompt.
|
|
30
|
+
"""
|
|
31
|
+
super().__init__(name=name)
|
|
32
|
+
self.message = message
|
|
33
|
+
|
|
34
|
+
def on_mount(self):
|
|
35
|
+
super().on_mount()
|
|
36
|
+
if not self.message:
|
|
37
|
+
self._launch_editor()
|
|
38
|
+
|
|
39
|
+
def on_key(self, event: events.Key):
|
|
40
|
+
if event.key == 'enter':
|
|
41
|
+
event.stop()
|
|
42
|
+
self._launch_editor()
|
|
43
|
+
|
|
44
|
+
def _launch_editor(self):
|
|
45
|
+
with self.app.suspend():
|
|
46
|
+
tmp = tempfile.NamedTemporaryFile()
|
|
47
|
+
filename = tmp.name
|
|
48
|
+
os.system(f"{get_editor_command()} {filename}")
|
|
49
|
+
with open(filename, 'r') as f:
|
|
50
|
+
content = f.read()
|
|
51
|
+
os.unlink(filename)
|
|
52
|
+
self.post_message(InquirerWidget.Submit(content))
|
|
53
|
+
|
|
54
|
+
def compose(self) -> ComposeResult:
|
|
55
|
+
if self.message:
|
|
56
|
+
with HorizontalGroup():
|
|
57
|
+
yield PromptMessage(self.message)
|
|
58
|
+
yield Static('[dim]Press <enter> to launch editor[/dim]')
|
|
59
|
+
else:
|
|
60
|
+
super().compose()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_editor_command() -> str:
|
|
64
|
+
editor = os.environ.get('VISUAL')
|
|
65
|
+
if not editor:
|
|
66
|
+
editor = os.environ.get('EDITOR')
|
|
67
|
+
if editor == 'nano':
|
|
68
|
+
return 'nano -R'
|
|
69
|
+
elif editor == 'code':
|
|
70
|
+
return 'code -w -n'
|
|
71
|
+
else:
|
|
72
|
+
return 'vim -f -o'
|
|
@@ -15,7 +15,7 @@ class InquirerMulti(InquirerWidget):
|
|
|
15
15
|
}
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
def __init__(self, widgets: list[InquirerWidget]) -> None:
|
|
18
|
+
def __init__(self, widgets: list[tuple[str, InquirerWidget]]) -> None:
|
|
19
19
|
"""
|
|
20
20
|
Args:
|
|
21
21
|
widgets (list[InquirerWidget]): A list of InquirerWidget instances to present in sequence.
|
|
@@ -23,24 +23,25 @@ class InquirerMulti(InquirerWidget):
|
|
|
23
23
|
super().__init__()
|
|
24
24
|
self.widgets = widgets
|
|
25
25
|
self._current_widget_index = 0
|
|
26
|
-
self.
|
|
26
|
+
self._return_values_dict: dict[str, Any] = {}
|
|
27
27
|
|
|
28
28
|
def on_mount(self):
|
|
29
29
|
self.query_one(ContentSwitcher).current = f'widget-{self._current_widget_index}'
|
|
30
30
|
self.query_one(ContentSwitcher).visible_content.focus()
|
|
31
31
|
|
|
32
32
|
def on_inquirer_widget_submit(self, message: InquirerWidget.Submit) -> None:
|
|
33
|
-
self.
|
|
33
|
+
current_widget = self.widgets[self._current_widget_index]
|
|
34
|
+
self._return_values_dict[current_widget[0]] = message.value
|
|
34
35
|
self._current_widget_index += 1
|
|
35
36
|
if self._current_widget_index < len(self.widgets):
|
|
36
37
|
message.stop()
|
|
37
38
|
self.query_one(ContentSwitcher).current = f'widget-{self._current_widget_index}'
|
|
38
39
|
self.query_one(ContentSwitcher).visible_content.focus()
|
|
39
40
|
else:
|
|
40
|
-
message.value = self.
|
|
41
|
+
message.value = self._return_values_dict
|
|
41
42
|
|
|
42
43
|
def compose(self) -> ComposeResult:
|
|
43
44
|
with ContentSwitcher(initial=f'widget-{self._current_widget_index}'):
|
|
44
45
|
for idx, widget in enumerate(self.widgets):
|
|
45
|
-
widget.id = f'widget-{idx}'
|
|
46
|
-
yield widget
|
|
46
|
+
widget[1].id = f'widget-{idx}'
|
|
47
|
+
yield widget[1]
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing_extensions import Self
|
|
4
|
-
|
|
5
3
|
from textual.app import ComposeResult
|
|
6
4
|
from textual.containers import HorizontalGroup
|
|
7
5
|
from textual.widgets import Input
|
|
6
|
+
from typing_extensions import Self, Literal
|
|
8
7
|
|
|
9
|
-
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
10
8
|
from inquirer_textual.common.PromptMessage import PromptMessage
|
|
9
|
+
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
class InquirerNumber(InquirerWidget):
|
|
@@ -26,17 +25,22 @@ class InquirerNumber(InquirerWidget):
|
|
|
26
25
|
}
|
|
27
26
|
"""
|
|
28
27
|
|
|
29
|
-
def __init__(self, message: str
|
|
28
|
+
def __init__(self, message: str, name: str | None = None, input_type: Literal['integer', 'number'] = 'integer',
|
|
29
|
+
mandatory: bool = False):
|
|
30
30
|
"""
|
|
31
31
|
Args:
|
|
32
32
|
message (str): The prompt message to display.
|
|
33
|
+
name (str | None): The name of the input field.
|
|
34
|
+
input_type (Literal['integer', 'number']): The type of number input ('integer' or 'number').
|
|
35
|
+
mandatory (bool): Whether the input is mandatory.
|
|
33
36
|
"""
|
|
34
|
-
super().__init__()
|
|
37
|
+
super().__init__(name=name, mandatory=mandatory)
|
|
35
38
|
self.message = message
|
|
39
|
+
self.input_type = input_type
|
|
36
40
|
self.input: Input | None = None
|
|
37
41
|
|
|
38
|
-
def on_input_submitted(self
|
|
39
|
-
self.
|
|
42
|
+
def on_input_submitted(self) -> None:
|
|
43
|
+
self.submit_current_value()
|
|
40
44
|
|
|
41
45
|
def focus(self, scroll_visible: bool = True) -> Self:
|
|
42
46
|
if self.input:
|
|
@@ -45,10 +49,15 @@ class InquirerNumber(InquirerWidget):
|
|
|
45
49
|
return super().focus(scroll_visible)
|
|
46
50
|
|
|
47
51
|
def current_value(self):
|
|
48
|
-
|
|
52
|
+
if self.input and self.input.value:
|
|
53
|
+
if self.input_type == 'integer':
|
|
54
|
+
return int(self.input.value)
|
|
55
|
+
elif self.input_type == 'number':
|
|
56
|
+
return float(self.input.value)
|
|
57
|
+
return None
|
|
49
58
|
|
|
50
59
|
def compose(self) -> ComposeResult:
|
|
51
60
|
with HorizontalGroup():
|
|
52
61
|
yield PromptMessage(self.message)
|
|
53
|
-
self.input = Input(id="inquirer-number-input", type=
|
|
62
|
+
self.input = Input(id="inquirer-number-input", type=self.input_type)
|
|
54
63
|
yield self.input
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.containers import HorizontalGroup
|
|
8
|
+
from textual.widgets import Input, Static
|
|
9
|
+
from typing_extensions import Self
|
|
10
|
+
|
|
11
|
+
from inquirer_textual.common.PromptMessage import PromptMessage
|
|
12
|
+
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PathType(Enum):
|
|
16
|
+
FILE = 'file'
|
|
17
|
+
DIRECTORY = 'directory'
|
|
18
|
+
ANY = 'any'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class InquirerPath(InquirerWidget):
|
|
22
|
+
"""An input prompt that allows the user to enter a file path."""
|
|
23
|
+
|
|
24
|
+
DEFAULT_CSS = """
|
|
25
|
+
InquirerPath {
|
|
26
|
+
height: auto;
|
|
27
|
+
}
|
|
28
|
+
#inquirer-path-input {
|
|
29
|
+
border: none;
|
|
30
|
+
background: transparent;
|
|
31
|
+
color: $input-color;
|
|
32
|
+
padding: 0;
|
|
33
|
+
height: 1;
|
|
34
|
+
}
|
|
35
|
+
#inquirer-path-error-message {
|
|
36
|
+
color: $error-color;
|
|
37
|
+
height: auto;
|
|
38
|
+
}
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, message: str, name: str | None = None, default: str = '', exists: bool = False,
|
|
42
|
+
path_type: PathType = PathType.ANY, mandatory: bool = False):
|
|
43
|
+
"""
|
|
44
|
+
Args:
|
|
45
|
+
message (str): The prompt message to display.
|
|
46
|
+
default (str): The default value if the user presses Enter without input.
|
|
47
|
+
exists (bool): If True, validate that the entered path exists.
|
|
48
|
+
"""
|
|
49
|
+
super().__init__(name=name, mandatory=mandatory)
|
|
50
|
+
self.message = message
|
|
51
|
+
self.input: Input | None = None
|
|
52
|
+
self.default = default
|
|
53
|
+
self.exists = exists
|
|
54
|
+
self.path_type = path_type
|
|
55
|
+
|
|
56
|
+
def on_mount(self):
|
|
57
|
+
super().on_mount()
|
|
58
|
+
self.input.value = self.default
|
|
59
|
+
|
|
60
|
+
def on_input_submitted(self, submitted: Input.Submitted):
|
|
61
|
+
if self.exists:
|
|
62
|
+
if not Path(submitted.value).exists():
|
|
63
|
+
self._show_validation_error("The specified path does not exist.")
|
|
64
|
+
return
|
|
65
|
+
if self.path_type != PathType.ANY:
|
|
66
|
+
path = Path(submitted.value)
|
|
67
|
+
if self.path_type == PathType.FILE and not path.is_file():
|
|
68
|
+
self._show_validation_error("The specified path is not a file.")
|
|
69
|
+
return
|
|
70
|
+
if self.path_type == PathType.DIRECTORY and not path.is_dir():
|
|
71
|
+
self._show_validation_error("The specified path is not a directory.")
|
|
72
|
+
return
|
|
73
|
+
self.submit_current_value()
|
|
74
|
+
|
|
75
|
+
def _show_validation_error(self, message: str):
|
|
76
|
+
error_message = self.query_one('#inquirer-path-error-message', Static)
|
|
77
|
+
error_message.update(message)
|
|
78
|
+
|
|
79
|
+
def focus(self, scroll_visible: bool = True) -> Self:
|
|
80
|
+
if self.input:
|
|
81
|
+
return self.input.focus(scroll_visible)
|
|
82
|
+
else:
|
|
83
|
+
return super().focus(scroll_visible)
|
|
84
|
+
|
|
85
|
+
def current_value(self):
|
|
86
|
+
return self.input.value if self.input else None
|
|
87
|
+
|
|
88
|
+
def compose(self) -> ComposeResult:
|
|
89
|
+
with HorizontalGroup():
|
|
90
|
+
yield PromptMessage(self.message)
|
|
91
|
+
self.input = Input(id="inquirer-path-input")
|
|
92
|
+
yield self.input
|
|
93
|
+
yield Static("", id="inquirer-path-error-message")
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from textual import on, events
|
|
4
|
+
from textual.app import ComposeResult
|
|
5
|
+
from textual.containers import VerticalGroup, HorizontalGroup
|
|
6
|
+
from textual.css.query import NoMatches
|
|
7
|
+
from textual.reactive import reactive
|
|
8
|
+
from textual.widgets import ListView, ListItem, Input, Static
|
|
9
|
+
from typing_extensions import Self
|
|
10
|
+
|
|
11
|
+
from inquirer_textual.common.Choice import Choice
|
|
12
|
+
from inquirer_textual.common.ChoiceLabel import ChoiceLabel
|
|
13
|
+
from inquirer_textual.common.PromptMessage import PromptMessage
|
|
14
|
+
from inquirer_textual.common.StandardTheme import StandardTheme
|
|
15
|
+
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InquirerPattern(InquirerWidget):
|
|
19
|
+
"""A select widget that allows a single selection from a list of choices with pattern filtering."""
|
|
20
|
+
|
|
21
|
+
DEFAULT_CSS = """
|
|
22
|
+
#inquirer-pattern-list-view {
|
|
23
|
+
background: transparent;
|
|
24
|
+
}
|
|
25
|
+
#inquirer-pattern-list-view ListItem.-highlight {
|
|
26
|
+
color: $select-list-item-highlight-foreground;
|
|
27
|
+
background: transparent;
|
|
28
|
+
}
|
|
29
|
+
#inquirer-pattern-query-container {
|
|
30
|
+
width: auto;
|
|
31
|
+
# border: red;
|
|
32
|
+
}
|
|
33
|
+
#inquirer-pattern-query {
|
|
34
|
+
border: none;
|
|
35
|
+
background: transparent;
|
|
36
|
+
color: $input-color;
|
|
37
|
+
padding: 0;
|
|
38
|
+
height: 1;
|
|
39
|
+
width: 20;
|
|
40
|
+
}
|
|
41
|
+
#inquirer-pattern-query-pointer {
|
|
42
|
+
width: auto;
|
|
43
|
+
color: $prompt-color;
|
|
44
|
+
}
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
candidates: reactive[list[str | Choice]] = reactive([])
|
|
48
|
+
|
|
49
|
+
def __init__(self, message: str, choices: list[str | Choice], name: str | None = None,
|
|
50
|
+
default: str | Choice | None = None, mandatory: bool = True):
|
|
51
|
+
"""
|
|
52
|
+
Args:
|
|
53
|
+
message (str): The prompt message to display.
|
|
54
|
+
choices (list[str | Choice]): A list of choices to present to the user.
|
|
55
|
+
default (str | Choice | None): The default choice to pre-select.
|
|
56
|
+
mandatory (bool): Whether a response is mandatory.
|
|
57
|
+
"""
|
|
58
|
+
super().__init__(name=name, mandatory=mandatory)
|
|
59
|
+
self.message = message
|
|
60
|
+
self.choices = choices
|
|
61
|
+
self.candidates = choices.copy()
|
|
62
|
+
self.list_view: ListView | None = None
|
|
63
|
+
self.selected_label: ChoiceLabel | None = None
|
|
64
|
+
self.selected_item: str | Choice | None = None
|
|
65
|
+
self.default = default
|
|
66
|
+
self.query: Input | None = None
|
|
67
|
+
|
|
68
|
+
def on_mount(self):
|
|
69
|
+
super().on_mount()
|
|
70
|
+
if self.app.is_inline:
|
|
71
|
+
self.styles.height = min(10, len(self.choices) + 1)
|
|
72
|
+
else:
|
|
73
|
+
self.styles.height = '1fr'
|
|
74
|
+
|
|
75
|
+
def on_list_view_highlighted(self, event: ListView.Highlighted) -> None:
|
|
76
|
+
if self.selected_label:
|
|
77
|
+
self.selected_label.remove_pointer()
|
|
78
|
+
if event.item:
|
|
79
|
+
try:
|
|
80
|
+
label = event.item.query_one(ChoiceLabel)
|
|
81
|
+
label.add_pointer()
|
|
82
|
+
self.selected_label = label
|
|
83
|
+
self.selected_item = label.item
|
|
84
|
+
return
|
|
85
|
+
except NoMatches:
|
|
86
|
+
pass
|
|
87
|
+
self.selected_label = None
|
|
88
|
+
self.selected_item = None
|
|
89
|
+
|
|
90
|
+
def on_list_view_selected(self, _: ListView.Selected):
|
|
91
|
+
if isinstance(self.selected_item, Choice):
|
|
92
|
+
self.submit_current_value(self.selected_item.command)
|
|
93
|
+
else:
|
|
94
|
+
self.submit_current_value()
|
|
95
|
+
|
|
96
|
+
def focus(self, scroll_visible: bool = True) -> Self:
|
|
97
|
+
if self.query:
|
|
98
|
+
return self.query.focus(scroll_visible)
|
|
99
|
+
else:
|
|
100
|
+
return super().focus(scroll_visible)
|
|
101
|
+
|
|
102
|
+
def current_value(self):
|
|
103
|
+
return self.selected_item
|
|
104
|
+
|
|
105
|
+
def _collect_list_items(self):
|
|
106
|
+
items: list[ListItem] = []
|
|
107
|
+
for candidate in self.candidates:
|
|
108
|
+
list_item = ListItem(ChoiceLabel(candidate, self.query.value if self.query else None))
|
|
109
|
+
items.append(list_item)
|
|
110
|
+
return items
|
|
111
|
+
|
|
112
|
+
def _find_initial_index(self):
|
|
113
|
+
initial_index = 0
|
|
114
|
+
for idx, choice in enumerate(self.choices):
|
|
115
|
+
if self.default and choice == self.default:
|
|
116
|
+
initial_index = idx
|
|
117
|
+
return initial_index
|
|
118
|
+
|
|
119
|
+
@on(Input.Changed, '#inquirer-pattern-query')
|
|
120
|
+
async def handle_query_changed(self, event: Input.Changed):
|
|
121
|
+
query = event.value.lower()
|
|
122
|
+
if query == '':
|
|
123
|
+
self.candidates = self.choices.copy()
|
|
124
|
+
else:
|
|
125
|
+
filtered = []
|
|
126
|
+
for choice in self.choices:
|
|
127
|
+
name = choice.name if isinstance(choice, Choice) else choice
|
|
128
|
+
if query in name.lower():
|
|
129
|
+
filtered.append(choice)
|
|
130
|
+
self.candidates = filtered
|
|
131
|
+
assert isinstance(self.list_view, ListView)
|
|
132
|
+
await self.list_view.clear()
|
|
133
|
+
list_items = self._collect_list_items()
|
|
134
|
+
await self.list_view.extend(list_items)
|
|
135
|
+
if list_items:
|
|
136
|
+
self.list_view.index = 0
|
|
137
|
+
|
|
138
|
+
def watch_candidates(self, candidates: list[str | Choice]) -> None:
|
|
139
|
+
count_suffix = f'[{len(candidates)}/{len(self.choices)}]'
|
|
140
|
+
try:
|
|
141
|
+
count_widget = self.query_one('#inquirer-pattern-query-count-suffix', Static)
|
|
142
|
+
count_widget.update(count_suffix)
|
|
143
|
+
except NoMatches:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
def on_key(self, event: events.Key):
|
|
147
|
+
assert isinstance(self.list_view, ListView)
|
|
148
|
+
if event.key == 'down':
|
|
149
|
+
event.stop()
|
|
150
|
+
self.list_view.action_cursor_down()
|
|
151
|
+
elif event.key == 'up':
|
|
152
|
+
event.stop()
|
|
153
|
+
self.list_view.action_cursor_up()
|
|
154
|
+
elif event.key == 'enter':
|
|
155
|
+
event.stop()
|
|
156
|
+
self.list_view.action_select_cursor()
|
|
157
|
+
|
|
158
|
+
def compose(self) -> ComposeResult:
|
|
159
|
+
with VerticalGroup():
|
|
160
|
+
self.list_view = ListView(*self._collect_list_items(), id='inquirer-pattern-list-view',
|
|
161
|
+
initial_index=self._find_initial_index())
|
|
162
|
+
with HorizontalGroup():
|
|
163
|
+
yield PromptMessage(self.message)
|
|
164
|
+
yield Static(f'[{len(self.candidates)}/{len(self.choices)}]', id='inquirer-pattern-query-count-suffix')
|
|
165
|
+
with HorizontalGroup(id='inquirer-pattern-query-container'):
|
|
166
|
+
yield Static(f'{StandardTheme.pointer_character} ', id='inquirer-pattern-query-pointer')
|
|
167
|
+
self.query = Input(id="inquirer-pattern-query")
|
|
168
|
+
yield self.query
|
|
169
|
+
yield self.list_view
|
|
@@ -25,12 +25,12 @@ class InquirerSecret(InquirerWidget):
|
|
|
25
25
|
}
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
|
-
def __init__(self, message: str):
|
|
28
|
+
def __init__(self, message: str, name: str | None = None, mandatory: bool = False):
|
|
29
29
|
"""
|
|
30
30
|
Args:
|
|
31
31
|
message (str): The prompt message to display.
|
|
32
32
|
"""
|
|
33
|
-
super().__init__()
|
|
33
|
+
super().__init__(name=name, mandatory=mandatory)
|
|
34
34
|
self.message = message
|
|
35
35
|
self.input: Input | None = None
|
|
36
36
|
|
|
@@ -24,8 +24,8 @@ class InquirerSelect(InquirerWidget):
|
|
|
24
24
|
}
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
|
-
def __init__(self, message: str, choices: list[str | Choice],
|
|
28
|
-
mandatory: bool = True):
|
|
27
|
+
def __init__(self, message: str, choices: list[str | Choice], name: str | None = None,
|
|
28
|
+
default: str | Choice | None = None, mandatory: bool = True):
|
|
29
29
|
"""
|
|
30
30
|
Args:
|
|
31
31
|
message (str): The prompt message to display.
|
|
@@ -33,7 +33,7 @@ class InquirerSelect(InquirerWidget):
|
|
|
33
33
|
default (str | Choice | None): The default choice to pre-select.
|
|
34
34
|
mandatory (bool): Whether a response is mandatory.
|
|
35
35
|
"""
|
|
36
|
-
super().__init__(mandatory)
|
|
36
|
+
super().__init__(name=name, mandatory=mandatory)
|
|
37
37
|
self.message = message
|
|
38
38
|
self.choices = choices
|
|
39
39
|
self.list_view: ListView | None = None
|
|
@@ -58,9 +58,9 @@ class InquirerSelect(InquirerWidget):
|
|
|
58
58
|
|
|
59
59
|
def on_list_view_selected(self, _: ListView.Selected):
|
|
60
60
|
if isinstance(self.selected_item, Choice):
|
|
61
|
-
self.
|
|
61
|
+
self.submit_current_value(self.selected_item.command)
|
|
62
62
|
else:
|
|
63
|
-
self.
|
|
63
|
+
self.submit_current_value()
|
|
64
64
|
|
|
65
65
|
def focus(self, scroll_visible: bool = True) -> Self:
|
|
66
66
|
if self.list_view:
|
|
@@ -28,7 +28,9 @@ class InquirerText(InquirerWidget):
|
|
|
28
28
|
}
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
|
-
def __init__(self, message: str,
|
|
31
|
+
def __init__(self, message: str, name: str | None = None, default: str = '',
|
|
32
|
+
validators: Validator | Iterable[Validator] | None = None,
|
|
33
|
+
mandatory: bool = False):
|
|
32
34
|
"""
|
|
33
35
|
Args:
|
|
34
36
|
message (str): The prompt message to display.
|
|
@@ -36,7 +38,7 @@ class InquirerText(InquirerWidget):
|
|
|
36
38
|
validators (Validator | Iterable[Validator] | None): A validator or list of validators to validate the
|
|
37
39
|
input.
|
|
38
40
|
"""
|
|
39
|
-
super().__init__()
|
|
41
|
+
super().__init__(name=name, mandatory=mandatory)
|
|
40
42
|
self.message = message
|
|
41
43
|
self.input: Input | None = None
|
|
42
44
|
self.default = default
|
|
@@ -48,7 +50,9 @@ class InquirerText(InquirerWidget):
|
|
|
48
50
|
|
|
49
51
|
def on_input_submitted(self, submitted: Input.Submitted):
|
|
50
52
|
if self.validators is None or submitted.validation_result.is_valid:
|
|
51
|
-
self.
|
|
53
|
+
if self.input:
|
|
54
|
+
self.input._cursor_visible = False
|
|
55
|
+
self.submit_current_value()
|
|
52
56
|
|
|
53
57
|
def focus(self, scroll_visible: bool = True) -> Self:
|
|
54
58
|
if self.input:
|
|
@@ -13,8 +13,8 @@ class InquirerWidget(Widget):
|
|
|
13
13
|
self.value = value
|
|
14
14
|
self.command = command
|
|
15
15
|
|
|
16
|
-
def __init__(self, mandatory: bool =
|
|
17
|
-
super().__init__()
|
|
16
|
+
def __init__(self, name: str | None = None, mandatory: bool = False):
|
|
17
|
+
super().__init__(name=name)
|
|
18
18
|
self.mandatory = mandatory
|
|
19
19
|
|
|
20
20
|
def on_mount(self):
|
|
@@ -26,3 +26,9 @@ class InquirerWidget(Widget):
|
|
|
26
26
|
|
|
27
27
|
def current_value(self):
|
|
28
28
|
raise NotImplementedError("Subclasses must implement current_value method")
|
|
29
|
+
|
|
30
|
+
async def set_selected_value(self, value: Any) -> None:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def submit_current_value(self, command: str | None = "select"):
|
|
34
|
+
self.post_message(InquirerWidget.Submit(self.current_value(), command))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: inquirer-textual
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Inquirer based on Textual
|
|
5
5
|
Project-URL: Changelog, https://github.com/robvanderleek/inquirer-textual/blob/master/CHANGELOG.md
|
|
6
6
|
Project-URL: Documentation, https://robvanderleek.github.io/inquirer-textual/
|
|
@@ -9,7 +9,9 @@ Author-email: Rob van der Leek <robvanderleek@gmail.com>
|
|
|
9
9
|
License-Expression: GPL-3.0-or-later
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Python: <3.15,>=3.9
|
|
12
|
-
Requires-Dist: textual>=
|
|
12
|
+
Requires-Dist: textual>=7.1.0
|
|
13
|
+
Provides-Extra: examples
|
|
14
|
+
Requires-Dist: textual-slider>=0.2.0; extra == 'examples'
|
|
13
15
|
Description-Content-Type: text/markdown
|
|
14
16
|
|
|
15
17
|
# Inquirer-Textual
|
|
@@ -36,11 +38,20 @@ Description-Content-Type: text/markdown
|
|
|
36
38
|
|
|
37
39
|
All terminal programs start small. Some stay small, and some become incredibly
|
|
38
40
|
big. The goal of this Python library is to make user input simple for small
|
|
39
|
-
programs,
|
|
41
|
+
programs, while enabling a smooth transition to a comprehensive UI library as
|
|
40
42
|
your program grows.
|
|
41
43
|
|
|
42
44
|
Read the [documentation here](https://robvanderleek.github.io/inquirer-textual/)
|
|
43
45
|
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
Create and activate a virtual environment (for example with
|
|
49
|
+
[uv](https://docs.astral.sh/uv/)), and then install this package:
|
|
50
|
+
|
|
51
|
+
```shell
|
|
52
|
+
pip install inquirer-textual
|
|
53
|
+
```
|
|
54
|
+
|
|
44
55
|
## Development
|
|
45
56
|
|
|
46
57
|
Add this library as an editable local dependency to another project using `uv`:
|
|
@@ -48,3 +59,31 @@ Add this library as an editable local dependency to another project using `uv`:
|
|
|
48
59
|
```shell
|
|
49
60
|
uv add --editable <path-to-inquirer-textual>
|
|
50
61
|
```
|
|
62
|
+
|
|
63
|
+
### Textual console
|
|
64
|
+
|
|
65
|
+
1. Open the Textual Development Console:
|
|
66
|
+
|
|
67
|
+
```shell
|
|
68
|
+
uv run textual console
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
2. Run application in development mode:
|
|
72
|
+
|
|
73
|
+
```shell
|
|
74
|
+
uv run textual run --dev examples/prompt_pattern.py
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Static documentation
|
|
78
|
+
|
|
79
|
+
Generating the static documentation:
|
|
80
|
+
|
|
81
|
+
```shell
|
|
82
|
+
uv run mkdocs build
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Viewing the static documentation:
|
|
86
|
+
|
|
87
|
+
```shell
|
|
88
|
+
uv run mkdocs serve
|
|
89
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
inquirer_textual/InquirerApp.py,sha256=ohFn5KpnZmd0AQVNRC3Ll3EMrXn-nwmedReb8sBRRsY,5979
|
|
2
|
+
inquirer_textual/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
inquirer_textual/prompts.py,sha256=lEVWbel8GvqRCgxGcp5ki_tSNMgVD84eaqawwySqwr4,4236
|
|
4
|
+
inquirer_textual/version.py,sha256=EqIccytbgmIh3EOfv7QIiksdjJSlB-o2LbyXxRtoMGs,18
|
|
5
|
+
inquirer_textual/common/AppConfig.py,sha256=z4PN75ABDQBg8QpRrJrWwL_PD5KEdx2lV1mlp6PEm7I,187
|
|
6
|
+
inquirer_textual/common/Choice.py,sha256=_0EGDbl6NGGiuOqCmfOkkmRX9IP09KQbRRmAdjPxwYs,203
|
|
7
|
+
inquirer_textual/common/ChoiceCheckboxLabel.py,sha256=BZg5vLe6_qc0ipYUaonNfygSOnHyJuEVrB8mmM6tOjw,675
|
|
8
|
+
inquirer_textual/common/ChoiceLabel.py,sha256=23ZtQ4FKPD4EdiuRPtTYPB4TOcQmNGq1JfoOlGowYZg,970
|
|
9
|
+
inquirer_textual/common/InquirerHeader.py,sha256=txl-TRe3DKsOPcmDKy1fhfTa-YAURsjTYePDDHzcPH8,802
|
|
10
|
+
inquirer_textual/common/InquirerResult.py,sha256=2inNdfc2xUTCUFqAGazKJJv6YUQjjFzn-f3NTDxm1-M,481
|
|
11
|
+
inquirer_textual/common/PromptMessage.py,sha256=MCXdsE9j0XlqVKjWUhXB-9qZ24Lk7dA5QWRgUWTdF8o,831
|
|
12
|
+
inquirer_textual/common/Shortcut.py,sha256=9mzLVnphImnehNo1HHU_gPMv-vFi5nXsb1G2CAbpuEY,297
|
|
13
|
+
inquirer_textual/common/StandardTheme.py,sha256=4y0HWIqKDTl1xT9i3TdzRWcDo1oDjvI6Wbzt_XBz2jI,233
|
|
14
|
+
inquirer_textual/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
inquirer_textual/widgets/InquirerCheckbox.py,sha256=84DoTtTLBL61dxJRTq23KPRZYPMcYF6rfT2B41A1gNA,3785
|
|
16
|
+
inquirer_textual/widgets/InquirerConfirm.py,sha256=oJSssC8L7_fWUqBJ46KLxwXajo8ZRVAdTNuxgWq-zvY,2432
|
|
17
|
+
inquirer_textual/widgets/InquirerEditor.py,sha256=n6SfKcaE2sNz8r_jvc9FvCWRyM8bHJcGj8Z5bQkgdnw,1999
|
|
18
|
+
inquirer_textual/widgets/InquirerMulti.py,sha256=wgrngSbbmJ4aTDig0_RTopYH8brTM4jPNvAKL7UZpZs,1754
|
|
19
|
+
inquirer_textual/widgets/InquirerNumber.py,sha256=mS5h0vi0W3q0JJwQxZEjAfoH-ARLoxMdrcffy9ANCSY,2120
|
|
20
|
+
inquirer_textual/widgets/InquirerPath.py,sha256=qiKkoWMS8yOVyOA82oezbrSvpUU4xIZxkOJMRR59FmY,3091
|
|
21
|
+
inquirer_textual/widgets/InquirerPattern.py,sha256=uyN6PchvPKOvQ7sSwYHnmDaplJMZwAVc7qnG7P0zCVo,6428
|
|
22
|
+
inquirer_textual/widgets/InquirerSecret.py,sha256=K2gZ-KsSjYZD-xPQa9PVqtlvIiQ17_X0ujGMnfBhWmo,1666
|
|
23
|
+
inquirer_textual/widgets/InquirerSelect.py,sha256=IsCDwLXTbzKMfwd-jjBdeRY3_734QOjoaJrjbGy3lMU,3237
|
|
24
|
+
inquirer_textual/widgets/InquirerText.py,sha256=xc21nreIFH-8YkuQfanQPsiLbLATfHVK7ggtTuWYU7Y,2316
|
|
25
|
+
inquirer_textual/widgets/InquirerWidget.py,sha256=TUU68Bef4A-S1bomgR0uFiVcQbfudrH-Vp9_h3WJgDg,1060
|
|
26
|
+
inquirer_textual/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
+
inquirer_textual-0.3.0.dist-info/METADATA,sha256=xdacOD3xrAWACJBJkN_jhPT4UNfGCGFW7v-Jz1Fhaps,2410
|
|
28
|
+
inquirer_textual-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
29
|
+
inquirer_textual-0.3.0.dist-info/licenses/LICENSE,sha256=fJuRou64yfkof3ZFdeHcgk7K8pqxis6SBr-vtUEgBuA,1073
|
|
30
|
+
inquirer_textual-0.3.0.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import TypeVar, Generic
|
|
5
|
-
|
|
6
|
-
T = TypeVar('T')
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@dataclass
|
|
10
|
-
class Result(Generic[T]):
|
|
11
|
-
command: str | None
|
|
12
|
-
value: T
|
|
13
|
-
|
|
14
|
-
def __str__(self):
|
|
15
|
-
return str(self.value)
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
inquirer_textual/InquirerApp.py,sha256=2lDgzimH0EXeRnPDlre97q24TPsgIE9mI-cJfhQ8hIk,5377
|
|
2
|
-
inquirer_textual/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
inquirer_textual/prompts.py,sha256=NvaU1hgLe962iSHH3EtmLkbB0K5Vlav38mcHSiYXrOg,3186
|
|
4
|
-
inquirer_textual/version.py,sha256=XQVhijSHeIVMVzY2S4fY9BKxV2XHSTr9VVHsYltGPvQ,18
|
|
5
|
-
inquirer_textual/common/Choice.py,sha256=ZRggsayRKcNMDLRh12Zblol_pusE2keVEXlHkaw5RsE,147
|
|
6
|
-
inquirer_textual/common/ChoiceCheckboxLabel.py,sha256=BZg5vLe6_qc0ipYUaonNfygSOnHyJuEVrB8mmM6tOjw,675
|
|
7
|
-
inquirer_textual/common/ChoiceLabel.py,sha256=Wxt5IZUSHJsVEza1Uj19HZP7oD1uNvclY1UDbaNiOOM,504
|
|
8
|
-
inquirer_textual/common/InquirerHeader.py,sha256=txl-TRe3DKsOPcmDKy1fhfTa-YAURsjTYePDDHzcPH8,802
|
|
9
|
-
inquirer_textual/common/PromptMessage.py,sha256=MCXdsE9j0XlqVKjWUhXB-9qZ24Lk7dA5QWRgUWTdF8o,831
|
|
10
|
-
inquirer_textual/common/Result.py,sha256=mC79hFPITwyXFrrxlHxiumdywKhqGpBM32KhxzJvm-E,255
|
|
11
|
-
inquirer_textual/common/Shortcut.py,sha256=9mzLVnphImnehNo1HHU_gPMv-vFi5nXsb1G2CAbpuEY,297
|
|
12
|
-
inquirer_textual/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
inquirer_textual/widgets/InquirerCheckbox.py,sha256=ytWNqRbJ46iItJxbKfFTHGVapFnJjF9DwG3XtKfRW0E,3008
|
|
14
|
-
inquirer_textual/widgets/InquirerConfirm.py,sha256=RFxSnxj1NS_kqirWWu_VLm0C2Bf4h-3zewDJ_IAtgYs,2293
|
|
15
|
-
inquirer_textual/widgets/InquirerMulti.py,sha256=WhjhEq2j9IL4PIT2kgvClXorazeii4Lxv1TdUDyC60c,1637
|
|
16
|
-
inquirer_textual/widgets/InquirerNumber.py,sha256=05ZEXHVeZ-jzvn7Ts0sJkpkP6oTlgBAWcZGEyrDG0H8,1550
|
|
17
|
-
inquirer_textual/widgets/InquirerSecret.py,sha256=XnMP77O8XWeHvqmttb7xj94a3pgX95a9pEgLvIehmBg,1586
|
|
18
|
-
inquirer_textual/widgets/InquirerSelect.py,sha256=P0_7B3945I-O2HXzXpGO5HDIqD7poR6aa0yOX6a9CGM,3259
|
|
19
|
-
inquirer_textual/widgets/InquirerText.py,sha256=6sQxS87pAooCOdZBcZbxWfUXxqQZA5VYv1ebLv7-3J8,2154
|
|
20
|
-
inquirer_textual/widgets/InquirerWidget.py,sha256=56rcnnK5vqD66WKfmv8V_kZ4YG63Vkxz3pYUq18OJKo,802
|
|
21
|
-
inquirer_textual/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
-
inquirer_textual-0.2.0.dist-info/METADATA,sha256=etLlsW92h4EEQbgExg2JUV0ZNBPEsyBKpdgl4A4o8sE,1768
|
|
23
|
-
inquirer_textual-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
24
|
-
inquirer_textual-0.2.0.dist-info/licenses/LICENSE,sha256=fJuRou64yfkof3ZFdeHcgk7K8pqxis6SBr-vtUEgBuA,1073
|
|
25
|
-
inquirer_textual-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|