inquirer-textual 0.1.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.
@@ -1,17 +1,24 @@
1
- from typing import TypeVar
1
+ from __future__ import annotations
2
2
 
3
- from textual.app import App
3
+ from asyncio import AbstractEventLoop
4
+ from threading import Event
5
+ from typing import TypeVar, Callable, Any
6
+
7
+ from textual.app import App, AutopilotCallbackType
4
8
  from textual.app import ComposeResult
9
+ from textual.binding import Binding, BindingsMap
5
10
  from textual.widgets import Footer
6
11
 
7
- from inquirer_textual.common.Result import Result
12
+ from inquirer_textual.common.InquirerHeader import InquirerHeader
13
+ from inquirer_textual.common.InquirerResult import InquirerResult
8
14
  from inquirer_textual.common.Shortcut import Shortcut
15
+ from inquirer_textual.common.StandardTheme import StandardTheme
9
16
  from inquirer_textual.widgets.InquirerWidget import InquirerWidget
10
17
 
11
18
  T = TypeVar('T')
12
19
 
13
20
 
14
- 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]
15
22
  CSS = """
16
23
  App {
17
24
  background: black;
@@ -25,37 +32,131 @@ class InquirerApp(App[Result[T]], inherit_bindings=False): # type: ignore[call-
25
32
  """
26
33
  ENABLE_COMMAND_PALETTE = False
27
34
  INLINE_PADDING = 0
35
+ BINDINGS = [
36
+ Binding("ctrl+d", "quit", "Quit", show=False, priority=True)
37
+ ]
28
38
 
29
- def __init__(self, widget: InquirerWidget, shortcuts: list[Shortcut] | None = None, show_footer: bool = False):
39
+ def __init__(self) -> None:
40
+ self.widget: InquirerWidget | None = None
41
+ self.shortcuts: list[Shortcut] | None = None
42
+ self.header: str | list[str] | None = None
43
+ self.show_footer: bool = False
44
+ self.result: InquirerResult[T] | None = None
45
+ self.result_ready: Event | None = None
46
+ self.inquiry_func: Callable[[InquirerApp[T]], None] | None = None
47
+ self.inquiry_func_stop: bool = False
30
48
  super().__init__()
31
- self.widget = widget
32
- self.shortcuts = shortcuts
33
- self.show_footer = show_footer
34
49
 
35
50
  def on_mount(self) -> None:
51
+ self._update_bindings()
52
+ if self.inquiry_func:
53
+ self.run_worker(self.inquiry_func_worker, thread=True)
54
+
55
+ def _update_bindings(self) -> None:
56
+ self._bindings = BindingsMap()
36
57
  if self.shortcuts:
37
58
  for shortcut in self.shortcuts:
38
59
  self._bindings.bind(shortcut.key, f'shortcut("{shortcut.command}")',
39
60
  description=shortcut.description,
40
61
  show=shortcut.show)
41
62
 
42
- def action_shortcut(self, command: str):
43
- self._exit_select(command)
63
+ def inquiry_func_worker(self):
64
+ if self.inquiry_func:
65
+ self.inquiry_func(self)
66
+
67
+ async def action_shortcut(self, command: str):
68
+ value = self.widget.current_value() if self.widget else None
69
+ await self._handle_result(command, value)
70
+
71
+ async def action_quit(self):
72
+ await self._handle_result('quit', None)
73
+
74
+ async def on_inquirer_widget_submit(self, event: InquirerWidget.Submit) -> None:
75
+ await self._handle_result(event.command, event.value)
44
76
 
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)))
77
+ async def _handle_result(self, command: str | None, value: Any | None):
78
+ if self.result_ready is not None:
79
+ self.result = InquirerResult(self.widget.name if self.widget else None, value, # type: ignore[arg-type]
80
+ command)
81
+ self.result_ready.set()
82
+ else:
83
+ if self.widget:
84
+ await self.widget.set_selected_value(value)
85
+ self.call_after_refresh(lambda: self._terminate(command, value))
47
86
 
48
- def _exit_select(self, command: str):
49
- self.call_after_refresh(lambda: self.app.exit(Result(command, self.widget.current_value())))
87
+ def _terminate(self, command: str | None = None, value: Any | None = None):
88
+ self.inquiry_func_stop = True
89
+ if self.result_ready:
90
+ self.result_ready.set()
91
+ if command is not None:
92
+ self.app.exit(InquirerResult(self.widget.name if self.widget else None, value, command))
93
+ else:
94
+ self.exit(InquirerResult(None, value, None))
50
95
 
51
96
  def compose(self) -> ComposeResult:
