inquirer-textual 0.2.0__py3-none-any.whl → 0.4.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.
@@ -10,22 +10,19 @@ 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.Result import Result
13
+ from inquirer_textual.common.InquirerResult import InquirerResult
14
14
  from inquirer_textual.common.Shortcut import Shortcut
15
+ from inquirer_textual.common.defaults import DEFAULT_THEME
15
16
  from inquirer_textual.widgets.InquirerWidget import InquirerWidget
16
17
 
17
18
  T = TypeVar('T')
18
19
 
19
20
 
20
- class InquirerApp(App[Result[T]], inherit_bindings=False): # type: ignore[call-arg]
21
+ class InquirerApp(App[InquirerResult[T]], inherit_bindings=False): # type: ignore[call-arg]
21
22
  CSS = """
22
- App {
23
- background: black;
24
- }
25
23
  Screen {
26
24
  border-top: none;
27
25
  border-bottom: none;
28
- background: transparent;
29
26
  height: auto;
30
27
  }
31
28
  """
@@ -35,18 +32,21 @@ class InquirerApp(App[Result[T]], inherit_bindings=False): # type: ignore[call-
35
32
  Binding("ctrl+d", "quit", "Quit", show=False, priority=True)
36
33
  ]
37
34
 
38
- def __init__(self) -> None:
35
+ def __init__(self, theme: str = 'inquirer-textual-default') -> None:
36
+ self._theme = theme
39
37
  self.widget: InquirerWidget | None = None
40
38
  self.shortcuts: list[Shortcut] | None = None
41
39
  self.header: str | list[str] | None = None
42
40
  self.show_footer: bool = False
43
- self.result: Result[T] | None = None
41
+ self.result: InquirerResult[T] | None = None
44
42
  self.result_ready: Event | None = None
45
43
  self.inquiry_func: Callable[[InquirerApp[T]], None] | None = None
46
44
  self.inquiry_func_stop: bool = False
47
45
  super().__init__()
48
46
 
49
47
  def on_mount(self) -> None:
48
+ self.register_theme(DEFAULT_THEME)
49
+ self.theme = self._theme
50
50
  self._update_bindings()
51
51
  if self.inquiry_func:
52
52
  self.run_worker(self.inquiry_func_worker, thread=True)
@@ -63,21 +63,24 @@ class InquirerApp(App[Result[T]], inherit_bindings=False): # type: ignore[call-
63
63
  if self.inquiry_func:
64
64
  self.inquiry_func(self)
65
65
 
66
- def action_shortcut(self, command: str):
66
+ async def action_shortcut(self, command: str):
67
67
  value = self.widget.current_value() if self.widget else None
68
- self._handle_result(command, value)
68
+ await self._handle_result(command, value)
69
69
 
70
70
  async def action_quit(self):
71
- self._handle_result('quit', None)
71
+ await self._handle_result('quit', None)
72
72
 
73
- def on_inquirer_widget_submit(self, event: InquirerWidget.Submit) -> None:
74
- self._handle_result(event.command, event.value)
73
+ async def on_inquirer_widget_submit(self, event: InquirerWidget.Submit) -> None:
74
+ await self._handle_result(event.command, event.value)
75
75
 
76
- def _handle_result(self, command: str | None, value: Any | None):
76
+ async def _handle_result(self, command: str | None, value: Any | None):
77
77
  if self.result_ready is not None:
78
- self.result = Result(command, value) # type: ignore[arg-type]
78
+ self.result = InquirerResult(self.widget.name if self.widget else None, value, # type: ignore[arg-type]
79
+ command)
79
80
  self.result_ready.set()
80
81
  else:
82
+ if self.widget:
83
+ await self.widget.set_selected_value(value)
81
84
  self.call_after_refresh(lambda: self._terminate(command, value))
82
85
 
83
86
  def _terminate(self, command: str | None = None, value: Any | None = None):
@@ -85,9 +88,9 @@ class InquirerApp(App[Result[T]], inherit_bindings=False): # type: ignore[call-
85
88
  if self.result_ready:
86
89
  self.result_ready.set()
87
90
  if command is not None:
