diff --git a/src/caelestia/subcommands/install.py b/src/caelestia/subcommands/install.py index ea7f7f3b..e13ebecc 100644 --- a/src/caelestia/subcommands/install.py +++ b/src/caelestia/subcommands/install.py @@ -10,7 +10,7 @@ from caelestia.utils.dots.manifest import ComponentError, Manifest, ManifestErro from caelestia.utils.dots.packages import DEFAULT_AUR_HELPER, PackageInstaller from caelestia.utils.dots.source import DotsSource, SourceError from caelestia.utils.dots.state import DotsState -from caelestia.utils.io import confirm, disable_input, fatal, info, log, pause, warn +from caelestia.utils.io import PROMPT_COLOUR, confirm, disable_input, fatal, format_msg, info, log, pause, prompt, warn from caelestia.utils.paths import ( config_backup_dir, config_dir, @@ -102,12 +102,14 @@ class Command: except SourceError as e: fatal(e) + enable = _parse_list_arg(self.args.enable_components) + disable = _parse_list_arg(self.args.disable_components) try: manifest = source.manifest_at(tip) - manifest.resolve_components( - enable=_parse_list_arg(self.args.enable_components), - disable=_parse_list_arg(self.args.disable_components), - ) + manifest.resolve_components(enable=enable, disable=disable) + + if enable is None and disable is None: + self.prompt_optional_components(manifest) except (SourceError, ManifestError, ComponentError) as e: fatal(e) @@ -116,6 +118,47 @@ class Command: return source, tip, manifest + def prompt_optional_components(self, manifest: Manifest) -> None: + comp_arr = manifest.disabled_components + if not comp_arr: + return + + print(format_msg(PROMPT_COLOUR, "Components to enable?")) + for i, comp in enumerate(comp_arr): + print(format_msg(PROMPT_COLOUR, f" [{i + 1}] {comp}")) + print(format_msg(PROMPT_COLOUR, "[A]ll or (1 2 3, 1-3, ^4)")) + ans = prompt("", end="").lower().strip() + + def _valid_v(v: str) -> int: + try: + i_v = int(v, base=10) - 1 # -1 to translate to 0 index + except ValueError: + fatal(f'Invalid input. Given value "{v}" must be an integer.') + if i_v < 0 or i_v >= len(comp_arr): + fatal(f'Invalid input. Given value "{v}" must be between 1 and {len(comp_arr)} inclusive.') + return i_v + + if ans in ("a", "all"): + manifest.resolve_components(enable=list(manifest.components)) + elif ans: + enabled: list[str] = [] + toks = ans.split() + for tok in toks: + fr, sep, to = tok.partition("-") + if sep: + fr = _valid_v(fr) + to = _valid_v(to) + if fr > to: + fatal(f'Invalid input. Given range "{tok}" must be lo-hi.') + enabled += comp_arr[fr : to + 1] + elif tok.startswith("^"): + t = _valid_v(tok[1:]) + enabled += comp_arr[:t] + comp_arr[t + 1 :] + else: + t = _valid_v(tok) + enabled.append(comp_arr[t]) + manifest.resolve_components(enable=list(set(enabled))) + def deploy_configs(self, source: DotsSource, manifest: Manifest) -> None: log("Installing configs...") deployer = Deployer() diff --git a/src/caelestia/utils/dots/manifest.py b/src/caelestia/utils/dots/manifest.py index 6488078d..01fbd0a3 100644 --- a/src/caelestia/utils/dots/manifest.py +++ b/src/caelestia/utils/dots/manifest.py @@ -68,7 +68,6 @@ class ManifestComponent: @dataclass class _ManifestData: - resolved_comps: bool = False enabled_comps: list[str] = field(default_factory=list) disabled_comps: list[str] = field(default_factory=list) @@ -121,9 +120,6 @@ class Manifest: ) -> None: """Resolves enabled/disabled components. This MUST be called before calling any other method.""" - if self._data.resolved_comps: - return - enable_set = set(enable or []) disable_set = set(disable or []) known = set(self.components) @@ -140,12 +136,13 @@ class Manifest: enabled |= enable_set enabled -= disable_set + self._data.enabled_comps.clear() + self._data.disabled_comps.clear() for name in self.components: if name in enabled: self._data.enabled_comps.append(name) else: self._data.disabled_comps.append(name) - self._data.resolved_comps = True def enabled_entries(self) -> list[ManifestEntry]: """The entries of every enabled component.""" diff --git a/src/caelestia/utils/io.py b/src/caelestia/utils/io.py index d4cc06d8..c9389bd9 100644 --- a/src/caelestia/utils/io.py +++ b/src/caelestia/utils/io.py @@ -1,6 +1,12 @@ import sys from typing import Never +LOG_COLOUR: int = 2 +INFO_COLOUR: int = 0 +PROMPT_COLOUR: int = 36 +WARNING_COLOUR: int = 33 +ERROR_COLOUR: int = 31 + _disable_input: bool = False @@ -25,28 +31,28 @@ def log_exception(func): return wrapper -def _format_msg(colour: int, msg: str) -> str: +def format_msg(colour: int, msg: str) -> str: return f"\033[{colour}m:: {msg}\033[0m" def log(msg: str) -> None: - print(_format_msg(2, msg)) + print(format_msg(LOG_COLOUR, msg)) def info(msg: str) -> None: - print(_format_msg(0, msg)) + print(format_msg(INFO_COLOUR, msg)) def warn(msg: str) -> None: - print(_format_msg(33, f"Warning: {msg}")) + print(format_msg(WARNING_COLOUR, f"Warning: {msg}")) def error(err: str | Exception) -> None: - print(_format_msg(31, f"Error: {err}"), file=sys.stderr) + print(format_msg(ERROR_COLOUR, f"Error: {err}"), file=sys.stderr) def fatal(err: str | Exception) -> Never: - print(_format_msg(31, f"Fatal: {err}"), file=sys.stderr) + print(format_msg(ERROR_COLOUR, f"Fatal: {err}"), file=sys.stderr) sys.exit(1) @@ -62,8 +68,8 @@ def _input(prompt: str) -> str: raise KeyboardInterrupt() -def prompt(msg: str) -> str: - return _input(_format_msg(36, msg) + " ") +def prompt(msg: str, end: str = " ") -> str: + return _input(format_msg(PROMPT_COLOUR, msg) + end) def confirm(msg: str, default: bool = True) -> bool: