summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rwxr-xr-xdots_manager/.main.py328
-rw-r--r--dots_manager/args.py2
-rw-r--r--dots_manager/cli.py14
-rw-r--r--dots_manager/env.py8
-rw-r--r--dots_manager/parallel.py4
-rw-r--r--dots_manager/setup.py19
-rw-r--r--dots_manager/stow.py8
-rw-r--r--dots_manager/template.py8
9 files changed, 21 insertions, 372 deletions
diff --git a/.gitignore b/.gitignore
index 4306b20..aea7090 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
*.egg-info
.compiled_dotfiles/
.venv
-__pycache__
+**/__pycache__
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: