Skip to content

Instantly share code, notes, and snippets.

@reinier
Created May 13, 2026 17:25
Show Gist options
  • Select an option

  • Save reinier/9fb6d763ef0d141b90dea1b6484a042d to your computer and use it in GitHub Desktop.

Select an option

Save reinier/9fb6d763ef0d141b90dea1b6484a042d to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# start.sh — base Niri + DMS install for CachyOS
#
#
# Vendor-aware: detects Framework or Apple hardware and applies the
# appropriate optimizations. Unknown vendors get a sensible generic baseline.
set -euo pipefail
# ─────────────────────────────────────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────────────────────────────────────
C_RESET='\033[0m'; C_BLUE='\033[1;34m'; C_GREEN='\033[1;32m'
C_YELLOW='\033[1;33m'; C_RED='\033[1;31m'; C_DIM='\033[2m'
log() { printf "${C_BLUE}[start]${C_RESET} %s\n" "$*" >&2; }
ok() { printf "${C_GREEN}[start]${C_RESET} ${C_DIM}✓ %s${C_RESET}\n" "$*" >&2; }
warn() { printf "${C_YELLOW}[start]${C_RESET} %s\n" "$*" >&2; }
die() { printf "${C_RED}[start]${C_RESET} ERROR: %s\n" "$*" >&2; exit 1; }
have() { command -v "$1" >/dev/null 2>&1; }
section() { printf "\n${C_BLUE}━━━ %s ━━━${C_RESET}\n" "$*" >&2; }
pacman_install() { sudo pacman -S --needed --noconfirm "$@" || die "pacman failed: $*"; }
aur_install() { paru -S --needed --noconfirm --skipreview "$@" || die "paru failed: $*"; }
backup_file() { [[ -f "$1" && ! -f "$1.start-bak" ]] && sudo cp -a "$1" "$1.start-bak"; }
detect_cpu_vendor() {
if grep -q "GenuineIntel" /proc/cpuinfo; then echo "intel"
elif grep -q "AuthenticAMD" /proc/cpuinfo; then echo "amd"
else echo "unknown"
fi
}
detect_sys_vendor() {
[[ -f /sys/class/dmi/id/sys_vendor ]] || { echo "unknown"; return; }
local v; v=$(< /sys/class/dmi/id/sys_vendor)
case "$v" in
*[Ff]ramework*) echo "framework" ;;
*Apple*) echo "apple" ;;
*) echo "other:$v" ;;
esac
}
detect_product() {
[[ -f /sys/class/dmi/id/product_name ]] && cat /sys/class/dmi/id/product_name || echo "unknown"
}
# ─────────────────────────────────────────────────────────────────────────────
# 1. Preflight
# ─────────────────────────────────────────────────────────────────────────────
preflight() {
section "Preflight"
[[ -f /etc/os-release ]] || die "No /etc/os-release"
grep -qE '^ID(_LIKE)?=.*arch' /etc/os-release || \
warn "This script expects an Arch-based system (CachyOS preferred)."
have sudo || die "sudo not installed"
sudo -v || die "Need sudo privileges"
while true; do sudo -nv; sleep 60; kill -0 "$$" 2>/dev/null || exit; done 2>/dev/null &
ok "Preflight passed"
}
# ─────────────────────────────────────────────────────────────────────────────
# 2. System update + AUR helper
# ─────────────────────────────────────────────────────────────────────────────
base_system() {
section "System update + AUR helper"
sudo pacman -Syu --noconfirm
pacman_install base-devel git curl
if ! have paru; then
log "Installing paru…"
local tmp; tmp=$(mktemp -d)
git -C "$tmp" clone https://aur.archlinux.org/paru-bin.git
(cd "$tmp/paru-bin" && makepkg -si --noconfirm)
rm -rf "$tmp"
fi
ok "Base system + paru ready"
}
# ─────────────────────────────────────────────────────────────────────────────
# 3. Remove i3 cruft
# ─────────────────────────────────────────────────────────────────────────────
remove_i3_cruft() {
section "Removing i3 cruft"
local to_remove=()
for pkg in i3-wm i3status i3blocks i3lock dmenu picom rofi feh xss-lock; do
if pacman -Qi "$pkg" >/dev/null 2>&1; then
to_remove+=("$pkg")
fi
done
if (( ${#to_remove[@]} > 0 )); then
log "Removing: ${to_remove[*]}"
sudo pacman -Rns --noconfirm "${to_remove[@]}" || warn "Some packages couldn't be removed cleanly"
else
log "No i3 cruft found, skipping"
fi
ok "Cleanup complete"
}
# ─────────────────────────────────────────────────────────────────────────────
# 4. Compositor stack
# ─────────────────────────────────────────────────────────────────────────────
compositor_stack() {
section "Compositor stack (Niri + portals + audio + auth)"
pacman_install \
niri uwsm \
xdg-desktop-portal xdg-desktop-portal-gnome xdg-desktop-portal-gtk \
polkit hyprpolkitagent \
gnome-keyring libsecret seahorse \
pipewire pipewire-pulse pipewire-jack wireplumber pavucontrol \
brightnessctl ddcutil cliphist wl-clipboard \
networkmanager network-manager-applet
systemctl --user enable pipewire.service pipewire-pulse.service wireplumber.service
systemctl --user enable hyprpolkitagent.service
sudo systemctl enable --now NetworkManager.service
ok "Compositor stack installed"
}
# ─────────────────────────────────────────────────────────────────────────────
# 5. DankMaterialShell — AUR-first ordering to avoid quickshell conflict
# ─────────────────────────────────────────────────────────────────────────────
dms_stack() {
section "DankMaterialShell"
# Install quickshell-git FIRST. Its PKGBUILD declares provides=(quickshell)
# and conflicts=(quickshell), so when `dms` is installed afterward and
# declares a quickshell dependency, it's satisfied without pulling in the
# stable package and triggering a conflict.
log "Installing quickshell-git first (avoids conflict with dms's quickshell dep)…"
aur_install quickshell-git
# Theming + system monitor backend for DMS widgets
aur_install matugen-bin dgop-bin
# DMS itself — prefer the official extra package, fall back to AUR git
if pacman -Si dms >/dev/null 2>&1; then
# Use paru rather than pacman so the `provides=` from quickshell-git is honored cleanly
aur_install dms
else
aur_install dms-shell-git
fi
# Fonts DMS expects (Material Symbols is non-optional for icons)
pacman_install \
noto-fonts noto-fonts-emoji noto-fonts-cjk \
ttf-jetbrains-mono-nerd inter-font ttf-fira-code
aur_install ttf-material-symbols-variable-git
# DMS as a systemd user service (Omarchy-style — no spawn-at-startup needed)
# It hooks into graphical-session.target which uwsm provides for Niri.
if systemctl --user list-unit-files dms.service >/dev/null 2>&1; then
systemctl --user enable dms.service
log "Enabled dms.service as systemd user unit"
else
warn "dms.service not shipped by the package — will fall back to spawn-at-startup in niri config"
fi
# Session-wide environment vars for Qt/GTK theming + Wayland compat.
# This is what dankinstall's 90-dms.conf would set. Applies to ALL user
# sessions (not only Niri spawns), which matters for things launched via
# systemd user units or run from a TTY.
mkdir -p "$HOME/.config/environment.d"
cat > "$HOME/.config/environment.d/90-dms.conf" <<'EOF'
QT_QPA_PLATFORM=wayland
QT_QPA_PLATFORMTHEME=qt6ct
QS_ICON_THEME=Adwaita
XDG_CURRENT_DESKTOP=niri
XDG_SESSION_DESKTOP=niri
EOF
ok "DMS installed and configured"
}
# ─────────────────────────────────────────────────────────────────────────────
# 6. dms-greeter — themed greetd login that syncs with user's DMS theme
# ─────────────────────────────────────────────────────────────────────────────
login_stack() {
section "Login stack (greetd + dms-greeter)"
pacman_install greetd
aur_install greetd-dms-greeter-git
# Greeter's own compositor config — independent of user's niri config.
# Minimal: just enough niri to host the greeter window.
sudo mkdir -p /etc/greetd
backup_file /etc/greetd/niri.kdl
sudo tee /etc/greetd/niri.kdl >/dev/null <<'EOF'
// Greeter compositor config — kept minimal so the greeter is the only thing
// visible on the login screen. Do not autostart DMS here; the greeter has
// its own UI.
input {
keyboard { xkb { layout "us"; } }
touchpad { tap; natural-scroll; }
}
layout {
gaps 0
}
cursor {
xcursor-theme "Adwaita"
xcursor-size 24
}
EOF
backup_file /etc/greetd/config.toml
sudo tee /etc/greetd/config.toml >/dev/null <<'EOF'
[terminal]
vt = 1
[default_session]
command = "dms-greeter --command niri -C /etc/greetd/niri.kdl"
user = "greeter"
EOF
# PAM for keyring unlock at login
if ! grep -q pam_gnome_keyring /etc/pam.d/greetd 2>/dev/null; then
sudo tee -a /etc/pam.d/greetd >/dev/null <<'EOF'
auth optional pam_gnome_keyring.so
session optional pam_gnome_keyring.so auto_start
EOF
fi
# Disable any other display manager
for dm in sddm gdm lightdm lxdm plasma-login-manager; do
sudo systemctl disable "$dm.service" 2>/dev/null || true
done
sudo systemctl enable greetd.service
# Best-effort initial sync. On first install there's no user DMS state yet,
# so this typically no-ops or fails harmlessly; the greeter falls back to
# its default theme until you've logged in once and re-synced.
if have dms; then
log "Attempting initial 'dms greeter sync' (may no-op on first install)…"
dms greeter sync 2>/dev/null || log " (no DMS user state yet — sync after first login)"
fi
ok "Login stack configured"
}
# ─────────────────────────────────────────────────────────────────────────────
# 7. Auto-sync greeter on DMS theme changes
# ─────────────────────────────────────────────────────────────────────────────
greeter_autosync() {
section "Greeter auto-sync (path unit)"
mkdir -p "$HOME/.config/systemd/user"
cat > "$HOME/.config/systemd/user/dms-greeter-sync.service" <<'EOF'
[Unit]
Description=Sync DMS greeter cache with current user theme
After=graphical-session.target
PartOf=graphical-session.target
[Service]
Type=oneshot
ExecStart=/usr/bin/dms greeter sync
# Don't fail the unit if sync isn't ready (e.g. very first run before any DMS state)
SuccessExitStatus=0 1
EOF
cat > "$HOME/.config/systemd/user/dms-greeter-sync.path" <<'EOF'
[Unit]
Description=Watch DMS settings for greeter sync trigger
[Path]
# Fires when DMS rewrites either of these — covers theme, wallpaper, layout, etc.
PathChanged=%h/.config/DankMaterialShell/settings.json
PathChanged=%h/.config/DankMaterialShell/clsettings.json
# Debounce: tiny delay so we don't fire mid-write
TriggerLimitIntervalSec=5s
TriggerLimitBurst=3
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
systemctl --user enable dms-greeter-sync.path
ok "Greeter will auto-sync when DMS settings change"
}
# ─────────────────────────────────────────────────────────────────────────────
# 8. Terminal
# ─────────────────────────────────────────────────────────────────────────────
terminal() {
section "Terminal (ghostty)"
pacman_install ghostty
ok "ghostty installed"
}
# ─────────────────────────────────────────────────────────────────────────────
# 9. Hardware tuning — vendor-aware
# ─────────────────────────────────────────────────────────────────────────────
hardware_tuning() {
section "Hardware tuning"
local sys_vendor cpu_vendor product
sys_vendor=$(detect_sys_vendor)
cpu_vendor=$(detect_cpu_vendor)
product=$(detect_product)
log "Detected: vendor=$sys_vendor, cpu=$cpu_vendor, product=$product"
case "$cpu_vendor" in
intel) pacman_install intel-ucode ;;
amd) pacman_install amd-ucode ;;
*) warn "Unknown CPU vendor — skipping microcode" ;;
esac
pacman_install bluez bluez-utils power-profiles-daemon
sudo systemctl enable --now bluetooth.service
sudo systemctl enable --now power-profiles-daemon.service
sudo usermod -aG video,input "$USER"
case "$sys_vendor" in
framework) tune_framework ;;
apple) tune_apple "$product" ;;
*) log "No vendor-specific tuning for $sys_vendor" ;;
esac
ok "Hardware tuning complete"
}
tune_framework() {
log "Framework laptop detected — applying optimizations"
pacman_install fwupd
sudo systemctl enable --now fwupd-refresh.timer
ok "Framework tuning applied"
}
tune_apple() {
local product="$1"
log "Apple hardware detected ($product) — applying optimizations"
log "Installing Broadcom Wi-Fi driver (broadcom-wl-dkms)…"
pacman_install linux-headers broadcom-wl-dkms
sudo tee /etc/modules-load.d/applesmc.conf >/dev/null <<'EOF'
applesmc
coretemp
EOF
pacman_install fwupd
sudo systemctl enable --now fwupd-refresh.timer 2>/dev/null || true
if [[ "$product" =~ MacBook(8|9|10),1 ]]; then
log "Fanless MacBook detected — configuring deep sleep"
configure_deep_sleep
fi
sudo tee /etc/modprobe.d/hid_apple.conf >/dev/null <<'EOF'
options hid_apple fnmode=2
EOF
log "initramfs will need regenerating: 'sudo mkinitcpio -P' before reboot"
ok "Apple tuning applied"
}
configure_deep_sleep() {
if [[ -f /etc/default/limine ]]; then
if ! grep -q 'mem_sleep_default=deep' /etc/default/limine; then
sudo sed -i 's/^\(KERNEL_CMDLINE\[default\]=".*\)"/\1 mem_sleep_default=deep"/' /etc/default/limine
have limine-update && sudo limine-update || warn "Run 'sudo limine-update' manually"
fi
elif [[ -f /etc/default/grub ]]; then
if ! grep -q 'mem_sleep_default=deep' /etc/default/grub; then
sudo sed -i 's/^\(GRUB_CMDLINE_LINUX_DEFAULT=".*\)"/\1 mem_sleep_default=deep"/' /etc/default/grub
sudo grub-mkconfig -o /boot/grub/grub.cfg
fi
else
warn "Couldn't find limine or grub config — set mem_sleep_default=deep manually"
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# 10. Locale — English only
# ─────────────────────────────────────────────────────────────────────────────
locale_setup() {
section "Locale (English-only)"
backup_file /etc/locale.gen
sudo sed -i 's/^#\(en_US.UTF-8 UTF-8\)/\1/' /etc/locale.gen
sudo locale-gen
backup_file /etc/locale.conf
echo 'LANG=en_US.UTF-8' | sudo tee /etc/locale.conf >/dev/null
pacman_install hunspell hunspell-en_us
ok "Locale configured"
}
# ─────────────────────────────────────────────────────────────────────────────
# 11. Theming
# ─────────────────────────────────────────────────────────────────────────────
theming() {
section "Theming (GTK + Qt + cursor)"
pacman_install \
adw-gtk-theme \
qt6ct kvantum \
bibata-cursor-theme
mkdir -p "$HOME/.config/gtk-3.0" "$HOME/.config/gtk-4.0"
cat > "$HOME/.config/gtk-3.0/settings.ini" <<'EOF'
[Settings]
gtk-application-prefer-dark-theme=1
gtk-theme-name=adw-gtk3-dark
EOF
cat > "$HOME/.config/gtk-4.0/settings.ini" <<'EOF'
[Settings]
gtk-application-prefer-dark-theme=1
gtk-theme-name=adw-gtk3-dark
EOF
if have gsettings; then
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' || true
gsettings set org.gnome.desktop.interface gtk-theme 'adw-gtk3-dark' || true
gsettings set org.gnome.desktop.interface cursor-theme 'Bibata-Modern-Classic' || true
fi
ok "Theming applied"
}
# ─────────────────────────────────────────────────────────────────────────────
# 12. Niri starter config — HiDPI-aware, DMS-aware
# ─────────────────────────────────────────────────────────────────────────────
niri_config() {
section "Niri starter config"
if [[ -f "$HOME/.config/niri/config.kdl" ]]; then
log "Existing Niri config detected — leaving alone"
return
fi
local sys_vendor product scale_hint dms_spawn_line
sys_vendor=$(detect_sys_vendor)
product=$(detect_product)
scale_hint="1.0"
case "$sys_vendor:$product" in
framework:*) scale_hint="2.0" ;;
apple:MacBook10,1) scale_hint="1.5" ;;
apple:MacBook9,1) scale_hint="1.5" ;;
apple:MacBook8,1) scale_hint="1.5" ;;
esac
# If dms.service is enabled we don't need spawn-at-startup; otherwise fall back to it
if systemctl --user is-enabled dms.service >/dev/null 2>&1; then
dms_spawn_line='// dms started via systemd user service; no spawn-at-startup needed'
else
dms_spawn_line='spawn-at-startup "dms" "run"'
fi
mkdir -p "$HOME/.config/niri"
cat > "$HOME/.config/niri/config.kdl" <<EOF
// Starter config from start.sh — replace with your real config once ready
// Detected: $sys_vendor / $product → scale=$scale_hint
input {
keyboard { xkb { layout "us"; } }
touchpad { tap; natural-scroll; click-method "clickfinger"; }
focus-follows-mouse
}
// Set scale on your primary output. Confirm the output name with:
// niri msg outputs
// Then replace "eDP-1" below if it differs.
output "eDP-1" {
scale ${scale_hint}
}
layout {
gaps 8
preset-column-widths {
proportion 0.33333
proportion 0.5
proportion 0.66667
}
default-column-width { proportion 0.5; }
}
// Keyring on session start. DMS itself starts via systemd user service.
spawn-at-startup "gnome-keyring-daemon" "--start" "--components=secrets,ssh"
${dms_spawn_line}
// DMS popout window rule — future-proofs DMS's floating overlays
window-rule {
match app-id="com.danklinux.dms"
open-floating true
}
// Env vars now live in ~/.config/environment.d/90-dms.conf for session-wide
// scope. Keeping only niri-specific bits here.
binds {
Mod+Return { spawn "ghostty"; }
Mod+D { spawn "dms" "ipc" "call" "spotlight" "toggle"; }
Mod+Q { close-window; }
Mod+Shift+E { quit; }
Mod+Left { focus-column-left; }
Mod+Right { focus-column-right; }
Mod+Up { focus-window-up; }
Mod+Down { focus-window-down; }
Mod+Shift+Left { move-column-left; }
Mod+Shift+Right { move-column-right; }
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
Mod+4 { focus-workspace 4; }
Mod+R { switch-preset-column-width; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
XF86AudioRaiseVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "5%+"; }
XF86AudioLowerVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "5%-"; }
XF86AudioMute { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }
XF86MonBrightnessUp { spawn "brightnessctl" "set" "5%+"; }
XF86MonBrightnessDown { spawn "brightnessctl" "set" "5%-"; }
}
EOF
ok "Starter Niri config written (scale=${scale_hint})"
}
# ─────────────────────────────────────────────────────────────────────────────
# 13. dms doctor — final health check
# ─────────────────────────────────────────────────────────────────────────────
dms_doctor() {
section "dms doctor (health check)"
if ! have dms; then
warn "dms CLI not on PATH — skipping doctor check"
return
fi
# Run dms doctor but don't fail the whole script on non-zero — first-run
# often reports things like 'greetd not running yet' that resolve on reboot.
if dms doctor; then
ok "dms doctor reports a clean install"
else
warn "dms doctor flagged some issues — review output above. Most resolve after reboot."
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# 14. Done
# ─────────────────────────────────────────────────────────────────────────────
post_install() {
section "Done"
local sys_vendor; sys_vendor=$(detect_sys_vendor)
cat <<'EOF'
╭─ start.sh complete ─────────────────────────────────────╮
│ │
│ Reboot, then log in via dms-greeter. │
│ Niri + DMS will start automatically. │
│ │
│ After first login: │
│ • The greeter will auto-sync to your DMS theme │
│ whenever you change wallpaper or settings. │
│ • Run `dms greeter sync` manually if you want to │
│ force a sync right away (the first time, you'll │
│ need to log out and back in for group membership │
│ changes to take effect). │
│ │
│ For the personalization layer: │
│ curl -fsSL https://install.reinierladan.nl/nice | sh │
│ │
╰──────────────────────────────────────────────────────────╯
EOF
if [[ "$sys_vendor" == "apple" ]]; then
cat <<'EOF'
╭─ Apple hardware notes ──────────────────────────────────╮
│ │
│ Before reboot, regenerate initramfs to pick up the │
│ Broadcom driver and applesmc: │
│ │
│ sudo mkinitcpio -P │
│ │
│ After reboot: │
│ • Wi-Fi: should work automatically (broadcom-wl) │
│ • Function keys behave as F1-F12 (fnmode=2) │
│ Hold Fn for brightness/volume/media │
│ • Sleep: deep mode active (better battery) │
│ • Performance: this is a Y-series 5W CPU. Be kind. │
│ Lean on Firefox over Chromium for daily browsing. │
│ │
╰──────────────────────────────────────────────────────────╯
EOF
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# Entry point
# ─────────────────────────────────────────────────────────────────────────────
main() {
preflight
base_system
remove_i3_cruft
compositor_stack
dms_stack
login_stack
greeter_autosync
terminal
hardware_tuning
locale_setup
theming
niri_config
dms_doctor
post_install
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment