py-sparcli 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.
- py_sparcli-0.3.0.dist-info/METADATA +257 -0
- py_sparcli-0.3.0.dist-info/RECORD +61 -0
- py_sparcli-0.3.0.dist-info/WHEEL +4 -0
- py_sparcli-0.3.0.dist-info/licenses/LICENSE +21 -0
- sparcli/__init__.py +184 -0
- sparcli/core/__init__.py +92 -0
- sparcli/core/border.py +160 -0
- sparcli/core/color.py +306 -0
- sparcli/core/cursor.py +59 -0
- sparcli/core/geometry.py +141 -0
- sparcli/core/markup.py +215 -0
- sparcli/core/render.py +245 -0
- sparcli/core/style.py +128 -0
- sparcli/core/terminal.py +127 -0
- sparcli/core/text.py +143 -0
- sparcli/core/theme.py +117 -0
- sparcli/core/width.py +244 -0
- sparcli/errors.py +38 -0
- sparcli/input/__init__.py +42 -0
- sparcli/input/calc.py +214 -0
- sparcli/input/confirm.py +188 -0
- sparcli/input/datepicker.py +298 -0
- sparcli/input/editor.py +189 -0
- sparcli/input/event.py +551 -0
- sparcli/input/field.py +90 -0
- sparcli/input/fuzzy.py +401 -0
- sparcli/input/guard.py +103 -0
- sparcli/input/history.py +160 -0
- sparcli/input/line_edit.py +229 -0
- sparcli/input/number.py +232 -0
- sparcli/input/outcome.py +107 -0
- sparcli/input/password.py +233 -0
- sparcli/input/prompt.py +134 -0
- sparcli/input/select.py +336 -0
- sparcli/input/shortcut.py +123 -0
- sparcli/input/text.py +564 -0
- sparcli/input/textarea.py +242 -0
- sparcli/input/validate.py +67 -0
- sparcli/output/__init__.py +56 -0
- sparcli/output/alert.py +107 -0
- sparcli/output/badge.py +82 -0
- sparcli/output/box.py +196 -0
- sparcli/output/columns.py +157 -0
- sparcli/output/compose.py +111 -0
- sparcli/output/diff.py +229 -0
- sparcli/output/kv.py +175 -0
- sparcli/output/layout.py +77 -0
- sparcli/output/list.py +274 -0
- sparcli/output/live.py +129 -0
- sparcli/output/multiprogress.py +73 -0
- sparcli/output/pager.py +83 -0
- sparcli/output/panel.py +125 -0
- sparcli/output/progress.py +233 -0
- sparcli/output/rule.py +134 -0
- sparcli/output/spinner.py +138 -0
- sparcli/output/table/__init__.py +17 -0
- sparcli/output/table/plan.py +198 -0
- sparcli/output/table/render.py +302 -0
- sparcli/output/table/table.py +351 -0
- sparcli/output/tree.py +157 -0
- sparcli/py.typed +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sparcli.input
|
|
3
|
+
============
|
|
4
|
+
|
|
5
|
+
Interactive single-input prompts driven by an :class:`EventSource`.
|
|
6
|
+
|
|
7
|
+
Every prompt exposes ``run()`` (real terminal), ``run_with(source)`` (the
|
|
8
|
+
dependency-injection seam used by tests) and ``frame()`` (a static preview).
|
|
9
|
+
Prompts return an :class:`~sparcli.input.outcome.Outcome`. This package
|
|
10
|
+
re-exports the prompt classes and the command :class:`History`; the grouped
|
|
11
|
+
helpers stay reachable as the ``event``, ``validate`` and ``shortcut``
|
|
12
|
+
submodules.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from sparcli.input.confirm import Confirm
|
|
16
|
+
from sparcli.input.datepicker import DatePicker
|
|
17
|
+
from sparcli.input.editor import edit_file
|
|
18
|
+
from sparcli.input.fuzzy import FuzzySelect
|
|
19
|
+
from sparcli.input.history import History
|
|
20
|
+
from sparcli.input.number import NumberInput
|
|
21
|
+
from sparcli.input.outcome import Outcome, OutcomeKind
|
|
22
|
+
from sparcli.input.password import PasswordInput
|
|
23
|
+
from sparcli.input.select import Select
|
|
24
|
+
from sparcli.input.shortcut import Shortcut
|
|
25
|
+
from sparcli.input.text import TextInput
|
|
26
|
+
from sparcli.input.textarea import Textarea
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"Confirm",
|
|
30
|
+
"DatePicker",
|
|
31
|
+
"FuzzySelect",
|
|
32
|
+
"History",
|
|
33
|
+
"NumberInput",
|
|
34
|
+
"Outcome",
|
|
35
|
+
"OutcomeKind",
|
|
36
|
+
"PasswordInput",
|
|
37
|
+
"Select",
|
|
38
|
+
"Shortcut",
|
|
39
|
+
"TextInput",
|
|
40
|
+
"Textarea",
|
|
41
|
+
"edit_file",
|
|
42
|
+
]
|
sparcli/input/calc.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sparcli.input.calc
|
|
3
|
+
==================
|
|
4
|
+
|
|
5
|
+
Defines the calculator-mode expression evaluator behind ``NumberInput``.
|
|
6
|
+
|
|
7
|
+
:func:`evaluate` is a small recursive-descent parser for the four arithmetic
|
|
8
|
+
operators, parentheses and unary minus, with the usual precedence.
|
|
9
|
+
:func:`parse_number` reads a single number for the plain (non-calculator)
|
|
10
|
+
submit path. Both accept ``,`` or ``.`` as the decimal separator and report
|
|
11
|
+
failures through :class:`CalcError`, whose message becomes the prompt's error
|
|
12
|
+
line. This module is an implementation detail, not part of the public API.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import enum
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CalcErrorKind(enum.Enum):
|
|
21
|
+
"""The reason a numeric expression could not be evaluated."""
|
|
22
|
+
|
|
23
|
+
NOT_A_NUMBER = "not a number"
|
|
24
|
+
INVALID_NUMBER = "invalid number"
|
|
25
|
+
DIVISION_BY_ZERO = "division by zero"
|
|
26
|
+
MISSING_PARENTHESIS = "missing closing parenthesis"
|
|
27
|
+
TRAILING_INPUT = "unexpected trailing input"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CalcError(Exception):
|
|
31
|
+
"""
|
|
32
|
+
Raised when parsing or evaluating a numeric expression fails.
|
|
33
|
+
|
|
34
|
+
Attributes
|
|
35
|
+
----------
|
|
36
|
+
kind : CalcErrorKind
|
|
37
|
+
Which failure occurred; its value is the human-readable message.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, kind: CalcErrorKind) -> None:
|
|
41
|
+
super().__init__(kind.value)
|
|
42
|
+
self.kind = kind
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_number(text: str) -> float:
|
|
46
|
+
"""
|
|
47
|
+
Parses a single number, accepting ``,`` or ``.`` as the separator.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
text : str
|
|
52
|
+
The raw field text; surrounding whitespace is ignored.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
float
|
|
57
|
+
The parsed value.
|
|
58
|
+
|
|
59
|
+
Raises
|
|
60
|
+
------
|
|
61
|
+
CalcError
|
|
62
|
+
With :attr:`CalcErrorKind.NOT_A_NUMBER` if ``text`` is empty or not a
|
|
63
|
+
valid number.
|
|
64
|
+
|
|
65
|
+
Examples
|
|
66
|
+
--------
|
|
67
|
+
>>> parse_number("3,5")
|
|
68
|
+
3.5
|
|
69
|
+
"""
|
|
70
|
+
normalized = text.strip().replace(",", ".")
|
|
71
|
+
if not normalized:
|
|
72
|
+
raise CalcError(CalcErrorKind.NOT_A_NUMBER)
|
|
73
|
+
try:
|
|
74
|
+
return float(normalized)
|
|
75
|
+
except ValueError:
|
|
76
|
+
raise CalcError(CalcErrorKind.NOT_A_NUMBER) from None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def evaluate(expr: str) -> float:
|
|
80
|
+
"""
|
|
81
|
+
Evaluates an arithmetic expression with ``+ - * / ( )``.
|
|
82
|
+
|
|
83
|
+
Accepts ``,`` or ``.`` as the decimal separator and honours the usual
|
|
84
|
+
operator precedence and unary minus.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
expr : str
|
|
89
|
+
The expression to evaluate.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
float
|
|
94
|
+
The value of the expression.
|
|
95
|
+
|
|
96
|
+
Raises
|
|
97
|
+
------
|
|
98
|
+
CalcError
|
|
99
|
+
On malformed input or division by zero.
|
|
100
|
+
|
|
101
|
+
Examples
|
|
102
|
+
--------
|
|
103
|
+
>>> evaluate("2 + 3 * 4")
|
|
104
|
+
14.0
|
|
105
|
+
>>> evaluate("(2 + 3) * 4")
|
|
106
|
+
20.0
|
|
107
|
+
"""
|
|
108
|
+
parser = _Parser(expr.replace(",", "."))
|
|
109
|
+
value = parser.expression()
|
|
110
|
+
parser.skip_spaces()
|
|
111
|
+
if not parser.at_end():
|
|
112
|
+
raise CalcError(CalcErrorKind.TRAILING_INPUT)
|
|
113
|
+
return value
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class _Parser:
|
|
117
|
+
"""A minimal recursive-descent arithmetic parser over a character list."""
|
|
118
|
+
|
|
119
|
+
__slots__ = ("_chars", "_pos")
|
|
120
|
+
|
|
121
|
+
def __init__(self, text: str) -> None:
|
|
122
|
+
self._chars: list[str] = list(text)
|
|
123
|
+
self._pos: int = 0
|
|
124
|
+
|
|
125
|
+
def expression(self) -> float:
|
|
126
|
+
"""Parses ``term (('+'|'-') term)*``."""
|
|
127
|
+
value = self.term()
|
|
128
|
+
while True:
|
|
129
|
+
self.skip_spaces()
|
|
130
|
+
operator = self._peek()
|
|
131
|
+
if operator == "+":
|
|
132
|
+
self._pos += 1
|
|
133
|
+
value += self.term()
|
|
134
|
+
elif operator == "-":
|
|
135
|
+
self._pos += 1
|
|
136
|
+
value -= self.term()
|
|
137
|
+
else:
|
|
138
|
+
return value
|
|
139
|
+
|
|
140
|
+
def term(self) -> float:
|
|
141
|
+
"""Parses ``factor (('*'|'/') factor)*``."""
|
|
142
|
+
value = self.factor()
|
|
143
|
+
while True:
|
|
144
|
+
self.skip_spaces()
|
|
145
|
+
operator = self._peek()
|
|
146
|
+
if operator == "*":
|
|
147
|
+
self._pos += 1
|
|
148
|
+
value *= self.factor()
|
|
149
|
+
elif operator == "/":
|
|
150
|
+
self._pos += 1
|
|
151
|
+
value = self._divide(value)
|
|
152
|
+
else:
|
|
153
|
+
return value
|
|
154
|
+
|
|
155
|
+
def factor(self) -> float:
|
|
156
|
+
"""Parses a number, a parenthesized expression or a unary minus."""
|
|
157
|
+
self.skip_spaces()
|
|
158
|
+
char = self._peek()
|
|
159
|
+
if char == "(":
|
|
160
|
+
return self._grouped()
|
|
161
|
+
if char == "-":
|
|
162
|
+
self._pos += 1
|
|
163
|
+
return -self.factor()
|
|
164
|
+
return self._number()
|
|
165
|
+
|
|
166
|
+
def skip_spaces(self) -> None:
|
|
167
|
+
"""Advances past any run of spaces."""
|
|
168
|
+
while self._peek() == " ":
|
|
169
|
+
self._pos += 1
|
|
170
|
+
|
|
171
|
+
def at_end(self) -> bool:
|
|
172
|
+
"""Returns whether the whole input has been consumed."""
|
|
173
|
+
return self._pos >= len(self._chars)
|
|
174
|
+
|
|
175
|
+
def _divide(self, value: float) -> float:
|
|
176
|
+
"""Consumes the divisor factor and divides, guarding against zero."""
|
|
177
|
+
divisor = self.factor()
|
|
178
|
+
if divisor == 0.0:
|
|
179
|
+
raise CalcError(CalcErrorKind.DIVISION_BY_ZERO)
|
|
180
|
+
return value / divisor
|
|
181
|
+
|
|
182
|
+
def _grouped(self) -> float:
|
|
183
|
+
"""Parses a parenthesized subexpression, requiring a closing ``)``."""
|
|
184
|
+
self._pos += 1
|
|
185
|
+
value = self.expression()
|
|
186
|
+
self.skip_spaces()
|
|
187
|
+
if self._peek() != ")":
|
|
188
|
+
raise CalcError(CalcErrorKind.MISSING_PARENTHESIS)
|
|
189
|
+
self._pos += 1
|
|
190
|
+
return value
|
|
191
|
+
|
|
192
|
+
def _number(self) -> float:
|
|
193
|
+
"""Parses a decimal number literal (digits and a decimal point)."""
|
|
194
|
+
start = self._pos
|
|
195
|
+
while _is_number_char(self._peek()):
|
|
196
|
+
self._pos += 1
|
|
197
|
+
text = "".join(self._chars[start : self._pos])
|
|
198
|
+
try:
|
|
199
|
+
return float(text)
|
|
200
|
+
except ValueError:
|
|
201
|
+
raise CalcError(CalcErrorKind.INVALID_NUMBER) from None
|
|
202
|
+
|
|
203
|
+
def _peek(self) -> str | None:
|
|
204
|
+
"""Returns the current character without consuming it."""
|
|
205
|
+
if self._pos >= len(self._chars):
|
|
206
|
+
return None
|
|
207
|
+
return self._chars[self._pos]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _is_number_char(char: str | None) -> bool:
|
|
211
|
+
"""Returns whether ``char`` may appear inside a number literal."""
|
|
212
|
+
return char is not None and (
|
|
213
|
+
char.isascii() and char.isdigit() or char == "."
|
|
214
|
+
)
|
sparcli/input/confirm.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sparcli.input.confirm
|
|
3
|
+
=====================
|
|
4
|
+
|
|
5
|
+
Defines :class:`Confirm`, a yes/no confirmation prompt.
|
|
6
|
+
|
|
7
|
+
The prompt shows two options; the selected one is highlighted and the other is
|
|
8
|
+
dimmed. ``y``/``n`` answer directly, Enter accepts the current selection, and
|
|
9
|
+
the arrows, Tab, ``h`` or ``l`` toggle it. Registered
|
|
10
|
+
:class:`~sparcli.input.shortcut.Shortcut` keys end the prompt with a shortcut
|
|
11
|
+
outcome and appear in a footer hint and the ``?`` help overlay.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Iterable
|
|
17
|
+
|
|
18
|
+
from sparcli.core.render import Rendered
|
|
19
|
+
from sparcli.core.style import Style
|
|
20
|
+
from sparcli.core.terminal import is_input_tty
|
|
21
|
+
from sparcli.core.text import Line, Span
|
|
22
|
+
from sparcli.core.theme import Theme, theme
|
|
23
|
+
from sparcli.errors import NoTerminalError
|
|
24
|
+
from sparcli.input.event import (
|
|
25
|
+
EventKind,
|
|
26
|
+
EventSource,
|
|
27
|
+
InputEvent,
|
|
28
|
+
KeyCode,
|
|
29
|
+
KeyPress,
|
|
30
|
+
TerminalSource,
|
|
31
|
+
)
|
|
32
|
+
from sparcli.input.guard import TerminalGuard
|
|
33
|
+
from sparcli.input.outcome import Outcome
|
|
34
|
+
from sparcli.input.prompt import Flow, run_prompt
|
|
35
|
+
from sparcli.input.shortcut import Shortcut, find, help_overlay, hint_line
|
|
36
|
+
|
|
37
|
+
# Keys that toggle the current selection.
|
|
38
|
+
_TOGGLE_CODES = frozenset(
|
|
39
|
+
{
|
|
40
|
+
KeyCode.LEFT,
|
|
41
|
+
KeyCode.RIGHT,
|
|
42
|
+
KeyCode.TAB,
|
|
43
|
+
KeyCode.char("h"),
|
|
44
|
+
KeyCode.char("l"),
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _State:
|
|
50
|
+
"""The mutable state of a running confirm prompt."""
|
|
51
|
+
|
|
52
|
+
__slots__ = ("yes", "help")
|
|
53
|
+
|
|
54
|
+
def __init__(self, yes: bool) -> None:
|
|
55
|
+
self.yes = yes
|
|
56
|
+
self.help = False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Confirm:
|
|
60
|
+
"""A yes/no confirmation prompt with custom labels and shortcuts."""
|
|
61
|
+
|
|
62
|
+
__slots__ = (
|
|
63
|
+
"_question",
|
|
64
|
+
"_default_yes",
|
|
65
|
+
"_yes_label",
|
|
66
|
+
"_no_label",
|
|
67
|
+
"_shortcuts",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
question: str = "",
|
|
73
|
+
*,
|
|
74
|
+
default_yes: bool = False,
|
|
75
|
+
yes_label: str = "Yes",
|
|
76
|
+
no_label: str = "No",
|
|
77
|
+
shortcuts: Iterable[Shortcut] | None = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
self._question = question
|
|
80
|
+
self._default_yes = default_yes
|
|
81
|
+
self._yes_label = yes_label
|
|
82
|
+
self._no_label = no_label
|
|
83
|
+
self._shortcuts: list[Shortcut] = (
|
|
84
|
+
list(shortcuts) if shortcuts is not None else []
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def shortcuts(self, shortcuts: Iterable[Shortcut]) -> Confirm:
|
|
88
|
+
"""Registers footer/overlay shortcuts and returns ``self``."""
|
|
89
|
+
self._shortcuts = list(shortcuts)
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def default_yes(self) -> Confirm:
|
|
93
|
+
"""Sets the initial selection to "Yes" and returns ``self``."""
|
|
94
|
+
self._default_yes = True
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def labels(self, yes: str, no: str) -> Confirm:
|
|
98
|
+
"""Sets custom labels for the two options and returns ``self``."""
|
|
99
|
+
self._yes_label = yes
|
|
100
|
+
self._no_label = no
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def run(self) -> Outcome[bool]:
|
|
104
|
+
"""
|
|
105
|
+
Runs the prompt on the real terminal.
|
|
106
|
+
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
Outcome[bool]
|
|
110
|
+
The chosen answer, a cancellation or a fired shortcut.
|
|
111
|
+
|
|
112
|
+
Raises
|
|
113
|
+
------
|
|
114
|
+
NoTerminalError
|
|
115
|
+
If there is no interactive terminal.
|
|
116
|
+
"""
|
|
117
|
+
if not is_input_tty():
|
|
118
|
+
raise NoTerminalError()
|
|
119
|
+
with TerminalGuard():
|
|
120
|
+
return self.run_with(TerminalSource())
|
|
121
|
+
|
|
122
|
+
def run_with(self, source: EventSource) -> Outcome[bool]:
|
|
123
|
+
"""Runs the prompt against any event source (used for tests)."""
|
|
124
|
+
state = _State(self._default_yes)
|
|
125
|
+
return run_prompt(source, state, self._render, self._handle)
|
|
126
|
+
|
|
127
|
+
def frame(self) -> Rendered:
|
|
128
|
+
"""Renders the prompt's static frame without running it."""
|
|
129
|
+
state = _State(self._default_yes)
|
|
130
|
+
return self._render(state, False)
|
|
131
|
+
|
|
132
|
+
def _render(self, state: _State, final: bool) -> Rendered:
|
|
133
|
+
"""Builds the prompt frame for the current selection."""
|
|
134
|
+
active = theme()
|
|
135
|
+
if state.help:
|
|
136
|
+
return Rendered(help_overlay(self._shortcuts))
|
|
137
|
+
spans = [
|
|
138
|
+
Span.raw(f"{self._question} "),
|
|
139
|
+
_option_span(self._yes_label, state.yes, active),
|
|
140
|
+
Span.raw(" "),
|
|
141
|
+
_option_span(self._no_label, not state.yes, active),
|
|
142
|
+
]
|
|
143
|
+
lines = [Line(spans)]
|
|
144
|
+
if self._shortcuts:
|
|
145
|
+
lines.append(hint_line(self._shortcuts))
|
|
146
|
+
return Rendered(lines)
|
|
147
|
+
|
|
148
|
+
def _handle(self, state: _State, event: InputEvent) -> Flow[bool]:
|
|
149
|
+
"""Handles one input event."""
|
|
150
|
+
if event.kind is not EventKind.KEY or event.key is None:
|
|
151
|
+
return Flow[bool].cont()
|
|
152
|
+
return self._handle_key(state, event.key)
|
|
153
|
+
|
|
154
|
+
def _handle_key(self, state: _State, key: KeyPress) -> Flow[bool]:
|
|
155
|
+
"""Handles a single key press."""
|
|
156
|
+
if state.help:
|
|
157
|
+
state.help = False
|
|
158
|
+
return Flow[bool].cont()
|
|
159
|
+
if key.is_ctrl("c"):
|
|
160
|
+
return Flow[bool].cancel()
|
|
161
|
+
if key.code == KeyCode.char("?") and self._shortcuts:
|
|
162
|
+
state.help = True
|
|
163
|
+
return Flow[bool].cont()
|
|
164
|
+
fired = find(key, self._shortcuts)
|
|
165
|
+
if fired is not None:
|
|
166
|
+
return Flow[bool].shortcut(fired)
|
|
167
|
+
return self._decide(state, key.code)
|
|
168
|
+
|
|
169
|
+
def _decide(self, state: _State, code: KeyCode) -> Flow[bool]:
|
|
170
|
+
"""Applies a selection, submission or cancellation key."""
|
|
171
|
+
if code == KeyCode.ESC:
|
|
172
|
+
return Flow[bool].cancel()
|
|
173
|
+
if code == KeyCode.ENTER:
|
|
174
|
+
return Flow[bool].submit(state.yes)
|
|
175
|
+
if code in (KeyCode.char("y"), KeyCode.char("Y")):
|
|
176
|
+
return Flow[bool].submit(True)
|
|
177
|
+
if code in (KeyCode.char("n"), KeyCode.char("N")):
|
|
178
|
+
return Flow[bool].submit(False)
|
|
179
|
+
if code in _TOGGLE_CODES:
|
|
180
|
+
state.yes = not state.yes
|
|
181
|
+
return Flow[bool].cont()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _option_span(label: str, selected: bool, active: Theme) -> Span:
|
|
185
|
+
"""Renders one option, highlighted when selected and dimmed otherwise."""
|
|
186
|
+
if selected:
|
|
187
|
+
return Span.styled(f"[{label}]", active.selection)
|
|
188
|
+
return Span.styled(f" {label} ", Style.new().dim())
|