169 lines
5.3 KiB
Python
Executable file
169 lines
5.3 KiB
Python
Executable file
#!/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/"),
|
|
("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/swayosd/", "~/.config/swayosd/"),
|
|
("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"),
|
|
]
|
|
|
|
|
|
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")
|