commit 913ebaab14d25ca66420a1cc41ad98a5d6621617 Author: Haylan Date: Sat Jul 4 23:48:24 2026 +0200 prepared dot files diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..096746c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d8f4b63 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,39 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this repo is + +Master dotfiles repo for a Hyprland desktop on Arch Linux. It has two halves: + +- `src/` — a small interactive TypeScript CLI (install/update/remove) that manages packages and dotfiles. +- `packages/` — the actual dotfiles, laid out as [GNU Stow](https://www.gnu.org/software/stow/) packages (one top-level dir per app, mirroring `$HOME`, e.g. `packages/waybar/.config/waybar/config`). + +The CLI is the only orchestration layer; it shells out to the real `stow`, `pacman`, and `yay` binaries rather than reimplementing symlinking or package management. + +## Commands + +``` +npm install +npm start install # interactive: choose packages, installs them, backs up conflicting files, symlinks into $HOME +npm start update # pacman -Syu + yay -Syu, then re-links (stow --restow) any new files +npm start remove # removes symlinks (stow -D), restores the most recent backup (does NOT uninstall packages) +npx tsc --noEmit # typecheck (no test suite or lint script exists) +``` + +There is no build step — `tsx` runs the TypeScript directly. + +## Architecture + +- `src/packages.ts` — the package model. Each entry has a `tier` (`required` installs with no prompt; `recommended` is offered via checkbox), an optional `group` for mutually-exclusive alternatives (e.g. `waybar` vs `ags` both have `group: "bar"`, only one gets picked via a select prompt), and an optional `dir` when the Stow directory name differs from the package name (e.g. package `rofi-wayland` → dir `rofi`). `toDirs()` converts chosen package names to their Stow directory names. +- `src/stow.ts` — all filesystem/process side effects live here: bootstrapping `yay` and oh-my-zsh/zinit if missing, installing packages via `yay`, backing up real (non-symlink) files that would collide with a Stow target into `~/.dotfiles-backup//`, and wrapping `stow`/`--restow`/`-D` (always invoked with an explicit `-d ` so behavior doesn't depend on cwd). Also reads/writes `~/.config/dotfiles/state.json`, which records which package dirs were chosen at install time — `update` and `remove` act on this list rather than re-prompting. +- `src/install.ts`, `src/update.ts`, `src/remove.ts` — the three subcommands, each a thin sequence of calls into `stow.ts`, using `@clack/prompts` for interactive selection in `install.ts`. +- `src/cli.ts` — dispatches `install|update|remove` via Node's built-in `node:util.parseArgs`. + +### Adding a new app + +Add a directory under `packages/` mirroring its `$HOME` layout (e.g. `packages/foo/.config/foo/config`), then add a matching entry to `src/packages.ts` (`dir` if the Stow directory name differs from the package name, `group` if it's a mutually-exclusive alternative to another package). + +### Backup/restore + +Backups are stored under `~/.dotfiles-backup//`, mirroring the relative path of whatever real file was moved aside before stowing. `remove` restores only the most recent backup automatically; restoring an older one is a manual `cp -a ~/.dotfiles-backup//. ~/`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..76f2a4e --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# DotFiles + +Master repo for a Hyprland desktop on Arch Linux. Dotfiles are organized as +[GNU Stow](https://www.gnu.org/software/stow/) packages (one top-level dir +per app, mirroring `$HOME`), and an interactive TypeScript CLI drives +package installation, symlinking, updates, and removal. + +## Prerequisites + +``` +sudo pacman -S --needed stow nodejs npm +``` +(`yay` is bootstrapped automatically by `install` if missing.) + +## Usage + +``` +npm install +npm start install # interactive: choose packages, installs them, backs up + # conflicting files, symlinks dotfiles into $HOME +npm start update # pacman -Syu + yay -Syu, then re-links any new files +npm start remove # removes symlinks, restores the most recent backup + # (does NOT uninstall packages) +``` + +Backups of any pre-existing files that would have collided with a symlink +are stored at `~/.dotfiles-backup//`. `remove` restores the most +recent one automatically; to restore an older one manually: + +``` +cp -a ~/.dotfiles-backup//. ~/ +``` + +Which packages/dirs were chosen at install time is recorded in +`~/.config/dotfiles/state.json`, so `update`/`remove` know what to act on +without re-prompting. + +## Wallpaper + +Default wallpaper by rahremix (via github.com/PsychedelicPalimpsest/dotfiles). + +## Adding a new app + +Add a directory under `packages/` mirroring its `$HOME` layout (e.g. +`packages/foo/.config/foo/config`), and add an entry to `src/packages.ts` with the +matching package `name` (and `dir` if it differs, or a `group` if it's a +mutually-exclusive alternative to another package). + +## Testing in a container + +`./docker/test.sh` runs a disposable Arch container with `stow`/`nodejs`/`npm` +preinstalled and the repo copied in, without touching the host: + +``` +./docker/test.sh # interactive shell, deps installed +./docker/test.sh "npx tsc --noEmit" # run one command non-interactively +``` diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..a31927b --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,8 @@ +FROM archlinux:base-devel + +RUN pacman -Sy --noconfirm stow nodejs npm git sudo \ + && useradd -m tester \ + && echo 'tester ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers + +USER tester +WORKDIR /home/tester diff --git a/docker/test.sh b/docker/test.sh new file mode 100755 index 0000000..1527c0e --- /dev/null +++ b/docker/test.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Disposable Arch container for testing the installer without touching the host. +# Usage: +# ./docker/test.sh # interactive shell, deps installed +# ./docker/test.sh "npx tsc --noEmit" # run one command non-interactively +set -euo pipefail +cd "$(dirname "$0")/.." + +docker build -t dotfiles-test -f docker/Dockerfile . + +tty_flags=(-i) +[ -t 0 ] && [ -t 1 ] && tty_flags+=(-t) + +docker run --rm "${tty_flags[@]}" -v "$PWD":/src:ro dotfiles-test \ + bash -lc 'cp -r /src ~/DotFiles && cd ~/DotFiles && npm install && exec bash -c "$1"' -- "${1:-bash}" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a9bd345 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,600 @@ +{ + "name": "dotfiles", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dotfiles", + "dependencies": { + "@clack/prompts": "^0.9.0" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } + }, + "node_modules/@clack/core": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.4.1.tgz", + "integrity": "sha512-Pxhij4UXg8KSr7rPek6Zowm+5M22rbd2g1nfojHJkxp5YkFqiZ2+YLEM/XGVIzvGOcM0nqjIFxrpDwWRZYWYjA==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.9.1.tgz", + "integrity": "sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==", + "license": "MIT", + "dependencies": { + "@clack/core": "0.4.1", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz", + "integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5f2e957 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "dotfiles", + "private": true, + "type": "module", + "scripts": { + "start": "tsx src/cli.ts" + }, + "dependencies": { + "@clack/prompts": "^0.9.0" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + } +} diff --git a/packages/ags/.config/ags/config.js b/packages/ags/.config/ags/config.js new file mode 100644 index 0000000..17b976c --- /dev/null +++ b/packages/ags/.config/ags/config.js @@ -0,0 +1,7 @@ +// ponytail: placeholder ags config, flesh out if ags is chosen over waybar +import App from "resource:///com/github/Aylur/ags/app.js"; + +App.config({ + style: "", + windows: [], +}); diff --git a/packages/foot/.config/foot/foot.ini b/packages/foot/.config/foot/foot.ini new file mode 100644 index 0000000..d12f88f --- /dev/null +++ b/packages/foot/.config/foot/foot.ini @@ -0,0 +1,5 @@ +[main] +font=monospace:size=11 + +[colors] +alpha=0.95 diff --git a/packages/hyprland/.config/hypr/hyprland.conf b/packages/hyprland/.config/hypr/hyprland.conf new file mode 100644 index 0000000..ee55876 --- /dev/null +++ b/packages/hyprland/.config/hypr/hyprland.conf @@ -0,0 +1,22 @@ +monitor=,preferred,auto,1 + +exec-once = waybar +exec-once = mako +exec-once = hyprpaper + +$mainMod = SUPER + +bind = $mainMod, RETURN, exec, kitty +bind = $mainMod, D, exec, wofi --show drun +bind = $mainMod, Q, killactive +bind = $mainMod, M, exit + +input { + kb_layout = us +} + +general { + gaps_in = 4 + gaps_out = 8 + border_size = 2 +} diff --git a/packages/hyprpaper/.config/hypr/hyprpaper.conf b/packages/hyprpaper/.config/hypr/hyprpaper.conf new file mode 100644 index 0000000..07e7c7c --- /dev/null +++ b/packages/hyprpaper/.config/hypr/hyprpaper.conf @@ -0,0 +1,3 @@ +splash = false +preload = ~/.config/hypr/wallpapers/wallpaper.png +wallpaper = ,~/.config/hypr/wallpapers/wallpaper.png diff --git a/packages/hyprpaper/.config/hypr/wallpapers/wallpaper.png b/packages/hyprpaper/.config/hypr/wallpapers/wallpaper.png new file mode 100644 index 0000000..423497b Binary files /dev/null and b/packages/hyprpaper/.config/hypr/wallpapers/wallpaper.png differ diff --git a/packages/kitty/.config/kitty/kitty.conf b/packages/kitty/.config/kitty/kitty.conf new file mode 100644 index 0000000..bdb1999 --- /dev/null +++ b/packages/kitty/.config/kitty/kitty.conf @@ -0,0 +1,4 @@ +font_family monospace +font_size 11.0 +background_opacity 0.95 +confirm_os_window_close 0 diff --git a/packages/mako/.config/mako/config b/packages/mako/.config/mako/config new file mode 100644 index 0000000..fd39a05 --- /dev/null +++ b/packages/mako/.config/mako/config @@ -0,0 +1,5 @@ +font=monospace 11 +background-color=#1e1e2e +text-color=#cdd6f4 +border-radius=6 +default-timeout=5000 diff --git a/packages/rofi/.config/rofi/config.rasi b/packages/rofi/.config/rofi/config.rasi new file mode 100644 index 0000000..67dd495 --- /dev/null +++ b/packages/rofi/.config/rofi/config.rasi @@ -0,0 +1,5 @@ +configuration { + modi: "drun,run"; + show-icons: true; + display-drun: "Apps"; +} diff --git a/packages/waybar/.config/waybar/config b/packages/waybar/.config/waybar/config new file mode 100644 index 0000000..228d9c4 --- /dev/null +++ b/packages/waybar/.config/waybar/config @@ -0,0 +1,7 @@ +{ + "layer": "top", + "position": "top", + "modules-left": ["hyprland/workspaces"], + "modules-center": ["clock"], + "modules-right": ["pulseaudio", "network", "battery"] +} diff --git a/packages/waybar/.config/waybar/style.css b/packages/waybar/.config/waybar/style.css new file mode 100644 index 0000000..1bdccc9 --- /dev/null +++ b/packages/waybar/.config/waybar/style.css @@ -0,0 +1,9 @@ +* { + font-family: monospace; + font-size: 13px; +} + +window#waybar { + background: rgba(30, 30, 46, 0.9); + color: #cdd6f4; +} diff --git a/packages/wofi/.config/wofi/config b/packages/wofi/.config/wofi/config new file mode 100644 index 0000000..98c7cb9 --- /dev/null +++ b/packages/wofi/.config/wofi/config @@ -0,0 +1,5 @@ +width=600 +height=400 +location=center +show=drun +prompt=Search... diff --git a/packages/zsh/.zshrc b/packages/zsh/.zshrc new file mode 100644 index 0000000..8269b52 --- /dev/null +++ b/packages/zsh/.zshrc @@ -0,0 +1,11 @@ +export ZSH="$HOME/.oh-my-zsh" +ZSH_THEME="robbyrussell" +plugins=(git) +source "$ZSH/oh-my-zsh.sh" + +# zinit +ZINIT_HOME="$HOME/.local/share/zinit/zinit.git" +source "$ZINIT_HOME/zinit.zsh" + +zinit light zsh-users/zsh-autosuggestions +zinit light zsh-users/zsh-syntax-highlighting diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..40ff935 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,22 @@ +import { parseArgs } from "node:util"; +import { install } from "./install.js"; +import { update } from "./update.js"; +import { remove } from "./remove.js"; + +const { positionals } = parseArgs({ allowPositionals: true }); +const [command] = positionals; + +switch (command) { + case "install": + await install(); + break; + case "update": + await update(); + break; + case "remove": + await remove(); + break; + default: + console.error("Usage: dotfiles "); + process.exit(1); +} diff --git a/src/install.ts b/src/install.ts new file mode 100644 index 0000000..9880cb4 --- /dev/null +++ b/src/install.ts @@ -0,0 +1,51 @@ +import * as p from "@clack/prompts"; +import { groups, packages, toDirs } from "./packages.js"; +import { + backupConflicts, + ensureYay, + ensureZsh, + installPackages, + stow, + writeState, +} from "./stow.js"; + +export async function install() { + p.intro("Hyprland dotfiles install"); + + ensureYay(); + + const chosen = new Set(packages.filter((pkg) => pkg.tier === "required").map((pkg) => pkg.name)); + + for (const group of groups) { + const options = packages.filter((pkg) => pkg.group === group); + const pick = await p.select({ + message: `Choose a ${group}`, + options: options.map((pkg) => ({ value: pkg.name, label: pkg.name })), + }); + if (p.isCancel(pick)) return p.cancel("Install cancelled."); + chosen.add(pick as string); + } + + const ungrouped = packages.filter((pkg) => pkg.tier === "recommended" && !pkg.group); + const picks = await p.multiselect({ + message: "Choose recommended packages", + options: ungrouped.map((pkg) => ({ value: pkg.name, label: pkg.name })), + initialValues: ungrouped.map((pkg) => pkg.name), + required: false, + }); + if (p.isCancel(picks)) return p.cancel("Install cancelled."); + for (const name of picks as string[]) chosen.add(name); + + installPackages([...chosen]); + + if (chosen.has("zsh")) ensureZsh(); + + const dirs = toDirs([...chosen]); + const backupDir = backupConflicts(dirs); + if (backupDir) p.log.info(`Backed up conflicting files to ${backupDir}`); + + stow(dirs); + writeState({ dirs }); + + p.outro("Done."); +} diff --git a/src/packages.ts b/src/packages.ts new file mode 100644 index 0000000..1faf3d4 --- /dev/null +++ b/src/packages.ts @@ -0,0 +1,31 @@ +export type Pkg = { + name: string; + /** Stow directory name, if it differs from the package name. */ + dir?: string; + aur?: boolean; + tier: "required" | "recommended"; + group?: string; +}; + +export const packages: Pkg[] = [ + { name: "hyprland", tier: "required" }, + { name: "xdg-desktop-portal-hyprland", tier: "required" }, + { name: "waybar", tier: "recommended", group: "bar" }, + { name: "ags", tier: "recommended", group: "bar", aur: true }, + { name: "wofi", tier: "recommended", group: "launcher" }, + { name: "rofi-wayland", dir: "rofi", tier: "recommended", group: "launcher", aur: true }, + { name: "kitty", tier: "recommended" }, + { name: "foot", tier: "recommended" }, + { name: "mako", tier: "recommended" }, + { name: "hyprpaper", tier: "recommended" }, + { name: "zsh", tier: "recommended" }, +]; + +export const groups = [...new Set(packages.filter((p) => p.group).map((p) => p.group!))]; + +const byName = new Map(packages.map((pkg) => [pkg.name, pkg])); + +/** Maps chosen package names to their stow directory names (only packages that have one). */ +export function toDirs(names: string[]): string[] { + return names.map((name) => byName.get(name)?.dir ?? name); +} diff --git a/src/remove.ts b/src/remove.ts new file mode 100644 index 0000000..d5de18d --- /dev/null +++ b/src/remove.ts @@ -0,0 +1,10 @@ +import * as p from "@clack/prompts"; +import { readState, restoreLatestBackup, unstow } from "./stow.js"; + +export async function remove() { + p.intro("Hyprland dotfiles remove"); + unstow(readState().dirs); + const restored = restoreLatestBackup(); + p.log.info(restored ? `Restored backup from ${restored}` : "No backup found; nothing to restore."); + p.outro("Done."); +} diff --git a/src/stow.ts b/src/stow.ts new file mode 100644 index 0000000..b562d88 --- /dev/null +++ b/src/stow.ts @@ -0,0 +1,148 @@ +import { execFileSync } from "node:child_process"; +import { + existsSync, + lstatSync, + mkdirSync, + readdirSync, + readFileSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +const REPO_ROOT = new URL("..", import.meta.url).pathname; +const PACKAGES_DIR = join(REPO_ROOT, "packages"); +const STATE_DIR = join(homedir(), ".config", "dotfiles"); +const STATE_FILE = join(STATE_DIR, "state.json"); +const BACKUP_ROOT = join(homedir(), ".dotfiles-backup"); + +function run(cmd: string, args: string[]) { + execFileSync(cmd, args, { stdio: "inherit" }); +} + +function commandExists(cmd: string): boolean { + try { + execFileSync("which", [cmd], { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +export function ensureYay() { + if (commandExists("yay")) return; + run("sudo", ["pacman", "-S", "--needed", "--noconfirm", "git", "base-devel"]); + const tmp = "/tmp/yay-bootstrap"; + rmSync(tmp, { recursive: true, force: true }); + run("git", ["clone", "https://aur.archlinux.org/yay.git", tmp]); + execFileSync("makepkg", ["-si", "--noconfirm"], { cwd: tmp, stdio: "inherit" }); + rmSync(tmp, { recursive: true, force: true }); +} + +export function installPackages(names: string[]) { + if (names.length === 0) return; + run("yay", ["-S", "--needed", "--noconfirm", ...names]); +} + +export function ensureZsh() { + const omzDir = join(homedir(), ".oh-my-zsh"); + if (!existsSync(omzDir)) { + run("sh", [ + "-c", + 'sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended', + ]); + } + const zinitDir = join(homedir(), ".local", "share", "zinit", "zinit.git"); + if (!existsSync(zinitDir)) { + run("sh", [ + "-c", + 'sh -c "$(curl -fsSL https://raw.githubusercontent.com/zdharma-continuum/zinit/HEAD/scripts/install.sh)"', + ]); + } +} + +// Move real (non-symlink) files that would collide with a stow target out of the way. +export function backupConflicts(dirs: string[]): string | null { + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const backupDir = join(BACKUP_ROOT, timestamp); + let backedUpAnything = false; + + function walk(pkgDir: string, current: string) { + for (const entry of readdirSync(current, { withFileTypes: true })) { + const src = join(current, entry.name); + if (entry.isDirectory()) { + walk(pkgDir, src); + continue; + } + const relative = src.slice(pkgDir.length + 1); + const target = join(homedir(), relative); + if (existsSync(target) && !isSymlink(target)) { + const dest = join(backupDir, relative); + mkdirSync(join(dest, ".."), { recursive: true }); + renameSync(target, dest); + backedUpAnything = true; + } + } + } + + for (const dir of dirs) { + const pkgDir = join(PACKAGES_DIR, dir); + if (existsSync(pkgDir)) walk(pkgDir, pkgDir); + } + + return backedUpAnything ? backupDir : null; +} + +function isSymlink(path: string): boolean { + try { + return lstatSync(path).isSymbolicLink(); + } catch { + return false; + } +} + +function existingDirs(dirs: string[]): string[] { + return dirs.filter((dir) => existsSync(join(PACKAGES_DIR, dir))); +} + +export function stow(dirs: string[]) { + const existing = existingDirs(dirs); + if (existing.length === 0) return; + run("stow", ["-v", "-d", PACKAGES_DIR, "-t", homedir(), ...existing]); +} + +export function restow(dirs: string[]) { + const existing = existingDirs(dirs); + if (existing.length === 0) return; + run("stow", ["-v", "--restow", "-d", PACKAGES_DIR, "-t", homedir(), ...existing]); +} + +export function unstow(dirs: string[]) { + const existing = existingDirs(dirs); + if (existing.length === 0) return; + run("stow", ["-v", "-D", "-d", PACKAGES_DIR, "-t", homedir(), ...existing]); +} + +export function restoreLatestBackup() { + if (!existsSync(BACKUP_ROOT)) return null; + const backups = readdirSync(BACKUP_ROOT).sort(); + const latest = backups.at(-1); + if (!latest) return null; + const latestDir = join(BACKUP_ROOT, latest); + run("cp", ["-a", `${latestDir}/.`, homedir()]); + return latestDir; +} + +export type State = { dirs: string[] }; + +export function writeState(state: State) { + mkdirSync(STATE_DIR, { recursive: true }); + writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); +} + +export function readState(): State { + if (!existsSync(STATE_FILE)) return { dirs: [] }; + return JSON.parse(readFileSync(STATE_FILE, "utf8")); +} diff --git a/src/update.ts b/src/update.ts new file mode 100644 index 0000000..f35c216 --- /dev/null +++ b/src/update.ts @@ -0,0 +1,11 @@ +import { execFileSync } from "node:child_process"; +import * as p from "@clack/prompts"; +import { readState, restow } from "./stow.js"; + +export async function update() { + p.intro("Hyprland dotfiles update"); + execFileSync("sudo", ["pacman", "-Syu"], { stdio: "inherit" }); + execFileSync("yay", ["-Syu"], { stdio: "inherit" }); + restow(readState().dirs); + p.outro("Done."); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2502c8f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src"] +}