Skip to content

Commit 149bcca

Browse files
committed
Update cmd2.Cmd.select to use prompt-toolkit choice
Key Changes: - prompt_toolkit.shortcuts.choice integration: The select method now utilizes the modern, interactive choice shortcut when both stdin and stdout are TTYs. This provides a more user-friendly selection menu (usually supports arrow keys and searching). - Backward Compatibility: Maintained the original numbered-list implementation as a fallback for non-TTY environments. This ensures that existing scripts, pipes, and tests (which mock read_input) continue to function correctly. - Robust Argument Handling: Standardized the conversion of various input formats (strings, lists of strings, lists of tuples) to the (value, label) format required by choice. - Error Handling: Wrapped the choice call in a loop and a try-except block to correctly handle KeyboardInterrupt (Ctrl-C) by printing ^C and re-raising, and to handle cancellations by reprompting, maintaining consistency with original select behavior.
1 parent 5ab816e commit 149bcca

File tree

2 files changed

+33
-10
lines changed

2 files changed

+33
-10
lines changed

cmd2/cmd2.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
from prompt_toolkit.key_binding import KeyBindings
8383
from prompt_toolkit.output import DummyOutput, create_output
8484
from prompt_toolkit.patch_stdout import patch_stdout
85-
from prompt_toolkit.shortcuts import CompleteStyle, PromptSession, set_title
85+
from prompt_toolkit.shortcuts import CompleteStyle, PromptSession, choice, set_title
8686
from rich.console import (
8787
Group,
8888
RenderableType,
@@ -4368,7 +4368,7 @@ def do_quit(self, _: argparse.Namespace) -> bool | None:
43684368
return True
43694369

43704370
def select(self, opts: str | Iterable[str] | Iterable[tuple[Any, str | None]], prompt: str = 'Your choice? ') -> Any:
4371-
"""Present a numbered menu to the user.
4371+
"""Present a menu to the user.
43724372
43734373
Modeled after the bash shell's SELECT. Returns the item chosen.
43744374
@@ -4385,15 +4385,29 @@ def select(self, opts: str | Iterable[str] | Iterable[tuple[Any, str | None]], p
43854385
local_opts = cast(list[tuple[Any, str | None]], list(zip(opts.split(), opts.split(), strict=False)))
43864386
else:
43874387
local_opts = opts
4388-
fulloptions: list[tuple[Any, str | None]] = []
4388+
fulloptions: list[tuple[Any, str]] = []
43894389
for opt in local_opts:
43904390
if isinstance(opt, str):
43914391
fulloptions.append((opt, opt))
43924392
else:
43934393
try:
4394-
fulloptions.append((opt[0], opt[1]))
4395-
except IndexError:
4396-
fulloptions.append((opt[0], opt[0]))
4394+
val = opt[0]
4395+
text = str(opt[1]) if len(opt) > 1 and opt[1] is not None else str(val)
4396+
fulloptions.append((val, text))
4397+
except (IndexError, TypeError):
4398+
fulloptions.append((opt[0], str(opt[0])))
4399+
4400+
if self.stdin.isatty() and self.stdout.isatty():
4401+
try:
4402+
while True:
4403+
result = choice(message=prompt, options=fulloptions)
4404+
if result is not None:
4405+
return result
4406+
except KeyboardInterrupt:
4407+
self.poutput('^C')
4408+
raise
4409+
4410+
# Non-interactive fallback
43974411
for idx, (_, text) in enumerate(fulloptions):
43984412
self.poutput(' %2d. %s' % (idx + 1, text)) # noqa: UP031
43994413

@@ -4411,10 +4425,10 @@ def select(self, opts: str | Iterable[str] | Iterable[tuple[Any, str | None]], p
44114425
continue
44124426

44134427
try:
4414-
choice = int(response)
4415-
if choice < 1:
4428+
choice_idx = int(response)
4429+
if choice_idx < 1:
44164430
raise IndexError # noqa: TRY301
4417-
return fulloptions[choice - 1][0]
4431+
return fulloptions[choice_idx - 1][0]
44184432
except (ValueError, IndexError):
44194433
self.poutput(f"'{response}' isn't a valid choice. Pick a number between 1 and {len(fulloptions)}:")
44204434

examples/remove_settable.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#!/usr/bin/env python
2-
"""A sample application for cmd2 demonstrating how to remove one of the built-in runtime settable parameters."""
2+
"""A sample application for cmd2 demonstrating how to remove one of the built-in runtime settable parameters.
3+
4+
It also demonstrates how to use the cmd2.Cmd.select method.
5+
"""
36

47
import cmd2
58

@@ -9,6 +12,12 @@ def __init__(self) -> None:
912
super().__init__()
1013
self.remove_settable('debug')
1114

15+
def do_eat(self, arg):
16+
sauce = self.select('sweet salty', 'Sauce? ')
17+
result = '{food} with {sauce} sauce, yum!'
18+
result = result.format(food=arg, sauce=sauce)
19+
self.stdout.write(result + '\n')
20+
1221

1322
if __name__ == '__main__':
1423
import sys

0 commit comments

Comments
 (0)