#!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.13" # dependencies = [ # "defopt", # ] # /// from difflib import unified_diff from pathlib import Path from subprocess import run from urllib.request import urlretrieve CFG = Path("~/.config").expanduser() DOTFILES = Path("~/dotfiles").expanduser() installmap = dict( fonts=("noto-fonts-emoji", "ttf-hack", "font-manager"), nushell=("nushell", "oh-my-posh", "carapace-bin", "zoxide"), tmux=("tmux", "urlscan"), nvim=("neovim", "ripgrep"), utils=("uv", "bat", "ncdu", "unzip", "jq"), gittools=("tig", "diff-so-fancy", "git-secret", "git-delta", "git-lfs", "lazygit"), pdftools=("sioyek", "zathura", "zathura-pdf-mupdf", "zathura-djvu", "zathura-ps"), media=("vlc", "mpv", "protobuf", "yt-dlp", "quodlibet", "qimgv"), filebrowsers=("pcmanfm", "yazi", "zoxide", "eza"), netbrowsers=( "qutebrowser", "firefox", "python-adblock", "python-tldextract", "bitwarden-cli", # for qutebrowser autofill ), emailcalrss=( "vdirsyncer", # sync calendar+contacts "khard", # contacts "khal", # calendar "aerc", # email "pandoc", # md2html emails "pass", # password manager "w3m", # terminal browser "newsboat", # rss reader "python-aiohttp-oauthlib", # for google vdirsyncer ), monitors=( "btop", # hardware "nvtop", # gpu "lazyjournal", # journald "isd", # systemd "bandwhich", # network ), apps=("bitwarden", "qalculate-gtk", "vesktop"), swaytools=( "flashfocus", # quick flash when changing app in focus "noisetorch", # noise cancellation "unipicker", # unicode symbol selector "wl-clip-persist", # keep clipboard after close "wlsunset", # eye saver "blueman", # bluetooth "wdisplays", # ui for display settings "wev", # debugging of ui "gtklock", # lock screen ), remotedata=("rclone", "dropbox", "minio-client"), screensharing=( "wireplumber", "xdg-desktop-portal", "xdg-desktop-portal-wlr", ), optional_nvidia=("cuda", "nvidia-settings"), optional_coolercontrol=("coolercontrol",), optional_containers=( "docker", "docker-compose", "docker-buildx", # advanced build "qemu-user-static-binfmt", # build arm64 "qemu-user-static", # build arm64 "dry-bin", # docker tui "k9s", # kubernetes tui ), optional_nvidia_containers=("nvidia-docker",), ) def compare_files(a: Path, b: Path) -> str: return "".join( unified_diff(open(a).readlines(), open(b).readlines(), str(a), str(b)) ) def helper_symlink_contents( source_folder: str | Path, target_folder: str | Path, overwrite: bool ) -> None: source_folder = Path(source_folder).expanduser() target_folder = Path(target_folder).expanduser() target_folder.mkdir(exist_ok=True) for folder in [source_folder, target_folder]: assert folder.expanduser().is_dir() for src in source_folder.iterdir(): tgt = target_folder / src.name if tgt.exists(): if src.is_dir(): if overwrite: tgt.rmdir() tgt.symlink_to(src) else: diff = "" for subsrc in src.glob("**/*"): subpath = subsrc.relative_to(source_folder) subtgt = target_folder / subpath if subtgt.exists(): diff += compare_files(subsrc, subtgt) if diff: print("DIFF:\n" + diff) if input("overwrite? (y/n) ") == "y": tgt.rmdir() tgt.symlink_to(src) else: if overwrite: tgt.unlink() tgt.symlink_to(src) else: tgt = target_folder / src.name diff = compare_files(src, tgt) if diff: print("DIFF:\n" + diff) if input("overwrite? (y/n) ") == "y": tgt.unlink() tgt.symlink_to(src) def helper_maybe_continue(path: Path, overwrite: bool) -> bool: path = path.expanduser() match path.exists(), overwrite: case False, _: return True case True, True: path.unlink() return True case True, False: return False case err: raise ValueError(err) def helper_check_if_installed(pkg: str) -> bool: return run(["yay", "-Qs", pkg], capture_output=True).returncode == 0 def helper_uninstall(*pkgs: str) -> None: for pkg in pkgs: run(["yay", "-Rns", pkg], capture_output=True) def helper_install(*pkgs: str, reinstall: bool) -> None: for pkg in pkgs: if reinstall or not helper_check_if_installed(pkg): assert run(["yay", "-S", pkg]).returncode == 0 def install_fonts(reinstall: bool) -> None: urlretrieve( "https://raw.githubusercontent.com/SUNET/static_sunet_se/refs/heads/master/fonts/Akkurat-Mono.otf", Path("~/.local/share/fonts/Akkurat-Mono.otf").expanduser(), ) helper_install(*installmap["fonts"], reinstall=reinstall) def install_nushell(overwrite: bool, reinstall: bool) -> None: helper_install(*installmap["nushell"], reinstall=reinstall) helper_symlink_contents(DOTFILES / "nushell", CFG / "nushell", overwrite) run("sudo chsh -s /usr/bin/nu".split()) url = "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/refs/heads/main/themes/peru.omp.json" run(["oh-my-posh", "init", "nu", "--config", url]) def install_tmux(overwrite: bool, reinstall: bool) -> None: helper_install(*installmap["tmux"], reinstall=reinstall) helper_symlink_contents(DOTFILES / "tmux", CFG / "tmux", overwrite) tpmpath = CFG / "tmux/plugins/tpm" if overwrite or not tpmpath.exists(): if tpmpath.exists(): tpmpath.rmdir() run(["git", "clone", "https://github.com/tmux-plugins/tpm", tpmpath]) def install_nvim(overwrite: bool, reinstall: bool) -> None: helper_install(*installmap["nvim"], reinstall=reinstall) helper_symlink_contents(DOTFILES / "nvim", CFG / "nvim", overwrite) def install_gittools(overwrite: bool, reinstall: bool) -> None: helper_install(*installmap["gittools"], reinstall=reinstall) helper_symlink_contents(DOTFILES / "tig", CFG / "tig", overwrite) gitcfgpath = Path("~/.gitconfig").expanduser() if overwrite or not gitcfgpath.exists(): if gitcfgpath.exists(): gitcfgpath.unlink() gitcfgpath.expanduser().symlink_to(DOTFILES / "HOME/.gitconfig") def install_pdftools(overwrite: bool, reinstall: bool) -> None: helper_install(*installmap["pdftools"], reinstall=reinstall) helper_symlink_contents(DOTFILES / "sioyek", CFG / "sioyek", overwrite) helper_symlink_contents(DOTFILES / "zathura", CFG / "zathura", overwrite) def install_filebrowsers(overwrite: bool, reinstall: bool) -> None: helper_install(*installmap["filebrowsers"], reinstall=reinstall) helper_symlink_contents(DOTFILES / "yazi", CFG / "yazi", overwrite) for plugin in [ "chmod", "git", "mount", "piper", "smart-enter", "smart-filter", "toggle-pane", ]: run(f"ya pkg add yazi-rs/plugins:{plugin}".split()) def install_emailcalrss(overwrite: bool, reinstall: bool) -> None: helper_install(*installmap["emailcalrss"], reinstall=reinstall) for tgt in ["vdirsyncer", "khard", "khal", "aerc", "newsboat"]: helper_symlink_contents(DOTFILES / tgt, CFG / tgt, overwrite) run(f"chmod 600 {CFG / 'aerc/accounts.conf'}".split()) def install_swaytools(overwrite: bool, reinstall: bool) -> None: helper_install(*installmap["swaytools"], reinstall=reinstall) sub = "sway/config.d" helper_symlink_contents(DOTFILES / sub, CFG / sub, overwrite) run("sudo systemctl enable --now bluetooth".split()) for sub in [ "etc/systemd/logind.conf.d/suspend.conf", "etc/systemd/sleep.conf.d/hibernate.conf", ]: run(["sudo", "cp", str(DOTFILES / "ROOT" / sub), str(Path("/") / sub)]) run("sudo systemctl enable --now bluetooth".split()) def configure_pytools(overwrite: bool) -> None: for sub in [".ipython/profile_default", ".jupyter"]: helper_symlink_contents(DOTFILES / "HOME" / sub, Path("~") / sub, overwrite) def installer( overwrite: bool = False, reinstall: bool = False, with_gpu: bool = False, with_containers: bool = False, with_coolercontrol: bool = False, ) -> None: if helper_check_if_installed("cliphist"): helper_uninstall("cliphist") print("removed cliphist") install_fonts(reinstall) print("installed fonts") install_nushell(overwrite, reinstall) print("installed nushell") install_tmux(overwrite, reinstall) print("installed tmux") install_nvim(overwrite, reinstall) print("installed nvim") helper_install(*installmap["utils"], reinstall=reinstall) print("installed utils") install_gittools(overwrite, reinstall) print("installed gittools") install_pdftools(overwrite, reinstall) print("installed pdftools") helper_install(*installmap["media"], reinstall=reinstall) print("installed media") install_filebrowsers(overwrite, reinstall) print("installed filebrowsers") helper_install(*installmap["netbrowsers"], reinstall=reinstall) print("installed netbrowsers") install_emailcalrss(overwrite, reinstall) print("installed emailcalrss") helper_install(*installmap["monitors"], reinstall=reinstall) print("installed monitors") helper_install(*installmap["apps"], reinstall=reinstall) print("installed apps") install_swaytools(overwrite, reinstall) print("installed sway") helper_install(*installmap["remotedata"], reinstall=reinstall) print("installed remotedata") helper_install(*installmap["screensharing"], reinstall=reinstall) print("installed screensharing") configure_pytools(overwrite=overwrite) print("configured python tools") if with_gpu: helper_install(*installmap["optional_nvidia"], reinstall=reinstall) print("installed nvidia") if with_containers: helper_install(*installmap["optional_containers"], reinstall=reinstall) run("sudo systemctl enable --now docker.service".split()) print("installed containers") if with_gpu and with_containers: helper_install(*installmap["optional_nvidia_containers"], reinstall=reinstall) print("installed nvidia containers") if with_coolercontrol: helper_install(*installmap["optional_coolercontrol"], reinstall=reinstall) run("sudo systemctl enable --now coolercontrold.service".split()) print("installed coolercontrol") print(""" MANUAL NEXT STEPS: set up vdirsyncer with google calendar using https://vdirsyncer.pimutils.org/en/stable/config.html#google allow firefox windowed fullscreen by setting full-screen-api.ignore-widgets to true in about:config set coolercontrold log level to WARN: `sudo systemctl edit coolercontrold.service` docker with non-root daemon `sudo groupadd docker && sudo usermod -aG docker $USER` """) if __name__ == "__main__": import defopt defopt.run(installer)