inquirer-textual 0.1.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 +61 -0
- inquirer_textual/__init__.py +0 -0
- inquirer_textual/common/Choice.py +9 -0
- inquirer_textual/common/ChoiceCheckboxLabel.py +22 -0
- inquirer_textual/common/ChoiceLabel.py +16 -0
- inquirer_textual/common/PromptMessage.py +30 -0
- inquirer_textual/common/Result.py +13 -0
- inquirer_textual/common/Shortcut.py +13 -0
- inquirer_textual/common/__init__.py +0 -0
- inquirer_textual/prompts.py +61 -0
- inquirer_textual/version.py +1 -0
- inquirer_textual/widgets/InquirerCheckbox.py +77 -0
- inquirer_textual/widgets/InquirerConfirm.py +57 -0
- inquirer_textual/widgets/InquirerMulti.py +46 -0
- inquirer_textual/widgets/InquirerNumber.py +52 -0
- inquirer_textual/widgets/InquirerSecret.py +53 -0
- inquirer_textual/widgets/InquirerSelect.py +80 -0
- inquirer_textual/widgets/InquirerText.py +63 -0
- inquirer_textual/widgets/InquirerWidget.py +26 -0
- inquirer_textual/widgets/__init__.py +0 -0
- inquirer_textual-0.1.0.dist-info/METADATA +37 -0
- inquirer_textual-0.1.0.dist-info/RECORD +24 -0
- inquirer_textual-0.1.0.dist-info/WHEEL +4 -0
- inquirer_textual-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import TypeVar
|
|
2
|
+
|
|
3
|
+
from textual.app import App
|
|
4
|
+
from textual.app import ComposeResult
|
|
5
|
+
from textual.widgets import Footer
|
|
6
|
+
|
|
7
|
+
from inquirer_textual.common.Result import Result
|
|
8
|
+
from inquirer_textual.common.Shortcut import Shortcut
|
|
9
|
+
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
10
|
+
|
|
11
|
+
T = TypeVar('T')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InquirerApp(App[Result[T]], inherit_bindings=False): # type: ignore[call-arg]
|
|
15
|
+
CSS = """
|
|
16
|
+
App {
|
|
17
|
+
background: black;
|
|
18
|
+
}
|
|
19
|
+
Screen {
|
|
20
|
+
border-top: none;
|
|
21
|
+
border-bottom: none;
|
|
22
|
+
background: transparent;
|
|
23
|
+
height: auto;
|
|
24
|
+
}
|
|
25
|
+
"""
|
|
26
|
+
ENABLE_COMMAND_PALETTE = False
|
|
27
|
+
INLINE_PADDING = 0
|
|
28
|
+
|
|
29
|
+
def __init__(self, widget: InquirerWidget, shortcuts: list[Shortcut] | None = None, show_footer: bool = False):
|
|
30
|
+
super().__init__()
|
|
31
|
+
self.widget = widget
|
|
32
|
+
self.shortcuts = shortcuts
|
|
33
|
+
self.show_footer = show_footer
|
|
34
|
+
|
|
35
|
+
def on_mount(self) -> None:
|
|
36
|
+
if self.shortcuts:
|
|
37
|
+
for shortcut in self.shortcuts:
|
|
38
|
+
self._bindings.bind(shortcut.key, f'shortcut("{shortcut.command}")',
|
|
39
|
+
description=shortcut.description,
|
|
40
|
+
show=shortcut.show)
|
|
41
|
+
|
|
42
|
+
def action_shortcut(self, command: str):
|
|
43
|
+
self._exit_select(command)
|
|
44
|
+
|
|
45
|
+
def on_inquirer_widget_submit(self, event: InquirerWidget.Submit) -> None:
|
|
46
|
+
self.call_after_refresh(lambda: self.app.exit(Result(event.command, event.value)))
|
|
47
|
+
|
|
48
|
+
def _exit_select(self, command: str):
|
|
49
|
+
self.call_after_refresh(lambda: self.app.exit(Result(command, self.widget.current_value())))
|
|
50
|
+
|
|
51
|
+
def compose(self) -> ComposeResult:
|
|
52
|
+
yield self.widget
|
|
53
|
+
if self.show_footer:
|
|
54
|
+
yield Footer()
|
|
55
|
+
|
|
56
|
+
def get_theme_variable_defaults(self) -> dict[str, str]:
|
|
57
|
+
return {
|
|
58
|
+
'select-question-mark': '#e5c07b',
|
|
59
|
+
'select-list-item-highlight-foreground': '#61afef',
|
|
60
|
+
'input-color': '#98c379'
|
|
61
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from textual.widgets import Label
|
|
2
|
+
|
|
3
|
+
from inquirer_textual.common.Choice import Choice
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ChoiceCheckboxLabel(Label):
|
|
7
|
+
def __init__(self, item: str | Choice):
|
|
8
|
+
super().__init__(f'\u25cb {item if isinstance(item, str) else item.name}')
|
|
9
|
+
self._text = item if isinstance(item, str) else item.name
|
|
10
|
+
self.item = item
|
|
11
|
+
self.checked = False
|
|
12
|
+
|
|
13
|
+
def check(self):
|
|
14
|
+
self.checked = True
|
|
15
|
+
self.update(f'\u25c9 {self._text}')
|
|
16
|
+
|
|
17
|
+
def uncheck(self):
|
|
18
|
+
self.checked = False
|
|
19
|
+
self.update(f'\u25cb {self._text}')
|
|
20
|
+
|
|
21
|
+
def toggle(self):
|
|
22
|
+
self.uncheck() if self.checked else self.check()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from textual.widgets import Label
|
|
2
|
+
|
|
3
|
+
from inquirer_textual.common.Choice import Choice
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ChoiceLabel(Label):
|
|
7
|
+
def __init__(self, item: str | Choice):
|
|
8
|
+
super().__init__(f' {item if isinstance(item, str) else item.name}')
|
|
9
|
+
self._text = item if isinstance(item, str) else item.name
|
|
10
|
+
self.item = item
|
|
11
|
+
|
|
12
|
+
def add_pointer(self):
|
|
13
|
+
self.update(f'\u276f {self._text}')
|
|
14
|
+
|
|
15
|
+
def remove_pointer(self):
|
|
16
|
+
self.update(f' {self._text}')
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from textual.app import ComposeResult
|
|
2
|
+
from textual.containers import HorizontalGroup
|
|
3
|
+
from textual.widget import Widget
|
|
4
|
+
from textual.widgets import Static, Label
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PromptMessage(Widget):
|
|
8
|
+
DEFAULT_CSS = """
|
|
9
|
+
PromptMessage {
|
|
10
|
+
width: auto;
|
|
11
|
+
height: auto;
|
|
12
|
+
}
|
|
13
|
+
#prompt-message-container {
|
|
14
|
+
width: auto;
|
|
15
|
+
margin-right: 1;
|
|
16
|
+
}
|
|
17
|
+
#prompt-message-question-mark {
|
|
18
|
+
width: auto;
|
|
19
|
+
color: $select-question-mark;
|
|
20
|
+
}
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, message: str):
|
|
24
|
+
super().__init__()
|
|
25
|
+
self.message = message
|
|
26
|
+
|
|
27
|
+
def compose(self) -> ComposeResult:
|
|
28
|
+
with HorizontalGroup(id='prompt-message-container'):
|
|
29
|
+
yield Static('? ', id='prompt-message-question-mark')
|
|
30
|
+
yield Label(self.message)
|
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import Any, Iterable
|
|
2
|
+
|
|
3
|
+
from inquirer_textual.InquirerApp import InquirerApp
|
|
4
|
+
from inquirer_textual.common.Choice import Choice
|
|
5
|
+
from inquirer_textual.widgets.InquirerConfirm import InquirerConfirm
|
|
6
|
+
from inquirer_textual.widgets.InquirerMulti import InquirerMulti
|
|
7
|
+
from inquirer_textual.widgets.InquirerNumber import InquirerNumber
|
|
8
|
+
from inquirer_textual.widgets.InquirerSecret import InquirerSecret
|
|
9
|
+
from inquirer_textual.widgets.InquirerText import InquirerText
|
|
10
|
+
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
11
|
+
from inquirer_textual.common.Result import Result
|
|
12
|
+
from inquirer_textual.common.Shortcut import Shortcut
|
|
13
|
+
from inquirer_textual.widgets.InquirerCheckbox import InquirerCheckbox
|
|
14
|
+
from inquirer_textual.widgets.InquirerSelect import InquirerSelect
|
|
15
|
+
from textual.validation import Validator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def text(message: str, shortcuts: list[Shortcut] | None = None,
|
|
19
|
+
validators: Validator | Iterable[Validator] | None = None) -> Result[str]:
|
|
20
|
+
widget = InquirerText(message, validators=validators)
|
|
21
|
+
app: InquirerApp[str] = InquirerApp(widget, shortcuts, show_footer=bool(shortcuts))
|
|
22
|
+
return app.run(inline=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def secret(message: str, shortcuts: list[Shortcut] | None = None) -> Result[str]:
|
|
26
|
+
widget = InquirerSecret(message)
|
|
27
|
+
app: InquirerApp[str] = InquirerApp(widget, shortcuts, show_footer=bool(shortcuts))
|
|
28
|
+
return app.run(inline=True)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def number(message: str, shortcuts: list[Shortcut] | None = None) -> Result[int]:
|
|
32
|
+
widget = InquirerNumber(message)
|
|
33
|
+
app: InquirerApp[int] = InquirerApp(widget, shortcuts, show_footer=bool(shortcuts))
|
|
34
|
+
return app.run(inline=True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def confirm(message: str, shortcuts: list[Shortcut] | None = None, default: bool = False, mandatory: bool = True) -> \
|
|
38
|
+
Result[bool]:
|
|
39
|
+
widget = InquirerConfirm(message, default=default, mandatory=mandatory)
|
|
40
|
+
app: InquirerApp[bool] = InquirerApp(widget, shortcuts, show_footer=bool(shortcuts))
|
|
41
|
+
return app.run(inline=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def select(message: str, choices: list[str | Choice], shortcuts: list[Shortcut] | None = None,
|
|
45
|
+
default: str | Choice | None = None, mandatory: bool = True) -> Result[str | Choice]:
|
|
46
|
+
widget = InquirerSelect(message, choices, default, mandatory)
|
|
47
|
+
app: InquirerApp[str | Choice] = InquirerApp(widget, shortcuts, show_footer=bool(shortcuts))
|
|
48
|
+
return app.run(inline=True)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def checkbox(message: str, choices: list[str | Choice], shortcuts: list[Shortcut] | None = None,
|
|
52
|
+
enabled: list[str | Choice] | None = None) -> Result[list[str | Choice]]:
|
|
53
|
+
widget = InquirerCheckbox(message, choices, enabled)
|
|
54
|
+
app: InquirerApp[list[str | Choice]] = InquirerApp(widget, shortcuts, show_footer=bool(shortcuts))
|
|
55
|
+
return app.run(inline=True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def multi(widgets: list[InquirerWidget], shortcuts: list[Shortcut] | None = None) -> Result[list[Any]]:
|
|
59
|
+
widget = InquirerMulti(widgets)
|
|
60
|
+
app: InquirerApp[list[Any]] = InquirerApp(widget, shortcuts, show_footer=bool(shortcuts))
|
|
61
|
+
return app.run(inline=True)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = "0.1.0"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import VerticalGroup
|
|
5
|
+
from textual.widgets import ListItem, ListView
|
|
6
|
+
|
|
7
|
+
from inquirer_textual.common.Choice import Choice
|
|
8
|
+
from inquirer_textual.common.ChoiceCheckboxLabel import ChoiceCheckboxLabel
|
|
9
|
+
from inquirer_textual.common.PromptMessage import PromptMessage
|
|
10
|
+
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InquirerCheckbox(InquirerWidget):
|
|
14
|
+
"""A checkbox widget that allows multiple selections from a list of choices."""
|
|
15
|
+
|
|
16
|
+
DEFAULT_CSS = """
|
|
17
|
+
#inquirer-checkbox-list-view {
|
|
18
|
+
background: transparent;
|
|
19
|
+
}
|
|
20
|
+
#inquirer-checkbox-list-view ListItem.-highlight {
|
|
21
|
+
color: $select-list-item-highlight-foreground;
|
|
22
|
+
background: transparent;
|
|
23
|
+
}
|
|
24
|
+
"""
|
|
25
|
+
BINDINGS = [
|
|
26
|
+
("space", "toggle_selected", "Toggle selection"),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
def __init__(self, message: str, choices: list[str | Choice], enabled: list[str | Choice] | None = None):
|
|
30
|
+
"""
|
|
31
|
+
Args:
|
|
32
|
+
message (str): The prompt message to display.
|
|
33
|
+
choices (list[str | Choice]): A list of choices to present to the user.
|
|
34
|
+
enabled (list[str | Choice] | None): A list of choices that should be pre-selected.
|
|
35
|
+
"""
|
|
36
|
+
super().__init__()
|
|
37
|
+
self.message = message
|
|
38
|
+
self.choices = choices
|
|
39
|
+
self.enabled = enabled
|
|
40
|
+
self.list_view: ListView | None = None
|
|
41
|
+
self.selected_label: ChoiceCheckboxLabel | None = None
|
|
42
|
+
self.selected_item: str | Choice | None = None
|
|
43
|
+
|
|
44
|
+
def on_mount(self):
|
|
45
|
+
self.styles.height = min(10, len(self.choices) + 1)
|
|
46
|
+
|
|
47
|
+
def action_toggle_selected(self) -> None:
|
|
48
|
+
if self.selected_label:
|
|
49
|
+
self.selected_label.toggle()
|
|
50
|
+
|
|
51
|
+
def on_list_view_highlighted(self, event: ListView.Highlighted) -> None:
|
|
52
|
+
label = event.item.query_one(ChoiceCheckboxLabel)
|
|
53
|
+
self.selected_label = label
|
|
54
|
+
self.selected_item = label.item
|
|
55
|
+
|
|
56
|
+
def on_list_view_selected(self, _: ListView.Selected) -> None:
|
|
57
|
+
self.post_message(InquirerWidget.Submit(self.current_value()))
|
|
58
|
+
|
|
59
|
+
def focus(self, scroll_visible: bool = True) -> Self:
|
|
60
|
+
if self.list_view:
|
|
61
|
+
return self.list_view.focus(scroll_visible)
|
|
62
|
+
else:
|
|
63
|
+
return super().focus(scroll_visible)
|
|
64
|
+
|
|
65
|
+
def current_value(self):
|
|
66
|
+
labels = self.query(ChoiceCheckboxLabel)
|
|
67
|
+
return [label.item for label in labels if label.checked]
|
|
68
|
+
|
|
69
|
+
def compose(self) -> ComposeResult:
|
|
70
|
+
with VerticalGroup():
|
|
71
|
+
items: list[ListItem] = []
|
|
72
|
+
for idx, choice in enumerate(self.choices):
|
|
73
|
+
list_item = ListItem(ChoiceCheckboxLabel(choice))
|
|
74
|
+
items.append(list_item)
|
|
75
|
+
self.list_view = ListView(*items, id='inquirer-checkbox-list-view')
|
|
76
|
+
yield PromptMessage(self.message)
|
|
77
|
+
yield self.list_view
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
2
|
+
from inquirer_textual.common.PromptMessage import PromptMessage
|
|
3
|
+
from textual import events
|
|
4
|
+
from textual.app import ComposeResult
|
|
5
|
+
from textual.containers import HorizontalGroup
|
|
6
|
+
from textual.widgets import Label
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InquirerConfirm(InquirerWidget):
|
|
10
|
+
"""A confirmation prompt that allows the user to confirm or reject."""
|
|
11
|
+
|
|
12
|
+
DEFAULT_CSS = """
|
|
13
|
+
InquirerConfirm {
|
|
14
|
+
height: auto;
|
|
15
|
+
}
|
|
16
|
+
"""
|
|
17
|
+
can_focus = True
|
|
18
|
+
|
|
19
|
+
def __init__(self, message: str, confirm_character: str = 'y', reject_character: str = 'n', default=False,
|
|
20
|
+
mandatory: bool = True):
|
|
21
|
+
"""
|
|
22
|
+
Args:
|
|
23
|
+
message (str): The prompt message to display.
|
|
24
|
+
confirm_character (str): The character to use for confirmation.
|
|
25
|
+
reject_character (str): The character to use for rejection.
|
|
26
|
+
default (bool): The default value if the user presses Enter without input.
|
|
27
|
+
mandatory (bool): Whether a response is mandatory.
|
|
28
|
+
"""
|
|
29
|
+
super().__init__(mandatory)
|
|
30
|
+
if len(confirm_character) != 1 or len(reject_character) != 1:
|
|
31
|
+
raise ValueError("confirm_character and reject_character must be a single character")
|
|
32
|
+
if confirm_character.lower() == reject_character.lower():
|
|
33
|
+
raise ValueError("confirm_character and reject_character must be different")
|
|
34
|
+
self.message = message
|
|
35
|
+
c = confirm_character if not default else confirm_character.upper()
|
|
36
|
+
r = reject_character if default else reject_character.upper()
|
|
37
|
+
self.label = Label(f'({c}/{r})')
|
|
38
|
+
self.value: bool = default
|
|
39
|
+
|
|
40
|
+
def on_key(self, event: events.Key):
|
|
41
|
+
if event.key.lower() == 'y':
|
|
42
|
+
self.value = True
|
|
43
|
+
self.post_message(InquirerWidget.Submit(self.value))
|
|
44
|
+
elif event.key.lower() == 'n':
|
|
45
|
+
self.value = False
|
|
46
|
+
self.post_message(InquirerWidget.Submit(self.value))
|
|
47
|
+
elif event.key == 'enter':
|
|
48
|
+
event.stop()
|
|
49
|
+
self.post_message(InquirerWidget.Submit(self.value))
|
|
50
|
+
|
|
51
|
+
def current_value(self):
|
|
52
|
+
return self.value
|
|
53
|
+
|
|
54
|
+
def compose(self) -> ComposeResult:
|
|
55
|
+
with HorizontalGroup():
|
|
56
|
+
yield PromptMessage(self.message)
|
|
57
|
+
yield self.label
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.widgets import ContentSwitcher
|
|
5
|
+
|
|
6
|
+
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InquirerMulti(InquirerWidget):
|
|
10
|
+
"""A prompt that allows the user to answer multiple prompts in sequence."""
|
|
11
|
+
|
|
12
|
+
DEFAULT_CSS = """
|
|
13
|
+
InquirerMulti {
|
|
14
|
+
height: auto;
|
|
15
|
+
}
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, widgets: list[InquirerWidget]) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Args:
|
|
21
|
+
widgets (list[InquirerWidget]): A list of InquirerWidget instances to present in sequence.
|
|
22
|
+
"""
|
|
23
|
+
super().__init__()
|
|
24
|
+
self.widgets = widgets
|
|
25
|
+
self._current_widget_index = 0
|
|
26
|
+
self._return_values: list[Any] = []
|
|
27
|
+
|
|
28
|
+
def on_mount(self):
|
|
29
|
+
self.query_one(ContentSwitcher).current = f'widget-{self._current_widget_index}'
|
|
30
|
+
self.query_one(ContentSwitcher).visible_content.focus()
|
|
31
|
+
|
|
32
|
+
def on_inquirer_widget_submit(self, message: InquirerWidget.Submit) -> None:
|
|
33
|
+
self._return_values.append(message.value)
|
|
34
|
+
self._current_widget_index += 1
|
|
35
|
+
if self._current_widget_index < len(self.widgets):
|
|
36
|
+
message.stop()
|
|
37
|
+
self.query_one(ContentSwitcher).current = f'widget-{self._current_widget_index}'
|
|
38
|
+
self.query_one(ContentSwitcher).visible_content.focus()
|
|
39
|
+
else:
|
|
40
|
+
message.value = self._return_values
|
|
41
|
+
|
|
42
|
+
def compose(self) -> ComposeResult:
|
|
43
|
+
with ContentSwitcher(initial=f'widget-{self._current_widget_index}'):
|
|
44
|
+
for idx, widget in enumerate(self.widgets):
|
|
45
|
+
widget.id = f'widget-{idx}'
|
|
46
|
+
yield widget
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import HorizontalGroup
|
|
5
|
+
from textual.widgets import Input
|
|
6
|
+
|
|
7
|
+
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
8
|
+
from inquirer_textual.common.PromptMessage import PromptMessage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InquirerNumber(InquirerWidget):
|
|
12
|
+
"""A number input widget that allows the user to input a numerical value."""
|
|
13
|
+
|
|
14
|
+
DEFAULT_CSS = """
|
|
15
|
+
InquirerNumber {
|
|
16
|
+
height: auto;
|
|
17
|
+
}
|
|
18
|
+
#inquirer-number-input {
|
|
19
|
+
border: none;
|
|
20
|
+
background: transparent;
|
|
21
|
+
color: $input-color;
|
|
22
|
+
padding: 0;
|
|
23
|
+
height: 1;
|
|
24
|
+
}
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, message: str):
|
|
28
|
+
"""
|
|
29
|
+
Args:
|
|
30
|
+
message (str): The prompt message to display.
|
|
31
|
+
"""
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.message = message
|
|
34
|
+
self.input: Input | None = None
|
|
35
|
+
|
|
36
|
+
def on_input_submitted(self, submitted: Input.Submitted):
|
|
37
|
+
self.post_message(InquirerWidget.Submit(submitted.value))
|
|
38
|
+
|
|
39
|
+
def focus(self, scroll_visible: bool = True) -> Self:
|
|
40
|
+
if self.input:
|
|
41
|
+
return self.input.focus(scroll_visible)
|
|
42
|
+
else:
|
|
43
|
+
return super().focus(scroll_visible)
|
|
44
|
+
|
|
45
|
+
def current_value(self):
|
|
46
|
+
return self.input.value if self.input else None
|
|
47
|
+
|
|
48
|
+
def compose(self) -> ComposeResult:
|
|
49
|
+
with HorizontalGroup():
|
|
50
|
+
yield PromptMessage(self.message)
|
|
51
|
+
self.input = Input(id="inquirer-number-input", type="integer")
|
|
52
|
+
yield self.input
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import HorizontalGroup
|
|
5
|
+
from textual.widgets import Input
|
|
6
|
+
|
|
7
|
+
from inquirer_textual.common.PromptMessage import PromptMessage
|
|
8
|
+
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InquirerSecret(InquirerWidget):
|
|
12
|
+
"""A secret input prompt that allows the user to enter a secret value (e.g., password)."""
|
|
13
|
+
|
|
14
|
+
DEFAULT_CSS = """
|
|
15
|
+
InquirerSecret {
|
|
16
|
+
height: auto;
|
|
17
|
+
}
|
|
18
|
+
#inquirer-secret-input {
|
|
19
|
+
border: none;
|
|
20
|
+
background: transparent;
|
|
21
|
+
color: $input-color;
|
|
22
|
+
padding: 0;
|
|
23
|
+
height: 1;
|
|
24
|
+
}
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, message: str):
|
|
28
|
+
"""
|
|
29
|
+
Args:
|
|
30
|
+
message (str): The prompt message to display.
|
|
31
|
+
"""
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.message = message
|
|
34
|
+
self.input: Input | None = None
|
|
35
|
+
|
|
36
|
+
def on_input_submitted(self, submitted: Input.Submitted):
|
|
37
|
+
self.post_message(InquirerWidget.Submit(submitted.value))
|
|
38
|
+
|
|
39
|
+
def focus(self, scroll_visible: bool = True) -> Self:
|
|
40
|
+
if self.input:
|
|
41
|
+
return self.input.focus(scroll_visible)
|
|
42
|
+
else:
|
|
43
|
+
return super().focus(scroll_visible)
|
|
44
|
+
|
|
45
|
+
def current_value(self):
|
|
46
|
+
return self.input.value if self.input else None
|
|
47
|
+
|
|
48
|
+
def compose(self) -> ComposeResult:
|
|
49
|
+
with HorizontalGroup():
|
|
50
|
+
yield PromptMessage(self.message)
|
|
51
|
+
self.input = Input(id="inquirer-secret-input")
|
|
52
|
+
self.input.password = True
|
|
53
|
+
yield self.input
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
from inquirer_textual.common.Choice import Choice
|
|
4
|
+
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
5
|
+
from inquirer_textual.common.PromptMessage import PromptMessage
|
|
6
|
+
from inquirer_textual.common.ChoiceLabel import ChoiceLabel
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.containers import VerticalGroup
|
|
9
|
+
from textual.widgets import ListView, ListItem
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InquirerSelect(InquirerWidget):
|
|
13
|
+
"""A select widget that allows a single selection from a list of choices."""
|
|
14
|
+
|
|
15
|
+
DEFAULT_CSS = """
|
|
16
|
+
#inquirer-select-list-view {
|
|
17
|
+
background: transparent;
|
|
18
|
+
}
|
|
19
|
+
#inquirer-select-list-view ListItem.-highlight {
|
|
20
|
+
color: $select-list-item-highlight-foreground;
|
|
21
|
+
background: transparent;
|
|
22
|
+
}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, message: str, choices: list[str | Choice], default: str | Choice | None = None,
|
|
26
|
+
mandatory: bool = True):
|
|
27
|
+
"""
|
|
28
|
+
Args:
|
|
29
|
+
message (str): The prompt message to display.
|
|
30
|
+
choices (list[str | Choice]): A list of choices to present to the user.
|
|
31
|
+
default (str | Choice | None): The default choice to pre-select.
|
|
32
|
+
mandatory (bool): Whether a response is mandatory.
|
|
33
|
+
"""
|
|
34
|
+
super().__init__(mandatory)
|
|
35
|
+
self.message = message
|
|
36
|
+
self.choices = choices
|
|
37
|
+
self.list_view: ListView | None = None
|
|
38
|
+
self.selected_label: ChoiceLabel | None = None
|
|
39
|
+
self.selected_item: str | Choice | None = None
|
|
40
|
+
self.default = default
|
|
41
|
+
|
|
42
|
+
def on_mount(self):
|
|
43
|
+
super().on_mount()
|
|
44
|
+
self.styles.height = min(10, len(self.choices) + 1)
|
|
45
|
+
|
|
46
|
+
def on_list_view_highlighted(self, event: ListView.Highlighted) -> None:
|
|
47
|
+
if self.selected_label:
|
|
48
|
+
self.selected_label.remove_pointer()
|
|
49
|
+
label = event.item.query_one(ChoiceLabel)
|
|
50
|
+
label.add_pointer()
|
|
51
|
+
self.selected_label = label
|
|
52
|
+
self.selected_item = label.item
|
|
53
|
+
|
|
54
|
+
def on_list_view_selected(self, _: ListView.Selected):
|
|
55
|
+
if isinstance(self.selected_item, Choice):
|
|
56
|
+
self.post_message(InquirerWidget.Submit(self.selected_item, self.selected_item.command))
|
|
57
|
+
else:
|
|
58
|
+
self.post_message(InquirerWidget.Submit(self.selected_item))
|
|
59
|
+
|
|
60
|
+
def focus(self, scroll_visible: bool = True) -> Self:
|
|
61
|
+
if self.list_view:
|
|
62
|
+
return self.list_view.focus(scroll_visible)
|
|
63
|
+
else:
|
|
64
|
+
return super().focus(scroll_visible)
|
|
65
|
+
|
|
66
|
+
def current_value(self):
|
|
67
|
+
return self.selected_item
|
|
68
|
+
|
|
69
|
+
def compose(self) -> ComposeResult:
|
|
70
|
+
with VerticalGroup():
|
|
71
|
+
initial_index = 0
|
|
72
|
+
items: list[ListItem] = []
|
|
73
|
+
for idx, choice in enumerate(self.choices):
|
|
74
|
+
list_item = ListItem(ChoiceLabel(choice))
|
|
75
|
+
items.append(list_item)
|
|
76
|
+
if self.default and choice == self.default:
|
|
77
|
+
initial_index = idx
|
|
78
|
+
self.list_view = ListView(*items, id='inquirer-select-list-view', initial_index=initial_index)
|
|
79
|
+
yield PromptMessage(self.message)
|
|
80
|
+
yield self.list_view
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from typing import Self, Iterable
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.containers import HorizontalGroup
|
|
5
|
+
from textual.validation import Validator
|
|
6
|
+
from textual.widgets import Input
|
|
7
|
+
|
|
8
|
+
from inquirer_textual.widgets.InquirerWidget import InquirerWidget
|
|
9
|
+
from inquirer_textual.common.PromptMessage import PromptMessage
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InquirerText(InquirerWidget):
|
|
13
|
+
"""A text input prompt that allows the user to enter a string."""
|
|
14
|
+
|
|
15
|
+
DEFAULT_CSS = """
|
|
16
|
+
InquirerText {
|
|
17
|
+
height: auto;
|
|
18
|
+
}
|
|
19
|
+
#inquirer-text-input {
|
|
20
|
+
border: none;
|
|
21
|
+
background: transparent;
|
|
22
|
+
color: $input-color;
|
|
23
|
+
padding: 0;
|
|
24
|
+
height: 1;
|
|
25
|
+
}
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, message: str, default: str = '', validators: Validator | Iterable[Validator] | None = None):
|
|
29
|
+
"""
|
|
30
|
+
Args:
|
|
31
|
+
message (str): The prompt message to display.
|
|
32
|
+
default (str): The default value if the user presses Enter without input.
|
|
33
|
+
validators (Validator | Iterable[Validator] | None): A validator or list of validators to validate the
|
|
34
|
+
input.
|
|
35
|
+
"""
|
|
36
|
+
super().__init__()
|
|
37
|
+
self.message = message
|
|
38
|
+
self.input: Input | None = None
|
|
39
|
+
self.default = default
|
|
40
|
+
self.validators = validators
|
|
41
|
+
|
|
42
|
+
def on_mount(self):
|
|
43
|
+
super().on_mount()
|
|
44
|
+
self.input.value = self.default
|
|
45
|
+
|
|
46
|
+
def on_input_submitted(self, submitted: Input.Submitted):
|
|
47
|
+
if self.validators is None or submitted.validation_result.is_valid:
|
|
48
|
+
self.post_message(InquirerWidget.Submit(submitted.value))
|
|
49
|
+
|
|
50
|
+
def focus(self, scroll_visible: bool = True) -> Self:
|
|
51
|
+
if self.input:
|
|
52
|
+
return self.input.focus(scroll_visible)
|
|
53
|
+
else:
|
|
54
|
+
return super().focus(scroll_visible)
|
|
55
|
+
|
|
56
|
+
def current_value(self):
|
|
57
|
+
return self.input.value if self.input else None
|
|
58
|
+
|
|
59
|
+
def compose(self) -> ComposeResult:
|
|
60
|
+
with HorizontalGroup():
|
|
61
|
+
yield PromptMessage(self.message)
|
|
62
|
+
self.input = Input(id="inquirer-text-input", validators=self.validators)
|
|
63
|
+
yield self.input
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from textual.message import Message
|
|
4
|
+
from textual.widget import Widget
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class InquirerWidget(Widget):
|
|
8
|
+
class Submit(Message):
|
|
9
|
+
def __init__(self, value: Any, command: str | None = "select"):
|
|
10
|
+
super().__init__()
|
|
11
|
+
self.value = value
|
|
12
|
+
self.command = command
|
|
13
|
+
|
|
14
|
+
def __init__(self, mandatory: bool = True):
|
|
15
|
+
super().__init__()
|
|
16
|
+
self.mandatory = mandatory
|
|
17
|
+
|
|
18
|
+
def on_mount(self):
|
|
19
|
+
if not self.mandatory:
|
|
20
|
+
self._bindings.bind('ctrl+c', 'exit_now', show=False)
|
|
21
|
+
|
|
22
|
+
def action_exit_now(self):
|
|
23
|
+
self.post_message(InquirerWidget.Submit(None, None))
|
|
24
|
+
|
|
25
|
+
def current_value(self):
|
|
26
|
+
raise NotImplementedError("Subclasses must implement current_value method")
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inquirer-textual
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Inquirer based on Textual
|
|
5
|
+
Project-URL: Changelog, https://github.com/robvanderleek/inquirer-textual/blob/master/CHANGELOG.md
|
|
6
|
+
Project-URL: Documentation, https://robvanderleek.github.io/inquirer-textual/
|
|
7
|
+
Project-URL: Source, https://github.com/robvanderleek/inquirer-textual
|
|
8
|
+
Author-email: Rob van der Leek <robvanderleek@gmail.com>
|
|
9
|
+
License-Expression: GPL-3.0-or-later
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Requires-Dist: textual>=6.2.1
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Inquirer-Textual
|
|
16
|
+
|
|
17
|
+
<div align="center">
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div align="center">
|
|
24
|
+
|
|
25
|
+
[](https://github.com/robvanderleek/inquirer-textual/actions/workflows/main.yml)
|
|
26
|
+
[](https://mypy-lang.org/)
|
|
27
|
+
[](https://github.com/astral-sh/ruff)
|
|
28
|
+
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
## Development
|
|
32
|
+
|
|
33
|
+
Add this library as an editable local dependency to another project using `uv`:
|
|
34
|
+
|
|
35
|
+
```shell
|
|
36
|
+
uv add --editable <path-to-inquirer-textual>
|
|
37
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
inquirer_textual/InquirerApp.py,sha256=jKni2W65cfthR9mVU-jljJjtYr9Zhk2yLUgjcmZegOg,2016
|
|
2
|
+
inquirer_textual/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
inquirer_textual/prompts.py,sha256=Rb4pwVje09wcIVXUCep_Qm_pCb5adQZ3iv2yGGiBsPc,3003
|
|
4
|
+
inquirer_textual/version.py,sha256=aOHawL1zuHMfBWKXqwUkXcW96oXLNCY-CXdHDqkz4g4,18
|
|
5
|
+
inquirer_textual/common/Choice.py,sha256=ZRggsayRKcNMDLRh12Zblol_pusE2keVEXlHkaw5RsE,147
|
|
6
|
+
inquirer_textual/common/ChoiceCheckboxLabel.py,sha256=lUIiR9STUxRsI6psH6y7-MFgK1shDG6W7oPhkdl_3e8,639
|
|
7
|
+
inquirer_textual/common/ChoiceLabel.py,sha256=SMsXjMzGngFrwxkSxtthnX2XdSvBShZVnDxJEttIusY,468
|
|
8
|
+
inquirer_textual/common/PromptMessage.py,sha256=MCXdsE9j0XlqVKjWUhXB-9qZ24Lk7dA5QWRgUWTdF8o,831
|
|
9
|
+
inquirer_textual/common/Result.py,sha256=HE-LkUQwRSy_bdnR1aT9b50Ym0oFs0T-qIMmuwokG44,201
|
|
10
|
+
inquirer_textual/common/Shortcut.py,sha256=Mvd0pbSC1GKphhLqjVZ-i54Nd_bEjoEYmR_Xjb6WyCo,260
|
|
11
|
+
inquirer_textual/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
inquirer_textual/widgets/InquirerCheckbox.py,sha256=CuJYe6gVM6ahxel2rsX9tYJD1LhUbc3tmX5B9LINSJ0,2962
|
|
13
|
+
inquirer_textual/widgets/InquirerConfirm.py,sha256=RFxSnxj1NS_kqirWWu_VLm0C2Bf4h-3zewDJ_IAtgYs,2293
|
|
14
|
+
inquirer_textual/widgets/InquirerMulti.py,sha256=WhjhEq2j9IL4PIT2kgvClXorazeii4Lxv1TdUDyC60c,1637
|
|
15
|
+
inquirer_textual/widgets/InquirerNumber.py,sha256=dFAP2GqqT7Dh0Lb1aZLfjyjbLbSBXqEbMghZXpeplq8,1503
|
|
16
|
+
inquirer_textual/widgets/InquirerSecret.py,sha256=CjPb_u32cOxK5So2GRVhxPqx2-3pjT4Ic-7NmhgwxSo,1540
|
|
17
|
+
inquirer_textual/widgets/InquirerSelect.py,sha256=__T_2qUSPFK7qR82UkRaKeNBjb6-hOaJ3oVQE7_9PBE,3124
|
|
18
|
+
inquirer_textual/widgets/InquirerText.py,sha256=IMev5EmYLs90M0TrX0tuhXkMyp8wBHIuyaHqwJzhqiY,2089
|
|
19
|
+
inquirer_textual/widgets/InquirerWidget.py,sha256=yxQ2qcqp1dsBXZUBqiqKzcSt3aTUjEPi2I0MbQ7E7Z0,762
|
|
20
|
+
inquirer_textual/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
inquirer_textual-0.1.0.dist-info/METADATA,sha256=fBixuYNdQCuU-nA09XmImFHeakveZwb5M_r8lvR_Pho,1339
|
|
22
|
+
inquirer_textual-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
23
|
+
inquirer_textual-0.1.0.dist-info/licenses/LICENSE,sha256=fJuRou64yfkof3ZFdeHcgk7K8pqxis6SBr-vtUEgBuA,1073
|
|
24
|
+
inquirer_textual-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Rob van der Leek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|