88
- self.app.exit(Result(command, value))
91
+ self.app.exit(InquirerResult(self.widget.name if self.widget else None, value, command))
89
92
  else:
90
- self.exit(value)
93
+ self.exit(InquirerResult(None, value, None))
91
94
 
92
95
  def compose(self) -> ComposeResult:
93
96
  if self.header is not None:
@@ -102,7 +105,7 @@ class InquirerApp(App[Result[T]], inherit_bindings=False): # type: ignore[call-
102
105
  if self.widget:
103
106
  self.call_after_refresh(self.widget.focus)
104
107
 
105
- def prompt(self, widget: InquirerWidget, shortcuts: list[Shortcut] | None = None) -> Result[T]:
108
+ def prompt(self, widget: InquirerWidget, shortcuts: list[Shortcut] | None = None) -> InquirerResult[T]:
106
109
  if shortcuts:
107
110
  self.shortcuts = shortcuts
108
111
  self.show_footer = True
@@ -135,7 +138,7 @@ class InquirerApp(App[Result[T]], inherit_bindings=False): # type: ignore[call-
135
138
  auto_pilot: AutopilotCallbackType | None = None,
136
139
  loop: AbstractEventLoop | None = None,
137
140
  inquiry_func: Callable[[InquirerApp[T]], None] | None = None,
138
- ) -> Result[T]:
141
+ ) -> InquirerResult[T]:
139
142
  if not self.inquiry_func:
140
143
  self.inquiry_func = inquiry_func
141
144
  return super().run(
@@ -150,7 +153,6 @@ class InquirerApp(App[Result[T]], inherit_bindings=False): # type: ignore[call-
150
153
 
151
154
  def get_theme_variable_defaults(self) -> dict[str, str]:
152
155
  return {
153
- 'select-question-mark': '#e5c07b',
154
- 'select-list-item-highlight-foreground': '#61afef',
155
- 'input-color': '#98c379'
156
+ 'inquirer-textual-question-mark': self.current_theme.foreground or 'initial',
157
+ 'inquirer-textual-input-color': self.current_theme.foreground or 'initial',
156
158
  }
@@ -0,0 +1,22 @@
1
+ from rich.text import Text
2
+ from textual.app import ComposeResult
3
+ from textual.widget import Widget
4
+ from textual.widgets import Static
5
+
6
+
7
+ class Answer(Widget):
8
+ DEFAULT_CSS = """
9
+ Answer {
10
+ height: auto;
11
+ }
12
+ #inquirer-textual-answer {
13
+ color: $inquirer-textual-input-color;
14
+ }
15
+ """
16
+
17
+ def __init__(self, text: str):
18
+ super().__init__()
19
+ self.text = text
20
+
21
+ def compose(self) -> ComposeResult:
22
+ yield Static(Text(self.text), id='inquirer-textual-answer')
@@ -7,3 +7,6 @@ class Choice:
7
7
  name: str
8
8
  data: Any = None
9
9
  command: str = 'select'
10
+
11
+ def __str__(self) -> str:
12
+ return self.name
@@ -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.defaults import DEFAULT_THEME, POINTER_CHARACTER
6
8
 
7
9
 
8
10
  class ChoiceLabel(Label):
9
- def __init__(self, item: str | Choice):
10
- super().__init__(f' {item if isinstance(item, str) else item.name}')
11
- self._text = item if isinstance(item, str) else item.name
11
+ def __init__(self, item: str | Choice, pattern: str | None = None):
12
+ self._text = self._get_text(item, pattern)
13
+ super().__init__(Text(' ').append_text(self._text))
12
14
  self.item = item
13
15
 
16
+ def _get_text(self, item: str | Choice, pattern: str | None = None) -> Text:
17
+ result = Text(item if isinstance(item, str) else item.name)
18
+ if pattern:
19
+ result.highlight_words([pattern], style=self.app.current_theme.accent or DEFAULT_THEME.accent,
20
+ case_sensitive=False)
21
+ return result
22
+
14
23
  def add_pointer(self):
15
- self.update(f'\u276f {self._text}')
24
+ self.update(Text(f'{POINTER_CHARACTER} ').append_text(self._text))
16
25
 
17
26
  def remove_pointer(self):
18
- self.update(f' {self._text}')
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)
@@ -4,9 +4,9 @@ from textual.widget import Widget
4
4
  from textual.widgets import Static, Label
