diff options
Diffstat (limited to 'dots_manager')
-rwxr-xr-x | dots_manager/.main.py | 328 | ||||
-rw-r--r-- | dots_manager/args.py | 2 | ||||
-rw-r--r-- | dots_manager/cli.py | 14 | ||||
-rw-r--r-- | dots_manager/env.py | 8 | ||||
-rw-r--r-- | dots_manager/parallel.py | 4 | ||||
-rw-r--r-- | dots_manager/setup.py | 19 | ||||
-rw-r--r-- | dots_manager/stow.py | 8 | ||||
-rw-r--r-- | dots_manager/template.py | 8 |
8 files changed, 20 insertions, 371 deletions
diff --git a/dots_manager/.main.py b/dots_manager/.main.py deleted file mode 100755 index c430320..0000000 --- a/dots_manager/.main.py +++ /dev/null @@ -1,328 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -sys.dont_write_bytecode = True - -from dataclasses import dataclass -from pathlib import Path -from typing import ( - Dict, - Generator, - List, - Optional, - Any, - Literal, - ParamSpec, - Tuple, - Callable, - TypeVar, -) -import argparse -import json -import logging -import os -import subprocess -import sys -import shutil -from concurrent.futures import Executor, ThreadPoolExecutor, as_completed -from functools import reduce -import jinja2 -from kawaii_logger import setup_logger - -R = TypeVar("R") -T = TypeVar("T") -P = ParamSpec("P") - - -@dataclass(frozen=True) -class Config: - default_target_dir: Path = Path.home() - default_source_dir: Path = Path(__file__).parent / "dots" - default_compiled_dir: Path = Path("./.compiled_dotfiles/") - - script_dir: Path = Path("home/scripts") - contexts: Path = Path("contexts.json") - - template_extension: str = ".j2" - max_workers: int = (os.cpu_count() or 1) * 2 - - -@dataclass(frozen=True) -class Environment: - platform: str - system_name: str - context: Dict[str, Any] - logger: logging.Logger - - -def main() -> None: - args = parse_arguments() - env = initialize_environment(args) - if not env: - sys.exit(1) - if not any((args.compile, args.stow, args.clean)): - env.logger.error("you gotta specify at least one action nya~ (⁎⁍̴̆‾⁍̴̆⁎)") - sys.exit(1) - - if args.compile and not compile_dotfiles(Path(args.source), Path(args.comp), env): - env.logger.error("uh oh! failed to compile dotfiles (ɐ•゚́•̀ɐ)") - sys.exit(1) - - if args.stow and not apply_stow_operation_to_packages( - Path(args.comp), Path(args.target), "--no-folding", env - ): - env.logger.error("failed to stow dotfiles... nyaaa (╥゚╥)") - sys.exit(1) - - if args.clean and not apply_stow_operation_to_packages( - Path(args.comp), Path(args.target), "-D", env - ): - env.logger.error("couldn’t clean dotfiles... sobs (ɐ•゚́•̀ɐ)") - sys.exit(1) - - env.logger.info("yay~ all done!! ₕᵒ. .ᵒₕ♡") - sys.exit(0) - - -def initialize_environment(args) -> Environment: - logger = setup_logger(args.verbose) - - scripts = args.source / Config.script_dir - platform = run_shell_command([str(scripts / "platform.sh")], logger) - if not platform: - raise ValueError("failed to determine platform... ") - - os.environ["PLATFORM"] = platform - system_name = run_shell_command([str(scripts / "system_name.sh")], logger) - if not system_name: - raise ValueError("failed to determine system name... ") - - context = load_context(platform, system_name, Config.contexts, logger) - return Environment(platform, system_name, context, logger) - - -def render_template_to( - template: Path, destination: Path, env: Environment -) -> Optional[Path]: - env.logger.debug(f"rendering template {template} to {destination} ✧*ฺ") - if not is_template(template): - env.logger.error(f"template {template} is nyot a vawid tempwate D:") - return None - - env.logger.debug(f"rendering template {template} ✧*ฺ") - jinja_env = jinja2.Environment( - loader=jinja2.BaseLoader, - undefined=jinja2.StrictUndefined, - trim_blocks=True, - lstrip_blocks=True, - ) - rendered = render(template, jinja_env, env) - if not rendered: - env.logger.error(f"failed to render template {template}") - return None - - env.logger.debug(f"rendered {template} successfully~ :D ✧*ฺ") - destination.write_text(rendered) - return destination - - -def compile_dotfiles(source: Path, output: Path, env: Environment) -> bool: - env.logger.info(f"compiling {source} to {output} ✨") - output.mkdir(exist_ok=True, parents=True) - shutil.copytree(source, output, dirs_exist_ok=True) - env.logger.debug(f"copied {source} to {output} ₰˜⋉♡") - - tasks = parallelize( - lambda template: is_some( - render_template_to, - template, - template.with_suffix(""), - env, - ) - and (template.unlink() is None), - find_templates_under(output), - env, - ) - return all(tasks) - - -def render( - source: Path, jinja_env: jinja2.Environment, env: Environment -) -> Optional[str]: - try: - env.logger.debug(f"rendering template {source} ✿.。.:・") - return jinja_env.from_string(source.read_text()).render(**env.context) - except Exception as e: - env.logger.error(f"couldn’t render {source}: {e} (;⌓̀_⌓́)") - return None - - -def apply_stow_operation_to_packages( - packages: Path, - target: Path, - stow_op: Literal["-D", "--no-folding"], - env: Environment, -) -> bool: - if not run_shell_command(["stow", "--version"], env.logger): - env.logger.error("stow not installed D:") - return False - package_command = ["stow", "-d", str(packages), "-t", str(target), stow_op] - - _packages = list_stowable_packages(packages) - env.logger.debug(f"found dotfile packages: {_packages}") - commands = [package_command + [package.name] for package in _packages] - - env.logger.debug(f"stowing packages: {commands}") - results = parallelize( - lambda command: is_some(run_shell_command, command, env.logger), - commands, - env, - ) - return len(commands) == sum(1 if x else 0 for x in results) - - -def parallelize( - worker: Callable[[T], R], - items: List[T], - env: Environment, - executor: Optional[Executor] = None, -) -> List[R]: - if executor is None: - executor = ThreadPoolExecutor(max_workers=Config.max_workers) - with executor as executor: - futures = [ - executor.submit( - worker, - item, - ) - for item in items - ] - env.logger.info(f"submitted {len(futures)} tasks to executor ₰˜.༄") - return [f.result() for f in as_completed(futures)] - - -def run_shell_command(cmd: List[str], logger: logging.Logger) -> Optional[str]: - logger.debug(f"running command: {cmd}") - try: - return subprocess.run( - cmd, capture_output=True, text=True, check=True - ).stdout.strip() - except subprocess.CalledProcessError as e: - logger.error(f"command failed: {cmd}, {e.stderr}") - return None - - -def is_template(path: Path) -> bool: - return path.suffix == Config.template_extension - - -def find_templates_under(destination: Path) -> List[Path]: - return [ - t - for t in destination.glob("**/*" + Config.template_extension) - if t.is_file() and is_template(t) - ] - - -def list_stowable_packages(packages: Path) -> List[Path]: - denylist = [".", "__"] - return [ - d - for d in packages.iterdir() - if d.is_dir() and all(y not in d.name for y in denylist) - ] - - -def load_context( - platform: str, system_name: str, context_file: Path, logger: logging.Logger -) -> Dict[str, Any]: - try: - logger.info(f"reading context file: {context_file} ✧*:。゚✧") - with open(context_file) as f: - contexts = json.load(f) - - global_config = contexts.get("_global", {}) - platform_defaults = contexts.get(platform, {}).get("_default", {}) - defaults = merge_dicts(global_config, platform_defaults) - - system_config = contexts.get(platform, {}).get(system_name, {}) - if not system_config: - logger.warning( - f"couldn’t find system-specific config for {platform}.{system_name} (ɐ•゚́•̀ɐ)" - ) - - return { - "platform": platform, - "system_name": system_name, - **merge_dicts(defaults, system_config), - } - except (FileNotFoundError, json.JSONDecodeError) as e: - logger.error(f"error loading context: {e} ⋆ฺ°☁。⋆ฺ °★ °。") - return {} - - -def is_some( - callable: Callable[P, Optional[T]], *args: P.args, **kwargs: P.kwargs -) -> Tuple[bool, Optional[T]]: - result = callable(*args, **kwargs) - return result is not None, result - - -def merge_dicts(*dicts: Dict[str, Any]) -> Dict[str, Any]: - def merge(a: Dict[str, Any], b: Dict[str, Any]) -> Dict[str, Any]: - out = dict(a) - for k, v in b.items(): - if k in out and isinstance(out[k], dict) and isinstance(v, dict): - out[k] = merge(out[k], v) - else: - out[k] = v - return out - - return reduce(merge, dicts, {}) - - -def parse_arguments() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="cute dotfiles manager ✧˖°") - - parser.add_argument( - "--compile", action="store_true", help="action: compile dotfiles" - ) - parser.add_argument( - "--source", - type=Path, - default=Config.default_source_dir, - help=f"directory with stowable dotfiles. default '{Config.default_source_dir}'. :)", - ) - parser.add_argument( - "--comp", - type=Path, - default=Config.default_compiled_dir, - help=f"compiled template output dir. default '{Config.default_compiled_dir}'. :3", - ) - - parser.add_argument( - "--stow", action="store_true", help="action: stow compiled dotfiles" - ) - parser.add_argument( - "--clean", action="store_true", help="action: clean stowed dotfiles" - ) - parser.add_argument( - "--target", - type=Path, - default=Config.default_target_dir, - help=f"stow target directory. default '{Config.default_target_dir}'. -.-", - ) - - parser.add_argument( - "--verbose", - "-v", - action="store_true", - help="enable verbose logging. default False. :D", - default=False, - ) - return parser.parse_args() - - -if __name__ == "__main__": - main() diff --git a/dots_manager/args.py b/dots_manager/args.py index 232c402..40fc42f 100644 --- a/dots_manager/args.py +++ b/dots_manager/args.py @@ -1,6 +1,6 @@ import argparse from pathlib import Path -from .config import Config +from dots_manager.config import Config def parse_arguments() -> argparse.Namespace: diff --git a/dots_manager/cli.py b/dots_manager/cli.py index 4df416f..f152a96 100644 --- a/dots_manager/cli.py +++ b/dots_manager/cli.py @@ -1,16 +1,12 @@ -from .args import parse_arguments -from .env import create_environment -from .template import compile_dotfiles -from .stow import apply_stow_operation_to_packages - -import sys - -sys.dont_write_bytecode = True +from dots_manager.args import parse_arguments +from dots_manager.env import initialize_environment +from dots_manager.template import compile_dotfiles +from dots_manager.stow import apply_stow_operation_to_packages def main(): args = parse_arguments() - env = create_environment(args) + env = initialize_environment(args) if args.clean: apply_stow_operation_to_packages(args.comp, args.target, "-D", env) diff --git a/dots_manager/env.py b/dots_manager/env.py index 514624b..4d89c9e 100644 --- a/dots_manager/env.py +++ b/dots_manager/env.py @@ -4,10 +4,10 @@ import logging from pathlib import Path from dataclasses import dataclass from typing import Dict, Any -from .config import Config -from .shell import run_shell_command -from .utils import merge_dicts -from .kawaii_logger import setup_logger +from dots_manager.config import Config +from dots_manager.shell import run_shell_command +from dots_manager.utils import merge_dicts +from dots_manager.kawaii_logger import setup_logger @dataclass(frozen=True) diff --git a/dots_manager/parallel.py b/dots_manager/parallel.py index e50c5a8..8c85660 100644 --- a/dots_manager/parallel.py +++ b/dots_manager/parallel.py @@ -1,7 +1,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed from typing import Callable, List, Optional, TypeVar -from .config import Config -from .env import Environment +from dots_manager.config import Config +from dots_manager.env import Environment T = TypeVar("T") R = TypeVar("R") diff --git a/dots_manager/setup.py b/dots_manager/setup.py deleted file mode 100644 index d04a0e0..0000000 --- a/dots_manager/setup.py +++ /dev/null @@ -1,19 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name="dotfiles", - version="0.1.0", - packages=find_packages(), - install_requires=[ - "jinja2", - ], - entry_points={"console_scripts": ["dotfiles-manager=dotfiles_manager.cli:main"]}, - python_requires=">=3.8", - include_package_data=True, - description="A cute dotfiles manager with Jinja2 templating and GNU Stow integration", - author="Your Name", - classifiers=[ - "Programming Language :: Python :: 3", - "Operating System :: OS Independent", - ], -) diff --git a/dots_manager/stow.py b/dots_manager/stow.py index 52a9e36..b78e2fc 100644 --- a/dots_manager/stow.py +++ b/dots_manager/stow.py @@ -1,9 +1,9 @@ from pathlib import Path from typing import Literal -from .env import Environment -from .shell import run_shell_command -from .parallel import parallelize -from .utils import is_some +from dots_manager.env import Environment +from dots_manager.shell import run_shell_command +from dots_manager.parallel import parallelize +from dots_manager.utils import is_some def list_stowable_packages(packages: Path) -> list[Path]: diff --git a/dots_manager/template.py b/dots_manager/template.py index a88158a..74d48ae 100644 --- a/dots_manager/template.py +++ b/dots_manager/template.py @@ -2,10 +2,10 @@ from pathlib import Path from typing import Optional import shutil import jinja2 -from .config import Config -from .parallel import parallelize -from .utils import is_some -from .env import Environment +from dots_manager.config import Config +from dots_manager.parallel import parallelize +from dots_manager.utils import is_some +from dots_manager.env import Environment def is_template(path: Path) -> bool: |