5 Commits

Author SHA1 Message Date
7cb7f10ff6 Fix: Only create Sunshine headless output on desktop (no battery)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:10:21 +01:00
88484f8495 Remove: Drop orbit, network click opens nm-connection-editor
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:10:21 +01:00
1babf2e334 Feature: Shared pacman options installed via system/install.sh
Enables Color through an Include snippet in /etc/pacman.d so pacman
options stay in sync between machines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:10:21 +01:00
cc78daea77 Merge branch 'main' of ssh://gitea.thomasleewilliams.co.uk:2222/thomas/dotfiles 2026-10-11 16:52:33 +01:00
0415804b67 applying changes 2026-10-11 16:50:54 +01:00
10 changed files with 250 additions and 8 deletions

View File

@@ -46,6 +46,11 @@ stow -d ~/Projects/dotfiles/ -t ~/ dotfiles
This will sync wallpapers/config files to where they are usually located This will sync wallpapers/config files to where they are usually located
System-level config (e.g. pacman options) lives in `system/` and is installed with sudo:
```console
~/Projects/dotfiles/system/install.sh
```
## 📦 Programs ## 📦 Programs
List of programs and tools I use. List of programs and tools I use.

View File

@@ -14,8 +14,8 @@ hl.on("hyprland.start", function()
hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP") 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") hl.exec_cmd("systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP")
-- Sunshine -- Sunshine (virtual monitor only on desktop; skipped on laptops, detected by a system battery)
hl.exec_cmd("sleep 1 && hyprctl output create headless") hl.exec_cmd("sleep 1 && ! ls -d /sys/class/power_supply/BAT* >/dev/null 2>&1 && hyprctl output create headless")
-- Polkit (Polkit Gnome / KDE) -- Polkit (Polkit Gnome / KDE)
hl.exec_cmd(scriptsDir .. "/Polkit.sh") hl.exec_cmd(scriptsDir .. "/Polkit.sh")

View File

@@ -1,5 +0,0 @@
accent_primary = "#007acc"
accent_secondary = "#06b6d4"
background = "#1e1e1e"
foreground = "#d4d4d8"
opacity = 0.91

View File