5
5
 
6
6
 
7
- class PromptMessage(Widget):
7
+ class Prompt(Widget):
8
8
  DEFAULT_CSS = """
9
- PromptMessage {
9
+ Prompt {
10
10
  width: auto;
11
11
  height: auto;
12
12
  }
@@ -16,7 +16,7 @@ class PromptMessage(Widget):
16
16
  }
17
17
  #prompt-message-question-mark {
18
18
  width: auto;
19
- color: $select-question-mark;
19
+ color: $inquirer-textual-question-mark;
20
20
  }
21
21
  """
22
22
 
@@ -0,0 +1,24 @@
1
+ from textual.theme import Theme
2
+
3
+ DEFAULT_THEME = Theme(
4
+ name="inquirer-textual-default",
5
+ primary="#0178D4",
6
+ secondary="#004578",
7
+ accent="#c678dd",
8
+ warning="#ffa62b",
9
+ error="#e06c75",
10
+ success="#4EBF71",
11
+ foreground="#e0e0e0",
12
+ background="black",
13
+ surface="transparent",
14
+ variables={
15
+ 'block-cursor-foreground': '#61afef',
16
+ 'block-cursor-background': 'transparent',
17
+ 'block-cursor-blurred-foreground': '#61afef',
18
+ 'block-cursor-blurred-background': 'transparent',
19
+ 'inquirer-textual-question-mark': '#e5c07b',
20
+ 'inquirer-textual-input-color': '#98c379',
21
+ }
22
+ )
23
+
24
+ POINTER_CHARACTER = '\u276f'
@@ -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 text(message: str, shortcuts: list[Shortcut] | None = None,
20
- validators: Validator | Iterable[Validator] | None = None) -> Result[str]:
21
- app: InquirerApp[str] = InquirerApp()
22
- app.widget = InquirerText(message, validators=validators)
23
- app.shortcuts = shortcuts
24
- app.show_footer = bool(shortcuts)
25
- return app.run(inline=True)
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 secret(message: str, shortcuts: list[Shortcut] | None = None) -> Result[str]:
33
+ def editor(message: str, clear: bool = False) -> str:
29
34
  app: InquirerApp[str] = InquirerApp()
30
- app.widget = InquirerSecret(message)
31
- app.shortcuts = shortcuts
32
- app.show_footer = bool(shortcuts)
33
- return app.run(inline=True)
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: dict[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, shortcuts: list[Shortcut] | None = None) -> Result[int]:
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.shortcuts = shortcuts
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 confirm(message: str, shortcuts: list[Shortcut] | None = None, default: bool = False, mandatory: bool = True) -> \
45
- Result[bool]:
46
- app: InquirerApp[bool] = InquirerApp()
47
- app.widget = InquirerConfirm(message, default=default, mandatory=mandatory)
48
- app.shortcuts = shortcuts
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 select(message: str, choices: list[str | Choice], shortcuts: list[Shortcut] | None = None,
54
- default: str | Choice | None = None, mandatory: bool = True) -> Result[str | Choice]:
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 = InquirerSelect(message, choices, default, mandatory)
57
- app.shortcuts = shortcuts
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 checkbox(message: str, choices: list[str | Choice], shortcuts: list[Shortcut] | None = None,
63
- enabled: list[str | Choice] | None = None) -> Result[list[str | Choice]]:
64
- app: InquirerApp[list[str | Choice]] = InquirerApp()
65
- app.widget = InquirerCheckbox(message, choices, enabled)
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 multi(widgets: list[InquirerWidget], shortcuts: list[Shortcut] | None = None) -> Result[list[Any]]:
72
- app: InquirerApp[list[Any]] = InquirerApp()
73
- app.widget = InquirerMulti(widgets)
74
- app.shortcuts = shortcuts
75
- app.show_footer = bool(shortcuts)
76
- return app.run(inline=True)
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
@@ -1 +1 @@
1
- version = "0.2.0"
1
+ version = "0.4.0"
@@ -1,46 +1,43 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from textual.app import ComposeResult
4
- from textual.containers import VerticalGroup
4
+ from textual.containers import VerticalGroup, HorizontalGroup
5
5
  from textual.widgets import ListItem, ListView
