#!/usr/bin/env python3 import os import filecmp import argparse from enum import Enum class SyncStatus: SAME = 1 DIFF = 2 SRC_MISSING = 3 DEST_MISSING = 4 BOTH_MISSING = 5 # https://stackoverflow.com/a/287944 class bcolors: RED = "\033[91m" GREEN = "\033[92m" YELLOW = "\033[93m" BLUE = "\033[94m" PURPLE = "\033[95m" ENDC = "\033[0m" BOLD = "\033[1m" UNDERLINE = "\033[4m" parser = argparse.ArgumentParser( prog="Dotfiles sync", description="Saves and restores my dotfiles", ) parser.add_argument("action", choices=["save", "restore"]) parser.add_argument( "-a", "--all", help="Sync all files", action="store_true", ) synced_files = [ ("editor/helix/", "~/.config/helix/"), ("de/i3/", "~/.config/i3/"), ("de/hypr/", "~/.config/hypr/"), ("shell/bash/.bash_profile", "~/.bash_profile"), ("shell/bash/.bashrc", "~/.bashrc"), ("shell/bash/.bash_aliases", "~/.bash_aliases"), ("shell/bash/.bash_exec", "~/.bash_exec"), ("shell/nu/.nu_aliases", "~/.nu_aliases"), ("term/rio/", "~/.config/rio/"), ("term/alacritty/", "~/.config/alacritty/"), ("term/tmux/tmux.conf", "~/.config/tmux/tmux.conf"), ("term/zellij/", "~/.config/zellij/"), ("bar/waybar/", "~/.config/waybar/"), ("bar/i3status-rust/", "~/.config/i3status-rust/"), ("bar/eww/", "~/.config/eww/"), ("home/xinitrc", "~/.xinitrc"), ("misc/picom/", "~/.config/picom/"), ("misc/runst/", "~/.config/runst/"), ("misc/mako/", "~/.config/mako/"), ("misc/end-rs/", "~/.config/end-rs/"), ("misc/swayosd/", "~/.config/swayosd/"), ("misc/anyrun/", "~/.config/anyrun/"), ("misc/x11-toggle-gpu/", "~/.local/share/x11-toggle-gpu/"), ("bin/swaylock-hyprland", "~/.local/bin/swaylock-hyprland"), ("bin/Hyprland", "~/.local/bin/Hyprland"), ("bin/jaaj", "~/.local/bin/jaaj"), ("bin/xtoggle-touchpad", "~/.local/bin/xtoggle-touchpad"), ("bin/wtoggle-touchpad", "~/.local/bin/wtoggle-touchpad"), ("bin/togglescreen", "~/.local/bin/togglescreen"), ("bin/mc-key-fix", "~/.local/bin/mc-key-fix"), ("bin/x11-toggle-primary-gpu", "~/.local/bin/x11-toggle-primary-gpu"), ("bin/uwu-launcher", "~/.local/bin/uwu-launcher"), ("bin/eww-bard", "~/.local/bin/eww-bard"), ("bin/wallpaperctl", "~/.local/bin/wallpaperctl"), # Submodules ("Ahurac-dotfiles/bin/ssh-fwd", "~/.local/bin/ssh-fwd"), ("Ahurac-dotfiles/bin/watchbatch", "~/.local/bin/watchbatch"), ] def save(fd: tuple[str, str]): folder = "/".join(fd[0].split("/")[0:-1]) if not os.path.exists(folder): os.mkdir(folder) os.system(f"rsync -r {fd[1]} {fd[0]}") def restore(fd: tuple[str, str]): os.system(f"rsync -r {fd[0]} {fd[1]}") def get_sync_status(fd: tuple[str, str]) -> SyncStatus: f1 = fd[0] f2 = os.path.expanduser(fd[1]) def has_differences(dcmp): differences = dcmp.left_only + dcmp.right_only + dcmp.diff_files if differences: return True return any([has_differences(subdcmp) for subdcmp in dcmp.subdirs.values()]) if not (os.path.exists(f1) or os.path.exists(f2)): return SyncStatus.BOTH_MISSING elif not os.path.exists(f1): return SyncStatus.SRC_MISSING elif not os.path.exists(f2): return SyncStatus.DEST_MISSING elif os.path.isfile(f1) and filecmp.cmp(f1, f2): return SyncStatus.SAME elif os.path.isdir(f1) and not has_differences(filecmp.dircmp(f1, f2)): return SyncStatus.SAME else: return SyncStatus.DIFF def list_files(reverse: bool = False) -> list[int]: len_len = len(str(len(synced_files))) for i, f in enumerate(synced_files): i = str(i) i = " " * (len_len - len(i)) + i status = get_sync_status(f) f1_c, f2_c = bcolors.GREEN, bcolors.GREEN if status == SyncStatus.DIFF: f1_c, f2_c = bcolors.YELLOW, bcolors.YELLOW elif status == SyncStatus.SRC_MISSING: f1_c = bcolors.RED if not reverse else f1_c f2_c = bcolors.RED if reverse else f2_c elif status == SyncStatus.DEST_MISSING: f1_c = bcolors.RED if reverse else f1_c f2_c = bcolors.RED if not reverse else f2_c elif status == SyncStatus.BOTH_MISSING: f1_c, f2_c = bcolors.RED, bcolors.RED print( f"{bcolors.PURPLE}{i} {f1_c}{f[int(reverse)]} {bcolors.ENDC}-> {f2_c}{f[int(not reverse)]}{bcolors.ENDC}" ) colon = ":" * len_len user_action = input( f"{bcolors.BOLD}{bcolors.BLUE}{colon}{bcolors.ENDC} {bcolors.BOLD}Files to sync (eg: 1 2 3, 1-3):\n{bcolors.BLUE}{colon}{bcolors.ENDC} " ).strip() # Parse input out = [] if user_action == "": pass elif user_action.count("-") == 0: out = list(map(lambda x: int(x), user_action.split())) elif user_action.count("-") == 1: out = list(range(*map(lambda x: int(x), user_action.split("-")))) out += [out[-1] + 1] else: raise Exception("Invalid user input") return out if __name__ == "__main__": args = parser.parse_args() msg = "" r = range(len(synced_files)) if args.all else list_files(args.action == "save") for ri, i in enumerate(r): if args.action == "save": save(synced_files[i]) elif args.action == "restore": restore(synced_files[i]) print(" " * len(msg), end="\r") index = "0" * (len(str(len(r))) - len(str(ri + 1))) + str(ri + 1) msg = f"[{index}/{len(r)}] Synced {synced_files[i]}" print(msg, end="\r")