52
- yield self.widget
53
- if self.show_footer:
54
- yield Footer()
97
+ if self.header is not None:
98
+ yield InquirerHeader(self.header)
99
+ if self.widget:
100
+ yield self.widget
101
+ self.widget.focus()
102
+ if self.show_footer:
103
+ yield Footer()
104
+
105
+ def focus_widget(self):
106
+ if self.widget:
107
+ self.call_after_refresh(self.widget.focus)
108
+
109
+ def prompt(self, widget: InquirerWidget, shortcuts: list[Shortcut] | None = None) -> InquirerResult[T]:
110
+ if shortcuts:
111
+ self.shortcuts = shortcuts
112
+ self.show_footer = True
113
+ self._update_bindings()
114
+ self.widget = widget
115
+ if not self.result_ready:
116
+ self.result_ready = Event()
117
+ self.call_from_thread(self.refresh, recompose=True)
118
+ self.call_from_thread(self.focus_widget)
119
+ self.result_ready.wait()
120
+ self.result_ready.clear()
121
+ if self.inquiry_func_stop:
122
+ raise RuntimeError("InquirerApp has been stopped.")
123
+ return self.result # type: ignore[return-value]
124
+
125
+ def stop(self, value: Any = None):
126
+ if value:
127
+ self.call_after_refresh(lambda: self._terminate(value=value))
128
+ else:
129
+ self.call_from_thread(self.exit)
130
+
131
+ def run(
132
+ self,
133
+ *,
134
+ headless: bool = False,
135
+ inline: bool = False,
136
+ inline_no_clear: bool = False,
137
+ mouse: bool = True,
138
+ size: tuple[int, int] | None = None,
139
+ auto_pilot: AutopilotCallbackType | None = None,
140
+ loop: AbstractEventLoop | None = None,
141
+ inquiry_func: Callable[[InquirerApp[T]], None] | None = None,
142
+ ) -> InquirerResult[T]:
143
+ if not self.inquiry_func:
144
+ self.inquiry_func = inquiry_func
145
+ return super().run(
146
+ headless=headless,
147
+ inline=inline,
148
+ inline_no_clear=inline_no_clear,
149
+ mouse=mouse,
150
+ size=size,
151
+ auto_pilot=auto_pilot,
152
+ loop=loop,
153
+ )
55
154
 
56
155
  def get_theme_variable_defaults(self) -> dict[str, str]:
57
156
  return {
58
- 'select-question-mark': '#e5c07b',
59
- 'select-list-item-highlight-foreground': '#61afef',
60
- 'input-color': '#98c379'
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,
61
162
  }
@@ -0,0 +1,9 @@
1
+ from dataclasses import dataclass
2
+
3
+ from inquirer_textual.common.Shortcut import Shortcut
4
+
5
+
6
+ @dataclass
7
+ class AppConfig:
8
+ inline: bool = True
9
+ shortcuts: list[Shortcut] | None = None
@@ -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,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from textual.widgets import Label
2
4
 
3
5
  from inquirer_textual.common.Choice import Choice
@@ -1,16 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from rich.text import Text
1
4
  from textual.widgets import Label
2
5
 
3
6
  from inquirer_textual.common.Choice import Choice
7
+ from inquirer_textual.common.StandardTheme import StandardTheme
4
8
 
5
9
 
6
10
  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
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))
10
14
  self.item = item
11
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
+
12
23
  def add_pointer(self):
13
- self.update(f'\u276f {self._text}')
24
+ self.update(Text(f'{StandardTheme.pointer_character} ').append_text(self._text))
14
25
 
15
26
  def remove_pointer(self):
16
- self.update(f' {self._text}')
27
+ self.update(Text(' ').append_text(self._text))
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.containers import Vertical
5
+ from textual.widget import Widget
6
+ from textual.widgets import Static
7
+
8
+
9
+ class InquirerHeader(Widget):
10
+ DEFAULT_CSS = """
11
+ InquirerHeader {
12
+ margin-bottom: 1;
13
+ }
14
+
15
+ #inquirer-header-static {
16
+ width: 1fr;
17
+ text-align: center;
18
+ }
19
+ """
20
+
21
+ def __init__(self, text: str | list[str]) -> None:
22
+ super().__init__()
23
+ self._text = text if isinstance(text, list) else [text]
24
+
25
+ def on_mount(self):
26
+ self.styles.height = len(self._text)
27
+
28
+ def compose(self) -> ComposeResult:
29
+ with Vertical():
30
+ for line in self._text:
31
+ yield Static(line, id="inquirer-header-static")
@@ -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)
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from dataclasses import dataclass
2
4
 
3
5
 
@@ -10,4 +12,4 @@ class Shortcut:
10
12
 
11
13
  def __post_init__(self):
12
14
  if self.description is None:
13
- self.description = self.command
15
+ self.description = self.command
@@ -0,0 +1,10 @@
1
+ class StandardTheme:
2
+ input_color = '#98c379'
3
+ prompt_color = '#c678dd'
4
+ error_color = '#e06c75'
5
+ pointer_character = '\u276f'
6
+ select_list_item_highlight_foreground = '#61afef'
7
+ select_question_mark = '#e5c07b'
8
+
9
+
10
+
@@ -1,61 +1,88 @@
1
1
  from typing import Any, Iterable
2
2
 
3
+ from textual.validation import Validator
4
+
3
5
  from inquirer_textual.InquirerApp import InquirerApp
4
6
  from inquirer_textual.common.Choice import Choice
7
+ from inquirer_textual.widgets.InquirerCheckbox import InquirerCheckbox
5
8
  from inquirer_textual.widgets.InquirerConfirm import InquirerConfirm
9
+ from inquirer_textual.widgets.InquirerEditor import InquirerEditor
6
10
  from inquirer_textual.widgets.InquirerMulti import InquirerMulti
7
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
8
14
  from inquirer_textual.widgets.InquirerSecret import InquirerSecret
15
+ from inquirer_textual.widgets.InquirerSelect import InquirerSelect
9
16
  from inquirer_textual.widgets.InquirerText import InquirerText
10
17
  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
18
 
17
19
 
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)
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
31
+
32
+
33
+ def editor(message: str, clear: bool = False) -> str:
34
+ app: InquirerApp[str] = InquirerApp()
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
23
49
 
24
50
 
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)
51
+ def number(message: str, mandatory: bool = False, clear: bool = False) -> int:
52
+ app: InquirerApp[int] = InquirerApp()
53
+ app.widget = InquirerNumber(message, mandatory=mandatory)
54
+ return app.run(inline=True, inline_no_clear=not clear).value
29
55
 
30
56
 
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)
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
35
62
 
36
63
 
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)
64
+ def pattern(message: str, choices: list[str | Choice], default: str | Choice | None = None,
65
+ mandatory: bool = False, clear: bool = False) -> str | Choice:
66
+ app: InquirerApp[str | Choice] = InquirerApp()
67
+ app.widget = InquirerPattern(message, choices, default=default, mandatory=mandatory)
68
+ return app.run(inline=True, inline_no_clear=not clear).value
42
69
 
43
70
 
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)
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
49
75
 
50
76
 
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)
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
56
82
 
57
83
 
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)
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.1.0"
1
+ version = "0.3.0"
@@ -1,8 +1,9 @@
1
- from typing import Self
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
+ from typing_extensions import Self
6
7
 
7
8
  from inquirer_textual.common.Choice import Choice
8
9
  from inquirer_textual.common.ChoiceCheckboxLabel import ChoiceCheckboxLabel
@@ -26,20 +27,25 @@ class InquirerCheckbox(InquirerWidget):
26
27
  ("space", "toggle_selected", "Toggle selection"),
27
28
  ]
28
29
 
29
- def __init__(self, message: str, choices: list[str | Choice], enabled: list[str | Choice] | None = None):
30
+ def __init__(self, message: str, choices: list[str | Choice], name: str | None = None,
31
+ enabled: list[str | Choice] | None = None, mandatory: bool = False):
30
32
  """
31
33
  Args:
32
34
  message (str): The prompt message to display.
33
35
  choices (list[str | Choice]): A list of choices to present to the user.
36
+ name (str | None): The name of the input field.
34
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.
35
39
  """
36
- super().__init__()
40
+ super().__init__(name=name, mandatory=mandatory)
37
41
  self.message = message
38
42
  self.choices = choices
39
43
  self.enabled = enabled
40
44
  self.list_view: ListView | None = None
41
45
  self.selected_label: ChoiceCheckboxLabel | None = None
42
46
  self.selected_item: str | Choice | None = None
47
+ self.selected_value: list[str | Choice] | None = None
48
+ self.show_selected_value: bool = False
43
49
 
44
50
  def on_mount(self):
45
51
  self.styles.height = min(10, len(self.choices) + 1)
@@ -54,7 +60,7 @@ class InquirerCheckbox(InquirerWidget):
54
60
  self.selected_item = label.item
55
61
 
56
62
  def on_list_view_selected(self, _: ListView.Selected) -> None:
57
- self.post_message(InquirerWidget.Submit(self.current_value()))
63
+ self.submit_current_value()
58
64
 
59
65
  def focus(self, scroll_visible: bool = True) -> Self:
60
66
  if self.list_view:
@@ -66,12 +72,23 @@ class InquirerCheckbox(InquirerWidget):
66
72
  labels = self.query(ChoiceCheckboxLabel)
67
73
  return [label.item for label in labels if label.checked]
68
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
+
69
81
  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
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 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.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', default=False,
20
- mandatory: bool = True):
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'