6
6
  from typing_extensions import Self
7
7
 
8
+ from inquirer_textual.common.Answer import Answer
8
9
  from inquirer_textual.common.Choice import Choice
9
10
  from inquirer_textual.common.ChoiceCheckboxLabel import ChoiceCheckboxLabel
10
- from inquirer_textual.common.PromptMessage import PromptMessage
11
+ from inquirer_textual.common.Prompt import Prompt
11
12
  from inquirer_textual.widgets.InquirerWidget import InquirerWidget
12
13
 
13
14
 
14
15
  class InquirerCheckbox(InquirerWidget):
15
16
  """A checkbox widget that allows multiple selections from a list of choices."""
16
17
 
17
- DEFAULT_CSS = """
18
- #inquirer-checkbox-list-view {
19
- background: transparent;
20
- }
21
- #inquirer-checkbox-list-view ListItem.-highlight {
22
- color: $select-list-item-highlight-foreground;
23
- background: transparent;
24
- }
25
- """
26
18
  BINDINGS = [
27
19
  ("space", "toggle_selected", "Toggle selection"),
28
20
  ]
29
21
 
30
- def __init__(self, message: str, choices: list[str | Choice], enabled: list[str | Choice] | None = None):
22
+ def __init__(self, message: str, choices: list[str | Choice], name: str | None = None,
23
+ enabled: list[str | Choice] | None = None, mandatory: bool = False):
31
24
  """
32
25
  Args:
33
26
  message (str): The prompt message to display.
34
27
  choices (list[str | Choice]): A list of choices to present to the user.
28
+ name (str | None): The name of the input field.
35
29
  enabled (list[str | Choice] | None): A list of choices that should be pre-selected.
30
+ mandatory (bool): Whether at least one selection is mandatory.
36
31
  """
37
- super().__init__()
32
+ super().__init__(name=name, mandatory=mandatory)
38
33
  self.message = message
39
34
  self.choices = choices
40
35
  self.enabled = enabled
41
36
  self.list_view: ListView | None = None
42
37
  self.selected_label: ChoiceCheckboxLabel | None = None
43
38
  self.selected_item: str | Choice | None = None
39
+ self.selected_value: list[str | Choice] | None = None
40
+ self.show_selected_value: bool = False
44
41
 
45
42
  def on_mount(self):
46
43
  self.styles.height = min(10, len(self.choices) + 1)
@@ -55,7 +52,7 @@ class InquirerCheckbox(InquirerWidget):
55
52
  self.selected_item = label.item
56
53
 
57
54
  def on_list_view_selected(self, _: ListView.Selected) -> None:
58
- self.post_message(InquirerWidget.Submit(self.current_value()))
55
+ self.submit_current_value()
59
56
 
60
57
  def focus(self, scroll_visible: bool = True) -> Self:
61
58
  if self.list_view:
@@ -67,12 +64,23 @@ class InquirerCheckbox(InquirerWidget):
67
64
  labels = self.query(ChoiceCheckboxLabel)
68
65
  return [label.item for label in labels if label.checked]
69
66
 
67
+ async def set_selected_value(self, value: list[str | Choice]) -> None:
68
+ self.selected_value = value
69
+ self.styles.height = 1
70
+ self.show_selected_value = True
71
+ await self.recompose()
72
+
70
73
  def compose(self) -> ComposeResult:
