diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index e2a3720..0a02d0e 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -14,12 +14,12 @@ jobs: - name: ShellCheck run: | - shellcheck install-scripts/*.sh config/tmux/scripts/*.sh scripts/* + shellcheck -x install-scripts/*.sh install-scripts/lib/*.sh config/sets/basic/tmux/scripts/*.sh scripts/* - name: Fish syntax check run: | sudo apt-get update && sudo apt-get install -y fish - for f in config/fish/*.fish install-scripts/*.fish; do + for f in config/sets/basic/fish/*.fish install-scripts/*.fish; do fish --no-execute "$f" done diff --git a/Makefile b/Makefile index d4367c7..86f3a0b 100644 --- a/Makefile +++ b/Makefile @@ -12,4 +12,15 @@ refresh: @bash ./install-scripts/02-move-files.sh @exec fish -.PHONY: full-install refresh +picky: + @bash ./install-scripts/pick-sets.sh + @bash ./install-scripts/01-install-packages.sh + @bash ./install-scripts/02-move-files.sh + @fish ./install-scripts/03-fisher-install.fish + @fish ./install-scripts/04-fish-plugins.fish + @fish ./install-scripts/05-tmux-plugins.fish + @fish ./install-scripts/06-vim-setup.fish + @bash ./install-scripts/07-last-touches.sh + @exec fish + +.PHONY: full-install refresh picky diff --git a/README.md b/README.md index c25c938..fd5cda0 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ A comprehensive dotfiles setup for a modern development environment featuring Ne - **Fish Shell**: Lightweight `simple.fish` prompt, cross-machine history sync, aliases, and productivity functions - **Tmux**: Custom keybindings, mouse support, and a modular status bar with toggleable gadgets - **Cross-platform**: Supports macOS, Linux (Arch, Debian/Ubuntu, Alpine, Fedora/RHEL, Gentoo), and FreeBSD +- **Config Sets**: Pick and choose which groups of config get deployed via an interactive `make picky` dialog - **Automated Setup**: One-command installation via Makefile ## Quick Install @@ -24,27 +25,109 @@ To re-sync just the config file symlinks (without reinstalling packages or plugi make refresh ``` +To interactively choose which config sets get deployed, then run the full install using that selection: + +```bash +make picky +``` + ## Repository Layout ``` config/ - vim/init.lua # Neovim configuration - fish/config.fish # Fish entrypoint (greeting + $EDITOR) - fish/aliases.fish # Shell aliases (deployed to conf.d/) - fish/functions.fish # Shell functions (deployed to conf.d/) - fish/envvars.fish # Environment variables (copied, per-machine) - tmux/tmux.conf # Tmux configuration - tmux/scripts/status.sh # Modular status bar renderer - tmux/scripts/status.conf # Status gadget list (copied, per-machine) - tmux/scripts/bluetooth-menu.sh # Bluetooth popup menu + sets/ + basic/ # The default config set (today's dotfiles) + description # One-line blurb shown by `make picky` + manifest # Declares what gets deployed and where + vim/init.lua # Neovim configuration + fish/config.fish # Fish entrypoint (greeting + $EDITOR) + fish/aliases.fish # Shell aliases (deployed to conf.d/) + fish/functions.fish # Shell functions (deployed to conf.d/) + fish/envvars.fish # Environment variables (copied, per-machine) + tmux/tmux.conf # Tmux configuration + tmux/scripts/status.sh # Modular status bar renderer + tmux/scripts/status.conf # Status gadget list (copied, per-machine) + tmux/scripts/bluetooth-menu.sh # Bluetooth popup menu + kde/ # KDE Plasma keyboard repeat settings + description + manifest + os # "Linux" — hidden from `make picky` elsewhere + apply.sh + macos/ # macOS keyboard repeat settings + description + manifest + os # "Darwin" — hidden from `make picky` elsewhere + apply.sh + # further sets live alongside these, following the same layout install-scripts/ # Numbered setup scripts run by the Makefile + lib/sets.sh # Shared helpers for discovering/enabling sets + pick-sets.sh # `make picky` — the dialog checklist scripts/ # Misc helper scripts (e.g. Gentoo kernel upgrade) tests/ # Dockerfiles used by CI to test installs per distro ``` +## Config Sets & `make picky` + +Config files are grouped into **sets** under `config/sets//`: + +- **`basic`** — everything this repo has always deployed (Neovim, Fish, Tmux) +- **`kde`** — sets the KDE Plasma keyboard repeat rate to 50/s with a 250ms + delay (via `kwriteconfig5`/`6` on `kcminputrc`) +- **`macos`** — sets the macOS keyboard repeat rate as fast as possible with + the shortest delay (via `defaults write`) + +Each set is independent and additive — enabling `kde` or `macos` doesn't +disturb `basic`. `kde` and `macos` each declare an `os` file restricting +them to their platform, so `make picky` only offers `kde` on Linux and +`macos` on macOS in the first place; if one somehow ends up enabled on the +wrong OS anyway (e.g. a shared `sets.conf`), it's skipped with a warning +at deploy time instead of failing. + +Run `make picky` to get an interactive checklist (space to toggle, enter to +confirm) of every set found under `config/sets/`. Confirming saves your +selection to `~/.config/dotfiles/sets.conf` and immediately kicks off the +full install (packages, plugins, config deployment, last touches) using it +— the same steps `make full-install` runs. Cancelling (Esc) leaves your +existing selection untouched and stops there, without installing anything. +The saved selection is remembered by future `make refresh` / `make +full-install` runs too, so you only need to pick once per machine. If +you've never run `make picky`, everything defaults to just `basic`, +matching this repo's historical behavior. + +### Adding a new set + +1. Create `config/sets//`. +2. Add a `manifest` file listing what to deploy, one entry per line: + ``` + link some/file ~/.config/some/file + copy some/per-machine ~/.config/some/per-machine + link-glob scripts/*.sh ~/.local/bin/ + run apply.sh + ``` + - `link` symlinks (so edits at the destination flow back into the repo) + - `copy` copies only if the destination doesn't already exist (for + per-machine files you don't want overwritten on re-runs) + - `link-glob` symlinks every file matching a glob into a destination + directory + - `run` executes a script instead of deploying a file — for settings that + aren't dotfiles, like the KDE/macOS keyboard repeat rate. It should + exit 0 even when it can't apply anything (wrong OS/DE), so other + enabled sets still get deployed; see `config/sets/kde/apply.sh` for an + example + - Paths are relative to the set's own directory; destinations may use `~` +3. Optionally add a one-line `description` file — shown next to the set's + name in the `make picky` checklist. +4. Optionally add an `os` file if the set only makes sense on certain + platforms — one `uname -s` value per line (e.g. `Darwin`, `Linux`). Sets + with an `os` file are hidden from `make picky` on any other OS; sets + without one are offered everywhere. See `config/sets/macos/os` / + `config/sets/kde/os` for examples. + ## Config Files Overview -### `config/vim/init.lua` - Neovim Configuration +The `basic` set is deployed by default; see [Config Sets & `make picky`](#config-sets--make-picky) above for how to add or select other sets. + +### `config/sets/basic/vim/init.lua` - Neovim Configuration - **Deployed to**: `~/.config/nvim/init.lua` (symlinked) - **Purpose**: Complete Neovim setup with modern IDE-like features - **Features**: @@ -56,14 +139,14 @@ tests/ # Dockerfiles used by CI to test installs per distro - Syntax highlighting with Treesitter - Custom keybindings and the carbonfox colorscheme (nightfox.nvim) -### `config/fish/config.fish` - Fish Shell Entrypoint +### `config/sets/basic/fish/config.fish` - Fish Shell Entrypoint - **Deployed to**: `~/.config/fish/config.fish` (symlinked) - **Purpose**: Minimal Fish entrypoint - **Features**: - Silences the welcome greeting - Sets `$EDITOR` to `nvim` -### `config/fish/aliases.fish` - Fish Aliases +### `config/sets/basic/fish/aliases.fish` - Fish Aliases - **Deployed to**: `~/.config/fish/conf.d/aliases.fish` (symlinked) - **Purpose**: Command aliases loaded automatically by Fish - **Features**: @@ -72,7 +155,7 @@ tests/ # Dockerfiles used by CI to test installs per distro - Kubernetes shortcuts (`k`, `kp`, `kc`) - Gentoo and PipeWire volume helpers -### `config/fish/functions.fish` - Fish Functions +### `config/sets/basic/fish/functions.fish` - Fish Functions - **Deployed to**: `~/.config/fish/conf.d/functions.fish` (symlinked) - **Purpose**: Custom shell functions - **Features**: @@ -80,12 +163,12 @@ tests/ # Dockerfiles used by CI to test installs per distro - `cheat` lookup against cht.sh - `gitissue` helper to branch off a fresh `master` -### `config/fish/envvars.fish` - Environment Variables +### `config/sets/basic/fish/envvars.fish` - Environment Variables - **Deployed to**: `~/.config/fish/conf.d/envvars.fish` (copied, not symlinked) - **Purpose**: Per-machine environment variable definitions - **Note**: Only copied if it doesn't already exist, so local customizations are preserved -### `config/tmux/tmux.conf` - Tmux Configuration +### `config/sets/basic/tmux/tmux.conf` - Tmux Configuration - **Deployed to**: `~/.tmux.conf` (symlinked) - **Purpose**: Tmux terminal multiplexer configuration - **Features**: @@ -97,7 +180,7 @@ tests/ # Dockerfiles used by CI to test installs per distro - Bluetooth popup menu bound to `prefix + b` - Plugin management with TPM (tpm, tmux-better-mouse-mode) -### `config/tmux/scripts/` - Tmux Status Scripts +### `config/sets/basic/tmux/scripts/` - Tmux Status Scripts - **Deployed to**: `~/.tmux/scripts/` (`.sh` symlinked, `status.conf` copied) - **Purpose**: Render the status bar and power the Bluetooth menu - **Features**: @@ -121,12 +204,13 @@ Installs essential packages based on your OS: - **Gentoo**: Uses emerge (`git` → `dev-vcs/git`) ### 2. File Deployment (`02-move-files.sh`) -Symlinks configuration files to their proper locations: -- Creates necessary directories (`~/.config/nvim`, `~/.config/fish`, `~/.tmux/scripts`, etc.) -- Symlinks tracked config files into place, so edits to the live config flow - straight back to the repo -- Copies (rather than symlinks) `envvars.fish` and `status.conf` so per-machine - customizations are preserved, and only if they don't already exist +Deploys each enabled config set (`basic` by default, or whatever was chosen +via `make picky`) by walking its `manifest`: +- Creates any destination directories on the fly +- Symlinks most files into place, so edits to the live config flow straight + back to the repo +- Copies (rather than symlinks) per-machine files like `envvars.fish` and + `status.conf`, and only if they don't already exist ### 3. Fisher Installation (`03-fisher-install.fish`) Installs Fisher, the Fish shell plugin manager @@ -166,20 +250,20 @@ If you prefer manual installation or want to customize the process: ```bash # Neovim mkdir -p ~/.config/nvim - ln -sf "$PWD/config/vim/init.lua" ~/.config/nvim/init.lua + ln -sf "$PWD/config/sets/basic/vim/init.lua" ~/.config/nvim/init.lua # Fish mkdir -p ~/.config/fish/conf.d - ln -sf "$PWD/config/fish/config.fish" ~/.config/fish/config.fish - ln -sf "$PWD/config/fish/aliases.fish" ~/.config/fish/conf.d/aliases.fish - ln -sf "$PWD/config/fish/functions.fish" ~/.config/fish/conf.d/functions.fish - cp "$PWD/config/fish/envvars.fish" ~/.config/fish/conf.d/ + ln -sf "$PWD/config/sets/basic/fish/config.fish" ~/.config/fish/config.fish + ln -sf "$PWD/config/sets/basic/fish/aliases.fish" ~/.config/fish/conf.d/aliases.fish + ln -sf "$PWD/config/sets/basic/fish/functions.fish" ~/.config/fish/conf.d/functions.fish + cp "$PWD/config/sets/basic/fish/envvars.fish" ~/.config/fish/conf.d/ # Tmux - ln -sf "$PWD/config/tmux/tmux.conf" ~/.tmux.conf + ln -sf "$PWD/config/sets/basic/tmux/tmux.conf" ~/.tmux.conf mkdir -p ~/.tmux/scripts - ln -sf "$PWD"/config/tmux/scripts/*.sh ~/.tmux/scripts/ - cp "$PWD/config/tmux/scripts/status.conf" ~/.tmux/scripts/ + ln -sf "$PWD"/config/sets/basic/tmux/scripts/*.sh ~/.tmux/scripts/ + cp "$PWD/config/sets/basic/tmux/scripts/status.conf" ~/.tmux/scripts/ ``` 3. **Run individual scripts**: Execute scripts in `install-scripts/` directory in order @@ -200,10 +284,10 @@ After installation: ## Customization All configuration files are designed to be easily customizable: -- **Add Fish aliases**: Edit `config/fish/aliases.fish` -- **Add Fish functions**: Edit `config/fish/functions.fish` -- **Modify Neovim plugins**: Edit `config/vim/init.lua` -- **Change Tmux keybindings**: Edit `config/tmux/tmux.conf` +- **Add Fish aliases**: Edit `config/sets/basic/fish/aliases.fish` +- **Add Fish functions**: Edit `config/sets/basic/fish/functions.fish` +- **Modify Neovim plugins**: Edit `config/sets/basic/vim/init.lua` +- **Change Tmux keybindings**: Edit `config/sets/basic/tmux/tmux.conf` - **Toggle status bar gadgets**: Edit `~/.tmux/scripts/status.conf` - **Add environment variables**: Edit `~/.config/fish/conf.d/envvars.fish` diff --git a/config/sets/basic/description b/config/sets/basic/description new file mode 100644 index 0000000..064cb6a --- /dev/null +++ b/config/sets/basic/description @@ -0,0 +1 @@ +Core setup: Neovim, Fish shell, and Tmux (today's default dotfiles) diff --git a/config/fish/aliases.fish b/config/sets/basic/fish/aliases.fish similarity index 100% rename from config/fish/aliases.fish rename to config/sets/basic/fish/aliases.fish diff --git a/config/fish/config.fish b/config/sets/basic/fish/config.fish similarity index 100% rename from config/fish/config.fish rename to config/sets/basic/fish/config.fish diff --git a/config/fish/envvars.fish b/config/sets/basic/fish/envvars.fish similarity index 100% rename from config/fish/envvars.fish rename to config/sets/basic/fish/envvars.fish diff --git a/config/fish/functions.fish b/config/sets/basic/fish/functions.fish similarity index 100% rename from config/fish/functions.fish rename to config/sets/basic/fish/functions.fish diff --git a/config/sets/basic/manifest b/config/sets/basic/manifest new file mode 100644 index 0000000..c98fa62 --- /dev/null +++ b/config/sets/basic/manifest @@ -0,0 +1,26 @@ +# Deployment manifest for the "basic" config set. +# Read by install-scripts/02-move-files.sh for every enabled set. +# +# Format: +# src is relative to this set's own directory. +# dest may use ~ for $HOME; its parent directory is created automatically. +# +# Actions: +# link symlink src into dest, backing up any pre-existing real file +# to dest.bak first (so edits at dest flow back into the repo) +# copy copy src to dest only if dest doesn't already exist (keeps +# per-machine customizations across re-runs) +# link-glob symlink every file matching the src glob into the dest +# directory, keeping each file's basename +# run execute src (relative to this set's dir) as a script; dest is +# unused. Should exit 0 even when it can't apply anything (e.g. +# wrong OS/DE) so other enabled sets still get deployed. + +link vim/init.lua ~/.config/nvim/init.lua +link fish/config.fish ~/.config/fish/config.fish +link fish/aliases.fish ~/.config/fish/conf.d/aliases.fish +link fish/functions.fish ~/.config/fish/conf.d/functions.fish +copy fish/envvars.fish ~/.config/fish/conf.d/envvars.fish +link tmux/tmux.conf ~/.tmux.conf +copy tmux/scripts/status.conf ~/.tmux/scripts/status.conf +link-glob tmux/scripts/*.sh ~/.tmux/scripts/ diff --git a/config/tmux/scripts/bluetooth-menu.sh b/config/sets/basic/tmux/scripts/bluetooth-menu.sh similarity index 100% rename from config/tmux/scripts/bluetooth-menu.sh rename to config/sets/basic/tmux/scripts/bluetooth-menu.sh diff --git a/config/tmux/scripts/status.conf b/config/sets/basic/tmux/scripts/status.conf similarity index 100% rename from config/tmux/scripts/status.conf rename to config/sets/basic/tmux/scripts/status.conf diff --git a/config/tmux/scripts/status.sh b/config/sets/basic/tmux/scripts/status.sh similarity index 100% rename from config/tmux/scripts/status.sh rename to config/sets/basic/tmux/scripts/status.sh diff --git a/config/tmux/tmux.conf b/config/sets/basic/tmux/tmux.conf similarity index 100% rename from config/tmux/tmux.conf rename to config/sets/basic/tmux/tmux.conf diff --git a/config/vim/init.lua b/config/sets/basic/vim/init.lua similarity index 100% rename from config/vim/init.lua rename to config/sets/basic/vim/init.lua diff --git a/config/sets/kde/apply.sh b/config/sets/kde/apply.sh new file mode 100755 index 0000000..2938cb9 --- /dev/null +++ b/config/sets/kde/apply.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -euo pipefail + +# Sets the KDE Plasma keyboard repeat rate to 50 repeats/sec with a 250ms +# initial delay (System Settings > Keyboard > Advanced), via kcminputrc. + +kwriteconfig="" +for candidate in kwriteconfig6 kwriteconfig5; do + if command -v "$candidate" >/dev/null 2>&1; then + kwriteconfig="$candidate" + break + fi +done + +if [ -z "$kwriteconfig" ]; then + echo "Warning: kwriteconfig5/6 not found (not a KDE Plasma system?); skipping keyboard repeat settings." >&2 + exit 0 +fi + +"$kwriteconfig" --file kcminputrc --group Keyboard --key RepeatDelay 250 +"$kwriteconfig" --file kcminputrc --group Keyboard --key RepeatRate 50 + +# Apply immediately in the current X11 session; a fresh Plasma login also +# re-applies it from kcminputrc either way. +if [ -n "${DISPLAY:-}" ] && command -v xset >/dev/null 2>&1; then + xset r rate 250 50 || true +fi diff --git a/config/sets/kde/description b/config/sets/kde/description new file mode 100644 index 0000000..518ef50 --- /dev/null +++ b/config/sets/kde/description @@ -0,0 +1 @@ +KDE Plasma: keyboard repeat rate 50/s, 250ms delay diff --git a/config/sets/kde/manifest b/config/sets/kde/manifest new file mode 100644 index 0000000..5a7828e --- /dev/null +++ b/config/sets/kde/manifest @@ -0,0 +1,3 @@ +# See config/sets/basic/manifest for the full manifest format reference. + +run apply.sh diff --git a/config/sets/kde/os b/config/sets/kde/os new file mode 100644 index 0000000..9b07567 --- /dev/null +++ b/config/sets/kde/os @@ -0,0 +1 @@ +Linux diff --git a/config/sets/macos/apply.sh b/config/sets/macos/apply.sh new file mode 100755 index 0000000..844e00a --- /dev/null +++ b/config/sets/macos/apply.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -euo pipefail + +# Sets the macOS keyboard repeat rate as fast as possible and the initial +# repeat delay as short as possible — below the range the System Settings +# slider exposes. Log out/in (or restart affected apps) for it to fully +# take effect everywhere. + +if [ "$(uname)" != "Darwin" ] || ! command -v defaults >/dev/null 2>&1; then + echo "Warning: not macOS (or 'defaults' unavailable); skipping keyboard repeat settings." >&2 + exit 0 +fi + +defaults write NSGlobalDomain KeyRepeat -int 1 +defaults write NSGlobalDomain InitialKeyRepeat -int 10 diff --git a/config/sets/macos/description b/config/sets/macos/description new file mode 100644 index 0000000..ebac978 --- /dev/null +++ b/config/sets/macos/description @@ -0,0 +1 @@ +macOS: fastest keyboard repeat rate, shortest delay diff --git a/config/sets/macos/manifest b/config/sets/macos/manifest new file mode 100644 index 0000000..5a7828e --- /dev/null +++ b/config/sets/macos/manifest @@ -0,0 +1,3 @@ +# See config/sets/basic/manifest for the full manifest format reference. + +run apply.sh diff --git a/config/sets/macos/os b/config/sets/macos/os new file mode 100644 index 0000000..b81c54f --- /dev/null +++ b/config/sets/macos/os @@ -0,0 +1 @@ +Darwin diff --git a/install-scripts/02-move-files.sh b/install-scripts/02-move-files.sh index f3f8144..d99164d 100755 --- a/install-scripts/02-move-files.sh +++ b/install-scripts/02-move-files.sh @@ -1,14 +1,18 @@ #!/bin/bash set -euo pipefail -# Symlink tracked config files into place so edits to the live config flow +# Deploy tracked config files into place so edits to the live config flow # straight back to the repo (no more "tweaked it and lost it on refresh"). -# Resolve the repo root so this works regardless of where it's invoked from. +# Which files get deployed depends on which config "sets" are enabled for +# this machine — see config/sets/ and `make picky`. REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=install-scripts/lib/sets.sh +source "$REPO/install-scripts/lib/sets.sh" # Symlink SRC -> DEST, backing up any pre-existing real file to DEST.bak first. link() { local src="$1" dest="$2" + mkdir -p "$(dirname "$dest")" if [ -e "$dest" ] && [ ! -L "$dest" ]; then echo "Backing up existing $dest -> $dest.bak" mv "$dest" "$dest.bak" @@ -16,32 +20,58 @@ link() { ln -sf "$src" "$dest" } -# create directories -mkdir -p ~/.config/nvim -mkdir -p ~/.config/fish -mkdir -p ~/.config/fish/conf.d - -# vim -link "$REPO/config/vim/init.lua" ~/.config/nvim/init.lua - -# fish -link "$REPO/config/fish/config.fish" ~/.config/fish/config.fish -link "$REPO/config/fish/aliases.fish" ~/.config/fish/conf.d/aliases.fish -link "$REPO/config/fish/functions.fish" ~/.config/fish/conf.d/functions.fish - -# Only copy envvars.fish if it doesn't exist. -# This way we can override it with our own per-machine config. -if [ ! -f ~/.config/fish/conf.d/envvars.fish ]; then - cp "$REPO/config/fish/envvars.fish" ~/.config/fish/conf.d/ -fi - -# tmux -link "$REPO/config/tmux/tmux.conf" ~/.tmux.conf -mkdir -p ~/.tmux/scripts -for script in "$REPO"/config/tmux/scripts/*.sh; do - link "$script" ~/.tmux/scripts/"$(basename "$script")" -done -# only copy status.conf if it doesn't exist, so local customizations are preserved -if [ ! -f ~/.tmux/scripts/status.conf ]; then - cp "$REPO/config/tmux/scripts/status.conf" ~/.tmux/scripts/ -fi +# Copy SRC -> DEST only if DEST doesn't already exist yet, so per-machine +# customizations (e.g. envvars.fish, status.conf) survive re-runs. +copy_if_missing() { + local src="$1" dest="$2" + mkdir -p "$(dirname "$dest")" + if [ ! -f "$dest" ]; then + cp "$src" "$dest" + fi +} + +# Deploy one config set by walking its manifest (see config/sets/*/manifest +# for the format). +deploy_set() { + local set="$1" + local set_dir="$SETS_DIR/$set" + local manifest="$set_dir/manifest" + + if [ ! -f "$manifest" ]; then + echo "Warning: set '$set' has no manifest, skipping." >&2 + return + fi + + echo "Deploying config set: $set" + while read -r action src dest; do + [ -z "$action" ] && continue + [[ "$action" == \#* ]] && continue + + dest="${dest/#\~/$HOME}" + + case "$action" in + link) + link "$set_dir/$src" "$dest" + ;; + copy) + copy_if_missing "$set_dir/$src" "$dest" + ;; + link-glob) + for f in "$set_dir"/$src; do + [ -e "$f" ] || continue + link "$f" "$dest/$(basename "$f")" + done + ;; + run) + bash "$set_dir/$src" || echo "Warning: $src (set '$set') exited non-zero." >&2 + ;; + *) + echo "Warning: unknown action '$action' in $manifest, skipping." >&2 + ;; + esac + done < "$manifest" +} + +while IFS= read -r set; do + deploy_set "$set" +done < <(enabled_sets) diff --git a/install-scripts/lib/sets.sh b/install-scripts/lib/sets.sh new file mode 100644 index 0000000..9a9bfda --- /dev/null +++ b/install-scripts/lib/sets.sh @@ -0,0 +1,70 @@ +# shellcheck shell=bash +# Shared helpers for working with config "sets" (config/sets//). +# Sourced by pick-sets.sh and 02-move-files.sh — not meant to be run directly. + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SETS_DIR="$REPO/config/sets" +SETS_STATE_FILE="$HOME/.config/dotfiles/sets.conf" + +# Print the names of all sets available in the repo, one per line. +list_available_sets() { + for dir in "$SETS_DIR"/*/; do + [ -d "$dir" ] || continue + basename "$dir" + done +} + +# Whether SET applies to the OS this is running on. Sets without an "os" +# file (see config/sets/*/os) are OS-agnostic and always apply; sets with +# one only apply if it lists the current `uname -s` output. +set_applies_to_current_os() { + local set="$1" + local os_file="$SETS_DIR/$set/os" + [ -f "$os_file" ] || return 0 + grep -qxF "$(uname -s)" "$os_file" +} + +# Print the names of sets available in the repo AND applicable to this OS, +# one per line -- what `make picky` should offer to choose from. +list_pickable_sets() { + while IFS= read -r set; do + set_applies_to_current_os "$set" && echo "$set" + done < <(list_available_sets) +} + +# Print the names of the sets currently enabled for this machine, one per +# line. Falls back to just "basic" if `make picky` has never been run, so a +# plain `make full-install` / `make refresh` keeps today's behavior. +enabled_sets() { + local sets=() + if [ -f "$SETS_STATE_FILE" ]; then + while IFS= read -r line; do + [ -z "$line" ] && continue + sets+=("$line") + done < "$SETS_STATE_FILE" + else + sets=("basic") + fi + + for set in "${sets[@]}"; do + if [ ! -d "$SETS_DIR/$set" ]; then + echo "Warning: enabled set '$set' no longer exists in the repo, skipping." >&2 + elif ! set_applies_to_current_os "$set"; then + echo "Warning: enabled set '$set' doesn't apply to this OS ($(uname -s)), skipping." >&2 + else + echo "$set" + fi + done +} + +# A short human-readable blurb for a set, shown next to it in `make picky`. +# Falls back to a generic description if the set doesn't provide one. +set_description() { + local set="$1" + local desc_file="$SETS_DIR/$set/description" + if [ -f "$desc_file" ]; then + head -n1 "$desc_file" + else + echo "$set settings" + fi +} diff --git a/install-scripts/pick-sets.sh b/install-scripts/pick-sets.sh new file mode 100755 index 0000000..7dd3884 --- /dev/null +++ b/install-scripts/pick-sets.sh @@ -0,0 +1,94 @@ +#!/bin/bash +set -euo pipefail + +# `make picky`: an interactive dialog checklist for choosing which config +# sets (config/sets//) get deployed to this machine. The selection is +# remembered in $SETS_STATE_FILE so future `make refresh` / `make +# full-install` runs keep using it without asking again — re-run `make +# picky` any time to change your mind. +# +# On confirm, the Makefile's `picky` target runs the full install right +# after this script exits successfully. Exiting non-zero (e.g. on cancel) +# stops `make` before that happens. +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=install-scripts/lib/sets.sh +source "$REPO/install-scripts/lib/sets.sh" + +ensure_dialog() { + command -v dialog >/dev/null 2>&1 && return + + echo "Installing 'dialog' (needed for the config picker)..." + if [ "$(uname)" == "Darwin" ]; then + brew install dialog + elif [ -f /etc/arch-release ]; then + sudo pacman -S --noconfirm dialog + elif [ -f /etc/debian_version ]; then + sudo apt update && sudo apt install -y dialog + elif [ -f /etc/alpine-release ]; then + sudo apk add dialog + elif [ "$(uname)" == "FreeBSD" ]; then + sudo pkg install -y dialog + elif [ -f /etc/redhat-release ]; then + sudo dnf install -y dialog + elif [ -f /etc/gentoo-release ]; then + sudo emerge dialog + else + echo "Don't know how to install 'dialog' on this system; please install it manually and re-run 'make picky'." >&2 + exit 1 + fi +} + +ensure_dialog + +mapfile -t available < <(list_pickable_sets) +if [ "${#available[@]}" -eq 0 ]; then + echo "No config sets found under config/sets/ that apply to this OS." >&2 + exit 1 +fi + +mapfile -t currently_enabled < <(enabled_sets) + +is_enabled() { + local set="$1" + for enabled in "${currently_enabled[@]}"; do + [ "$set" == "$enabled" ] && return 0 + done + return 1 +} + +checklist_args=() +for set in "${available[@]}"; do + status=off + is_enabled "$set" && status=on + checklist_args+=("$set" "$(set_description "$set")" "$status") +done + +# Classic dialog idiom: swap stdout/stderr so the UI still draws on the real +# terminal (fd 3) while we capture the selected tags (written to stderr). +exec 3>&1 +set +e +selection="$(dialog --backtitle "dotfiles" \ + --title "Pick your config sets" \ + --separate-output \ + --checklist "Space to toggle, Enter to confirm, Esc to cancel:" \ + 20 70 "${#available[@]}" \ + "${checklist_args[@]}" \ + 2>&1 1>&3)" +status=$? +set -e +exec 3>&- +clear + +if [ "$status" -ne 0 ]; then + echo "Cancelled, install not started." + exit 1 +fi + +mkdir -p "$(dirname "$SETS_STATE_FILE")" +printf '%s\n' "$selection" | sed '/^$/d' > "$SETS_STATE_FILE" + +if [ -s "$SETS_STATE_FILE" ]; then + echo "Enabled sets: $(paste -sd, "$SETS_STATE_FILE")" +else + echo "Enabled sets: (none)" +fi