Add options_manager, rework bottom_bar to only render through closures, easy add of options to bottom bar
This commit is contained in:
		| @@ -39,7 +39,7 @@ pip install falyx | |||||||
| > Or install from source: | > Or install from source: | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| git clone https://github.com/yourname/falyx.git | git clone https://github.com/rolandtjr/falyx.git | ||||||
| cd falyx | cd falyx | ||||||
| poetry install | poetry install | ||||||
| ``` | ``` | ||||||
|   | |||||||
							
								
								
									
										0
									
								
								falyx/.pytyped
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								falyx/.pytyped
									
									
									
									
									
										Normal file
									
								
							| @@ -38,4 +38,5 @@ def build_falyx() -> Falyx: | |||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     logging.basicConfig(level=logging.WARNING) |     logging.basicConfig(level=logging.WARNING) | ||||||
|     falyx = build_falyx() |     falyx = build_falyx() | ||||||
|     asyncio.run(falyx.cli()) |     asyncio.run(falyx.run()) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -471,8 +471,6 @@ class ProcessAction(BaseAction): | |||||||
|         try: |         try: | ||||||
|             import pickle |             import pickle | ||||||
|             pickle.dumps(obj) |             pickle.dumps(obj) | ||||||
|             print("YES") |  | ||||||
|             return True |             return True | ||||||
|         except (pickle.PicklingError, TypeError): |         except (pickle.PicklingError, TypeError): | ||||||
|             print("NO") |  | ||||||
|             return False |             return False | ||||||
|   | |||||||
| @@ -1,53 +1,92 @@ | |||||||
| """bottom_bar.py""" | """bottom_bar.py""" | ||||||
| from typing import Any, Callable, Optional |  | ||||||
|  | from typing import Any, Callable | ||||||
|  |  | ||||||
| from prompt_toolkit.formatted_text import HTML, merge_formatted_text | from prompt_toolkit.formatted_text import HTML, merge_formatted_text | ||||||
| from prompt_toolkit.key_binding import KeyBindings | from prompt_toolkit.key_binding import KeyBindings | ||||||
| from rich.console import Console | from rich.console import Console | ||||||
|  |  | ||||||
|  | from falyx.options_manager import OptionsManager | ||||||
| from falyx.themes.colors import OneColors | from falyx.themes.colors import OneColors | ||||||
| from falyx.utils import CaseInsensitiveDict | from falyx.utils import CaseInsensitiveDict | ||||||
|  |  | ||||||
|  |  | ||||||
| class BottomBar: | class BottomBar: | ||||||
|     """Bottom Bar class for displaying a bottom bar in the terminal.""" |     """ | ||||||
|     def __init__(self, columns: int = 3, key_bindings: KeyBindings | None = None): |     Bottom Bar class for displaying a bottom bar in the terminal. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         columns (int): Number of columns in the bottom bar. | ||||||
|  |         key_bindings (KeyBindings, optional): Key bindings for the bottom bar. | ||||||
|  |         key_validator (Callable[[str], bool], optional): Function to validate toggle keys. | ||||||
|  |             Must return True if key is available, otherwise False. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__( | ||||||
|  |         self, | ||||||
|  |         columns: int = 3, | ||||||
|  |         key_bindings: KeyBindings | None = None, | ||||||
|  |         key_validator: Callable[[str], bool] | None = None, | ||||||
|  |     ) -> None: | ||||||
|         self.columns = columns |         self.columns = columns | ||||||
|         self.console = Console() |         self.console = Console() | ||||||
|         self._items: list[Callable[[], HTML]] = [] |         self._items: list[Callable[[], HTML]] = [] | ||||||
|         self._named_items: dict[str, Callable[[], HTML]] = {} |         self._named_items: dict[str, Callable[[], HTML]] = {} | ||||||
|         self._states: dict[str, Any] = CaseInsensitiveDict() |         self._value_getters: dict[str, Callable[[], Any]] = CaseInsensitiveDict() | ||||||
|         self.toggles: list[str] = [] |         self.toggle_keys: list[str] = [] | ||||||
|         self.key_bindings = key_bindings or KeyBindings() |         self.key_bindings = key_bindings or KeyBindings() | ||||||
|  |         self.key_validator = key_validator | ||||||
|  |  | ||||||
|     def get_space(self) -> int: |     @staticmethod | ||||||
|  |     def default_render(label: str, value: Any, fg: str, bg: str, width: int) -> HTML: | ||||||
|  |         return HTML(f"<style fg='{fg}' bg='{bg}'>{label}: {value:^{width}}</style>") | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def space(self) -> int: | ||||||
|         return self.console.width // self.columns |         return self.console.width // self.columns | ||||||
|  |  | ||||||
|  |     def add_custom( | ||||||
|  |         self, | ||||||
|  |         name: str, | ||||||
|  |         render_fn: Callable[[], HTML] | ||||||
|  |     ) -> None: | ||||||
|  |         """Add a custom render function to the bottom bar.""" | ||||||
|  |         if not callable(render_fn): | ||||||
|  |             raise ValueError("`render_fn` must be callable") | ||||||
|  |         self._add_named(name, render_fn) | ||||||
|  |  | ||||||
|     def add_static( |     def add_static( | ||||||
|         self, name: str, text: str, fg: str = OneColors.BLACK, bg: str = OneColors.WHITE |         self, | ||||||
|  |         name: str, | ||||||
|  |         text: str, | ||||||
|  |         fg: str = OneColors.BLACK, | ||||||
|  |         bg: str = OneColors.WHITE, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         def render(): |         def render(): | ||||||
|             return HTML( |             return HTML( | ||||||
|                 f"<style fg='{fg}' bg='{bg}'>{text:^{self.get_space()}}</style>" |                 f"<style fg='{fg}' bg='{bg}'>{text:^{self.space}}</style>" | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         self._add_named(name, render) |         self._add_named(name, render) | ||||||
|  |  | ||||||
|     def add_counter( |     def add_value_tracker( | ||||||
|         self, |         self, | ||||||
|         name: str, |         name: str, | ||||||
|         label: str, |         label: str, | ||||||
|         current: int, |         get_value: Callable[[], Any], | ||||||
|         fg: str = OneColors.BLACK, |         fg: str = OneColors.BLACK, | ||||||
|         bg: str = OneColors.WHITE, |         bg: str = OneColors.WHITE, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         self._states[name] = (label, current) |         if not callable(get_value): | ||||||
|  |             raise ValueError("`get_value` must be a callable returning any value") | ||||||
|  |         self._value_getters[name] = get_value | ||||||
|  |  | ||||||
|         def render(): |         def render(): | ||||||
|             label_, current_ = self._states[name] |             get_value_ = self._value_getters[name] | ||||||
|             text = f"{label_}: {current_}" |             current_ = get_value_() | ||||||
|  |             text = f"{label}: {current_}" | ||||||
|             return HTML( |             return HTML( | ||||||
|                 f"<style fg='{fg}' bg='{bg}'>{text:^{self.get_space()}}</style>" |                 f"<style fg='{fg}' bg='{bg}'>{text:^{self.space}}</style>" | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         self._add_named(name, render) |         self._add_named(name, render) | ||||||
| @@ -56,23 +95,26 @@ class BottomBar: | |||||||
|         self, |         self, | ||||||
|         name: str, |         name: str, | ||||||
|         label: str, |         label: str, | ||||||
|         current: int, |         get_current: Callable[[], int], | ||||||
|         total: int, |         total: int, | ||||||
|         fg: str = OneColors.BLACK, |         fg: str = OneColors.BLACK, | ||||||
|         bg: str = OneColors.WHITE, |         bg: str = OneColors.WHITE, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         self._states[name] = (label, current, total) |         if not callable(get_current): | ||||||
|  |             raise ValueError("`get_current` must be a callable returning int") | ||||||
|  |  | ||||||
|         if current > total: |         self._value_getters[name] = get_current | ||||||
|             raise ValueError( |  | ||||||
|                 f"Current value {current} is greater than total value {total}" |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|         def render(): |         def render(): | ||||||
|             label_, current_, text_ = self._states[name] |             get_current_ = self._value_getters[name] | ||||||
|             text = f"{label_}: {current_}/{text_}" |             current_value = get_current_() | ||||||
|  |             if current_value > total: | ||||||
|  |                 raise ValueError( | ||||||
|  |                     f"Current value {current_value} is greater than total value {total}" | ||||||
|  |                 ) | ||||||
|  |             text = f"{label}: {current_value}/{total}" | ||||||
|             return HTML( |             return HTML( | ||||||
|                 f"<style fg='{fg}' bg='{bg}'>{text:^{self.get_space()}}</style>" |                 f"<style fg='{fg}' bg='{bg}'>{text:^{self.space}}</style>" | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         self._add_named(name, render) |         self._add_named(name, render) | ||||||
| @@ -81,24 +123,31 @@ class BottomBar: | |||||||
|         self, |         self, | ||||||
|         key: str, |         key: str, | ||||||
|         label: str, |         label: str, | ||||||
|         state: bool, |         get_state: Callable[[], bool], | ||||||
|  |         toggle_state: Callable[[], None], | ||||||
|         fg: str = OneColors.BLACK, |         fg: str = OneColors.BLACK, | ||||||
|         bg_on: str = OneColors.GREEN, |         bg_on: str = OneColors.GREEN, | ||||||
|         bg_off: str = OneColors.DARK_RED, |         bg_off: str = OneColors.DARK_RED, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|  |         if not callable(get_state): | ||||||
|  |             raise ValueError("`get_state` must be a callable returning bool") | ||||||
|  |         if not callable(toggle_state): | ||||||
|  |             raise ValueError("`toggle_state` must be a callable") | ||||||
|         key = key.upper() |         key = key.upper() | ||||||
|         if key in self.toggles: |         if key in self.toggle_keys: | ||||||
|             raise ValueError(f"Key {key} is already used as a toggle") |             raise ValueError(f"Key {key} is already used as a toggle") | ||||||
|         self._states[key] = (label, state) |         if self.key_validator and not self.key_validator(key): | ||||||
|         self.toggles.append(key) |             raise ValueError(f"Key '{key}' conflicts with existing command, toggle, or reserved key.") | ||||||
|  |         self._value_getters[key] = get_state | ||||||
|  |         self.toggle_keys.append(key) | ||||||
|  |  | ||||||
|         def render(): |         def render(): | ||||||
|             label_, state_ = self._states[key] |             get_state_ = self._value_getters[key] | ||||||
|             color = bg_on if state_ else bg_off |             color = bg_on if get_state_() else bg_off | ||||||
|             status = "ON" if state_ else "OFF" |             status = "ON" if get_state_() else "OFF" | ||||||
|             text = f"({key.upper()}) {label_}: {status}" |             text = f"({key.upper()}) {label}: {status}" | ||||||
|             return HTML( |             return HTML( | ||||||
|                 f"<style bg='{color}' fg='{fg}'>{text:^{self.get_space()}}</style>" |                 f"<style bg='{color}' fg='{fg}'>{text:^{self.space}}</style>" | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         self._add_named(key, render) |         self._add_named(key, render) | ||||||
| @@ -106,43 +155,43 @@ class BottomBar: | |||||||
|         for k in (key.upper(), key.lower()): |         for k in (key.upper(), key.lower()): | ||||||
|  |  | ||||||
|             @self.key_bindings.add(k) |             @self.key_bindings.add(k) | ||||||
|             def _(event, key=k): |             def _(event): | ||||||
|                 self.toggle_state(key) |                 toggle_state() | ||||||
|  |  | ||||||
|     def toggle_state(self, name: str) -> bool: |     def add_toggle_from_option( | ||||||
|         label, state = self._states.get(name, (None, False)) |         self, | ||||||
|         new_state = not state |         key: str, | ||||||
|         self.update_toggle(name, new_state) |         label: str, | ||||||
|         return new_state |         options: OptionsManager, | ||||||
|  |         option_name: str, | ||||||
|     def update_toggle(self, name: str, state: bool) -> None: |         namespace_name: str = "cli_args", | ||||||
|         if name in self._states: |         fg: str = OneColors.BLACK, | ||||||
|             label, _ = self._states[name] |         bg_on: str = OneColors.GREEN, | ||||||
|             self._states[name] = (label, state) |         bg_off: str = OneColors.DARK_RED, | ||||||
|  |  | ||||||
|     def increment_counter(self, name: str) -> None: |  | ||||||
|         if name in self._states: |  | ||||||
|             label, current = self._states[name] |  | ||||||
|             self._states[name] = (label, current + 1) |  | ||||||
|  |  | ||||||
|     def increment_total_counter(self, name: str) -> None: |  | ||||||
|         if name in self._states: |  | ||||||
|             label, current, total = self._states[name] |  | ||||||
|             if current < total: |  | ||||||
|                 self._states[name] = (label, current + 1, total) |  | ||||||
|  |  | ||||||
|     def update_counter( |  | ||||||
|         self, name: str, current: Optional[int] = None, total: Optional[int] = None |  | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         if name in self._states: |         self.add_toggle( | ||||||
|             label, c, t = self._states[name] |             key=key, | ||||||
|             self._states[name] = ( |             label=label, | ||||||
|                 label, |             get_state=options.get_value_getter(option_name, namespace_name), | ||||||
|                 current if current is not None else c, |             toggle_state=options.get_toggle_function(option_name, namespace_name), | ||||||
|                 total if total is not None else t, |             fg=fg, | ||||||
|             ) |             bg_on=bg_on, | ||||||
|  |             bg_off=bg_off, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def values(self) -> dict[str, Any]: | ||||||
|  |         """Return the current computed values for all registered items.""" | ||||||
|  |         return {label: getter() for label, getter in self._value_getters.items()} | ||||||
|  |  | ||||||
|  |     def get_value(self, name: str) -> Any: | ||||||
|  |         if name not in self._value_getters: | ||||||
|  |             raise ValueError(f"No value getter registered under name: '{name}'") | ||||||
|  |         return self._value_getters[name]() | ||||||
|  |  | ||||||
|     def _add_named(self, name: str, render_fn: Callable[[], HTML]) -> None: |     def _add_named(self, name: str, render_fn: Callable[[], HTML]) -> None: | ||||||
|  |         if name in self._named_items: | ||||||
|  |             raise ValueError(f"Bottom bar item '{name}' already exists") | ||||||
|         self._named_items[name] = render_fn |         self._named_items[name] = render_fn | ||||||
|         self._items = list(self._named_items.values()) |         self._items = list(self._named_items.values()) | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										130
									
								
								falyx/falyx.py
									
									
									
									
									
								
							
							
						
						
									
										130
									
								
								falyx/falyx.py
									
									
									
									
									
								
							| @@ -39,7 +39,8 @@ from falyx.exceptions import (CommandAlreadyExistsError, FalyxError, | |||||||
|                               InvalidActionError, NotAFalyxError) |                               InvalidActionError, NotAFalyxError) | ||||||
| from falyx.execution_registry import ExecutionRegistry as er | from falyx.execution_registry import ExecutionRegistry as er | ||||||
| from falyx.hook_manager import Hook, HookManager, HookType | from falyx.hook_manager import Hook, HookManager, HookType | ||||||
| from falyx.parsers import FalyxParsers, get_arg_parsers | from falyx.options_manager import OptionsManager | ||||||
|  | from falyx.parsers import get_arg_parsers | ||||||
| from falyx.retry import RetryPolicy | from falyx.retry import RetryPolicy | ||||||
| from falyx.themes.colors import OneColors, get_nord_theme | from falyx.themes.colors import OneColors, get_nord_theme | ||||||
| from falyx.utils import CaseInsensitiveDict, async_confirm, chunks, logger | from falyx.utils import CaseInsensitiveDict, async_confirm, chunks, logger | ||||||
| @@ -78,7 +79,7 @@ class Falyx: | |||||||
|         title: str | Markdown = "Menu", |         title: str | Markdown = "Menu", | ||||||
|         prompt: str | AnyFormattedText = "> ", |         prompt: str | AnyFormattedText = "> ", | ||||||
|         columns: int = 3, |         columns: int = 3, | ||||||
|         bottom_bar: BottomBar | str | Callable[[], None] | None = None, |         bottom_bar: BottomBar | str | Callable[[], Any] | None = None, | ||||||
|         welcome_message: str | Markdown | dict[str, Any] = "", |         welcome_message: str | Markdown | dict[str, Any] = "", | ||||||
|         exit_message: str | Markdown | dict[str, Any] = "", |         exit_message: str | Markdown | dict[str, Any] = "", | ||||||
|         key_bindings: KeyBindings | None = None, |         key_bindings: KeyBindings | None = None, | ||||||
| @@ -88,6 +89,7 @@ class Falyx: | |||||||
|         never_confirm: bool = False, |         never_confirm: bool = False, | ||||||
|         always_confirm: bool = False, |         always_confirm: bool = False, | ||||||
|         cli_args: Namespace | None = None, |         cli_args: Namespace | None = None, | ||||||
|  |         options: OptionsManager | None = None, | ||||||
|         custom_table: Callable[["Falyx"], Table] | Table | None = None, |         custom_table: Callable[["Falyx"], Table] | Table | None = None, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         """Initializes the Falyx object.""" |         """Initializes the Falyx object.""" | ||||||
| @@ -104,12 +106,35 @@ class Falyx: | |||||||
|         self.hooks: HookManager = HookManager() |         self.hooks: HookManager = HookManager() | ||||||
|         self.last_run_command: Command | None = None |         self.last_run_command: Command | None = None | ||||||
|         self.key_bindings: KeyBindings = key_bindings or KeyBindings() |         self.key_bindings: KeyBindings = key_bindings or KeyBindings() | ||||||
|         self.bottom_bar: BottomBar | str | Callable[[], None] = bottom_bar or BottomBar(columns=columns, key_bindings=self.key_bindings) |         self.bottom_bar: BottomBar | str | Callable[[], None] = bottom_bar | ||||||
|         self.confirm_on_error: bool = confirm_on_error |         self.confirm_on_error: bool = confirm_on_error | ||||||
|         self._never_confirm: bool = never_confirm |         self._never_confirm: bool = never_confirm | ||||||
|         self._always_confirm: bool = always_confirm |         self._always_confirm: bool = always_confirm | ||||||
|         self.cli_args: Namespace | None = cli_args |         self.cli_args: Namespace | None = cli_args | ||||||
|         self.custom_table: Callable[["Falyx"], Table] | Table | None = custom_table |         self.custom_table: Callable[["Falyx"], Table] | Table | None = custom_table | ||||||
|  |         self.set_options(cli_args, options) | ||||||
|  |  | ||||||
|  |     def set_options( | ||||||
|  |             self, | ||||||
|  |             cli_args: Namespace | None, | ||||||
|  |             options: OptionsManager | None = None, | ||||||
|  |         ) -> None: | ||||||
|  |         """Checks if the options are set correctly.""" | ||||||
|  |         self.options: OptionsManager = options or OptionsManager() | ||||||
|  |         if not cli_args and not options: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         if options and not cli_args: | ||||||
|  |             raise FalyxError("Options are set, but CLI arguments are not.") | ||||||
|  |  | ||||||
|  |         if options is None: | ||||||
|  |             self.options.from_namespace(cli_args, "cli_args") | ||||||
|  |  | ||||||
|  |         if not isinstance(self.options, OptionsManager): | ||||||
|  |             raise FalyxError("Options must be an instance of OptionsManager.") | ||||||
|  |  | ||||||
|  |         if not isinstance(self.cli_args, Namespace): | ||||||
|  |             raise FalyxError("CLI arguments must be a Namespace object.") | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def _name_map(self) -> dict[str, Command]: |     def _name_map(self) -> dict[str, Command]: | ||||||
| @@ -248,8 +273,8 @@ class Falyx: | |||||||
|             keys.add(cmd.key.upper()) |             keys.add(cmd.key.upper()) | ||||||
|             keys.update({alias.upper() for alias in cmd.aliases}) |             keys.update({alias.upper() for alias in cmd.aliases}) | ||||||
|  |  | ||||||
|         if isinstance(self.bottom_bar, BottomBar): |         if isinstance(self._bottom_bar, BottomBar): | ||||||
|             toggle_keys = {key.upper() for key in self.bottom_bar.toggles} |             toggle_keys = {key.upper() for key in self._bottom_bar.toggle_keys} | ||||||
|         else: |         else: | ||||||
|             toggle_keys = set() |             toggle_keys = set() | ||||||
|  |  | ||||||
| @@ -277,57 +302,47 @@ class Falyx: | |||||||
|         if hasattr(self, "session"): |         if hasattr(self, "session"): | ||||||
|             del self.session |             del self.session | ||||||
|  |  | ||||||
|     def add_toggle(self, key: str, label: str, state: bool) -> None: |  | ||||||
|         """Adds a toggle to the bottom bar.""" |  | ||||||
|         assert isinstance(self.bottom_bar, BottomBar), "Bottom bar must be an instance of BottomBar." |  | ||||||
|         self.bottom_bar.add_toggle(key, label, state) |  | ||||||
|         self._invalidate_session_cache() |  | ||||||
|  |  | ||||||
|     def add_counter(self, name: str, label: str, current: int) -> None: |  | ||||||
|         """Adds a counter to the bottom bar.""" |  | ||||||
|         assert isinstance(self.bottom_bar, BottomBar), "Bottom bar must be an instance of BottomBar." |  | ||||||
|         self.bottom_bar.add_counter(name, label, current) |  | ||||||
|         self._invalidate_session_cache() |  | ||||||
|  |  | ||||||
|     def add_total_counter(self, name: str, label: str, current: int, total: int) -> None: |  | ||||||
|         """Adds a counter to the bottom bar.""" |  | ||||||
|         assert isinstance(self.bottom_bar, BottomBar), "Bottom bar must be an instance of BottomBar." |  | ||||||
|         self.bottom_bar.add_total_counter(name, label, current, total) |  | ||||||
|         self._invalidate_session_cache() |  | ||||||
|  |  | ||||||
|     def add_static(self, name: str, text: str) -> None: |  | ||||||
|         """Adds a static element to the bottom bar.""" |  | ||||||
|         assert isinstance(self.bottom_bar, BottomBar), "Bottom bar must be an instance of BottomBar." |  | ||||||
|         self.bottom_bar.add_static(name, text) |  | ||||||
|         self._invalidate_session_cache |  | ||||||
|  |  | ||||||
|     def get_toggle_state(self, key: str) -> bool | None: |  | ||||||
|         assert isinstance(self.bottom_bar, BottomBar), "Bottom bar must be an instance of BottomBar." |  | ||||||
|         if key.upper() in self.bottom_bar._states: |  | ||||||
|             """Returns the state of a toggle.""" |  | ||||||
|             return self.bottom_bar._states[key.upper()][1] |  | ||||||
|         return None |  | ||||||
|  |  | ||||||
|     def add_help_command(self): |     def add_help_command(self): | ||||||
|         """Adds a help command to the menu if it doesn't already exist.""" |         """Adds a help command to the menu if it doesn't already exist.""" | ||||||
|         if not self.help_command: |         if not self.help_command: | ||||||
|             self.help_command = self._get_help_command() |             self.help_command = self._get_help_command() | ||||||
|             self._invalidate_session_cache() |  | ||||||
|  |  | ||||||
|     def add_history_command(self): |     def add_history_command(self): | ||||||
|         """Adds a history command to the menu if it doesn't already exist.""" |         """Adds a history command to the menu if it doesn't already exist.""" | ||||||
|         if not self.history_command: |         if not self.history_command: | ||||||
|             self.history_command = self._get_history_command() |             self.history_command = self._get_history_command() | ||||||
|             self._invalidate_session_cache() |  | ||||||
|  |  | ||||||
|     def _get_bottom_bar(self) -> Callable[[], Any] | str | None: |     @property | ||||||
|  |     def bottom_bar(self) -> BottomBar | str | Callable[[], Any] | None: | ||||||
|  |         """Returns the bottom bar for the menu.""" | ||||||
|  |         return self._bottom_bar | ||||||
|  |  | ||||||
|  |     @bottom_bar.setter | ||||||
|  |     def bottom_bar(self, bottom_bar: BottomBar | str |  Callable[[], Any] | None) -> None: | ||||||
|  |         """Sets the bottom bar for the menu.""" | ||||||
|  |         if bottom_bar is None: | ||||||
|  |             self._bottom_bar = BottomBar(self.columns, self.key_bindings, key_validator=self.is_key_available) | ||||||
|  |         elif isinstance(bottom_bar, BottomBar): | ||||||
|  |             bottom_bar.key_validator = self.is_key_available | ||||||
|  |             bottom_bar.key_bindings = self.key_bindings | ||||||
|  |             self._bottom_bar = bottom_bar | ||||||
|  |         elif (isinstance(bottom_bar, str) or | ||||||
|  |             callable(bottom_bar)): | ||||||
|  |             self._bottom_bar = bottom_bar | ||||||
|  |         else: | ||||||
|  |             raise FalyxError("Bottom bar must be a string, callable, or BottomBar instance.") | ||||||
|  |         self._invalidate_session_cache() | ||||||
|  |  | ||||||
|  |     def _get_bottom_bar_render(self) -> Callable[[], Any] | str | None: | ||||||
|         """Returns the bottom bar for the menu.""" |         """Returns the bottom bar for the menu.""" | ||||||
|         if isinstance(self.bottom_bar, BottomBar) and self.bottom_bar._items: |         if isinstance(self.bottom_bar, BottomBar) and self.bottom_bar._items: | ||||||
|             return self.bottom_bar.render |             return self._bottom_bar.render | ||||||
|         elif callable(self.bottom_bar): |         elif callable(self._bottom_bar): | ||||||
|             return self.bottom_bar |             return self._bottom_bar | ||||||
|         elif isinstance(self.bottom_bar, str): |         elif isinstance(self._bottom_bar, str): | ||||||
|             return self.bottom_bar |             return self._bottom_bar | ||||||
|  |         elif self._bottom_bar is None: | ||||||
|  |             return None | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
|     @cached_property |     @cached_property | ||||||
| @@ -339,7 +354,7 @@ class Falyx: | |||||||
|             completer=self._get_completer(), |             completer=self._get_completer(), | ||||||
|             reserve_space_for_menu=1, |             reserve_space_for_menu=1, | ||||||
|             validator=self._get_validator(), |             validator=self._get_validator(), | ||||||
|             bottom_toolbar=self._get_bottom_bar(), |             bottom_toolbar=self._get_bottom_bar_render(), | ||||||
|             key_bindings=self.key_bindings, |             key_bindings=self.key_bindings, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -382,10 +397,24 @@ class Falyx: | |||||||
|             logger.debug(f"[Command '{key}'] after: {hook_names(command.hooks._hooks[HookType.AFTER])}") |             logger.debug(f"[Command '{key}'] after: {hook_names(command.hooks._hooks[HookType.AFTER])}") | ||||||
|             logger.debug(f"[Command '{key}'] on_teardown: {hook_names(command.hooks._hooks[HookType.ON_TEARDOWN])}") |             logger.debug(f"[Command '{key}'] on_teardown: {hook_names(command.hooks._hooks[HookType.ON_TEARDOWN])}") | ||||||
|  |  | ||||||
|  |     def is_key_available(self, key: str) -> bool: | ||||||
|  |         key = key.upper() | ||||||
|  |         toggles = self._bottom_bar.toggle_keys if isinstance(self._bottom_bar, BottomBar) else [] | ||||||
|  |  | ||||||
|  |         conflicts = ( | ||||||
|  |             key in self.commands, | ||||||
|  |             key == self.exit_command.key.upper(), | ||||||
|  |             self.history_command and key == self.history_command.key.upper(), | ||||||
|  |             self.help_command and key == self.help_command.key.upper(), | ||||||
|  |             key in toggles | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return not any(conflicts) | ||||||
|  |  | ||||||
|     def _validate_command_key(self, key: str) -> None: |     def _validate_command_key(self, key: str) -> None: | ||||||
|         """Validates the command key to ensure it is unique.""" |         """Validates the command key to ensure it is unique.""" | ||||||
|         key = key.upper() |         key = key.upper() | ||||||
|         toggles = self.bottom_bar.toggles if isinstance(self.bottom_bar, BottomBar) else [] |         toggles = self._bottom_bar.toggle_keys if isinstance(self._bottom_bar, BottomBar) else [] | ||||||
|         collisions = [] |         collisions = [] | ||||||
|  |  | ||||||
|         if key in self.commands: |         if key in self.commands: | ||||||
| @@ -423,7 +452,6 @@ class Falyx: | |||||||
|             confirm=confirm, |             confirm=confirm, | ||||||
|             confirm_message=confirm_message, |             confirm_message=confirm_message, | ||||||
|         ) |         ) | ||||||
|         self._invalidate_session_cache() |  | ||||||
|  |  | ||||||
|     def add_submenu(self, key: str, description: str, submenu: "Falyx", color: str = OneColors.CYAN) -> None: |     def add_submenu(self, key: str, description: str, submenu: "Falyx", color: str = OneColors.CYAN) -> None: | ||||||
|         """Adds a submenu to the menu.""" |         """Adds a submenu to the menu.""" | ||||||
| @@ -431,7 +459,6 @@ class Falyx: | |||||||
|             raise NotAFalyxError("submenu must be an instance of Falyx.") |             raise NotAFalyxError("submenu must be an instance of Falyx.") | ||||||
|         self._validate_command_key(key) |         self._validate_command_key(key) | ||||||
|         self.add_command(key, description, submenu.menu, color=color) |         self.add_command(key, description, submenu.menu, color=color) | ||||||
|         self._invalidate_session_cache() |  | ||||||
|  |  | ||||||
|     def add_commands(self, commands: list[dict]) -> None: |     def add_commands(self, commands: list[dict]) -> None: | ||||||
|         """Adds multiple commands to the menu.""" |         """Adds multiple commands to the menu.""" | ||||||
| @@ -511,7 +538,6 @@ class Falyx: | |||||||
|             command.hooks.register(HookType.ON_TEARDOWN, hook) |             command.hooks.register(HookType.ON_TEARDOWN, hook) | ||||||
|  |  | ||||||
|         self.commands[key] = command |         self.commands[key] = command | ||||||
|         self._invalidate_session_cache() |  | ||||||
|         return command |         return command | ||||||
|  |  | ||||||
|     def get_bottom_row(self) -> list[str]: |     def get_bottom_row(self) -> list[str]: | ||||||
| @@ -755,10 +781,10 @@ class Falyx: | |||||||
|         if self.exit_message: |         if self.exit_message: | ||||||
|             self.print_message(self.exit_message) |             self.print_message(self.exit_message) | ||||||
|  |  | ||||||
|     async def run(self, parsers: FalyxParsers | None = None) -> None: |     async def run(self) -> None: | ||||||
|         """Run Falyx CLI with structured subcommands.""" |         """Run Falyx CLI with structured subcommands.""" | ||||||
|         parsers = parsers or get_arg_parsers() |         if not self.cli_args: | ||||||
|         self.cli_args = parsers.root.parse_args() |             self.cli_args = get_arg_parsers().root.parse_args() | ||||||
|  |  | ||||||
|         if self.cli_args.verbose: |         if self.cli_args.verbose: | ||||||
|             logging.getLogger("falyx").setLevel(logging.DEBUG) |             logging.getLogger("falyx").setLevel(logging.DEBUG) | ||||||
|   | |||||||
| @@ -2,8 +2,9 @@ import asyncio | |||||||
| import logging | import logging | ||||||
| from rich.markdown import Markdown | from rich.markdown import Markdown | ||||||
|  |  | ||||||
| from falyx import Action, Falyx, HookType | from falyx import Action, Falyx | ||||||
| from falyx.hooks import log_before, log_success, log_error, log_after | from falyx.hook_manager import HookType | ||||||
|  | from falyx.debug import log_before, log_success, log_error, log_after | ||||||
| from falyx.themes.colors import OneColors | from falyx.themes.colors import OneColors | ||||||
| from falyx.utils import setup_logging | from falyx.utils import setup_logging | ||||||
|  |  | ||||||
| @@ -76,7 +77,7 @@ def main(): | |||||||
|         spinner=True, |         spinner=True, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     asyncio.run(menu.cli()) |     asyncio.run(menu.run()) | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
| @@ -84,4 +85,4 @@ if __name__ == "__main__": | |||||||
|     Entry point for the Falyx CLI demo application. |     Entry point for the Falyx CLI demo application. | ||||||
|     This function initializes the menu and runs it. |     This function initializes the menu and runs it. | ||||||
|     """ |     """ | ||||||
|     main() |     main() | ||||||
|   | |||||||
							
								
								
									
										72
									
								
								falyx/options_manager.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								falyx/options_manager.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | |||||||
|  | """options_manager.py""" | ||||||
|  |  | ||||||
|  | from argparse import Namespace | ||||||
|  | from collections import defaultdict | ||||||
|  | from typing import Any, Callable | ||||||
|  |  | ||||||
|  | from falyx.utils import logger | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class OptionsManager: | ||||||
|  |     def __init__(self, namespaces: list[tuple[str, Namespace]] = None) -> None: | ||||||
|  |         self.options = defaultdict(lambda: Namespace()) | ||||||
|  |         if namespaces: | ||||||
|  |             for namespace_name, namespace in namespaces: | ||||||
|  |                 self.from_namespace(namespace, namespace_name) | ||||||
|  |  | ||||||
|  |     def from_namespace( | ||||||
|  |         self, namespace: Namespace, namespace_name: str = "cli_args" | ||||||
|  |     ) -> None: | ||||||
|  |         self.options[namespace_name] = namespace | ||||||
|  |  | ||||||
|  |     def get( | ||||||
|  |         self, option_name: str, default: Any = None, namespace_name: str = "cli_args" | ||||||
|  |     ) -> Any: | ||||||
|  |         """Get the value of an option.""" | ||||||
|  |         return getattr(self.options[namespace_name], option_name, default) | ||||||
|  |  | ||||||
|  |     def set( | ||||||
|  |         self, option_name: str, value: Any, namespace_name: str = "cli_args" | ||||||
|  |     ) -> None: | ||||||
|  |         """Set the value of an option.""" | ||||||
|  |         setattr(self.options[namespace_name], option_name, value) | ||||||
|  |  | ||||||
|  |     def has_option(self, option_name: str, namespace_name: str = "cli_args") -> bool: | ||||||
|  |         """Check if an option exists in the namespace.""" | ||||||
|  |         return hasattr(self.options[namespace_name], option_name) | ||||||
|  |  | ||||||
|  |     def toggle(self, option_name: str, namespace_name: str = "cli_args") -> None: | ||||||
|  |         """Toggle a boolean option.""" | ||||||
|  |         current = self.get(option_name, namespace_name=namespace_name) | ||||||
|  |         if not isinstance(current, bool): | ||||||
|  |             raise TypeError( | ||||||
|  |                 f"Cannot toggle non-boolean option: '{option_name}' in '{namespace_name}'" | ||||||
|  |             ) | ||||||
|  |         self.set(option_name, not current, namespace_name=namespace_name) | ||||||
|  |         logger.debug(f"Toggled '{option_name}' in '{namespace_name}' to {not current}") | ||||||
|  |  | ||||||
|  |     def get_value_getter( | ||||||
|  |         self, option_name: str, namespace_name: str = "cli_args" | ||||||
|  |     ) -> Callable[[], Any]: | ||||||
|  |         """Get the value of an option as a getter function.""" | ||||||
|  |  | ||||||
|  |         def _getter() -> Any: | ||||||
|  |             return self.get(option_name, namespace_name=namespace_name) | ||||||
|  |  | ||||||
|  |         return _getter | ||||||
|  |  | ||||||
|  |     def get_toggle_function( | ||||||
|  |         self, option_name: str, namespace_name: str = "cli_args" | ||||||
|  |     ) -> Callable[[], None]: | ||||||
|  |         """Get the toggle function for a boolean option.""" | ||||||
|  |  | ||||||
|  |         def _toggle() -> None: | ||||||
|  |             self.toggle(option_name, namespace_name=namespace_name) | ||||||
|  |  | ||||||
|  |         return _toggle | ||||||
|  |  | ||||||
|  |     def get_namespace_dict(self, namespace_name: str) -> Namespace: | ||||||
|  |         """Return all options in a namespace as a dictionary.""" | ||||||
|  |         if namespace_name not in self.options: | ||||||
|  |             raise ValueError(f"Namespace '{namespace_name}' not found.") | ||||||
|  |         return vars(self.options[namespace_name]) | ||||||
| @@ -1,8 +1,9 @@ | |||||||
| """parsers.py | """parsers.py | ||||||
| This module contains the argument parsers used for the Falyx CLI. | This module contains the argument parsers used for the Falyx CLI. | ||||||
| """ | """ | ||||||
|  | from argparse import ArgumentParser, HelpFormatter, Namespace | ||||||
| from dataclasses import asdict, dataclass | from dataclasses import asdict, dataclass | ||||||
| from argparse import ArgumentParser | from typing import Any, Sequence | ||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass | @dataclass | ||||||
| @@ -15,6 +16,10 @@ class FalyxParsers: | |||||||
|     list: ArgumentParser |     list: ArgumentParser | ||||||
|     version: ArgumentParser |     version: ArgumentParser | ||||||
|  |  | ||||||
|  |     def parse_args(self, args: Sequence[str] | None = None) -> Namespace: | ||||||
|  |         """Parse the command line arguments.""" | ||||||
|  |         return self.root.parse_args(args) | ||||||
|  |  | ||||||
|     def as_dict(self) -> dict[str, ArgumentParser]: |     def as_dict(self) -> dict[str, ArgumentParser]: | ||||||
|         """Convert the FalyxParsers instance to a dictionary.""" |         """Convert the FalyxParsers instance to a dictionary.""" | ||||||
|         return asdict(self) |         return asdict(self) | ||||||
| @@ -24,9 +29,37 @@ class FalyxParsers: | |||||||
|         return self.as_dict().get(name) |         return self.as_dict().get(name) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_arg_parsers() -> FalyxParsers: | def get_arg_parsers( | ||||||
|  |         prog: str |None = "falyx", | ||||||
|  |         usage: str | None = None, | ||||||
|  |         description: str | None = "Falyx CLI - Run structured async command workflows.", | ||||||
|  |         epilog: str | None = None, | ||||||
|  |         parents: Sequence[ArgumentParser] = [], | ||||||
|  |         formatter_class: HelpFormatter = HelpFormatter, | ||||||
|  |         prefix_chars: str = "-", | ||||||
|  |         fromfile_prefix_chars: str | None = None, | ||||||
|  |         argument_default: Any = None, | ||||||
|  |         conflict_handler: str = "error", | ||||||
|  |         add_help: bool = True, | ||||||
|  |         allow_abbrev: bool = True, | ||||||
|  |         exit_on_error: bool = True, | ||||||
|  |     ) -> FalyxParsers: | ||||||
|     """Returns the argument parser for the CLI.""" |     """Returns the argument parser for the CLI.""" | ||||||
|     parser = ArgumentParser(prog="falyx", description="Falyx CLI - Run structured async command workflows.") |     parser = ArgumentParser( | ||||||
|  |         prog=prog, | ||||||
|  |         usage=usage, | ||||||
|  |         description=description, | ||||||
|  |         epilog=epilog, | ||||||
|  |         parents=parents, | ||||||
|  |         formatter_class=formatter_class, | ||||||
|  |         prefix_chars=prefix_chars, | ||||||
|  |         fromfile_prefix_chars=fromfile_prefix_chars, | ||||||
|  |         argument_default=argument_default, | ||||||
|  |         conflict_handler=conflict_handler, | ||||||
|  |         add_help=add_help, | ||||||
|  |         allow_abbrev=allow_abbrev, | ||||||
|  |         exit_on_error=exit_on_error, | ||||||
|  |     ) | ||||||
|     parser.add_argument("-v", "--verbose", action="store_true", help="Enable debug logging for Falyx.") |     parser.add_argument("-v", "--verbose", action="store_true", help="Enable debug logging for Falyx.") | ||||||
|     parser.add_argument("--debug-hooks", action="store_true", help="Enable default lifecycle debug logging") |     parser.add_argument("--debug-hooks", action="store_true", help="Enable default lifecycle debug logging") | ||||||
|     parser.add_argument("--version", action="store_true", help="Show Falyx version") |     parser.add_argument("--version", action="store_true", help="Show Falyx version") | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| __version__ = "0.1.0" | __version__ = "0.1.5" | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| [tool.poetry] | [tool.poetry] | ||||||
| name = "falyx" | name = "falyx" | ||||||
| version = "0.1.4" | version = "0.1.5" | ||||||
| description = "Reliable and introspectable async CLI action framework." | description = "Reliable and introspectable async CLI action framework." | ||||||
| authors = ["Roland Thomas Jr <roland@rtj.dev>"] | authors = ["Roland Thomas Jr <roland@rtj.dev>"] | ||||||
| license = "MIT" | license = "MIT" | ||||||
| @@ -12,12 +12,12 @@ python = ">=3.10" | |||||||
| prompt_toolkit = "^3.0" | prompt_toolkit = "^3.0" | ||||||
| rich = "^13.0" | rich = "^13.0" | ||||||
| pydantic = "^2.0" | pydantic = "^2.0" | ||||||
|  | python-json-logger = "^3.3.0" | ||||||
|  |  | ||||||
| [tool.poetry.group.dev.dependencies] | [tool.poetry.group.dev.dependencies] | ||||||
| pytest = "^7.0" | pytest = "^7.0" | ||||||
| pytest-asyncio = "^0.20" | pytest-asyncio = "^0.20" | ||||||
| ruff = "^0.3" | ruff = "^0.3" | ||||||
| python-json-logger = "^3.3.0" |  | ||||||
|  |  | ||||||
| [tool.poetry.scripts] | [tool.poetry.scripts] | ||||||
| falyx = "falyx.cli.main:main" | falyx = "falyx.cli.main:main" | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								setup.py
									
									
									
									
									
								
							| @@ -1,18 +0,0 @@ | |||||||
| from setuptools import setup, find_packages |  | ||||||
|  |  | ||||||
| setup( |  | ||||||
|     name="falyx", |  | ||||||
|     version="0.0.1", |  | ||||||
|     description="Reserved package name for future CLI framework.", |  | ||||||
|     long_description=open("README.md").read(), |  | ||||||
|     long_description_content_type="text/markdown", |  | ||||||
|     author="Roland Thomas Jr", |  | ||||||
|     author_email="roland@rtj.dev", |  | ||||||
|     packages=find_packages(), |  | ||||||
|     python_requires=">=3.10", |  | ||||||
|     classifiers=[ |  | ||||||
|         "Programming Language :: Python :: 3", |  | ||||||
|         "License :: OSI Approved :: MIT License", |  | ||||||
|         "Development Status :: 1 - Planning", |  | ||||||
|     ], |  | ||||||
| ) |  | ||||||
		Reference in New Issue
	
	Block a user