summaryrefslogtreecommitdiff
path: root/dots.py
diff options
context:
space:
mode:
Diffstat (limited to 'dots.py')
-rwxr-xr-xdots.py223
1 files changed, 109 insertions, 114 deletions
diff --git a/dots.py b/dots.py
index 7dec208..f94bcfe 100755
--- a/dots.py
+++ b/dots.py
@@ -2,7 +2,7 @@
from dataclasses import dataclass
from pathlib import Path
-from typing import Dict, List, Optional
+from typing import Dict, List, Optional, Any
import argparse
import json
import logging
@@ -11,24 +11,26 @@ 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:
- SCRIPT_DIR: Path = Path(__file__).parent / "home/scripts"
- COMPILED_DIR: Path = Path(".compiled_dotfiles")
- CONTEXTS: Path = Path("context.json")
- DEFAULT_TARGET_DIR: Path = Path.home()
- DEFAULT_SOURCE_DIR: Path = Path(".")
- MAX_WORKERS: int = (os.cpu_count() or 1) * 2
+ script_dir: Path = Path(__file__).parent / "home/scripts"
+ compiled_dir: Path = Path(".compiled_dotfiles")
+ contexts: Path = Path("context.json")
+ default_target_dir: Path = Path.home()
+ default_source_dir: Path = Path(".")
+ max_workers: int = (os.cpu_count() or 1) * 2
@dataclass(frozen=True)
class Environment:
platform: str
- device_name: str
- context: Dict
+ system_name: str
+ context: Dict[str, Any]
verbose: bool
logger: logging.Logger
@@ -38,134 +40,129 @@ def main() -> None:
env = initialize_environment(args.verbose)
if not (args.compile or args.stow or args.clean):
- env.logger.error("At least one of --compile, --stow, or --clean must be specified")
+ env.logger.error("you gotta specify at least one action nya~ (⁎⁍̴̆‾⁍̴̆⁎)")
sys.exit(1)
if args.compile:
- env.logger.debug(f"Compiling {args.source} -> {args.comp}")
- if not compile_dotfiles(args.source, args.comp, env):
- env.logger.error("Failed to compile dotfiles")
+ 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:
- env.logger.debug(f"Stowing {(args.target)}")
- if not stow_dotfiles(args.comp, args.target, env, clean=False):
- env.logger.error("Failed to stow dotfiles")
+ 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.debug(f"Cleaning dotfiles from {(args.target)}")
- if not stow_dotfiles(args.comp, args.target, env, clean=True):
- env.logger.error("Failed to clean dotfiles")
+ 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("Done!")
+ env.logger.info("yay~ all done!! ₕᵒ. .ᵒₕ♡")
sys.exit(0)
def initialize_environment(verbose: bool) -> Environment:
- logger = setup_logging(verbose)
+ logger = setup_logger(verbose)
platform = get_platform()
- device_name = get_device_name()
+ system_name = get_system_name()
context = {
- **load_context(platform, device_name, Config.CONTEXTS),
- "platform": platform,
- "device_name": device_name,
+ **merge_dicts(
+ load_context(platform, system_name, Config.contexts, logger),
+ {"platform": platform, "system_name": system_name},
+ )
}
-
- return Environment(platform, device_name, context, verbose, logger)
+ return Environment(platform, system_name, context, verbose, logger)
-def copy_with_templates_rendered(executor: Executor, source: Path, destination: Path, env: Environment) -> bool:
+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"copytree {source} -> {destination}")
+ 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"{source} has no templates")
+ env.logger.debug(f"no templates to render in {source} (•ᴗ•)⁎")
return True
for template in templates:
- env.logger.debug(f"submitting work to render {source} . {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}")
+ env.logger.debug(f"rendering template {template} ✧*ฺ")
jinja_env = jinja2.Environment(
- undefined=jinja2.StrictUndefined, trim_blocks=True, lstrip_blocks=True
+ 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(render(template, jinja_env, env) or "")
- env.logger.debug(f"stripping template suffix of {template}")
+ 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):
+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_directories = [
- dir for dir in source_dir.iterdir() if dir.is_dir() and "." not in dir.name
- ]
- env.logger.debug(f"compiling dotfile directories {dotfile_directories}")
+ with ThreadPoolExecutor(max_workers=Config.max_workers) as executor:
+ dotfile_dirs = list_dotfiles(source_dir)
+ env.logger.debug(f"found dotfile dirs: {dotfile_dirs}")
+
futures = [
executor.submit(
- copy_with_templates_rendered, executor, dotfiles, target_dir / dotfiles.name, env
+ copy_with_templates_rendered,
+ executor,
+ d,
+ target_dir / d.name,
+ env,
)
- for dotfiles in dotfile_directories
+ for d in dotfile_dirs
]
- env.logger.debug(f"constructed {len(futures)} tasks")
- return sum(1 for future in as_completed(futures) if future.result()) == len(
- futures
- )
- return False
+ 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]:
+def render(source: Path, jinja_env: jinja2.Environment, env: Environment) -> Optional[str]:
try:
- env.logger.debug(f"Reading {source}")
with open(source, "r") as f:
- template_content = f.read()
- env.logger.debug(f"Compiling template {source}")
- template = jinja_env.from_string(template_content)
+ 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"Error rendering template {source}: {e}")
+ env.logger.error(f"couldn’t render {source}: {e} (;⌓̀_⌓́)")
return None
-def stow_package_task(
- stow_dir: Path, package: Path, target_dir: Path, env: Environment
-) -> bool:
- try:
- return True
- except subprocess.SubprocessError as e:
- env.logger.error(f"Error stowing package {package.name}: {e}")
- return False
-
-
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 = [d for d in source_dir.iterdir() if d.is_dir()]
- stow_op = ["-D"] if clean else ["--no-folding"]
+ packages = list_dotfiles(source_dir)
+ stow_cmd = ["-D"] if clean else ["--no-folding"]
for pkg in packages:
- run_shell_command(["stow", *stow_op, pkg.name])
+ env.logger.info(f"running stow 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"])
+ return run_shell_command(["stow", "--version"]) != ""
def run_shell_command(cmd: List[str]) -> str:
@@ -174,61 +171,59 @@ def run_shell_command(cmd: List[str]) -> str:
def get_platform() -> str:
- return run_shell_command([str(Config.SCRIPT_DIR / "platform.sh")])
+ return run_shell_command([str(Config.script_dir / "platform.sh")])
-def get_device_name() -> str:
- return run_shell_command([str(Config.SCRIPT_DIR / "system_name.sh")])
+def get_system_name() -> str:
+ os.environ["PLATFORM"] = get_platform()
+ return run_shell_command([str(Config.script_dir / "system_name.sh")])
+def list_dotfiles(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, device_name: str, context_file: Path) -> Dict:
+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)
- if platform not in contexts:
- return {}
+ global_config = contexts.get("_global", {})
+ platform_defaults = contexts.get(platform, {}).get("_default", {})
+ defaults = merge_dicts(global_config, platform_defaults)
- return contexts[platform].get(
- device_name, contexts[platform].get("default", {})
- )
- except (FileNotFoundError, json.JSONDecodeError):
+ 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 merge_dicts(defaults, system_config)
+ except (FileNotFoundError, json.JSONDecodeError) as e:
+ logger.error(f"error loading context: {e} ⋆ฺ°☁。⋆ฺ °★ °。")
return {}
-def parse_arguments() -> argparse.Namespace:
- parser = argparse.ArgumentParser(description="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 (remove) stowed dotfiles")
- parser.add_argument(
- "--source",
- help=f"Target directory for dotfiles (default: {Config.DEFAULT_SOURCE_DIR})",
- default=Config.DEFAULT_SOURCE_DIR,
- )
- parser.add_argument(
- "--comp",
- help=f"Target directory for compiled templates (default: {Config.COMPILED_DIR})",
- default=Config.COMPILED_DIR,
- )
- parser.add_argument(
- "--target",
- help=f"Target directory for stow (default: {Config.DEFAULT_TARGET_DIR})",
- default=Config.DEFAULT_TARGET_DIR,
- )
- parser.add_argument(
- "--verbose", "-v", action="store_true", help="Enable verbose logging"
- )
- return parser.parse_args()
+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 setup_logging(verbose: bool) -> logging.Logger:
- logging.basicConfig(
- level=logging.DEBUG if verbose else logging.INFO,
- format="%(asctime)s - %(levelname)s - %(message)s",
- datefmt="%Y-%m-%d %H:%M:%S",
- )
- return logging.getLogger("dotfiles")
+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="source dir for dotfiles")
+ parser.add_argument("--comp", default=Config.compiled_dir, help="compiled template output dir")
+ parser.add_argument("--target", default=Config.default_target_dir, help="stow target directory")
+ parser.add_argument("--verbose", "-v", action="store_true", help="enable verbose logging ✧*ฺ", default=False)
+ return parser.parse_args()
if __name__ == "__main__":