20 Commits

Author SHA1 Message Date
ea11fe0eb6 Feature: Make home UniFi VPN row a collapsible card
Put the Replace config option behind an expand chevron, like Mullvad's
options, and frame the card while it's expanded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:07:01 +01:00
19e7d3f673 Feature: Add config import button for home UniFi VPN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:45:12 +01:00
bf86ffe258 Feature: Add gavin homelab host to SSH config
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:41:26 +01:00
8d73d2a37e Feature: Add home UniFi WireGuard tunnel to VPN panel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:40:23 +01:00
54b91a72e2 Feature: Add new-install package review list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:24:30 +01:00
788d4939f0 Fix: Drop plain Ctrl+V paste binding in foot
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:23:41 +01:00
9a038b74c3 Fix: Restart portals after env import so screen sharing works
xdg-desktop-portal-hyprland could start before Hyprland imported
WAYLAND_DISPLAY/XDG_CURRENT_DESKTOP, leaving no ScreenCast backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:23:41 +01:00
954dedf7cd Fix: Scale bar volume scroll by distance for touchpads
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 03:40:27 +01:00
90d0e2bed0 Feature: Show Plexamp icon in media notifications
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 03:37:01 +01:00
14f72af3f2 Fix: Fall back to grim/slurp for screenshots without hyprshot
Super+Print and Super+Shift+Print use hyprshot when installed and
grim/slurp otherwise. Add Super+Shift+S (area) and Super+F6 (full
screen) for keyboards without a Print key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 03:31:57 +01:00
836848dedd Fix: Bind workspace number keys by name in Lua config
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 02:26:10 +01:00
fbaffc0816 Fix: Use Lua dispatch syntax for bar workspace buttons
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 02:26:10 +01:00
ccae379490 Feature: Ctrl+C/Ctrl+V copy and paste in foot
Ctrl+C and Ctrl+V now copy and paste like in other apps. The interrupt
signal (^C) moves to Ctrl+Shift+C.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:33:49 +01:00
7f0fad5922 Fix: Run Plex under XWayland to stop a blank window
hypr env sets QT_QPA_PLATFORM="wayland;xcb", and under Wayland Plex's
bundled QtWebEngine never loads the web client, so the window stays
blank. Both the PATH wrapper and the .desktop entry now force xcb, as
upstream does, and set XCURSOR_SIZE=24 so the X11 cursor isn't huge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:33:12 +01:00
b40d4a9fa4 Fix: Point Sunshine tray link at new tailnet domain
The tailnet's MagicDNS suffix changed from twhomelab.local to
twhomelab.tailnet, so the Sunshine web UI link now opens
xol.twhomelab.tailnet:47990.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 06:33:12 +01:00
19d6936aa6 Feature: Enable hypridle without locking during Sunshine streams
Sunshine's global_prep_cmd now runs hypr/scripts/SunshineIdle.sh, which
holds a systemd idle inhibitor for the length of a stream (hypridle
respects it) and unlocks hyprlock if the session was already locked.
With that in place hypridle starts at login again.

Sunshine's config (sunshine.conf, apps.json, prep scripts) moves into
the dotfiles; credentials, logs and state stay local and are gitignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:14:44 +01:00
4a57e9902f Feature: Bootstrap script for new installs
bootstrap.sh installs everything listed in system/packages.txt (pacman,
AUR via yay, flatpak, official install scripts), auto-detects CPU
microcode and GPU drivers, enables services, links dotfiles with stow
and runs system/install.sh. Optional --svt-av1 installs
svt-av1-essential-git and rebuilds ffmpeg against it via
system/rebuild-ffmpeg.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:10:27 +01:00
1201e6a4cc Feature: Sync dotfiles on system update
update-system.sh now runs sync-dotfiles.sh first, which pulls the repo
(fast-forward only) and re-stows it so both machines pick up config changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:34:07 +01:00
626c7dbcc1 Feature: Move volume OSD to bottom-center and enlarge slightly
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:28:55 +01:00
35374800bf Fix: Volume keys use wpctl instead of missing pamixer
Quickshell's VolumeOsd now shows changes, so drop notify-send popups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:28:55 +01:00
33 changed files with 1504 additions and 348 deletions

View File

