1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-29 07:52:50 -05:00

native NetworkManager + all native dbus bindings via dms

- Scrap janky NetworkService in favor of, dms' native NM integration
  socket
- Scrap all gdbus usage in favor of native dbus bindings in dms
  (loginctl, freedesktop)

It means that - some features won't work if running without dms wrapper.

But the trade off is certainly worth it, in the long-run for efficiency
improvements.
This commit is contained in:
bbedward
2025-10-08 12:03:50 -04:00
parent 1ed4abd347
commit 27f9b3cd0b
27 changed files with 1739 additions and 1792 deletions

62
Common/DankSocket.qml Normal file
View File

@@ -0,0 +1,62 @@
import QtQuick
import Quickshell.Io
Item {
id: root
property alias path: socket.path
property alias parser: socket.parser
property bool connected: false
property int reconnectBaseMs: 400
property int reconnectMaxMs: 15000
property int _reconnectAttempt: 0
signal connectionStateChanged()
onConnectedChanged: {
socket.connected = connected
}
Socket {
id: socket
onConnectionStateChanged: {
root.connectionStateChanged()
if (connected) {
root._reconnectAttempt = 0
return
}
if (root.connected) {
root._scheduleReconnect()
}
}
}
Timer {
id: reconnectTimer
interval: 0
repeat: false
onTriggered: {
socket.connected = false
Qt.callLater(() => socket.connected = true)
}
}
function send(data) {
const json = typeof data === "string" ? data : JSON.stringify(data)
const message = json.endsWith("\n") ? json : json + "\n"
socket.write(message)
socket.flush()
}
function _scheduleReconnect() {
const pow = Math.min(_reconnectAttempt, 10)
const base = Math.min(reconnectBaseMs * Math.pow(2, pow), reconnectMaxMs)
const jitter = Math.floor(Math.random() * Math.floor(base / 4))
reconnectTimer.interval = base + jitter
reconnectTimer.restart()
_reconnectAttempt++
}
}

View File

@@ -17,7 +17,7 @@ DankModal {
networkData = data networkData = data
networkInfoModalVisible = true networkInfoModalVisible = true
open() open()
NetworkService.fetchNetworkInfo(ssid) NetworkManagerService.fetchNetworkInfo(ssid)
} }
function hideDialog() { function hideDialog() {
@@ -101,7 +101,7 @@ DankModal {
id: detailsText id: detailsText
width: parent.width width: parent.width
text: NetworkService.networkInfoDetails && NetworkService.networkInfoDetails.replace(/\\n/g, '\n') || "No information available" text: NetworkManagerService.networkInfoDetails && NetworkManagerService.networkInfoDetails.replace(/\\n/g, '\n') || "No information available"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
wrapMode: Text.WordWrap wrapMode: Text.WordWrap

View File

@@ -9,44 +9,64 @@ DankModal {
property string wifiPasswordSSID: "" property string wifiPasswordSSID: ""
property string wifiPasswordInput: "" property string wifiPasswordInput: ""
property string wifiUsernameInput: ""
property bool requiresEnterprise: false
function show(ssid) { function show(ssid) {
wifiPasswordSSID = ssid wifiPasswordSSID = ssid
wifiPasswordInput = "" wifiPasswordInput = ""
wifiUsernameInput = ""
const network = NetworkManagerService.wifiNetworks.find(n => n.ssid === ssid)
requiresEnterprise = network?.enterprise || false
open() open()
Qt.callLater(() => { Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.passwordInput) if (contentLoader.item) {
contentLoader.item.passwordInput.forceActiveFocus() if (requiresEnterprise && contentLoader.item.usernameInput) {
contentLoader.item.usernameInput.forceActiveFocus()
} else if (contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus()
}
}
}) })
} }
shouldBeVisible: false shouldBeVisible: false
width: 420 width: 420
height: 230 height: requiresEnterprise ? 310 : 230
onShouldBeVisibleChanged: () => { onShouldBeVisibleChanged: () => {
if (!shouldBeVisible) if (!shouldBeVisible) {
wifiPasswordInput = "" wifiPasswordInput = ""
wifiUsernameInput = ""
}
} }
onOpened: { onOpened: {
Qt.callLater(() => { Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.passwordInput) if (contentLoader.item) {
contentLoader.item.passwordInput.forceActiveFocus() if (requiresEnterprise && contentLoader.item.usernameInput) {
contentLoader.item.usernameInput.forceActiveFocus()
} else if (contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus()
}
}
}) })
} }
onBackgroundClicked: () => { onBackgroundClicked: () => {
close() close()
wifiPasswordInput = "" wifiPasswordInput = ""
wifiUsernameInput = ""
} }
Connections { Connections {
target: NetworkService target: NetworkManagerService
function onPasswordDialogShouldReopenChanged() { function onPasswordDialogShouldReopenChanged() {
if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") { if (NetworkManagerService.passwordDialogShouldReopen && NetworkManagerService.connectingSSID !== "") {
wifiPasswordSSID = NetworkService.connectingSSID wifiPasswordSSID = NetworkManagerService.connectingSSID
wifiPasswordInput = "" wifiPasswordInput = ""
open() open()
NetworkService.passwordDialogShouldReopen = false NetworkManagerService.passwordDialogShouldReopen = false
} }
} }
} }
@@ -55,6 +75,7 @@ DankModal {
FocusScope { FocusScope {
id: wifiContent id: wifiContent
property alias usernameInput: usernameInput
property alias passwordInput: passwordInput property alias passwordInput: passwordInput
anchors.fill: parent anchors.fill: parent
@@ -62,6 +83,7 @@ DankModal {
Keys.onEscapePressed: event => { Keys.onEscapePressed: event => {
close() close()
wifiPasswordInput = "" wifiPasswordInput = ""
wifiUsernameInput = ""
event.accepted = true event.accepted = true
} }
@@ -85,7 +107,7 @@ DankModal {
} }
StyledText { StyledText {
text: `Enter password for "${wifiPasswordSSID}"` text: requiresEnterprise ? `Enter credentials for "${wifiPasswordSSID}"` : `Enter password for "${wifiPasswordSSID}"`
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium color: Theme.surfaceTextMedium
width: parent.width width: parent.width
@@ -100,10 +122,48 @@ DankModal {
onClicked: () => { onClicked: () => {
close() close()
wifiPasswordInput = "" wifiPasswordInput = ""
wifiUsernameInput = ""
} }
} }
} }
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: usernameInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: usernameInput.activeFocus ? 2 : 1
visible: requiresEnterprise
MouseArea {
anchors.fill: parent
onClicked: () => {
usernameInput.forceActiveFocus()
}
}
DankTextField {
id: usernameInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiUsernameInput
placeholderText: "Username"
backgroundColor: "transparent"
enabled: root.shouldBeVisible
onTextEdited: () => {
wifiUsernameInput = text
}
onAccepted: () => {
if (passwordInput) {
passwordInput.forceActiveFocus()
}
}
}
}
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 50 height: 50
@@ -127,21 +187,24 @@ DankModal {
textColor: Theme.surfaceText textColor: Theme.surfaceText
text: wifiPasswordInput text: wifiPasswordInput
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
placeholderText: "" placeholderText: requiresEnterprise ? "Password" : ""
backgroundColor: "transparent" backgroundColor: "transparent"
focus: true focus: !requiresEnterprise
enabled: root.shouldBeVisible enabled: root.shouldBeVisible
onTextEdited: () => { onTextEdited: () => {
wifiPasswordInput = text wifiPasswordInput = text
} }
onAccepted: () => { onAccepted: () => {
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text) const username = requiresEnterprise ? usernameInput.text : ""
NetworkManagerService.connectToWifi(wifiPasswordSSID, passwordInput.text, username)
close() close()
wifiPasswordInput = "" wifiPasswordInput = ""
wifiUsernameInput = ""
passwordInput.text = "" passwordInput.text = ""
if (requiresEnterprise) usernameInput.text = ""
} }
Component.onCompleted: () => { Component.onCompleted: () => {
if (root.shouldBeVisible) if (root.shouldBeVisible && !requiresEnterprise)
focusDelayTimer.start() focusDelayTimer.start()
} }
@@ -151,8 +214,13 @@ DankModal {
interval: 100 interval: 100
repeat: false repeat: false
onTriggered: () => { onTriggered: () => {
if (root.shouldBeVisible) if (root.shouldBeVisible) {
passwordInput.forceActiveFocus() if (requiresEnterprise && usernameInput) {
usernameInput.forceActiveFocus()
} else {
passwordInput.forceActiveFocus()
}
}
} }
} }
@@ -244,6 +312,7 @@ DankModal {
onClicked: () => { onClicked: () => {
close() close()
wifiPasswordInput = "" wifiPasswordInput = ""
wifiUsernameInput = ""
} }
} }
} }
@@ -253,7 +322,7 @@ DankModal {
height: 36 height: 36
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: passwordInput.text.length > 0 enabled: requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0
opacity: enabled ? 1 : 0.5 opacity: enabled ? 1 : 0.5
StyledText { StyledText {
@@ -274,10 +343,13 @@ DankModal {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: parent.enabled enabled: parent.enabled
onClicked: () => { onClicked: () => {
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text) const username = requiresEnterprise ? usernameInput.text : ""
NetworkManagerService.connectToWifi(wifiPasswordSSID, passwordInput.text, username)
close() close()
wifiPasswordInput = "" wifiPasswordInput = ""
wifiUsernameInput = ""
passwordInput.text = "" passwordInput.text = ""
if (requiresEnterprise) usernameInput.text = ""
} }
} }

View File

@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -8,6 +9,10 @@ import qs.Modules.Plugins
PluginComponent { PluginComponent {
id: root id: root
Ref {
service: VpnService
}
ccWidgetIcon: VpnService.isBusy ? "sync" : (VpnService.connected ? "vpn_lock" : "vpn_key_off") ccWidgetIcon: VpnService.isBusy ? "sync" : (VpnService.connected ? "vpn_lock" : "vpn_key_off")
ccWidgetPrimaryText: "VPN" ccWidgetPrimaryText: "VPN"
ccWidgetSecondaryText: { ccWidgetSecondaryText: {

View File

@@ -48,7 +48,10 @@ Item {
const builtinId = root.expandedSection const builtinId = root.expandedSection
let builtinInstance = null let builtinInstance = null
if (builtinId === "builtin_vpn" && widgetModel?.vpnBuiltinInstance) { if (builtinId === "builtin_vpn") {
if (widgetModel?.vpnLoader) {
widgetModel.vpnLoader.active = true
}
builtinInstance = widgetModel.vpnBuiltinInstance builtinInstance = widgetModel.vpnBuiltinInstance
} }

View File

@@ -126,7 +126,15 @@ Column {
return builtinPluginWidgetComponent return builtinPluginWidgetComponent
} else if (id.startsWith("plugin_")) { } else if (id.startsWith("plugin_")) {
return pluginWidgetComponent return pluginWidgetComponent
} else if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") { } else if (id === "wifi") {
if (!DMSService.dmsAvailable) {
return errorPillComponent
}
if (DMSService.dmsAvailable && !DMSService.capabilities.includes("network")) {
return errorPillComponent
}
return compoundPillComponent
} else if (id === "bluetooth" || id === "audioOutput" || id === "audioInput") {
return compoundPillComponent return compoundPillComponent
} else if (id === "volumeSlider") { } else if (id === "volumeSlider") {
return audioSliderComponent return audioSliderComponent
@@ -175,6 +183,22 @@ Column {
} }
} }
Component {
id: errorPillComponent
ErrorPill {
property var widgetData: parent.widgetData || {}
width: parent.width
height: 60
primaryMessage: {
if (!DMSService.dmsAvailable) {
return qsTr("DMS_SOCKET not available")
}
return qsTr("NM not supported")
}
secondaryMessage: qsTr("update dms for NM integration.")
}
}
Component { Component {
id: compoundPillComponent id: compoundPillComponent
CompoundPill { CompoundPill {
@@ -187,13 +211,13 @@ Column {
switch (widgetData.id || "") { switch (widgetData.id || "") {
case "wifi": case "wifi":
{ {
if (NetworkService.wifiToggling) if (NetworkManagerService.wifiToggling)
return "sync" return "sync"
if (NetworkService.networkStatus === "ethernet") if (NetworkManagerService.networkStatus === "ethernet")
return "settings_ethernet" return "settings_ethernet"
if (NetworkService.networkStatus === "wifi") if (NetworkManagerService.networkStatus === "wifi")
return NetworkService.wifiSignalIcon return NetworkManagerService.wifiSignalIcon
if (NetworkService.wifiEnabled) if (NetworkManagerService.wifiEnabled)
return "wifi_off" return "wifi_off"
return "wifi_off" return "wifi_off"
} }
@@ -246,13 +270,13 @@ Column {
switch (widgetData.id || "") { switch (widgetData.id || "") {
case "wifi": case "wifi":
{ {
if (NetworkService.wifiToggling) if (NetworkManagerService.wifiToggling)
return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..." return NetworkManagerService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
if (NetworkService.networkStatus === "ethernet") if (NetworkManagerService.networkStatus === "ethernet")
return "Ethernet" return "Ethernet"
if (NetworkService.networkStatus === "wifi" && NetworkService.currentWifiSSID) if (NetworkManagerService.networkStatus === "wifi" && NetworkManagerService.currentWifiSSID)
return NetworkService.currentWifiSSID return NetworkManagerService.currentWifiSSID
if (NetworkService.wifiEnabled) if (NetworkManagerService.wifiEnabled)
return "Not connected" return "Not connected"
return "WiFi off" return "WiFi off"
} }
@@ -278,13 +302,13 @@ Column {
switch (widgetData.id || "") { switch (widgetData.id || "") {
case "wifi": case "wifi":
{ {
if (NetworkService.wifiToggling) if (NetworkManagerService.wifiToggling)
return "Please wait..." return "Please wait..."
if (NetworkService.networkStatus === "ethernet") if (NetworkManagerService.networkStatus === "ethernet")
return "Connected" return "Connected"
if (NetworkService.networkStatus === "wifi") if (NetworkManagerService.networkStatus === "wifi")
return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected" return NetworkManagerService.wifiSignalStrength > 0 ? NetworkManagerService.wifiSignalStrength + "%" : "Connected"
if (NetworkService.wifiEnabled) if (NetworkManagerService.wifiEnabled)
return "Select network" return "Select network"
return "" return ""
} }
@@ -332,13 +356,13 @@ Column {
switch (widgetData.id || "") { switch (widgetData.id || "") {
case "wifi": case "wifi":
{ {
if (NetworkService.wifiToggling) if (NetworkManagerService.wifiToggling)
return false return false
if (NetworkService.networkStatus === "ethernet") if (NetworkManagerService.networkStatus === "ethernet")
return true return true
if (NetworkService.networkStatus === "wifi") if (NetworkManagerService.networkStatus === "wifi")
return true return true
return NetworkService.wifiEnabled return NetworkManagerService.wifiEnabled
} }
case "bluetooth": case "bluetooth":
return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled) return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled)
@@ -723,12 +747,16 @@ Column {
width: parent.width width: parent.width
height: 60 height: 60
property var builtinInstance: { property var builtinInstance: null
Component.onCompleted: {
const id = widgetData.id || "" const id = widgetData.id || ""
if (id === "builtin_vpn") { if (id === "builtin_vpn") {
return root.model?.vpnBuiltinInstance if (root.model?.vpnLoader) {
root.model.vpnLoader.active = true
}
builtinInstance = Qt.binding(() => root.model?.vpnBuiltinInstance)
} }
return null
} }
sourceComponent: { sourceComponent: {

View File

@@ -73,13 +73,13 @@ DankPopout {
onShouldBeVisibleChanged: { onShouldBeVisibleChanged: {
if (shouldBeVisible) { if (shouldBeVisible) {
Qt.callLater(() => { Qt.callLater(() => {
NetworkService.autoRefreshEnabled = NetworkService.wifiEnabled NetworkManagerService.autoRefreshEnabled = NetworkManagerService.wifiEnabled
if (UserInfoService) if (UserInfoService)
UserInfoService.getUptime() UserInfoService.getUptime()
}) })
} else { } else {
Qt.callLater(() => { Qt.callLater(() => {
NetworkService.autoRefreshEnabled = false NetworkManagerService.autoRefreshEnabled = false
if (BluetoothService.adapter && BluetoothService.adapter.discovering) if (BluetoothService.adapter && BluetoothService.adapter.discovering)
BluetoothService.adapter.discovering = false BluetoothService.adapter.discovering = false
editMode = false editMode = false

View File

@@ -8,10 +8,10 @@ import qs.Modals
Rectangle { Rectangle {
implicitHeight: { implicitHeight: {
if (NetworkService.wifiToggling) { if (NetworkManagerService.wifiToggling) {
return headerRow.height + wifiToggleContent.height + Theme.spacingM return headerRow.height + wifiToggleContent.height + Theme.spacingM
} }
if (NetworkService.wifiEnabled) { if (NetworkManagerService.wifiEnabled) {
return headerRow.height + wifiContent.height + Theme.spacingM return headerRow.height + wifiContent.height + Theme.spacingM
} }
return headerRow.height + wifiOffContent.height + Theme.spacingM return headerRow.height + wifiOffContent.height + Theme.spacingM
@@ -20,16 +20,13 @@ Rectangle {
color: Theme.surfaceContainerHigh color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0 border.width: 0
Component.onCompleted: { Component.onCompleted: {
NetworkService.addRef() NetworkManagerService.addRef()
if (NetworkService.wifiEnabled) {
NetworkService.scanWifi()
}
} }
Component.onDestruction: { Component.onDestruction: {
NetworkService.removeRef() NetworkManagerService.removeRef()
} }
Row { Row {
@@ -59,16 +56,31 @@ Rectangle {
DankButtonGroup { DankButtonGroup {
id: preferenceControls id: preferenceControls
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.ethernetConnected && NetworkService.wifiConnected visible: NetworkManagerService.ethernetConnected
property int currentPreferenceIndex: NetworkService.userPreference === "ethernet" ? 0 : 1 property int currentPreferenceIndex: {
const pref = NetworkManagerService.userPreference
const status = NetworkManagerService.networkStatus
let index = 1
if (pref === "ethernet") {
index = 0
} else if (pref === "wifi") {
index = 1
} else {
index = status === "ethernet" ? 0 : 1
}
return index
}
model: ["Ethernet", "WiFi"] model: ["Ethernet", "WiFi"]
currentIndex: currentPreferenceIndex currentIndex: currentPreferenceIndex
selectionMode: "single" selectionMode: "single"
onSelectionChanged: (index, selected) => { onSelectionChanged: (index, selected) => {
if (!selected) return if (!selected) return
NetworkService.setNetworkPreference(index === 0 ? "ethernet" : "wifi") console.log("NetworkDetail: Setting preference to", index === 0 ? "ethernet" : "wifi")
NetworkManagerService.setNetworkPreference(index === 0 ? "ethernet" : "wifi")
} }
} }
} }
@@ -80,31 +92,31 @@ Rectangle {
anchors.right: parent.right anchors.right: parent.right
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM anchors.topMargin: Theme.spacingM
visible: NetworkService.wifiToggling visible: NetworkManagerService.wifiToggling
height: visible ? 80 : 0 height: visible ? 80 : 0
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingM spacing: Theme.spacingM
DankIcon { DankIcon {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
name: "sync" name: "sync"
size: 32 size: 32
color: Theme.primary color: Theme.primary
RotationAnimation on rotation { RotationAnimation on rotation {
running: NetworkService.wifiToggling running: NetworkManagerService.wifiToggling
loops: Animation.Infinite loops: Animation.Infinite
from: 0 from: 0
to: 360 to: 360
duration: 1000 duration: 1000
} }
} }
StyledText { StyledText {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..." text: NetworkManagerService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -119,7 +131,7 @@ Rectangle {
anchors.right: parent.right anchors.right: parent.right
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM anchors.topMargin: Theme.spacingM
visible: !NetworkService.wifiEnabled && !NetworkService.wifiToggling visible: !NetworkManagerService.wifiEnabled && !NetworkManagerService.wifiToggling
height: visible ? 120 : 0 height: visible ? 120 : 0
Column { Column {
@@ -165,7 +177,7 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: NetworkService.toggleWifiRadio() onClicked: NetworkManagerService.toggleWifiRadio()
} }
} }
@@ -180,7 +192,7 @@ Rectangle {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM anchors.topMargin: Theme.spacingM
visible: NetworkService.wifiInterface && NetworkService.wifiEnabled && !NetworkService.wifiToggling visible: NetworkManagerService.wifiInterface && NetworkManagerService.wifiEnabled && !NetworkManagerService.wifiToggling
contentHeight: wifiColumn.height contentHeight: wifiColumn.height
clip: true clip: true
@@ -192,16 +204,16 @@ Rectangle {
Item { Item {
width: parent.width width: parent.width
height: 200 height: 200
visible: NetworkService.wifiInterface && NetworkService.wifiNetworks?.length < 1 && !NetworkService.wifiToggling visible: NetworkManagerService.wifiInterface && NetworkManagerService.wifiNetworks?.length < 1 && !NetworkManagerService.wifiToggling && NetworkManagerService.isScanning
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "refresh" name: "refresh"
size: 48 size: 48
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3) color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3)
RotationAnimation on rotation { RotationAnimation on rotation {
running: true running: NetworkManagerService.isScanning
loops: Animation.Infinite loops: Animation.Infinite
from: 0 from: 0
to: 360 to: 360
@@ -211,14 +223,18 @@ Rectangle {
} }
Repeater { Repeater {
model: { model: sortedNetworks
let networks = [...NetworkService.wifiNetworks]
networks.sort((a, b) => { property var sortedNetworks: {
if (a.ssid === NetworkService.currentWifiSSID) return -1 const ssid = NetworkManagerService.currentWifiSSID
if (b.ssid === NetworkService.currentWifiSSID) return 1 const networks = NetworkManagerService.wifiNetworks
let sorted = [...networks]
sorted.sort((a, b) => {
if (a.ssid === ssid) return -1
if (b.ssid === ssid) return 1
return b.signal - a.signal return b.signal - a.signal
}) })
return networks return sorted
} }
delegate: Rectangle { delegate: Rectangle {
required property var modelData required property var modelData
@@ -228,7 +244,7 @@ Rectangle {
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHighest color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.surfaceContainerHighest
border.color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: modelData.ssid === NetworkManagerService.currentWifiSSID ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0 border.width: 0
Row { Row {
@@ -245,7 +261,7 @@ Rectangle {
return "wifi_1_bar" return "wifi_1_bar"
} }
size: Theme.iconSize - 4 size: Theme.iconSize - 4
color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Theme.surfaceText color: modelData.ssid === NetworkManagerService.currentWifiSSID ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@@ -257,16 +273,16 @@ Rectangle {
text: modelData.ssid || "Unknown Network" text: modelData.ssid || "Unknown Network"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
font.weight: modelData.ssid === NetworkService.currentWifiSSID ? Font.Medium : Font.Normal font.weight: modelData.ssid === NetworkManagerService.currentWifiSSID ? Font.Medium : Font.Normal
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
} }
Row { Row {
spacing: Theme.spacingXS spacing: Theme.spacingXS
StyledText { StyledText {
text: modelData.ssid === NetworkService.currentWifiSSID ? "Connected" : (modelData.secured ? "Secured" : "Open") text: modelData.ssid === NetworkManagerService.currentWifiSSID ? "Connected" : (modelData.secured ? "Secured" : "Open")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
@@ -300,7 +316,7 @@ Rectangle {
} else { } else {
networkContextMenu.currentSSID = modelData.ssid networkContextMenu.currentSSID = modelData.ssid
networkContextMenu.currentSecured = modelData.secured networkContextMenu.currentSecured = modelData.secured
networkContextMenu.currentConnected = modelData.ssid === NetworkService.currentWifiSSID networkContextMenu.currentConnected = modelData.ssid === NetworkManagerService.currentWifiSSID
networkContextMenu.currentSaved = modelData.saved networkContextMenu.currentSaved = modelData.saved
networkContextMenu.currentSignal = modelData.signal networkContextMenu.currentSignal = modelData.signal
networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS) networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS)
@@ -315,11 +331,11 @@ Rectangle {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: function(event) { onClicked: function(event) {
if (modelData.ssid !== NetworkService.currentWifiSSID) { if (modelData.ssid !== NetworkManagerService.currentWifiSSID) {
if (modelData.secured && !modelData.saved) { if (modelData.secured && !modelData.saved) {
wifiPasswordModal.show(modelData.ssid) wifiPasswordModal.show(modelData.ssid)
} else { } else {
NetworkService.connectToWifi(modelData.ssid) NetworkManagerService.connectToWifi(modelData.ssid)
} }
} }
event.accepted = true event.accepted = true
@@ -368,12 +384,12 @@ Rectangle {
onTriggered: { onTriggered: {
if (networkContextMenu.currentConnected) { if (networkContextMenu.currentConnected) {
NetworkService.disconnectWifi() NetworkManagerService.disconnectWifi()
} else { } else {
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) { if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
wifiPasswordModal.show(networkContextMenu.currentSSID) wifiPasswordModal.show(networkContextMenu.currentSSID)
} else { } else {
NetworkService.connectToWifi(networkContextMenu.currentSSID) NetworkManagerService.connectToWifi(networkContextMenu.currentSSID)
} }
} }
} }
@@ -397,7 +413,7 @@ Rectangle {
} }
onTriggered: { onTriggered: {
let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID) let networkData = NetworkManagerService.getNetworkInfo(networkContextMenu.currentSSID)
networkInfoModal.showNetworkInfo(networkContextMenu.currentSSID, networkData) networkInfoModal.showNetworkInfo(networkContextMenu.currentSSID, networkData)
} }
} }
@@ -421,7 +437,7 @@ Rectangle {
} }
onTriggered: { onTriggered: {
NetworkService.forgetWifiNetwork(networkContextMenu.currentSSID) NetworkManagerService.forgetWifiNetwork(networkContextMenu.currentSSID)
} }
} }
} }

View File

@@ -7,7 +7,30 @@ import "../utils/widgets.js" as WidgetUtils
QtObject { QtObject {
id: root id: root
property var vpnBuiltinInstance: VpnWidget {} property var vpnBuiltinInstance: null
property var vpnLoader: Loader {
active: false
sourceComponent: Component {
VpnWidget {}
}
onItemChanged: {
root.vpnBuiltinInstance = item
}
Connections {
target: SettingsData
function onControlCenterWidgetsChanged() {
const widgets = SettingsData.controlCenterWidgets || []
const hasVpnWidget = widgets.some(w => w.id === "builtin_vpn")
if (!hasVpnWidget && vpnLoader.active) {
console.log("VpnWidget: No VPN widget in control center, deactivating loader")
vpnLoader.active = false
}
}
}
}
readonly property var coreWidgetDefinitions: [{ readonly property var coreWidgetDefinitions: [{
"id": "nightMode", "id": "nightMode",
@@ -44,8 +67,8 @@ QtObject {
"description": "Wi-Fi and Ethernet connection", "description": "Wi-Fi and Ethernet connection",
"icon": "wifi", "icon": "wifi",
"type": "connection", "type": "connection",
"enabled": NetworkService.wifiAvailable, "enabled": NetworkManagerService.wifiAvailable,
"warning": !NetworkService.wifiAvailable ? "Wi-Fi not available" : undefined "warning": !NetworkManagerService.wifiAvailable ? "Wi-Fi not available" : undefined
}, { }, {
"id": "bluetooth", "id": "bluetooth",
"text": "Bluetooth", "text": "Bluetooth",

View File

@@ -0,0 +1,51 @@
import QtQuick
import qs.Common
import qs.Widgets
StyledRect {
id: root
property string primaryMessage: ""
property string secondaryMessage: ""
radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.1)
border.color: Theme.warning
border.width: 1
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
DankIcon {
name: "warning"
size: 16
color: Theme.warning
anchors.top: parent.top
anchors.topMargin: 2
}
Column {
width: parent.width - 16 - parent.spacing
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: root.primaryMessage
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
font.weight: Font.Medium
wrapMode: Text.WordWrap
}
StyledText {
width: parent.width
text: root.secondaryMessage
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
visible: text.length > 0
}
}
}
}

View File

@@ -1,78 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Widgets
CompoundPill {
id: root
isActive: {
if (NetworkService.wifiToggling) {
return false
}
if (NetworkService.networkStatus === "ethernet") {
return true
}
if (NetworkService.networkStatus === "wifi") {
return true
}
return NetworkService.wifiEnabled
}
iconName: {
if (NetworkService.wifiToggling) {
return "sync"
}
if (NetworkService.networkStatus === "ethernet") {
return "settings_ethernet"
}
if (NetworkService.networkStatus === "wifi") {
return NetworkService.wifiSignalIcon
}
if (NetworkService.wifiEnabled) {
return "wifi_off"
}
return "wifi_off"
}
primaryText: {
if (NetworkService.wifiToggling) {
return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
}
if (NetworkService.networkStatus === "ethernet") {
return "Ethernet"
}
if (NetworkService.networkStatus === "wifi" && NetworkService.currentWifiSSID) {
return NetworkService.currentWifiSSID
}
if (NetworkService.wifiEnabled) {
return "Not connected"
}
return "WiFi off"
}
secondaryText: {
if (NetworkService.wifiToggling) {
return "Please wait..."
}
if (NetworkService.networkStatus === "ethernet") {
return "Connected"
}
if (NetworkService.networkStatus === "wifi") {
return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected"
}
if (NetworkService.wifiEnabled) {
return "Select network"
}
return ""
}
onToggled: {
if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) {
NetworkService.toggleWifiRadio()
}
}
}

View File

@@ -1003,8 +1003,8 @@ Item {
} }
controlCenterLoader.item.triggerScreen = barWindow.screen controlCenterLoader.item.triggerScreen = barWindow.screen
controlCenterLoader.item.toggle() controlCenterLoader.item.toggle()
if (controlCenterLoader.item.shouldBeVisible && NetworkService.wifiEnabled) { if (controlCenterLoader.item.shouldBeVisible && NetworkManagerService.wifiEnabled) {
NetworkService.scanWifi() NetworkManagerService.scanWifi()
} }
} }
} }

View File

@@ -13,6 +13,10 @@ import qs.Widgets
DankPopout { DankPopout {
id: root id: root
Ref {
service: VpnService
}
property var triggerScreen: null property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen) { function setTriggerPosition(x, y, width, section, screen) {

View File

@@ -42,26 +42,26 @@ Rectangle {
DankIcon { DankIcon {
name: { name: {
if (NetworkService.wifiToggling) { if (NetworkManagerService.wifiToggling) {
return "sync" return "sync"
} }
if (NetworkService.networkStatus === "ethernet") { if (NetworkManagerService.networkStatus === "ethernet") {
return "lan" return "lan"
} }
return NetworkService.wifiSignalIcon return NetworkManagerService.wifiSignalIcon
} }
size: Theme.barIconSize(barThickness) size: Theme.barIconSize(barThickness)
color: { color: {
if (NetworkService.wifiToggling) { if (NetworkManagerService.wifiToggling) {
return Theme.primary return Theme.primary
} }
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton return NetworkManagerService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
} }
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
visible: root.showNetworkIcon visible: root.showNetworkIcon && NetworkManagerService.networkAvailable
} }
DankIcon { DankIcon {
@@ -141,26 +141,26 @@ Rectangle {
id: networkIcon id: networkIcon
name: { name: {
if (NetworkService.wifiToggling) { if (NetworkManagerService.wifiToggling) {
return "sync"; return "sync";
} }
if (NetworkService.networkStatus === "ethernet") { if (NetworkManagerService.networkStatus === "ethernet") {
return "lan"; return "lan";
} }
return NetworkService.wifiSignalIcon; return NetworkManagerService.wifiSignalIcon;
} }
size: Theme.barIconSize(barThickness) size: Theme.barIconSize(barThickness)
color: { color: {
if (NetworkService.wifiToggling) { if (NetworkManagerService.wifiToggling) {
return Theme.primary; return Theme.primary;
} }
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton; return NetworkManagerService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton;
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.showNetworkIcon visible: root.showNetworkIcon && NetworkManagerService.networkAvailable
} }

View File

@@ -7,6 +7,10 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
Ref {
service: VpnService
}
property bool isVertical: axis?.isVertical ?? false property bool isVertical: axis?.isVertical ?? false
property var axis: null property var axis: null
property int widgetThickness: 28 property int widgetThickness: 28

View File

@@ -7,120 +7,57 @@ import qs.Services
Item { Item {
id: root id: root
property string sid: Quickshell.env("XDG_SESSION_ID") || "self"
property string sessionPath: ""
function activate() { function activate() {
loader.activeAsync = true loader.activeAsync = true
} }
Component.onCompleted: { Component.onCompleted: {
getSessionPath.running = true if (SessionService.loginctlAvailable) {
} if (SessionService.locked || SessionService.lockedHint) {
console.log("Lock: Session locked on startup")
Component.onDestruction: { loader.activeAsync = true
lockStateMonitor.running = false }
}
} }
Connections { Connections {
target: IdleService target: IdleService
function onLockRequested() { function onLockRequested() {
console.log("Lock: Received lock request from IdleService") console.log("Lock: Received lock request from IdleService")
activate() SessionService.lockSession()
} }
} }
Process { Connections {
id: getSessionPath target: SessionService
command: ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", "/org/freedesktop/login1", "--method", "org.freedesktop.login1.Manager.GetSession", sid]
running: false
stdout: StdioCollector { function onSessionLocked() {
onStreamFinished: { console.log("Lock: Lock signal received -> show lock")
const match = text.match(/objectpath '([^']+)'/) loader.activeAsync = true
if (match) { }
root.sessionPath = match[1]
console.log("Found session path:", root.sessionPath) function onSessionUnlocked() {
checkCurrentLockState.running = true console.log("Lock: Unlock signal received -> hide lock")
lockStateMonitor.running = true loader.active = false
} else { }
console.warn("Could not determine session path")
} function onLoginctlStateChanged() {
if (SessionService.lockedHint && !loader.active) {
console.log("Lock: LockedHint=true -> show lock")
loader.activeAsync = true
} else if (!SessionService.locked && !SessionService.lockedHint && loader.active) {
console.log("Lock: LockedHint=false -> hide lock")
loader.active = false
} }
} }
onExited: (exitCode, exitStatus) => { function onPrepareForSleep() {
if (exitCode !== 0) { if (SessionService.preparingForSleep && SessionData.lockBeforeSuspend) {
console.warn("Failed to get session path, exit code:", exitCode) console.log("Lock: PrepareForSleep -> lock before suspend")
} loader.activeAsync = true
}
}
Process {
id: checkCurrentLockState
command: root.sessionPath ? ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", root.sessionPath, "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.login1.Session", "LockedHint"] : []
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.includes("true")) {
console.log("Session is locked on startup, activating lock screen")
loader.activeAsync = true
}
} }
} }
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
console.warn("Failed to check initial lock state, exit code:", exitCode)
}
}
}
Process {
id: lockStateMonitor
command: root.sessionPath ? ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.login1"] : []
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
if (line.includes(root.sessionPath)) {
if (line.includes("org.freedesktop.login1.Session.Lock")) {
console.log("login1: Lock signal received -> show lock")
loader.activeAsync = true
return
}
if (line.includes("org.freedesktop.login1.Session.Unlock")) {
console.log("login1: Unlock signal received -> hide lock")
loader.active = false
return
}
if (line.includes("LockedHint") && line.includes("true")) {
console.log("login1: LockedHint=true -> show lock")
loader.activeAsync = true
return
}
if (line.includes("LockedHint") && line.includes("false")) {
console.log("login1: LockedHint=false -> hide lock")
loader.active = false
return
}
}
if (line.includes("PrepareForSleep") &&
line.includes("true") &&
SessionData.lockBeforeSuspend) {
loader.activeAsync = true
}
}
}
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
console.warn("gdbus monitor failed, exit code:", exitCode)
}
}
} }
LazyLoader { LazyLoader {
@@ -160,7 +97,7 @@ Item {
function lock() { function lock() {
console.log("Lock screen requested via IPC") console.log("Lock screen requested via IPC")
loader.activeAsync = true SessionService.lockSession()
} }
function demo() { function demo() {
@@ -169,7 +106,7 @@ Item {
} }
function isLocked(): bool { function isLocked(): bool {
return loader.active return SessionService.locked || loader.active
} }
} }
} }

View File

@@ -906,20 +906,20 @@ Item {
height: 24 height: 24
color: Qt.rgba(255, 255, 255, 0.2) color: Qt.rgba(255, 255, 255, 0.2)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: WeatherService.weather.available && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio) || BatteryService.batteryAvailable) visible: WeatherService.weather.available && (NetworkManagerService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio) || BatteryService.batteryAvailable)
} }
Row { Row {
spacing: Theme.spacingM spacing: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.networkStatus !== "disconnected" || (BluetoothService.available && BluetoothService.enabled) || (AudioService.sink && AudioService.sink.audio) visible: NetworkManagerService.networkStatus !== "disconnected" || (BluetoothService.available && BluetoothService.enabled) || (AudioService.sink && AudioService.sink.audio)
DankIcon { DankIcon {
name: NetworkService.networkStatus === "ethernet" ? "lan" : NetworkService.wifiSignalIcon name: NetworkManagerService.networkStatus === "ethernet" ? "lan" : NetworkManagerService.wifiSignalIcon
size: Theme.iconSize - 2 size: Theme.iconSize - 2
color: NetworkService.networkStatus !== "disconnected" ? "white" : Qt.rgba(255, 255, 255, 0.5) color: NetworkManagerService.networkStatus !== "disconnected" ? "white" : Qt.rgba(255, 255, 255, 0.5)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.networkStatus !== "disconnected" visible: NetworkManagerService.networkStatus !== "disconnected"
} }
DankIcon { DankIcon {
@@ -955,7 +955,7 @@ Item {
height: 24 height: 24
color: Qt.rgba(255, 255, 255, 0.2) color: Qt.rgba(255, 255, 255, 0.2)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable && (NetworkService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio)) visible: BatteryService.batteryAvailable && (NetworkManagerService.networkStatus !== "disconnected" || BluetoothService.enabled || (AudioService.sink && AudioService.sink.audio))
} }
Row { Row {

View File

@@ -69,7 +69,7 @@ DankFlickable {
} }
StyledText { StyledText {
text: `Up ${UserInfoService.uptime} Boot: ${DgopService.bootTime}` text: `${UserInfoService.uptime} Boot: ${DgopService.bootTime}`
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)

View File

@@ -6,11 +6,13 @@ import QtCore
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common
Singleton { Singleton {
id: root id: root
property bool dmsAvailable: false property bool dmsAvailable: false
property var capabilities: []
property var availablePlugins: [] property var availablePlugins: []
property var installedPlugins: [] property var installedPlugins: []
property bool isConnected: false property bool isConnected: false
@@ -18,14 +20,15 @@ Singleton {
readonly property string socketPath: Quickshell.env("DMS_SOCKET") readonly property string socketPath: Quickshell.env("DMS_SOCKET")
property int nextRequestId: 1
property var pendingRequests: ({}) property var pendingRequests: ({})
property int requestIdCounter: 0
signal pluginsListReceived(var plugins) signal pluginsListReceived(var plugins)
signal installedPluginsReceived(var plugins) signal installedPluginsReceived(var plugins)
signal searchResultsReceived(var plugins) signal searchResultsReceived(var plugins)
signal operationSuccess(string message) signal operationSuccess(string message)
signal operationError(string error) signal operationError(string error)
signal connectionStateChanged()
Component.onCompleted: { Component.onCompleted: {
if (socketPath && socketPath.length > 0) { if (socketPath && socketPath.length > 0) {
@@ -60,7 +63,7 @@ Singleton {
socket.connected = true socket.connected = true
} }
Socket { DankSocket {
id: socket id: socket
path: root.socketPath path: root.socketPath
connected: false connected: false
@@ -69,9 +72,11 @@ Singleton {
if (connected) { if (connected) {
root.isConnected = true root.isConnected = true
root.isConnecting = false root.isConnecting = false
root.connectionStateChanged()
} else { } else {
root.isConnected = false root.isConnected = false
root.isConnecting = false root.isConnecting = false
root.connectionStateChanged()
} }
} }
@@ -83,6 +88,12 @@ Singleton {
try { try {
const response = JSON.parse(line) const response = JSON.parse(line)
if (response.capabilities) {
root.capabilities = response.capabilities
return
}
handleResponse(response) handleResponse(response)
} catch (e) { } catch (e) {
console.warn("DMSService: Failed to parse response:", line, e) console.warn("DMSService: Failed to parse response:", line, e)
@@ -101,7 +112,8 @@ Singleton {
return return
} }
const id = nextRequestId++ requestIdCounter++
const id = Date.now() + requestIdCounter
const request = { const request = {
"id": id, "id": id,
"method": method "method": method
@@ -115,11 +127,21 @@ Singleton {
pendingRequests[id] = callback pendingRequests[id] = callback
} }
const json = JSON.stringify(request) + "\n" socket.send(request)
socket.write(json)
} }
property var networkUpdateCallback: null
function handleResponse(response) { function handleResponse(response) {
if (response.id === undefined && response.result) {
if (response.result.type === "state_changed" && response.result.data) {
if (networkUpdateCallback) {
networkUpdateCallback(response.result.data)
}
}
return
}
const callback = pendingRequests[response.id] const callback = pendingRequests[response.id]
if (callback) { if (callback) {

View File

@@ -0,0 +1,564 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
property bool networkAvailable: false
property string networkStatus: "disconnected"
property string primaryConnection: ""
property string ethernetIP: ""
property string ethernetInterface: ""
property bool ethernetConnected: false
property string ethernetConnectionUuid: ""
property string wifiIP: ""
property string wifiInterface: ""
property bool wifiConnected: false
property bool wifiEnabled: true
property string wifiConnectionUuid: ""
property string wifiDevicePath: ""
property string activeAccessPointPath: ""
property string currentWifiSSID: ""
property int wifiSignalStrength: 0
property var wifiNetworks: []
property var savedConnections: []
property var ssidToConnectionName: ({})
property var wifiSignalIcon: {
if (!wifiConnected || networkStatus !== "wifi") {
return "wifi_off"
}
if (wifiSignalStrength >= 50) {
return "wifi"
}
if (wifiSignalStrength >= 25) {
return "wifi_2_bar"
}
return "wifi_1_bar"
}
property string userPreference: "auto"
property bool isConnecting: false
property string connectingSSID: ""
property string connectionError: ""
property bool isScanning: false
property bool autoScan: false
property bool wifiAvailable: true
property bool wifiToggling: false
property bool changingPreference: false
property string targetPreference: ""
property var savedWifiNetworks: []
property string connectionStatus: ""
property string lastConnectionError: ""
property bool passwordDialogShouldReopen: false
property bool autoRefreshEnabled: false
property string wifiPassword: ""
property string forgetSSID: ""
property string networkInfoSSID: ""
property string networkInfoDetails: ""
property bool networkInfoLoading: false
property int refCount: 0
property bool stateInitialized: false
signal networksUpdated
signal connectionChanged
property var dmsService: null
property bool subscriptionConnected: false
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
Component.onCompleted: {
root.userPreference = SettingsData.networkPreference
Qt.callLater(initializeDMSConnection)
}
DankSocket {
id: subscriptionSocket
path: root.socketPath
connected: networkAvailable
onConnectionStateChanged: {
root.subscriptionConnected = connected
if (connected) {
console.log("NetworkManagerService: Subscription socket connected")
}
}
parser: SplitParser {
onRead: line => {
if (!line || line.length === 0) {
return
}
try {
const response = JSON.parse(line)
if (response.capabilities) {
console.log("NetworkManagerService: Subscription socket received capabilities")
Qt.callLater(() => sendSubscribeRequest())
return
}
if (response.result && response.result.type === "state_changed" && response.result.data) {
const networksCount = response.result.data.wifiNetworks?.length ?? "null"
console.log("NetworkManagerService: Subscription update received, networks:", networksCount)
updateState(response.result.data)
}
} catch (e) {
console.warn("NetworkManagerService: Failed to parse subscription response:", line, e)
}
}
}
}
function sendSubscribeRequest() {
subscriptionSocket.send({
"id": 1,
"method": "network.subscribe"
})
console.log("NetworkManagerService: Sent network.subscribe request")
}
function initializeDMSConnection() {
try {
console.log("NetworkManagerService: Initializing DMS connection...")
dmsService = Qt.createQmlObject('import QtQuick; import qs.Services; QtObject { property var service: DMSService }', root)
if (dmsService && dmsService.service) {
console.log("NetworkManagerService: DMS service reference created")
checkCapabilities()
dmsService.service.connectionStateChanged.connect(onDMSConnectionStateChanged)
dmsService.service.capabilitiesChanged.connect(onDMSCapabilitiesChanged)
console.log("NetworkManagerService: Callbacks registered, isConnected:", dmsService.service.isConnected, "capabilities:", JSON.stringify(dmsService.service.capabilities))
} else {
console.warn("NetworkManagerService: Failed to get DMS service reference")
}
} catch (e) {
console.warn("NetworkManagerService: Failed to initialize DMS connection:", e)
}
}
function checkCapabilities() {
if (dmsService && dmsService.service && dmsService.service.isConnected) {
onDMSConnected()
}
}
function onDMSConnectionStateChanged() {
if (dmsService && dmsService.service && dmsService.service.isConnected) {
onDMSConnected()
}
}
function onDMSCapabilitiesChanged() {
console.log("NetworkManagerService: onDMSCapabilitiesChanged called, capabilities:", dmsService ? JSON.stringify(dmsService.service.capabilities) : "no service")
if (dmsService && dmsService.service && dmsService.service.capabilities.includes("network")) {
console.log("NetworkManagerService: Network capability detected!")
networkAvailable = true
if (dmsService.service.isConnected && !stateInitialized) {
console.log("NetworkManagerService: DMS is connected, fetching state and starting subscription socket...")
stateInitialized = true
getState()
subscriptionSocket.connected = true
}
}
}
function onDMSConnected() {
console.log("NetworkManagerService: onDMSConnected called")
if (dmsService && dmsService.service && dmsService.service.capabilities && dmsService.service.capabilities.length > 0) {
console.log("NetworkManagerService: Capabilities:", JSON.stringify(dmsService.service.capabilities))
networkAvailable = dmsService.service.capabilities.includes("network")
console.log("NetworkManagerService: Network available:", networkAvailable)
if (networkAvailable && !stateInitialized) {
console.log("NetworkManagerService: Requesting network state and starting subscription socket...")
stateInitialized = true
getState()
subscriptionSocket.connected = true
}
} else {
console.log("NetworkManagerService: No capabilities yet or service not ready")
}
}
function addRef() {
refCount++
if (refCount === 1 && networkAvailable) {
startAutoScan()
}
}
function removeRef() {
refCount = Math.max(0, refCount - 1)
if (refCount === 0) {
stopAutoScan()
}
}
property bool initialStateFetched: false
function getState() {
if (!networkAvailable || !dmsService || !dmsService.service) return
dmsService.service.sendRequest("network.getState", null, response => {
if (response.result) {
updateState(response.result)
if (!initialStateFetched && response.result.wifiEnabled && (!response.result.wifiNetworks || response.result.wifiNetworks.length === 0)) {
console.log("NetworkManagerService: Initial state has no networks, triggering scan")
initialStateFetched = true
Qt.callLater(() => scanWifi())
}
}
})
}
function updateState(state) {
networkStatus = state.networkStatus || "disconnected"
primaryConnection = state.primaryConnection || ""
ethernetIP = state.ethernetIP || ""
ethernetInterface = state.ethernetDevice || ""
ethernetConnected = state.ethernetConnected || false
ethernetConnectionUuid = state.ethernetConnectionUuid || ""
wifiIP = state.wifiIP || ""
wifiInterface = state.wifiDevice || ""
wifiConnected = state.wifiConnected || false
wifiEnabled = state.wifiEnabled !== undefined ? state.wifiEnabled : true
wifiConnectionUuid = state.wifiConnectionUuid || ""
wifiDevicePath = state.wifiDevicePath || ""
activeAccessPointPath = state.activeAccessPointPath || ""
currentWifiSSID = state.wifiSSID || ""
wifiSignalStrength = state.wifiSignal || 0
if (state.wifiNetworks) {
wifiNetworks = state.wifiNetworks
const saved = []
const mapping = {}
for (const network of state.wifiNetworks) {
if (network.saved) {
saved.push({
ssid: network.ssid,
saved: true
})
mapping[network.ssid] = network.ssid
}
}
savedConnections = saved
savedWifiNetworks = saved
ssidToConnectionName = mapping
networksUpdated()
}
userPreference = state.preference || "auto"
isConnecting = state.isConnecting || false
connectingSSID = state.connectingSSID || ""
connectionError = state.lastError || ""
lastConnectionError = state.lastError || ""
connectionChanged()
}
function scanWifi() {
if (!networkAvailable || isScanning || !wifiEnabled || !dmsService || !dmsService.service) return
console.log("NetworkManagerService: Starting WiFi scan...")
isScanning = true
dmsService.service.sendRequest("network.wifi.scan", null, response => {
isScanning = false
if (response.error) {
console.warn("NetworkManagerService: WiFi scan failed:", response.error)
} else {
console.log("NetworkManagerService: Scan completed, requesting fresh state...")
Qt.callLater(() => getState())
}
})
}
function scanWifiNetworks() {
scanWifi()
}
function connectToWifi(ssid, password = "", username = "") {
if (!networkAvailable || isConnecting || !dmsService || !dmsService.service) return
isConnecting = true
connectingSSID = ssid
connectionError = ""
connectionStatus = "connecting"
const params = { ssid: ssid }
if (password) params.password = password
if (username) params.username = username
dmsService.service.sendRequest("network.wifi.connect", params, response => {
if (response.error) {
connectionError = response.error
lastConnectionError = response.error
connectionStatus = response.error.includes("password") || response.error.includes("authentication")
? "invalid_password"
: "failed"
if (connectionStatus === "invalid_password") {
passwordDialogShouldReopen = true
ToastService.showError(`Invalid password for ${ssid}`)
} else {
ToastService.showError(`Failed to connect to ${ssid}`)
}
} else {
connectionError = ""
connectionStatus = "connected"
ToastService.showInfo(`Connected to ${ssid}`)
if (userPreference === "wifi" || userPreference === "auto") {
setConnectionPriority("wifi")
}
}
isConnecting = false
connectingSSID = ""
})
}
function disconnectWifi() {
if (!networkAvailable || !wifiInterface || !dmsService || !dmsService.service) return
dmsService.service.sendRequest("network.wifi.disconnect", null, response => {
if (response.error) {
ToastService.showError("Failed to disconnect WiFi")
} else {
ToastService.showInfo("Disconnected from WiFi")
currentWifiSSID = ""
connectionStatus = ""
}
})
}
function forgetWifiNetwork(ssid) {
if (!networkAvailable || !dmsService || !dmsService.service) return
forgetSSID = ssid
dmsService.service.sendRequest("network.wifi.forget", { ssid: ssid }, response => {
if (response.error) {
console.warn("Failed to forget network:", response.error)
} else {
ToastService.showInfo(`Forgot network ${ssid}`)
savedConnections = savedConnections.filter(s => s.ssid !== ssid)
savedWifiNetworks = savedWifiNetworks.filter(s => s.ssid !== ssid)
const updated = [...wifiNetworks]
for (const network of updated) {
if (network.ssid === ssid) {
network.saved = false
if (network.connected) {
network.connected = false
currentWifiSSID = ""
}
}
}
wifiNetworks = updated
networksUpdated()
}
forgetSSID = ""
})
}
function toggleWifiRadio() {
if (!networkAvailable || wifiToggling || !dmsService || !dmsService.service) return
wifiToggling = true
dmsService.service.sendRequest("network.wifi.toggle", null, response => {
wifiToggling = false
if (response.error) {
console.warn("Failed to toggle WiFi:", response.error)
} else if (response.result) {
wifiEnabled = response.result.enabled
ToastService.showInfo(wifiEnabled ? "WiFi enabled" : "WiFi disabled")
}
})
}
function enableWifiDevice() {
if (!networkAvailable || !dmsService || !dmsService.service) return
dmsService.service.sendRequest("network.wifi.enable", null, response => {
if (response.error) {
ToastService.showError("Failed to enable WiFi")
} else {
ToastService.showInfo("WiFi enabled")
}
})
}
function setNetworkPreference(preference) {
if (!networkAvailable || !dmsService || !dmsService.service) return
userPreference = preference
changingPreference = true
targetPreference = preference
SettingsData.setNetworkPreference(preference)
dmsService.service.sendRequest("network.preference.set", { preference: preference }, response => {
changingPreference = false
targetPreference = ""
if (response.error) {
console.warn("Failed to set network preference:", response.error)
}
})
}
function setConnectionPriority(type) {
if (type === "wifi") {
setNetworkPreference("wifi")
} else if (type === "ethernet") {
setNetworkPreference("ethernet")
}
}
function connectToWifiAndSetPreference(ssid, password, username = "") {
connectToWifi(ssid, password, username)
setNetworkPreference("wifi")
}
function toggleNetworkConnection(type) {
if (!networkAvailable || !dmsService || !dmsService.service) return
if (type === "ethernet") {
if (networkStatus === "ethernet") {
dmsService.service.sendRequest("network.ethernet.disconnect", null, null)
} else {
dmsService.service.sendRequest("network.ethernet.connect", null, null)
}
}
}
function startAutoScan() {
autoScan = true
autoRefreshEnabled = true
if (networkAvailable && wifiEnabled) {
scanWifi()
}
}
function stopAutoScan() {
autoScan = false
autoRefreshEnabled = false
}
function fetchNetworkInfo(ssid) {
if (!networkAvailable || !dmsService || !dmsService.service) return
networkInfoSSID = ssid
networkInfoLoading = true
networkInfoDetails = "Loading network information..."
dmsService.service.sendRequest("network.info", { ssid: ssid }, response => {
networkInfoLoading = false
if (response.error) {
networkInfoDetails = "Failed to fetch network information"
} else if (response.result) {
formatNetworkInfo(response.result)
}
})
}
function formatNetworkInfo(info) {
let details = ""
if (!info || !info.bands || info.bands.length === 0) {
details = "Network information not found or network not available."
} else {
for (const band of info.bands) {
const freqGHz = band.frequency / 1000
let bandName = "Unknown"
if (band.frequency >= 2400 && band.frequency <= 2500) {
bandName = "2.4 GHz"
} else if (band.frequency >= 5000 && band.frequency <= 6000) {
bandName = "5 GHz"
} else if (band.frequency >= 6000) {
bandName = "6 GHz"
}
const statusPrefix = band.connected ? "● " : " "
const statusSuffix = band.connected ? " (Connected)" : ""
details += statusPrefix + bandName + statusSuffix + " - " + band.signal + "%\\n"
details += " Channel " + band.channel + " (" + freqGHz.toFixed(1) + " GHz) • " + band.rate + " Mbit/s\\n"
details += " BSSID: " + band.bssid + "\\n"
details += " Mode: " + band.mode + "\\n"
details += " Security: " + (band.secured ? "Secured" : "Open") + "\\n"
if (band.saved) {
details += " Status: Saved network\\n"
}
details += "\\n"
}
}
networkInfoDetails = details
}
function getNetworkInfo(ssid) {
const network = wifiNetworks.find(n => n.ssid === ssid)
if (!network) {
return null
}
return {
"ssid": network.ssid,
"signal": network.signal,
"secured": network.secured,
"saved": network.saved,
"connected": network.connected,
"bssid": network.bssid
}
}
function refreshNetworkState() {
if (networkAvailable) {
getState()
}
}
function splitNmcliFields(line) {
const parts = []
let cur = ""
let escape = false
for (var i = 0; i < line.length; i++) {
const ch = line[i]
if (escape) {
cur += ch
escape = false
} else if (ch === '\\') {
escape = true
} else if (ch === ':') {
parts.push(cur)
cur = ""
} else {
cur += ch
}
}
parts.push(cur)
return parts
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -36,26 +36,10 @@ Singleton {
property bool matugenSuppression: false property bool matugenSuppression: false
property bool configGenerationPending: false property bool configGenerationPending: false
property bool _wantSockets: true
property int _reconnectAttempt: 0
readonly property int _reconnectBaseMs: 400
readonly property int _reconnectMaxMs: 15000
signal windowUrgentChanged() signal windowUrgentChanged()
Component.onCompleted: fetchOutputs() Component.onCompleted: fetchOutputs()
Timer {
id: reconnectTimer
interval: 0
repeat: false
onTriggered: {
root._wantSockets = false
Qt.callLater(() => root._wantSockets = true)
}
}
Timer { Timer {
id: suppressToastTimer id: suppressToastTimer
interval: 3000 interval: 3000
@@ -150,20 +134,15 @@ Singleton {
} }
} }
Socket { DankSocket {
id: eventStreamSocket id: eventStreamSocket
path: root.socketPath path: root.socketPath
connected: CompositorService.isNiri && root._wantSockets connected: CompositorService.isNiri
onConnectionStateChanged: { onConnectionStateChanged: {
if (connected) { if (connected) {
_reconnectAttempt = 0 send('"EventStream"')
write('"EventStream"\n')
fetchOutputs() fetchOutputs()
return
}
if (CompositorService.isNiri) {
_scheduleReconnect()
} }
} }
@@ -179,20 +158,10 @@ Singleton {
} }
} }
Socket { DankSocket {
id: requestSocket id: requestSocket
path: root.socketPath path: root.socketPath
connected: CompositorService.isNiri && root._wantSockets connected: CompositorService.isNiri
onConnectionStateChanged: {
if (connected) {
_reconnectAttempt = 0
return
}
if (CompositorService.isNiri) {
_scheduleReconnect()
}
}
} }
function fetchOutputs() { function fetchOutputs() {
@@ -200,16 +169,6 @@ Singleton {
outputsProcess.running = true outputsProcess.running = true
} }
function _scheduleReconnect() {
const pow = Math.min(_reconnectAttempt, 10)
const base = Math.min(_reconnectBaseMs * Math.pow(2, pow), _reconnectMaxMs)
const jitter = Math.floor(Math.random() * Math.floor(base / 4))
reconnectTimer.interval = base + jitter
reconnectTimer.restart()
_reconnectAttempt++
console.warn("NiriService: scheduling reconnect in ~", reconnectTimer.interval, "ms (attempt", _reconnectAttempt, ")")
}
function sortWindowsByLayout(windowList) { function sortWindowsByLayout(windowList) {
return [...windowList].sort((a, b) => { return [...windowList].sort((a, b) => {
const aWorkspace = workspaces[a.workspace_id] const aWorkspace = workspaces[a.workspace_id]
@@ -500,7 +459,7 @@ Singleton {
function send(request) { function send(request) {
if (!CompositorService.isNiri || !requestSocket.connected) return false if (!CompositorService.isNiri || !requestSocket.connected) return false
requestSocket.write(JSON.stringify(request) + "\n") requestSocket.send(request)
return true return true
} }

View File

@@ -13,12 +13,30 @@ Singleton {
property string systemProfileImage: "" property string systemProfileImage: ""
property string profileImage: "" property string profileImage: ""
property bool settingsPortalAvailable: false property bool settingsPortalAvailable: false
property int systemColorScheme: 0 // 0=default, 1=prefer-dark, 2=prefer-light property int systemColorScheme: 0
property var dmsService: null
property bool freedeskAvailable: false
function init() {} function init() {}
function getSystemProfileImage() { function getSystemProfileImage() {
systemProfileCheckProcess.running = true if (!freedeskAvailable || !dmsService || !dmsService.service) return
const username = Quickshell.env("USER")
if (!username) return
dmsService.service.sendRequest("freedesktop.accounts.getUserIconFile", { username: username }, response => {
if (response.result && response.result.success) {
const iconFile = response.result.value || ""
if (iconFile && iconFile !== "" && iconFile !== "/var/lib/AccountsService/icons/") {
systemProfileImage = iconFile
if (!profileImage || profileImage === "") {
profileImage = iconFile
}
}
}
})
} }
function getUserProfileImage(username) { function getUserProfileImage(username) {
@@ -30,23 +48,20 @@ Singleton {
profileImage = "" profileImage = ""
return return
} }
userProfileCheckProcess.command = [ if (!freedeskAvailable || !dmsService || !dmsService.service) return
"bash", "-c",
`uid=$(id -u ${username} 2>/dev/null) && [ -n "$uid" ] && dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$uid org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile 2>/dev/null | grep -oP 'string "\\K[^"]+' || echo ""`
]
userProfileCheckProcess.running = true
}
function getGreeterUserProfileImage(username) { dmsService.service.sendRequest("freedesktop.accounts.getUserIconFile", { username: username }, response => {
if (!username) { if (response.result && response.result.success) {
profileImage = "" const icon = response.result.value || ""
return if (icon && icon !== "" && icon !== "/var/lib/AccountsService/icons/") {
} profileImage = icon
userProfileCheckProcess.command = [ } else {
"bash", "-c", profileImage = ""
`uid=$(id -u ${username} 2>/dev/null) && [ -n "$uid" ] && dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$uid org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile 2>/dev/null | grep -oP 'string "\\K[^"]+' || echo ""` }
] } else {
userProfileCheckProcess.running = true profileImage = ""
}
})
} }
function setProfileImage(imagePath) { function setProfileImage(imagePath) {
@@ -61,7 +76,23 @@ Singleton {
} }
function getSystemColorScheme() { function getSystemColorScheme() {
systemColorSchemeCheckProcess.running = true if (!freedeskAvailable || !dmsService || !dmsService.service) return
dmsService.service.sendRequest("freedesktop.settings.getColorScheme", null, response => {
if (response.result) {
systemColorScheme = response.result.value || 0
if (typeof Theme !== "undefined") {
const shouldBeLightMode = (systemColorScheme === 2)
if (Theme.isLightMode !== shouldBeLightMode) {
Theme.isLightMode = shouldBeLightMode
if (typeof SessionData !== "undefined") {
SessionData.setLightMode(shouldBeLightMode)
}
}
}
}
})
} }
function setLightMode(isLightMode) { function setLightMode(isLightMode) {
@@ -71,91 +102,118 @@ Singleton {
} }
function setSystemColorScheme(isLightMode) { function setSystemColorScheme(isLightMode) {
if (!settingsPortalAvailable) { if (!settingsPortalAvailable) return
return
}
const colorScheme = isLightMode ? "default" : "prefer-dark" const colorScheme = isLightMode ? "default" : "prefer-dark"
const script = `gsettings set org.gnome.desktop.interface color-scheme '${colorScheme}'` colorSchemeSetProcess.command = ["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", colorScheme]
colorSchemeSetProcess.running = true
systemColorSchemeSetProcess.command = ["bash", "-c", script]
systemColorSchemeSetProcess.running = true
}
function setSystemProfileImage(imagePath) {
if (!accountsServiceAvailable) {
return
}
const path = imagePath || ""
const script = `dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.Accounts.User.SetIconFile string:'${path}'`
systemProfileSetProcess.command = ["bash", "-c", script]
systemProfileSetProcess.running = true
}
Component.onCompleted: {
checkAccountsService()
checkSettingsPortal()
}
function checkAccountsService() {
accountsServiceCheckProcess.running = true
}
function checkSettingsPortal() {
settingsPortalCheckProcess.running = true
} }
Process { Process {
id: accountsServiceCheckProcess id: colorSchemeSetProcess
command: ["bash", "-c", "dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts org.freedesktop.Accounts.FindUserByName string:\"$USER\""]
running: false
onExited: exitCode => {
root.accountsServiceAvailable = (exitCode === 0)
if (root.accountsServiceAvailable) {
root.getSystemProfileImage()
}
}
}
Process {
id: systemProfileCheckProcess
command: ["bash", "-c", "dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile"]
running: false
stdout: StdioCollector {
onStreamFinished: {
const match = text.match(/string\s+"([^"]+)"/)
if (match && match[1] && match[1] !== "" && match[1] !== "/var/lib/AccountsService/icons/") {
root.systemProfileImage = match[1]
if (!root.profileImage || root.profileImage === "") {
root.profileImage = root.systemProfileImage
}
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
root.systemProfileImage = ""
}
}
}
Process {
id: systemProfileSetProcess
running: false running: false
onExited: exitCode => { onExited: exitCode => {
if (exitCode === 0) { if (exitCode === 0) {
root.getSystemProfileImage() Qt.callLater(() => getSystemColorScheme())
} }
} }
} }
function setSystemProfileImage(imagePath) {
if (!accountsServiceAvailable || !freedeskAvailable || !dmsService || !dmsService.service) return
dmsService.service.sendRequest("freedesktop.accounts.setIconFile", { path: imagePath || "" }, response => {
if (response.error) {
console.warn("PortalService: Failed to set icon file:", response.error)
} else {
Qt.callLater(() => getSystemProfileImage())
}
})
}
Component.onCompleted: {
Qt.callLater(initializeDMSConnection)
}
function initializeDMSConnection() {
try {
dmsService = Qt.createQmlObject('import QtQuick; import qs.Services; QtObject { property var service: DMSService }', root)
if (dmsService && dmsService.service) {
dmsService.service.connectionStateChanged.connect(onDMSConnectionStateChanged)
dmsService.service.capabilitiesChanged.connect(onDMSCapabilitiesChanged)
if (dmsService.service.isConnected) {
onDMSConnected()
}
}
} catch (e) {
console.warn("PortalService: Failed to initialize DMS connection:", e)
}
}
function onDMSConnectionStateChanged() {
if (dmsService && dmsService.service && dmsService.service.isConnected) {
onDMSConnected()
}
}
function onDMSCapabilitiesChanged() {
if (dmsService && dmsService.service && dmsService.service.capabilities.includes("freedesktop")) {
freedeskAvailable = true
checkAccountsService()
checkSettingsPortal()
}
}
function onDMSConnected() {
if (dmsService && dmsService.service && dmsService.service.capabilities && dmsService.service.capabilities.length > 0) {
freedeskAvailable = dmsService.service.capabilities.includes("freedesktop")
if (freedeskAvailable) {
checkAccountsService()
checkSettingsPortal()
}
}
}
function checkAccountsService() {
if (!freedeskAvailable || !dmsService || !dmsService.service) return
dmsService.service.sendRequest("freedesktop.getState", null, response => {
if (response.result && response.result.accounts) {
accountsServiceAvailable = response.result.accounts.available || false
if (accountsServiceAvailable) {
getSystemProfileImage()
}
}
})
}
function checkSettingsPortal() {
if (!freedeskAvailable || !dmsService || !dmsService.service) return
dmsService.service.sendRequest("freedesktop.getState", null, response => {
if (response.result && response.result.settings) {
settingsPortalAvailable = response.result.settings.available || false
if (settingsPortalAvailable) {
getSystemColorScheme()
}
}
})
}
// For greeter use alternate method to get user profile image - since we dont run with dms
function getGreeterUserProfileImage(username) {
if (!username) {
profileImage = ""
return
}
userProfileCheckProcess.command = [
"bash", "-c",
`uid=$(id -u ${username} 2>/dev/null) && [ -n "$uid" ] && dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$uid org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile 2>/dev/null | grep -oP 'string "\\K[^"]+' || echo ""`
]
userProfileCheckProcess.running = true
}
Process { Process {
id: userProfileCheckProcess id: userProfileCheckProcess
command: [] command: []
@@ -179,63 +237,6 @@ Singleton {
} }
} }
Process {
id: settingsPortalCheckProcess
command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"]
running: false
onExited: exitCode => {
root.settingsPortalAvailable = (exitCode === 0)
if (root.settingsPortalAvailable) {
root.getSystemColorScheme()
}
}
}
Process {
id: systemColorSchemeCheckProcess
command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"]
running: false
stdout: StdioCollector {
onStreamFinished: {
const match = text.match(/uint32 (\d+)/)
if (match && match[1]) {
root.systemColorScheme = parseInt(match[1])
if (typeof Theme !== "undefined") {
const shouldBeLightMode = (root.systemColorScheme === 2)
if (Theme.isLightMode !== shouldBeLightMode) {
Theme.isLightMode = shouldBeLightMode
if (typeof SessionData !== "undefined") {
SessionData.setLightMode(shouldBeLightMode)
}
}
}
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
root.systemColorScheme = 0
}
}
}
Process {
id: systemColorSchemeSetProcess
running: false
onExited: exitCode => {
if (exitCode === 0) {
Qt.callLater(() => {
root.getSystemColorScheme()
})
}
}
}
IpcHandler { IpcHandler {
target: "profile" target: "profile"

View File

@@ -28,6 +28,30 @@ Singleton {
} }
} }
property bool loginctlAvailable: false
property string sessionId: ""
property string sessionPath: ""
property bool locked: false
property bool active: false
property bool idleHint: false
property bool lockedHint: false
property bool preparingForSleep: false
property string sessionType: ""
property string userName: ""
property string seat: ""
property string display: ""
signal sessionLocked()
signal sessionUnlocked()
signal prepareForSleep()
signal loginctlStateChanged()
property var dmsService: null
property bool subscriptionConnected: false
property bool stateInitialized: false
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
Timer { Timer {
id: sessionInitTimer id: sessionInitTimer
interval: 200 interval: 200
@@ -38,6 +62,7 @@ Singleton {
detectHibernateProcess.running = true detectHibernateProcess.running = true
detectPrimeRunProcess.running = true detectPrimeRunProcess.running = true
console.log("SessionService: Native inhibitor available:", nativeInhibitorAvailable) console.log("SessionService: Native inhibitor available:", nativeInhibitorAvailable)
Qt.callLater(initializeDMSConnection)
} }
} }
@@ -243,4 +268,155 @@ Singleton {
} }
} }
DankSocket {
id: subscriptionSocket
path: root.socketPath
connected: loginctlAvailable
onConnectionStateChanged: {
root.subscriptionConnected = connected
}
parser: SplitParser {
onRead: line => {
if (!line || line.length === 0) {
return
}
try {
const response = JSON.parse(line)
if (response.capabilities) {
Qt.callLater(() => sendSubscribeRequest())
return
}
if (response.result && response.result.type === "loginctl_event") {
handleLoginctlEvent(response.result)
} else if (response.result && response.result.type === "state_changed" && response.result.data) {
updateLoginctlState(response.result.data)
}
} catch (e) {
console.warn("SessionService: Failed to parse subscription response:", line, e)
}
}
}
}
function sendSubscribeRequest() {
subscriptionSocket.send({
"id": 2,
"method": "loginctl.subscribe"
})
}
function initializeDMSConnection() {
try {
dmsService = Qt.createQmlObject('import QtQuick; import qs.Services; QtObject { property var service: DMSService }', root)
if (dmsService && dmsService.service) {
checkCapabilities()
dmsService.service.connectionStateChanged.connect(onDMSConnectionStateChanged)
dmsService.service.capabilitiesChanged.connect(onDMSCapabilitiesChanged)
} else {
console.warn("SessionService: Failed to get DMS service reference")
}
} catch (e) {
console.warn("SessionService: Failed to initialize DMS connection:", e)
}
}
function checkCapabilities() {
if (dmsService && dmsService.service && dmsService.service.isConnected) {
onDMSConnected()
}
}
function onDMSConnectionStateChanged() {
if (dmsService && dmsService.service && dmsService.service.isConnected) {
onDMSConnected()
}
}
function onDMSCapabilitiesChanged() {
if (dmsService && dmsService.service && dmsService.service.capabilities.includes("loginctl")) {
loginctlAvailable = true
if (dmsService.service.isConnected && !stateInitialized) {
stateInitialized = true
getLoginctlState()
subscriptionSocket.connected = true
}
}
}
function onDMSConnected() {
if (dmsService && dmsService.service && dmsService.service.capabilities && dmsService.service.capabilities.length > 0) {
loginctlAvailable = dmsService.service.capabilities.includes("loginctl")
if (loginctlAvailable && !stateInitialized) {
stateInitialized = true
getLoginctlState()
subscriptionSocket.connected = true
}
}
}
function getLoginctlState() {
if (!loginctlAvailable || !dmsService || !dmsService.service) return
dmsService.service.sendRequest("loginctl.getState", null, response => {
if (response.result) {
updateLoginctlState(response.result)
}
})
}
function updateLoginctlState(state) {
sessionId = state.sessionId || ""
sessionPath = state.sessionPath || ""
locked = state.locked || false
active = state.active || false
idleHint = state.idleHint || false
lockedHint = state.lockedHint || false
sessionType = state.sessionType || ""
userName = state.userName || ""
seat = state.seat || ""
display = state.display || ""
const wasPreparing = preparingForSleep
preparingForSleep = state.preparingForSleep || false
if (preparingForSleep && !wasPreparing) {
prepareForSleep()
}
loginctlStateChanged()
}
function handleLoginctlEvent(event) {
if (event.event === "Lock") {
locked = true
lockedHint = true
sessionLocked()
} else if (event.event === "Unlock") {
locked = false
lockedHint = false
sessionUnlocked()
} else if (event.event === "PrepareForSleep") {
preparingForSleep = event.data?.sleeping || false
if (preparingForSleep) {
prepareForSleep()
}
}
}
function lockSession() {
if (!loginctlAvailable || !dmsService || !dmsService.service) return
dmsService.service.sendRequest("loginctl.lock", null, response => {
if (response.error) {
console.warn("SessionService: Failed to lock session:", response.error)
}
})
}
} }

View File

@@ -9,6 +9,20 @@ import Quickshell.Io
Singleton { Singleton {
id: root id: root
property int refCount: 0
onRefCountChanged: {
console.log("VpnService: refCount changed to", refCount)
if (refCount > 0 && !nmMonitor.running) {
console.log("VpnService: Starting nmMonitor")
nmMonitor.running = true
refreshAll()
} else if (refCount === 0 && nmMonitor.running) {
console.log("VpnService: Stopping nmMonitor")
nmMonitor.running = false
}
}
// State // State
property bool available: true property bool available: true
property bool isBusy: false property bool isBusy: false
@@ -36,18 +50,6 @@ Singleton {
// Use implicit property notify signals (profilesChanged, activeUuidChanged, etc.) // Use implicit property notify signals (profilesChanged, activeUuidChanged, etc.)
Component.onCompleted: initialize()
Component.onDestruction: {
nmMonitor.running = false
}
function initialize() {
// Start monitoring NetworkManager for changes
nmMonitor.running = true
refreshAll()
}
function refreshAll() { function refreshAll() {
listProfiles() listProfiles()
refreshActive() refreshActive()

File diff suppressed because it is too large Load Diff

View File

@@ -174,6 +174,13 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "Apps Icon",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Apps are ordered by usage frequency, then last used, then alphabetically.", "term": "Apps are ordered by usage frequency, then last used, then alphabetically.",
"translation": "", "translation": "",
@@ -440,6 +447,13 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "Choose Launcher Logo Color",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Choose icon", "term": "Choose icon",
"translation": "", "translation": "",
@@ -447,6 +461,13 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "Choose the logo displayed on the launcher button in DankBar",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Clear", "term": "Clear",
"translation": "", "translation": "",
@@ -594,6 +615,13 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "Custom",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Custom Location", "term": "Custom Location",
"translation": "", "translation": "",
@@ -629,6 +657,13 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "DMS_SOCKET not available",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Daily at:", "term": "Daily at:",
"translation": "", "translation": "",
@@ -657,6 +692,13 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "DankBar Font Scale",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Date Format", "term": "Date Format",
"translation": "", "translation": "",
@@ -1112,6 +1154,13 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "Invert on mode change",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Kill Process", "term": "Kill Process",
"translation": "", "translation": "",
@@ -1148,7 +1197,14 @@
"comment": "" "comment": ""
}, },
{ {
"term": "Launcher Button", "term": "Launch on dGPU",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{
"term": "Launcher Button Logo",
"translation": "", "translation": "",
"context": "", "context": "",
"reference": "", "reference": "",
@@ -1329,6 +1385,13 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "NM not supported",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Named Workspace Icons", "term": "Named Workspace Icons",
"translation": "", "translation": "",
@@ -1539,6 +1602,13 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "OS Logo",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Office", "term": "Office",
"translation": "", "translation": "",
@@ -1784,6 +1854,13 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "Scale DankBar font sizes independently",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Scale all font sizes", "term": "Scale all font sizes",
"translation": "", "translation": "",
@@ -1826,6 +1903,13 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "Select Launcher Logo",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Select a color from the palette or use custom sliders", "term": "Select a color from the palette or use custom sliders",
"translation": "", "translation": "",
@@ -1840,6 +1924,13 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "Select an image file...",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Select which transitions to include in randomization", "term": "Select which transitions to include in randomization",
"translation": "", "translation": "",
@@ -1938,6 +2029,13 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "Size Offset",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Spacing", "term": "Spacing",
"translation": "", "translation": "",
@@ -2183,13 +2281,6 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "Use OS Logo",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "Use%", "term": "Use%",
"translation": "", "translation": "",
@@ -2344,6 +2435,13 @@
"reference": "", "reference": "",
"comment": "" "comment": ""
}, },
{
"term": "update dms for NM integration.",
"translation": "",
"context": "",
"reference": "",
"comment": ""
},
{ {
"term": "• Install only from trusted sources", "term": "• Install only from trusted sources",
"translation": "", "translation": "",