71
- with VerticalGroup():
72
- items: list[ListItem] = []
73
- for idx, choice in enumerate(self.choices):
74
- list_item = ListItem(ChoiceCheckboxLabel(choice))
75
- items.append(list_item)
76
- self.list_view = ListView(*items, id='inquirer-checkbox-list-view')
77
- yield PromptMessage(self.message)
78
- yield self.list_view
74
+ if self.show_selected_value:
75
+ with HorizontalGroup():
76
+ yield Prompt(self.message)
77
+ yield Answer(str(self.selected_value))
78
+ else:
79
+ with VerticalGroup():
80
+ items: list[ListItem] = []
81
+ for idx, choice in enumerate(self.choices):
82
+ list_item = ListItem(ChoiceCheckboxLabel(choice))
83
+ items.append(list_item)
84
+ self.list_view = ListView(*items, id='inquirer-checkbox-list-view')
85
+ yield Prompt(self.message)
86
+ yield self.list_view
@@ -1,10 +1,14 @@
1
- from inquirer_textual.widgets.InquirerWidget import InquirerWidget
2
- from inquirer_textual.common.PromptMessage import PromptMessage
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.Answer import Answer
9
+ from inquirer_textual.common.Prompt import Prompt
10
+ from inquirer_textual.widgets.InquirerWidget import InquirerWidget
11
+
8
12
 
9
13
  class InquirerConfirm(InquirerWidget):
10
14
  """A confirmation prompt that allows the user to confirm or reject."""
@@ -16,42 +20,58 @@ class InquirerConfirm(InquirerWidget):
16
20
  """
17
21
  can_focus = True
18
22
 
19
- def __init__(self, message: str, confirm_character: str = 'y', reject_character: str = 'n', default=False,
20
- mandatory: bool = True):
23
+ def __init__(self, message: str, confirm_character: str = 'y', reject_character: str = 'n', name: str | None = None,
24
+ default=False, mandatory: bool = False):
21
25
  """
22
26
  Args:
23
27
  message (str): The prompt message to display.
24
28
  confirm_character (str): The character to use for confirmation.
25
29
  reject_character (str): The character to use for rejection.
30
+ name (str | None): The name of the prompt.
26
31
  default (bool): The default value if the user presses Enter without input.
27
32
  mandatory (bool): Whether a response is mandatory.
28
33
  """
29
- super().__init__(mandatory)
34
+ super().__init__(name=name, mandatory=mandatory)
30
35
  if len(confirm_character) != 1 or len(reject_character) != 1:
31
36
  raise ValueError("confirm_character and reject_character must be a single character")
32
37
  if confirm_character.lower() == reject_character.lower():
33
38
  raise ValueError("confirm_character and reject_character must be different")
34
39
  self.message = message
40
+ self.confirm_character = confirm_character
41
+ self.reject_character = reject_character
35
42
  c = confirm_character if not default else confirm_character.upper()
36
43
  r = reject_character if default else reject_character.upper()
37
44
  self.label = Label(f'({c}/{r})')
38
45
  self.value: bool = default
46
+ self.selected_value: bool | None = None
47
+ self.show_selected_value: bool = False
39
48
 
40
49
  def on_key(self, event: events.Key):
41
50
  if event.key.lower() == 'y':
42
51
  self.value = True
43
- self.post_message(InquirerWidget.Submit(self.value))
52
+ self.submit_current_value()
44
53
  elif event.key.lower() == 'n':
45
54
  self.value = False
46
- self.post_message(InquirerWidget.Submit(self.value))
55
+ self.submit_current_value()
47
56
  elif event.key == 'enter':
48
57
  event.stop()
49
- self.post_message(InquirerWidget.Submit(self.value))
58
+ self.submit_current_value()
50
59
 
51
60
  def current_value(self):
52
61
  return self.value
53
62
 
63
+ async def set_selected_value(self, value: bool) -> None:
64
+ self.selected_value = value
65
+ self.styles.height = 1
66
+ self.show_selected_value = True
67
+ await self.recompose()
68
+
54
69
  def compose(self) -> ComposeResult:
55
- with HorizontalGroup():
56
- yield PromptMessage(self.message)
57
- yield self.label
70
+ if self.show_selected_value:
71
+ with HorizontalGroup():
72
+ yield Prompt(self.message)
73
+ yield Answer(self.confirm_character if self.selected_value else self.reject_character)
74
+ else:
75
+ with HorizontalGroup():
76
+ yield Prompt(self.message)
77
+ yield self.label
@@ -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.Prompt import Prompt
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 Prompt(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'