@@ -39,7 +39,15 @@ mkdir Projects
git clone ssh://git@gitea.thomasleewilliams.co.uk:2222/thomas/dotfiles.git
```
Once that is complete, run this command:
On a fresh install, run the bootstrap script. It installs drivers (auto-detected),
every package in `system/packages.txt`, yay, flatpaks, then links the dotfiles and
system config below:
```console
~/Projects/dotfiles/bootstrap.sh # asks about the optional SVT-AV1 + ffmpeg rebuild
~/Projects/dotfiles/bootstrap.sh --svt-av1 # or --no-svt-av1 to skip the question
```
To only link the dotfiles, run this command:
```console
stow -d ~/Projects/dotfiles/ -t ~/ dotfiles
```

168
bootstrap.sh Executable file
View File

@@ -0,0 +1,168 @@
#!/usr/bin/env bash
# Sets up a fresh Arch install: drivers, packages, dotfiles and system config.
# Safe to re-run; anything already installed is skipped.
#
# Usage: ./bootstrap.sh [--svt-av1 | --no-svt-av1]
# --svt-av1 also install svt-av1-essential-git and rebuild ffmpeg against it
# --no-svt-av1 skip that without asking
# (neither) ask
set -euo pipefail
repo="$(cd "$(dirname "$0")" && pwd)"
list="$repo/system/packages.txt"
svt_av1=ask
for arg in "$@"; do
case "$arg" in
--svt-av1) svt_av1=yes ;;
--no-svt-av1) svt_av1=no ;;
*) echo "Unknown option: $arg" >&2; exit 1 ;;
esac
done
if (( EUID == 0 )); then
echo "Run this as your normal user, not root (it uses sudo where needed)." >&2
exit 1
fi
step() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }
# Prints the packages under a [section] of packages.txt, skipping comments
packages() {
awk -v want="[$1]" '
/^\[/ { section = $1; next }
section == want { sub(/#.*/, ""); if (NF) print $1 }
' "$list"
}
# --- System config -----------------------------------------------------------
step "System config (pacman options)"
"$repo/system/install.sh"
step "Enabling multilib (needed for Steam and 32-bit drivers)"
if ! grep -q '^\[multilib\]' /etc/pacman.conf; then
sudo sed -i '/^#\[multilib\]/,/^#Include/ s/^#//' /etc/pacman.conf
fi
sudo pacman -Syu --needed --noconfirm pciutils git base-devel
# --- Hardware ----------------------------------------------------------------
step "Detecting CPU and GPU"
hw=()
case "$(grep -m1 '^vendor_id' /proc/cpuinfo | awk '{print $3}')" in
AuthenticAMD) hw+=(amd-ucode); echo "CPU: AMD" ;;
GenuineIntel) hw+=(intel-ucode); echo "CPU: Intel" ;;
*) echo "CPU: unknown vendor, skipping microcode" ;;
esac
# Vendor column of every display controller, e.g. "NVIDIA Corporation"
gpus="$(lspci -mm | awk -F'"' '$2 ~ /VGA|3D|Display/ { print $4 }')"
if grep -q NVIDIA <<<"$gpus"; then
echo "GPU: NVIDIA"
# nvidia-open only ships modules for the stock `linux` kernel; any other
# kernel needs the DKMS build. Supports Turing (GTX 16xx / RTX 20xx) and newer.
if pacman -Qq linux-lts linux-zen linux-hardened >/dev/null 2>&1; then
hw+=(nvidia-open-dkms linux-headers)
else
hw+=(nvidia-open)
fi
hw+=(nvidia-utils lib32-nvidia-utils libva-nvidia-driver)
fi
if grep -q 'Advanced Micro Devices' <<<"$gpus"; then
echo "GPU: AMD"
hw+=(mesa lib32-mesa vulkan-radeon lib32-vulkan-radeon)
fi
if grep -q 'Intel Corporation' <<<"$gpus"; then
echo "GPU: Intel"
hw+=(mesa lib32-mesa vulkan-intel lib32-vulkan-intel intel-media-driver)
fi
# Installed before the main list so Steam's 32-bit Vulkan dependency is
# already satisfied by the right driver
if (( ${#hw[@]} )); then
sudo pacman -S --needed --noconfirm "${hw[@]}"
fi
# --- Packages ----------------------------------------------------------------
step "Official repo packages"
mapfile -t pkgs < <(packages pacman)
sudo pacman -S --needed --noconfirm "${pkgs[@]}"
step "yay (AUR helper)"
if ! command -v yay >/dev/null; then
build="$(mktemp -d)"
git clone https://aur.archlinux.org/yay-bin.git "$build/yay-bin"
(cd "$build/yay-bin" && makepkg -si --noconfirm)
rm -rf "$build"
else
echo "already installed"
fi
step "AUR packages"
mapfile -t pkgs < <(packages aur)
yay -S --needed --noconfirm "${pkgs[@]}"
step "Flatpak apps"
sudo flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
mapfile -t pkgs < <(packages flatpak)
sudo flatpak install -y --noninteractive flathub "${pkgs[@]}"
step "Apps from official install scripts"
mkdir -p "$HOME/.local/bin"
for app in $(packages installer); do
if command -v "$app" >/dev/null || [[ -x "$HOME/.local/bin/$app" ]]; then
echo "$app: already installed"
continue
fi
case "$app" in
claude) curl -fsSL https://claude.ai/install.sh | bash ;;
oh-my-posh) curl -fsSL https://ohmyposh.dev/install.sh | bash -s -- -d "$HOME/.local/bin" ;;
*) echo "$app: no install recipe in bootstrap.sh, skipping" >&2 ;;
esac
done
# --- Optional: SVT-AV1 essential + ffmpeg rebuild ----------------------------
if [[ $svt_av1 == ask ]]; then
read -rp $'\nInstall svt-av1-essential-git and rebuild ffmpeg against it? (slow) [y/N] ' reply
[[ $reply == [yY]* ]] && svt_av1=yes || svt_av1=no
fi
if [[ $svt_av1 == yes ]]; then
step "svt-av1-essential-git"
# Interactive on purpose: pacman asks to replace the conflicting svt-av1,
# and --noconfirm would answer no
yay -S --needed svt-av1-essential-git
step "Rebuilding ffmpeg against svt-av1-essential-git"
"$repo/system/rebuild-ffmpeg.sh"
fi
# --- Services ----------------------------------------------------------------
step "Enabling services"
# Not started now: sddm would take over the screen mid-script. They come up on reboot.
mapfile -t units < <(packages services)
sudo systemctl enable "${units[@]}"
# --- Dotfiles ----------------------------------------------------------------
step "Linking dotfiles with stow"
# Create real parent dirs first, otherwise stow symlinks the whole directory
# (e.g. ~/.config) into the repo and every app's files end up in git
mkdir -p "$HOME/.config" "$HOME/.local/bin" "$HOME/.local/share" "$HOME/Pictures"
# Sunshine keeps credentials and logs beside its config; those must stay out of git
mkdir -p "$HOME/.config/sunshine"
mkdir -p -m 700 "$HOME/.ssh"
if ! stow -d "$repo" -t "$HOME" --restow dotfiles; then
echo "stow hit conflicts (listed above). Move those files aside, then run:" >&2
echo " stow -d \"$repo\" -t \"\$HOME\" dotfiles" >&2
fi
if [[ "$(getent passwd "$USER" | cut -d: -f7)" != */zsh ]]; then
step "Setting zsh as login shell"
chsh -s /usr/bin/zsh
fi
step "Done — reboot to load drivers and start Hyprland"

View File

@@ -47,3 +47,11 @@ regular4=53BDFA # blue
regular5=FAE994 # magenta
regular6=99faf2 # cyan
regular7=C7C7C7 # white
[key-bindings]
clipboard-copy=Control+c XF86Copy
clipboard-paste=Control+Shift+v XF86Paste
[text-bindings]
# Ctrl+C now copies, so send the interrupt signal (^C) from Ctrl+Shift+C instead
\x03=Control+Shift+c

View File

@@ -29,6 +29,8 @@ hl.monitor({ output = "DP-2", mode = "1920x1080@165.00Hz", position = "0x0", sca
-- (unbind = SUPER SHIFT, Print / unbind = SUPER, up from the source .conf have no Lua equivalent needed —
-- we just don't define a conflicting bind elsewhere; SUPER + up -> movefocus u below is otherwise identical
-- to the one already in keybinds.lua, kept here too for parity with the original file)
hl.bind("SUPER + Print", hl.dsp.exec_cmd("hyprshot -o " .. os.getenv("HOME") .. "/Pictures/Screenshots-Linux -m region"))
hl.bind("SUPER + Print", hl.dsp.exec_cmd([[sh -c 'if command -v hyprshot >/dev/null; then hyprshot -o "$HOME/Pictures/Screenshots-Linux" -m region; else grim -g "$(slurp)" "$HOME/Pictures/Screenshots-Linux/$(date +%Y-%m-%d_%H-%M-%S).png" && notify-send "Screenshot saved"; fi']])) -- drag an area (hyprshot if installed, else grim/slurp)
hl.bind("SUPER + SHIFT + S", hl.dsp.exec_cmd([[sh -c 'grim -g "$(slurp)" "$HOME/Pictures/Screenshots-Linux/$(date +%Y-%m-%d_%H-%M-%S).png" && notify-send "Screenshot saved"']])) -- region; Surface keyboard has no Print key
hl.bind("SUPER + F6", hl.dsp.exec_cmd([[sh -c 'grim "$HOME/Pictures/Screenshots-Linux/$(date +%Y-%m-%d_%H-%M-%S).png" && notify-send "Screenshot saved"']])) -- full screen
hl.bind("SUPER + up", hl.dsp.focus({ direction = "u" }))
hl.bind("SUPER + SHIFT + Print", hl.dsp.exec_cmd("hyprshot -o " .. os.getenv("HOME") .. "/Pictures/Screenshots-Linux -m window"))
hl.bind("SUPER + SHIFT + Print", hl.dsp.exec_cmd([[sh -c 'if command -v hyprshot >/dev/null; then hyprshot -o "$HOME/Pictures/Screenshots-Linux" -m window; else grim "$HOME/Pictures/Screenshots-Linux/$(date +%Y-%m-%d_%H-%M-%S).png" && notify-send "Screenshot saved"; fi']])) -- window (hyprshot) or full screen (fallback)

View File

@@ -13,6 +13,8 @@ hl.on("hyprland.start", function()
-- Startup
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP")
hl.exec_cmd("systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP")
-- restart portals once the env is imported, otherwise screen sharing breaks
hl.exec_cmd("sleep 1 && systemctl --user restart xdg-desktop-portal-hyprland && systemctl --user restart xdg-desktop-portal")
-- Sunshine (virtual monitor only on desktop; skipped on laptops, detected by a system battery)
hl.exec_cmd("sleep 1 && ! ls -d /sys/class/power_supply/BAT* >/dev/null 2>&1 && hyprctl output create headless")
@@ -34,7 +36,8 @@ hl.on("hyprland.start", function()
hl.exec_cmd("wl-paste --type image --watch cliphist store")
-- Starting hypridle to start hyprlock
-- hl.exec_cmd("hypridle")
-- Sunshine streams hold an idle inhibitor (scripts/SunshineIdle.sh) so this never locks mid-stream
hl.exec_cmd("hypridle")
-- Sunshine for streaming media
hl.exec_cmd("sunshine")

View File

@@ -116,23 +116,22 @@ hl.bind(mainMod .. " + SHIFT + tab", hl.dsp.focus({ workspace = "m-1" }))
hl.bind(mainMod .. " + U", hl.dsp.workspace.toggle_special("nyx"))
hl.bind(mainMod .. " + SHIFT + U", hl.dsp.window.move({ workspace = "special:nyx" }))
-- The following mappings use key codes to better support various keyboard layouts
-- 1 is code:10, 2 is code:11, etc
-- Number-row keys (the Lua bind parser does not accept the code:NN keycode syntax)
-- Switch workspaces with mainMod + [0-9]
for i, code in ipairs({ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }) do
hl.bind(mainMod .. " + code:" .. code, hl.dsp.focus({ workspace = i })) -- code:10 = key 1 ... code:19 = key 0
for i, key in ipairs({ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }) do
hl.bind(mainMod .. " + " .. key, hl.dsp.focus({ workspace = i }))
end
-- Move active window and follow to workspace mainMod + SHIFT + [0-9]
for i, code in ipairs({ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }) do
hl.bind(mainMod .. " + SHIFT + code:" .. code, hl.dsp.window.move({ workspace = i, follow = true }))
for i, key in ipairs({ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }) do
hl.bind(mainMod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i, follow = true }))
end
hl.bind(mainMod .. " + SHIFT + bracketleft", hl.dsp.window.move({ workspace = -1, follow = true })) -- brackets [
hl.bind(mainMod .. " + SHIFT + bracketright", hl.dsp.window.move({ workspace = 1, follow = true })) -- brackets ]
-- Move active window to a workspace silently mainMod + CTRL + [0-9]
for i, code in ipairs({ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 }) do
hl.bind(mainMod .. " + CTRL + code:" .. code, hl.dsp.window.move({ workspace = i, follow = false }))
for i, key in ipairs({ "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }) do
hl.bind(mainMod .. " + CTRL + " .. key, hl.dsp.window.move({ workspace = i, follow = false }))
end
hl.bind(mainMod .. " + CTRL + bracketleft", hl.dsp.window.move({ workspace = -1, follow = false })) -- brackets [
hl.bind(mainMod .. " + CTRL + bracketright", hl.dsp.window.move({ workspace = 1, follow = false })) -- brackets ]

View File

@@ -4,6 +4,11 @@
music_icon="$HOME/.config/swaync/icons/music.png"
# Use the app's own icon for known players
case "$(playerctl metadata --format '{{playerName}}' 2>/dev/null)" in
Plexamp) music_icon="com.plexamp.Plexamp" ;;
esac
# Play the next track
play_next() {
playerctl next

View File

@@ -0,0 +1,29 @@
#!/bin/bash
# Keeps hypridle from locking the screen while Sunshine is streaming.
# Hooked up as Sunshine's global_prep_cmd, so it runs for every app:
# start -> hold a systemd idle inhibitor (hypridle respects these) and
# unlock hyprlock if the session was already locked
# stop -> release the inhibitor so normal idle locking resumes
pidfile="${XDG_RUNTIME_DIR:-/tmp}/sunshine-idle-inhibit.pid"
case "$1" in
start)
if ! { [ -f "$pidfile" ] && kill -0 "$(cat "$pidfile")" 2>/dev/null; }; then
setsid systemd-inhibit --what=idle --who=Sunshine --why="Streaming" \
sleep infinity >/dev/null 2>&1 &
echo $! > "$pidfile"
fi
# SIGUSR1 tells hyprlock to unlock
pkill -USR1 -x hyprlock
;;
stop)
[ -f "$pidfile" ] && kill "$(cat "$pidfile")" 2>/dev/null
rm -f "$pidfile"
;;
*)
echo "Usage: $0 start|stop" >&2
exit 1
;;
esac
exit 0

View File

@@ -1,143 +1,37 @@
#!/bin/bash
# HyprFlux — https://github.com/ahmad9059/HyprFlux
# Scripts for volume controls for audio and mic
# Volume controls for audio and mic via wpctl (PipeWire).
# Quickshell's VolumeOsd watches PipeWire directly, so no notifications here.
iDIR="$HOME/.config/swaync/icons"
sDIR="$HOME/.config/hypr/scripts"
SINK="@DEFAULT_AUDIO_SINK@"
SOURCE="@DEFAULT_AUDIO_SOURCE@"
STEP="5%"
# Get Volume
# Print volume as an integer percent, or "Muted"
get_volume() {
volume=$(pamixer --get-volume)
if [[ "$volume" -eq "0" ]]; then
local out
out=$(wpctl get-volume "$1")
if [[ "$out" == *MUTED* ]]; then
echo "Muted"
else
echo "$volume %"
awk '{ printf "%d %%\n", $2 * 100 + 0.5 }' <<< "$out"
fi
}
# Get icons
get_icon() {
current=$(get_volume)
if [[ "$current" == "Muted" ]]; then
echo "$iDIR/volume-mute.png"
elif [[ "${current%\%}" -le 30 ]]; then
echo "$iDIR/volume-low.png"
elif [[ "${current%\%}" -le 60 ]]; then
echo "$iDIR/volume-mid.png"
else
echo "$iDIR/volume-high.png"
fi
is_muted() {
[[ "$(wpctl get-volume "$1")" == *MUTED* ]]
}
# Notify
notify_user() {
if [[ "$(get_volume)" == "Muted" ]]; then
notify-send -e -h string:x-canonical-private-synchronous:volume_notif -h boolean:SWAYNC_BYPASS_DND:true -u low -i "$(get_icon)" " Volume:" " Muted"
else
notify-send -e -h int:value:"$(get_volume | sed 's/%//')" -h string:x-canonical-private-synchronous:volume_notif -h boolean:SWAYNC_BYPASS_DND:true -u low -i "$(get_icon)" " Volume Level:" " $(get_volume)" &&
"$sDIR/Sounds.sh" --volume
fi
}
# Raising/lowering while muted unmutes first, matching the old behaviour
inc() { is_muted "$1" && wpctl set-mute "$1" 0 || wpctl set-volume -l 1.0 "$1" "$STEP+"; }
dec() { is_muted "$1" && wpctl set-mute "$1" 0 || wpctl set-volume "$1" "$STEP-"; }
# Increase Volume
inc_volume() {
if [ "$(pamixer --get-mute)" == "true" ]; then
toggle_mute
else
pamixer -i 5 --allow-boost --set-limit 150 && notify_user
fi
}
# Decrease Volume
dec_volume() {
if [ "$(pamixer --get-mute)" == "true" ]; then
toggle_mute
else
pamixer -d 5 && notify_user
fi
}
# Toggle Mute
toggle_mute() {
if [ "$(pamixer --get-mute)" == "false" ]; then
pamixer -m && notify-send -e -u low -h boolean:SWAYNC_BYPASS_DND:true -i "$iDIR/volume-mute.png" " Mute"
elif [ "$(pamixer --get-mute)" == "true" ]; then
pamixer -u && notify-send -e -u low -h boolean:SWAYNC_BYPASS_DND:true -i "$(get_icon)" " Volume:" " Switched ON"
fi
}
# Toggle Mic
toggle_mic() {
if [ "$(pamixer --default-source --get-mute)" == "false" ]; then
pamixer --default-source -m && notify-send -e -u low -h boolean:SWAYNC_BYPASS_DND:true -i "$iDIR/microphone-mute.png" " Microphone:" " Switched OFF"
elif [ "$(pamixer --default-source --get-mute)" == "true" ]; then
pamixer -u --default-source u && notify-send -e -u low -h boolean:SWAYNC_BYPASS_DND:true -i "$iDIR/microphone.png" " Microphone:" " Switched ON"
fi
}
# Get Mic Icon
get_mic_icon() {
current=$(pamixer --default-source --get-volume)
if [[ "$current" -eq "0" ]]; then
echo "$iDIR/microphone-mute.png"
else
echo "$iDIR/microphone.png"
fi
}
# Get Microphone Volume
get_mic_volume() {
volume=$(pamixer --default-source --get-volume)
if [[ "$volume" -eq "0" ]]; then
echo "Muted"
else
echo "$volume %"
fi
}
# Notify for Microphone
notify_mic_user() {
volume=$(get_mic_volume)
icon=$(get_mic_icon)
notify-send -e -h int:value:"$volume" -h "string:x-canonical-private-synchronous:volume_notif" -h boolean:SWAYNC_BYPASS_DND:true -u low -i "$icon" " Mic Level:" " $volume"
}
# Increase MIC Volume
inc_mic_volume() {
if [ "$(pamixer --default-source --get-mute)" == "true" ]; then
toggle_mic
else
pamixer --default-source -i 5 && notify_mic_user
fi
}
# Decrease MIC Volume
dec_mic_volume() {
if [ "$(pamixer --default-source --get-mute)" == "true" ]; then
toggle-mic
else
pamixer --default-source -d 5 && notify_mic_user
fi
}
# Execute accordingly
if [[ "$1" == "--get" ]]; then
get_volume
elif [[ "$1" == "--inc" ]]; then
inc_volume
elif [[ "$1" == "--dec" ]]; then
dec_volume
elif [[ "$1" == "--toggle" ]]; then
toggle_mute
elif [[ "$1" == "--toggle-mic" ]]; then
toggle_mic
elif [[ "$1" == "--get-icon" ]]; then
get_icon
elif [[ "$1" == "--get-mic-icon" ]]; then
get_mic_icon
elif [[ "$1" == "--mic-inc" ]]; then
inc_mic_volume
elif [[ "$1" == "--mic-dec" ]]; then
dec_mic_volume
else
get_volume
fi
case "$1" in
--inc) inc "$SINK" ;;
--dec) dec "$SINK" ;;
--toggle) wpctl set-mute "$SINK" toggle ;;
--toggle-mic) wpctl set-mute "$SOURCE" toggle ;;
--mic-inc) inc "$SOURCE" ;;
--mic-dec) dec "$SOURCE" ;;
--get-mic) get_volume "$SOURCE" ;;
*) get_volume "$SINK" ;;
esac

View File

@@ -1,16 +1,17 @@
pragma Singleton
import QtQuick
// Tailscale, Mullvad, and ProtonVPN all fight over the default route/DNS if
// more than one is active at once (see VpnIndicator.qml's header comment) —
// only one of the three should ever actually be connected. This singleton
// is the shared signal between the two widget files: VpnIndicator.qml
// writes Mullvad/ProtonVPN's live connection state here as it polls them;
// TailscaleIndicator.qml reads it to back off (`tailscale down`) rather
// than stay up alongside either one, and to refuse turning Tailscale back
// on while one is still connected.
// Tailscale, Mullvad, ProtonVPN, and the home UniFi WireGuard tunnel all
// fight over the default route/DNS if more than one is active at once (see
// VpnIndicator.qml's header comment) — only one should ever actually be
// connected. This singleton is the shared signal between the two widget
// files: VpnIndicator.qml writes each tunnel's live connection state here
// as it polls them; TailscaleIndicator.qml reads it to back off (`tailscale
// down`) rather than stay up alongside one, and to refuse turning Tailscale
// back on while one is still connected.
QtObject {
property bool mullvadConnected: false
property bool protonConnected: false
readonly property bool commercialVpnActive: mullvadConnected || protonConnected
property bool homeWgConnected: false
readonly property bool commercialVpnActive: mullvadConnected || protonConnected || homeWgConnected
}

