Added airpllane toggle. Minimal wifi UI

This commit is contained in:
Turann_
2026-02-11 15:08:01 +03:00
parent f59f4e6cf3
commit 0acae22bc9
9 changed files with 350 additions and 214 deletions
+2 -1
View File
@@ -505,7 +505,8 @@
"width": "Width",
"windows": "Windows",
"yes": "Yes",
"confirm": "Confirm"
"confirm": "Confirm",
"settings": "Settings"
},
"control-center": {
"power-profile": {
+3 -2
View File
@@ -99,8 +99,8 @@
"clockStyle": "custom",
"clockFormat": "hh\\nmm",
"lockScreenMonitors": [],
"lockScreenBlur": 0.0,
"lockScreenTint": 0.0,
"lockScreenBlur": 0,
"lockScreenTint": 0,
"keybinds": {
"keyUp": [
"Up"
@@ -335,6 +335,7 @@
},
"network": {
"wifiEnabled": true,
"airplaneModeEnabled": false,
"bluetoothRssiPollingEnabled": false,
"bluetoothRssiPollIntervalMs": 60000,
"wifiDetailsViewMode": "grid",
+1
View File
@@ -540,6 +540,7 @@ Singleton {
// network
property JsonObject network: JsonObject {
property bool wifiEnabled: true
property bool airplaneModeEnabled: false // New property for persistent airplane mode state
property bool bluetoothRssiPollingEnabled: false // Opt-in Bluetooth RSSI polling (uses bluetoothctl)
property int bluetoothRssiPollIntervalMs: 60000 // Polling interval in milliseconds for RSSI queries
property string wifiDetailsViewMode: "grid" // "grid" or "list"
+16 -1
View File
@@ -3,6 +3,7 @@ import QtQuick.Controls
import Quickshell
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Modules.Panels.Settings // For SettingsPanel
import qs.Services.Networking
import qs.Services.UI
import qs.Widgets
@@ -49,6 +50,16 @@ Item {
"action": "toggle-wifi",
"icon": Settings.data.network.wifiEnabled ? "wifi-off" : "wifi"
},
{
"label": I18n.tr("tooltips.manage-wifi") + " " + I18n.tr("tooltips.open-settings"),
"action": "wifi-settings",
"icon": "settings"
},
{
"label": I18n.tr("panels.connections.ethernet") + " " + I18n.tr("tooltips.open-settings"),
"action": "ethernet-settings",
"icon": "settings"
},
{
"label": I18n.tr("actions.widget-settings"),
"action": "widget-settings",
@@ -62,7 +73,11 @@ Item {
if (action === "toggle-wifi") {
NetworkService.setWifiEnabled(!Settings.data.network.wifiEnabled);
} else if (action === "widget-settings") {
} else if (action === "wifi-settings") {
SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 0, screen);
}else if (action === "ethernet-settings") {
SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 2, screen);
}else if (action === "widget-settings") {
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
}
}
@@ -174,6 +174,7 @@ Item {
NToggle {
checked: BluetoothService.enabled
enabled: !NetworkService.bluetoothBlocked || !Settings.data.network.airplaneModeEnabled
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
Layout.alignment: Qt.AlignVCenter
}
@@ -1,22 +1,106 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import Quickshell
import Quickshell.Bluetooth
import qs.Commons
import qs.Services.Networking
import qs.Services.System
import qs.Services.UI
import qs.Widgets
ColumnLayout {
id: root
spacing: Style.marginL
Item {
id: wifiprefs
Layout.fillWidth: true
implicitHeight: mainLayout.implicitHeight
NToggle {
label: I18n.tr("actions.enable-wifi")
description: I18n.tr("panels.connections.wifi-description")
checked: ProgramCheckerService.nmcliAvailable && Settings.data.network.wifiEnabled
onToggled: checked => NetworkService.setWifiEnabled(checked)
enabled: ProgramCheckerService.nmcliAvailable
// Combined visibility check: tab must be visible AND the window must be visible
readonly property bool effectivelyVisible: wifiprefs.visible && Window.window && Window.window.visible
ColumnLayout {
id: mainLayout
anchors.left: parent.left
anchors.right: parent.right
spacing: Style.marginL
// Airplane Mode Toggle
NBox {
Layout.fillWidth: true
Layout.preferredHeight: masterControlColAirplane.implicitHeight
ColumnLayout {
id: masterControlColAirplane
anchors.fill: parent
spacing: Style.marginM
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NIcon {
icon: (NetworkService.bluetoothBlocked || NetworkService.wifiBlocked) ? "plane" : "plane-off"
pointSize: Style.fontSizeXXL
color: (NetworkService.bluetoothBlocked || NetworkService.wifiBlocked) ? Color.mPrimary : Color.mOnSurfaceVariant
}
NText {
text: I18n.tr("toast.airplane-mode.title")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NToggle {
checked: Settings.data.network.airplaneModeEnabled
onToggled: checked => {
Settings.data.network.airplaneModeEnabled = checked; // Store the new state
NetworkService.setAirplaneMode(checked); // Call new function
}
Layout.alignment: Qt.AlignVCenter
}
}
}
}
// Wi-Fi Master Control
NBox {
Layout.fillWidth: true
Layout.preferredHeight: masterControlCol.implicitHeight
ColumnLayout {
id: masterControlCol
anchors.fill: parent
spacing: Style.marginM
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM
NIcon {
icon: Settings.data.network.wifiEnabled ? "wifi" : "wifi-off"
pointSize: Style.fontSizeXXL
color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant
}
NText {
text: I18n.tr("tooltips.manage-wifi")
pointSize: Style.fontSizeL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NToggle {
checked: Settings.data.network.wifiEnabled
onToggled: checked => NetworkService.setWifiEnabled(checked)
Layout.alignment: Qt.AlignVCenter
enabled: !NetworkService.wifiBlocked || !Settings.data.network.airplaneModeEnabled
}
}
}
}
}
}
@@ -33,8 +33,8 @@ ColumnLayout {
}
NTabButton {
text: I18n.tr("panels.connections.ethernet")
// visible: NetworkService.wifiAvailable
enabled: NetworkService.wifiAvailable // Remove when work finished, only use visibility
// visible: NetworkService.ethernetAvailable
enabled: NetworkService.ethernetAvailable // Remove when work finished, only use visibility
tabIndex: 2
checked: subTabBar.currentIndex === 2
}
+66 -168
View File
@@ -15,19 +15,16 @@ Singleton {
readonly property int ctlPollMs: 10000
readonly property int ctlPollSoonMs: 250
property bool airplaneModeToggled: false
property bool lastBluetoothBlocked: false
property bool lastWifiBlocked: false
readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter
// Power/blocked/availability state
readonly property bool bluetoothAvailable: adapter ? adapter !== null : false
property bool ctlAvailable: false
readonly property bool bluetoothAvailable: !!adapter
readonly property bool enabled: adapter ? adapter.enabled : root.ctlPowered
readonly property bool blocked: adapter?.state === BluetoothAdapterState.Blocked
property bool ctlPowered: false
property bool ctlDiscovering: false
property bool ctlDiscoverable: false
// Adapter discoverability (advertising) flag (driven by bluetoothctl)
readonly property bool discoverable: root.ctlDiscoverable
readonly property var devices: adapter ? adapter.devices : null
@@ -35,16 +32,17 @@ Singleton {
if (!adapter || !adapter.devices) {
return [];
}
return adapter.devices.values.filter(function (dev) {
return dev && dev.connected;
});
return adapter.devices.values.filter(dev => dev && dev.connected);
}
// Current backend in use for scanning (e.g., "native", "bluetoothctl")
property string backendUsed: ""
// Experimental: besteffort RSSI polling for connected devices (without root)
// Enabled in debug mode or via user setting in Settings > Network
property bool rssiPollingEnabled: (Settings && (Settings.isDebug || (Settings.data && Settings.data.network && Settings.data.network.bluetoothRssiPollingEnabled))) ? true : false
property bool rssiPollingEnabled: Settings?.data?.network?.bluetoothRssiPollingEnabled || Settings?.isDebug || false
// Interval can be configured from Settings; defaults to 60s
property int rssiPollIntervalMs: (Settings && Settings.data && Settings.data.network && Settings.data.network.bluetoothRssiPollIntervalMs) ? Settings.data.network.bluetoothRssiPollIntervalMs : 60000
property int rssiPollIntervalMs: Settings?.data?.network?.bluetoothRssiPollIntervalMs || 60000
// RSSI helper subcomponent
property BluetoothRssi rssi: BluetoothRssi {
enabled: root.enabled && root.rssiPollingEnabled
@@ -60,57 +58,36 @@ Singleton {
// Internal: temporarily pause discovery during pair/connect to reduce HCI churn
property bool _discoveryWasRunning: false
// Persistent process for fallback scanning to keep the session alive
// Persistent process for bluetoothctl scanning when native discovery is unavailable
Process {
id: fallbackScanProcess
// Pipe scan on and a long sleep to bluetoothctl to keep it running
// Afaik we don't need bluetoothctl scanning for an entire hour. back then this was made to save to day. (when i originaly wrote this)
command: ["sh", "-c", "(echo 'scan on'; sleep 30) | bluetoothctl"] // trap is not sending sigterm to bctl, guess what to parent (qs)... | BAD Idea should have guessed that, expected to sh to crash and stop.
onExited: Logger.d("Bluetooth", "Fallback scan process exited")
id: bluetoothctlScanProcess
command: ["bluetoothctl", "scan", "on"]
onExited: Logger.d("Bluetooth", "bluetoothctl scan process exited.")
}
// Unify discovery controls
function setScanActive(active) {
Logger.d("Bluetooth", "setScanActive called with active=" + active); // used for debugging
// Prefer Quickshell API if available, fallback to bluetoothctl
var nativeSuccess = false;
try {
if (adapter) {
if (active && adapter.discovering !== undefined) {
Logger.d("Bluetooth", "Starting discovery with Quickshell API"); // used for debugging
adapter.discovering = true;
nativeSuccess = true;
} else if (!active && adapter.discovering !== undefined) {
Logger.d("Bluetooth", "Stopping discovery with Quickshell API"); // used for debugging
adapter.discovering = false;
nativeSuccess = true;
if (adapter && adapter.discovering !== undefined) {
if (active || adapter.discovering) { // Only attempt to set if activating, or if deactivating and currently currently discovering
adapter.discovering = active;
}
} else {
Logger.w("Bluetooth", "Adapter is null/undefined in setScanActive");
nativeSuccess = true; // Mark as success if adapter was handled without error
}
} catch (e1) {
Logger.e("Bluetooth", "setScanActive failed with exception", e1);
} catch (e) {
Logger.e("Bluetooth", "setScanActive native failed", e);
}
Logger.d("Bluetooth", "nativeSuccess=" + nativeSuccess);
// Only issue bluetoothctl if we didn't use the adapter API
if (!nativeSuccess) {
if (active) {
Logger.d("Bluetooth", "Starting fallback scan process");
fallbackScanProcess.running = true;
bluetoothctlScanProcess.running = true;
} else {
Logger.d("Bluetooth", "Stopping fallback scan process");
fallbackScanProcess.running = false;
// Explicitly send scan off command as well to ensure state is cleared
bluetoothctlScanProcess.running = false;
btExec(["bluetoothctl", "scan", "off"]);
}
} else {
Logger.d("Bluetooth", "Skipping bluetoothctl fallback as native API was used");
// Ensure fallback process is stopped if we switched to native
if (fallbackScanProcess.running) {
fallbackScanProcess.running = false;
}
} else if (bluetoothctlScanProcess.running) {
bluetoothctlScanProcess.running = false;
}
requestCtlPoll(ctlPollSoonMs);
@@ -123,76 +100,20 @@ Singleton {
Logger.i("Bluetooth", "Service started");
}
Component.onCompleted: {
// Prime state immediately so UI reflects correct power/discovery flags
pollCtlState();
}
// Note: We intentionally avoid creating or managing a custom BlueZ agent in-process.
// Pairing flows are delegated to `bluetoothctl` as needed to keep behavior
// consistent and reduce maintenance complexity.
// No implicit discovery auto-start; state polled from bluetoothctl instead
Component.onCompleted: pollCtlState()
// Track adapter state changes
Connections {
target: adapter
function onStateChanged() {
if (!adapter) {
return;
}
if (adapter.state === BluetoothAdapter.Enabling || adapter.state === BluetoothAdapter.Disabling) {
return;
}
Logger.i("Bluetooth", "Bluetooth state change command executed");
const bluetoothBlockedToggled = (root.blocked !== lastBluetoothBlocked);
root.lastBluetoothBlocked = root.blocked;
if (bluetoothBlockedToggled) {
checkWifiBlocked.running = true;
} else if (adapter.state === BluetoothAdapter.Enabled) {
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.enabled"), "bluetooth");
Logger.d("Bluetooth", "Adapter enabled");
} else if (adapter.state === BluetoothAdapter.Disabled) {
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.disabled"), "bluetooth-off");
Logger.d("Bluetooth", "Adapter disabled");
}
if (!adapter || adapter.state === BluetoothAdapter.Enabling || adapter.state === BluetoothAdapter.Disabling) {
return;
}
}
Process {
id: checkWifiBlocked
running: false
command: ["rfkill", "list", "wifi"]
stdout: StdioCollector {
onStreamFinished: {
var wifiBlocked = text && text.trim().indexOf("Soft blocked: yes") !== -1;
Logger.d("Network", "Wi-Fi adapter was detected as blocked:", wifiBlocked);
// Check if airplane mode has been toggled
if (wifiBlocked && root.blocked) {
root.airplaneModeToggled = true;
root.lastWifiBlocked = true;
NetworkService.setWifiEnabled(false);
ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.enabled"), "plane");
} else if (!wifiBlocked && lastWifiBlocked) {
root.airplaneModeToggled = true;
root.lastWifiBlocked = false;
NetworkService.setWifiEnabled(true);
ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.disabled"), "plane-off");
} else if (adapter.enabled) {
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.enabled"), "bluetooth");
Logger.d("Bluetooth", "Adapter enabled");
} else {
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.disabled"), "bluetooth-off");
Logger.d("Bluetooth", "Adapter disabled");
}
root.airplaneModeToggled = false;
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
Logger.w("Bluetooth", "rfkill (wifi) stderr:", text.trim());
}
if (adapter.state === BluetoothAdapter.Enabled) {
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.enabled"), "bluetooth");
} else if (adapter.state === BluetoothAdapter.Disabled && !root.blocked) {
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.disabled"), "bluetooth-off");
}
}
}
@@ -201,25 +122,21 @@ Singleton {
Process {
id: ctlShowProcess
running: false
stdout: StdioCollector {
id: ctlStdout
}
stdout: StdioCollector { id: ctlStdout }
onExited: function (exitCode, exitStatus) {
try {
var text = ctlStdout.text || "";
Logger.d("Bluetooth", "ctlShowProcess exited. Output length: " + text.length);
// Parse Powered/Discoverable/Discovering lines
var mp = text.match(/\bPowered:\s*(yes|no)\b/i);
if (mp && mp.length > 1) {
root.ctlPowered = (mp[1].toLowerCase() === "yes");
}
var md = text.match(/\bDiscoverable:\s*(yes|no)\b/i);
if (md && md.length > 1) {
root.ctlDiscoverable = (md[1].toLowerCase() === "yes");
}
var ms = text.match(/\bDiscovering:\s*(yes|no)\b/i);
if (ms && ms.length > 1) {
root.ctlDiscovering = (ms[1].toLowerCase() === "yes");
var lines = text.split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
var mp = line.match(/\bPowered:\s*(yes|no)\b/i);
if (mp) { root.ctlPowered = (mp[1].toLowerCase() === "yes"); }
var md = line.match(/\bDiscoverable:\s*(yes|no)\b/i);
if (md) { root.ctlDiscoverable = (md[1].toLowerCase() === "yes"); }
var ms = line.match(/\bDiscovering:\s*(yes|no)\b/i);
if (ms) { root.ctlDiscovering = (ms[1].toLowerCase() === "yes"); }
}
} catch (e) {
Logger.d("Bluetooth", "Failed to parse bluetoothctl show output", e);
@@ -243,20 +160,17 @@ Singleton {
interval: ctlPollMs
repeat: true
running: root.enabled
onTriggered: pollCtlState()
}
// Short-delay poll scheduler
Timer {
id: pollCtlStateSoonTimer
interval: ctlPollSoonMs
repeat: false
onTriggered: pollCtlState()
onTriggered: {
pollCtlState();
if (interval !== ctlPollMs) {
interval = ctlPollMs;
}
}
}
function requestCtlPoll(delayMs) {
pollCtlStateSoonTimer.interval = Math.max(50, delayMs || ctlPollSoonMs);
pollCtlStateSoonTimer.restart();
ctlPollTimer.interval = Math.max(50, delayMs || ctlPollSoonMs);
ctlPollTimer.restart();
}
// Handle system wakeup to force-poll and ensure state is up-to-date
@@ -264,8 +178,7 @@ Singleton {
target: Time
function onResumed() {
Logger.i("Bluetooth", "System resumed - forcing state poll");
ctlPollTimer.restart();
pollCtlState();
requestCtlPoll();
}
}
@@ -327,15 +240,6 @@ Singleton {
if (!device) {
return false;
}
/*
Paired
Means youve successfully exchanged keys with the device.
The devices remember each other and can authenticate without repeating the pairing process.
Example: once your headphones are paired, you dont need to type a PIN every time.
Hence, instead of !device.paired, should be device.connected
*/
// Only allow connect if device is already paired or trusted
return !device.connected && (device.paired || device.trusted) && !device.pairing && !device.blocked;
}
@@ -349,16 +253,21 @@ Singleton {
// Textual signal quality (translated)
function getSignalStrength(device) {
var p = getSignalPercent(device);
if (p === null)
if (p === null) {
return I18n.tr("bluetooth.panel.signal-text-unknown");
if (p >= 80)
}
if (p >= 80) {
return I18n.tr("bluetooth.panel.signal-text-excellent");
if (p >= 60)
}
if (p >= 60) {
return I18n.tr("bluetooth.panel.signal-text-good");
if (p >= 40)
}
if (p >= 40) {
return I18n.tr("bluetooth.panel.signal-text-fair");
if (p >= 20)
}
if (p >= 20) {
return I18n.tr("bluetooth.panel.signal-text-poor");
}
return I18n.tr("bluetooth.panel.signal-text-very-poor");
}
@@ -382,7 +291,6 @@ Singleton {
if (!device) {
return false;
}
return device.pairing || device.state === BluetoothDevice.Disconnecting || device.state === BluetoothDevice.Connecting;
}
@@ -410,7 +318,6 @@ Singleton {
return;
}
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.pairing"), "bluetooth");
// Delegate pairing to bluetoothctl which registers/uses its own agent
try {
pairWithBluetoothctl(device);
} catch (e) {
@@ -441,8 +348,7 @@ Singleton {
id: pairingProcess
stdout: SplitParser {
onRead: data => {
var chunk = data;
if (chunk.indexOf("[PIN_REQ]") !== -1) {
if (data.indexOf("[PIN_REQ]") !== -1) {
root.pinRequired = true;
Logger.d("Bluetooth", "PIN required for pairing");
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("bluetooth.panel.pin-required"), "lock");
@@ -477,13 +383,11 @@ Singleton {
Logger.i("Bluetooth", "pairWithBluetoothctl", addr);
// Stop any previous pairing attempt
if (pairingProcess.running) {
pairingProcess.running = false;
}
root.pinRequired = false;
// Compute bounded waits from tunables
const pairWait = Math.max(5, Number(root.pairWaitSeconds) | 0);
const attempts = Math.max(1, Number(root.connectAttempts) | 0);
const intervalMs = Math.max(500, Number(root.connectRetryIntervalMs) | 0);
@@ -496,7 +400,6 @@ Singleton {
}
const scriptPath = Quickshell.shellDir + "/Scripts/python/src/network/bluetooth-pair.py";
pairingProcess.command = ["python3", scriptPath, String(addr), String(pairWait), String(attempts), String(intervalSec)];
pairingProcess.running = true;
}
@@ -516,20 +419,15 @@ Singleton {
return "";
}
try {
if (device.pairing)
return "pairing";
if (device.blocked)
return "blocked";
if (device.state === BluetoothDevice.Connecting)
return "connecting";
if (device.state === BluetoothDevice.Disconnecting)
return "disconnecting";
if (device.pairing) return "pairing";
if (device.blocked) return "blocked";
if (device.state === BluetoothDevice.Connecting) return "connecting";
if (device.state === BluetoothDevice.Disconnecting) return "disconnecting";
} catch (_) {}
return "";
}
function unpairDevice(device) {
// Alias to forgetDevice for clarity in UI
forgetDevice(device);
}
@@ -570,4 +468,4 @@ Singleton {
ToastService.showWarning(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.forget-failed"));
}
}
}
}
+166 -31
View File
@@ -70,18 +70,22 @@ Singleton {
}
}
property bool airplaneModeToggled: false
property bool bluetoothBlocked: false
property bool wifiBlocked: false
Connections {
target: Settings.data.network
function onWifiEnabledChanged() {
if (Settings.data.network.wifiEnabled) {
if (!BluetoothService.airplaneModeToggled) {
if (!root.airplaneModeToggled) {
ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("common.enabled"), "wifi");
}
// Perform a scan to update the UI
delayedScanTimer.interval = 3000;
delayedScanTimer.restart();
} else {
if (!BluetoothService.airplaneModeToggled) {
if (!root.airplaneModeToggled) {
ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("common.disabled"), "wifi-off");
}
// Clear networks so the widget icon changes
@@ -90,6 +94,100 @@ Singleton {
}
}
// Handle Airplane Mode detection via rfkill
Process {
id: checkWifiBlocked
running: false
command: ["rfkill", "list", "wifi"]
stdout: StdioCollector {
onStreamFinished: {
var wifiBlocked = text && text.trim().indexOf("Soft blocked: yes") !== -1;
checkBluetoothBlocked.wifiBlockedState = wifiBlocked;
checkBluetoothBlocked.running = true;
}
}
}
Process {
id: checkBluetoothBlocked
running: false
command: ["rfkill", "list", "bluetooth"]
property bool wifiBlockedState: false // To pass state from checkWifiBlocked
stdout: StdioCollector {
onStreamFinished: {
var wifiBlocked = checkBluetoothBlocked.wifiBlockedState;
var btBlocked = text && text.trim().indexOf("Soft blocked: yes") !== -1;
// Check if airplane mode is desired by the user
var desiredAirplaneMode = Settings.data.network.airplaneModeEnabled;
// Track if actual state changed
var actualAirplaneModeActive = wifiBlocked && btBlocked;
var previousAirplaneModeActive = root.wifiBlocked && root.bluetoothBlocked;
// Enforcement: If desired state doesn't match actual state, force it.
if (desiredAirplaneMode && !actualAirplaneModeActive) {
// User wants airplane mode ON, but it's not actually active. Force it.
Logger.i("Network", "Enforcing Airplane Mode ON (rfkill block all)");
Quickshell.execDetached(["rfkill", "block", "wifi"]);
Quickshell.execDetached(["rfkill", "block", "bluetooth"]);
// We expect subsequent rfkill checks to confirm the state change.
} else if (!desiredAirplaneMode && actualAirplaneModeActive) {
// User wants airplane mode OFF, but it's still active. Force it off.
Logger.i("Network", "Enforcing Airplane Mode OFF (rfkill unblock all)");
Quickshell.execDetached(["rfkill", "unblock", "wifi"]);
Quickshell.execDetached(["rfkill", "unblock", "bluetooth"]);
// We expect subsequent rfkill checks to confirm the state change.
}
// Now handle toasts and update internal state based on current rfkill states.
// This part needs to be outside the enforcement blocks to correctly react to delayed rfkill changes.
if (actualAirplaneModeActive && !previousAirplaneModeActive) {
root.airplaneModeToggled = true; // Temporarily set to suppress individual toasts
ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.enabled"), "plane");
root.airplaneModeToggled = false;
} else if (!actualAirplaneModeActive && previousAirplaneModeActive) {
root.airplaneModeToggled = true; // Temporarily set to suppress individual toasts
ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.disabled"), "plane-off");
root.airplaneModeToggled = false;
} else {
// Standard state change notifications for WiFi only, if not in airplane mode context
if (wifiBlocked !== root.wifiBlocked) {
// Only show individual wifi toast if airplane mode is not currently active
if (!actualAirplaneModeActive) {
if (wifiBlocked) {
ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("common.disabled"), "wifi-off");
} else {
ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("common.enabled"), "wifi");
}
}
}
}
// Update current blocked states (always reflect actual rfkill state)
root.wifiBlocked = wifiBlocked;
root.bluetoothBlocked = btBlocked;
}
}
}
// Handle system resume to refresh state and connectivity
Connections {
target: Time
function onResumed() {
Logger.i("Network", "System resumed - forcing state poll");
ethernetStateProcess.running = true;
root.scan();
root.refreshActiveWifiDetails();
root.refreshActiveEthernetDetails();
connectivityCheckProcess.running = true;
checkWifiBlocked.running = true; // Refresh airplane mode state after resume
}
}
Component.onCompleted: {
Logger.i("Network", "Service started");
if (ProgramCheckerService.nmcliAvailable) {
@@ -100,6 +198,7 @@ Singleton {
ethernetStateProcess.running = true;
refreshActiveWifiDetails();
refreshActiveEthernetDetails();
checkWifiBlocked.running = true; // Trigger airplane mode check on startup
}
}
@@ -164,8 +263,9 @@ Singleton {
function refreshActiveWifiDetails() {
const now = Date.now();
// If we're already fetching, don't start a new one
if (detailsLoading)
if (detailsLoading) {
return;
}
// Use cached details if they are fresh
if (activeWifiIf && activeWifiDetails && (now - activeWifiDetailsTimestamp) < activeWifiDetailsTtlMs)
@@ -199,8 +299,9 @@ Singleton {
// Refresh details for the currently active Ethernet link
function refreshActiveEthernetDetails() {
const now = Date.now();
if (ethernetDetailsLoading)
if (ethernetDetailsLoading) {
return;
}
if (!root.ethernetConnected) {
// Link is down: keep the selected interface so UI can still show its info as disconnected
// Only clear details to avoid showing stale IP/speed/etc.
@@ -226,23 +327,40 @@ Singleton {
onTriggered: connectivityCheckProcess.running = true
}
function setAirplaneMode(enabled) {
if (enabled) {
Logger.i("Network", "Executing rfkill block all (airplane mode ON)");
Quickshell.execDetached(["rfkill", "block", "wifi"]);
Quickshell.execDetached(["rfkill", "block", "bluetooth"]);
} else {
Logger.i("Network", "Executing rfkill unblock all (airplane mode OFF)");
Quickshell.execDetached(["rfkill", "unblock", "wifi"]);
Quickshell.execDetached(["rfkill", "unblock", "bluetooth"]);
}
// Trigger the check immediately to reflect state changes
checkWifiBlocked.running = true;
}
// Core functions
function syncWifiState() {
if (!ProgramCheckerService.nmcliAvailable)
if (!ProgramCheckerService.nmcliAvailable) {
return;
}
wifiStateProcess.running = true;
}
function setWifiEnabled(enabled) {
if (!ProgramCheckerService.nmcliAvailable)
if (!ProgramCheckerService.nmcliAvailable) {
return;
}
Settings.data.network.wifiEnabled = enabled;
wifiStateEnableProcess.running = true;
}
function scan() {
if (!ProgramCheckerService.nmcliAvailable || !Settings.data.network.wifiEnabled)
if (!ProgramCheckerService.nmcliAvailable || !Settings.data.network.wifiEnabled) {
return;
}
if (scanning) {
// Mark current scan results to be ignored and schedule a new scan
Logger.d("Network", "Scan already in progress, will ignore results and rescan");
@@ -267,15 +385,17 @@ Singleton {
// Refresh only Ethernet state/details
function refreshEthernet() {
if (!ProgramCheckerService.nmcliAvailable)
if (!ProgramCheckerService.nmcliAvailable) {
return;
}
ethernetStateProcess.running = true;
refreshActiveEthernetDetails();
}
function connect(ssid, password = "") {
if (!ProgramCheckerService.nmcliAvailable || connecting)
if (!ProgramCheckerService.nmcliAvailable || connecting) {
return;
}
connecting = true;
connectingTo = ssid;
lastError = "";
@@ -295,16 +415,18 @@ Singleton {
}
function disconnect(ssid) {
if (!ProgramCheckerService.nmcliAvailable)
if (!ProgramCheckerService.nmcliAvailable) {
return;
}
disconnectingFrom = ssid;
disconnectProcess.ssid = ssid;
disconnectProcess.running = true;
}
function forget(ssid) {
if (!ProgramCheckerService.nmcliAvailable)
if (!ProgramCheckerService.nmcliAvailable) {
return;
}
forgettingNetwork = ssid;
// Remove from cache
@@ -358,16 +480,21 @@ Singleton {
// Helper functions
function signalIcon(signal, isConnected) {
if (isConnected === undefined)
if (isConnected === undefined) {
isConnected = false;
if (isConnected && !root.internetConnectivity)
}
if (isConnected && !root.internetConnectivity) {
return "world-off";
if (signal >= 80)
}
if (signal >= 80) {
return "wifi";
if (signal >= 50)
}
if (signal >= 50) {
return "wifi-2";
if (signal >= 20)
}
if (signal >= 20) {
return "wifi-1";
}
return "wifi-0";
}
@@ -488,8 +615,9 @@ Singleton {
});
root.ethernetInterfaces = ethList;
if (ifname) {
if (root.activeEthernetIf !== ifname)
root.activeEthernetIf = ifname;
if (root.activeEthernetIf !== ifname) {
root.activeEthernetIf = ifname;
}
ethernetDeviceShowProcess.ifname = ifname;
ethernetDeviceShowProcess.running = true;
} else {
@@ -529,11 +657,13 @@ Singleton {
const lines = text.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line)
continue;
if (!line) {
continue;
}
const idx = line.indexOf(":");
if (idx === -1)
continue;
if (idx === -1) {
continue;
}
const key = line.substring(0, idx);
const val = line.substring(idx + 1);
if (key === "GENERAL.CONNECTION") {
@@ -637,8 +767,9 @@ Singleton {
details.speed = speedText;
// Try to derive numeric value
const m = speedText.match(/([0-9]+(?:\.[0-9]+)?)\s*Mbit\/s/i);
if (m)
details.speedMbit = parseFloat(m[1]);
if (m) {
details.speedMbit = parseFloat(m[1]);
}
root.activeEthernetDetails = details;
}
root.activeEthernetDetailsTimestamp = Date.now();
@@ -711,11 +842,13 @@ Singleton {
const lines = text.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line)
continue;
if (!line) {
continue;
}
const idx = line.indexOf(":");
if (idx === -1)
continue;
if (idx === -1) {
continue;
}
const key = line.substring(0, idx);
const val = line.substring(idx + 1);
if (key.indexOf("IP4.ADDRESS") === 0) {
@@ -806,8 +939,9 @@ Singleton {
var compact = [];
for (var i = 0; i < parts.length; i++) {
var p = parts[i];
if (p && p.length > 0)
if (p && p.length > 0) {
compact.push(p);
}
}
// Find a token that represents Mbit/s and use the previous number
var unitIdx = -1;
@@ -1037,8 +1171,9 @@ Singleton {
for (var i = 0; i < lines.length; ++i) {
const line = lines[i].trim();
if (!line)
continue;
if (!line) {
continue;
}
// Parse from the end to handle SSIDs with colons
// Format is SSID:SECURITY:SIGNAL:IN-USE