falyx 0.1.32__tar.gz → 0.1.34__tar.gz
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.
- {falyx-0.1.32 → falyx-0.1.34}/PKG-INFO +1 -1
- {falyx-0.1.32 → falyx-0.1.34}/falyx/action/__init__.py +2 -0
- falyx-0.1.34/falyx/action/prompt_menu_action.py +134 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/action/selection_action.py +40 -9
- {falyx-0.1.32 → falyx-0.1.34}/falyx/command.py +4 -2
- {falyx-0.1.32 → falyx-0.1.34}/falyx/falyx.py +18 -10
- {falyx-0.1.32 → falyx-0.1.34}/falyx/menu.py +8 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/parsers/argparse.py +84 -36
- {falyx-0.1.32 → falyx-0.1.34}/falyx/parsers/parsers.py +1 -1
- {falyx-0.1.32 → falyx-0.1.34}/falyx/parsers/signature.py +1 -1
- {falyx-0.1.32 → falyx-0.1.34}/falyx/selection.py +1 -1
- falyx-0.1.34/falyx/version.py +1 -0
- {falyx-0.1.32 → falyx-0.1.34}/pyproject.toml +1 -1
- falyx-0.1.32/falyx/version.py +0 -1
- {falyx-0.1.32 → falyx-0.1.34}/LICENSE +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/README.md +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/.pytyped +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/__init__.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/__main__.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/action/.pytyped +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/action/action.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/action/action_factory.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/action/http_action.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/action/io_action.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/action/menu_action.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/action/select_file_action.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/action/signal_action.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/action/types.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/action/user_input_action.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/bottom_bar.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/config.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/context.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/debug.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/exceptions.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/execution_registry.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/hook_manager.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/hooks.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/init.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/logger.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/options_manager.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/parsers/.pytyped +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/parsers/__init__.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/parsers/utils.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/prompt_utils.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/protocols.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/retry.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/retry_utils.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/signals.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/tagged_table.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/themes/__init__.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/themes/colors.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/utils.py +0 -0
- {falyx-0.1.32 → falyx-0.1.34}/falyx/validators.py +0 -0
@@ -18,6 +18,7 @@ from .action_factory import ActionFactoryAction
|
|
18
18
|
from .http_action import HTTPAction
|
19
19
|
from .io_action import BaseIOAction, ShellAction
|
20
20
|
from .menu_action import MenuAction
|
21
|
+
from .prompt_menu_action import PromptMenuAction
|
21
22
|
from .select_file_action import SelectFileAction
|
22
23
|
from .selection_action import SelectionAction
|
23
24
|
from .signal_action import SignalAction
|
@@ -40,4 +41,5 @@ __all__ = [
|
|
40
41
|
"FallbackAction",
|
41
42
|
"LiteralInputAction",
|
42
43
|
"UserInputAction",
|
44
|
+
"PromptMenuAction",
|
43
45
|
]
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
|
+
"""prompt_menu_action.py"""
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from prompt_toolkit import PromptSession
|
6
|
+
from prompt_toolkit.formatted_text import FormattedText, merge_formatted_text
|
7
|
+
from rich.console import Console
|
8
|
+
from rich.tree import Tree
|
9
|
+
|
10
|
+
from falyx.action.action import BaseAction
|
11
|
+
from falyx.context import ExecutionContext
|
12
|
+
from falyx.execution_registry import ExecutionRegistry as er
|
13
|
+
from falyx.hook_manager import HookType
|
14
|
+
from falyx.logger import logger
|
15
|
+
from falyx.menu import MenuOptionMap
|
16
|
+
from falyx.signals import BackSignal, QuitSignal
|
17
|
+
from falyx.themes import OneColors
|
18
|
+
|
19
|
+
|
20
|
+
class PromptMenuAction(BaseAction):
|
21
|
+
"""PromptMenuAction class for creating prompt -> actions."""
|
22
|
+
|
23
|
+
def __init__(
|
24
|
+
self,
|
25
|
+
name: str,
|
26
|
+
menu_options: MenuOptionMap,
|
27
|
+
*,
|
28
|
+
prompt_message: str = "Select > ",
|
29
|
+
default_selection: str = "",
|
30
|
+
inject_last_result: bool = False,
|
31
|
+
inject_into: str = "last_result",
|
32
|
+
console: Console | None = None,
|
33
|
+
prompt_session: PromptSession | None = None,
|
34
|
+
never_prompt: bool = False,
|
35
|
+
include_reserved: bool = True,
|
36
|
+
):
|
37
|
+
super().__init__(
|
38
|
+
name,
|
39
|
+
inject_last_result=inject_last_result,
|
40
|
+
inject_into=inject_into,
|
41
|
+
never_prompt=never_prompt,
|
42
|
+
)
|
43
|
+
self.menu_options = menu_options
|
44
|
+
self.prompt_message = prompt_message
|
45
|
+
self.default_selection = default_selection
|
46
|
+
self.console = console or Console(color_system="auto")
|
47
|
+
self.prompt_session = prompt_session or PromptSession()
|
48
|
+
self.include_reserved = include_reserved
|
49
|
+
|
50
|
+
def get_infer_target(self) -> tuple[None, None]:
|
51
|
+
return None, None
|
52
|
+
|
53
|
+
async def _run(self, *args, **kwargs) -> Any:
|
54
|
+
kwargs = self._maybe_inject_last_result(kwargs)
|
55
|
+
context = ExecutionContext(
|
56
|
+
name=self.name,
|
57
|
+
args=args,
|
58
|
+
kwargs=kwargs,
|
59
|
+
action=self,
|
60
|
+
)
|
61
|
+
|
62
|
+
effective_default = self.default_selection
|
63
|
+
maybe_result = str(self.last_result)
|
64
|
+
if maybe_result in self.menu_options:
|
65
|
+
effective_default = maybe_result
|
66
|
+
elif self.inject_last_result:
|
67
|
+
logger.warning(
|
68
|
+
"[%s] Injected last result '%s' not found in menu options",
|
69
|
+
self.name,
|
70
|
+
maybe_result,
|
71
|
+
)
|
72
|
+
|
73
|
+
if self.never_prompt and not effective_default:
|
74
|
+
raise ValueError(
|
75
|
+
f"[{self.name}] 'never_prompt' is True but no valid default_selection"
|
76
|
+
" was provided."
|
77
|
+
)
|
78
|
+
|
79
|
+
context.start_timer()
|
80
|
+
try:
|
81
|
+
await self.hooks.trigger(HookType.BEFORE, context)
|
82
|
+
key = effective_default
|
83
|
+
if not self.never_prompt:
|
84
|
+
placeholder_formatted_text = []
|
85
|
+
for index, (key, option) in enumerate(self.menu_options.items()):
|
86
|
+
placeholder_formatted_text.append(option.render_prompt(key))
|
87
|
+
if index < len(self.menu_options) - 1:
|
88
|
+
placeholder_formatted_text.append(
|
89
|
+
FormattedText([(OneColors.WHITE, " | ")])
|
90
|
+
)
|
91
|
+
placeholder = merge_formatted_text(placeholder_formatted_text)
|
92
|
+
key = await self.prompt_session.prompt_async(
|
93
|
+
message=self.prompt_message, placeholder=placeholder
|
94
|
+
)
|
95
|
+
option = self.menu_options[key]
|
96
|
+
result = await option.action(*args, **kwargs)
|
97
|
+
context.result = result
|
98
|
+
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
99
|
+
return result
|
100
|
+
|
101
|
+
except BackSignal:
|
102
|
+
logger.debug("[%s][BackSignal] ← Returning to previous menu", self.name)
|
103
|
+
return None
|
104
|
+
except QuitSignal:
|
105
|
+
logger.debug("[%s][QuitSignal] ← Exiting application", self.name)
|
106
|
+
raise
|
107
|
+
except Exception as error:
|
108
|
+
context.exception = error
|
109
|
+
await self.hooks.trigger(HookType.ON_ERROR, context)
|
110
|
+
raise
|
111
|
+
finally:
|
112
|
+
context.stop_timer()
|
113
|
+
await self.hooks.trigger(HookType.AFTER, context)
|
114
|
+
await self.hooks.trigger(HookType.ON_TEARDOWN, context)
|
115
|
+
er.record(context)
|
116
|
+
|
117
|
+
async def preview(self, parent: Tree | None = None):
|
118
|
+
label = f"[{OneColors.LIGHT_YELLOW_b}]📋 PromptMenuAction[/] '{self.name}'"
|
119
|
+
tree = parent.add(label) if parent else Tree(label)
|
120
|
+
for key, option in self.menu_options.items():
|
121
|
+
tree.add(
|
122
|
+
f"[dim]{key}[/]: {option.description} → [italic]{option.action.name}[/]"
|
123
|
+
)
|
124
|
+
await option.action.preview(parent=tree)
|
125
|
+
if not parent:
|
126
|
+
self.console.print(tree)
|
127
|
+
|
128
|
+
def __str__(self) -> str:
|
129
|
+
return (
|
130
|
+
f"PromptMenuAction(name={self.name!r}, options={list(self.menu_options.keys())!r}, "
|
131
|
+
f"default_selection={self.default_selection!r}, "
|
132
|
+
f"include_reserved={self.include_reserved}, "
|
133
|
+
f"prompt={'off' if self.never_prompt else 'on'})"
|
134
|
+
)
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
2
2
|
"""selection_action.py"""
|
3
|
+
from copy import copy
|
3
4
|
from typing import Any
|
4
5
|
|
5
6
|
from prompt_toolkit import PromptSession
|
@@ -72,6 +73,7 @@ class SelectionAction(BaseAction):
|
|
72
73
|
self.default_selection = default_selection
|
73
74
|
self.prompt_message = prompt_message
|
74
75
|
self.show_table = show_table
|
76
|
+
self.cancel_key = self._find_cancel_key()
|
75
77
|
|
76
78
|
def _coerce_return_type(
|
77
79
|
self, return_type: SelectionReturnType | str
|
@@ -115,12 +117,40 @@ class SelectionAction(BaseAction):
|
|
115
117
|
)
|
116
118
|
|
117
119
|
def _find_cancel_key(self) -> str:
|
118
|
-
"""
|
119
|
-
|
120
|
-
|
121
|
-
|
120
|
+
"""Find the cancel key in the selections."""
|
121
|
+
if isinstance(self.selections, dict):
|
122
|
+
for index in range(len(self.selections) + 1):
|
123
|
+
if str(index) not in self.selections:
|
124
|
+
return str(index)
|
122
125
|
return str(len(self.selections))
|
123
126
|
|
127
|
+
@property
|
128
|
+
def cancel_key(self) -> str:
|
129
|
+
return self._cancel_key
|
130
|
+
|
131
|
+
@cancel_key.setter
|
132
|
+
def cancel_key(self, value: str) -> None:
|
133
|
+
"""Set the cancel key for the selection."""
|
134
|
+
if not isinstance(value, str):
|
135
|
+
raise TypeError("Cancel key must be a string.")
|
136
|
+
if isinstance(self.selections, dict) and value in self.selections:
|
137
|
+
raise ValueError(
|
138
|
+
"Cancel key cannot be one of the selection keys. "
|
139
|
+
f"Current selections: {self.selections}"
|
140
|
+
)
|
141
|
+
if isinstance(self.selections, list):
|
142
|
+
if not value.isdigit() or int(value) > len(self.selections):
|
143
|
+
raise ValueError(
|
144
|
+
"cancel_key must be a digit and not greater than the number of selections."
|
145
|
+
)
|
146
|
+
self._cancel_key = value
|
147
|
+
|
148
|
+
def cancel_formatter(self, index: int, selection: str) -> str:
|
149
|
+
"""Format the cancel option for display."""
|
150
|
+
if self.cancel_key == str(index):
|
151
|
+
return f"[{index}] [{OneColors.DARK_RED}]Cancel[/]"
|
152
|
+
return f"[{index}] {selection}"
|
153
|
+
|
124
154
|
def get_infer_target(self) -> tuple[None, None]:
|
125
155
|
return None, None
|
126
156
|
|
@@ -164,16 +194,17 @@ class SelectionAction(BaseAction):
|
|
164
194
|
|
165
195
|
context.start_timer()
|
166
196
|
try:
|
167
|
-
cancel_key = self._find_cancel_key()
|
197
|
+
self.cancel_key = self._find_cancel_key()
|
168
198
|
await self.hooks.trigger(HookType.BEFORE, context)
|
169
199
|
if isinstance(self.selections, list):
|
170
200
|
table = render_selection_indexed_table(
|
171
201
|
title=self.title,
|
172
202
|
selections=self.selections + ["Cancel"],
|
173
203
|
columns=self.columns,
|
204
|
+
formatter=self.cancel_formatter,
|
174
205
|
)
|
175
206
|
if not self.never_prompt:
|
176
|
-
index = await prompt_for_index(
|
207
|
+
index: int | str = await prompt_for_index(
|
177
208
|
len(self.selections),
|
178
209
|
table,
|
179
210
|
default_selection=effective_default,
|
@@ -184,12 +215,12 @@ class SelectionAction(BaseAction):
|
|
184
215
|
)
|
185
216
|
else:
|
186
217
|
index = effective_default
|
187
|
-
if index == cancel_key:
|
218
|
+
if int(index) == int(self.cancel_key):
|
188
219
|
raise CancelSignal("User cancelled the selection.")
|
189
220
|
result: Any = self.selections[int(index)]
|
190
221
|
elif isinstance(self.selections, dict):
|
191
222
|
cancel_option = {
|
192
|
-
cancel_key: SelectionOption(
|
223
|
+
self.cancel_key: SelectionOption(
|
193
224
|
description="Cancel", value=CancelSignal, style=OneColors.DARK_RED
|
194
225
|
)
|
195
226
|
}
|
@@ -210,7 +241,7 @@ class SelectionAction(BaseAction):
|
|
210
241
|
)
|
211
242
|
else:
|
212
243
|
key = effective_default
|
213
|
-
if key == cancel_key:
|
244
|
+
if key == self.cancel_key:
|
214
245
|
raise CancelSignal("User cancelled the selection.")
|
215
246
|
if self.return_type == SelectionReturnType.KEY:
|
216
247
|
result = key
|
@@ -139,7 +139,7 @@ class Command(BaseModel):
|
|
139
139
|
|
140
140
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
141
141
|
|
142
|
-
def parse_args(
|
142
|
+
async def parse_args(
|
143
143
|
self, raw_args: list[str] | str, from_validate: bool = False
|
144
144
|
) -> tuple[tuple, dict]:
|
145
145
|
if callable(self.custom_parser):
|
@@ -165,7 +165,9 @@ class Command(BaseModel):
|
|
165
165
|
raw_args,
|
166
166
|
)
|
167
167
|
return ((), {})
|
168
|
-
return self.arg_parser.parse_args_split(
|
168
|
+
return await self.arg_parser.parse_args_split(
|
169
|
+
raw_args, from_validate=from_validate
|
170
|
+
)
|
169
171
|
|
170
172
|
@field_validator("action", mode="before")
|
171
173
|
@classmethod
|
@@ -83,8 +83,11 @@ class CommandValidator(Validator):
|
|
83
83
|
self.error_message = error_message
|
84
84
|
|
85
85
|
def validate(self, document) -> None:
|
86
|
+
pass
|
87
|
+
|
88
|
+
async def validate_async(self, document) -> None:
|
86
89
|
text = document.text
|
87
|
-
is_preview, choice, _, __ = self.falyx.get_command(text, from_validate=True)
|
90
|
+
is_preview, choice, _, __ = await self.falyx.get_command(text, from_validate=True)
|
88
91
|
if is_preview:
|
89
92
|
return None
|
90
93
|
if not choice:
|
@@ -188,7 +191,7 @@ class Falyx:
|
|
188
191
|
self.cli_args: Namespace | None = cli_args
|
189
192
|
self.render_menu: Callable[[Falyx], None] | None = render_menu
|
190
193
|
self.custom_table: Callable[[Falyx], Table] | Table | None = custom_table
|
191
|
-
self.
|
194
|
+
self._hide_menu_table: bool = hide_menu_table
|
192
195
|
self.validate_options(cli_args, options)
|
193
196
|
self._prompt_session: PromptSession | None = None
|
194
197
|
self.mode = FalyxMode.MENU
|
@@ -740,7 +743,7 @@ class Falyx:
|
|
740
743
|
return True, input_str[1:].strip()
|
741
744
|
return False, input_str.strip()
|
742
745
|
|
743
|
-
def get_command(
|
746
|
+
async def get_command(
|
744
747
|
self, raw_choices: str, from_validate=False
|
745
748
|
) -> tuple[bool, Command | None, tuple, dict[str, Any]]:
|
746
749
|
"""
|
@@ -773,7 +776,9 @@ class Falyx:
|
|
773
776
|
if is_preview:
|
774
777
|
return True, name_map[choice], args, kwargs
|
775
778
|
try:
|
776
|
-
args, kwargs = name_map[choice].parse_args(
|
779
|
+
args, kwargs = await name_map[choice].parse_args(
|
780
|
+
input_args, from_validate
|
781
|
+
)
|
777
782
|
except CommandArgumentError as error:
|
778
783
|
if not from_validate:
|
779
784
|
if not name_map[choice].show_help():
|
@@ -834,7 +839,7 @@ class Falyx:
|
|
834
839
|
"""Processes the action of the selected command."""
|
835
840
|
with patch_stdout(raw=True):
|
836
841
|
choice = await self.prompt_session.prompt_async()
|
837
|
-
is_preview, selected_command, args, kwargs = self.get_command(choice)
|
842
|
+
is_preview, selected_command, args, kwargs = await self.get_command(choice)
|
838
843
|
if not selected_command:
|
839
844
|
logger.info("Invalid command '%s'.", choice)
|
840
845
|
return True
|
@@ -876,7 +881,7 @@ class Falyx:
|
|
876
881
|
) -> Any:
|
877
882
|
"""Run a command by key without displaying the menu (non-interactive mode)."""
|
878
883
|
self.debug_hooks()
|
879
|
-
is_preview, selected_command, _, __ = self.get_command(command_key)
|
884
|
+
is_preview, selected_command, _, __ = await self.get_command(command_key)
|
880
885
|
kwargs = kwargs or {}
|
881
886
|
|
882
887
|
self.last_run_command = selected_command
|
@@ -975,7 +980,7 @@ class Falyx:
|
|
975
980
|
self.print_message(self.welcome_message)
|
976
981
|
try:
|
977
982
|
while True:
|
978
|
-
if not self.hide_menu_table:
|
983
|
+
if not self.options.get("hide_menu_table", self._hide_menu_table):
|
979
984
|
if callable(self.render_menu):
|
980
985
|
self.render_menu(self)
|
981
986
|
else:
|
@@ -1012,6 +1017,9 @@ class Falyx:
|
|
1012
1017
|
if not self.options.get("force_confirm"):
|
1013
1018
|
self.options.set("force_confirm", self._force_confirm)
|
1014
1019
|
|
1020
|
+
if not self.options.get("hide_menu_table"):
|
1021
|
+
self.options.set("hide_menu_table", self._hide_menu_table)
|
1022
|
+
|
1015
1023
|
if self.cli_args.verbose:
|
1016
1024
|
logging.getLogger("falyx").setLevel(logging.DEBUG)
|
1017
1025
|
|
@@ -1029,7 +1037,7 @@ class Falyx:
|
|
1029
1037
|
|
1030
1038
|
if self.cli_args.command == "preview":
|
1031
1039
|
self.mode = FalyxMode.PREVIEW
|
1032
|
-
_, command, args, kwargs = self.get_command(self.cli_args.name)
|
1040
|
+
_, command, args, kwargs = await self.get_command(self.cli_args.name)
|
1033
1041
|
if not command:
|
1034
1042
|
self.console.print(
|
1035
1043
|
f"[{OneColors.DARK_RED}]❌ Command '{self.cli_args.name}' not found."
|
@@ -1043,7 +1051,7 @@ class Falyx:
|
|
1043
1051
|
|
1044
1052
|
if self.cli_args.command == "run":
|
1045
1053
|
self.mode = FalyxMode.RUN
|
1046
|
-
is_preview, command, _, __ = self.get_command(self.cli_args.name)
|
1054
|
+
is_preview, command, _, __ = await self.get_command(self.cli_args.name)
|
1047
1055
|
if is_preview:
|
1048
1056
|
if command is None:
|
1049
1057
|
sys.exit(1)
|
@@ -1054,7 +1062,7 @@ class Falyx:
|
|
1054
1062
|
sys.exit(1)
|
1055
1063
|
self._set_retry_policy(command)
|
1056
1064
|
try:
|
1057
|
-
args, kwargs = command.parse_args(self.cli_args.command_args)
|
1065
|
+
args, kwargs = await command.parse_args(self.cli_args.command_args)
|
1058
1066
|
except HelpSignal:
|
1059
1067
|
sys.exit(0)
|
1060
1068
|
try:
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
from dataclasses import dataclass
|
4
4
|
|
5
|
+
from prompt_toolkit.formatted_text import FormattedText
|
6
|
+
|
5
7
|
from falyx.action import BaseAction
|
6
8
|
from falyx.signals import BackSignal, QuitSignal
|
7
9
|
from falyx.themes import OneColors
|
@@ -26,6 +28,12 @@ class MenuOption:
|
|
26
28
|
"""Render the menu option for display."""
|
27
29
|
return f"[{OneColors.WHITE}][{key}][/] [{self.style}]{self.description}[/]"
|
28
30
|
|
31
|
+
def render_prompt(self, key: str) -> FormattedText:
|
32
|
+
"""Render the menu option for prompt display."""
|
33
|
+
return FormattedText(
|
34
|
+
[(OneColors.WHITE, f"[{key}] "), (self.style, self.description)]
|
35
|
+
)
|
36
|
+
|
29
37
|
|
30
38
|
class MenuOptionMap(CaseInsensitiveDict):
|
31
39
|
"""
|
@@ -39,7 +39,7 @@ class ArgumentAction(Enum):
|
|
39
39
|
class Argument:
|
40
40
|
"""Represents a command-line argument."""
|
41
41
|
|
42
|
-
flags:
|
42
|
+
flags: tuple[str, ...]
|
43
43
|
dest: str # Destination name for the argument
|
44
44
|
action: ArgumentAction = (
|
45
45
|
ArgumentAction.STORE
|
@@ -49,7 +49,7 @@ class Argument:
|
|
49
49
|
choices: list[str] | None = None # List of valid choices for the argument
|
50
50
|
required: bool = False # True if the argument is required
|
51
51
|
help: str = "" # Help text for the argument
|
52
|
-
nargs: int | str =
|
52
|
+
nargs: int | str | None = None # int, '?', '*', '+', None
|
53
53
|
positional: bool = False # True if no leading - or -- in flags
|
54
54
|
|
55
55
|
def get_positional_text(self) -> str:
|
@@ -77,7 +77,11 @@ class Argument:
|
|
77
77
|
and not self.positional
|
78
78
|
):
|
79
79
|
choice_text = self.dest.upper()
|
80
|
-
elif
|
80
|
+
elif self.action in (
|
81
|
+
ArgumentAction.STORE,
|
82
|
+
ArgumentAction.APPEND,
|
83
|
+
ArgumentAction.EXTEND,
|
84
|
+
) or isinstance(self.nargs, str):
|
81
85
|
choice_text = self.dest
|
82
86
|
|
83
87
|
if self.nargs == "?":
|
@@ -147,6 +151,7 @@ class CommandArgumentParser:
|
|
147
151
|
aliases: list[str] | None = None,
|
148
152
|
) -> None:
|
149
153
|
"""Initialize the CommandArgumentParser."""
|
154
|
+
self.console = Console(color_system="auto")
|
150
155
|
self.command_key: str = command_key
|
151
156
|
self.command_description: str = command_description
|
152
157
|
self.command_style: str = command_style
|
@@ -159,7 +164,6 @@ class CommandArgumentParser:
|
|
159
164
|
self._flag_map: dict[str, Argument] = {}
|
160
165
|
self._dest_set: set[str] = set()
|
161
166
|
self._add_help()
|
162
|
-
self.console = Console(color_system="auto")
|
163
167
|
|
164
168
|
def _add_help(self):
|
165
169
|
"""Add help argument to the parser."""
|
@@ -181,9 +185,7 @@ class CommandArgumentParser:
|
|
181
185
|
raise CommandArgumentError("Positional arguments cannot have multiple flags")
|
182
186
|
return positional
|
183
187
|
|
184
|
-
def _get_dest_from_flags(
|
185
|
-
self, flags: tuple[str, ...], dest: str | None
|
186
|
-
) -> str | None:
|
188
|
+
def _get_dest_from_flags(self, flags: tuple[str, ...], dest: str | None) -> str:
|
187
189
|
"""Convert flags to a destination name."""
|
188
190
|
if dest:
|
189
191
|
if not dest.replace("_", "").isalnum():
|
@@ -212,7 +214,7 @@ class CommandArgumentParser:
|
|
212
214
|
return dest
|
213
215
|
|
214
216
|
def _determine_required(
|
215
|
-
self, required: bool, positional: bool, nargs: int | str
|
217
|
+
self, required: bool, positional: bool, nargs: int | str | None
|
216
218
|
) -> bool:
|
217
219
|
"""Determine if the argument is required."""
|
218
220
|
if required:
|
@@ -230,7 +232,22 @@ class CommandArgumentParser:
|
|
230
232
|
|
231
233
|
return required
|
232
234
|
|
233
|
-
def _validate_nargs(
|
235
|
+
def _validate_nargs(
|
236
|
+
self, nargs: int | str | None, action: ArgumentAction
|
237
|
+
) -> int | str | None:
|
238
|
+
if action in (
|
239
|
+
ArgumentAction.STORE_FALSE,
|
240
|
+
ArgumentAction.STORE_TRUE,
|
241
|
+
ArgumentAction.COUNT,
|
242
|
+
ArgumentAction.HELP,
|
243
|
+
):
|
244
|
+
if nargs is not None:
|
245
|
+
raise CommandArgumentError(
|
246
|
+
f"nargs cannot be specified for {action} actions"
|
247
|
+
)
|
248
|
+
return None
|
249
|
+
if nargs is None:
|
250
|
+
nargs = 1
|
234
251
|
allowed_nargs = ("?", "*", "+")
|
235
252
|
if isinstance(nargs, int):
|
236
253
|
if nargs <= 0:
|
@@ -242,7 +259,9 @@ class CommandArgumentParser:
|
|
242
259
|
raise CommandArgumentError(f"nargs must be an int or one of {allowed_nargs}")
|
243
260
|
return nargs
|
244
261
|
|
245
|
-
def _normalize_choices(
|
262
|
+
def _normalize_choices(
|
263
|
+
self, choices: Iterable | None, expected_type: Any
|
264
|
+
) -> list[Any]:
|
246
265
|
if choices is not None:
|
247
266
|
if isinstance(choices, dict):
|
248
267
|
raise CommandArgumentError("choices cannot be a dict")
|
@@ -289,8 +308,34 @@ class CommandArgumentParser:
|
|
289
308
|
f"Default list value {default!r} for '{dest}' cannot be coerced to {expected_type.__name__}"
|
290
309
|
)
|
291
310
|
|
311
|
+
def _validate_action(
|
312
|
+
self, action: ArgumentAction | str, positional: bool
|
313
|
+
) -> ArgumentAction:
|
314
|
+
if not isinstance(action, ArgumentAction):
|
315
|
+
try:
|
316
|
+
action = ArgumentAction(action)
|
317
|
+
except ValueError:
|
318
|
+
raise CommandArgumentError(
|
319
|
+
f"Invalid action '{action}' is not a valid ArgumentAction"
|
320
|
+
)
|
321
|
+
if action in (
|
322
|
+
ArgumentAction.STORE_TRUE,
|
323
|
+
ArgumentAction.STORE_FALSE,
|
324
|
+
ArgumentAction.COUNT,
|
325
|
+
ArgumentAction.HELP,
|
326
|
+
):
|
327
|
+
if positional:
|
328
|
+
raise CommandArgumentError(
|
329
|
+
f"Action '{action}' cannot be used with positional arguments"
|
330
|
+
)
|
331
|
+
|
332
|
+
return action
|
333
|
+
|
292
334
|
def _resolve_default(
|
293
|
-
self,
|
335
|
+
self,
|
336
|
+
default: Any,
|
337
|
+
action: ArgumentAction,
|
338
|
+
nargs: str | int | None,
|
294
339
|
) -> Any:
|
295
340
|
"""Get the default value for the argument."""
|
296
341
|
if default is None:
|
@@ -324,7 +369,18 @@ class CommandArgumentParser:
|
|
324
369
|
f"Flag '{flag}' must be a single character or start with '--'"
|
325
370
|
)
|
326
371
|
|
327
|
-
def add_argument(
|
372
|
+
def add_argument(
|
373
|
+
self,
|
374
|
+
*flags,
|
375
|
+
action: str | ArgumentAction = "store",
|
376
|
+
nargs: int | str | None = None,
|
377
|
+
default: Any = None,
|
378
|
+
type: Any = str,
|
379
|
+
choices: Iterable | None = None,
|
380
|
+
required: bool = False,
|
381
|
+
help: str = "",
|
382
|
+
dest: str | None = None,
|
383
|
+
) -> None:
|
328
384
|
"""Add an argument to the parser.
|
329
385
|
Args:
|
330
386
|
name or flags: Either a name or prefixed flags (e.g. 'faylx', '-f', '--falyx').
|
@@ -337,9 +393,10 @@ class CommandArgumentParser:
|
|
337
393
|
help: A brief description of the argument.
|
338
394
|
dest: The name of the attribute to be added to the object returned by parse_args().
|
339
395
|
"""
|
396
|
+
expected_type = type
|
340
397
|
self._validate_flags(flags)
|
341
398
|
positional = self._is_positional(flags)
|
342
|
-
dest = self._get_dest_from_flags(flags,
|
399
|
+
dest = self._get_dest_from_flags(flags, dest)
|
343
400
|
if dest in self._dest_set:
|
344
401
|
raise CommandArgumentError(
|
345
402
|
f"Destination '{dest}' is already defined.\n"
|
@@ -347,18 +404,9 @@ class CommandArgumentParser:
|
|
347
404
|
"is not supported. Define a unique 'dest' for each argument."
|
348
405
|
)
|
349
406
|
self._dest_set.add(dest)
|
350
|
-
action =
|
351
|
-
|
352
|
-
|
353
|
-
action = ArgumentAction(action)
|
354
|
-
except ValueError:
|
355
|
-
raise CommandArgumentError(
|
356
|
-
f"Invalid action '{action}' is not a valid ArgumentAction"
|
357
|
-
)
|
358
|
-
flags = list(flags)
|
359
|
-
nargs = self._validate_nargs(kwargs.get("nargs", 1))
|
360
|
-
default = self._resolve_default(action, kwargs.get("default"), nargs)
|
361
|
-
expected_type = kwargs.get("type", str)
|
407
|
+
action = self._validate_action(action, positional)
|
408
|
+
nargs = self._validate_nargs(nargs, action)
|
409
|
+
default = self._resolve_default(default, action, nargs)
|
362
410
|
if (
|
363
411
|
action in (ArgumentAction.STORE, ArgumentAction.APPEND, ArgumentAction.EXTEND)
|
364
412
|
and default is not None
|
@@ -367,14 +415,12 @@ class CommandArgumentParser:
|
|
367
415
|
self._validate_default_list_type(default, expected_type, dest)
|
368
416
|
else:
|
369
417
|
self._validate_default_type(default, expected_type, dest)
|
370
|
-
choices = self._normalize_choices(
|
418
|
+
choices = self._normalize_choices(choices, expected_type)
|
371
419
|
if default is not None and choices and default not in choices:
|
372
420
|
raise CommandArgumentError(
|
373
421
|
f"Default value '{default}' not in allowed choices: {choices}"
|
374
422
|
)
|
375
|
-
required = self._determine_required(
|
376
|
-
kwargs.get("required", False), positional, nargs
|
377
|
-
)
|
423
|
+
required = self._determine_required(required, positional, nargs)
|
378
424
|
argument = Argument(
|
379
425
|
flags=flags,
|
380
426
|
dest=dest,
|
@@ -383,7 +429,7 @@ class CommandArgumentParser:
|
|
383
429
|
default=default,
|
384
430
|
choices=choices,
|
385
431
|
required=required,
|
386
|
-
help=
|
432
|
+
help=help,
|
387
433
|
nargs=nargs,
|
388
434
|
positional=positional,
|
389
435
|
)
|
@@ -426,11 +472,11 @@ class CommandArgumentParser:
|
|
426
472
|
values = []
|
427
473
|
i = start
|
428
474
|
if isinstance(spec.nargs, int):
|
429
|
-
# assert i + spec.nargs <= len(
|
430
|
-
# args
|
431
|
-
# ), "Not enough arguments provided: shouldn't happen"
|
432
475
|
values = args[i : i + spec.nargs]
|
433
476
|
return values, i + spec.nargs
|
477
|
+
elif spec.nargs is None:
|
478
|
+
values = [args[i]]
|
479
|
+
return values, i + 1
|
434
480
|
elif spec.nargs == "+":
|
435
481
|
if i >= len(args):
|
436
482
|
raise CommandArgumentError(
|
@@ -475,6 +521,8 @@ class CommandArgumentParser:
|
|
475
521
|
for next_spec in positional_args[j + 1 :]:
|
476
522
|
if isinstance(next_spec.nargs, int):
|
477
523
|
min_required += next_spec.nargs
|
524
|
+
elif next_spec.nargs is None:
|
525
|
+
min_required += 1
|
478
526
|
elif next_spec.nargs == "+":
|
479
527
|
min_required += 1
|
480
528
|
elif next_spec.nargs == "?":
|
@@ -517,7 +565,7 @@ class CommandArgumentParser:
|
|
517
565
|
|
518
566
|
return i
|
519
567
|
|
520
|
-
def parse_args(
|
568
|
+
async def parse_args(
|
521
569
|
self, args: list[str] | None = None, from_validate: bool = False
|
522
570
|
) -> dict[str, Any]:
|
523
571
|
"""Parse Falyx Command arguments."""
|
@@ -665,7 +713,7 @@ class CommandArgumentParser:
|
|
665
713
|
result.pop("help", None)
|
666
714
|
return result
|
667
715
|
|
668
|
-
def parse_args_split(
|
716
|
+
async def parse_args_split(
|
669
717
|
self, args: list[str], from_validate: bool = False
|
670
718
|
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
671
719
|
"""
|
@@ -673,7 +721,7 @@ class CommandArgumentParser:
|
|
673
721
|
tuple[args, kwargs] - Positional arguments in defined order,
|
674
722
|
followed by keyword argument mapping.
|
675
723
|
"""
|
676
|
-
parsed = self.parse_args(args, from_validate)
|
724
|
+
parsed = await self.parse_args(args, from_validate)
|
677
725
|
args_list = []
|
678
726
|
kwargs_dict = {}
|
679
727
|
for arg in self._arguments:
|
@@ -271,7 +271,7 @@ async def prompt_for_index(
|
|
271
271
|
prompt_session: PromptSession | None = None,
|
272
272
|
prompt_message: str = "Select an option > ",
|
273
273
|
show_table: bool = True,
|
274
|
-
):
|
274
|
+
) -> int:
|
275
275
|
prompt_session = prompt_session or PromptSession()
|
276
276
|
console = console or Console(color_system="auto")
|
277
277
|
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.1.34"
|
falyx-0.1.32/falyx/version.py
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
__version__ = "0.1.32"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|