View File

@@ -32,7 +32,7 @@ RowLayout {
icon: Qt.resolvedUrl("../../assets/sunshine.svg"),
binary: "sunshine",
launch: ["systemctl", "--user", "start", "app-dev.lizardbyte.app.Sunshine.service"],
secondary: ["xdg-open", "https://xol.twhomelab.local:47990"],
secondary: ["xdg-open", "https://xol.twhomelab.tailnet:47990"],
},
]

View File

@@ -54,7 +54,7 @@ Item {
// Set when the VPN-exclusivity guard (below) forces Tailscale down —
// as opposed to you turning it off yourself — so it knows to bring
// Tailscale back up automatically once Mullvad/ProtonVPN disconnect,
// Tailscale back up automatically once the other VPN disconnects,
// without overriding a deliberate manual "off".
property bool _forcedDownByVpn: false
@@ -204,7 +204,7 @@ Item {
return
}
if (VpnExclusivity.commercialVpnActive) {
root.actionStatus = "Disconnect Mullvad/ProtonVPN first"
root.actionStatus = "Disconnect the active VPN first"
actionStatusTimer.restart()
return
}

View File

@@ -137,15 +137,25 @@ Item {
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
// Scroll distance is accumulated rather than stepping per event:
// a mouse wheel sends one 120-unit event per notch, but a touchpad
// sends a stream of tiny events, which at a fixed step per event
// made touchpad scrolling far too fast. One notch's worth (120) of
// scrolling = one volume step, regardless of input device.
property real wheelAccum: 0
readonly property real volumeStep: 0.05
readonly property real unitsPerStep: 120
onClicked: {
panel.visible = !panel.visible
grab.active = panel.visible
}
onWheel: (wheel) => {
if (!root.sink || !root.sink.audio) return
const step = 0.05
const delta = wheel.angleDelta.y > 0 ? step : -step
root.sink.audio.volume = Math.max(0, Math.min(1, root.sink.audio.volume + delta))
wheelAccum += wheel.angleDelta.y
const steps = Math.trunc(wheelAccum / unitsPerStep)
if (steps === 0) return
wheelAccum -= steps * unitsPerStep
root.sink.audio.volume = Math.max(0, Math.min(1, root.sink.audio.volume + steps * volumeStep))
}
}

View File

@@ -18,6 +18,11 @@ import "../../config"
// - ProtonVPN: official `protonvpn` CLI (proton-vpn-cli package), plain-
// text status only — parsed defensively since its exact wording isn't
// guaranteed across versions.
// - Home (UniFi): the UniFi gateway's built-in WireGuard server, driven
// through a NetworkManager profile via `nmcli` (no sudo needed). The
// row's "Add config" button imports the client .conf UniFi generates
// (via scripts/ImportUnifiWireguard.sh --pick), which names the profile
// to match homeWgConnection below.
Item {
id: root
implicitWidth: label.implicitWidth + 4
@@ -39,9 +44,17 @@ Item {
property string protonState: "disconnected"
property string protonDetail: ""
readonly property bool anyConnected: mullvadState === "connected" || protonState === "connected"
// NetworkManager profile name for the home UniFi WireGuard tunnel.
// "Installed" here means the profile exists — `nmcli connection show`
// exits non-zero for an unknown name.
property string homeWgConnection: "unifi-home"
property bool homeWgInstalled: true
property string homeWgState: "disconnected"
property string homeWgDetail: ""
// Set while switching from one provider to the other: holds which one
readonly property bool anyConnected: mullvadState === "connected" || protonState === "connected" || homeWgState === "connected"
// Set while switching from one provider to another: holds which one
// to connect once the current one has finished disconnecting.
property string pendingConnect: ""
@@ -51,6 +64,11 @@ Item {
// show up hanging under the other's row.
property string mullvadActionStatus: ""
property string protonActionStatus: ""
property string homeWgActionStatus: ""
property bool homeWgImporting: homeWgImportProc.running
// Whether Home's options (Replace config) are expanded — kept across
// panel opens, like mullvadOptionsOpen.
property bool homeWgOptionsOpen: false
// --- Mullvad advanced options (auto-connect, relay location, multihop) --
// ProtonVPN's CLI is plain-text-only and far less predictable across
@@ -114,6 +132,20 @@ Item {
VpnExclusivity.protonConnected = root.protonState === "connected"
}
// `nmcli -g GENERAL.STATE,IP4.ADDRESS connection show <name>` prints
// nothing for an inactive profile, otherwise "activated"/"activating"/
// "deactivating" followed by the tunnel address(es).
function applyHomeWgStatus(text) {
const lines = String(text || "").trim().split("\n").map(function(l) { return l.trim() }).filter(Boolean)
const raw = (lines[0] || "").toLowerCase()
root.homeWgState = raw === "activated" ? "connected"
: raw === "activating" ? "connecting"
: raw === "deactivating" ? "disconnecting"
: "disconnected"
root.homeWgDetail = root.homeWgState === "connected" && lines[1] ? lines[1].split(" | ")[0] : ""
VpnExclusivity.homeWgConnected = root.homeWgState === "connected"
}
function applyMullvadAutoConnect(text) {
const m = String(text || "").match(/autoconnect:\s*(on|off)/i)
root.mullvadAutoConnect = !!m && m[1].toLowerCase() === "on"
@@ -225,33 +257,61 @@ Item {
}
}
function refreshHomeWg() {
if (!homeWgStatusProc.running) homeWgStatusProc.running = true
}
function refresh() {
root.refreshMullvad()
root.refreshProton()
root.refreshHomeWg()
}
// Disconnects every provider other than `keep` that isn't already
// down; returns whether it had to, so the caller can defer its own
// connect to root.resumePendingConnect() instead of racing it.
function stopOthers(keep) {
let stopping = false
if (keep !== "mullvad" && root.mullvadState !== "disconnected") { root.disconnectMullvad(); stopping = true }
if (keep !== "protonvpn" && root.protonState !== "disconnected") { root.disconnectProton(); stopping = true }
if (keep !== "homewg" && root.homeWgState !== "disconnected") { root.disconnectHomeWg(); stopping = true }
return stopping
}
// Called from each disconnect's onExited. The provider that just went
// down is marked disconnected optimistically before this runs — its
// status poll is async, and without that the pending connect would see
// it still "connected" and try to disconnect it all over again.
function resumePendingConnect() {
const next = root.pendingConnect
root.pendingConnect = ""
if (next === "mullvad") root.connectMullvad()
else if (next === "protonvpn") root.connectProton()
else if (next === "homewg") root.connectHomeWg()
}
function connectMullvad() {
if (mullvadConnectProc.running || mullvadDisconnectProc.running) return
if (root.protonState !== "disconnected") {
root.pendingConnect = "mullvad"
root.disconnectProton()
return
}
if (root.stopOthers("mullvad")) { root.pendingConnect = "mullvad"; return }
root.pendingConnect = ""
mullvadConnectProc.running = true
}
function connectProton() {
if (protonConnectProc.running || protonDisconnectProc.running) return
if (root.mullvadState !== "disconnected") {
root.pendingConnect = "protonvpn"
root.disconnectMullvad()
return
}
if (root.stopOthers("protonvpn")) { root.pendingConnect = "protonvpn"; return }
root.pendingConnect = ""
protonConnectProc.running = true
}
function connectHomeWg() {
if (homeWgConnectProc.running || homeWgDisconnectProc.running) return
if (root.stopOthers("homewg")) { root.pendingConnect = "homewg"; return }
root.pendingConnect = ""
root.homeWgState = "connecting"
homeWgConnectProc.running = true
}
function disconnectMullvad() {
if (mullvadDisconnectProc.running) return
mullvadDisconnectProc.running = true
@@ -262,6 +322,12 @@ Item {
protonDisconnectProc.running = true
}
function disconnectHomeWg() {
if (homeWgDisconnectProc.running) return
root.homeWgState = "disconnecting"
homeWgDisconnectProc.running = true
}
function toggleMullvad() {
if (mullvadConnectProc.running || mullvadDisconnectProc.running) return
if (root.mullvadState === "connected") root.disconnectMullvad()
@@ -274,7 +340,23 @@ Item {
else root.connectProton()
}
// Tailscale fights Mullvad/ProtonVPN for the default route/DNS, so fire
// Opens a file chooser for the UniFi client .conf and imports it as
// homeWgConnection, replacing any existing profile. The chooser takes
// focus, which clears the focus grab and closes the panel — so the
// import reopens it when done to show the result.
function importHomeWgConfig() {
if (homeWgImportProc.running || root.homeWgState !== "disconnected") return
root.homeWgActionStatus = ""
homeWgImportProc.running = true
}
function toggleHomeWg() {
if (homeWgConnectProc.running || homeWgDisconnectProc.running) return
if (root.homeWgState === "connected") root.disconnectHomeWg()
else root.connectHomeWg()
}
// Tailscale fights Mullvad/ProtonVPN/home WireGuard for the default route/DNS, so fire
// this right after either one finishes connecting — a harmless no-op
// if Tailscale was already down. TailscaleIndicator.qml also backs
// itself off reactively (via VpnExclusivity) as a backstop, but doing
@@ -336,10 +418,11 @@ Item {
if (exitCode !== 0) {
root.mullvadActionStatus = root.shortLine(mullvadDisconnectErr.text || "Disconnect failed")
mullvadActionStatusTimer.restart()
} else {
root.mullvadState = "disconnected"
}
root.refreshMullvad()
if (root.pendingConnect === "mullvad") { root.pendingConnect = ""; root.connectMullvad() }
else if (root.pendingConnect === "protonvpn") { root.pendingConnect = ""; root.connectProton() }
root.resumePendingConnect()
}
}
@@ -471,10 +554,11 @@ Item {
if (exitCode !== 0) {
root.protonActionStatus = root.shortLine(protonDisconnectErr.text || "Disconnect failed")
protonActionStatusTimer.restart()
} else {
root.protonState = "disconnected"
}
root.refreshProton()
if (root.pendingConnect === "mullvad") { root.pendingConnect = ""; root.connectMullvad() }
else if (root.pendingConnect === "protonvpn") { root.pendingConnect = ""; root.connectProton() }
root.resumePendingConnect()
}
}
@@ -485,6 +569,75 @@ Item {
onTriggered: if (protonStatusProc.running) protonStatusProc.running = false
}
// --- Home WireGuard (UniFi, via NetworkManager) ------------------------
Process {
id: homeWgStatusProc
command: ["nmcli", "-g", "GENERAL.STATE,IP4.ADDRESS", "connection", "show", root.homeWgConnection]
stdout: StdioCollector {
id: homeWgStatusCollector
onStreamFinished: root.applyHomeWgStatus(homeWgStatusCollector.text)
}
onExited: (exitCode, exitStatus) => {
root.homeWgInstalled = exitCode === 0
if (!root.homeWgInstalled) root.applyHomeWgStatus("")
}
}
// `connection up` blocks until NetworkManager reports the profile
// activated (or gives up), so its exit code is the real result. Note
// WireGuard "activated" only means the interface is configured — the
// handshake itself happens lazily on first traffic.
Process {
id: homeWgConnectProc
command: ["nmcli", "connection", "up", root.homeWgConnection]
stdout: StdioCollector {}
stderr: StdioCollector { id: homeWgConnectErr }
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
root.homeWgActionStatus = root.shortLine(homeWgConnectErr.text || "Connect failed")
homeWgActionStatusTimer.restart()
} else if (!tailscaleDownProc.running) {
tailscaleDownProc.running = true
}
root.refreshHomeWg()
}
}
Process {
id: homeWgDisconnectProc
command: ["nmcli", "connection", "down", root.homeWgConnection]
stdout: StdioCollector {}
stderr: StdioCollector { id: homeWgDisconnectErr }
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
root.homeWgActionStatus = root.shortLine(homeWgDisconnectErr.text || "Disconnect failed")
homeWgActionStatusTimer.restart()
} else {
root.homeWgState = "disconnected"
}
root.refreshHomeWg()
root.resumePendingConnect()
}
}
Process {
id: homeWgImportProc
command: ["bash", Quickshell.shellPath("scripts/ImportUnifiWireguard.sh"),
"--pick", root.homeWgConnection, "--replace"]
stdout: StdioCollector {}
stderr: StdioCollector { id: homeWgImportErr }
onExited: (exitCode, exitStatus) => {
// 3 = chooser cancelled — nothing to report.
if (exitCode === 0) root.homeWgActionStatus = "Config imported"
else if (exitCode !== 3) root.homeWgActionStatus = root.shortLine(homeWgImportErr.text || "Import failed")
if (root.homeWgActionStatus !== "") homeWgActionStatusTimer.restart()
root.refreshHomeWg()
panel.visible = true
grab.active = true
}
}
// --- Shared timers -----------------------------------------------------
Timer {
@@ -510,6 +663,13 @@ Item {
onTriggered: root.protonActionStatus = ""
}
Timer {
id: homeWgActionStatusTimer
interval: 4000
repeat: false
onTriggered: root.homeWgActionStatus = ""
}
// --- Bar icon ------------------------------------------------------------
// A fixed, wallpaper-independent green rather than Colors.primary — the
@@ -615,6 +775,7 @@ Item {
property string detailText: ""
property string state: "disconnected"
property bool installed: true
property string missingText: "Not installed"
// When true, shows a chevron that expands a provider-specific
// options section below this row (used for Mullvad's advanced
// options; ProtonVPN's row leaves this false).
@@ -664,7 +825,7 @@ Item {
}
Text {
Layout.fillWidth: true
text: !provRow.installed ? "Not installed"
text: !provRow.installed ? provRow.missingText
: provRow.state === "connecting" ? "Connecting…"
: provRow.state === "disconnecting" ? "Disconnecting…"
: provRow.state === "connected" ? (provRow.detailText || "Connected")
@@ -848,7 +1009,124 @@ Item {
Column {
width: parent.width
spacing: 2
// Room for the cards' outlines (drawn 3px outside them)
// so two expanded ones don't touch.
spacing: 8
// Same collapsible card as Mullvad below — framed only
// while its options (Replace config) are expanded.
Item {
readonly property bool _framed: root.homeWgOptionsOpen && root.homeWgInstalled
width: parent.width
implicitHeight: homeWgSectionColumn.implicitHeight
Behavior on implicitHeight {
NumberAnimation { duration: Appearance.animationDuration; easing.type: Easing.OutCubic }
}
// Outline offset outside the content rather than drawn on
// its own edge — same as Volume.qml's DeviceRow hover —
// so the row doesn't shift when the options open.
Rectangle {
anchors.fill: parent
anchors.margins: -3
radius: 0
color: "transparent"
border.color: Colors.foreground
border.width: parent._framed ? 1 : 0
}
// Clipped separately so the outline above isn't.
Item {
anchors.fill: parent
clip: true
Column {
id: homeWgSectionColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
spacing: 8
ProviderRow {
label: "Home (UniFi)"
subIcon: "home"
state: root.homeWgState
detailText: root.homeWgDetail
installed: root.homeWgInstalled
missingText: "Not set up"
expandable: root.homeWgInstalled
expanded: root.homeWgOptionsOpen
onToggle: root.toggleHomeWg()
onExpandToggle: root.homeWgOptionsOpen = !root.homeWgOptionsOpen
}
StatusLine { message: root.homeWgActionStatus }
// Imports (or replaces) the UniFi client .conf through a
// file chooser. Shown outright until a profile exists
// (so first-time setup isn't hidden behind the
// chevron), then only inside the expanded options.
// Hidden while connected — replacing deletes the
// profile, which would drop the tunnel.
Item {
width: parent.width
implicitHeight: importRow.implicitHeight + 8
visible: root.homeWgState === "disconnected"
&& (!root.homeWgInstalled || root.homeWgOptionsOpen)
Row {
id: importRow
anchors.left: parent.left
anchors.leftMargin: 34
anchors.verticalCenter: parent.verticalCenter
spacing: 4
opacity: root.homeWgImporting ? 0.5 : 1
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.homeWgInstalled ? "sync" : "upload_file"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 13
color: importMouse.containsMouse ? Colors.foreground : Colors.primary
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.homeWgImporting ? "Choosing file…"
: root.homeWgInstalled ? "Replace config" : "Add config"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
font.underline: importMouse.containsMouse
color: importMouse.containsMouse ? Colors.foreground : Colors.primary
}
}
MouseArea {
id: importMouse
anchors.fill: importRow
anchors.margins: -4
enabled: !root.homeWgImporting
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.importHomeWgConfig()
}
}
Text {
x: 34
width: parent.width - x
visible: root.homeWgOptionsOpen && root.homeWgInstalled
&& root.homeWgState !== "disconnected"
text: "Disconnect to replace config"
bottomPadding: 6
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
wrapMode: Text.WordWrap
}
}
}
}
// Mullvad's connect row and its advanced options share a
// single bordered card — the border wraps the whole
@@ -856,14 +1134,11 @@ Item {
// grows to include the options/picker as they open,
// rather than the options having a separate box of
// their own below an unbordered connect row.
Rectangle {
// Only outlined while the options are expanded.
Item {
readonly property bool _framed: root.mullvadOptionsOpen && root.mullvadInstalled
width: parent.width
radius: 0
color: Colors.background
border.color: Colors.muted
border.width: 1
implicitHeight: mullvadSectionColumn.implicitHeight + 16
clip: true
implicitHeight: mullvadSectionColumn.implicitHeight
// Without this, the card snapped to its new size
// instantly while the popup around it was still
// animating open, so the border visibly lagged
@@ -873,12 +1148,28 @@ Item {
NumberAnimation { duration: Appearance.animationDuration; easing.type: Easing.OutCubic }
}
// Outline offset outside the content rather than drawn on
// its own edge — same as Volume.qml's DeviceRow hover —
// so the row doesn't shift when the options open.
Rectangle {
anchors.fill: parent
anchors.margins: -3
radius: 0
color: "transparent"
border.color: Colors.foreground
border.width: parent._framed ? 1 : 0
}
// Clipped separately so the outline above isn't.
Item {
anchors.fill: parent
clip: true
Column {
id: mullvadSectionColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 8
spacing: 8
ProviderRow {
@@ -1041,6 +1332,7 @@ Item {
}
}
}
}
ProviderRow {
label: "ProtonVPN"
@@ -1056,7 +1348,7 @@ Item {
Text {
width: parent.width
text: "Connecting one disconnects the other — only one VPN tunnel at a time."
text: "Connecting one disconnects the others — only one VPN tunnel at a time."
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted

View File

@@ -37,7 +37,8 @@ RowLayout {
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Hyprland.dispatch("workspace " + modelData.name)
// Hyprland's Lua config expects Lua syntax from dispatch
onClicked: Hyprland.dispatch("hl.dsp.focus({ workspace = " + JSON.stringify(modelData.id > 0 ? modelData.id : modelData.name) + " })")
}
}
}

View File

@@ -11,16 +11,13 @@ PanelWindow {
color: "transparent"
exclusiveZone: 0
// Top-right, just under the bar — same fixed distance from the bar and
// screen edge (Appearance.spacing) that every popup opened from the bar
// itself uses, so this lines up with them instead of floating
// bottom-center.
anchors { top: true; right: true }
margins.top: Appearance.spacing
margins.right: Appearance.spacing
// Bottom-center: anchoring only the bottom edge lets the layer shell
// center it horizontally.
anchors { bottom: true }
margins.bottom: Appearance.spacing * 4
implicitWidth: 272
implicitHeight: 54
implicitWidth: 300
implicitHeight: 60
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "quickshell:osd"
@@ -49,13 +46,13 @@ PanelWindow {
RowLayout {
anchors.fill: parent
anchors.margins: 14
spacing: 12
anchors.margins: 15
spacing: 13
Text {
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 18
font.pixelSize: 21
color: Colors.foreground
text: {
const audio = osd.sink ? osd.sink.audio : null
@@ -67,7 +64,7 @@ PanelWindow {
Item {
id: slider
Layout.fillWidth: true
implicitHeight: 16
implicitHeight: 18
readonly property real value: osd.sink && osd.sink.audio ? osd.sink.audio.volume : 0
readonly property bool enabled: !!osd.sink
@@ -76,7 +73,7 @@ PanelWindow {
id: track
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 4
height: 5
radius: 0
color: Colors.muted
opacity: 0.35
@@ -85,15 +82,15 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
width: track.width * Math.max(0, Math.min(1, slider.value))
height: 4
height: 5
radius: 0
color: Colors.primary
opacity: slider.enabled ? 1 : 0.4
}
Rectangle {
width: 12
height: 12
radius: 6
width: 14
height: 14
radius: 7
color: Colors.primary
opacity: slider.enabled ? 1 : 0.4
anchors.verticalCenter: parent.verticalCenter
@@ -113,10 +110,10 @@ PanelWindow {
}
Text {
Layout.preferredWidth: 32
Layout.preferredWidth: 38
horizontalAlignment: Text.AlignRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
font.pixelSize: 13
color: Colors.muted
text: Math.round(slider.value * 100) + "%"
}

View File

@@ -159,6 +159,21 @@ if [ $have_mullvad -eq 0 ] && [ $have_proton -eq 0 ]; then
warn "neither Mullvad nor ProtonVPN CLI is installed — VpnIndicator.qml will show both as 'Not installed'"
fi
# Keep in sync with homeWgConnection in VpnIndicator.qml.
if nmcli -g connection.type connection show unifi-home >/dev/null 2>&1; then
pass "NetworkManager profile 'unifi-home' exists — used by VpnIndicator.qml (Home/UniFi row)"
else
warn "no NetworkManager profile 'unifi-home' — import one from the VPN panel's 'Add config' button (optional)"
fi
# The 'Add config' button's file chooser (scripts/PickFile.py) talks to
# xdg-desktop-portal over D-Bus via PyGObject.
if python3 -c "from gi.repository import Gio" >/dev/null 2>&1; then
pass "PyGObject available — used by PickFile.py (VPN 'Add config' file chooser)"
else
warn "PyGObject (python-gobject) missing — the VPN panel's 'Add config' button can't open a file chooser"
fi
# --- 4. VPN daemons + functional status (Mullvad/ProtonVPN/Tailscale) -------
# The exclusivity logic (VpnExclusivity.qml backing off Tailscale when a
# commercial VPN connects) is pure QML and ships with the dotfiles — it

View File

@@ -0,0 +1,84 @@
#!/bin/bash
# Imports a UniFi WireGuard VPN Server client config into NetworkManager
# under the profile name VpnIndicator.qml expects (homeWgConnection,
# "unifi-home" by default), so the bar's VPN panel can toggle it.
#
# Get the .conf from UniFi Network: Settings → VPN → VPN Server → your
# WireGuard server → Add Client → Download.
#
# Usage: ./ImportUnifiWireguard.sh <client.conf> [profile-name] [--replace]
# ./ImportUnifiWireguard.sh --pick [profile-name] [--replace]
#
# --pick opens a file chooser (PickFile.py) instead of taking a path — this
# is what the VPN panel's "Add config" button runs. Exits 3 if cancelled.
#
# NetworkManager names both the profile and the interface after the file's
# basename, so the config is copied to <profile-name>.conf first (interface
# names max out at 15 chars). The private key ends up in a root-only file
# under /etc/NetworkManager/system-connections — delete the downloaded
# .conf afterwards.
set -euo pipefail
replace=0
pick=0
args=()
for a in "$@"; do
case "$a" in
--replace) replace=1 ;;
--pick) pick=1 ;;
-h|--help) sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) args+=("$a") ;;
esac
done
if [ $pick -eq 1 ]; then
conf=""
name="${args[0]:-unifi-home}"
rc=0
conf=$("$(dirname "$0")/PickFile.py" "Select UniFi WireGuard config" "*.conf") || rc=$?
if [ $rc -eq 1 ]; then echo "Cancelled" >&2; exit 3; fi
if [ $rc -ne 0 ]; then echo "Couldn't open a file chooser" >&2; exit 1; fi
else
conf="${args[0]:-}"
name="${args[1]:-unifi-home}"
fi
if [ -z "$conf" ] || [ ! -f "$conf" ]; then
echo "Usage: $0 <client.conf> [profile-name] [--replace]" >&2
exit 1
fi
if ! [[ "$name" =~ ^[A-Za-z0-9_-]{1,15}$ ]]; then
echo "Profile name must be 1-15 chars of [A-Za-z0-9_-] (it doubles as the interface name)" >&2
exit 1
fi
if nmcli -g connection.id connection show "$name" >/dev/null 2>&1; then
if [ $replace -eq 1 ]; then
nmcli connection delete "$name" >/dev/null
else
echo "A NetworkManager profile named '$name' already exists — rerun with --replace to overwrite it" >&2
exit 1
fi
fi
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
install -m 600 "$conf" "$tmpdir/$name.conf"
if ! grep -qi '^\[Interface\]' "$tmpdir/$name.conf" || ! grep -qi '^\[Peer\]' "$tmpdir/$name.conf"; then
echo "$(basename "$conf") doesn't look like a WireGuard config" >&2
exit 1
fi
nmcli connection import type wireguard file "$tmpdir/$name.conf"
# Only ever brought up deliberately from the bar — never on boot or on
# joining a network, where it'd silently fight Tailscale/Mullvad/ProtonVPN.
nmcli connection modify "$name" connection.autoconnect no
# Import itself defaults to autoconnect=yes, so NetworkManager may already
# have started bringing it up in the moment before the modify above.
nmcli connection down "$name" >/dev/null 2>&1 || true
echo
echo "Imported as '$name'. Toggle it from the VPN panel, or: nmcli connection up $name"
echo "You can now delete $conf"

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env python3
# Opens the xdg-desktop-portal file chooser and prints the picked file's
# path. Quickshell has no usable file dialog of its own (QtQuick.Dialogs'
# fallback renders inside the calling window, i.e. the tiny bar popup), and
# no zenity/kdialog is installed — the portal goes through
# xdg-desktop-portal-gtk, which is.
#
# Usage: PickFile.py [title] [glob ...]
# Exits 0 and prints the path on a pick, 1 on cancel, 2 on error.
import os
import sys
from urllib.parse import unquote, urlparse
from gi.repository import Gio, GLib
title = sys.argv[1] if len(sys.argv) > 1 else "Open File"
globs = sys.argv[2:]
try:
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
except GLib.Error as e:
print(f"No session bus: {e.message}", file=sys.stderr)
sys.exit(2)
# The portal answers on a Request object whose path is derived from our
# unique bus name and handle_token, so subscribe to it before calling —
# otherwise a fast response could arrive before we're listening.
token = f"quickshell_{os.getpid()}"
sender = bus.get_unique_name().lstrip(":").replace(".", "_")
handle = f"/org/freedesktop/portal/desktop/request/{sender}/{token}"
loop = GLib.MainLoop()
result = {"code": 2, "uris": []}
def on_response(_conn, _sender, _path, _iface, _signal, params):
code, results = params.unpack()
result["code"] = code
result["uris"] = results.get("uris", [])
loop.quit()
bus.signal_subscribe("org.freedesktop.portal.Desktop", "org.freedesktop.portal.Request",
"Response", handle, None, Gio.DBusSignalFlags.NONE, on_response)
options = {
"handle_token": GLib.Variant("s", token),
"modal": GLib.Variant("b", True),
}
filters = []
if globs:
filters.append((", ".join(globs), [(0, g) for g in globs]))
filters.append(("All files", [(0, "*")]))
options["filters"] = GLib.Variant("a(sa(us))", filters)
downloads = os.path.expanduser("~/Downloads")
if os.path.isdir(downloads):
options["current_folder"] = GLib.Variant("ay", downloads.encode() + b"\0")
try:
bus.call_sync("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop",
"org.freedesktop.portal.FileChooser", "OpenFile",
GLib.Variant("(ssa{sv})", ("", title, options)),
GLib.VariantType("(o)"), Gio.DBusCallFlags.NONE, -1, None)
except GLib.Error as e:
print(f"File chooser portal unavailable: {e.message}", file=sys.stderr)
sys.exit(2)
loop.run()
# Response codes: 0 = picked, 1 = cancelled, 2 = other failure.
if result["code"] != 0 or not result["uris"]:
sys.exit(1 if result["code"] == 1 else 2)
print(unquote(urlparse(result["uris"][0]).path))

View File

@@ -0,0 +1,50 @@
#!/bin/bash
# Run by update-system.sh inside the update terminal, before the system
# update — pulls the dotfiles repo and re-stows it so both machines pick up
# config changes on every update.
#
# Never fails the update: a pull that can't fast-forward (local commits or
# uncommitted changes in the way) or a stow conflict just prints a warning
# and leaves things as they were, then the system update carries on.
DOTFILES="$HOME/Projects/dotfiles"
warn() { printf '\033[1;33m%s\033[0m\n' "$*"; }
info() { printf '\033[1;36m%s\033[0m\n' "$*"; }
if [ ! -d "$DOTFILES/.git" ]; then
warn "Dotfiles repo not found at $DOTFILES — skipping."
exit 0
fi
info "Pulling dotfiles..."
# Fetch and merge separately so the warning says which one went wrong — the
# remote is SSH via Bitwarden's agent, so a failed fetch is almost always
# Bitwarden being closed or locked.
if ! git -C "$DOTFILES" fetch; then
warn "Couldn't reach the dotfiles remote — is Bitwarden open and unlocked? Skipping stow."
echo
exit 0
fi
# --ff-only so a diverged branch never produces a surprise merge commit;
# sort that out by hand instead.
if ! git -C "$DOTFILES" merge --ff-only; then
warn "Couldn't fast-forward dotfiles (local changes or diverged branch) — skipping stow."
echo
exit 0
fi
if ! command -v stow &> /dev/null; then
warn "stow not installed — skipping."
exit 0
fi
info "Applying dotfiles with stow..."
# -R restows: drops links for files removed from the repo and adds new ones.
if stow -R -d "$DOTFILES" -t "$HOME" dotfiles; then
info "Dotfiles applied."
else
warn "stow hit a conflict — nothing was changed. Fix it and re-run:"
warn " stow -R -d $DOTFILES -t ~ dotfiles"
fi
echo

View File

@@ -58,5 +58,9 @@ fi
# any output/errors with it.
WAIT_PROMPT="echo; read -p 'Update finished — press Enter to close...'"
foot -T update bash -c "$BANNER; $CMD; $WAIT_PROMPT"
# Pull + re-stow the dotfiles first, so config changes from the other machine
# land alongside the package update.
SYNC="$(printf '%q' "$(dirname "$0")/sync-dotfiles.sh")"
foot -T update bash -c "$BANNER; $SYNC; $CMD; $WAIT_PROMPT"
notify-send -i "$iDIR/profile-pic.jpg" -u low "$LABEL" 'has been updated.'

5
dotfiles/.config/sunshine/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
# Machine-local Sunshine state lives next to the stowed config; never commit it
credentials/
*.log
*.log.*
sunshine_state.json

View File

@@ -0,0 +1,47 @@
{
"env": {
"PATH": "$(PATH):$(HOME)/.local/bin"
},
"apps": [
{
"name": "Desktop",
"image-path": "desktop.png"
},
{
"name": "Low Res Desktop",
"image-path": "desktop.png",
"prep-cmd": [
{
"do": "xrandr --output HDMI-1 --mode 1920x1080",
"undo": "xrandr --output HDMI-1 --mode 1920x1200"
}
]
},
{
"name": "Steam Big Picture",
"detached": [
"setsid steam steam://open/bigpicture"
],
"prep-cmd": [
{
"do": "/home/thomas/.config/sunshine/scripts/steam-bigpicture-prep.sh",
"undo": "setsid steam steam://close/bigpicture"
}
],
"image-path": "steam.png"
},
{
"name": "Steam Big Picture (TV)",
"detached": [
"setsid steam steam://open/bigpicture"
],
"prep-cmd": [
{
"do": "/home/thomas/.config/sunshine/scripts/steam-bigpicture-tv-prep.sh",
"undo": "/home/thomas/.config/sunshine/scripts/steam-bigpicture-tv-undo.sh"
}
],
"image-path": "steam.png"
}
]
}

View File

@@ -0,0 +1,17 @@
#!/bin/sh
# Run before Sunshine launches Steam Big Picture: focus the headless virtual
# monitor and park the cursor on it, so Big Picture (which spawns on whatever
# monitor currently has focus/the mouse) opens there instead of wherever the
# mouse happened to be.
set -e
hyprctl dispatch 'hl.dsp.focus({monitor = "HEADLESS-1"})'
hyprctl dispatch 'hl.dsp.cursor.move({x = 5344, y = 540})'
# Sunshine only flips the system default audio sink to sink-sunshine-stereo
# after the streaming session/client handshake completes (~500ms after this
# script runs and Steam is spawned) — see sunshine.log timestamps. If Steam's
# own audio (SDL) initializes in that window, it binds to whatever was
# default at the time (the real HDMI output) and never re-homes even after
# Sunshine's swap. Set it here first so it's already correct before Steam
# ever launches; Sunshine's own (redundant) swap right after is harmless.
pactl set-default-sink sink-sunshine-stereo || true

View File

@@ -0,0 +1,18 @@
#!/bin/sh
# Run before Sunshine launches Steam Big Picture for the 4K TV target.
# Switches the headless virtual monitor from its default 1728x1080@60
# (used for the Steam Deck target) up to 3840x2160@60, then focuses it
# and parks the cursor there so Big Picture opens on it. Reverted by
# steam-bigpicture-tv-undo.sh once the stream ends.
set -e
# NOTE: this Hyprland build uses the Lua config layer, which rejects the
# usual `hyprctl keyword monitor ...` syntax ("keyword can't work with
# non-legacy parsers. Use eval.") — hl.monitor() via `hyprctl eval` is the
# runtime equivalent of the hl.monitor() call in layout/monitors.lua.
hyprctl eval 'hl.monitor({ output = "HEADLESS-1", mode = "3840x2160@60.00000", position = "4480x0", scale = 1.00000000, transform = 0, vrr = 0 })'
hyprctl dispatch 'hl.dsp.focus({monitor = "HEADLESS-1"})'
hyprctl dispatch 'hl.dsp.cursor.move({x = 6400, y = 1080})'
# See steam-bigpicture-prep.sh for why this runs here instead of relying on
# Sunshine's own post-handshake audio-sink swap.
pactl set-default-sink sink-sunshine-stereo || true

View File

@@ -0,0 +1,6 @@
#!/bin/sh
# Undo for steam-bigpicture-tv-prep.sh: close Big Picture and restore
# HEADLESS-1 to its default mode (the one the Steam Deck target expects).
set -e
setsid steam steam://close/bigpicture
hyprctl eval 'hl.monitor({ output = "HEADLESS-1", mode = "1728x1080@60.00000", position = "4480x0", scale = 1.00000000, transform = 0, vrr = 0 })'

View File

@@ -0,0 +1,5 @@
audio_sink = sink-sunshine-stereo
csrf_allowed_origins = https://xol.twhomelab.local:47990
nvenc_preset = 4
output_name = HEADLESS-1
global_prep_cmd = [{"do":"/home/thomas/.config/hypr/scripts/SunshineIdle.sh start","undo":"/home/thomas/.config/hypr/scripts/SunshineIdle.sh stop"}]

View File

@@ -9,5 +9,10 @@
# ~/.local/share/plex/Logs/Plex.log). This dir is first on $PATH, so it
# shadows /usr/bin/Plex for both a terminal launch and the .desktop entry
# (Exec=Plex is PATH-resolved) without touching the global env var.
# QT_QPA_PLATFORM: hypr env sets "wayland;xcb"; under Wayland the bundled
# QtWebEngine never runs the web client and the window stays blank.
unset QT_QUICK_CONTROLS_STYLE
export QT_QPA_PLATFORM=xcb
# Unset in the session; without it the X11 cursor in Plex is huge.
export XCURSOR_SIZE=24
exec /opt/plex-desktop/Plex.sh "$@"

View File

@@ -3,7 +3,12 @@
# same filename, so this is picked up by app launchers instead automatically
# — no association/mimeapps changes needed).
#
# Only change from upstream: Exec unsets QT_QUICK_CONTROLS_STYLE and calls
# QT_QPA_PLATFORM=xcb is kept from upstream: hypr env sets "wayland;xcb", and
# under Wayland the bundled QtWebEngine never runs the web client (blank
# window, no [Web] lines in Plex.log). XCURSOR_SIZE=24 is also kept from
# upstream: it is unset in the session, so the X11 cursor comes out huge.
#
# Change from upstream: Exec unsets QT_QUICK_CONTROLS_STYLE and calls
# the package's Plex.sh directly, bypassing $PATH entirely. Plex Desktop
# bundles its own private Qt/QML runtime that doesn't ship the
# "org.hyprland.style" QQC2 style hypr/core/env.lua sets globally for other
@@ -17,7 +22,7 @@
Type=Application
Name=Plex
Comment=Plex client for desktop computers
Exec=env -u QT_QUICK_CONTROLS_STYLE /opt/plex-desktop/Plex.sh
Exec=env -u QT_QUICK_CONTROLS_STYLE QT_QPA_PLATFORM=xcb XCURSOR_SIZE=24 /opt/plex-desktop/Plex.sh
Icon=tv.plex.PlexDesktop
Terminal=false
Categories=AudioVideo;Player;X-Media;

View File

@@ -1,3 +1,7 @@
Host gavin.twhomelab.local
HostName gavin.twhomelab.local
User thoams
Host 192.168.1.87
HostName 192.168.1.87
User thoams

176
system/packages-review.txt Normal file
View File

@@ -0,0 +1,176 @@
# ============================================================================
# New-install package review — generated from this machine on 2026-10-11
#
# HOW TO REVIEW
# - Keep a line -> it gets installed
# - Delete the line, or put a "#" at the start -> it gets skipped
# - Everything after "#" on a line is just a description for you
#
# Notes I flagged (look for "!!" below):
# - Hardware-specific packages (CPU microcode, NVIDIA driver) only make
# sense on matching hardware — the script can auto-detect these instead.
# - Some packages are leftovers from other setups (e.g. XFCE, waybar,
# orbit-wifi which you removed in commit 88484f8).
# ============================================================================
## ---------------- Official repos (pacman) ----------------
adw-gtk-theme # Unofficial GTK 3 port of the libadwaita theme
amd-ucode # Microcode update image for AMD CPUs !! AMD CPU only — auto-detect?
awww # An Answer to your Wayland Wallpaper Woes
baobab # A graphical directory tree analyzer
base # Minimal package set to define a basic Arch Linux installation
base-devel # Basic tools to build Arch Linux packages
blueman # GTK+ Bluetooth Manager
bluez # Daemons for the bluetooth protocol stack
bluez-utils # Development and debugging utilities for the bluetooth protocol stack
btop # A monitor of system resources, bpytop ported to C++
dolphin # KDE File Manager
dunst # Customizable and lightweight notification-daemon !! you use swaync for notifications
efibootmgr # Linux user-space application to modify the EFI Boot Manager
fastfetch # A feature-rich and performance oriented neofetch like system information tool
ffmpeg # Complete solution to record, convert and stream audio and video
filezilla # Fast and reliable FTP, FTPS and SFTP client
firefox # Fast, Private & Safe Web Browser
flatpak # Linux application sandboxing and distribution framework (formerly xdg-app)
foot # Fast, lightweight, and minimalistic Wayland terminal emulator
fzf # Command-line fuzzy finder
galculator # GTK based scientific calculator
#garcon # Freedesktop.org compliant menu library !! XFCE desktop leftover?
gdu # Fast disk usage analyzer
git # the fast distributed version control system
gnome-calendar # Simple and beautiful calendar application designed to perfectly fit the GNOME desktop
gnome-disk-utility # Disk Management Utility for GNOME
gnome-font-viewer # A font viewer utility for GNOME
gnome-keyring # Stores passwords and encryption keys
gnome-text-editor # A simple text editor for the GNOME desktop
grim # Screenshot utility for Wayland
gst-plugin-pipewire # Multimedia graph framework - pipewire plugin
gvfs # Virtual filesystem implementation for GIO
gvfs-smb # Virtual filesystem implementation for GIO - SMB/CIFS backend (Windows file sharing)
htop # Interactive process viewer
hypridle # hyprland’s idle daemon !! commented out in startup.lua
hyprland # a highly customizable dynamic tiling Wayland compositor
hyprlock # hyprland’s GPU-accelerated screen locking utility !! hypridle is commented out in startup.lua
hyprshot # Hyprland screenshot utility
imv # Image viewer for Wayland and X11
jq # Command-line JSON processor
kitty # A modern, hackable, featureful, OpenGL-based terminal emulator !! you use foot as terminal (kitty config not in dotfiles)
libgsf # Extensible I/O abstraction library for dealing with structured file formats
libmatroska # Matroska library
libpulse # A featureful, general-purpose sound server (client library)
libva-nvidia-driver # VA-API implementation that uses NVDEC as a backend !! NVIDIA GPU only — auto-detect?
linux # The Linux kernel and modules
linux-firmware # Firmware files for Linux - Default set
mkinitcpio # Modular initramfs image creation utility
mkvtoolnix-cli # Set of tools to create, edit and inspect Matroska files
mpv # a free, open source, and cross-platform media player
mullvad-vpn-daemon # Mullvad VPN client (daemon and CLI)
nano # Pico editor clone with enhancements
network-manager-applet # Applet for managing network connections
networkmanager # Network connection manager and user applications
noto-fonts # Google Noto TTF fonts
noto-fonts-cjk # Google Noto CJK fonts
noto-fonts-emoji # Google Noto Color Emoji font
noto-fonts-extra # Google Noto TTF fonts - additional variants
npm # JavaScript package manager
nss-mdns # glibc plugin providing host name resolution via mDNS
nvidia-open # NVIDIA open kernel modules !! NVIDIA GPU only — auto-detect?
pacman-contrib # Contributed scripts and tools for pacman systems
pam-u2f # Universal 2nd Factor (U2F) PAM authentication module from Yubico
papirus-icon-theme # Papirus icon theme
pipewire # Low-latency audio/video router and processor
pipewire-alsa # Low-latency audio/video router and processor - ALSA configuration
pipewire-jack # Low-latency audio/video router and processor - JACK replacement
pipewire-pulse # Low-latency audio/video router and processor - PulseAudio replacement
pnpm # Fast, disk space efficient package manager
polkit-kde-agent # Daemon providing a polkit authentication UI for KDE
postgresql # Sophisticated object-relational DBMS
postgresql-libs # Sophisticated object-relational DBMS - Client binaries and libraries
proton-vpn-cli # Official ProtonVPN CLI Linux app
qt5-wayland # Provides APIs for Wayland
qt6-quickeffectmaker # Qt Quick Effect Maker (QQEM) for creating and editing custom shader effects
qt6-wayland # Provides APIs for Wayland
rofi # A window switcher, application launcher and dmenu replacement
sddm # QML based X11 and Wayland display manager
slurp # Select a region in a Wayland compositor
smartmontools # Control and monitor S.M.A.R.T. enabled ATA and SCSI Hard Drives
steam # Valve's digital software delivery system
stow # Manage installation of multiple softwares in the same directory tree
sudo # Give certain users the ability to run some commands as root
swaync # A simple GTK based notification daemon for Sway
tailscale # A mesh VPN that makes it easy to connect your devices, wherever they are.
thunar # Modern, fast and easy-to-use file manager for Xfce
thunar-archive-plugin # Adds archive operations to the Thunar file context menus
thunar-media-tags-plugin # Adds special features for media files to the Thunar File Manager
thunar-shares-plugin # Thunar plugin to quickly share a folder using Samba without requiring root access
thunar-volman # Automatic management of removable drives and media for Thunar
ttf-dejavu # Font family based on the Bitstream Vera Fonts with a wider range of characters
ttf-iosevka-nerd # Patched font Iosevka from nerd fonts library
ttf-jetbrains-mono # Typeface for developers, by JetBrains
ttf-jetbrains-mono-nerd # Patched font JetBrains Mono from nerd fonts library
ttf-liberation # Font family which aims at metric compatibility with Arial, Times New Roman, and Courier New
ttf-material-icons # Material Design icons by Google (deprecated classic Material Icons static style)
ttf-material-symbols-variable # Material Design icons by Google (Material Symbols - 4 axis variable)
ttf-noto-nerd # Patched font Noto from nerd fonts library
tumbler # Thumbnail service implementing the thumbnail management D-Bus specification
uv # An extremely fast Python package installer and resolver written in Rust
uwsm # A standalone Wayland session manager
vim # Vi Improved, a highly configurable, improved version of the vi text editor
#waybar # Highly customizable Wayland bar for Sway and Wlroots based compositors !! you've switched to quickshell (waybar is commented out in startup.lua)
webp-pixbuf-loader # WebM GDK Pixbuf Loader library
wget # Network utility to retrieve files from the web
wireplumber # Session / policy manager implementation for PipeWire
wofi # launcher for wlroots-based wayland compositors !! you also have rofi — need both?
wpa_supplicant # A utility providing key negotiation for WPA wireless networks
xarchiver # GTK frontend to various command line archivers
xdg-desktop-portal-hyprland # xdg-desktop-portal backend for hyprland
xdg-utils # Command line tools that assist applications with a variety of desktop integration tasks
xfce4-appfinder # Application launcher and finder !! XFCE desktop leftover?
xfce4-panel # Panel for the Xfce desktop environment !! XFCE desktop leftover?
xfce4-power-manager # Power Manager for Xfce !! XFCE desktop leftover?
xfce4-session # Xfce's session manager !! XFCE desktop leftover?
xfce4-settings # Xfce's Configuration System !! XFCE desktop leftover?
xfce4-terminal # A modern terminal emulator primarily for the Xfce desktop environment !! XFCE desktop leftover?
xfdesktop # Xfce's desktop manager !! XFCE desktop leftover?
xfwm4 # Xfce's window manager !! XFCE desktop leftover?
zram-generator # Systemd unit generator for zram devices
zsh # A very advanced and programmable command interpreter (shell) for UNIX
## ---------------- AUR (installed via yay) ----------------
bitwarden-bin # A secure and free password manager for all of your devices.
caelestia-shell # The desktop shell for the Caelestia dotfiles !! alternative quickshell config, not used by your shell.qml
flutter-bin # Cross platform widget toolkit for Dart (monolithic)
google-chrome # The popular web browser by Google (Stable Channel)
hyprmod # Native GTK4/libadwaita settings app for Hyprland
jellyfin-desktop-debug # Detached debugging symbols for jellyfin-desktop !! debug symbols, not needed
minecraft-launcher # Official Minecraft Launcher
orbit-wifi # A WiFi/Bluetooth manager for Wayland with glassmorphism UI !! removed from your config in commit 88484f8
plex-desktop # Plex desktop client for linux
plexamp-bin # Modern music client for Plex.(Prebuilt version.Use system-wide electron)
puddletag # An audio tag editor for GNU/Linux
pwvucontrol # Pipewire volume control for GNOME
python-pywal # Generate and change colorschemes on the fly
raw-thumbnailer # A lightweight and fast raw image thumbnailer that can be used by file managers.
seafile-client # GUI client for synchronizing your local files with seafile server
sunshine # Self-hosted game stream host for Moonlight
svt-av1-essential-git # A no-nonsense SVT-AV1 encoder fork with sensible defaults and Quality of Life improvements !! -git build, check if still needed
ttf-icomoon-feather # Icomoon feather font
tumbler-extra-thumbnailers # Customized thumbnailers for Tumbler
turritopsis # Desktop Jellyfin client with mpv playback
vesktop # A standalone Electron-based Discord app with Vencord & improved Linux support
visual-studio-code-bin # Visual Studio Code (vscode)
waypaper # GUI wallpaper setter for Wayland and Xorg window managers. It works as a frontend for popular wallpaper backends like swaybg, swww, wallutils, hyprpaper and feh.
yay # Yet another yogurt. Pacman wrapper and AUR helper written in go. !! bootstrapped by the script itself anyway
yay-debug # Detached debugging symbols for yay !! debug symbols, not needed
zen-browser-bin # Official package for Zen, a privacy-focused, feature packed Firefox-based web browser
## ---------------- Flatpak (Flathub) ----------------
com.makemkv.MakeMKV # MakeMKV
org.jellyfin.JellyfinDesktop # Jellyfin Desktop
## ---------------- Manually installed binaries (~/.local/bin) ----------------
## These aren't from a package manager; I'd install them via their official installers.
claude # Claude Code CLI (official install script)
oh-my-posh # Prompt theme engine (official install script; your theme is already in dotfiles)
ffmpeg # Static ffmpeg build — !! duplicate, ffmpeg is also installed via pacman above
python3.13 # Standalone Python 3.13 — !! probably not needed, uv can provide Python

197
system/packages.txt Normal file
View File

@@ -0,0 +1,197 @@
# ============================================================================
# Packages installed by bootstrap.sh on a new machine.
#
# - One package per line; anything after "#" is a description.
# - Lines starting with "#" are skipped (left here for reference).
# - [section] lines tell bootstrap.sh which installer to use;
# [services] lists systemd units to enable.
#
# Not listed here (bootstrap.sh handles them itself):
# - CPU microcode and GPU drivers: auto-detected from the hardware
# - yay: bootstrapped from the AUR before the [aur] section
# - svt-av1-essential-git: optional, also rebuilds ffmpeg against it
# ============================================================================
[pacman]
adw-gtk-theme # Unofficial GTK 3 port of the libadwaita theme
# amd-ucode # Microcode update image for AMD CPUs -- auto-detected by bootstrap.sh
awww # An Answer to your Wayland Wallpaper Woes
baobab # A graphical directory tree analyzer
base # Minimal package set to define a basic Arch Linux installation
base-devel # Basic tools to build Arch Linux packages
blueman # GTK+ Bluetooth Manager
bluez # Daemons for the bluetooth protocol stack
bluez-utils # Development and debugging utilities for the bluetooth protocol stack
btop # A monitor of system resources, bpytop ported to C++
dolphin # KDE File Manager
# dunst # Customizable and lightweight notification-daemon !! you use swaync for notifications
efibootmgr # Linux user-space application to modify the EFI Boot Manager
fastfetch # A feature-rich and performance oriented neofetch like system information tool
ffmpeg # Complete solution to record, convert and stream audio and video
filezilla # Fast and reliable FTP, FTPS and SFTP client
firefox # Fast, Private & Safe Web Browser
flatpak # Linux application sandboxing and distribution framework (formerly xdg-app)
foot # Fast, lightweight, and minimalistic Wayland terminal emulator
fzf # Command-line fuzzy finder
galculator # GTK based scientific calculator
# garcon # Freedesktop.org compliant menu library !! XFCE desktop leftover?
gdu # Fast disk usage analyzer
git # the fast distributed version control system
gnome-calendar # Simple and beautiful calendar application designed to perfectly fit the GNOME desktop
gnome-disk-utility # Disk Management Utility for GNOME
gnome-font-viewer # A font viewer utility for GNOME
gnome-keyring # Stores passwords and encryption keys
gnome-text-editor # A simple text editor for the GNOME desktop
grim # Screenshot utility for Wayland
gst-plugin-pipewire # Multimedia graph framework - pipewire plugin
gvfs # Virtual filesystem implementation for GIO
gvfs-smb # Virtual filesystem implementation for GIO - SMB/CIFS backend (Windows file sharing)
htop # Interactive process viewer
hypridle # hyprland’s idle daemon
hyprland # a highly customizable dynamic tiling Wayland compositor
hyprlock # hyprland’s GPU-accelerated screen locking utility
hyprshot # Hyprland screenshot utility
imv # Image viewer for Wayland and X11
jq # Command-line JSON processor
# kitty # A modern, hackable, featureful, OpenGL-based terminal emulator !! you use foot as terminal (kitty config not in dotfiles)
libgsf # Extensible I/O abstraction library for dealing with structured file formats
libmatroska # Matroska library
libpulse # A featureful, general-purpose sound server (client library)
# libva-nvidia-driver # VA-API implementation that uses NVDEC as a backend -- auto-detected by bootstrap.sh
linux # The Linux kernel and modules
linux-firmware # Firmware files for Linux - Default set
mkinitcpio # Modular initramfs image creation utility
mkvtoolnix-cli # Set of tools to create, edit and inspect Matroska files
mpv # a free, open source, and cross-platform media player
mullvad-vpn-daemon # Mullvad VPN client (daemon and CLI)
nano # Pico editor clone with enhancements
network-manager-applet # Applet for managing network connections
networkmanager # Network connection manager and user applications
noto-fonts # Google Noto TTF fonts
noto-fonts-cjk # Google Noto CJK fonts
noto-fonts-emoji # Google Noto Color Emoji font
noto-fonts-extra # Google Noto TTF fonts - additional variants
npm # JavaScript package manager
nss-mdns # glibc plugin providing host name resolution via mDNS
# nvidia-open # NVIDIA open kernel modules -- auto-detected by bootstrap.sh
pacman-contrib # Contributed scripts and tools for pacman systems
pam-u2f # Universal 2nd Factor (U2F) PAM authentication module from Yubico
papirus-icon-theme # Papirus icon theme
pipewire # Low-latency audio/video router and processor
pipewire-alsa # Low-latency audio/video router and processor - ALSA configuration
pipewire-jack # Low-latency audio/video router and processor - JACK replacement
pipewire-pulse # Low-latency audio/video router and processor - PulseAudio replacement
pnpm # Fast, disk space efficient package manager
polkit-kde-agent # Daemon providing a polkit authentication UI for KDE
postgresql # Sophisticated object-relational DBMS
postgresql-libs # Sophisticated object-relational DBMS - Client binaries and libraries
proton-vpn-cli # Official ProtonVPN CLI Linux app
qt5-wayland # Provides APIs for Wayland
qt6-quickeffectmaker # Qt Quick Effect Maker (QQEM) for creating and editing custom shader effects
qt6-wayland # Provides APIs for Wayland
rofi # A window switcher, application launcher and dmenu replacement
sddm # QML based X11 and Wayland display manager
slurp # Select a region in a Wayland compositor
smartmontools # Control and monitor S.M.A.R.T. enabled ATA and SCSI Hard Drives
steam # Valve's digital software delivery system
stow # Manage installation of multiple softwares in the same directory tree
sudo # Give certain users the ability to run some commands as root
swaync # A simple GTK based notification daemon for Sway
tailscale # A mesh VPN that makes it easy to connect your devices, wherever they are.
thunar # Modern, fast and easy-to-use file manager for Xfce
thunar-archive-plugin # Adds archive operations to the Thunar file context menus
thunar-media-tags-plugin # Adds special features for media files to the Thunar File Manager
thunar-shares-plugin # Thunar plugin to quickly share a folder using Samba without requiring root access
thunar-volman # Automatic management of removable drives and media for Thunar
ttf-dejavu # Font family based on the Bitstream Vera Fonts with a wider range of characters
ttf-iosevka-nerd # Patched font Iosevka from nerd fonts library
ttf-jetbrains-mono # Typeface for developers, by JetBrains
ttf-jetbrains-mono-nerd # Patched font JetBrains Mono from nerd fonts library
ttf-liberation # Font family which aims at metric compatibility with Arial, Times New Roman, and Courier New
ttf-material-icons # Material Design icons by Google (deprecated classic Material Icons static style)
ttf-material-symbols-variable # Material Design icons by Google (Material Symbols - 4 axis variable)
ttf-noto-nerd # Patched font Noto from nerd fonts library
tumbler # Thumbnail service implementing the thumbnail management D-Bus specification
uv # An extremely fast Python package installer and resolver written in Rust
uwsm # A standalone Wayland session manager
vim # Vi Improved, a highly configurable, improved version of the vi text editor
# waybar # Highly customizable Wayland bar for Sway and Wlroots based compositors !! you've switched to quickshell (waybar is commented out in startup.lua)
webp-pixbuf-loader # WebM GDK Pixbuf Loader library
wget # Network utility to retrieve files from the web
wireplumber # Session / policy manager implementation for PipeWire
# wofi # launcher for wlroots-based wayland compositors !! you also have rofi — need both?
wpa_supplicant # A utility providing key negotiation for WPA wireless networks
xarchiver # GTK frontend to various command line archivers
xdg-desktop-portal-hyprland # xdg-desktop-portal backend for hyprland
xdg-utils # Command line tools that assist applications with a variety of desktop integration tasks
# xfce4-appfinder # Application launcher and finder !! XFCE desktop leftover?
# xfce4-panel # Panel for the Xfce desktop environment !! XFCE desktop leftover?
# xfce4-power-manager # Power Manager for Xfce !! XFCE desktop leftover?
# xfce4-session # Xfce's session manager !! XFCE desktop leftover?
# xfce4-settings # Xfce's Configuration System !! XFCE desktop leftover?
# xfce4-terminal # A modern terminal emulator primarily for the Xfce desktop environment !! XFCE desktop leftover?
# xfdesktop # Xfce's desktop manager !! XFCE desktop leftover?
# xfwm4 # Xfce's window manager !! XFCE desktop leftover?
zram-generator # Systemd unit generator for zram devices
zsh # A very advanced and programmable command interpreter (shell) for UNIX
# Used by the dotfiles, previously only pulled in by caelestia-shell / waybar
brightnessctl # Backlight control (quickshell, hypr keybinds)
cliphist # Clipboard history (wl-paste watcher in startup.lua)
playerctl # Media key control
power-profiles-daemon # Power profiles (quickshell battery widget)
wl-clipboard # wl-copy / wl-paste
openssh # SSH client/server (sshd service, git over ssh)
avahi # mDNS daemon (avahi-daemon service, used by nss-mdns)
[aur]
bitwarden-bin # A secure and free password manager for all of your devices.
# caelestia-shell # The desktop shell for the Caelestia dotfiles !! alternative quickshell config, not used by your shell.qml
flutter-bin # Cross platform widget toolkit for Dart (monolithic)
google-chrome # The popular web browser by Google (Stable Channel)
hyprmod # Native GTK4/libadwaita settings app for Hyprland
# jellyfin-desktop-debug # Detached debugging symbols for jellyfin-desktop !! debug symbols, not needed
minecraft-launcher # Official Minecraft Launcher
# orbit-wifi # A WiFi/Bluetooth manager for Wayland with glassmorphism UI !! removed from your config in commit 88484f8
plex-desktop # Plex desktop client for linux
plexamp-bin # Modern music client for Plex.(Prebuilt version.Use system-wide electron)
puddletag # An audio tag editor for GNU/Linux
quickshell-git # The shell your bar/OSD run on (previously a caelestia-shell dependency)
pwvucontrol # Pipewire volume control for GNOME
python-pywal # Generate and change colorschemes on the fly
raw-thumbnailer # A lightweight and fast raw image thumbnailer that can be used by file managers.
seafile-client # GUI client for synchronizing your local files with seafile server
sunshine # Self-hosted game stream host for Moonlight
# svt-av1-essential-git # A no-nonsense SVT-AV1 encoder fork with sensible defaults and Quality of Life improvements -- optional step in bootstrap.sh (rebuilds ffmpeg)
ttf-icomoon-feather # Icomoon feather font
tumbler-extra-thumbnailers # Customized thumbnailers for Tumbler
turritopsis # Desktop Jellyfin client with mpv playback
vesktop # A standalone Electron-based Discord app with Vencord & improved Linux support
visual-studio-code-bin # Visual Studio Code (vscode)
waypaper # GUI wallpaper setter for Wayland and Xorg window managers. It works as a frontend for popular wallpaper backends like swaybg, swww, wallutils, hyprpaper and feh.
# yay # Yet another yogurt. Pacman wrapper and AUR helper written in go. !! bootstrapped by the script itself anyway
# yay-debug # Detached debugging symbols for yay !! debug symbols, not needed
zen-browser-bin # Official package for Zen, a privacy-focused, feature packed Firefox-based web browser
[flatpak]
com.makemkv.MakeMKV # MakeMKV
org.jellyfin.JellyfinDesktop # Jellyfin Desktop
[installer] # official install scripts, see bootstrap.sh
claude # Claude Code CLI (official install script)
oh-my-posh # Prompt theme engine (official install script; your theme is already in dotfiles)
# ffmpeg # Static ffmpeg build — !! duplicate, ffmpeg is also installed via pacman above
# python3.13 # Standalone Python 3.13 — !! probably not needed, uv can provide Python
[services] # enabled with systemctl, start on next boot
NetworkManager.service # Networking
bluetooth.service # Bluetooth
sddm.service # Login screen
sshd.service # SSH server
avahi-daemon.service # mDNS / .local hostnames
systemd-resolved.service # DNS resolver (used by the VPNs)
systemd-timesyncd.service # Clock sync
fstrim.timer # Weekly SSD trim
tailscaled.service # Tailscale
mullvad-daemon.service # Mullvad VPN
proton.VPN.service # Proton VPN

27
system/rebuild-ffmpeg.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Rebuilds Arch's ffmpeg package from source so it links against the installed
# svt-av1-essential-git instead of the repo svt-av1 it was built with.
#
# Re-run this after a `pacman -Syu` that updates ffmpeg, otherwise the repo
# build replaces this one.
set -euo pipefail
if ! pacman -Qq svt-av1-essential-git >/dev/null 2>&1; then
echo "svt-av1-essential-git is not installed; nothing to rebuild against." >&2
exit 1
fi
build="$(mktemp -d)"
trap 'rm -rf "$build"' EXIT
git clone --depth 1 https://gitlab.archlinux.org/archlinux/packaging/packages/ffmpeg.git "$build/ffmpeg"
cd "$build/ffmpeg"
# The PKGBUILD verifies a signed ffmpeg tag; makepkg won't fetch the keys itself
mapfile -t keys < <(source ./PKGBUILD && printf '%s\n' "${validpgpkeys[@]}")
if (( ${#keys[@]} )); then
gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys "${keys[@]}"
fi
# No --needed: the version matches what's installed, we want it replaced anyway
makepkg --syncdeps --install --cleanbuild --noconfirm