diff options
Diffstat (limited to 'main.py')
-rwxr-xr-x | main.py | 255 |
1 files changed, 0 insertions, 255 deletions
diff --git a/main.py b/main.py deleted file mode 100755 index 34dbc04..0000000 --- a/main.py +++ /dev/null @@ -1,255 +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, List, Optional, Any -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 - - -@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") - 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 (args.compile or args.stow or args.clean): - env.logger.error("you gotta specify at least one action nya~ (⁎⁍̴̆‾⁍̴̆⁎)") - sys.exit(1) - - if args.compile: - env.logger.info(f"compiling {args.source} to {args.comp} ✨✧˖°") - if 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: - if not stow_dotfiles(Path(args.comp), Path(args.target), env, clean=False): - env.logger.error("failed to stow dotfiles... nyaaa (╥゚╥)") - sys.exit(1) - - if args.clean: - env.logger.info(f"cleaning dotfiles from {args.target} (⌟‾╥ ‾╥)°") - if not stow_dotfiles(Path(args.comp), Path(args.target), env, clean=True): - 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")]) - os.environ["PLATFORM"] = platform - system_name = run_shell_command([str(scripts / "system_name.sh")]) - - context = load_context(platform, system_name, Config.contexts, logger) - return Environment(platform, system_name, context, logger) - - -def copy_with_templates_rendered( - executor: Executor, source: Path, destination: Path, env: Environment -) -> bool: - shutil.copytree(source, destination, dirs_exist_ok=True) - env.logger.debug(f"copied {source} to {destination} ₰˜⋉♡") - - templates = [t for t in destination.glob("**/*.j2") if t.is_file()] - if not templates: - env.logger.debug(f"no templates to render in {source} (•ᴗ•)⁎") - return True - - for template in templates: - env.logger.debug(f"submitting template render for {template} ₰˜൨൨") - executor.submit(replace_with_rendered_template, template, env) - - return True - - -def replace_with_rendered_template(template: Path, env: Environment) -> bool: - 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) - with open(template, "w") as t: - t.write(rendered) - env.logger.debug(f"removing .j2 suffix from {template.absolute()} ✧˖°") - template.rename(template.with_suffix("")) - return True - - -def compile_dotfiles(source_dir: Path, target_dir: Path, env: Environment) -> bool: - target_dir.mkdir(exist_ok=True, parents=True) - - with ThreadPoolExecutor(max_workers=Config.max_workers) as executor: - dotfile_dirs = list_dotfile_stows(source_dir) - env.logger.debug(f"found dotfile dirs: {dotfile_dirs}") - - futures = [ - executor.submit( - copy_with_templates_rendered, - executor, - d, - target_dir / d.name, - env, - ) - for d in dotfile_dirs - ] - env.logger.info(f"submitted {len(futures)} tasks to executor ₰˜.༄") - - return sum(1 for f in as_completed(futures) if f.result()) == len(futures) - - -def render( - source: Path, jinja_env: jinja2.Environment, env: Environment -) -> Optional[str]: - try: - with open(source, "r") as f: - content = f.read() - env.logger.debug(f"reading template {source} ✿.。.:・") - template = jinja_env.from_string(content) - env.logger.debug(f"rendered template from {source} ~ nyaaa :3") - return template.render(**env.context) - except Exception as e: - env.logger.error(f"couldn’t render {source}: {e} (;⌓̀_⌓́)") - return None - - -def stow_dotfiles( - source_dir: Path, target_dir: Path, env: Environment, clean: bool = False -) -> bool: - if not stow_installed(): - env.logger.error("stow not installed (╥゚╥)") - return False - - packages = list_dotfile_stows(source_dir) - stow_cmd = ["-D"] if clean else ["--no-folding"] - - for pkg in packages: - env.logger.info(f"running stow {stow_cmd} for {pkg.name} ₰˜݆༿") - run_shell_command( - ["stow", "-d", source_dir, "-t", target_dir, *stow_cmd, pkg.name] - ) - - return True - - -def stow_installed() -> bool: - return run_shell_command(["stow", "--version"]) != "" - - -def run_shell_command(cmd: List[str]) -> str: - result = subprocess.run(cmd, capture_output=True, text=True, check=True) - return result.stdout.strip() - - -def list_dotfile_stows(p: Path) -> list[Path]: - denylist = [".", "__"] - return [ - d for d in p.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 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="compile dotfiles") - parser.add_argument("--stow", action="store_true", help="stow compiled dotfiles") - parser.add_argument("--clean", action="store_true", help="clean stowed dotfiles") - parser.add_argument( - "--source", default=Config.default_source_dir, help=f"directory with stowable dotfiles. default '{Config.default_source_dir}'." - ) - parser.add_argument( - "--comp", default=Config.default_compiled_dir, help=f"compiled template output dir. default '{Config.default_compiled_dir}'. :3" - ) - parser.add_argument( - "--target", 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() |