diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 2fd78da..b3106ba 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -14,7 +14,12 @@ jobs: - name: ShellCheck run: | - shellcheck -x install-scripts/*.sh install-scripts/lib/*.sh config/sets/*/apply.sh config/sets/basic/tmux/scripts/*.sh scripts/* + shellcheck -x install-scripts/*.sh install-scripts/lib/*.sh config/desktop-common.sh config/sets/*/apply.sh config/sets/basic/tmux/scripts/*.sh scripts/* tests/*.sh + + # The gnome/kde/xfce sets apply the same decisions through three + # unrelated backends, so nothing else would notice one falling behind. + - name: Desktop intent coverage + run: bash tests/check-desktop-intents.sh - name: Fish syntax check run: | diff --git a/README.md b/README.md index 59bc8b2..76d75cb 100644 --- a/README.md +++ b/README.md @@ -91,11 +91,13 @@ config/ os # "Linux" — hidden from `make picky` elsewhere apply.sh # further sets live alongside these, following the same layout + desktop-common.sh # What gnome/kde/xfce all agree to apply (see below) install-scripts/ # Numbered setup scripts run by the Makefile lib/sets.sh # Shared helpers for discovering/enabling sets pick-sets.sh # the dialog checklist; takes set names to skip it scripts/ # Misc helper scripts (e.g. Gentoo kernel upgrade) tests/ # Dockerfiles used by CI to test installs per distro + check-desktop-intents.sh # Fails CI if a desktop set falls behind the others ``` ## Config Sets & `make picky` @@ -179,6 +181,45 @@ full-install` runs too, so you only need to pick once per machine. If you've never picked, everything defaults to just `basic`, matching this repo's historical behavior. +### Desktop intents + +`gnome`, `kde` and `xfce` apply the same handful of decisions — key repeat +rate, Caps Lock as Escape, no screen lock, no idle display blanking, no +automount, an editable path bar, a dark theme — through three completely +unrelated backends (`gsettings`, `kwriteconfig`, `xfconf-query`). There's no +code worth sharing between them, so what gets shared instead is the decision. + +`config/desktop-common.sh` holds both halves of that: + +- the **values** all three should end up applying (`KEY_REPEAT_RATE`, + `KEY_REPEAT_DELAY_MS`), so changing the repeat rate is one edit rather than + three in three different unit systems — GNOME wants the gap between + repeats in milliseconds, the other two want a rate +- the **list** of settings all three are expected to cover, as + `DESKTOP_INTENTS` + +Each `apply.sh` marks where it handles an intent with a `# intent: ` +comment above the relevant section, and `tests/check-desktop-intents.sh` +cross-references the two: + +``` +$ bash tests/check-desktop-intents.sh + intent gnome kde xfce + keyboard-repeat yes yes yes + caps-as-escape yes yes yes + ... +``` + +It runs in CI and fails if a desktop is missing an intent, or marks one that +isn't declared (which catches typos in the markers). So adding a setting to +one desktop and forgetting the other two now breaks the build instead of +going unnoticed. Settings that genuinely only exist on one desktop — Dolphin's +menu bar, XFCE's panel layout — aren't intents and need no marker. + +The `macos` set is deliberately outside this: it isn't a desktop environment +in the same sense, most of its settings have no Linux equivalent, and the few +that overlap use units that don't convert cleanly. + ### Adding a new set 1. Create `config/sets//`. @@ -403,6 +444,8 @@ When you submit a pull request, the following automated checks run: - **Build Verification**: The GitHub Action (`pr-test.yml`) verifies that the dotfiles can be successfully built on each supported platform, and that the install actually landed (symlinks in place, fish functions loaded) — not just that the install command exited 0 +- **Lint & Consistency**: `shellcheck` over every shell script in the repo, a `fish --no-execute` parse of every fish script, and `tests/check-desktop-intents.sh` to catch the `gnome`/`kde`/`xfce` sets drifting apart (see [Desktop intents](#desktop-intents)) + ### Workflow Details The PR testing workflow: diff --git a/config/desktop-common.sh b/config/desktop-common.sh new file mode 100644 index 0000000..bcd5902 --- /dev/null +++ b/config/desktop-common.sh @@ -0,0 +1,43 @@ +# shellcheck shell=bash +# Shared ground truth for the desktop sets — gnome, kde and xfce. +# +# Those three sets say the same things to three completely different +# backends (gsettings, kwriteconfig, xfconf-query), so there's no code worth +# sharing between them. What *is* worth sharing is the decision behind the +# code: which settings this repo has an opinion about, and what that opinion +# is. Without that written down once, adding a setting to one desktop and +# forgetting the other two is invisible. +# +# So: every setting all three desktops are expected to cover gets an entry in +# DESKTOP_INTENTS below, and each apply.sh marks where it handles it with a +# +# # intent: +# +# comment. tests/check-desktop-intents.sh cross-references the two and fails +# if a desktop is missing one, or claims an id that doesn't exist here. +# Settings that only make sense on one desktop (Dolphin's menu bar, XFCE's +# panel layout) aren't intents and need no annotation. +# +# The macos set is deliberately out of scope: it isn't a desktop environment +# in this sense, its settings mostly have no Linux equivalent, and the few +# that overlap use units that don't convert cleanly. +# +# Sourced by config/sets/{gnome,kde,xfce}/apply.sh. + +# Values every desktop should end up applying, in their canonical units. +# Each backend converts as needed — GNOME, for one, wants the repeat +# *interval* in milliseconds rather than a rate. +KEY_REPEAT_RATE=50 # repeats per second, once repeating starts +KEY_REPEAT_DELAY_MS=250 # milliseconds held before repeating starts + +# ":", one per setting all three desktops should cover. +# shellcheck disable=SC2034 # read by tests/check-desktop-intents.sh, which sources this +DESKTOP_INTENTS=( + "keyboard-repeat:Key repeat at ${KEY_REPEAT_RATE}/s after a ${KEY_REPEAT_DELAY_MS}ms delay" + "caps-as-escape:Caps Lock acts as Escape" + "no-screen-lock:Screen locking disabled entirely" + "no-idle-display:Display never dims or blanks when idle, on AC or battery" + "no-automount:Removable devices are not automounted" + "editable-path-bar:File dialogs offer an editable path entry, not breadcrumbs" + "dark-theme:A dark colour scheme" +) diff --git a/config/sets/gnome/apply.sh b/config/sets/gnome/apply.sh index 56bad2e..d25c63a 100755 --- a/config/sets/gnome/apply.sh +++ b/config/sets/gnome/apply.sh @@ -4,6 +4,14 @@ set -euo pipefail # GNOME settings that diverge from stock defaults on this machine, mirroring # config/sets/kde/apply.sh's choices for the GNOME desktop. # Each section below is independent and safe to re-run. +# +# The "# intent:" markers below tie each section to an entry in +# config/desktop-common.sh, which is the one place the kde/gnome/xfce sets +# agree on what they're all supposed to cover. + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +# shellcheck source=config/desktop-common.sh +source "$REPO/config/desktop-common.sh" if ! command -v gsettings >/dev/null 2>&1; then echo "Warning: gsettings not found (not a GNOME system?); skipping GNOME settings." >&2 @@ -21,20 +29,23 @@ gset() { fi } -# Keyboard repeat rate: 50 repeats/sec (20ms interval) with a 250ms initial -# delay (Settings > Keyboard). -gset org.gnome.desktop.peripherals.keyboard delay 250 -gset org.gnome.desktop.peripherals.keyboard repeat-interval 20 +# intent: keyboard-repeat +# GNOME takes the gap *between* repeats in milliseconds rather than a rate, +# so the shared rate is inverted here (Settings > Keyboard). +gset org.gnome.desktop.peripherals.keyboard delay "$KEY_REPEAT_DELAY_MS" +gset org.gnome.desktop.peripherals.keyboard repeat-interval "$(( 1000 / KEY_REPEAT_RATE ))" -# Remap Caps Lock to Escape (gnome-tweaks > Keyboard & Mouse > Additional -# Layout Options > Caps Lock Behavior). +# intent: caps-as-escape +# gnome-tweaks > Keyboard & Mouse > Additional Layout Options > Caps Lock +# Behavior. gset org.gnome.desktop.input-sources xkb-options "['caps:escape']" -# Disable screen locking entirely (Settings > Privacy & Security > Screen -# Lock). +# intent: no-screen-lock +# Settings > Privacy & Security > Screen Lock. gset org.gnome.desktop.screensaver lock-enabled false gset org.gnome.desktop.screensaver idle-activation-enabled false +# intent: no-idle-display # Never dim or blank the display when idle, on AC or battery (Settings > # Power). gset org.gnome.desktop.session idle-delay 0 @@ -42,15 +53,17 @@ gset org.gnome.settings-daemon.plugins.power idle-dim false gset org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type nothing gset org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type nothing -# Disable automounting of removable devices (Settings > Removable Media). +# intent: no-automount +# Settings > Removable Media. gset org.gnome.desktop.media-handling automount false gset org.gnome.desktop.media-handling automount-open false -# GTK file open/save dialogs: use an editable path bar instead of -# breadcrumbs, matching Dolphin's setting. +# intent: editable-path-bar +# GTK file open/save dialogs: an editable path bar instead of breadcrumbs, +# matching Dolphin's setting. gset org.gtk.Settings.FileChooser location-mode filename-entry gset org.gtk.gtk4.Settings.FileChooser location-mode filename-entry -# Prefer a dark color scheme, GNOME's equivalent of Breeze Dark (Settings > -# Appearance). +# intent: dark-theme +# GNOME's equivalent of Breeze Dark (Settings > Appearance). gset org.gnome.desktop.interface color-scheme prefer-dark diff --git a/config/sets/kde/apply.sh b/config/sets/kde/apply.sh index ffc4ea3..4fa910b 100755 --- a/config/sets/kde/apply.sh +++ b/config/sets/kde/apply.sh @@ -3,6 +3,14 @@ set -euo pipefail # KDE Plasma settings that diverge from stock defaults on this machine. # Each section below is independent and safe to re-run. +# +# The "# intent:" markers below tie each section to an entry in +# config/desktop-common.sh, which is the one place the kde/gnome/xfce sets +# agree on what they're all supposed to cover. + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +# shellcheck source=config/desktop-common.sh +source "$REPO/config/desktop-common.sh" kwriteconfig="" for candidate in kwriteconfig6 kwriteconfig5; do @@ -17,25 +25,27 @@ if [ -z "$kwriteconfig" ]; then exit 0 fi -# Keyboard repeat rate: 50 repeats/sec with a 250ms initial delay -# (System Settings > Keyboard > Advanced), via kcminputrc. -"$kwriteconfig" --file kcminputrc --group Keyboard --key RepeatDelay 250 -"$kwriteconfig" --file kcminputrc --group Keyboard --key RepeatRate 50 +# intent: keyboard-repeat +# System Settings > Keyboard > Advanced, via kcminputrc. +"$kwriteconfig" --file kcminputrc --group Keyboard --key RepeatDelay "$KEY_REPEAT_DELAY_MS" +"$kwriteconfig" --file kcminputrc --group Keyboard --key RepeatRate "$KEY_REPEAT_RATE" # 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 + xset r rate "$KEY_REPEAT_DELAY_MS" "$KEY_REPEAT_RATE" || true fi -# Remap Caps Lock to Escape (System Settings > Keyboard > Advanced > Caps -# Lock behavior). +# intent: caps-as-escape +# System Settings > Keyboard > Advanced > Caps Lock behavior. "$kwriteconfig" --file kxkbrc --group Layout --key Options "caps:escape" -# Disable screen locking entirely (System Settings > Screen Locking). +# intent: no-screen-lock +# System Settings > Screen Locking. "$kwriteconfig" --file kscreenlockerrc --group Daemon --key Autolock false "$kwriteconfig" --file kscreenlockerrc --group Daemon --key Timeout 0 +# intent: no-idle-display # Never dim or turn off the display when idle, on AC or battery # (System Settings > Power Management > Energy Saving). for profile in AC Battery; do @@ -43,17 +53,19 @@ for profile in AC Battery; do "$kwriteconfig" --file powerdevilrc --group "$profile" --group Display --key TurnOffDisplayWhenIdle false done -# Disable automounting of removable devices (System Settings > Removable -# Storage). +# intent: no-automount +# System Settings > Removable Storage. "$kwriteconfig" --file kded5rc --group Module-device_automounter --key autoload false -# Dolphin: hide the menu bar, and use an editable path bar instead of -# breadcrumbs in file open/save dialogs. +# Dolphin: hide the menu bar. KDE-only, so not a shared intent. "$kwriteconfig" --file dolphinrc --group MainWindow --key MenuBar Disabled + +# intent: editable-path-bar +# An editable path bar instead of breadcrumbs in file open/save dialogs. "$kwriteconfig" --file kdeglobals --group "KFileDialog Settings" --key "Breadcrumb Navigation" false -# Use the Breeze Dark global theme (System Settings > Appearance > Global -# Theme). +# intent: dark-theme +# The Breeze Dark global theme (System Settings > Appearance > Global Theme). if command -v plasma-apply-lookandfeel >/dev/null 2>&1; then plasma-apply-lookandfeel -a org.kde.breezedark.desktop || true else diff --git a/config/sets/xfce/apply.sh b/config/sets/xfce/apply.sh index 5d3097f..fa9d18a 100755 --- a/config/sets/xfce/apply.sh +++ b/config/sets/xfce/apply.sh @@ -5,6 +5,14 @@ set -euo pipefail # config/sets/kde/apply.sh and config/sets/gnome/apply.sh's choices for the # XFCE desktop. # Each section below is independent and safe to re-run. +# +# The "# intent:" markers below tie each section to an entry in +# config/desktop-common.sh, which is the one place the kde/gnome/xfce sets +# agree on what they're all supposed to cover. + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +# shellcheck source=config/desktop-common.sh +source "$REPO/config/desktop-common.sh" if ! command -v xfconf-query >/dev/null 2>&1; then echo "Warning: xfconf-query not found (not an XFCE system?); skipping XFCE settings." >&2 @@ -23,19 +31,20 @@ xset_prop() { # --- Keyboard --------------------------------------------------------- -# Keyboard repeat rate: 50 repeats/sec with a 250ms initial delay -# (Settings > Keyboard > Behaviour). +# intent: keyboard-repeat +# Settings > Keyboard > Behaviour. xset_prop keyboards /Default/KeyRepeat bool true -xset_prop keyboards /Default/KeyRepeat/Rate int 50 -xset_prop keyboards /Default/KeyRepeat/Delay int 250 +xset_prop keyboards /Default/KeyRepeat/Rate int "$KEY_REPEAT_RATE" +xset_prop keyboards /Default/KeyRepeat/Delay int "$KEY_REPEAT_DELAY_MS" # Apply immediately in the current X11 session; a fresh login also # re-applies it from the "keyboards" channel either way. if [ -n "${DISPLAY:-}" ] && command -v xset >/dev/null 2>&1; then - xset r rate 250 50 || true + xset r rate "$KEY_REPEAT_DELAY_MS" "$KEY_REPEAT_RATE" || true fi -# Remap Caps Lock to Escape. Unlike the layout/group/compose-key options, +# intent: caps-as-escape +# Unlike the layout/group/compose-key options, # xfsettingsd has no xfconf-backed setting for this XKB option group, so it # can't be stored declaratively the way the kde/gnome sets do it — it's # applied directly via setxkbmap instead, both now and on every future @@ -57,7 +66,8 @@ fi # --- Screen locking & power -------------------------------------------- -# Disable screen locking entirely (Settings > Screensaver). xfce4-screensaver +# intent: no-screen-lock +# Settings > Screensaver. xfce4-screensaver # is a separate, optional package (older XFCE releases use light-locker # instead, which has no xfconf-backed settings of its own). if command -v xfce4-screensaver >/dev/null 2>&1; then @@ -67,6 +77,7 @@ else echo "Warning: xfce4-screensaver not found; skipping screen lock settings." >&2 fi +# intent: no-idle-display # Never dim or turn off the display when idle, on AC or battery, and don't # lock on suspend/hibernate (Settings > Power Manager). if command -v xfce4-power-manager >/dev/null 2>&1; then @@ -80,9 +91,9 @@ fi # --- Device automount ---------------------------------------------------- -# Disable automounting of removable devices (Settings > Removable Drives -# and Media). thunar-volman is a separate, optional package from Thunar -# itself. +# intent: no-automount +# Settings > Removable Drives and Media. thunar-volman is a separate, +# optional package from Thunar itself. if command -v thunar-volman >/dev/null 2>&1; then xset_prop thunar-volman /autobrowse/enabled bool false xset_prop thunar-volman /automount-drives/enabled bool false @@ -94,6 +105,7 @@ fi # --- File manager / dialogs ---------------------------------------------- +# intent: editable-path-bar # Thunar: use an editable path bar instead of breadcrumbs. if command -v thunar >/dev/null 2>&1; then xset_prop thunar /last-location-bar string ThunarLocationEntry @@ -118,6 +130,7 @@ fi # --- Appearance ------------------------------------------------------------ +# intent: dark-theme # Prefer a dark GTK + window manager theme, whichever of these common dark # themes is actually installed (Settings > Appearance / Window Manager). pick_dark_theme() { diff --git a/tests/check-desktop-intents.sh b/tests/check-desktop-intents.sh new file mode 100755 index 0000000..1763066 --- /dev/null +++ b/tests/check-desktop-intents.sh @@ -0,0 +1,86 @@ +#!/bin/bash +set -euo pipefail + +# Checks that every desktop set accounts for every intent declared in +# config/desktop-common.sh. +# +# The gnome, kde and xfce sets apply the same decisions through three +# unrelated backends, so nothing stops one of them quietly falling behind the +# others. This is what stops it: add an intent to DESKTOP_INTENTS and this +# fails until all three sets mark where they handle it with a +# +# # intent: +# +# comment. It also fails on an intent id that isn't declared, which catches +# typos in those markers. +# +# Run by the lint job in .github/workflows/pr-test.yml, and by hand any time: +# bash tests/check-desktop-intents.sh + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# shellcheck source=config/desktop-common.sh +source "$REPO/config/desktop-common.sh" + +DESKTOP_SETS=(gnome kde xfce) + +# Intent ids, in declared order. +intent_ids=() +for entry in "${DESKTOP_INTENTS[@]}"; do + intent_ids+=("${entry%%:*}") +done + +# Every "# intent: " marker found in a set's apply.sh, one per line. +markers_in() { + grep -hoE '^# intent: [a-z0-9-]+' "$1" | sed 's/^# intent: //' || true +} + +failures=() + +# --- Coverage: every set implements every declared intent ------------------ + +printf 'Desktop intent coverage (config/desktop-common.sh):\n\n' +printf ' %-20s' 'intent' +printf '%-9s' "${DESKTOP_SETS[@]}" +printf '\n' + +for id in "${intent_ids[@]}"; do + printf ' %-20s' "$id" + for set in "${DESKTOP_SETS[@]}"; do + apply="$REPO/config/sets/$set/apply.sh" + if [ ! -f "$apply" ]; then + printf '%-9s' 'NO SET' + failures+=("config/sets/$set/apply.sh does not exist") + elif markers_in "$apply" | grep -qxF "$id"; then + printf '%-9s' 'yes' + else + printf '%-9s' 'MISSING' + failures+=("$set does not handle intent '$id' (add a '# intent: $id' marker to config/sets/$set/apply.sh, or drop the intent)") + fi + done + printf '\n' +done +printf '\n' + +# --- The other direction: no set claims an intent that isn't declared ------ + +for set in "${DESKTOP_SETS[@]}"; do + apply="$REPO/config/sets/$set/apply.sh" + [ -f "$apply" ] || continue + while IFS= read -r marker; do + [ -z "$marker" ] && continue + printf '%s\n' "${intent_ids[@]}" | grep -qxF "$marker" && continue + failures+=("$set marks intent '$marker', which isn't declared in config/desktop-common.sh (typo?)") + done < <(markers_in "$apply") +done + +# --- Verdict --------------------------------------------------------------- + +if [ "${#failures[@]}" -gt 0 ]; then + echo "Desktop intents are out of sync:" >&2 + for failure in "${failures[@]}"; do + echo " - $failure" >&2 + done + exit 1 +fi + +echo "All ${#intent_ids[@]} intents covered by all ${#DESKTOP_SETS[@]} desktop sets."