summaryrefslogtreecommitdiff
path: root/dots_manager
diff options
context:
space:
mode:
Diffstat (limited to 'dots_manager')
-rw-r--r--dots_manager/args.py43
-rw-r--r--dots_manager/cli.py30
-rw-r--r--dots_manager/config.py145
-rw-r--r--dots_manager/env.py56
-rw-r--r--dots_manager/kawaii_logger.py53
-rw-r--r--dots_manager/parallel.py21
-rw-r--r--dots_manager/shell.py3
-rw-r--r--dots_manager/stow.py12
-rw-r--r--dots_manager/template.py18
-rw-r--r--dots_manager/utils.py21
10 files changed, 226 insertions, 176 deletions
diff --git a/dots_manager/args.py b/dots_manager/args.py
deleted file mode 100644
index 40fc42f..0000000
--- a/dots_manager/args.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import argparse
-from pathlib import Path
-from dots_manager.config import Config
-
-
-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"default: '{Config.default_source_dir}'",
- )
- parser.add_argument(
- "--comp",
- type=Path,
- default=Config.default_compiled_dir,
- help=f"default: '{Config.default_compiled_dir}'",
- )
- 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"default: '{Config.default_target_dir}'",
- )
- parser.add_argument(
- "--verbose",
- "-v",
- action="store_true",
- default=False,
- help="enable verbose logging",
- )
- return parser.parse_args()
diff --git a/dots_manager/cli.py b/dots_manager/cli.py
index f152a96..6621954 100644
--- a/dots_manager/cli.py
+++ b/dots_manager/cli.py
@@ -1,16 +1,28 @@
-from dots_manager.args import parse_arguments
-from dots_manager.env import initialize_environment
+import sys
+import shutil
+from dots_manager.config import Environment, parse_arguments
from dots_manager.template import compile_dotfiles
from dots_manager.stow import apply_stow_operation_to_packages
def main():
args = parse_arguments()
- env = initialize_environment(args)
+ env = Environment.from_argv(args)
- if args.clean:
- apply_stow_operation_to_packages(args.comp, args.target, "-D", env)
- if args.compile:
- compile_dotfiles(args.source, args.comp, env)
- if args.stow:
- apply_stow_operation_to_packages(args.comp, args.target, "--no-folding", env)
+ if args.clean and not (
+ apply_stow_operation_to_packages(args.output, args.target, "--delete", env)
+ and (not args.output.exists() or shutil.rmtree(args.output) is None)
+ ):
+ env.logger.error("could not clean up stowed dotfiles <_mood.sad>")
+ sys.exit(1)
+ if args.compile and not compile_dotfiles(args.source, args.output, env):
+ env.logger.error("could not compile dotfiles <_mood.sad>")
+ sys.exit(1)
+ if args.stow and not apply_stow_operation_to_packages(
+ args.output, args.target, "--no-folding", env
+ ):
+ env.logger.error("could not stow dotfile packages <_mood.sad>")
+ sys.exit(1)
+
+ env.logger.info("done! <_mood.happy>")
+ sys.exit(0)
diff --git a/dots_manager/config.py b/dots_manager/config.py
index d6194f6..96d3293 100644
--- a/dots_manager/config.py
+++ b/dots_manager/config.py
@@ -1,14 +1,147 @@
+import argparse
+import os
+import json
+import logging
from dataclasses import dataclass
+from typing import Dict, Any, Optional
from pathlib import Path
-import os
+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)
+class Environment:
+ platform: str
+ system_name: str
+ context: Dict[str, Any]
+ logger: logging.Logger
+
+ @staticmethod
+ def from_argv(args: argparse.Namespace):
+ logger = setup_logger(verbose=args.verbose, logger_name="dots_manager")
+
+ platform = args.platform or run_shell_command(
+ [str(args.helper_scripts / "platform.sh")], logger
+ )
+ if not platform:
+ raise ValueError("failed to determine platform... ")
+ os.environ["PLATFORM"] = platform
+
+ system_name: Optional[str] = args.system_name or run_shell_command(
+ [str(args.helper_scripts / "system_name.sh")], logger
+ )
+ if not system_name:
+ raise ValueError("failed to determine system name... ")
+
+ context = load_context(platform, system_name, args.context, logger)
+ return Environment(platform, system_name, context, logger)
@dataclass(frozen=True)
-class Config:
+class Constants:
default_target_dir: Path = Path.home()
- default_source_dir: Path = Path(__file__).parent.parent / "dots"
- default_compiled_dir: Path = Path("./.compiled_dotfiles/")
- script_dir: Path = Path("home/scripts")
- contexts: Path = Path("contexts.json")
+
+ dots_repo_dir: Path = Path.home() / Path("dotfiles")
+ default_source_dir: Path = dots_repo_dir / Path("dots")
+ default_compiled_dir: Path = dots_repo_dir / Path(".compiled_dotfiles")
+
+ default_script_dir: Path = default_source_dir / Path("home/scripts")
+ default_context: Path = dots_repo_dir / Path("context.json")
+
template_extension: str = ".j2"
max_workers: int = (os.cpu_count() or 1) * 2
+
+ global_context_key: str = "_global"
+ platform_default_context_key: str = "_default"
+
+
+def load_context(
+ platform: str, system_name: str, context_file: Path, logger: logging.Logger
+):
+ logger.info(f"reading context file: {context_file} ✧*:。゚✧")
+
+ context = json.loads(context_file.read_text())
+
+ global_context = context.get(Constants.global_context_key, {})
+ platform_defaults = context.get(platform, {}).get(
+ Constants.platform_default_context_key, {}
+ )
+ defaults = merge_dicts(global_context, platform_defaults)
+
+ system_config = context.get(platform, {}).get(system_name, {})
+ if not system_config:
+ logger.warning(
+ f"could not find context for 'contexts.{platform}.{system_name}' in {context_file.absolute()}"
+ )
+ return {
+ "platform": platform,
+ "system_name": system_name,
+ **merge_dicts(defaults, system_config),
+ }
+
+
+def parse_arguments() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description="૮ ․ ․ ྀིა emprespresso's dotfiles manager 🐧✧˖°"
+ )
+ parser.add_argument(
+ "--verbose",
+ "-v",
+ action="store_true",
+ default=os.environ.get("DEBUG", "false").lower() in ("y", "yes", "true", "t"),
+ help="enables verbose logging.",
+ )
+
+ parser.add_argument("--compile", action="store_true", help="compile le dotfiles.")
+ parser.add_argument(
+ "--source",
+ type=Path,
+ default=Constants.default_source_dir,
+ help=f"where to look for templated dotfile packages. default: '{Constants.default_source_dir}'.",
+ )
+ parser.add_argument(
+ "--output",
+ type=Path,
+ default=Constants.default_compiled_dir,
+ help=f"where to store compiled, stowable dotfile packages. default: '{Constants.default_compiled_dir}'.",
+ )
+ parser.add_argument(
+ "--context",
+ type=Path,
+ default=Constants.default_context,
+ help=f"path to contexts, stored as json. default: '{Constants.default_context}'.",
+ )
+ parser.add_argument(
+ "--helper-scripts",
+ type=Path,
+ default=Constants.default_script_dir,
+ help="where to find executable scripts to determine system info (device name, platform, etc.).",
+ )
+ parser.add_argument(
+ "--system-name",
+ type=str,
+ default=None,
+ help="the system's name. when unspecified, inferred via the hostname, or on osx, computername.",
+ )
+ parser.add_argument(
+ "--platform",
+ type=str,
+ default=None,
+ help="the system's os platform (i.e. osx, linux, bsd, windows, etc.). when unspecified, inferred via $OSTYPE.",
+ )
+
+ 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=Constants.default_target_dir,
+ help=f"where the dotfile packages' symlinks will be stowed. default: '{Constants.default_target_dir}'",
+ )
+
+ return parser.parse_args()
diff --git a/dots_manager/env.py b/dots_manager/env.py
deleted file mode 100644
index 4d89c9e..0000000
--- a/dots_manager/env.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import os
-import json
-import logging
-from pathlib import Path
-from dataclasses import dataclass
-from typing import Dict, Any
-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)
-class Environment:
- platform: str
- system_name: str
- context: Dict[str, Any]
- logger: logging.Logger
-
-
-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 load_context(platform, system_name, context_file, logger):
- 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 {}
diff --git a/dots_manager/kawaii_logger.py b/dots_manager/kawaii_logger.py
index f82da23..f13ea14 100644
--- a/dots_manager/kawaii_logger.py
+++ b/dots_manager/kawaii_logger.py
@@ -2,20 +2,22 @@ import logging
import sys
import random
-MOOD_EMOTICONS = {
- "debug": ["(=^・^=)", "(=・ェ・=)", "(=^-ω-^=)", "(=^‥^=)"],
- "info": ["(^• ω •^)", "(=^・ω・^)y=", "(≚ᄌ≚)", "(。♥‿♥。)"],
- "warning": ["(; ̄Д ̄)", "(¬_¬;)", "(・ัω・ั)", "(・_・ヾ"],
- "error": ["(╥﹏╥)", "(≧Д≦)", "(;′⌒`)", "(T▽T)"],
+MOOD_KAOMOJI = {
+ "<_mood.happy>": ["(ノ◕ヮ◕)ノ*:・゚✧", "(^▽^)", "( ˘⌣˘)♡(˘⌣˘ )"],
+ "<_mood.excited>": ["(๑˃ᴗ˂)ﻭ"],
+ "<_mood.sad>": ["(。•́︿•̀。)", "(╯︵╰,)", "(ಥ﹏ಥ)", "(︶︹︺)"],
+ "<_mood.anxious>": ["(ノдヽ)", "(◎_◎;)", "(・_・;)", "(゚Д゚;)"],
}
-MOOD_SUFFIXES = {
- "_happy": ["(ノ◕ヮ◕)ノ*:・゚✧", "(๑˃ᴗ˂)ﻭ", "(^▽^)", "( ˘⌣˘)♡(˘⌣˘ )"],
- "_sad": ["(。•́︿•̀。)", "(╯︵╰,)", "(ಥ﹏ಥ)", "(︶︹︺)"],
- "_anxious": ["(ノдヽ)", "(◎_◎;)", "(・_・;)", "(゚Д゚;)"],
+LEVEL_KAOMOJI = {
+ logging.DEBUG: ["(=^・^=)", "(=・ェ・=)", "(=^-ω-^=)", "(=^‥^=)"],
+ logging.INFO: ["(^• ω •^)", "(=^・ω・^)y=", "(≚ᄌ≚)", "(。♥‿♥。)"],
+ logging.WARNING: ["(; ̄Д ̄)", "(¬_¬;)", "(・ัω・ั)", "(・_・ヾ"],
+ logging.ERROR: ["(╥﹏╥)", "(≧Д≦)", "(;′⌒`)", "(T▽T)"],
+ logging.CRITICAL: ["(╯°□°)╯︵ ┻━┻"],
}
-LEVEL_COLORS = {
+LEVEL_ANSI_STYLES = {
logging.DEBUG: "\033[95m", # light magenta
logging.INFO: "\033[96m", # light cyan
logging.WARNING: "\033[93m", # light yellow
@@ -23,32 +25,33 @@ LEVEL_COLORS = {
logging.CRITICAL: "\033[35m", # magenta
}
-RESET_COLOR = "\033[0m"
+RESET_ANSI = "\033[0m"
class KawaiiFormatter(logging.Formatter):
- def format(self, record):
- level = record.levelname.lower()
- color = LEVEL_COLORS.get(record.levelno, "")
- base_emotes = MOOD_EMOTICONS.get(level, [])
- mood_emotes = []
-
- emote_pool = mood_emotes if mood_emotes else base_emotes
- emote = random.choice(emote_pool) if emote_pool else "(・ω・)"
-
+ def format(self, record: logging.LogRecord):
message = record.msg.strip()
ts = self.formatTime(record, "%Y-%m-%d %H:%M:%S")
lvl = record.levelname
filename = record.filename
lineno = str(record.lineno)
- formatted = f"[{ts}] {color}[{lvl} {filename}:{lineno}]{RESET_COLOR} {color}{message}{RESET_COLOR} {emote}"
- return f"{formatted}{RESET_COLOR}"
+ split_last_word = message.rsplit(" ", 1)
+ last_word = split_last_word[-1] if split_last_word else ""
+ emote_pool = LEVEL_KAOMOJI.get(record.levelno, [])
+ if last_word in MOOD_KAOMOJI:
+ message = split_last_word[0] if len(split_last_word) > 1 else ""
+ emote_pool = MOOD_KAOMOJI.get(last_word, [])
+ emote = random.choice(emote_pool) if emote_pool else "(・ω・)"
+
+ color = LEVEL_ANSI_STYLES.get(record.levelno, "")
+
+ formatted = f"[{ts}] {color}[{lvl} {filename}:{lineno}]{RESET_ANSI} {color}{message}{RESET_ANSI} {emote}"
+ return f"{formatted}{RESET_ANSI}"
-def setup_logger(verbose: bool = False) -> logging.Logger:
- """sets up a super cute logger with sparkles and cat faces ✨"""
- logger = logging.getLogger("dotfiles")
+def setup_logger(verbose: bool = False, logger_name: str = "") -> logging.Logger:
+ logger = logging.getLogger(logger_name)
logger.setLevel(logging.DEBUG if verbose else logging.INFO)
handler = logging.StreamHandler(sys.stdout)
diff --git a/dots_manager/parallel.py b/dots_manager/parallel.py
deleted file mode 100644
index 8c85660..0000000
--- a/dots_manager/parallel.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from concurrent.futures import ThreadPoolExecutor, as_completed
-from typing import Callable, List, Optional, TypeVar
-from dots_manager.config import Config
-from dots_manager.env import Environment
-
-T = TypeVar("T")
-R = TypeVar("R")
-
-
-def parallelize(
- worker: Callable[[T], R],
- items: List[T],
- env: Environment,
- executor: Optional[ThreadPoolExecutor] = None,
-) -> List[R]:
- if executor is None:
- executor = ThreadPoolExecutor(max_workers=Config.max_workers)
- with executor as exec:
- futures = [exec.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)]
diff --git a/dots_manager/shell.py b/dots_manager/shell.py
index afd705a..09e84b2 100644
--- a/dots_manager/shell.py
+++ b/dots_manager/shell.py
@@ -9,6 +9,9 @@ def run_shell_command(cmd: List[str], logger: logging.Logger) -> Optional[str]:
return subprocess.run(
cmd, capture_output=True, text=True, check=True
).stdout.strip()
+ except FileNotFoundError as e:
+ logger.error(f"excecutable not found: {cmd[0]}, {e}")
+ return None
except subprocess.CalledProcessError as e:
logger.error(f"command failed: {cmd}, {e.stderr}")
return None
diff --git a/dots_manager/stow.py b/dots_manager/stow.py
index b78e2fc..da4fbbb 100644
--- a/dots_manager/stow.py
+++ b/dots_manager/stow.py
@@ -1,9 +1,8 @@
from pathlib import Path
from typing import Literal
-from dots_manager.env import Environment
+from dots_manager.config import Environment
from dots_manager.shell import run_shell_command
-from dots_manager.parallel import parallelize
-from dots_manager.utils import is_some
+from dots_manager.utils import is_some, parallelize
def list_stowable_packages(packages: Path) -> list[Path]:
@@ -18,9 +17,12 @@ def list_stowable_packages(packages: Path) -> list[Path]:
def apply_stow_operation_to_packages(
packages: Path,
target: Path,
- stow_op: Literal["-D", "--no-folding"],
+ stow_op: Literal["--delete", "--no-folding"],
env: Environment,
) -> bool:
+ if not packages.exists():
+ env.logger.warn("nothing to clean up <_mood.anxious>")
+ return True
if not run_shell_command(["stow", "--version"], env.logger):
env.logger.error("stow not installed D:")
return False
@@ -35,6 +37,6 @@ def apply_stow_operation_to_packages(
results = parallelize(
lambda command: is_some(run_shell_command, command, env.logger)[0],
commands,
- env,
+ env.logger,
)
return len(commands) == sum(1 if x else 0 for x in results)
diff --git a/dots_manager/template.py b/dots_manager/template.py
index 4c62500..13aecf7 100644
--- a/dots_manager/template.py
+++ b/dots_manager/template.py
@@ -2,20 +2,18 @@ from pathlib import Path
from typing import Optional
import shutil
import jinja2
-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
+from dots_manager.config import Constants, Environment
+from dots_manager.utils import is_some, parallelize
def is_template(path: Path) -> bool:
- return path.suffix == Config.template_extension
+ return path.suffix == Constants.template_extension
def find_templates_under(destination: Path) -> list[Path]:
return [
t
- for t in destination.glob("**/*" + Config.template_extension)
+ for t in destination.glob("**/*" + Constants.template_extension)
if t.is_file() and is_template(t)
]
@@ -47,15 +45,15 @@ def render(
source: Path, jinja_env: jinja2.Environment, env: Environment
) -> Optional[str]:
try:
- env.logger.debug(f"rendering template {source} ✿.。.:・")
+ env.logger.debug(f"rendering template {source} <_mood.happy>")
return jinja_env.from_string(source.read_text()).render(**env.context)
except Exception as e:
- env.logger.error(f"couldn’t render {source}: {e}")
+ env.logger.error(f"couldn’t render {source}: {e} <_mood.sad>")
return None
def compile_dotfiles(source: Path, output: Path, env: Environment) -> bool:
- env.logger.info(f"compiling {source} to {output}")
+ env.logger.info(f"compiling {source} to {output} <_mood.anxious>")
output.mkdir(exist_ok=True, parents=True)
shutil.copytree(source, output, dirs_exist_ok=True)
@@ -68,6 +66,6 @@ def compile_dotfiles(source: Path, output: Path, env: Environment) -> bool:
)[0]
and (template.unlink() is None),
find_templates_under(output),
- env,
+ env.logger,
)
return all(tasks)
diff --git a/dots_manager/utils.py b/dots_manager/utils.py
index e8210cc..1b900f0 100644
--- a/dots_manager/utils.py
+++ b/dots_manager/utils.py
@@ -1,8 +1,11 @@
-from typing import Callable, Optional, TypeVar, Tuple, ParamSpec, Dict, Any
+import logging
+from typing import Callable, Optional, TypeVar, Tuple, ParamSpec, Dict, Any, List
+from concurrent.futures import ThreadPoolExecutor, as_completed
from functools import reduce
P = ParamSpec("P")
T = TypeVar("T")
+R = TypeVar("R")
def is_some(
@@ -23,3 +26,19 @@ def merge_dicts(*dicts: Dict[str, Any]) -> Dict[str, Any]:
return out
return reduce(merge, dicts, {})
+
+
+def parallelize(
+ worker: Callable[[T], R],
+ items: List[T],
+ logger: logging.Logger,
+ executor: Optional[ThreadPoolExecutor] = None,
+) -> List[R]:
+ if executor is None:
+ from dots_manager.config import Constants
+
+ executor = ThreadPoolExecutor(max_workers=Constants.max_workers)
+ with executor as exec:
+ futures = [exec.submit(worker, item) for item in items]
+ logger.info(f"submitted {len(futures)} tasks to executor <_mood.excited>")
+ return [f.result() for f in as_completed(futures)]