diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0cacd88..bfec651 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,6 +18,6 @@ repos: hooks: - id: sync-version name: Sync version from pyproject.toml - entry: poetry run sync-version + entry: python scripts/sync_version.py language: system files: ^pyproject\.toml$ diff --git a/falyx/__main__.py b/falyx/__main__.py index ddd98d4..b106d5f 100644 --- a/falyx/__main__.py +++ b/falyx/__main__.py @@ -46,7 +46,6 @@ def get_falyx_parsers() -> FalyxParsers: falyx_parsers.subparsers.add_parser( "init-global", help="Set up ~/.config/falyx with example tasks" ) - return falyx_parsers diff --git a/falyx/init.py b/falyx/init.py index 6b8857c..ba30288 100644 --- a/falyx/init.py +++ b/falyx/init.py @@ -27,10 +27,20 @@ TEMPLATE_CONFIG = """\ spinner: true """ +GLOBAL_TEMPLATE_TASKS = """\ +async def cleanup(): + print("🧹 Cleaning temp files...") +""" + +GLOBAL_CONFIG = """\ +async def cleanup(): + print("🧹 Cleaning temp files...") +""" + console = Console(color_system="auto") -def init_project(name: str = "."): +def init_project(name: str = ".") -> None: target = Path(name).resolve() target.mkdir(parents=True, exist_ok=True) @@ -39,7 +49,7 @@ def init_project(name: str = "."): if tasks_path.exists() or config_path.exists(): console.print(f"⚠️ Project already initialized at {target}") - return + return None tasks_path.write_text(TEMPLATE_TASKS) config_path.write_text(TEMPLATE_CONFIG) @@ -47,7 +57,7 @@ def init_project(name: str = "."): print(f"✅ Initialized Falyx project in {target}") -def init_global(): +def init_global() -> None: config_dir = Path.home() / ".config" / "falyx" config_dir.mkdir(parents=True, exist_ok=True) @@ -56,22 +66,9 @@ def init_global(): if tasks_path.exists() or config_path.exists(): console.print("⚠️ Global Falyx config already exists at ~/.config/falyx") - return + return None - tasks_path.write_text( - """\ -async def cleanup(): - print("🧹 Cleaning temp files...") -""" - ) - - config_path.write_text( - """\ -- key: C - description: Cleanup temp files - action: tasks.cleanup - aliases: [clean, cleanup] -""" - ) + tasks_path.write_text(GLOBAL_TEMPLATE_TASKS) + config_path.write_text(GLOBAL_CONFIG) console.print("✅ Initialized global Falyx config at ~/.config/falyx") diff --git a/falyx/utils.py b/falyx/utils.py index 7c57493..7bdc3c4 100644 --- a/falyx/utils.py +++ b/falyx/utils.py @@ -19,6 +19,7 @@ from prompt_toolkit.formatted_text import ( from rich.logging import RichHandler from falyx.themes.colors import OneColors +from falyx.validators import yes_no_validator logger = logging.getLogger("falyx") @@ -69,18 +70,20 @@ def chunks(iterator, size): yield chunk -async def confirm_async(message: AnyFormattedText = "Are you sure?") -> bool: - session: PromptSession = PromptSession() - while True: - merged_message: AnyFormattedText = merge_formatted_text( - [message, FormattedText([(OneColors.LIGHT_YELLOW_b, " [Y/n] ")])] - ) - answer: str = (await session.prompt_async(merged_message)).strip().lower() - if answer in ("y", "yes"): - return True - if answer in ("n", "no", ""): - return False - print("Please enter y or n.") +async def confirm_async( + message: AnyFormattedText = "Are you sure?", + prefix: AnyFormattedText = FormattedText([(OneColors.CYAN, "❓ ")]), + suffix: AnyFormattedText = FormattedText([(OneColors.LIGHT_YELLOW_b, " [Y/n] > ")]), + session: PromptSession | None = None, +) -> bool: + """Prompt the user with a yes/no async confirmation and return True for 'Y'.""" + session = session or PromptSession() + merged_message: AnyFormattedText = merge_formatted_text([prefix, message, suffix]) + answer = await session.prompt_async( + merged_message, + validator=yes_no_validator(), + ) + return True if answer.upper() == "Y" else False class CaseInsensitiveDict(dict): diff --git a/falyx/validators.py b/falyx/validators.py index 68eaf96..e79531f 100644 --- a/falyx/validators.py +++ b/falyx/validators.py @@ -16,7 +16,10 @@ def int_range_validator(minimum: int, maximum: int) -> Validator: except ValueError: return False - return Validator.from_callable(validate, error_message="Invalid input.") + return Validator.from_callable( + validate, + error_message=f"Invalid input. Enter a number between {minimum} and {maximum}.", + ) def key_validator(keys: Sequence[str] | KeysView[str]) -> Validator: @@ -27,4 +30,17 @@ def key_validator(keys: Sequence[str] | KeysView[str]) -> Validator: return False return True - return Validator.from_callable(validate, error_message="Invalid input.") + return Validator.from_callable( + validate, error_message=f"Invalid input. Available keys: {', '.join(keys)}." + ) + + +def yes_no_validator() -> Validator: + """Validator for yes/no inputs.""" + + def validate(input: str) -> bool: + if input.upper() not in ["Y", "N"]: + return False + return True + + return Validator.from_callable(validate, error_message="Enter 'Y' or 'n'.") diff --git a/falyx/version.py b/falyx/version.py index fb69db9..f3b4574 100644 --- a/falyx/version.py +++ b/falyx/version.py @@ -1 +1 @@ -__version__ = "0.1.14" +__version__ = "0.1.15" diff --git a/pyproject.toml b/pyproject.toml index 26d487c..038bfa4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "falyx" -version = "0.1.14" +version = "0.1.15" description = "Reliable and introspectable async CLI action framework." authors = ["Roland Thomas Jr "] license = "MIT" @@ -13,6 +13,8 @@ prompt_toolkit = "^3.0" rich = "^13.0" pydantic = "^2.0" python-json-logger = "^3.3.0" +toml = "^0.10" +pyyaml = "^6.0" [tool.poetry.group.dev.dependencies] pytest = "^7.0" @@ -26,7 +28,6 @@ pytest-cov = "^4.0" [tool.poetry.scripts] falyx = "falyx.__main__:main" -sync-version = "scripts.sync_version:main" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/test_main.py b/tests/test_main.py index 58485d9..d369159 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -35,8 +35,8 @@ def test_bootstrap_no_config(): sys_path_before = list(sys.path) bootstrap_path = bootstrap() assert bootstrap_path is None - assert str(Path.cwd()) in sys.path sys.path = sys_path_before + assert str(Path.cwd()) not in sys.path def test_bootstrap_with_global_config():