Merge quattro into plugin auth boundary

This commit is contained in:
acrogenesis committed 2026-09-02 10:12:15 -06:00
commit 19985314c2
40 files changed
+1829 -46

No files matched your search

+3
View File
@@ -83,6 +83,9 @@ separate PAM services: `omarchy-lock-password` for password auth and,
only when fingerprints are enrolled, `omarchy-lock-fingerprint` for
fingerprint auth. It mirrors the previous lock screen field dimensions,
colors, blurred wallpaper, placeholder, and Hyprland-driven corners.
The plugin sets `keepLoaded: true` so a plugin hot-reload (for example
an installed bar widget changing on disk) does not destroy the lock
client while Hyprland still holds the session lock.
## Polkit agent
+21 -3
View File
@@ -14,9 +14,25 @@ function wifiIconFor(strength) {
return icons[index]
}
function connectionIcon(kind, signalStrength) {
if (kind === "wifi") return wifiIconFor(signalStrength)
if (kind === "ethernet") return "󰈀"
// A known plain-HTTP endpoint lets the network redirect the browser to its
// login page. Never execute or automatically open an untrusted Location header.
var captivePortalUrl = "http://ping.archlinux.org/nm-check.txt"
function connectivityState(kind, connectivity, states, checksEnabled) {
if (kind === "disconnected") return "none"
// Ignore stale cached results when the operator has disabled probing.
if (!checksEnabled) return "unknown"
if (connectivity === states.Portal) return "portal"
if (connectivity === states.Limited) return "limited"
if (connectivity === states.Full) return "full"
if (connectivity === states.None) return "none"
return "unknown"
}
function connectionIcon(kind, signalStrength, connectivity) {
var restricted = connectivity === "portal" || connectivity === "limited"
if (kind === "wifi") return restricted ? "󰤩" : wifiIconFor(signalStrength)
if (kind === "ethernet") return restricted ? "󰈂" : "󰈀"
return "󰤮"
}
@@ -352,6 +368,8 @@ if (typeof module !== "undefined") {
parseNetworkStatus: parseNetworkStatus,
wifiIconFor: wifiIconFor,
connectionIcon: connectionIcon,
connectivityState: connectivityState,
captivePortalUrl: captivePortalUrl,
formatHeaderSpeed: formatHeaderSpeed,
formatHeaderFreq: formatHeaderFreq,
headerDetail: headerDetail,
+129 -12
View File
@@ -119,9 +119,9 @@ Panel {
property bool cursorActive: false
// Keyboard focus zone for the panel. j/k crosses row boundaries:
// header actions ⇄ band ⇄ DNS row ⇄ Wi-Fi networks. h/l move
// header actions ⇄ portal ⇄ band ⇄ DNS row ⇄ Wi-Fi networks. h/l move
// within header actions, band pills, or DNS providers.
property string focusSection: "dns" // "header" | "band" | "dns" | "wifi"
property string focusSection: "dns" // "header" | "portal" | "band" | "dns" | "wifi"
property int headerIndex: 0
readonly property bool canDisconnect: !!connectedWifiNetwork
readonly property bool headerHasDisconnect: false
@@ -220,6 +220,8 @@ Panel {
// network target; both cards are their own plugins now.
function showQr() { root.summonWifiQr(true) }
function speedTest() { root.summonSpeedTest() }
function openCaptivePortal() { root.openCaptivePortal() }
function checkConnectivity() { root.checkConnectivity() }
}
function activateHeader() {
@@ -322,11 +324,11 @@ Panel {
refresh(true)
selectedIndex = wifiNetworks.length > 0 ? 0 : -1
wifiActionFocused = false
focusSection = wifiNetworks.length > 0 ? "wifi" : "dns"
focusSection = hasCaptivePortal ? "portal" : (wifiNetworks.length > 0 ? "wifi" : "dns")
var idx = dnsProviders.indexOf(dnsProvider)
dnsIndex = idx >= 0 ? idx : 0
syncBandIndex()
cursorActive = false
cursorActive = hasCaptivePortal
} else {
// Drop a restart armed by this open: without it a close/reopen inside
// the 100ms window reuses the running timer and re-enables the scanner
@@ -450,7 +452,59 @@ Panel {
Quickshell.execDetached(["bash", "-c", "printf %s " + Util.shellQuote(value) + " | wl-copy"])
}
readonly property string icon: Model.connectionIcon(kind, signalStrength)
// NetworkManager performs the HTTP probe (including unexpected page bodies,
// not just redirects). Consume its native notifications rather than running
// a second curl loop or mistaking an ordinary timeout for a captive portal.
readonly property bool connectivityChecksEnabled: networkManagerAvailable
&& Networking.canCheckConnectivity && Networking.connectivityCheckEnabled
readonly property string connectivity: Model.connectivityState(kind, Networking.connectivity, {
Portal: NetworkConnectivity.Portal, Limited: NetworkConnectivity.Limited,
Full: NetworkConnectivity.Full, None: NetworkConnectivity.None
}, connectivityChecksEnabled)
readonly property bool hasCaptivePortal: connectivity === "portal"
readonly property bool restricted: hasCaptivePortal || connectivity === "limited"
readonly property string icon: Model.connectionIcon(kind, signalStrength, connectivity)
readonly property string connectionKey: kind === "wifi" && wifiDevice && connectedWifiNetwork
? kind + ":" + wifiDevice.name + ":" + connectedWifiNetwork.name
: (kind === "ethernet" && wiredDevice ? kind + ":" + wiredDevice.name : "")
onConnectionKeyChanged: Qt.callLater(checkConnectivity)
onConnectivityChecksEnabledChanged: Qt.callLater(checkConnectivity)
onHasCaptivePortalChanged: {
if (hasCaptivePortal && opened && passwordSsid === "") {
focusSection = "portal"
cursorActive = true
} else if (!hasCaptivePortal && focusSection === "portal") {
focusSection = headerActionCount > 0 ? "header" : "dns"
headerIndex = 0
}
}
onRestrictedChanged: {
connectionPhraseSwap.stop()
heroMeta.opacity = 1.0
}
function checkConnectivity() {
if (connectivityChecksEnabled && kind !== "disconnected") Networking.checkConnectivity()
}
function openCaptivePortal() {
if (!hasCaptivePortal) return
// Explicit user action only. argv (not a shell string), and a fixed HTTP
// URL: let the browser handle the redirect without trusting portal input.
Quickshell.execDetached(["omarchy-launch-browser", Model.captivePortalUrl])
close()
}
// Keep checking while login is needed, even with the panel closed in favour
// of the browser. Normal connected operation relies on NM's own schedule.
Timer {
id: connectivityPoll
interval: 10000
repeat: true
running: root.restricted && root.connectivityChecksEnabled
onTriggered: root.checkConnectivity()
}
// The share card is its own panel plugin (omarchy.wifiqr) so a replacement
// design can take it over; summon() routes to whichever implementation is
@@ -469,6 +523,7 @@ Panel {
}
function refresh(scanWifi) {
checkConnectivity()
if (scanWifi === undefined) scanWifi = false
if (!detailsProc.running) detailsProc.running = true
if (!dnsProc.running) {
@@ -901,7 +956,7 @@ Panel {
Timer {
id: connectionPhraseTimer
interval: 2800
running: root.opened && (root.info.type === "ethernet" || (root.info.type === "wifi" && root.canDisconnect))
running: root.opened && !root.restricted && (root.info.type === "ethernet" || (root.info.type === "wifi" && root.canDisconnect))
repeat: true
onTriggered: connectionPhraseSwap.restart()
}
@@ -958,6 +1013,9 @@ Panel {
anchors.fill: parent
bar: root.bar
text: root.icon
active: root.restricted
tooltipText: root.hasCaptivePortal ? "Sign in to this network"
: (root.restricted ? "Limited internet access" : "")
onPressed: function(b) {
if (root.opened) root.close()
@@ -1001,23 +1059,34 @@ Panel {
if (dy >= 0) return
}
if (dy !== 0) {
// Vertical order is header ⇄ band ⇄ DNS ⇄ wifi, with the band section
// dropping out of the chain entirely when it isn't on screen.
// Hidden sections drop out of the keyboard chain entirely.
if (root.focusSection === "header") {
if (dy > 0) {
if (root.canSelectBand) {
if (root.hasCaptivePortal) {
root.focusSection = "portal"
} else if (root.canSelectBand) {
root.focusSection = "band"
root.bandAutoFocused = true
} else {
root.focusSection = "dns"
}
}
} else if (root.focusSection === "portal") {
if (dy < 0 && root.headerActionCount > 0) {
root.focusSection = "header"
root.headerIndex = 0
} else if (dy > 0) {
root.focusSection = root.canSelectBand ? "band" : "dns"
root.bandAutoFocused = true
}
} else if (root.focusSection === "band") {
// Automatic on the header line, then the pills -- which collapse
// away under Automatic, leaving a single row to walk.
if (dy < 0) {
if (!root.bandAutoFocused) {
root.bandAutoFocused = true
} else if (root.hasCaptivePortal) {
root.focusSection = "portal"
} else if (root.headerActionCount > 0) {
root.focusSection = "header"
root.headerIndex = 0
@@ -1035,6 +1104,8 @@ Panel {
if (root.canSelectBand) {
root.focusSection = "band"
root.bandAutoFocused = !root.bandPillsVisible
} else if (root.hasCaptivePortal) {
root.focusSection = "portal"
} else if (root.headerActionCount > 0) {
root.focusSection = "header"
root.headerIndex = 0
@@ -1063,6 +1134,7 @@ Panel {
onActivateRequested: {
if (root.cursorActive) {
if (root.focusSection === "header") root.activateHeader()
else if (root.focusSection === "portal") root.openCaptivePortal()
else if (root.focusSection === "band") root.activateBand()
else if (root.focusSection === "dns") root.activateDns()
else root.activateSelected()
@@ -1092,7 +1164,7 @@ Panel {
id: heroIcon
textFormat: Text.PlainText
text: root.icon
color: root.bar.foreground
color: root.restricted ? root.bar.urgent : root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.display
opacity: root.networkManagerAvailable ? 1.0 : 0.5
@@ -1175,6 +1247,9 @@ Panel {
width: parent.width
readonly property string title: {
// The HTTP restriction does not undo association. Show the live
// SSID even before route/details polling has returned anything.
if (root.kind === "wifi" && root.connectedWifiNetwork) return root.connectedWifiNetwork.name || "Wi-Fi"
if (root.info.type === "wifi") return root.info.ssid || "Wi-Fi"
if (root.info.type === "ethernet") return "Ethernet"
return root.info.iface || (root.kind === "disconnected" ? "Disconnected" : "No connection")
@@ -1194,6 +1269,8 @@ Panel {
textFormat: Text.PlainText
width: parent.width
text: {
if (root.hasCaptivePortal) return "SIGN-IN REQUIRED"
if (root.restricted) return "LIMITED INTERNET ACCESS"
if (root.info.type === "wifi") {
if (root.canDisconnect) return root.connectionPhrase.toUpperCase()
if (root.kind === "disconnected") return "NOT CONNECTED"
@@ -1204,7 +1281,7 @@ Panel {
return ""
}
visible: text !== ""
color: Qt.darker(root.bar.foreground, 1.4)
color: root.restricted ? root.bar.urgent : Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
font.pixelSize: Style.font.caption
font.bold: true
@@ -1215,6 +1292,43 @@ Panel {
}
Column {
visible: root.hasCaptivePortal
width: parent.width
spacing: Style.space(6)
Button {
id: portalAction
width: parent.width
text: "Open Captive Portal"
iconText: "󰏌"
foreground: root.bar.urgent
accent: root.bar.urgent
fontFamily: root.bar.fontFamily
verticalPadding: Style.space(10)
bordered: true
active: true
hasCursor: root.cursorActive && root.focusSection === "portal"
onHovered: function(on) {
if (!on) return
root.cursorActive = true
root.focusSection = "portal"
}
onClicked: root.openCaptivePortal()
}
Text {
width: parent.width
text: "Sign in or accept this network’s terms to access the internet."
textFormat: Text.PlainText
wrapMode: Text.WordWrap
color: root.bar.foreground
opacity: 0.7
font.family: root.bar.fontFamily
font.pixelSize: Style.font.bodySmall
}
}
// Connection details: transfer metrics first, then IP/Gateway.
Column {
visible: !!root.info.iface
@@ -1654,6 +1768,7 @@ Panel {
if (isBusy && root.actionKind === "disconnect") return "Disconnecting…"
if (isBusy && root.actionKind === "forget") return "Forgetting…"
if (isFailed) return root.failureReason || "Failed"
if (isConnected && root.kind === "wifi" && root.hasCaptivePortal) return "Sign-in required"
if (isConnected) return "Connected"
return ""
}
@@ -1661,6 +1776,7 @@ Panel {
readonly property color statusColor: {
if (isFailed) return root.bar.urgent
if (isBusy) return root.bar.foreground
if (isConnected && root.kind === "wifi" && root.hasCaptivePortal) return root.bar.urgent
if (isConnected) return root.bar.foreground
return Qt.darker(root.bar.foreground, 1.5)
}
@@ -1715,7 +1831,8 @@ Panel {
Text {
id: networkIcon
textFormat: Text.PlainText
text: row.net ? root.wifiIconFor(row.net.signal) : ""
text: row.net ? Model.connectionIcon("wifi", row.net.signal,
row.isConnected && root.kind === "wifi" ? root.connectivity : "") : ""
color: row.statusColor
font.family: root.bar.fontFamily
font.pixelSize: Style.font.title