Initial Commit
Initial dotfile manager and config with tools Signed-off-by: Nicholas Mello <nick@nmello.dev>
This commit is contained in:
30
config.toml
Normal file
30
config.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[tools]
|
||||
[tools.nvim]
|
||||
repo_path = "nvim"
|
||||
system_path = "~/.config/nvim"
|
||||
type = "dir"
|
||||
|
||||
[tools.fish]
|
||||
repo_path = "fish"
|
||||
system_path = "~/.config/fish"
|
||||
type = "dir"
|
||||
|
||||
[tools.git.config]
|
||||
repo_path = "git/gitconfig"
|
||||
system_path = "~/.gitconfig"
|
||||
type = "file"
|
||||
|
||||
[tools.git.message]
|
||||
repo_path = "git/gitmessage"
|
||||
system_path = "~/.gitmessage"
|
||||
type = "file"
|
||||
|
||||
[tools.tmux]
|
||||
repo_path = "tmux/tmux.conf"
|
||||
system_path = "~/.tmux.conf"
|
||||
type = "file"
|
||||
|
||||
[tools.bash]
|
||||
repo_path = "bash/bashrc.sh"
|
||||
system_path = "~/.bashrc"
|
||||
type = "file"
|
||||
172
manager
Executable file
172
manager
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from enum import Enum
|
||||
|
||||
|
||||
CONFIG_FILE = Path(__file__).parent / "config.toml"
|
||||
|
||||
|
||||
class ToolType(str, Enum):
|
||||
FILE = "file"
|
||||
DIR = "dir"
|
||||
|
||||
|
||||
def parse_dotfiles_toml(path: Path):
|
||||
"""
|
||||
Parse a dotfiles TOML and return a flat list of entries.
|
||||
Rules:
|
||||
- Every entry (top-level or nested) must have a 'type' (file or dir)
|
||||
- Nested entries belong to the same tool
|
||||
Returns a list of dicts with keys: tool, repo_path, system_path, type, ...
|
||||
"""
|
||||
with open(path, "rb") as f:
|
||||
cfg = tomllib.load(f)
|
||||
|
||||
all_entries = []
|
||||
tools = cfg.get("tools", {})
|
||||
|
||||
def process_table(tool_name, table):
|
||||
"""
|
||||
Process a tool table: flatten nested entries, enforce 'type'.
|
||||
"""
|
||||
# If table itself has 'type', treat it as a single entry
|
||||
if "type" in table:
|
||||
if table["type"] not in ("file", "dir"):
|
||||
raise ValueError(f"Invalid type for tool '{tool_name}': {table['type']}")
|
||||
entry = dict(table)
|
||||
entry["tool"] = tool_name
|
||||
all_entries.append(entry)
|
||||
return # stop recursion here
|
||||
|
||||
# Otherwise recurse into nested tables
|
||||
for key, value in table.items():
|
||||
if isinstance(value, dict):
|
||||
process_table(tool_name, value)
|
||||
else:
|
||||
raise ValueError(f"Invalid entry in tool '{tool_name}': {key}={value}")
|
||||
|
||||
for tool_name, tool_data in tools.items():
|
||||
if not isinstance(tool_data, dict):
|
||||
raise ValueError(f"Tool '{tool_name}' must be a table")
|
||||
process_table(tool_name, tool_data)
|
||||
|
||||
return all_entries
|
||||
|
||||
|
||||
def filter_entries(entries, targets=None):
|
||||
if not targets:
|
||||
return entries
|
||||
|
||||
available_tools = {e["tool"] for e in entries}
|
||||
unknown = [t for t in targets if t not in available_tools]
|
||||
if unknown:
|
||||
raise ValueError(f"Unknown tool(s) specified: {', '.join(unknown)}")
|
||||
|
||||
return [e for e in entries if e["tool"] in targets]
|
||||
|
||||
|
||||
def install(entries):
|
||||
"""
|
||||
Install all entries from repo_path → system_path.
|
||||
If system_path exists, move it to .old before overwriting.
|
||||
"""
|
||||
for entry in entries:
|
||||
repo_path = Path(entry["repo_path"]).expanduser()
|
||||
system_path = Path(entry["system_path"]).expanduser()
|
||||
backup_path = system_path.with_name(system_path.name + ".old")
|
||||
|
||||
# Backup existing file/folder
|
||||
if system_path.exists():
|
||||
if backup_path.exists():
|
||||
shutil.rmtree(backup_path) if backup_path.is_dir() else backup_path.unlink()
|
||||
system_path.rename(backup_path)
|
||||
|
||||
# Copy from repo to system
|
||||
if entry["type"] == "dir":
|
||||
shutil.copytree(repo_path, system_path)
|
||||
else: # file
|
||||
system_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(repo_path, system_path)
|
||||
|
||||
|
||||
def store(entries):
|
||||
"""
|
||||
Store all entries: system_path → repo_path (strict sync).
|
||||
- For directories: remove repo folder first
|
||||
- For files: overwrite
|
||||
"""
|
||||
for entry in entries:
|
||||
repo_path = Path(entry["repo_path"]).expanduser()
|
||||
system_path = Path(entry["system_path"]).expanduser()
|
||||
|
||||
if entry["type"] == "dir":
|
||||
if repo_path.exists():
|
||||
shutil.rmtree(repo_path)
|
||||
shutil.copytree(system_path, repo_path)
|
||||
else: # file
|
||||
repo_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(system_path, repo_path)
|
||||
|
||||
|
||||
def restore(entries):
|
||||
"""
|
||||
Restore all entries from .old → system_path.
|
||||
"""
|
||||
for entry in entries:
|
||||
system_path = Path(entry["system_path"]).expanduser()
|
||||
backup_path = system_path.with_name(system_path.name + ".old")
|
||||
|
||||
if backup_path.exists():
|
||||
if system_path.exists():
|
||||
if system_path.is_dir():
|
||||
shutil.rmtree(system_path)
|
||||
else:
|
||||
system_path.unlink()
|
||||
backup_path.rename(system_path)
|
||||
|
||||
|
||||
def clean(entries):
|
||||
"""
|
||||
Remove all .old backups created during install.
|
||||
"""
|
||||
for entry in entries:
|
||||
system_path = Path(entry["system_path"]).expanduser()
|
||||
backup_path = system_path.with_name(system_path.name + ".old")
|
||||
|
||||
if backup_path.exists():
|
||||
if backup_path.is_dir():
|
||||
shutil.rmtree(backup_path)
|
||||
else:
|
||||
backup_path.unlink()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Manage dotfiles from TOML config")
|
||||
parser.add_argument(
|
||||
"action",
|
||||
choices=["install", "store", "restore", "clean"],
|
||||
help="Action to perform"
|
||||
)
|
||||
parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
help="Tools to act on (default: all)"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
entries = parse_dotfiles_toml(CONFIG_FILE)
|
||||
entries = filter_entries(entries, args.targets)
|
||||
|
||||
if args.action == "install":
|
||||
install(entries)
|
||||
elif args.action == "store":
|
||||
store(entries)
|
||||
elif args.action == "restore":
|
||||
restore(entries)
|
||||
elif args.action == "clean":
|
||||
clean(entries)
|
||||
Reference in New Issue
Block a user