@@ -222,6 +222,82 @@ Item {
root.passwordSsid = "" root.passwordSsid = ""
} }
// --- Captive portal ---------------------------------------------------
// NetworkManager tells "portal" (a walled-garden sign-in page sits
// between us and the real internet) apart from "full" connectivity only
// via an active probe — nmcli's cached CONNECTIVITY field just reflects
// NM's own internal check, which runs every few minutes and is far too
// slow to catch a portal right after joining a network. So this forces a
// fresh probe itself, same shell-out approach as the stats below, and
// runs it whenever we're connected — not gated to panel.visible like
// those — since the whole point is noticing this before the popup is
// ever opened.
//
// "limited" is treated the same as "portal", not just "full" vs.
// everything-else: NM only reports "portal" when the probe request
// actually comes back *different* (redirected/rewritten) from what it
// expected. Plenty of real captive portals instead black-hole every
// request that isn't to their own login domain, so the probe just times
// out — which is also why this is slow to resolve — and NM reports
// "limited" (it got a route/gateway, but the internet check itself
// never landed) rather than "portal". From the user's side both look
// identical: connected, but no real internet.
property string connectivityState: "unknown"
readonly property bool connectivityRestricted: root.connectivityState === "portal" || root.connectivityState === "limited"
// A plain, unencrypted HTTP page whose only job is to be interceptable
// by a captive portal's redirect. An https:// URL mostly just gets
// silently blocked instead of redirected, which looks identical to the
// portal not existing at all.
readonly property string captivePortalCheckUrl: "http://neverssl.com/"
// Auto-opened once per network join (mirrors TailscaleIndicator's
// openAuthUrl/_authUrlOpened) so flapping connectivity on the same
// network doesn't keep popping a new browser tab — the manual "Open"
// button in the panel banner below still works regardless.
property bool _portalAutoOpened: false
property bool _wasConnected: false
onConnectedDeviceChanged: {
const isConnected = root.connectedDevice !== null
if (isConnected && !root._wasConnected) {
root._portalAutoOpened = false
root.connectivityState = "unknown"
}
root._wasConnected = isConnected
}
onConnectivityRestrictedChanged: {
if (root.connectivityRestricted && !root._portalAutoOpened)
root.openCaptivePortal()
}
function openCaptivePortal() {
root._portalAutoOpened = true
Qt.openUrlExternally(root.captivePortalCheckUrl)
}
Process {
id: connectivityProc
command: ["nmcli", "networking", "connectivity", "check"]
stdout: StdioCollector {
id: connectivityCollector
onStreamFinished: root.connectivityState = connectivityCollector.text.trim()
}
}
Timer {
id: connectivityTimer
// Poll quickly while things are still unresolved (fresh join, or an
// active portal we're waiting to clear) and back off once
// connectivity is confirmed full, rather than shelling out every
// few seconds forever for no reason.
interval: root.connectivityState === "full" ? 20000 : 3000
repeat: true
triggeredOnStart: true
running: root.connectedDevice !== null
onTriggered: if (!connectivityProc.running) connectivityProc.running = true
}
// --- Live stats -------------------------------------------------- // --- Live stats --------------------------------------------------
// None of this comes from Quickshell's Networking API — it only knows // None of this comes from Quickshell's Networking API — it only knows
// about devices/networks, not throughput, ping, or routing. So this // about devices/networks, not throughput, ping, or routing. So this
@@ -595,6 +671,34 @@ Item {
} }
} }
// Small warning badge over the icon's corner when the connection looks
// restricted (a captive portal, confirmed or just suspected — see
// connectivityRestricted above) — same treatment as TailscaleIndicator's
// login-needed badge, so "this needs your attention" reads the same way
// across indicators.
Rectangle {
visible: root.connectivityRestricted
width: Math.max(7, label.font.pixelSize * 0.42)
height: width
radius: width / 2
color: Colors.urgent
border.color: Colors.surface
border.width: 1
anchors.right: label.right
anchors.bottom: label.bottom
anchors.rightMargin: -1
anchors.bottomMargin: -1
Text {
anchors.centerIn: parent
text: "!"
color: Colors.surface
font.family: Appearance.fontFamily
font.pixelSize: Math.max(6, parent.height * 0.75)
font.bold: true
}
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
@@ -890,6 +994,88 @@ Item {
} }
// Captive portal banner — shown when the connectivity probe
// above finds either a confirmed sign-in page or a
// "limited" result standing between this network and the
// real internet (see connectivityRestricted). openCaptivePortal()
// already fired once automatically when this was first
// detected; this stays up as a manual retry in case that tab
// got closed.
Rectangle {
width: parent.width
visible: root.connectivityRestricted
radius: 0
color: Qt.rgba(Colors.urgent.r, Colors.urgent.g, Colors.urgent.b, 0.18)
border.color: Colors.urgent
border.width: 1
implicitHeight: portalBannerRow.implicitHeight + 12
RowLayout {
id: portalBannerRow
anchors.fill: parent
anchors.margins: 6
spacing: 8
Text {
text: "lock_open"
font.family: Appearance.iconFontFamilyRounded
font.variableAxes: Appearance.iconFontAxes
font.pixelSize: 16
color: Colors.urgent
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Text {
Layout.fillWidth: true
text: root.connectivityState === "portal" ? "Sign-in required" : "No internet access"
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSize
font.bold: true
color: Colors.foreground
}
Text {
Layout.fillWidth: true
text: root.connectivityState === "portal"
? "This network needs a web sign-in first"
: "This network may need a web sign-in first"
elide: Text.ElideRight
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: Colors.muted
}
}
Rectangle {
implicitWidth: openPortalText.implicitWidth + 16
implicitHeight: 24
radius: 0
color: openPortalMouse.containsMouse ? Colors.urgent : "transparent"
border.color: Colors.urgent
border.width: 1
Text {
id: openPortalText
anchors.centerIn: parent
text: "Open"
font.family: Appearance.fontFamily
font.pixelSize: Appearance.fontSizeSmall
color: openPortalMouse.containsMouse ? Colors.primaryText : Colors.urgent
}
MouseArea {
id: openPortalMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.openCaptivePortal()
}
}
}
}
// Live stats — ping/loss/throughput/IP/gateway for whichever // Live stats — ping/loss/throughput/IP/gateway for whichever
// device is currently connected. // device is currently connected.
GridLayout { GridLayout {

View File

@@ -13,7 +13,7 @@
"tooltip-format": "{ipaddr} via {gwaddr}", "tooltip-format": "{ipaddr} via {gwaddr}",
"tooltip-format-disconnected": "Disconnected", "tooltip-format-disconnected": "Disconnected",
"max-length": 50, "max-length": 50,
"on-click": "orbit toggle top-right", "on-click": "nm-connection-editor",
"on-click-right": "kitty -e nmtui", "on-click-right": "kitty -e nmtui",
"format-icons": { "format-icons": {
"wifi": ["󰤯", "󰤟", "󰤢", "󰤥", "󰤨"], "wifi": ["󰤯", "󰤟", "󰤢", "󰤥", "󰤨"],

13
dotfiles/.local/bin/Plex Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
# Wrapper for the plex-desktop package's /usr/bin/Plex (-> /opt/plex-desktop/Plex.sh).
#
# QT_QUICK_CONTROLS_STYLE=org.hyprland.style is set globally in
# hypr/core/env.lua for other Qt/QML apps' theming (hyprland-qt-support), but
# Plex Desktop bundles its own private Qt/QML runtime that doesn't ship that
# style plugin — inheriting the var makes it fail to load its UI and exit
# immediately with "module \"org.hyprland.style\" is not installed" (see
# ~/.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.
unset QT_QUICK_CONTROLS_STYLE
exec /opt/plex-desktop/Plex.sh "$@"

View File

@@ -0,0 +1,26 @@
# User override of /usr/share/applications/tv.plex.PlexDesktop.desktop
# (~/.local/share/applications takes XDG precedence over /usr/share for the
# 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
# 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
# apps' theming — inheriting it makes Plex fail to load its UI and exit
# immediately with "module \"org.hyprland.style\" is not installed" (see
# ~/.local/share/plex/Logs/Plex.log). Going straight to the fixed install
# path (rather than relying on the .local/bin/Plex wrapper shadowing
# /usr/bin/Plex on $PATH) means this keeps working even if $PATH order ever
# changes.
[Desktop Entry]
Type=Application
Name=Plex
Comment=Plex client for desktop computers
Exec=env -u QT_QUICK_CONTROLS_STYLE /opt/plex-desktop/Plex.sh
Icon=tv.plex.PlexDesktop
Terminal=false
Categories=AudioVideo;Player;X-Media;
X-Flatpak=tv.plex.PlexDesktop
StartupWMClass=Plex
Name[en_US]=Plex

View File

@@ -51,3 +51,4 @@ export ZED_ALLOW_EMULATED_GPU=1
fastfetch fastfetch
eval "$(oh-my-posh init zsh --config $HOME/.config/ohmyposh/zen.toml)" eval "$(oh-my-posh init zsh --config $HOME/.config/ohmyposh/zen.toml)"
export PATH="$HOME/.local/share/turritopsis/flutter-sdk/bin:$PATH"

11
system/install.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Installs system-level config that stow can't manage (needs root).
set -euo pipefail
repo="$(cd "$(dirname "$0")" && pwd)"
# pacman: shared [options] snippet
sudo install -m644 "$repo/pacman/options.conf" /etc/pacman.d/options.conf
if ! grep -q '^Include = /etc/pacman.d/options.conf' /etc/pacman.conf; then
sudo sed -i '/^\[options\]/a Include = /etc/pacman.d/options.conf' /etc/pacman.conf
fi

View File

@@ -0,0 +1,5 @@
# Shared pacman [options], included from /etc/pacman.conf.
# Installed to /etc/pacman.d/options.conf by system/install.sh.
Color
#ILoveCandy
#VerbosePkgLists