import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Io import Quickshell.Networking import qs.Ui import qs.Commons import "Model.js" as Model Panel { id: root moduleName: "omarchy.network" ipcTarget: "omarchy.network" // manageIpc: false so this panel can own the single IpcHandler the target // permits — needed for the toggleNetwork method below. manageIpc: false // Centralized close so callers can't forget to drop the passphrase prompt. readonly property bool overlayVisible: qrVisible || speedTestModalOpen // Shadows the base open(): a summon or toggle while a centered card is up // dismisses the card instead of opening the compact panel behind an // exclusive overlay. The base toggle() dispatches here, so the keybind, // the bar icon, and every IPC route all get this behavior. function open() { if (overlayVisible) { hideWifiQr() hideSpeedTest() return } root.controller.show() } function close() { root.controller.hide() cancelPasswordPrompt() // The centered cards outlive the compact panel, but the widget's // canonical close must not leave an overlay (or its traffic) behind. hideWifiQr() hideSpeedTest() } function cancelPasswordPrompt() { passwordSsid = "" passwordText = "" identityText = "" } // Live connection details from `ip` / /sys / iw. property var info: ({}) // { iface, type, ip, prefix, gateway, speed, duplex, ssid, signal, freq, bitrate, rx_bytes, tx_bytes, router_ping_ms, internet_ping_ms } // Throughput tracking. Rates are computed as deltas between successive // `omarchy-network-status --verbose` samples (~1.5s apart via detailsPoll). // We hold "prev" alongside a timestamp so the first sample after open or // after an interface switch doesn't manufacture a spike. property real prevRxBytes: 0 property real prevTxBytes: 0 property real prevSampleTime: 0 property string prevIface: "" property real downloadRate: 0 // bytes/sec property real uploadRate: 0 // bytes/sec property string pingIface: "" property var routerPingSamples: [] property var internetPingSamples: [] property real routerPingLatency: -1 property real internetPingLatency: -1 property int internetPingPacketLoss: 0 readonly property int pingHistoryWindow: 24 readonly property int pingAverageWindow: 5 readonly property bool hasInternetPing: internetPingSamples.length > 0 // Every stat row stays mounted whether or not there is data behind it, so a // sample arriving late never reflows the grid. This says whether the numbers // are real yet or the row should read "--". readonly property bool hasTransferStats: info.rx_bytes !== undefined property int connectionPhraseIndex: 0 readonly property var connectionPhrases: [ "Wiring bits", "Handling packets", "Sorting frames", "Hauling bytes", "Routing crumbs", "Counting collisions", "Bending light", ] readonly property string connectionPhrase: connectionPhrases[connectionPhraseIndex % connectionPhrases.length] readonly property bool networkManagerAvailable: Networking.backend === NetworkBackendType.NetworkManager readonly property var networkDevices: Networking.devices ? Networking.devices.values : [] readonly property var wifiDevice: findDevice(DeviceType.Wifi) readonly property var wifiNetworkObjects: wifiDevice && wifiDevice.networks ? wifiDevice.networks.values : [] readonly property var connectedWifiNetwork: findConnectedWifiNetwork() property var wifiNetworks: [] property bool scanning: false property bool wifiStationAvailable: false property string dnsProvider: "" property string pendingDnsProvider: "" // Wi-Fi band state from `omarchy-network-band`. `bandCurrent` is the band // the radio is actually on; `bandSelected` is the pinned choice ("auto" when // nothing is pinned), and the two differ whenever Auto is in effect. property string bandCurrent: "" property string bandSelected: "auto" property var bandAvailable: [] property string pendingBand: "" property bool speedTestRunning: false property bool speedTestModalOpen: false property bool speedTestExpectedStop: false property bool pendingSpeedRun: false property string speedTestPhase: "" property string speedTestStderr: "" property string speedTestDownloadMbps: "" property string speedTestUploadMbps: "" property string speedTestError: "" // Per-row in-flight state. `actionSsid` flips on for the row whose action // is currently running so it can render "Connecting…" / "Disconnecting…" / // "Forgetting…". `passwordSsid` is the row currently expanded into // password-entry mode; we keep it open across refresh cycles so a slow scan // doesn't collapse the input the user is typing into. Rows must gate // comparisons on the matching `*Kind`/`*Reason` being non-empty so a // hidden-SSID row (ssid == "") doesn't collide with the "" defaults. property string actionSsid: "" property string actionKind: "" // "connect" | "disconnect" | "forget" property string failureSsid: "" property string failureReason: "" property string passwordSsid: "" property string passwordText: "" property string identityText: "" property var qrRows: [] property int qrSize: 0 property string qrError: "" property bool qrLoading: false property bool qrExpectedStop: false property bool pendingQrShow: false property bool pendingQrDetect: false property string qrPassword: "" property bool qrPasswordVisible: false property string qrPasswordError: "" readonly property bool qrVisible: qrLoading || qrSize > 0 || qrError !== "" // True while any wifi action is mid-flight. Rows // disable themselves on this so clicks on the other rows don't silently // no-op against runNetworkAction's serialized guard. readonly property bool busy: actionKind !== "" // Index into `wifiNetworks` for keyboard navigation. -1 = no selection. property int selectedIndex: -1 property bool wifiActionFocused: false 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 // within header actions, band pills, or DNS providers. property string focusSection: "dns" // "header" | "band" | "dns" | "wifi" property int headerIndex: 0 readonly property bool canDisconnect: !!connectedWifiNetwork readonly property bool headerHasDisconnect: false readonly property bool canShareWifi: info.type === "wifi" && canShareNetwork(connectedWifiNetwork) // The hero switch is the Wi-Fi radio, so it only exists when there is a // radio to switch. On a wired box it would otherwise sit there reading // "off" beside a perfectly live Ethernet connection. readonly property bool canToggleWifi: networkManagerAvailable && wifiStationAvailable readonly property int qrHeaderIndex: canShareWifi ? 0 : -1 readonly property int speedHeaderIndex: canRunSpeedTest ? (canShareWifi ? 1 : 0) : -1 readonly property int toggleHeaderIndex: canToggleWifi ? (canShareWifi ? 1 : 0) + (canRunSpeedTest ? 1 : 0) : -1 readonly property int headerActionCount: (canShareWifi ? 1 : 0) + (canRunSpeedTest ? 1 : 0) + (canToggleWifi ? 1 : 0) readonly property bool qrHeaderHasCursor: cursorActive && focusSection === "header" && headerIndex === qrHeaderIndex readonly property bool speedHeaderHasCursor: cursorActive && focusSection === "header" && headerIndex === speedHeaderIndex readonly property bool toggleHeaderHasCursor: cursorActive && focusSection === "header" && headerIndex === toggleHeaderIndex readonly property string toggleHint: Networking.wifiEnabled ? "Turn Wi-Fi off" : "Turn Wi-Fi on" readonly property var dnsProviders: ["DHCP", "Cloudflare", "Google", "Custom"] property int dnsIndex: 0 // ["2.4", "5", ...], or empty when there is nothing to choose between. // Wi-Fi only: on Ethernet the band of a secondary radio is not what the // panel is describing. // `bandBusy` keeps the section mounted across the reconnect a band change // causes: `kind` stops being "wifi" for a second or two in the middle of it, // and without this the whole segment would vanish and rebuild itself. // Worth showing when there is a real choice, or when a pin is in force even // though only one band answers right now -- otherwise the Automatic switch // vanishes and the pin becomes unclearable from the panel. readonly property bool canSelectBand: (kind === "wifi" || bandBusy) && (bandAvailable.length > 1 || bandPinned) // While a change is in flight, show the state that was asked for rather than // the one still in force, so the row answers the click immediately instead of // after the reconnect. actionProc puts it back if the change failed. readonly property string bandEffective: pendingBand !== "" ? pendingBand : bandSelected readonly property bool bandPinned: bandEffective !== "auto" // Under Automatic there is nothing to pick, so the pills collapse away and // the header states the live band instead. readonly property bool bandPillsVisible: canSelectBand && bandPinned readonly property string bandSectionTitle: Model.bandSectionTitle(bandEffective, bandCurrent) readonly property bool bandBusy: pendingBand !== "" // The speed test needs an interface to test, so its hero action only // appears once there is one. readonly property bool canRunSpeedTest: !!info.iface property int bandIndex: 0 // The band section has up to two cursor rows: the Automatic switch on the // header line, then the pills. Same shape as wifiActionFocused. property bool bandAutoFocused: true onHeaderActionCountChanged: clampHeaderIndex() // Availability shifts as scans land, so the option list can shrink out from // under the cursor. Clamp the index and evacuate the section before it // disappears, or the panel is left highlighting nothing. onBandAvailableChanged: { if (bandIndex > bandAvailable.length - 1) bandIndex = Math.max(0, bandAvailable.length - 1) } onCanSelectBandChanged: { if (!canSelectBand && focusSection === "band") { focusSection = "dns" bandAutoFocused = true } } // Collapsing the pills out from under the cursor would leave it pointing at // nothing, so send it up to the switch that is still on screen. onBandPillsVisibleChanged: { if (!bandPillsVisible) bandAutoFocused = true } function clampHeaderIndex() { var max = Math.max(0, headerActionCount - 1) if (headerIndex > max) headerIndex = max if (headerIndex < 0) headerIndex = 0 } function selectHeaderByDelta(delta) { headerIndex = Math.max(0, Math.min(headerActionCount - 1, headerIndex + delta)) } function toggleNetwork() { if (!networkManagerAvailable) return Networking.wifiEnabled = !Networking.wifiEnabled Qt.callLater(function() { root.refresh(true) }) } IpcHandler { target: "omarchy.network" function open() { root.open() } function close() { root.close() } function show() { root.open() } function hide() { root.close() } function toggle() { root.toggle() } function toggleNetwork() { root.toggleNetwork() } // Menu routes: summon the centered cards directly, panel open or not. function showQr() { root.refresh() root.showWifiQr(true) } function speedTest() { root.refresh() root.showSpeedTest() } } function activateHeader() { if (headerIndex === qrHeaderIndex) showWifiQr() else if (headerIndex === speedHeaderIndex) showSpeedTest() else if (headerIndex === toggleHeaderIndex) toggleNetwork() } function setHeaderCursor(index) { cursorActive = true focusSection = "header" headerIndex = index } function selectDnsByDelta(delta) { dnsIndex = Math.max(0, Math.min(dnsProviders.length - 1, dnsIndex + delta)) } function activateDns() { if (dnsIndex < 0 || dnsIndex >= dnsProviders.length) return setDns(dnsProviders[dnsIndex]) } function selectBandByDelta(delta) { bandIndex = Math.max(0, Math.min(bandAvailable.length - 1, bandIndex + delta)) } function activateBand() { if (bandAutoFocused) { toggleBandAuto() return } if (bandIndex < 0 || bandIndex >= bandAvailable.length) return setBand(bandAvailable[bandIndex]) } // Switching Automatic off has to commit to something, so it pins whatever // band the radio already landed on -- the reading the pills are showing. function toggleBandAuto() { if (bandSelected !== "auto") { setBand("auto") return } if (bandCurrent === "") return setBand(bandCurrent) } // Park the cursor on the pinned band, so opening the panel highlights the // pill the user would expect. Under Automatic there are no pills, so the // cursor belongs on the switch. function syncBandIndex() { var idx = bandAvailable.indexOf(bandSelected) bandIndex = idx >= 0 ? idx : 0 bandAutoFocused = !bandPillsVisible } function bandLabel(band) { return Model.bandLabel(band) } function bandTooltip(band) { return Model.bandTooltip(band) } // Single cursor model: exactly one highlighted spot across the whole // panel, located via `focusSection` + (`headerIndex` | `dnsIndex` | // `selectedIndex`). Mouse hover and keyboard nav both mutate this state // at the root; items never read containsMouse for visuals. See // CursorSurface for the shared chrome shared by rows and pills. readonly property color hoverFill: bar ? Style.hoverFillFor(bar.foreground, Color.accent) : "transparent" readonly property color selectedFill: bar ? Style.selectedFillFor(bar.foreground, Color.accent) : "transparent" // KeyboardPanel primes layer-shell focus whenever the panel opens. That's // what makes the SUPER+CTRL+W keybind land here with navigation ready. onOpenedChanged: { if (opened) { refresh(true) selectedIndex = wifiNetworks.length > 0 ? 0 : -1 wifiActionFocused = false focusSection = wifiNetworks.length > 0 ? "wifi" : "dns" var idx = dnsProviders.indexOf(dnsProvider) dnsIndex = idx >= 0 ? idx : 0 syncBandIndex() cursorActive = false } else { // Reset throughput tracking so the next open doesn't compute a fake // rate from a sample taken minutes ago. prevSampleTime = 0 downloadRate = 0 uploadRate = 0 pingIface = "" routerPingSamples = [] internetPingSamples = [] routerPingLatency = -1 internetPingLatency = -1 internetPingPacketLoss = 0 if (wifiDevice) wifiDevice.scannerEnabled = false } } // When the passphrase prompt closes (Esc / Cancel / success) restore // focus to the keyCatcher so j/k/Enter resume working without a click. // The KeyboardPanel's focusTarget covers initial popup-open; this handles // the inline-editor case where focus was handed off to a child. onPasswordSsidChanged: { if (passwordSsid === "" && opened) { passwordText = "" Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() }) } } // Keep selectedIndex valid as scans refresh the network list. // If the list empties (station gone, e.g. wifi off), bounce the cursor // back to the DNS row so the panel doesn't end up with no cursor at all. onWifiNetworksChanged: { if (wifiNetworks.length === 0) { selectedIndex = -1 wifiActionFocused = false if (focusSection === "wifi") focusSection = "dns" } else if (passwordSsid !== "") { var passwordIndex = wifiIndexForSsid(passwordSsid) if (passwordIndex >= 0) { selectedIndex = passwordIndex focusSection = "wifi" } } else if (selectedIndex >= wifiNetworks.length) { selectedIndex = wifiNetworks.length - 1 } else if (selectedIndex < 0 && opened) { selectedIndex = 0 } if (selectedIndex < 0 || selectedIndex >= wifiNetworks.length || !canForgetNetwork(wifiNetworks[selectedIndex])) { wifiActionFocused = false } } onWifiDeviceChanged: { if (wifiDevice) wifiDevice.scannerEnabled = opened syncWifiNetworks() } onWifiNetworkObjectsChanged: syncWifiNetworks() function selectByDelta(delta) { if (wifiNetworks.length === 0) { selectedIndex = -1; return } if (selectedIndex < 0) selectedIndex = delta > 0 ? 0 : wifiNetworks.length - 1 else selectedIndex = Math.max(0, Math.min(wifiNetworks.length - 1, selectedIndex + delta)) wifiActionFocused = false } function canForgetNetwork(net) { return !!(net && net.known && isProtected(net.security) && !net.connected) } function canShareNetwork(net) { if (!net || !net.connected) return false return net.security !== WifiSecurityType.Wpa2Eap && net.security !== WifiSecurityType.WpaEap } function selectWifiActionByDelta(delta) { if (selectedIndex < 0 || selectedIndex >= wifiNetworks.length) return if (!canForgetNetwork(wifiNetworks[selectedIndex])) { wifiActionFocused = false return } if (delta > 0) wifiActionFocused = true else if (delta < 0) wifiActionFocused = false } // Enter/Space on the highlighted row. Mirrors row-click semantics: // connected → disconnect, protected-unknown → password prompt, // open/known → connect. function activateSelected() { if (busy || selectedIndex < 0 || selectedIndex >= wifiNetworks.length) return var net = wifiNetworks[selectedIndex] if (!net) return if (wifiActionFocused && canForgetNetwork(net)) { forget(net); return } if (net.connected) { disconnect(net.network); return } if (isProtected(net.security) && !net.known) { openPasswordPrompt(net.ssid); return } connectKnown(net.ssid) } // Bar pill state, derived from the native NetworkManager service so the // icon reflects connection changes without polling. Wired is preferred // when both are up, matching the default-route device. readonly property var wiredDevice: findDevice(DeviceType.Wired) readonly property string kind: { if (wiredDevice && wiredDevice.connected) return "ethernet" if (connectedWifiNetwork) return "wifi" return "disconnected" } readonly property int signalStrength: connectedWifiNetwork ? Math.round((connectedWifiNetwork.signalStrength || 0) * 100) : -1 function copyToClipboard(value) { if (!value || !root.bar) return Quickshell.execDetached(["bash", "-c", "printf %s " + Util.shellQuote(value) + " | wl-copy"]) } readonly property string icon: Model.connectionIcon(kind, signalStrength) function showWifiQr(forceDetect) { if (qrProc.running) { // A dismissal's SIGTERM is still in flight; Process.running stays true // until the child exits, so queue the reopen for onExited. if (qrExpectedStop) { pendingQrShow = true pendingQrDetect = !!forceDetect } return } qrSize = 0 qrRows = [] qrError = "" qrLoading = true qrExpectedStop = false // The panel's own button shares the interface it is showing. The IPC // route forces self-detection instead: details polling stops while the // panel is closed, so its cached interface can be stale. qrProc.command = !forceDetect && info.type === "wifi" && info.iface ? ["omarchy-network-qr", info.iface] : ["omarchy-network-qr"] qrProc.running = true // Leave the compact network panel behind while the centered share card is open. controller.hide() cancelPasswordPrompt() } function hideWifiQr() { pendingQrShow = false if (qrProc.running) { qrExpectedStop = true qrProc.running = false } if (pwProc.running) pwProc.running = false qrSize = 0 qrRows = [] qrError = "" qrLoading = false qrPassword = "" qrPasswordVisible = false qrPasswordError = "" } function updateQr(raw) { var matrix = Model.parseQrMatrix(raw) qrRows = matrix.rows qrSize = matrix.size } function toggleQrPassword() { if (qrPasswordVisible) { qrPasswordVisible = false; return } if (qrPassword !== "") { qrPasswordVisible = true; return } if (pwProc.running || !info.iface) return qrPasswordError = "" pwProc.command = ["omarchy-network-password", info.iface] pwProc.running = true } function refresh(scanWifi) { if (scanWifi === undefined) scanWifi = false if (!detailsProc.running) detailsProc.running = true if (!dnsProc.running) { dnsProc.command = ["bash", "-c", root.dnsCommand("")] dnsProc.running = true } if (!bandProc.running) { bandProc.command = ["omarchy-network-band"] bandProc.running = true } if (wifiDevice) { if (scanWifi) { scanning = true wifiDevice.scannerEnabled = false scanRestart.start() } else { wifiDevice.scannerEnabled = true } } syncWifiNetworks() } function formatHeaderSpeed(mbps) { return Model.formatHeaderSpeed(mbps) } function formatHeaderFreq(mhz) { return Model.formatHeaderFreq(mhz) } function headerDetail() { return Model.headerDetail(info) } function updateDetails(raw) { var next = Model.parseKeyValue(raw) // A band change tears the link down and brings it back, and the status // command reports nothing at all while there is no route. Publishing that // would blank every stat and unmount the whole section mid-toggle, so the // last good sample stands until the reconnect settles. A real disconnect is // still reported, because nothing is in flight then. if (bandBusy && !next.iface) return info = next updateThroughput(next) updatePingLatency(next) } function updateThroughput(next) { var state = Model.throughputState({ prevIface: prevIface, prevRxBytes: prevRxBytes, prevTxBytes: prevTxBytes, prevSampleTime: prevSampleTime, downloadRate: downloadRate, uploadRate: uploadRate }, next, Date.now() / 1000) prevIface = state.prevIface prevRxBytes = state.prevRxBytes prevTxBytes = state.prevTxBytes prevSampleTime = state.prevSampleTime downloadRate = state.downloadRate uploadRate = state.uploadRate } function updatePingLatency(next) { var state = Model.pingLatencyState({ pingIface: pingIface, routerPingSamples: routerPingSamples, internetPingSamples: internetPingSamples }, next, pingHistoryWindow, pingAverageWindow) pingIface = state.pingIface routerPingSamples = state.routerPingSamples internetPingSamples = state.internetPingSamples routerPingLatency = state.routerPingLatency internetPingLatency = state.internetPingLatency internetPingPacketLoss = state.internetPingPacketLoss } function formatBytes(bytes) { return Model.formatBytes(bytes) } function formatRate(bytesPerSec) { return Model.formatRate(bytesPerSec) } function formatPingLatency(ms) { return Model.formatPingLatency(ms, hasInternetPing) } function formatPacketLoss(percent) { return Model.formatPacketLoss(percent, hasInternetPing) } // Prefer a connected device: a machine can expose several NICs of the // same type (e.g. an idle onboard port alongside the active adapter), // and the first-enumerated one may be carrierless. function findDevice(type) { var devices = networkDevices || [] var fallback = null for (var i = 0; i < devices.length; i++) { var device = devices[i] if (!device || device.type !== type) continue if (device.connected) return device if (!fallback) fallback = device } return fallback } function findConnectedWifiNetwork() { var networks = wifiNetworkObjects || [] for (var i = 0; i < networks.length; i++) { if (networks[i] && networks[i].connected) return networks[i] } return null } function syncWifiNetworks() { var nets = [] var networks = wifiNetworkObjects || [] for (var i = 0; i < networks.length; i++) { var network = networks[i] if (!network) continue checkActionCompletion(network) var row = Model.wifiRow(network) if (row) nets.push(row) } wifiNetworks = Model.sortWifiRows(nets) wifiStationAvailable = !!wifiDevice scanning = false } function wifiSectionTitle(index) { return Model.wifiSectionTitle(wifiNetworks, index) } function wifiIconFor(strength) { return Model.wifiIconFor(strength) } function updateDns(raw) { var value = String(raw || "").trim() dnsProvider = value || "DHCP" } function updateBand(raw) { var status = Model.parseBandStatus(raw) // Mid-reconnect there is no connected station, so the command reports // nothing. Publishing that would empty the option list and unmount the // section on every toggle -- same guard as updateDetails. if (bandBusy && status.available.length === 0) return bandCurrent = status.band bandSelected = status.selected bandAvailable = status.available } // Pinning a band reassociates, but the panel deliberately stays open: the // reconnect is the thing you want to watch, and the details rows above // report it as it happens. function setBand(band) { if (!band || actionProc.running) return root.pendingBand = band actionProc.command = ["omarchy-network-band", band] actionProc.running = true } function updateSpeedTestLine(line) { var value = parseFloat(line) if (!isFinite(value) || value < 0) return if (speedTestPhase === "down") speedTestDownloadMbps = String(value) else if (speedTestPhase === "up") speedTestUploadMbps = String(value) speedTestError = "" } // The speed test lives in a centered modal card like the QR share. // Opening it starts a fresh run; dismissing it stops the traffic, so the // download workers never keep saturating the link behind a closed card. function showSpeedTest() { if (!speedTestModalOpen) { speedTestModalOpen = true controller.hide() cancelPasswordPrompt() } runSpeedTest() } function hideSpeedTest() { speedTestModalOpen = false pendingSpeedRun = false speedTestPhaseTimer.stop() // Clear the phase before killing the process: onExited advances to the // upload phase when it still reads "down". speedTestPhase = "" speedTestRunning = false if (speedTestProc.running) { speedTestExpectedStop = true speedTestProc.running = false } } function runSpeedTest() { if (speedTestProc.running) { // A dismissal's SIGTERM is still in flight; Process.running stays true // until the child exits, so queue the fresh run for onExited. if (speedTestExpectedStop) pendingSpeedRun = true return } speedTestError = "" speedTestDownloadMbps = "" speedTestUploadMbps = "" speedTestRunning = true startSpeedTestPhase("down") } function startSpeedTestPhase(phase) { speedTestExpectedStop = false speedTestPhase = phase speedTestStderr = "" speedTestProc.command = ["omarchy-network-speedtest", phase] speedTestProc.running = true speedTestPhaseTimer.restart() } function stopSpeedTestPhase() { speedTestPhaseTimer.stop() if (speedTestProc.running) { speedTestExpectedStop = true speedTestProc.running = false return } finishSpeedTestPhase() } function finishSpeedTestPhase() { if (speedTestPhase === "down") { startSpeedTestPhase("up") return } speedTestPhase = "" speedTestRunning = false speedTestExpectedStop = false } function dnsCommand(provider) { var command = "omarchy-dns" if (provider) command += " " + Util.shellQuote(provider) return command } function setDns(provider) { if (!root.bar || !provider || actionProc.running) return if (provider === "Custom") { var launcher = "omarchy-launch-floating-terminal-with-presentation" root.bar.run(launcher + " " + Util.shellQuote(root.dnsCommand(provider))) root.close() return } root.pendingDnsProvider = provider actionProc.command = ["bash", "-c", root.dnsCommand(provider)] actionProc.running = true root.close() } function isProtected(security) { return Model.isProtected(security, WifiSecurityType.Open) } function openPasswordPrompt(ssid) { if (passwordSsid !== ssid) { passwordText = "" identityText = "" } passwordSsid = ssid } function networkForSsid(ssid) { var networks = wifiNetworkObjects || [] for (var i = 0; i < networks.length; i++) { if (networks[i] && networks[i].name === ssid) return networks[i] } return null } function wifiIndexForSsid(ssid) { for (var i = 0; i < wifiNetworks.length; i++) { if (wifiNetworks[i] && wifiNetworks[i].ssid === ssid) return i } return -1 } function runNetworkAction(kind, network, callback) { if (actionKind !== "" || !network) return var ssid = network.name || "" actionSsid = ssid actionKind = kind failureSsid = "" failureReason = "" callback(network) // Safety net: if onExited never fires (process death, signal handler // throws, etc.), clear the busy state so the row doesn't get stuck on // "Connecting…" / "Disconnecting…" forever. actionTimeout.restart() } function clearNetworkAction() { actionTimeout.stop() if (actionKind === "connect") passwordSsid = "" failureSsid = "" failureReason = "" actionSsid = "" actionKind = "" refresh() } function failNetworkAction(network, reason) { if (!network || actionKind === "" || actionSsid !== (network.name || "")) return actionTimeout.stop() failureSsid = actionSsid failureReason = networkFailureReason(reason) actionSsid = "" actionKind = "" refresh() } function networkFailureReason(reason) { return Model.networkFailureReason(reason, { NoSecrets: ConnectionFailReason.NoSecrets, WifiAuthTimeout: ConnectionFailReason.WifiAuthTimeout, WifiNetworkLost: ConnectionFailReason.WifiNetworkLost, WifiClientDisconnected: ConnectionFailReason.WifiClientDisconnected, WifiClientFailed: ConnectionFailReason.WifiClientFailed }) } function checkActionCompletion(network) { if (!network || actionKind === "" || actionSsid !== (network.name || "")) return if (actionKind === "connect" && network.connected) clearNetworkAction() else if (actionKind === "disconnect" && !network.connected && !network.stateChanging) clearNetworkAction() else if (actionKind === "forget" && !network.known && !network.stateChanging) clearNetworkAction() } function connectKnown(ssid) { runNetworkAction("connect", networkForSsid(ssid), function(network) { network.connect() }) } function connectWithPassphrase(ssid, passphrase) { runNetworkAction("connect", networkForSsid(ssid), function(network) { network.connectWithPsk(passphrase) }) } function connectEnterprise(ssid, identity, passphrase) { runNetworkAction("connect", networkForSsid(ssid), function(network) { enterpriseConnect.secret = passphrase enterpriseConnect.command = ["bash", "-c", Model.enterpriseConnectScript, "nmcli-eap", ssid, identity] enterpriseConnect.running = true }) } // Creates and activates the 802.1X profile (see Model.enterpriseConnectScript). // The password goes over stdin, never argv. Process { id: enterpriseConnect property string secret: "" stdinEnabled: true onStarted: { write(secret + "\n") secret = "" } } function disconnect(network) { runNetworkAction("disconnect", network || connectedWifiNetwork, function(net) { net.disconnect() }) } function forget(net) { runNetworkAction("forget", net ? net.network : null, function(network) { network.forget() }) } implicitWidth: button.implicitWidth implicitHeight: button.implicitHeight Component.onCompleted: refresh() // Pulls everything we want about the active route's interface in one shot. Process { id: detailsProc command: ["omarchy-network-status", "--verbose"] stdout: StdioCollector { waitForEnd: true onStreamFinished: root.updateDetails(text) } } Timer { id: scanRestart interval: 100 repeat: false onTriggered: { if (root.wifiDevice) root.wifiDevice.scannerEnabled = true scanDone.start() } } Timer { id: scanDone interval: 1500 repeat: false onTriggered: root.syncWifiNetworks() } Process { id: qrProc // Both collectors check qrExpectedStop: a dismissal mid-generation kills // the process, but buffered output still arrives afterwards and would // repopulate qrSize -- reopening the card the user just closed. The flag // stays set through onExited (showWifiQr resets it) because the exit and // stream-finished signals have no guaranteed order. stdout: StdioCollector { waitForEnd: true onStreamFinished: if (!root.qrExpectedStop) root.updateQr(text) } stderr: StdioCollector { waitForEnd: true onStreamFinished: if (!root.qrExpectedStop) root.qrError = String(text || "").trim() } onExited: function(exitCode) { root.qrLoading = false if (root.pendingQrShow) { root.pendingQrShow = false root.qrExpectedStop = false Qt.callLater(function() { root.showWifiQr(root.pendingQrDetect) }) return } if (root.qrExpectedStop) return if (exitCode !== 0 || root.qrSize === 0) { root.qrSize = 0 root.qrRows = [] if (root.qrError === "") root.qrError = "Could not generate the Wi-Fi QR code" } } } // The Wi-Fi password only enters shell memory when the user clicks to // reveal it, and hideWifiQr drops it again when the share card closes. // Both handlers bail when the card is gone so a fetch that was in flight // during dismissal can't stash the secret into a closed panel's state. Process { id: pwProc stdout: StdioCollector { waitForEnd: true onStreamFinished: if (root.qrVisible) root.qrPassword = String(text || "").trim() } onExited: function(exitCode) { if (!root.qrVisible) return if (exitCode === 0 && root.qrPassword !== "") root.qrPasswordVisible = true else root.qrPasswordError = "Could not read the Wi-Fi password" } } Process { id: dnsProc stdout: StdioCollector { waitForEnd: true onStreamFinished: root.updateDns(text) } } Process { id: bandProc stdout: StdioCollector { waitForEnd: true onStreamFinished: root.updateBand(text) } } // Slower than detailsPoll on purpose: this shells out to nmcli several times, // and band availability only moves when a scan turns up a new BSSID. Timer { id: bandPoll interval: 4000 repeat: true running: root.opened onTriggered: { if (bandProc.running) return bandProc.command = ["omarchy-network-band"] bandProc.running = true } } Process { id: speedTestProc stdout: SplitParser { onRead: function(line) { root.updateSpeedTestLine(line) } } stderr: StdioCollector { waitForEnd: true onStreamFinished: root.speedTestStderr = String(text || "").trim() } onExited: function(exitCode) { speedTestPhaseTimer.stop() if (root.pendingSpeedRun) { root.pendingSpeedRun = false root.speedTestExpectedStop = false if (root.speedTestModalOpen) Qt.callLater(root.runSpeedTest) return } if (!root.speedTestExpectedStop && exitCode !== 0) { root.speedTestError = root.speedTestStderr || "Speed test failed" root.speedTestPhase = "" root.speedTestRunning = false return } root.speedTestExpectedStop = false root.finishSpeedTestPhase() } } Timer { id: speedTestPhaseTimer interval: 5000 repeat: false onTriggered: root.stopSpeedTestPhase() } // Action runner for DNS provider changes. Wi-Fi actions use the // Quickshell.Networking NetworkManager backend directly. Process { id: actionProc stdout: StdioCollector { id: actionStdout; waitForEnd: true } stderr: StdioCollector { id: actionStderr; waitForEnd: true } onExited: function(exitCode) { if (root.pendingDnsProvider !== "") { if (exitCode === 0) root.dnsProvider = root.pendingDnsProvider root.pendingDnsProvider = "" } if (root.pendingBand !== "") { // A refused or reverted pin leaves bandSelected alone, so the pills // keep showing what is actually in force rather than what was asked. if (exitCode === 0) root.bandSelected = root.pendingBand root.pendingBand = "" // The panel stayed open through the reconnect, so pull fresh state now // instead of leaving stale readings until the next poll tick. root.refresh() } } } // Poll details while the panel is open so the IP/route header catches up // as soon as NetworkManager finishes activating a connection. Timer { id: detailsPoll interval: 1500 repeat: true running: root.opened onTriggered: if (!detailsProc.running) detailsProc.running = true } Timer { id: connectionPhraseTimer interval: 2800 running: root.opened && (root.info.type === "ethernet" || (root.info.type === "wifi" && root.canDisconnect)) repeat: true onTriggered: connectionPhraseSwap.restart() } SequentialAnimation { id: connectionPhraseSwap PropertyAnimation { target: heroMeta; property: "opacity" to: 0.0; duration: 180; easing.type: Easing.OutQuad } ScriptAction { script: root.connectionPhraseIndex = (root.connectionPhraseIndex + 1) % root.connectionPhrases.length } PropertyAnimation { target: heroMeta; property: "opacity" to: 1.0; duration: 260; easing.type: Easing.InQuad } } Connections { target: root function onInfoChanged() { if (!(root.info.type === "ethernet" || (root.info.type === "wifi" && root.canDisconnect))) { connectionPhraseSwap.stop() heroMeta.opacity = 1.0 } } } Timer { id: actionTimeout interval: 15000 repeat: false onTriggered: { if (!root.actionKind) return var reason if (root.actionKind === "connect") reason = "Timed out connecting" else if (root.actionKind === "disconnect") reason = "Timed out disconnecting" else reason = "Timed out forgetting" root.failureSsid = root.actionSsid root.failureReason = reason root.actionSsid = "" root.actionKind = "" root.refresh() } } BarIconButton { id: button anchors.fill: parent bar: root.bar text: root.icon onPressed: function(b) { if (root.opened) root.close() else { root.open(); root.refresh() } } } // Keyboard-driven popup anchored to the bar widget icon. The shared // KeyboardPanel handles the layer-shell PanelWindow scaffolding // (focus priming on open, screen binding, anchored-to-icon positioning, // outside-click via an overlay MouseArea + Region mask that lets the bar // remain clickable, fade animation, popout coordination). What stays // here is the wifi-specific UI inside. KeyboardPanel { id: panel anchorItem: button owner: root bar: root.bar open: root.opened focusTarget: keyCatcher contentWidth: panel.fittedContentWidth(Style.space(380)) contentHeight: panel.fittedContentHeight(column.implicitHeight) // Catches all unhandled keys for keyboard navigation. AfterItem priority // lets the passphrase TextField (a child via focus chain) get its keys // first; only events the focused subtree ignores bubble back here. PanelKeyCatcher { id: keyCatcher anchors.fill: parent // Freeze the cursor model while the inline password prompt is open; // the TextField inside owns input until Esc/Enter/Cancel. blocked: root.passwordSsid !== "" onMoveRequested: function(dx, dy) { if (!root.cursorActive) { root.cursorActive = true 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. if (root.focusSection === "header") { if (dy > 0) { if (root.canSelectBand) { root.focusSection = "band" root.bandAutoFocused = true } else { root.focusSection = "dns" } } } 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.headerActionCount > 0) { root.focusSection = "header" root.headerIndex = 0 } } else if (root.bandAutoFocused && root.bandPillsVisible) { root.bandAutoFocused = false } else { root.focusSection = "dns" } } else if (root.focusSection === "dns") { // k from DNS moves up into the band section when it's on screen, // then the disconnect button; otherwise stays put. j drops into the // wifi list if there's anywhere to land. if (dy < 0) { if (root.canSelectBand) { root.focusSection = "band" root.bandAutoFocused = !root.bandPillsVisible } else if (root.headerActionCount > 0) { root.focusSection = "header" root.headerIndex = 0 } } else if (root.wifiNetworks.length > 0) { root.focusSection = "wifi" if (root.selectedIndex < 0) root.selectedIndex = 0 } } else { // wifi // k from the top row escapes back up to the DNS row rather than // wrapping around to the bottom of the list. if (dy < 0 && root.selectedIndex <= 0) { root.focusSection = "dns" root.wifiActionFocused = false } else root.selectByDelta(dy) } } if (dx !== 0) { if (root.focusSection === "header") root.selectHeaderByDelta(dx) else if (root.focusSection === "band") { if (!root.bandAutoFocused) root.selectBandByDelta(dx) } else if (root.focusSection === "dns") root.selectDnsByDelta(dx) else if (root.focusSection === "wifi") root.selectWifiActionByDelta(dx) } } onActivateRequested: { if (root.cursorActive) { if (root.focusSection === "header") root.activateHeader() else if (root.focusSection === "band") root.activateBand() else if (root.focusSection === "dns") root.activateDns() else root.activateSelected() } } onCloseRequested: root.close() onTabRequested: function(direction) { root.switchPanel(direction) } onTextKey: function(t) { if (t === "r" || t === "R") root.refresh() else if (t === "w" || t === "W") root.toggleNetwork() } Column { id: column anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top spacing: Style.space(12) // ---------- Hero: network icon · SSID + state · actions ---------- Item { width: parent.width implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight, heroActions.implicitHeight) // Status only — the switch owns toggling, mouse and keyboard alike. Text { id: heroIcon text: root.icon color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.display opacity: root.networkManagerAvailable ? 1.0 : 0.5 anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter } // Sharing belongs to the connected-network hero rather than the scan // result row. The radio switch remains beside it as the other hero action. RowLayout { id: heroActions spacing: Style.space(8) anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter Button { id: qrAction visible: root.canShareWifi iconText: "󰐲" tooltipText: "Show QR code" foreground: root.bar.foreground fontFamily: root.bar.fontFamily iconSize: Style.font.subtitle * 1.5 horizontalPadding: Style.space(5) verticalPadding: Style.space(2) hasCursor: root.qrHeaderHasCursor Layout.alignment: Qt.AlignVCenter onHovered: function(on) { if (on) root.setHeaderCursor(root.qrHeaderIndex) } onClicked: root.showWifiQr() } Button { id: speedAction visible: root.canRunSpeedTest iconText: "󰓅" tooltipText: "Run a speed test" foreground: root.bar.foreground fontFamily: root.bar.fontFamily iconSize: Style.font.subtitle * 1.5 horizontalPadding: Style.space(5) verticalPadding: Style.space(2) hasCursor: root.speedHeaderHasCursor Layout.alignment: Qt.AlignVCenter onHovered: function(on) { if (on) root.setHeaderCursor(root.speedHeaderIndex) } onClicked: root.showSpeedTest() } ToggleSwitch { id: powerSwitch visible: root.canToggleWifi checked: Networking.wifiEnabled hasCursor: root.toggleHeaderHasCursor foreground: root.bar.foreground Layout.alignment: Qt.AlignVCenter onHovered: function(on) { if (on) root.setHeaderCursor(root.toggleHeaderIndex) } onToggled: root.toggleNetwork() PanelToolTip { visible: powerSwitch.containsMouse text: root.toggleHint fontFamily: root.bar.fontFamily } } } Column { id: heroLabels anchors.left: heroIcon.right anchors.leftMargin: Style.space(14) anchors.right: parent.right anchors.rightMargin: heroActions.width > 0 ? heroActions.width + Style.space(12) : 0 anchors.verticalCenter: parent.verticalCenter spacing: Style.space(2) // Link detail rides inline after the name — "Ethernet (2.5gbit)" — // rather than in a pill, which crowded the on/off switch. Text { id: heroSsid width: parent.width readonly property string title: { 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") } readonly property string detail: root.headerDetail() text: heroSsid.detail !== "" ? heroSsid.title + " (" + heroSsid.detail + ")" : heroSsid.title color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.title font.bold: true elide: Text.ElideRight } Text { id: heroMeta width: parent.width text: { if (root.info.type === "wifi") { if (root.canDisconnect) return root.connectionPhrase.toUpperCase() if (root.kind === "disconnected") return "NOT CONNECTED" return "" } if (root.info.type === "ethernet") return root.connectionPhrase.toUpperCase() if (root.kind === "disconnected") return "NOT CONNECTED" return "" } visible: text !== "" color: Qt.darker(root.bar.foreground, 1.4) font.family: root.bar.fontFamily font.pixelSize: Style.font.caption font.bold: true font.letterSpacing: 1.2 elide: Text.ElideRight } } } // Connection details: transfer metrics first, then IP/Gateway. Column { visible: !!root.info.iface width: parent.width spacing: Style.spacing.labelGap GridLayout { width: parent.width columns: 4 columnSpacing: Style.space(20) rowSpacing: Style.spacing.labelGap // Always mounted: these two used to appear a beat after the panel // opened, once the first probe returned, shoving everything below // them down. They now hold their place and read "--" until there is // a sample. InfoLabel { text: "Ping" } DetailValue { text: root.formatPingLatency(root.internetPingLatency) color: root.internetPingPacketLoss > 0 ? root.bar.urgent : root.bar.foreground } InfoLabel { text: "Packet Loss" } DetailValue { text: root.formatPacketLoss(root.internetPingPacketLoss) color: root.internetPingPacketLoss > 0 ? root.bar.urgent : root.bar.foreground } InfoLabel { text: "Receiving" } DetailValue { text: root.hasTransferStats ? root.formatRate(root.downloadRate) : "--" } InfoLabel { text: "Sending" } DetailValue { text: root.hasTransferStats ? root.formatRate(root.uploadRate) : "--" } InfoLabel { text: "Downloaded" } DetailValue { text: root.hasTransferStats ? root.formatBytes(parseFloat(root.info.rx_bytes || "0")) : "--" } InfoLabel { text: "Uploaded" } DetailValue { text: root.hasTransferStats ? root.formatBytes(parseFloat(root.info.tx_bytes || "0")) : "--" } InfoLabel { text: "IP Address" } DetailValue { text: root.info.ip || "--" copyable: !!root.info.ip tooltipText: "Copy IP" } InfoLabel { text: "Gateway" } DetailValue { text: root.info.gateway || "--" copyable: !!root.info.gateway tooltipText: "Copy gateway" } } } // Wi-Fi band selection. Only on Wi-Fi, and only when the network answers // on more than one band -- a single-band AP has nothing to toggle. PanelSeparator { visible: root.canSelectBand foreground: root.bar.foreground } Column { visible: root.canSelectBand width: parent.width spacing: Style.space(10) // "Automatic" rides on the header line rather than under the pills: it // qualifies the whole row, and at header scale it reads as a modifier // instead of competing with the band choices for attention. Item { width: parent.width implicitHeight: Math.max(bandHeader.implicitHeight, bandAutoRow.implicitHeight) PanelSectionHeader { id: bandHeader text: root.bandSectionTitle foreground: root.bar.foreground fontFamily: root.bar.fontFamily anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter } Row { id: bandAutoRow anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: Style.space(6) PanelSectionHeader { id: bandAutoLabel text: "AUTOMATIC" foreground: root.bar.foreground fontFamily: root.bar.fontFamily anchors.verticalCenter: parent.verticalCenter } // Sized off the label rather than the theme's control height so it // reads as part of the header, and centred on the label's *glyphs*: // PanelSectionHeader carries topPadding to protect Nerd Font // overshoot, which pushes its text below its own box centre, so a // plain verticalCenter would sit the switch visibly high. ToggleSwitch { id: bandAutoSwitch trackHeight: Math.round(bandAutoLabel.font.pixelSize * 1.2) cursorPad: Style.space(3) anchors.verticalCenter: bandAutoLabel.verticalCenter anchors.verticalCenterOffset: Math.round(bandAutoLabel.topPadding / 2) checked: !root.bandPinned busy: root.bandBusy hasCursor: root.cursorActive && root.focusSection === "band" && root.bandAutoFocused foreground: root.bar.foreground onToggled: root.toggleBandAuto() onHovered: function(isHovered) { if (!isHovered) return root.cursorActive = true root.focusSection = "band" root.bandAutoFocused = true } PanelToolTip { visible: bandAutoSwitch.containsMouse text: root.bandPinned ? "Let Wi-Fi pick the band" : "Stay on " + root.bandLabel(root.bandCurrent) fontFamily: root.bar.fontFamily } } } } // Collapsing container: the pills animate their height so toggling // Automatic slides the sections below into place instead of snapping. // `visible` only drops at a real zero, which keeps the row rendered for // the whole animation and takes it out of the Column's spacing once // it's actually gone. Item { id: bandPillsClip width: parent.width clip: true visible: height > 0 height: root.bandPillsVisible ? bandRow.implicitHeight : 0 opacity: root.bandPillsVisible ? 1 : 0 Behavior on height { NumberAnimation { duration: 140; easing.type: Easing.OutCubic } } Behavior on opacity { NumberAnimation { duration: 140; easing.type: Easing.OutCubic } } Row { id: bandRow width: parent.width spacing: Style.space(6) readonly property int count: Math.max(1, root.bandAvailable.length) readonly property real cellWidth: (width - spacing * (count - 1)) / count // Wrapper takes modelData/index from the Repeater's delegate // context, which doesn't bind into nested `component` declarations, // and passes them down explicitly -- same shape as the network // list delegate. Repeater { model: root.bandAvailable delegate: Item { required property var modelData required property int index width: bandRow.cellWidth height: bandPill.implicitHeight BandPill { id: bandPill band: modelData slot: index width: parent.width } } } } } } // DNS provider selection. PanelSeparator { foreground: root.bar.foreground } Column { width: parent.width spacing: Style.space(10) PanelSectionHeader { text: "DNS PROVIDER" foreground: root.bar.foreground fontFamily: root.bar.fontFamily } Row { id: dnsRow width: parent.width spacing: Style.space(6) readonly property int count: 4 readonly property real cellWidth: (width - spacing * (count - 1)) / count DnsProviderPill { provider: "DHCP" index: 0 tooltipText: "Use DNS from DHCP" width: dnsRow.cellWidth onClicked: root.setDns(provider) } DnsProviderPill { provider: "Cloudflare" index: 1 tooltipText: "Set DNS to Cloudflare" width: dnsRow.cellWidth onClicked: root.setDns(provider) } DnsProviderPill { provider: "Google" index: 2 tooltipText: "Set DNS to Google" width: dnsRow.cellWidth onClicked: root.setDns(provider) } DnsProviderPill { provider: "Custom" index: 3 tooltipText: "Set custom DNS servers" width: dnsRow.cellWidth onClicked: root.setDns(provider) } } } // Wi-Fi networks (only if a Wi-Fi station is available). PanelSeparator { visible: root.wifiStationAvailable foreground: root.bar.foreground } PanelSectionHeader { visible: root.wifiStationAvailable && root.scanning text: "SCANNING WI-FI…" foreground: root.bar.foreground fontFamily: root.bar.fontFamily } // Scrollable network list — cap the height so a busy neighbourhood // doesn't push the popup off-screen. ListView (vs Repeater+Column) // gives us positionViewAtIndex for free, which is what keeps the // keyboard-selected row scrolled into view as j/k walk past the // visible window. ListView { id: networkList visible: root.wifiStationAvailable width: parent.width height: Math.min(contentHeight, Style.space(240)) spacing: Style.space(4) clip: true boundsBehavior: Flickable.StopAtBounds interactive: contentHeight > height ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } model: root.wifiStationAvailable ? root.wifiNetworks : [] currentIndex: root.selectedIndex onCurrentIndexChanged: if (currentIndex >= 0) positionViewAtIndex(currentIndex, ListView.Contain) // Wrapper takes the required props from ListView's delegate context // (which doesn't bind into nested `component` declarations like // NetworkRow) and passes them down explicitly. delegate: Item { required property var modelData required property int index readonly property string sectionTitle: root.wifiSectionTitle(index) width: ListView.view.width height: delegateColumn.implicitHeight Column { id: delegateColumn width: parent.width spacing: Style.space(4) PanelSectionHeader { visible: sectionTitle !== "" text: sectionTitle foreground: root.bar.foreground fontFamily: root.bar.fontFamily height: visible ? implicitHeight : 0 } NetworkRow { id: row width: parent.width net: modelData index: parent.parent.index } } } } } } } WifiQrPanel { anchorItem: button bar: root.bar qrRows: root.qrRows qrSize: root.qrSize loading: root.qrLoading error: root.qrError ssid: root.info.ssid || "" secured: root.connectedWifiNetwork ? root.isProtected(root.connectedWifiNetwork.security) : false password: root.qrPassword passwordVisible: root.qrPasswordVisible passwordError: root.qrPasswordError open: root.qrVisible onCloseRequested: root.hideWifiQr() onPasswordToggleRequested: root.toggleQrPassword() } SpeedTestPanel { anchorItem: button bar: root.bar running: root.speedTestRunning phase: root.speedTestPhase downloadMbps: root.speedTestDownloadMbps uploadMbps: root.speedTestUploadMbps error: root.speedTestError connectionName: { if (root.info.type === "wifi") return root.info.ssid || "Wi-Fi" if (root.info.type === "ethernet") return "Ethernet" return "" } open: root.speedTestModalOpen onCloseRequested: root.hideSpeedTest() onRunAgainRequested: root.runSpeedTest() } // One Wi-Fi band pill. `active` (fill) is the band actually in use and // `selected` (bold) is the pinned choice; with Automatic on nothing is // pinned, so only the live band lights up and the two can no longer read as // a contradiction. They land on the same pill once a band is pinned. component BandPill: Button { id: pill required property string band required property int slot text: root.bandLabel(band) tooltipText: root.bandTooltip(band) fontSize: Style.font.bodySmall foreground: root.bar.foreground fontFamily: root.bar.fontFamily horizontalPadding: Style.spacing.controlPaddingX verticalPadding: Style.spacing.controlPaddingY + Style.space(2) bordered: true active: root.bandCurrent === band selected: root.bandEffective === band hasCursor: root.cursorActive && root.focusSection === "band" && !root.bandAutoFocused && root.bandIndex === slot onClicked: root.setBand(band) onHovered: function(isHovered) { if (!isHovered) return root.cursorActive = true root.focusSection = "band" root.bandIndex = pill.slot } } // One DNS provider pill. The cursor + current visuals come entirely from // CursorSurface; this component just binds them to the panel's cursor // state and renders the label/tooltip/click target. component DnsProviderPill: Button { id: pill required property string provider required property int index text: provider fontSize: Style.font.bodySmall foreground: root.bar.foreground fontFamily: root.bar.fontFamily horizontalPadding: Style.spacing.controlPaddingX verticalPadding: Style.spacing.controlPaddingY + Style.space(2) bordered: true // Map the panel's domain semantics onto Button's structural props: // `current DNS` is the pill's `active` fill; the keyboard cursor lights // up `hasCursor`. active: root.dnsProvider === provider hasCursor: root.cursorActive && root.focusSection === "dns" && root.dnsIndex === index onHovered: function(isHovered) { if (!isHovered) return root.cursorActive = true root.focusSection = "dns" root.dnsIndex = pill.index } } // A single Wi-Fi network entry. Collapses to a one-line pill normally; // expands inline to a passphrase prompt when the user picks a protected // network we don't have credentials for. Clicking a connected row // disconnects. component NetworkRow: CursorSurface { id: row required property var net required property int index readonly property bool isConnected: net && net.connected readonly property bool isKnown: !!(net && net.known) readonly property bool isProtected: net ? root.isProtected(net.security) : false readonly property bool isEnterprise: net ? (net.security === WifiSecurityType.Wpa2Eap || net.security === WifiSecurityType.WpaEap) : false readonly property bool canForgetFromLock: isKnown && isProtected && !isConnected readonly property bool isSelected: root.focusSection === "wifi" && root.selectedIndex === index readonly property bool forgetFocused: isSelected && root.wifiActionFocused && canForgetFromLock readonly property bool forgetVisible: canForgetFromLock && (forgetFocused || rightMouse.containsMouse) hasCursor: root.cursorActive && isSelected && !root.wifiActionFocused current: isConnected foreground: root.bar.foreground fill: root.hoverFill currentFill: root.selectedFill // Gate on the matching *Kind/*Reason being non-empty so a hidden-SSID // row (ssid == "") doesn't match the "" defaults of actionSsid etc. readonly property bool isBusy: root.actionKind !== "" && root.actionSsid === (net ? net.ssid : "") readonly property bool isFailed: root.failureReason !== "" && root.failureSsid === (net ? net.ssid : "") readonly property bool isPasswordOpen: root.passwordSsid !== "" && root.passwordSsid === (net ? net.ssid : "") function submitCredentials() { if (!net || root.busy || root.passwordText.length === 0) return if (!isEnterprise) return root.connectWithPassphrase(net.ssid, root.passwordText) if (root.identityText.length > 0) root.connectEnterprise(net.ssid, root.identityText, root.passwordText) } Connections { target: row.net ? row.net.network : null function onConnectionFailed(reason) { root.failNetworkAction(row.net.network, reason) if (reason === ConnectionFailReason.NoSecrets) root.openPasswordPrompt(row.net.ssid) } function onConnectedChanged() { if (row.net) root.checkActionCompletion(row.net.network) } function onKnownChanged() { if (row.net) root.checkActionCompletion(row.net.network) } function onStateChangingChanged() { if (row.net) root.checkActionCompletion(row.net.network) } } readonly property string statusText: { if (!net) return "" if (isPasswordOpen) return "" if (isBusy && root.actionKind === "connect") return "Connecting…" if (isBusy && root.actionKind === "disconnect") return "Disconnecting…" if (isBusy && root.actionKind === "forget") return "Forgetting…" if (isFailed) return root.failureReason || "Failed" if (isConnected) return "Connected" return "" } readonly property color statusColor: { if (isFailed) return root.bar.urgent if (isBusy) return root.bar.foreground if (isConnected) return root.bar.foreground return Qt.darker(root.bar.foreground, 1.5) } implicitHeight: rowBody.implicitHeight + (isPasswordOpen ? passwordPanel.implicitHeight + Style.spacing.md : 0) MouseArea { id: rowMouse anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top height: rowBody.implicitHeight hoverEnabled: true acceptedButtons: Qt.LeftButton cursorShape: Qt.PointingHandCursor enabled: !root.busy // Move the cursor here when the mouse enters; mouse leaving doesn't // clear it (so the cursor stays where the mouse last was and // subsequent j/k pick up from this row). onContainsMouseChanged: if (containsMouse) { root.cursorActive = true; root.focusSection = "wifi"; root.selectedIndex = row.index; root.wifiActionFocused = false } onClicked: { if (!row.net) return // Resync cursor in case keyboard nav moved it away while the mouse // stayed parked on this row — the click target is unambiguously here. root.cursorActive = true root.focusSection = "wifi" root.selectedIndex = row.index root.wifiActionFocused = false if (row.isConnected) { root.disconnect(row.net.network) return } if (row.isProtected && !row.isKnown) { root.openPasswordPrompt(row.net.ssid) return } root.connectKnown(row.net.ssid) } } Item { id: rowBody anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.leftMargin: Style.space(10) anchors.rightMargin: Style.space(10) implicitHeight: Math.max(networkIcon.implicitHeight, networkInfo.implicitHeight, rightAction.implicitHeight) + Style.spacing.rowPaddingX Text { id: networkIcon text: row.net ? root.wifiIconFor(row.net.signal) : "" color: row.statusColor font.family: root.bar.fontFamily font.pixelSize: Style.font.title anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter } // Shows a lock glyph for protected networks. Known disconnected // networks reveal the forget action when hovering that right edge. Item { id: rightAction visible: row.isProtected width: Style.space(22) implicitHeight: lockIndicator.implicitHeight anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter Text { id: lockIndicator width: parent.width anchors.verticalCenter: parent.verticalCenter horizontalAlignment: Text.AlignHCenter text: row.forgetVisible ? "󰅙" : "󰌾" color: row.forgetVisible ? root.bar.urgent : Qt.darker(root.bar.foreground, 1.4) font.family: root.bar.fontFamily font.pixelSize: Style.font.subtitle } BorderSurface { anchors.fill: parent visible: row.forgetFocused color: Style.hoverFillFor(root.bar.urgent, root.bar.urgent) borderSpec: Border.controlSpec("hover-cursor", root.bar.urgent, root.bar.urgent) radius: Style.cornerRadius z: -1 } MouseArea { id: rightMouse anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.LeftButton enabled: row.canForgetFromLock && !root.busy cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor onContainsMouseChanged: if (containsMouse) { root.cursorActive = true; root.focusSection = "wifi"; root.selectedIndex = row.index; root.wifiActionFocused = true } onClicked: if (row.net) root.forget(row.net) } PanelToolTip { visible: rightMouse.containsMouse || row.forgetFocused text: "Forget network" fontFamily: root.bar.fontFamily } } Column { id: networkInfo spacing: Style.space(1) anchors.left: networkIcon.right anchors.leftMargin: Style.space(10) anchors.right: rightAction.visible ? rightAction.left : parent.right anchors.rightMargin: rightAction.visible ? Style.space(8) : 0 anchors.verticalCenter: parent.verticalCenter Text { text: row.net ? (row.net.ssid || "Hidden") : "" color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.body elide: Text.ElideRight width: parent.width } Text { // Signal strength is conveyed by the wifi-bars icon and the // right-edge glyph/buttons carry protection or forget affordances, // so the second line only carries action status (Connecting…, // Connected, Failed, etc.). Collapses to zero height when empty // so rows without status keep a tight one-line look. text: row.statusText visible: row.statusText !== "" height: visible ? implicitHeight : 0 color: row.statusColor font.family: root.bar.fontFamily font.pixelSize: Style.font.caption elide: Text.ElideRight width: parent.width } } } Timer { id: failureTimer interval: 2000 running: row.isFailed && row.isPasswordOpen onTriggered: { root.failureSsid = "" root.failureReason = "" pwField.forceActiveFocus() } } // Inline passphrase prompt — only shown when we hit a protected network // we don't have saved credentials for. Submitting (Enter or the check // button) fires connect; Esc cancels back to the row. Item { id: passwordPanel visible: row.isPasswordOpen anchors.left: parent.left anchors.right: parent.right anchors.top: rowMouse.bottom anchors.leftMargin: Style.space(10) anchors.rightMargin: Style.space(10) anchors.topMargin: Style.space(4) implicitHeight: (idField.visible ? idField.implicitHeight + Style.space(4) : 0) + pwField.implicitHeight + Style.spacing.rowGap height: implicitHeight TextField { id: idField visible: row.isEnterprise && !row.isBusy && !row.isFailed anchors.left: parent.left anchors.right: connectPwBtn.left anchors.top: parent.top anchors.rightMargin: Style.space(6) placeholderText: "Identity (user@domain)" font.family: Style.font.family font.pixelSize: Style.font.body foreground: root.bar.foreground horizontalPadding: Style.spacing.controlGap verticalPadding: Style.spacing.controlPaddingY enabled: !row.isBusy text: row.isPasswordOpen ? root.identityText : "" onAccepted: pwField.forceActiveFocus() onTextChanged: if (row.isPasswordOpen && text !== root.identityText) root.identityText = text Keys.onEscapePressed: root.cancelPasswordPrompt() onVisibleChanged: if (visible) Qt.callLater(forceActiveFocus) Component.onCompleted: if (visible) Qt.callLater(forceActiveFocus) } TextField { id: pwField visible: !row.isBusy && !row.isFailed anchors.left: parent.left anchors.right: connectPwBtn.left anchors.bottom: parent.bottom anchors.bottomMargin: Style.spacing.rowGap / 2 anchors.rightMargin: Style.space(6) password: true placeholderText: "Passphrase" font.family: Style.font.family font.pixelSize: Style.font.body foreground: root.bar.foreground horizontalPadding: Style.spacing.controlGap verticalPadding: Style.spacing.controlPaddingY enabled: !row.isBusy text: row.isPasswordOpen ? root.passwordText : "" onAccepted: row.submitCredentials() onTextChanged: if (row.isPasswordOpen && text !== root.passwordText) root.passwordText = text Keys.onEscapePressed: root.cancelPasswordPrompt() onVisibleChanged: if (visible && !row.isEnterprise) Qt.callLater(forceActiveFocus) Component.onCompleted: if (visible && !row.isEnterprise) Qt.callLater(forceActiveFocus) } BorderSurface { id: statusMsgWrapper visible: row.isBusy || row.isFailed anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter height: Style.spacing.controlHeight color: Style.normalFillFor(root.bar.foreground) borderSpec: Border.controlSpec("normal", root.bar.foreground, Color.accent) radius: Style.cornerRadius Text { anchors.fill: parent horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter text: row.isFailed ? "Wrong password" : "Connecting..." color: row.isFailed ? root.bar.urgent : root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.bodySmall } } // 22×22 right-anchored to line up with lockIndicator above. Esc closes // the prompt (handled by pwField.Keys.onEscapePressed) // so there's no separate cancel button. PanelActionButton { id: connectPwBtn visible: !row.isBusy && !row.isFailed anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter enabled: row.net && pwField.text.length > 0 && (!row.isEnterprise || idField.text.length > 0) iconText: "󰄬" tooltipText: "Connect" foreground: root.bar.foreground fontFamily: root.bar.fontFamily onClicked: row.submitCredentials() } } } component DetailValue: InfoValue { property bool copyable: false property string tooltipText: "Copy to clipboard" Layout.fillWidth: true horizontalAlignment: Text.AlignRight MouseArea { id: valueMouse anchors.fill: parent enabled: copyable && parent.text !== "" hoverEnabled: enabled cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor onClicked: root.copyToClipboard(parent.text) } PanelToolTip { visible: valueMouse.enabled && valueMouse.containsMouse text: tooltipText fontFamily: root.bar.fontFamily } } component InfoLabel: Text { color: root.bar.foreground opacity: 0.6 font.family: root.bar.fontFamily font.pixelSize: Style.font.bodySmall } component InfoValue: Text { color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: Style.font.bodySmall } }