15 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
18 changed files with 877 additions and 194 deletions

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")

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

@@ -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

@@ -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

@@ -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