summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElizabeth <lizhunt@amazon.com>2025-05-28 18:19:25 -0700
committerElizabeth <lizhunt@amazon.com>2025-05-28 18:48:35 -0700
commit2a0f7892fd3d00daa62a24359d2bae186092916f (patch)
tree04a8c5c6ba697440b3af812879905587bc97a2ae
parent1c2bd3faf3a1f0cd456a25b19ec404e1f79518c3 (diff)
downloaddotfiles-2a0f7892fd3d00daa62a24359d2bae186092916f.tar.gz
dotfiles-2a0f7892fd3d00daa62a24359d2bae186092916f.zip
Fix that AI shit
-rw-r--r--.gitignore1
-rwxr-xr-x[-rw-r--r--]dots.py363
-rwxr-xr-xdots.sh12
3 files changed, 205 insertions, 171 deletions
diff --git a/.gitignore b/.gitignore
index 9daf20c..042f81c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
.compiled_dotfiles/
+.venv
diff --git a/dots.py b/dots.py
index ed0af77..7dec208 100644..100755
--- a/dots.py
+++ b/dots.py
@@ -1,190 +1,235 @@
#!/usr/bin/env python3
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Dict, List, Optional
import argparse
import json
+import logging
import os
-import shutil
import subprocess
import sys
-from pathlib import Path
+import shutil
+from concurrent.futures import Executor, ThreadPoolExecutor, as_completed
import jinja2
-def get_platform():
- """Get the platform using the platform.sh script"""
- try:
- script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "home/scripts/platform.sh")
- result = subprocess.run([script_path], capture_output=True, text=True, check=True)
- return result.stdout.strip()
- except subprocess.CalledProcessError as e:
- print(f"Error getting platform: {e}")
+
+@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
+
+
+@dataclass(frozen=True)
+class Environment:
+ platform: str
+ device_name: str
+ context: Dict
+ verbose: bool
+ logger: logging.Logger
+
+
+def main() -> None:
+ args = parse_arguments()
+ 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")
sys.exit(1)
-def get_device_name():
- """Get the device name using the system_name.sh script"""
+ 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")
+ 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")
+ 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")
+ sys.exit(1)
+
+ env.logger.info("Done!")
+ sys.exit(0)
+
+
+def initialize_environment(verbose: bool) -> Environment:
+ logger = setup_logging(verbose)
+ platform = get_platform()
+ device_name = get_device_name()
+ context = {
+ **load_context(platform, device_name, Config.CONTEXTS),
+ "platform": platform,
+ "device_name": device_name,
+ }
+
+ return Environment(platform, device_name, context, verbose, 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"copytree {source} -> {destination}")
+
+ templates = [t for t in destination.glob("**/*.j2") if t.is_file()]
+ if not templates:
+ env.logger.debug(f"{source} has no templates")
+ return True
+
+ for template in templates:
+ env.logger.debug(f"submitting work to render {source} . {templates}")
+ 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}")
+ jinja_env = jinja2.Environment(
+ undefined=jinja2.StrictUndefined, trim_blocks=True, lstrip_blocks=True
+ )
+ with open(template, "w") as t:
+ t.write(render(template, jinja_env, env) or "")
+ env.logger.debug(f"stripping template suffix of {template}")
+ template.rename(template.with_suffix(""))
+ return True
+
+
+
+def compile_dotfiles(source_dir: Path, target_dir: Path, env: Environment):
+ 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}")
+ futures = [
+ executor.submit(
+ copy_with_templates_rendered, executor, dotfiles, target_dir / dotfiles.name, env
+ )
+ for dotfiles in dotfile_directories
+ ]
+ env.logger.debug(f"constructed {len(futures)} tasks")
+ return sum(1 for future in as_completed(futures) if future.result()) == len(
+ futures
+ )
+ return False
+
+
+def render(
+ source: Path,
+ jinja_env: jinja2.Environment,
+ env: Environment,
+) -> Optional[str]:
try:
- script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "home/scripts/system_name.sh")
- result = subprocess.run([script_path], capture_output=True, text=True, check=True)
- return result.stdout.strip()
- except subprocess.CalledProcessError as e:
- print(f"Error getting device name: {e}")
- sys.exit(1)
+ 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)
+ return template.render(**env.context)
+ except Exception as e:
+ env.logger.error(f"Error rendering template {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():
+ return False
-def load_context(platform, device_name):
- """Load the appropriate context from contexts.json"""
+ packages = [d for d in source_dir.iterdir() if d.is_dir()]
+ stow_op = ["-D"] if clean else ["--no-folding"]
+
+ for pkg in packages:
+ run_shell_command(["stow", *stow_op, 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 get_platform() -> str:
+ 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 load_context(platform: str, device_name: str, context_file: Path) -> Dict:
try:
- with open('contexts.json', 'r') as f:
+ with open(context_file) as f:
contexts = json.load(f)
if platform not in contexts:
- print(f"Warning: Platform '{platform}' not found in contexts.json")
- return {}
-
- if device_name not in contexts[platform]:
- print(f"Warning: Device '{device_name}' not found for platform '{platform}' in contexts.json")
- # Try to use 'default' for the platform if device not found
- if 'default' in contexts[platform]:
- print(f"Using default context for platform '{platform}'")
- return contexts[platform]['default']
return {}
- return contexts[platform][device_name]
- except FileNotFoundError:
- print("Warning: contexts.json not found")
+ return contexts[platform].get(
+ device_name, contexts[platform].get("default", {})
+ )
+ except (FileNotFoundError, json.JSONDecodeError):
return {}
- except json.JSONDecodeError as e:
- print(f"Error parsing contexts.json: {e}")
- sys.exit(1)
-
-def compile_dotfiles(source_dir, target_dir, context):
- """Compile dotfiles, processing Jinja templates with context"""
- # Create jinja environment
- env = jinja2.Environment(
- undefined=jinja2.StrictUndefined,
- trim_blocks=True,
- lstrip_blocks=True
- )
- # Ensure target directory exists
- target_path = Path(target_dir)
- target_path.mkdir(exist_ok=True, parents=True)
-
- # Process each file in source directory
- for root, dirs, files in os.walk(source_dir):
- # Skip .git directories
- if '.git' in dirs:
- dirs.remove('.git')
-
- # Create relative path from source_dir
- rel_path = os.path.relpath(root, source_dir)
- if rel_path == '.':
- rel_path = ''
-
- # Create target directory
- if rel_path:
- target_subdir = target_path / rel_path
- target_subdir.mkdir(exist_ok=True, parents=True)
- else:
- target_subdir = target_path
-
- for file in files:
- source_file = os.path.join(root, file)
-
- # Determine target filename (remove .j2 extension for templates)
- target_file_name = file[:-3] if file.endswith('.j2') else file
- target_file = target_subdir / target_file_name
-
- print(f"Processing: {source_file} -> {target_file}")
-
- if file.endswith('.j2'):
- # Render Jinja2 template
- try:
- with open(source_file, 'r') as f:
- template_content = f.read()
-
- # Use the environment to create templates
- template = env.from_string(template_content)
- rendered_content = template.render(**context)
-
- # Write rendered content to target file
- with open(target_file, 'w') as f:
- f.write(rendered_content)
-
- # Make executable if source is executable
- if os.access(source_file, os.X_OK):
- os.chmod(target_file, 0o755)
-
- except Exception as e:
- print(f"Error rendering template {source_file}: {e}")
- else:
- # Copy file as-is
- shutil.copy2(source_file, target_file)
-
-def stow_dotfiles(dotfiles_dir, target_dir=None):
- """Use GNU Stow to symlink the compiled dotfiles"""
- if target_dir is None:
- target_dir = os.path.expanduser("~")
-
- # Check if stow is installed
- try:
- subprocess.run(["stow", "--version"], capture_output=True, check=True)
- except (subprocess.CalledProcessError, FileNotFoundError):
- print("Error: GNU Stow not found. Please install it before using --stow.")
- sys.exit(1)
- # Get list of directories in dotfiles_dir (each is a stow package)
- packages = [d for d in os.listdir(dotfiles_dir) if os.path.isdir(os.path.join(dotfiles_dir, d))]
-
- for package in packages:
- print(f"Stowing package: {package}")
- try:
- # Use --adopt to replace existing files
- # Use --no-folding to enable leaf mode (each file individually linked)
- subprocess.run([
- "stow",
- "--dir=" + dotfiles_dir,
- "--target=" + target_dir,
- "--adopt",
- "--no-folding",
- package
- ], check=True)
- print(f"Successfully stowed {package}")
- except subprocess.CalledProcessError as e:
- print(f"Error stowing {package}: {e}")
-
-def main():
+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("--target", help="Target directory for stow (default: $HOME)")
- args = parser.parse_args()
-
- if not args.compile and not args.stow:
- parser.print_help()
- sys.exit(1)
-
- platform = get_platform()
- device_name = get_device_name()
- print(f"Platform: {platform}, Device: {device_name}")
-
- context = load_context(platform, device_name)
- print(f"Loaded context: {context}")
-
- # Add platform and device_name to context
- context['platform'] = platform
- context['device_name'] = device_name
+ 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()
- compiled_dir = ".compiled_dotfiles"
- if args.compile:
- print(f"Compiling dotfiles from 'dotfiles' to '{compiled_dir}'...")
- compile_dotfiles("dotfiles", compiled_dir, context)
- print("Compilation complete.")
+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")
- if args.stow:
- target_dir = args.target if args.target else None
- print(f"Stowing dotfiles from '{compiled_dir}'...")
- stow_dotfiles(compiled_dir, target_dir)
- print("Stowing complete.")
if __name__ == "__main__":
main()
diff --git a/dots.sh b/dots.sh
deleted file mode 100755
index 38b4923..0000000
--- a/dots.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/sh
-
-# usage: ./dots.sh [update|clean]?
-
-OP="${1:-'update'}"
-
-STOW_OP="--no-folding"
-if [ "$OP" = "clean" ]; then STOW_OP="-D"; fi
-
-find . -type d -mindepth 1 -maxdepth 1 -not -path '*/.*' \
- | sed "s/^\.\///" \
- | while read dir; do stow "$STOW_OP" "$dir"; done