1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-28 15:32:50 -05:00

Systematic cleanup and qmlfmt of all services

- qmlfmt kinda sucks but it's what qt creator uses
This commit is contained in:
bbedward
2025-09-02 22:45:06 -04:00
parent 21089aa66e
commit 531d6334fb
21 changed files with 1119 additions and 1223 deletions

View File

@@ -138,7 +138,7 @@ DankModal {
wifiPasswordInput = text wifiPasswordInput = text
} }
onAccepted: { onAccepted: {
NetworkService.connectToWifiWithPassword( NetworkService.connectToWifi(
wifiPasswordSSID, passwordInput.text) wifiPasswordSSID, passwordInput.text)
close() close()
wifiPasswordInput = "" wifiPasswordInput = ""
@@ -286,7 +286,7 @@ DankModal {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: parent.enabled enabled: parent.enabled
onClicked: { onClicked: {
NetworkService.connectToWifiWithPassword( NetworkService.connectToWifi(
wifiPasswordSSID, wifiPasswordSSID,
passwordInput.text) passwordInput.text)
close() close()

View File

@@ -1,10 +1,9 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import "../Common/fuzzysort.js" as Fuzzy import "../Common/fuzzysort.js" as Fuzzy
Singleton { Singleton {
@@ -13,60 +12,46 @@ Singleton {
property var applications: DesktopEntries.applications.values.filter(app => !app.noDisplay && !app.runInTerminal) property var applications: DesktopEntries.applications.values.filter(app => !app.noDisplay && !app.runInTerminal)
property var preppedApps: applications.map(app => ({ property var preppedApps: applications.map(app => ({
"name": Fuzzy.prepare( "name": Fuzzy.prepare(app.name || ""),
app.name "comment": Fuzzy.prepare(app.comment || ""),
|| ""),
"comment": Fuzzy.prepare(
app.comment
|| ""),
"entry": app "entry": app
})) }))
function searchApplications(query) { function searchApplications(query) {
if (!query || query.length === 0) { if (!query || query.length === 0)
return applications return applications
} if (preppedApps.length === 0)
if (preppedApps.length === 0) {
return [] return []
}
var results = Fuzzy.go(query, preppedApps, { var results = Fuzzy.go(query, preppedApps, {
"all": false, "all": false,
"keys": ["name", "comment"], "keys": ["name", "comment"],
"scoreFn": r => { "scoreFn": r => {
var nameScore = r[0] ? r[0].score : 0 const nameScore = r[0]?.score || 0
var commentScore = r[1] ? r[1].score : 0 const commentScore = r[1]?.score || 0
var appName = r.obj.entry.name || "" const appName = r.obj.entry.name || ""
var finalScore = 0
if (nameScore > 0) { if (nameScore === 0) {
var queryLower = query.toLowerCase() return commentScore * 0.1
var nameLower = appName.toLowerCase()
if (nameLower === queryLower) {
finalScore = nameScore * 100
} else if (nameLower.startsWith(
queryLower)) {
finalScore = nameScore * 50
} else if (nameLower.includes(
" " + queryLower)
|| nameLower.includes(
queryLower + " ")
|| nameLower.endsWith(
" " + queryLower)) {
finalScore = nameScore * 25
} else if (nameLower.includes(
queryLower)) {
finalScore = nameScore * 10
} else {
finalScore = nameScore * 2 + commentScore * 0.1
}
} else {
finalScore = commentScore * 0.1
} }
return finalScore const queryLower = query.toLowerCase()
const nameLower = appName.toLowerCase()
if (nameLower === queryLower) {
return nameScore * 100
}
if (nameLower.startsWith(queryLower)) {
return nameScore * 50
}
if (nameLower.includes(" " + queryLower) || nameLower.includes(queryLower + " ") || nameLower.endsWith(" " + queryLower)) {
return nameScore * 25
}
if (nameLower.includes(queryLower)) {
return nameScore * 10
}
return nameScore * 2 + commentScore * 0.1
}, },
"limit": 50 "limit": 50
}) })
@@ -75,10 +60,10 @@ Singleton {
} }
function getCategoriesForApp(app) { function getCategoriesForApp(app) {
if (!app || !app.categories) if (!app?.categories)
return [] return []
var categoryMap = { const categoryMap = {
"AudioVideo": "Media", "AudioVideo": "Media",
"Audio": "Media", "Audio": "Media",
"Video": "Media", "Video": "Media",
@@ -105,19 +90,16 @@ Singleton {
"TerminalEmulator": "Utilities" "TerminalEmulator": "Utilities"
} }
var mappedCategories = new Set() const mappedCategories = new Set()
for (var i = 0; i < app.categories.length; i++) { for (const cat of app.categories) {
var cat = app.categories[i] if (categoryMap[cat])
if (categoryMap[cat]) {
mappedCategories.add(categoryMap[cat]) mappedCategories.add(categoryMap[cat])
}
} }
return Array.from(mappedCategories) return Array.from(mappedCategories)
} }
// Category icon mappings
property var categoryIcons: ({ property var categoryIcons: ({
"All": "apps", "All": "apps",
"Media": "music_video", "Media": "music_video",
@@ -136,10 +118,10 @@ Singleton {
} }
function getAllCategories() { function getAllCategories() {
var categories = new Set(["All"]) const categories = new Set(["All"])
for (var i = 0; i < applications.length; i++) { for (const app of applications) {
var appCategories = getCategoriesForApp(applications[i]) const appCategories = getCategoriesForApp(app)
appCategories.forEach(cat => categories.add(cat)) appCategories.forEach(cat => categories.add(cat))
} }
@@ -152,8 +134,7 @@ Singleton {
} }
return applications.filter(app => { return applications.filter(app => {
var appCategories = getCategoriesForApp( const appCategories = getCategoriesForApp(app)
app)
return appCategories.includes(category) return appCategories.includes(category)
}) })
} }

View File

@@ -17,8 +17,9 @@ Singleton {
signal micMuteChanged signal micMuteChanged
function displayName(node) { function displayName(node) {
if (!node) if (!node) {
return "" return ""
}
if (node.properties && node.properties["device.description"]) { if (node.properties && node.properties["device.description"]) {
return node.properties["device.description"] return node.properties["device.description"]
@@ -32,39 +33,51 @@ Singleton {
return node.nickname return node.nickname
} }
if (node.name.includes("analog-stereo")) if (node.name.includes("analog-stereo")) {
return "Built-in Speakers" return "Built-in Speakers"
else if (node.name.includes("bluez")) }
if (node.name.includes("bluez")) {
return "Bluetooth Audio" return "Bluetooth Audio"
else if (node.name.includes("usb")) }
if (node.name.includes("usb")) {
return "USB Audio" return "USB Audio"
else if (node.name.includes("hdmi")) }
if (node.name.includes("hdmi")) {
return "HDMI Audio" return "HDMI Audio"
}
return node.name return node.name
} }
function subtitle(name) { function subtitle(name) {
if (!name) if (!name) {
return "" return ""
}
if (name.includes('usb-')) { if (name.includes('usb-')) {
if (name.includes('SteelSeries')) { if (name.includes('SteelSeries')) {
return "USB Gaming Headset" return "USB Gaming Headset"
} else if (name.includes('Generic')) { }
if (name.includes('Generic')) {
return "USB Audio Device" return "USB Audio Device"
} }
return "USB Audio" return "USB Audio"
} else if (name.includes('pci-')) { }
if (name.includes('pci-')) {
if (name.includes('01_00.1') || name.includes('01:00.1')) { if (name.includes('01_00.1') || name.includes('01:00.1')) {
return "NVIDIA GPU Audio" return "NVIDIA GPU Audio"
} }
return "PCI Audio" return "PCI Audio"
} else if (name.includes('bluez')) { }
if (name.includes('bluez')) {
return "Bluetooth Audio" return "Bluetooth Audio"
} else if (name.includes('analog')) { }
if (name.includes('analog')) {
return "Built-in Audio" return "Built-in Audio"
} else if (name.includes('hdmi')) { }
if (name.includes('hdmi')) {
return "HDMI Audio" return "HDMI Audio"
} }
@@ -72,48 +85,48 @@ Singleton {
} }
PwObjectTracker { PwObjectTracker {
objects: Pipewire.nodes.values.filter( objects: Pipewire.nodes.values.filter(node => node.audio && !node.isStream)
node => node.audio && !node.isStream
)
} }
// Volume control functions
function setVolume(percentage) { function setVolume(percentage) {
if (root.sink && root.sink.audio) { if (!root.sink?.audio) {
const clampedVolume = Math.max(0, Math.min(100, percentage)) return "No audio sink available"
root.sink.audio.volume = clampedVolume / 100
root.volumeChanged()
return "Volume set to " + clampedVolume + "%"
} }
return "No audio sink available"
const clampedVolume = Math.max(0, Math.min(100, percentage))
root.sink.audio.volume = clampedVolume / 100
root.volumeChanged()
return `Volume set to ${clampedVolume}%`
} }
function toggleMute() { function toggleMute() {
if (root.sink && root.sink.audio) { if (!root.sink?.audio) {
root.sink.audio.muted = !root.sink.audio.muted return "No audio sink available"
return root.sink.audio.muted ? "Audio muted" : "Audio unmuted"
} }
return "No audio sink available"
root.sink.audio.muted = !root.sink.audio.muted
return root.sink.audio.muted ? "Audio muted" : "Audio unmuted"
} }
function setMicVolume(percentage) { function setMicVolume(percentage) {
if (root.source && root.source.audio) { if (!root.source?.audio) {
const clampedVolume = Math.max(0, Math.min(100, percentage)) return "No audio source available"
root.source.audio.volume = clampedVolume / 100
return "Microphone volume set to " + clampedVolume + "%"
} }
return "No audio source available"
const clampedVolume = Math.max(0, Math.min(100, percentage))
root.source.audio.volume = clampedVolume / 100
return `Microphone volume set to ${clampedVolume}%`
} }
function toggleMicMute() { function toggleMicMute() {
if (root.source && root.source.audio) { if (!root.source?.audio) {
root.source.audio.muted = !root.source.audio.muted return "No audio source available"
return root.source.audio.muted ? "Microphone muted" : "Microphone unmuted"
} }
return "No audio source available"
root.source.audio.muted = !root.source.audio.muted
return root.source.audio.muted ? "Microphone muted" : "Microphone unmuted"
} }
// IPC Handler for external control
IpcHandler { IpcHandler {
target: "audio" target: "audio"
@@ -122,35 +135,39 @@ Singleton {
} }
function increment(step: string): string { function increment(step: string): string {
if (root.sink && root.sink.audio) { if (!root.sink?.audio) {
if (root.sink.audio.muted) { return "No audio sink available"
root.sink.audio.muted = false
}
const currentVolume = Math.round(root.sink.audio.volume * 100)
const newVolume = Math.max(0, Math.min(100,
currentVolume + parseInt(
step || "5")))
root.sink.audio.volume = newVolume / 100
root.volumeChanged()
return "Volume increased to " + newVolume + "%"
} }
return "No audio sink available"
if (root.sink.audio.muted) {
root.sink.audio.muted = false
}
const currentVolume = Math.round(root.sink.audio.volume * 100)
const stepValue = parseInt(step || "5")
const newVolume = Math.max(0, Math.min(100, currentVolume + stepValue))
root.sink.audio.volume = newVolume / 100
root.volumeChanged()
return `Volume increased to ${newVolume}%`
} }
function decrement(step: string): string { function decrement(step: string): string {
if (root.sink && root.sink.audio) { if (!root.sink?.audio) {
if (root.sink.audio.muted) { return "No audio sink available"
root.sink.audio.muted = false
}
const currentVolume = Math.round(root.sink.audio.volume * 100)
const newVolume = Math.max(0, Math.min(100,
currentVolume - parseInt(
step || "5")))
root.sink.audio.volume = newVolume / 100
root.volumeChanged()
return "Volume decreased to " + newVolume + "%"
} }
return "No audio sink available"
if (root.sink.audio.muted) {
root.sink.audio.muted = false
}
const currentVolume = Math.round(root.sink.audio.volume * 100)
const stepValue = parseInt(step || "5")
const newVolume = Math.max(0, Math.min(100, currentVolume - stepValue))
root.sink.audio.volume = newVolume / 100
root.volumeChanged()
return `Volume decreased to ${newVolume}%`
} }
function mute(): string { function mute(): string {
@@ -171,17 +188,19 @@ Singleton {
function status(): string { function status(): string {
let result = "Audio Status:\n" let result = "Audio Status:\n"
if (root.sink && root.sink.audio) {
if (root.sink?.audio) {
const volume = Math.round(root.sink.audio.volume * 100) const volume = Math.round(root.sink.audio.volume * 100)
result += "Output: " + volume + "%" const muteStatus = root.sink.audio.muted ? " (muted)" : ""
+ (root.sink.audio.muted ? " (muted)" : "") + "\n" result += `Output: ${volume}%${muteStatus}\n`
} else { } else {
result += "Output: No sink available\n" result += "Output: No sink available\n"
} }
if (root.source && root.source.audio) { if (root.source?.audio) {
const micVolume = Math.round(root.source.audio.volume * 100) const micVolume = Math.round(root.source.audio.volume * 100)
result += "Input: " + micVolume + "%" + (root.source.audio.muted ? " (muted)" : "") const muteStatus = root.source.audio.muted ? " (muted)" : ""
result += `Input: ${micVolume}%${muteStatus}`
} else { } else {
result += "Input: No source available" result += "Input: No source available"
} }

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -9,68 +10,50 @@ Singleton {
id: root id: root
readonly property UPowerDevice device: UPower.displayDevice readonly property UPowerDevice device: UPower.displayDevice
readonly property bool batteryAvailable: device && device.ready readonly property bool batteryAvailable: device && device.ready && device.isLaptopBattery
&& device.isLaptopBattery readonly property real batteryLevel: batteryAvailable ? Math.round(device.percentage * 100) : 0
readonly property real batteryLevel: batteryAvailable ? Math.round( readonly property bool isCharging: batteryAvailable && device.state === UPowerDeviceState.Charging && device.changeRate > 0
device.percentage * 100) : 0 readonly property bool isPluggedIn: batteryAvailable && (device.state !== UPowerDeviceState.Discharging && device.state !== UPowerDeviceState.Empty)
readonly property bool isCharging: batteryAvailable
&& device.state === UPowerDeviceState.Charging
&& device.changeRate > 0
readonly property bool isPluggedIn: batteryAvailable
&& (device.state !== UPowerDeviceState.Discharging
&& device.state !== UPowerDeviceState.Empty)
readonly property bool isLowBattery: batteryAvailable && batteryLevel <= 20 readonly property bool isLowBattery: batteryAvailable && batteryLevel <= 20
readonly property string batteryHealth: { readonly property string batteryHealth: {
if (!batteryAvailable) if (!batteryAvailable) {
return "N/A" return "N/A"
}
if (device.healthSupported && device.healthPercentage > 0) if (device.healthSupported && device.healthPercentage > 0) {
return Math.round(device.healthPercentage) + "%" return `${Math.round(device.healthPercentage)}%`
}
// Calculate health from energy capacity vs design capacity
if (device.energyCapacity > 0 && device.energy > 0) { if (device.energyCapacity > 0 && device.energy > 0) {
// energyCapacity is current full capacity, we need design capacity const healthPercent = (device.energyCapacity / 90.0045) * 100
// Use a rough estimate based on typical battery degradation patterns return `${Math.round(healthPercent)}%`
var healthPercent = (device.energyCapacity / 90.0045)
* 100 // your design capacity from upower
return Math.round(healthPercent) + "%"
} }
return "N/A" return "N/A"
} }
readonly property real batteryCapacity: batteryAvailable readonly property real batteryCapacity: batteryAvailable && device.energyCapacity > 0 ? device.energyCapacity : 0
&& device.energyCapacity > 0 ? device.energyCapacity : 0
readonly property string batteryStatus: { readonly property string batteryStatus: {
if (!batteryAvailable) if (!batteryAvailable) {
return "No Battery" return "No Battery"
}
if (device.state === UPowerDeviceState.Charging if (device.state === UPowerDeviceState.Charging && device.changeRate <= 0) {
&& device.changeRate <= 0) return "Plugged In"
return "Plugged In" }
return UPowerDeviceState.toString(device.state) return UPowerDeviceState.toString(device.state)
} }
readonly property bool suggestPowerSaver: batteryAvailable && isLowBattery readonly property bool suggestPowerSaver: batteryAvailable && isLowBattery && UPower.onBattery && (typeof PowerProfiles !== "undefined" && PowerProfiles.profile !== PowerProfile.PowerSaver)
&& UPower.onBattery
&& (typeof PowerProfiles !== "undefined"
&& PowerProfiles.profile
!== PowerProfile.PowerSaver)
readonly property var bluetoothDevices: { readonly property var bluetoothDevices: {
var btDevices = [] const btDevices = []
const bluetoothTypes = [UPowerDeviceType.BluetoothGeneric, UPowerDeviceType.Headphones, UPowerDeviceType.Headset, UPowerDeviceType.Keyboard, UPowerDeviceType.Mouse, UPowerDeviceType.Speakers]
for (var i = 0; i < UPower.devices.count; i++) { for (var i = 0; i < UPower.devices.count; i++) {
var dev = UPower.devices.get(i) const dev = UPower.devices.get(i)
if (dev if (dev && dev.ready && bluetoothTypes.includes(dev.type)) {
&& dev.ready && (dev.type === UPowerDeviceType.BluetoothGeneric || dev.type
=== UPowerDeviceType.Headphones || dev.type
=== UPowerDeviceType.Headset || dev.type
=== UPowerDeviceType.Keyboard || dev.type
=== UPowerDeviceType.Mouse || dev.type
=== UPowerDeviceType.Speakers)) {
btDevices.push({ btDevices.push({
"name": dev.model "name": dev.model || UPowerDeviceType.toString(dev.type),
|| UPowerDeviceType.toString(
dev.type),
"percentage": Math.round(dev.percentage), "percentage": Math.round(dev.percentage),
"type": dev.type "type": dev.type
}) })
@@ -80,20 +63,23 @@ Singleton {
} }
function formatTimeRemaining() { function formatTimeRemaining() {
if (!batteryAvailable) if (!batteryAvailable) {
return "Unknown" return "Unknown"
}
var timeSeconds = isCharging ? device.timeToFull : device.timeToEmpty const timeSeconds = isCharging ? device.timeToFull : device.timeToEmpty
if (!timeSeconds || timeSeconds <= 0 || timeSeconds > 86400) if (!timeSeconds || timeSeconds <= 0 || timeSeconds > 86400) {
return "Unknown" return "Unknown"
}
var hours = Math.floor(timeSeconds / 3600) const hours = Math.floor(timeSeconds / 3600)
var minutes = Math.floor((timeSeconds % 3600) / 60) const minutes = Math.floor((timeSeconds % 3600) / 60)
if (hours > 0) if (hours > 0) {
return hours + "h " + minutes + "m" return `${hours}h ${minutes}m`
else }
return minutes + "m"
return `${minutes}m`
} }
} }

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -12,230 +13,238 @@ Singleton {
readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter
readonly property bool available: adapter !== null readonly property bool available: adapter !== null
readonly property bool enabled: (adapter && adapter.enabled) ?? false readonly property bool enabled: (adapter && adapter.enabled) ?? false
readonly property bool discovering: (adapter readonly property bool discovering: (adapter && adapter.discovering) ?? false
&& adapter.discovering) ?? false
readonly property var devices: adapter ? adapter.devices : null readonly property var devices: adapter ? adapter.devices : null
readonly property var pairedDevices: { readonly property var pairedDevices: {
if (!adapter || !adapter.devices) if (!adapter || !adapter.devices) {
return [] return []
}
return adapter.devices.values.filter(dev => { return adapter.devices.values.filter(dev => {
return dev && (dev.paired return dev && (dev.paired || dev.trusted)
|| dev.trusted)
}) })
} }
readonly property var allDevicesWithBattery: { readonly property var allDevicesWithBattery: {
if (!adapter || !adapter.devices) if (!adapter || !adapter.devices) {
return [] return []
}
return adapter.devices.values.filter(dev => { return adapter.devices.values.filter(dev => {
return dev return dev && dev.batteryAvailable && dev.battery > 0
&& dev.batteryAvailable
&& dev.battery > 0
}) })
} }
function sortDevices(devices) { function sortDevices(devices) {
return devices.sort((a, b) => { return devices.sort((a, b) => {
var aName = a.name || a.deviceName || "" const aName = a.name || a.deviceName || ""
var bName = b.name || b.deviceName || "" const bName = b.name || b.deviceName || ""
var aHasRealName = aName.includes(" ") const aHasRealName = aName.includes(" ") && aName.length > 3
&& aName.length > 3 const bHasRealName = bName.includes(" ") && bName.length > 3
var bHasRealName = bName.includes(" ")
&& bName.length > 3
if (aHasRealName && !bHasRealName) if (aHasRealName && !bHasRealName) {
return -1 return -1
if (!aHasRealName && bHasRealName) }
return 1 if (!aHasRealName && bHasRealName) {
return 1
}
var aSignal = (a.signalStrength !== undefined const aSignal = (a.signalStrength !== undefined && a.signalStrength > 0) ? a.signalStrength : 0
&& a.signalStrength > 0) ? a.signalStrength : 0 const bSignal = (b.signalStrength !== undefined && b.signalStrength > 0) ? b.signalStrength : 0
var bSignal = (b.signalStrength !== undefined
&& b.signalStrength > 0) ? b.signalStrength : 0
return bSignal - aSignal return bSignal - aSignal
}) })
} }
function getDeviceIcon(device) { function getDeviceIcon(device) {
if (!device) if (!device) {
return "bluetooth" return "bluetooth"
}
var name = (device.name || device.deviceName || "").toLowerCase() const name = (device.name || device.deviceName || "").toLowerCase()
var icon = (device.icon || "").toLowerCase() const icon = (device.icon || "").toLowerCase()
if (icon.includes("headset") || icon.includes("audio") || name.includes(
"headphone") || name.includes("airpod") || name.includes( const audioKeywords = ["headset", "audio", "headphone", "airpod", "arctis"]
"headset") || name.includes("arctis")) if (audioKeywords.some(keyword => icon.includes(keyword) || name.includes(keyword))) {
return "headset" return "headset"
}
if (icon.includes("mouse") || name.includes("mouse")) if (icon.includes("mouse") || name.includes("mouse")) {
return "mouse" return "mouse"
}
if (icon.includes("keyboard") || name.includes("keyboard")) if (icon.includes("keyboard") || name.includes("keyboard")) {
return "keyboard" return "keyboard"
}
if (icon.includes("phone") || name.includes("phone") || name.includes( const phoneKeywords = ["phone", "iphone", "android", "samsung"]
"iphone") || name.includes("android") || name.includes( if (phoneKeywords.some(keyword => icon.includes(keyword) || name.includes(keyword))) {
"samsung"))
return "smartphone" return "smartphone"
}
if (icon.includes("watch") || name.includes("watch")) if (icon.includes("watch") || name.includes("watch")) {
return "watch" return "watch"
}
if (icon.includes("speaker") || name.includes("speaker")) if (icon.includes("speaker") || name.includes("speaker")) {
return "speaker" return "speaker"
}
if (icon.includes("display") || name.includes("tv")) if (icon.includes("display") || name.includes("tv")) {
return "tv" return "tv"
}
return "bluetooth" return "bluetooth"
} }
function canConnect(device) { function canConnect(device) {
if (!device) if (!device) {
return false return false
}
return !device.paired && !device.pairing && !device.blocked return !device.paired && !device.pairing && !device.blocked
} }
function getSignalStrength(device) { function getSignalStrength(device) {
if (!device || device.signalStrength === undefined if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
|| device.signalStrength <= 0)
return "Unknown" return "Unknown"
}
var signal = device.signalStrength const signal = device.signalStrength
if (signal >= 80) if (signal >= 80) {
return "Excellent" return "Excellent"
}
if (signal >= 60) if (signal >= 60) {
return "Good" return "Good"
}
if (signal >= 40) if (signal >= 40) {
return "Fair" return "Fair"
}
if (signal >= 20) if (signal >= 20) {
return "Poor" return "Poor"
}
return "Very Poor" return "Very Poor"
} }
function getSignalIcon(device) { function getSignalIcon(device) {
if (!device || device.signalStrength === undefined if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
|| device.signalStrength <= 0)
return "signal_cellular_null" return "signal_cellular_null"
}
var signal = device.signalStrength const signal = device.signalStrength
if (signal >= 80) if (signal >= 80) {
return "signal_cellular_4_bar" return "signal_cellular_4_bar"
}
if (signal >= 60) if (signal >= 60) {
return "signal_cellular_3_bar" return "signal_cellular_3_bar"
}
if (signal >= 40) if (signal >= 40) {
return "signal_cellular_2_bar" return "signal_cellular_2_bar"
}
if (signal >= 20) if (signal >= 20) {
return "signal_cellular_1_bar" return "signal_cellular_1_bar"
}
return "signal_cellular_0_bar" return "signal_cellular_0_bar"
} }
function isDeviceBusy(device) { function isDeviceBusy(device) {
if (!device) if (!device) {
return false return false
return device.pairing }
|| device.state === BluetoothDeviceState.Disconnecting return device.pairing || device.state === BluetoothDeviceState.Disconnecting || device.state === BluetoothDeviceState.Connecting
|| device.state === BluetoothDeviceState.Connecting
} }
function connectDeviceWithTrust(device) { function connectDeviceWithTrust(device) {
if (!device) if (!device) {
return return
}
device.trusted = true device.trusted = true
device.connect() device.connect()
} }
function getCardName(device) { function getCardName(device) {
if (!device) if (!device) {
return "" return ""
return "bluez_card." + device.address.replace(/:/g, "_") }
return `bluez_card.${device.address.replace(/:/g, "_")}`
} }
function isAudioDevice(device) { function isAudioDevice(device) {
if (!device) if (!device) {
return false return false
let icon = getDeviceIcon(device) }
const icon = getDeviceIcon(device)
return icon === "headset" || icon === "speaker" return icon === "headset" || icon === "speaker"
} }
function getCodecInfo(codecName) { function getCodecInfo(codecName) {
let codec = codecName.replace(/-/g, "_").toUpperCase() const codec = codecName.replace(/-/g, "_").toUpperCase()
let codecMap = { const codecMap = {
"LDAC": { "LDAC": {
name: "LDAC", "name": "LDAC",
description: "Highest quality • Higher battery usage", "description": "Highest quality • Higher battery usage",
qualityColor: "#4CAF50" "qualityColor": "#4CAF50"
}, },
"APTX_HD": { "APTX_HD": {
name: "aptX HD", "name": "aptX HD",
description: "High quality • Balanced battery", "description": "High quality • Balanced battery",
qualityColor: "#FF9800" "qualityColor": "#FF9800"
}, },
"APTX": { "APTX": {
name: "aptX", "name": "aptX",
description: "Good quality • Low latency", "description": "Good quality • Low latency",
qualityColor: "#FF9800" "qualityColor": "#FF9800"
}, },
"AAC": { "AAC": {
name: "AAC", "name": "AAC",
description: "Balanced quality and battery", "description": "Balanced quality and battery",
qualityColor: "#2196F3" "qualityColor": "#2196F3"
}, },
"SBC_XQ": { "SBC_XQ": {
name: "SBC-XQ", "name": "SBC-XQ",
description: "Enhanced SBC • Better compatibility", "description": "Enhanced SBC • Better compatibility",
qualityColor: "#2196F3" "qualityColor": "#2196F3"
}, },
"SBC": { "SBC": {
name: "SBC", "name": "SBC",
description: "Basic quality • Universal compatibility", "description": "Basic quality • Universal compatibility",
qualityColor: "#9E9E9E" "qualityColor": "#9E9E9E"
}, },
"MSBC": { "MSBC": {
name: "mSBC", "name": "mSBC",
description: "Modified SBC • Optimized for speech", "description": "Modified SBC • Optimized for speech",
qualityColor: "#9E9E9E" "qualityColor": "#9E9E9E"
}, },
"CVSD": { "CVSD": {
name: "CVSD", "name": "CVSD",
description: "Basic speech codec • Legacy compatibility", "description": "Basic speech codec • Legacy compatibility",
qualityColor: "#9E9E9E" "qualityColor": "#9E9E9E"
} }
} }
return codecMap[codec] || { return codecMap[codec] || {
name: codecName, "name": codecName,
description: "Unknown codec", "description": "Unknown codec",
qualityColor: "#9E9E9E" "qualityColor": "#9E9E9E"
} }
} }
property var deviceCodecs: ({}) property var deviceCodecs: ({})
function updateDeviceCodec(deviceAddress, codec) { function updateDeviceCodec(deviceAddress, codec) {
deviceCodecs[deviceAddress] = codec deviceCodecs[deviceAddress] = codec
deviceCodecsChanged() deviceCodecsChanged()
} }
function refreshDeviceCodec(device) { function refreshDeviceCodec(device) {
if (!device || !device.connected || !isAudioDevice(device)) { if (!device || !device.connected || !isAudioDevice(device)) {
return return
} }
let cardName = getCardName(device) const cardName = getCardName(device)
codecQueryProcess.cardName = cardName codecQueryProcess.cardName = cardName
codecQueryProcess.deviceAddress = device.address codecQueryProcess.deviceAddress = device.address
codecQueryProcess.availableCodecs = [] codecQueryProcess.availableCodecs = []
@@ -243,14 +252,14 @@ Singleton {
codecQueryProcess.detectedCodec = "" codecQueryProcess.detectedCodec = ""
codecQueryProcess.running = true codecQueryProcess.running = true
} }
function getCurrentCodec(device, callback) { function getCurrentCodec(device, callback) {
if (!device || !device.connected || !isAudioDevice(device)) { if (!device || !device.connected || !isAudioDevice(device)) {
callback("") callback("")
return return
} }
let cardName = getCardName(device) const cardName = getCardName(device)
codecQueryProcess.cardName = cardName codecQueryProcess.cardName = cardName
codecQueryProcess.callback = callback codecQueryProcess.callback = callback
codecQueryProcess.availableCodecs = [] codecQueryProcess.availableCodecs = []
@@ -258,14 +267,14 @@ Singleton {
codecQueryProcess.detectedCodec = "" codecQueryProcess.detectedCodec = ""
codecQueryProcess.running = true codecQueryProcess.running = true
} }
function getAvailableCodecs(device, callback) { function getAvailableCodecs(device, callback) {
if (!device || !device.connected || !isAudioDevice(device)) { if (!device || !device.connected || !isAudioDevice(device)) {
callback([], "") callback([], "")
return return
} }
let cardName = getCardName(device) const cardName = getCardName(device)
codecFullQueryProcess.cardName = cardName codecFullQueryProcess.cardName = cardName
codecFullQueryProcess.callback = callback codecFullQueryProcess.callback = callback
codecFullQueryProcess.availableCodecs = [] codecFullQueryProcess.availableCodecs = []
@@ -273,33 +282,33 @@ Singleton {
codecFullQueryProcess.detectedCodec = "" codecFullQueryProcess.detectedCodec = ""
codecFullQueryProcess.running = true codecFullQueryProcess.running = true
} }
function switchCodec(device, profileName, callback) { function switchCodec(device, profileName, callback) {
if (!device || !isAudioDevice(device)) { if (!device || !isAudioDevice(device)) {
callback(false, "Invalid device") callback(false, "Invalid device")
return return
} }
let cardName = getCardName(device) const cardName = getCardName(device)
codecSwitchProcess.cardName = cardName codecSwitchProcess.cardName = cardName
codecSwitchProcess.profile = profileName codecSwitchProcess.profile = profileName
codecSwitchProcess.callback = callback codecSwitchProcess.callback = callback
codecSwitchProcess.running = true codecSwitchProcess.running = true
} }
Process { Process {
id: codecQueryProcess id: codecQueryProcess
property string cardName: "" property string cardName: ""
property string deviceAddress: "" property string deviceAddress: ""
property var callback: null property var callback: null
property bool parsingTargetCard: false property bool parsingTargetCard: false
property string detectedCodec: "" property string detectedCodec: ""
property var availableCodecs: [] property var availableCodecs: []
command: ["pactl", "list", "cards"] command: ["pactl", "list", "cards"]
onExited: function(exitCode, exitStatus) { onExited: (exitCode, exitStatus) => {
if (exitCode === 0 && detectedCodec) { if (exitCode === 0 && detectedCodec) {
if (deviceAddress) { if (deviceAddress) {
root.updateDeviceCodec(deviceAddress, detectedCodec) root.updateDeviceCodec(deviceAddress, detectedCodec)
@@ -310,35 +319,35 @@ Singleton {
} else if (callback) { } else if (callback) {
callback("") callback("")
} }
parsingTargetCard = false parsingTargetCard = false
detectedCodec = "" detectedCodec = ""
availableCodecs = [] availableCodecs = []
deviceAddress = "" deviceAddress = ""
callback = null callback = null
} }
stdout: SplitParser { stdout: SplitParser {
splitMarker: "\n" splitMarker: "\n"
onRead: (data) => { onRead: data => {
let line = data.trim() let line = data.trim()
if (line.includes(`Name: ${codecQueryProcess.cardName}`)) { if (line.includes(`Name: ${codecQueryProcess.cardName}`)) {
codecQueryProcess.parsingTargetCard = true codecQueryProcess.parsingTargetCard = true
return return
} }
if (codecQueryProcess.parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecQueryProcess.cardName)) { if (codecQueryProcess.parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecQueryProcess.cardName)) {
codecQueryProcess.parsingTargetCard = false codecQueryProcess.parsingTargetCard = false
return return
} }
if (codecQueryProcess.parsingTargetCard) { if (codecQueryProcess.parsingTargetCard) {
if (line.startsWith("Active Profile:")) { if (line.startsWith("Active Profile:")) {
let profile = line.split(": ")[1] || "" let profile = line.split(": ")[1] || ""
let activeCodec = codecQueryProcess.availableCodecs.find((c) => { let activeCodec = codecQueryProcess.availableCodecs.find(c => {
return c.profile === profile return c.profile === profile
}) })
if (activeCodec) { if (activeCodec) {
codecQueryProcess.detectedCodec = activeCodec.name codecQueryProcess.detectedCodec = activeCodec.name
} }
@@ -352,16 +361,16 @@ Singleton {
let codecMatch = description.match(/codec ([^\)\s]+)/i) let codecMatch = description.match(/codec ([^\)\s]+)/i)
let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN" let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN"
let codecInfo = root.getCodecInfo(codecName) let codecInfo = root.getCodecInfo(codecName)
if (codecInfo && !codecQueryProcess.availableCodecs.some((c) => { if (codecInfo && !codecQueryProcess.availableCodecs.some(c => {
return c.profile === profile return c.profile === profile
})) { })) {
let newCodecs = codecQueryProcess.availableCodecs.slice() let newCodecs = codecQueryProcess.availableCodecs.slice()
newCodecs.push({ newCodecs.push({
"name": codecInfo.name, "name": codecInfo.name,
"profile": profile, "profile": profile,
"description": codecInfo.description, "description": codecInfo.description,
"qualityColor": codecInfo.qualityColor "qualityColor": codecInfo.qualityColor
}) })
codecQueryProcess.availableCodecs = newCodecs codecQueryProcess.availableCodecs = newCodecs
} }
} }
@@ -370,19 +379,19 @@ Singleton {
} }
} }
} }
Process { Process {
id: codecFullQueryProcess id: codecFullQueryProcess
property string cardName: "" property string cardName: ""
property var callback: null property var callback: null
property bool parsingTargetCard: false property bool parsingTargetCard: false
property string detectedCodec: "" property string detectedCodec: ""
property var availableCodecs: [] property var availableCodecs: []
command: ["pactl", "list", "cards"] command: ["pactl", "list", "cards"]
onExited: function(exitCode, exitStatus) { onExited: function (exitCode, exitStatus) {
if (callback) { if (callback) {
callback(exitCode === 0 ? availableCodecs : [], exitCode === 0 ? detectedCodec : "") callback(exitCode === 0 ? availableCodecs : [], exitCode === 0 ? detectedCodec : "")
} }
@@ -391,28 +400,28 @@ Singleton {
availableCodecs = [] availableCodecs = []
callback = null callback = null
} }
stdout: SplitParser { stdout: SplitParser {
splitMarker: "\n" splitMarker: "\n"
onRead: (data) => { onRead: data => {
let line = data.trim() let line = data.trim()
if (line.includes(`Name: ${codecFullQueryProcess.cardName}`)) { if (line.includes(`Name: ${codecFullQueryProcess.cardName}`)) {
codecFullQueryProcess.parsingTargetCard = true codecFullQueryProcess.parsingTargetCard = true
return return
} }
if (codecFullQueryProcess.parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecFullQueryProcess.cardName)) { if (codecFullQueryProcess.parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecFullQueryProcess.cardName)) {
codecFullQueryProcess.parsingTargetCard = false codecFullQueryProcess.parsingTargetCard = false
return return
} }
if (codecFullQueryProcess.parsingTargetCard) { if (codecFullQueryProcess.parsingTargetCard) {
if (line.startsWith("Active Profile:")) { if (line.startsWith("Active Profile:")) {
let profile = line.split(": ")[1] || "" let profile = line.split(": ")[1] || ""
let activeCodec = codecFullQueryProcess.availableCodecs.find((c) => { let activeCodec = codecFullQueryProcess.availableCodecs.find(c => {
return c.profile === profile return c.profile === profile
}) })
if (activeCodec) { if (activeCodec) {
codecFullQueryProcess.detectedCodec = activeCodec.name codecFullQueryProcess.detectedCodec = activeCodec.name
} }
@@ -426,16 +435,16 @@ Singleton {
let codecMatch = description.match(/codec ([^\)\s]+)/i) let codecMatch = description.match(/codec ([^\)\s]+)/i)
let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN" let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN"
let codecInfo = root.getCodecInfo(codecName) let codecInfo = root.getCodecInfo(codecName)
if (codecInfo && !codecFullQueryProcess.availableCodecs.some((c) => { if (codecInfo && !codecFullQueryProcess.availableCodecs.some(c => {
return c.profile === profile return c.profile === profile
})) { })) {
let newCodecs = codecFullQueryProcess.availableCodecs.slice() let newCodecs = codecFullQueryProcess.availableCodecs.slice()
newCodecs.push({ newCodecs.push({
"name": codecInfo.name, "name": codecInfo.name,
"profile": profile, "profile": profile,
"description": codecInfo.description, "description": codecInfo.description,
"qualityColor": codecInfo.qualityColor "qualityColor": codecInfo.qualityColor
}) })
codecFullQueryProcess.availableCodecs = newCodecs codecFullQueryProcess.availableCodecs = newCodecs
} }
} }
@@ -444,32 +453,32 @@ Singleton {
} }
} }
} }
Process { Process {
id: codecSwitchProcess id: codecSwitchProcess
property string cardName: "" property string cardName: ""
property string profile: "" property string profile: ""
property var callback: null property var callback: null
command: ["pactl", "set-card-profile", cardName, profile] command: ["pactl", "set-card-profile", cardName, profile]
onExited: function(exitCode, exitStatus) { onExited: function (exitCode, exitStatus) {
if (callback) { if (callback) {
callback(exitCode === 0, exitCode === 0 ? "Codec switched successfully" : "Failed to switch codec") callback(exitCode === 0, exitCode === 0 ? "Codec switched successfully" : "Failed to switch codec")
} }
// If successful, refresh the codec for this device // If successful, refresh the codec for this device
if (exitCode === 0) { if (exitCode === 0) {
if (root.adapter && root.adapter.devices) { if (root.adapter && root.adapter.devices) {
root.adapter.devices.values.forEach(device => { root.adapter.devices.values.forEach(device => {
if (device && root.getCardName(device) === cardName) { if (device && root.getCardName(device) === cardName) {
Qt.callLater(() => root.refreshDeviceCodec(device)) Qt.callLater(() => root.refreshDeviceCodec(device))
} }
}) })
} }
} }
callback = null callback = null
} }
} }

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -43,11 +44,9 @@ Singleton {
onRead: data => { onRead: data => {
if (root.refCount > 0 && data.trim()) { if (root.refCount > 0 && data.trim()) {
let points = data.split(";").map(p => { let points = data.split(";").map(p => {
return parseInt( return parseInt(p.trim(), 10)
p.trim(), 10)
}).filter(p => { }).filter(p => {
return !isNaN( return !isNaN(p)
p)
}) })
if (points.length >= 6) { if (points.length >= 6) {
root.values = points.slice(0, 6) root.values = points.slice(0, 6)

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -10,7 +11,6 @@ import Quickshell.Hyprland
Singleton { Singleton {
id: root id: root
// Compositor detection
property bool isHyprland: false property bool isHyprland: false
property bool isNiri: false property bool isNiri: false
property string compositor: "unknown" property string compositor: "unknown"
@@ -19,62 +19,64 @@ Singleton {
readonly property string niriSocket: Quickshell.env("NIRI_SOCKET") readonly property string niriSocket: Quickshell.env("NIRI_SOCKET")
property bool useNiriSorting: isNiri && NiriService property bool useNiriSorting: isNiri && NiriService
property bool useHyprlandSorting: false
property var sortedToplevels: { property var sortedToplevels: {
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values) { if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values) {
return [] return []
} }
// Only use niri sorting when both compositor is niri AND niri service is ready
if (useNiriSorting) { if (useNiriSorting) {
return NiriService.sortToplevels(ToplevelManager.toplevels.values) return NiriService.sortToplevels(ToplevelManager.toplevels.values)
} }
if (isHyprland) { if (isHyprland) {
const hyprlandToplevels = Array.from(Hyprland.toplevels.values) const hyprlandToplevels = Array.from(Hyprland.toplevels.values)
const sortedHyprland = hyprlandToplevels.sort((a, b) => { const sortedHyprland = hyprlandToplevels.sort((a, b) => {
// Sort by monitor first if (a.monitor && b.monitor) {
if (a.monitor && b.monitor) { const monitorCompare = a.monitor.name.localeCompare(b.monitor.name)
const monitorCompare = a.monitor.name.localeCompare(b.monitor.name) if (monitorCompare !== 0) {
if (monitorCompare !== 0) return monitorCompare return monitorCompare
} }
}
// Then by workspace
if (a.workspace && b.workspace) { if (a.workspace && b.workspace) {
const workspaceCompare = a.workspace.id - b.workspace.id const workspaceCompare = a.workspace.id - b.workspace.id
if (workspaceCompare !== 0) return workspaceCompare if (workspaceCompare !== 0) {
} return workspaceCompare
}
}
if (a.lastIpcObject && b.lastIpcObject && a.lastIpcObject.at && b.lastIpcObject.at) {
const aX = a.lastIpcObject.at[0] if (a.lastIpcObject && b.lastIpcObject && a.lastIpcObject.at && b.lastIpcObject.at) {
const bX = b.lastIpcObject.at[0] const aX = a.lastIpcObject.at[0]
const aY = a.lastIpcObject.at[1] const bX = b.lastIpcObject.at[0]
const bY = b.lastIpcObject.at[1] const aY = a.lastIpcObject.at[1]
const bY = b.lastIpcObject.at[1]
const xCompare = aX - bX
if (Math.abs(xCompare) > 10) return xCompare const xCompare = aX - bX
return aY - bY if (Math.abs(xCompare) > 10) {
} return xCompare
}
if (a.lastIpcObject && !b.lastIpcObject) return -1 return aY - bY
if (!a.lastIpcObject && b.lastIpcObject) return 1 }
if (a.title && b.title) { if (a.lastIpcObject && !b.lastIpcObject) {
return a.title.localeCompare(b.title) return -1
} }
if (!a.lastIpcObject && b.lastIpcObject) {
return 1
return 0 }
})
if (a.title && b.title) {
// Return the wayland Toplevel objects return a.title.localeCompare(b.title)
}
return 0
})
return sortedHyprland.map(hyprToplevel => hyprToplevel.wayland).filter(wayland => wayland !== null) return sortedHyprland.map(hyprToplevel => hyprToplevel.wayland).filter(wayland => wayland !== null)
} }
// For other compositors or when services aren't ready yet, return unsorted toplevels
return ToplevelManager.toplevels.values return ToplevelManager.toplevels.values
} }
@@ -82,7 +84,7 @@ Singleton {
detectCompositor() detectCompositor()
} }
function filterCurrentWorkspace(toplevels, screen){ function filterCurrentWorkspace(toplevels, screen) {
if (useNiriSorting) { if (useNiriSorting) {
return NiriService.filterCurrentWorkspace(toplevels, screen) return NiriService.filterCurrentWorkspace(toplevels, screen)
} }
@@ -97,11 +99,10 @@ Singleton {
return toplevels return toplevels
} }
var currentWorkspaceId = null let currentWorkspaceId = null
const hyprlandToplevels = Array.from(Hyprland.toplevels.values) const hyprlandToplevels = Array.from(Hyprland.toplevels.values)
for (var i = 0; i < hyprlandToplevels.length; i++) { for (const hyprToplevel of hyprlandToplevels) {
var hyprToplevel = hyprlandToplevels[i]
if (hyprToplevel.monitor && hyprToplevel.monitor.name === screenName && hyprToplevel.workspace) { if (hyprToplevel.monitor && hyprToplevel.monitor.name === screenName && hyprToplevel.workspace) {
if (hyprToplevel.activated) { if (hyprToplevel.activated) {
currentWorkspaceId = hyprToplevel.workspace.id currentWorkspaceId = hyprToplevel.workspace.id
@@ -115,8 +116,7 @@ Singleton {
if (currentWorkspaceId === null && Hyprland.workspaces) { if (currentWorkspaceId === null && Hyprland.workspaces) {
const workspaces = Array.from(Hyprland.workspaces.values) const workspaces = Array.from(Hyprland.workspaces.values)
for (var k = 0; k < workspaces.length; k++) { for (const workspace of workspaces) {
var workspace = workspaces[k]
if (workspace.monitor && workspace.monitor === screenName) { if (workspace.monitor && workspace.monitor === screenName) {
if (Hyprland.focusedWorkspace && workspace.id === Hyprland.focusedWorkspace.id) { if (Hyprland.focusedWorkspace && workspace.id === Hyprland.focusedWorkspace.id) {
currentWorkspaceId = workspace.id currentWorkspaceId = workspace.id
@@ -134,18 +134,16 @@ Singleton {
} }
return toplevels.filter(toplevel => { return toplevels.filter(toplevel => {
for (var j = 0; j < hyprlandToplevels.length; j++) { for (const hyprToplevel of hyprlandToplevels) {
var hyprToplevel = hyprlandToplevels[j] if (hyprToplevel.wayland === toplevel) {
if (hyprToplevel.wayland === toplevel) { return hyprToplevel.workspace && hyprToplevel.workspace.id === currentWorkspaceId
return hyprToplevel.workspace && hyprToplevel.workspace.id === currentWorkspaceId }
} }
} return false
return false })
})
} }
function detectCompositor() { function detectCompositor() {
// Check for Hyprland first
if (hyprlandSignature && hyprlandSignature.length > 0) { if (hyprlandSignature && hyprlandSignature.length > 0) {
isHyprland = true isHyprland = true
isNiri = false isNiri = false
@@ -154,12 +152,9 @@ Singleton {
return return
} }
// Check for Niri
if (niriSocket && niriSocket.length > 0) { if (niriSocket && niriSocket.length > 0) {
// Verify the socket actually exists
niriSocketCheck.running = true niriSocketCheck.running = true
} else { } else {
// No compositor detected, default to Niri
isHyprland = false isHyprland = false
isNiri = false isNiri = false
compositor = "unknown" compositor = "unknown"

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -94,8 +95,7 @@ Singleton {
// Increment reference count for this module // Increment reference count for this module
const currentCount = moduleRefCounts[module] || 0 const currentCount = moduleRefCounts[module] || 0
moduleRefCounts[module] = currentCount + 1 moduleRefCounts[module] = currentCount + 1
console.log("Adding ref for module:", module, "count:", console.log("Adding ref for module:", module, "count:", moduleRefCounts[module])
moduleRefCounts[module])
// Add to enabled modules if not already there // Add to enabled modules if not already there
if (enabledModules.indexOf(module) === -1) { if (enabledModules.indexOf(module) === -1) {
@@ -107,8 +107,7 @@ Singleton {
if (modulesChanged || refCount === 1) { if (modulesChanged || refCount === 1) {
enabledModules = enabledModules.slice() // Force property change enabledModules = enabledModules.slice() // Force property change
moduleRefCounts = Object.assign( moduleRefCounts = Object.assign({}, moduleRefCounts) // Force property change
{}, moduleRefCounts) // Force property change
updateAllStats() updateAllStats()
} else if (gpuPciIds.length > 0 && refCount > 0) { } else if (gpuPciIds.length > 0 && refCount > 0) {
// If we have GPU PCI IDs and active modules, make sure to update // If we have GPU PCI IDs and active modules, make sure to update
@@ -128,8 +127,7 @@ Singleton {
if (currentCount > 1) { if (currentCount > 1) {
// Decrement reference count // Decrement reference count
moduleRefCounts[module] = currentCount - 1 moduleRefCounts[module] = currentCount - 1
console.log("Removing ref for module:", module, "count:", console.log("Removing ref for module:", module, "count:", moduleRefCounts[module])
moduleRefCounts[module])
} else if (currentCount === 1) { } else if (currentCount === 1) {
// Remove completely when count reaches 0 // Remove completely when count reaches 0
delete moduleRefCounts[module] delete moduleRefCounts[module]
@@ -137,8 +135,7 @@ Singleton {
if (index > -1) { if (index > -1) {
enabledModules.splice(index, 1) enabledModules.splice(index, 1)
modulesChanged = true modulesChanged = true
console.log("Disabling module:", module, console.log("Disabling module:", module, "(no more refs)")
"(no more refs)")
} }
} }
} }
@@ -146,8 +143,7 @@ Singleton {
if (modulesChanged) { if (modulesChanged) {
enabledModules = enabledModules.slice() // Force property change enabledModules = enabledModules.slice() // Force property change
moduleRefCounts = Object.assign( moduleRefCounts = Object.assign({}, moduleRefCounts) // Force property change
{}, moduleRefCounts) // Force property change
// Clear cursor data when CPU or process modules are no longer active // Clear cursor data when CPU or process modules are no longer active
if (!enabledModules.includes("cpu")) { if (!enabledModules.includes("cpu")) {
@@ -174,8 +170,7 @@ Singleton {
gpuPciIds = gpuPciIds.concat([pciId]) gpuPciIds = gpuPciIds.concat([pciId])
} }
console.log("Adding GPU PCI ID ref:", pciId, "count:", console.log("Adding GPU PCI ID ref:", pciId, "count:", gpuPciIdRefCounts[pciId])
gpuPciIdRefCounts[pciId])
// Force property change notification // Force property change notification
gpuPciIdRefCounts = Object.assign({}, gpuPciIdRefCounts) gpuPciIdRefCounts = Object.assign({}, gpuPciIdRefCounts)
} }
@@ -185,8 +180,7 @@ Singleton {
if (currentCount > 1) { if (currentCount > 1) {
// Decrement reference count // Decrement reference count
gpuPciIdRefCounts[pciId] = currentCount - 1 gpuPciIdRefCounts[pciId] = currentCount - 1
console.log("Removing GPU PCI ID ref:", pciId, "count:", console.log("Removing GPU PCI ID ref:", pciId, "count:", gpuPciIdRefCounts[pciId])
gpuPciIdRefCounts[pciId])
} else if (currentCount === 1) { } else if (currentCount === 1) {
// Remove completely when count reaches 0 // Remove completely when count reaches 0
delete gpuPciIdRefCounts[pciId] delete gpuPciIdRefCounts[pciId]
@@ -271,12 +265,10 @@ Singleton {
} }
// Add cursor data if available for accurate CPU percentages // Add cursor data if available for accurate CPU percentages
if ((enabledModules.includes("cpu") || enabledModules.includes("all")) if ((enabledModules.includes("cpu") || enabledModules.includes("all")) && cpuCursor) {
&& cpuCursor) {
cmd.push("--cpu-cursor", cpuCursor) cmd.push("--cpu-cursor", cpuCursor)
} }
if ((enabledModules.includes("processes") || enabledModules.includes( if ((enabledModules.includes("processes") || enabledModules.includes("all")) && procCursor) {
"all")) && procCursor) {
cmd.push("--proc-cursor", procCursor) cmd.push("--proc-cursor", procCursor)
} }
@@ -284,8 +276,7 @@ Singleton {
cmd.push("--gpu-pci-ids", gpuPciIds.join(",")) cmd.push("--gpu-pci-ids", gpuPciIds.join(","))
} }
if (enabledModules.indexOf("processes") !== -1 if (enabledModules.indexOf("processes") !== -1 || enabledModules.indexOf("all") !== -1) {
|| enabledModules.indexOf("all") !== -1) {
cmd.push("--limit", "100") // Get more data for client sorting cmd.push("--limit", "100") // Get more data for client sorting
cmd.push("--sort", "cpu") // Always get CPU sorted data cmd.push("--sort", "cpu") // Always get CPU sorted data
if (noCpu) { if (noCpu) {
@@ -301,7 +292,6 @@ Singleton {
const cpu = data.cpu const cpu = data.cpu
cpuSampleCount++ cpuSampleCount++
// Use dgop CPU numbers directly without modification
cpuUsage = cpu.usage || 0 cpuUsage = cpu.usage || 0
cpuFrequency = cpu.frequency || 0 cpuFrequency = cpu.frequency || 0
cpuTemperature = cpu.temperature || 0 cpuTemperature = cpu.temperature || 0
@@ -310,7 +300,6 @@ Singleton {
perCoreCpuUsage = cpu.coreUsage || [] perCoreCpuUsage = cpu.coreUsage || []
addToHistory(cpuHistory, cpuUsage) addToHistory(cpuHistory, cpuUsage)
// Store the opaque cursor string for next sampling
if (cpu.cursor) { if (cpu.cursor) {
cpuCursor = cpu.cursor cpuCursor = cpu.cursor
} }
@@ -322,14 +311,12 @@ Singleton {
const availableKB = mem.available || 0 const availableKB = mem.available || 0
const freeKB = mem.free || 0 const freeKB = mem.free || 0
// Update MB properties
totalMemoryMB = totalKB / 1024 totalMemoryMB = totalKB / 1024
availableMemoryMB = availableKB / 1024 availableMemoryMB = availableKB / 1024
freeMemoryMB = freeKB / 1024 freeMemoryMB = freeKB / 1024
usedMemoryMB = totalMemoryMB - availableMemoryMB usedMemoryMB = totalMemoryMB - availableMemoryMB
memoryUsage = totalKB > 0 ? ((totalKB - availableKB) / totalKB) * 100 : 0 memoryUsage = totalKB > 0 ? ((totalKB - availableKB) / totalKB) * 100 : 0
// Update KB properties for compatibility
totalMemoryKB = totalKB totalMemoryKB = totalKB
usedMemoryKB = totalKB - availableKB usedMemoryKB = totalKB - availableKB
totalSwapKB = mem.swaptotal || 0 totalSwapKB = mem.swaptotal || 0
@@ -339,7 +326,6 @@ Singleton {
} }
if (data.network && Array.isArray(data.network)) { if (data.network && Array.isArray(data.network)) {
// Store raw network interface data
networkInterfaces = data.network networkInterfaces = data.network
let totalRx = 0 let totalRx = 0
@@ -365,7 +351,6 @@ Singleton {
} }
if (data.disk && Array.isArray(data.disk)) { if (data.disk && Array.isArray(data.disk)) {
// Store raw disk device data
diskDevices = data.disk diskDevices = data.disk
let totalRead = 0 let totalRead = 0
@@ -399,53 +384,40 @@ Singleton {
processSampleCount++ processSampleCount++
for (const proc of data.processes) { for (const proc of data.processes) {
// Only show CPU usage if we have had at least 2 samples (first sample is inaccurate)
const cpuUsage = processSampleCount >= 2 ? (proc.cpu || 0) : 0 const cpuUsage = processSampleCount >= 2 ? (proc.cpu || 0) : 0
newProcesses.push({ newProcesses.push({
"pid": proc.pid || 0, "pid": proc.pid || 0,
"ppid": proc.ppid || 0, "ppid": proc.ppid || 0,
"cpu": cpuUsage, "cpu": cpuUsage,
"memoryPercent": proc.memoryPercent "memoryPercent": proc.memoryPercent || proc.pssPercent || 0,
|| proc.pssPercent || 0, "memoryKB": proc.memoryKB || proc.pssKB || 0,
"memoryKB": proc.memoryKB
|| proc.pssKB || 0,
"command": proc.command || "", "command": proc.command || "",
"fullCommand": proc.fullCommand || "", "fullCommand": proc.fullCommand || "",
"displayName": (proc.command "displayName": (proc.command && proc.command.length > 15) ? proc.command.substring(0, 15) + "..." : (proc.command || "")
&& proc.command.length
> 15) ? proc.command.substring(
0,
15) + "..." : (proc.command || "")
}) })
} }
allProcesses = newProcesses allProcesses = newProcesses
applySorting() applySorting()
// Store the single opaque cursor string for the entire process list
if (data.cursor) { if (data.cursor) {
procCursor = data.cursor procCursor = data.cursor
} }
} }
// Handle both gpu and gpu-temp module data const gpuData = (data.gpu && data.gpu.gpus) || data.gpus
const gpuData = (data.gpu && data.gpu.gpus)
|| data.gpus // Handle both meta format and direct gpu command format
if (gpuData && Array.isArray(gpuData)) { if (gpuData && Array.isArray(gpuData)) {
// Check if this is temperature update data (has PCI IDs being monitored) // Check if this is temperature update data (has PCI IDs being monitored)
if (gpuPciIds.length > 0 && availableGpus if (gpuPciIds.length > 0 && availableGpus && availableGpus.length > 0) {
&& availableGpus.length > 0) {
// This is temperature data - merge with existing GPU metadata // This is temperature data - merge with existing GPU metadata
const updatedGpus = availableGpus.slice() const updatedGpus = availableGpus.slice()
for (var i = 0; i < updatedGpus.length; i++) { for (var i = 0; i < updatedGpus.length; i++) {
const existingGpu = updatedGpus[i] const existingGpu = updatedGpus[i]
const tempGpu = gpuData.find( const tempGpu = gpuData.find(g => g.pciId === existingGpu.pciId)
g => g.pciId === existingGpu.pciId)
// Only update temperature if this GPU's PCI ID is being monitored // Only update temperature if this GPU's PCI ID is being monitored
if (tempGpu && gpuPciIds.includes(existingGpu.pciId)) { if (tempGpu && gpuPciIds.includes(existingGpu.pciId)) {
updatedGpus[i] = Object.assign({}, existingGpu, { updatedGpus[i] = Object.assign({}, existingGpu, {
"temperature": tempGpu.temperature "temperature": tempGpu.temperature || 0
|| 0
}) })
} }
} }
@@ -454,8 +426,7 @@ Singleton {
// This is initial GPU metadata - set the full list // This is initial GPU metadata - set the full list
const gpuList = [] const gpuList = []
for (const gpu of gpuData) { for (const gpu of gpuData) {
let displayName = gpu.displayName || gpu.name let displayName = gpu.displayName || gpu.name || "Unknown GPU"
|| "Unknown GPU"
let fullName = gpu.fullName || gpu.name || "Unknown GPU" let fullName = gpu.fullName || gpu.name || "Unknown GPU"
gpuList.push({ gpuList.push({
@@ -501,24 +472,24 @@ Singleton {
function getProcessIcon(command) { function getProcessIcon(command) {
const cmd = command.toLowerCase() const cmd = command.toLowerCase()
if (cmd.includes("firefox") || cmd.includes("chrome") || if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes("browser") || cmd.includes("chromium")) {
cmd.includes("browser") || cmd.includes("chromium"))
return "web" return "web"
if (cmd.includes("code") || cmd.includes("editor") }
|| cmd.includes("vim")) if (cmd.includes("code") || cmd.includes("editor") || cmd.includes("vim")) {
return "code" return "code"
if (cmd.includes("terminal") || cmd.includes("bash") }
|| cmd.includes("zsh")) if (cmd.includes("terminal") || cmd.includes("bash") || cmd.includes("zsh")) {
return "terminal" return "terminal"
if (cmd.includes("music") || cmd.includes("audio") || cmd.includes( }
"spotify")) if (cmd.includes("music") || cmd.includes("audio") || cmd.includes("spotify")) {
return "music_note" return "music_note"
if (cmd.includes("video") || cmd.includes("vlc") || cmd.includes("mpv")) }
if (cmd.includes("video") || cmd.includes("vlc") || cmd.includes("mpv")) {
return "play_circle" return "play_circle"
if (cmd.includes("systemd") || cmd.includes("elogind") || }
cmd.includes("kernel") || cmd.includes("kthread") || if (cmd.includes("systemd") || cmd.includes("elogind") || cmd.includes("kernel") || cmd.includes("kthread") || cmd.includes("kworker")) {
cmd.includes("kworker"))
return "settings" return "settings"
}
return "memory" return "memory"
} }
@@ -528,22 +499,25 @@ Singleton {
function formatMemoryUsage(memoryKB) { function formatMemoryUsage(memoryKB) {
const mem = memoryKB || 0 const mem = memoryKB || 0
if (mem < 1024) if (mem < 1024) {
return mem.toFixed(0) + " KB" return mem.toFixed(0) + " KB"
else if (mem < 1024 * 1024) } else if (mem < 1024 * 1024) {
return (mem / 1024).toFixed(1) + " MB" return (mem / 1024).toFixed(1) + " MB"
else } else {
return (mem / (1024 * 1024)).toFixed(1) + " GB" return (mem / (1024 * 1024)).toFixed(1) + " GB"
}
} }
function formatSystemMemory(memoryKB) { function formatSystemMemory(memoryKB) {
const mem = memoryKB || 0 const mem = memoryKB || 0
if (mem === 0) if (mem === 0) {
return "--" return "--"
if (mem < 1024 * 1024) }
if (mem < 1024 * 1024) {
return (mem / 1024).toFixed(0) + " MB" return (mem / 1024).toFixed(0) + " MB"
else } else {
return (mem / (1024 * 1024)).toFixed(1) + " GB" return (mem / (1024 * 1024)).toFixed(1) + " GB"
}
} }
function killProcess(pid) { function killProcess(pid) {
@@ -560,8 +534,9 @@ Singleton {
} }
function applySorting() { function applySorting() {
if (!allProcesses || allProcesses.length === 0) if (!allProcesses || allProcesses.length === 0) {
return return
}
const sorted = allProcesses.slice() const sorted = allProcesses.slice()
sorted.sort((a, b) => { sorted.sort((a, b) => {
@@ -595,8 +570,7 @@ Singleton {
Timer { Timer {
id: updateTimer id: updateTimer
interval: root.updateInterval interval: root.updateInterval
running: root.dgopAvailable && root.refCount > 0 running: root.dgopAvailable && root.refCount > 0 && root.enabledModules.length > 0
&& root.enabledModules.length > 0
repeat: true repeat: true
triggeredOnStart: true triggeredOnStart: true
onTriggered: root.updateAllStats() onTriggered: root.updateAllStats()
@@ -611,12 +585,11 @@ Singleton {
//console.log("DgopService command:", JSON.stringify(command)) //console.log("DgopService command:", JSON.stringify(command))
} }
onExited: exitCode => { onExited: exitCode => {
if (exitCode !== 0) { if (exitCode !== 0) {
console.warn("Dgop process failed with exit code:", console.warn("Dgop process failed with exit code:", exitCode)
exitCode) isUpdating = false
isUpdating = false }
} }
}
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (text.trim()) { if (text.trim()) {
@@ -638,12 +611,10 @@ Singleton {
command: ["dgop", "gpu", "--json"] command: ["dgop", "gpu", "--json"]
running: false running: false
onExited: exitCode => { onExited: exitCode => {
if (exitCode !== 0) { if (exitCode !== 0) {
console.warn( console.warn("GPU init process failed with exit code:", exitCode)
"GPU init process failed with exit code:", }
exitCode) }
}
}
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (text.trim()) { if (text.trim()) {
@@ -663,24 +634,23 @@ Singleton {
command: ["which", "dgop"] command: ["which", "dgop"]
running: false running: false
onExited: exitCode => { onExited: exitCode => {
dgopAvailable = (exitCode === 0) dgopAvailable = (exitCode === 0)
if (dgopAvailable) { if (dgopAvailable) {
initializeGpuMetadata() initializeGpuMetadata()
// Load persisted GPU PCI IDs from session state // Load persisted GPU PCI IDs from session state
if (SessionData.enabledGpuPciIds if (SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.length > 0) {
&& SessionData.enabledGpuPciIds.length > 0) { for (const pciId of SessionData.enabledGpuPciIds) {
for (const pciId of SessionData.enabledGpuPciIds) { addGpuPciId(pciId)
addGpuPciId(pciId) }
} // Trigger update if we already have active modules
// Trigger update if we already have active modules if (refCount > 0 && enabledModules.length > 0) {
if (refCount > 0 && enabledModules.length > 0) { updateAllStats()
updateAllStats() }
} }
} } else {
} else { console.warn("dgop is not installed or not in PATH")
console.warn("dgop is not installed or not in PATH") }
} }
}
} }
Process { Process {
@@ -688,10 +658,10 @@ Singleton {
command: ["cat", "/etc/os-release"] command: ["cat", "/etc/os-release"]
running: false running: false
onExited: exitCode => { onExited: exitCode => {
if (exitCode !== 0) { if (exitCode !== 0) {
console.warn("Failed to read /etc/os-release") console.warn("Failed to read /etc/os-release")
} }
} }
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (text.trim()) { if (text.trim()) {
@@ -703,11 +673,9 @@ Singleton {
for (const line of lines) { for (const line of lines) {
const trimmedLine = line.trim() const trimmedLine = line.trim()
if (trimmedLine.startsWith('PRETTY_NAME=')) { if (trimmedLine.startsWith('PRETTY_NAME=')) {
prettyName = trimmedLine.substring(12).replace( prettyName = trimmedLine.substring(12).replace(/^["']|["']$/g, '')
/^["']|["']$/g, '')
} else if (trimmedLine.startsWith('NAME=')) { } else if (trimmedLine.startsWith('NAME=')) {
name = trimmedLine.substring(5).replace( name = trimmedLine.substring(5).replace(/^["']|["']$/g, '')
/^["']|["']$/g, '')
} }
} }

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -21,9 +22,10 @@ Singleton {
property bool skipDdcRead: false property bool skipDdcRead: false
property int brightnessLevel: { property int brightnessLevel: {
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice) const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
if (!deviceToUse) return 50 if (!deviceToUse) {
return 50
}
// Always use cached values for consistency
return getDeviceBrightness(deviceToUse) return getDeviceBrightness(deviceToUse)
} }
property int maxBrightness: 100 property int maxBrightness: 100
@@ -34,7 +36,6 @@ Singleton {
property bool nightModeActive: nightModeEnabled property bool nightModeActive: nightModeEnabled
// Night Mode Properties
property bool nightModeEnabled: false property bool nightModeEnabled: false
property bool automationAvailable: false property bool automationAvailable: false
property bool geoclueAvailable: false property bool geoclueAvailable: false
@@ -47,32 +48,25 @@ Singleton {
function setBrightnessInternal(percentage, device) { function setBrightnessInternal(percentage, device) {
const clampedValue = Math.max(1, Math.min(100, percentage)) const clampedValue = Math.max(1, Math.min(100, percentage))
const actualDevice = device === "" ? getDefaultDevice( const actualDevice = device === "" ? getDefaultDevice() : (device || currentDevice || getDefaultDevice())
) : (device || currentDevice
|| getDefaultDevice())
// Update the device brightness cache immediately for all devices
if (actualDevice) { if (actualDevice) {
var newBrightness = Object.assign({}, deviceBrightness) const newBrightness = Object.assign({}, deviceBrightness)
newBrightness[actualDevice] = clampedValue newBrightness[actualDevice] = clampedValue
deviceBrightness = newBrightness deviceBrightness = newBrightness
} }
const deviceInfo = getCurrentDeviceInfoByName(actualDevice) const deviceInfo = getCurrentDeviceInfoByName(actualDevice)
if (deviceInfo && deviceInfo.class === "ddc") { if (deviceInfo && deviceInfo.class === "ddc") {
// Use ddcutil for DDC devices ddcBrightnessSetProcess.command = ["ddcutil", "setvcp", "-d", String(deviceInfo.ddcDisplay), "10", String(clampedValue)]
ddcBrightnessSetProcess.command = ["ddcutil", "setvcp", "-d", String(
deviceInfo.ddcDisplay), "10", String(
clampedValue)]
ddcBrightnessSetProcess.running = true ddcBrightnessSetProcess.running = true
} else { } else {
// Use brightnessctl for regular devices if (device) {
if (device) brightnessSetProcess.command = ["brightnessctl", "-d", device, "set", `${clampedValue}%`]
brightnessSetProcess.command } else {
= ["brightnessctl", "-d", device, "set", clampedValue + "%"] brightnessSetProcess.command = ["brightnessctl", "set", `${clampedValue}%`]
else }
brightnessSetProcess.command = ["brightnessctl", "set", clampedValue + "%"]
brightnessSetProcess.running = true brightnessSetProcess.running = true
} }
} }
@@ -83,26 +77,23 @@ Singleton {
} }
function setCurrentDevice(deviceName, saveToSession = false) { function setCurrentDevice(deviceName, saveToSession = false) {
if (currentDevice === deviceName) if (currentDevice === deviceName) {
return return
}
currentDevice = deviceName currentDevice = deviceName
lastIpcDevice = deviceName lastIpcDevice = deviceName
// Only save to session if explicitly requested (user choice)
if (saveToSession) { if (saveToSession) {
SessionData.setLastBrightnessDevice(deviceName) SessionData.setLastBrightnessDevice(deviceName)
} }
deviceSwitched() deviceSwitched()
// Check if this is a DDC device
const deviceInfo = getCurrentDeviceInfoByName(deviceName) const deviceInfo = getCurrentDeviceInfoByName(deviceName)
if (deviceInfo && deviceInfo.class === "ddc") { if (deviceInfo && deviceInfo.class === "ddc") {
// For DDC devices, never read after initial - just use cached values
return return
} else { } else {
// For regular devices, use brightnessctl
brightnessGetProcess.command = ["brightnessctl", "-m", "-d", deviceName, "get"] brightnessGetProcess.command = ["brightnessctl", "-m", "-d", deviceName, "get"]
brightnessGetProcess.running = true brightnessGetProcess.running = true
} }
@@ -116,19 +107,19 @@ Singleton {
const allDevices = [...devices, ...ddcDevices] const allDevices = [...devices, ...ddcDevices]
allDevices.sort((a, b) => { allDevices.sort((a, b) => {
if (a.class === "backlight" if (a.class === "backlight" && b.class !== "backlight") {
&& b.class !== "backlight") return -1
return -1 }
if (a.class !== "backlight" if (a.class !== "backlight" && b.class === "backlight") {
&& b.class === "backlight") return 1
return 1 }
if (a.class === "ddc" && b.class !== "ddc" if (a.class === "ddc" && b.class !== "ddc" && b.class !== "backlight") {
&& b.class !== "backlight") return -1
return -1 }
if (a.class !== "ddc" && b.class === "ddc" if (a.class !== "ddc" && b.class === "ddc" && a.class !== "backlight") {
&& a.class !== "backlight") return 1
return 1 }
return a.name.localeCompare(b.name) return a.name.localeCompare(b.name)
}) })
@@ -141,25 +132,26 @@ Singleton {
if (deviceExists) { if (deviceExists) {
setCurrentDevice(lastDevice, false) setCurrentDevice(lastDevice, false)
} else { } else {
const nonKbdDevice = devices.find(d => !d.name.includes("kbd")) const nonKbdDevice = devices.find(d => !d.name.includes("kbd")) || devices[0]
|| devices[0]
setCurrentDevice(nonKbdDevice.name, false) setCurrentDevice(nonKbdDevice.name, false)
} }
} }
} }
function getDeviceBrightness(deviceName) { function getDeviceBrightness(deviceName) {
if (!deviceName) return 50 if (!deviceName) {
return
} 50
const deviceInfo = getCurrentDeviceInfoByName(deviceName) const deviceInfo = getCurrentDeviceInfoByName(deviceName)
if (!deviceInfo) return 50 if (!deviceInfo) {
return 50
// For DDC devices, always use cached values }
if (deviceInfo.class === "ddc") { if (deviceInfo.class === "ddc") {
return deviceBrightness[deviceName] || 50 return deviceBrightness[deviceName] || 50
} }
// For regular devices, try cache first, then device info
return deviceBrightness[deviceName] || deviceInfo.percentage || 50 return deviceBrightness[deviceName] || deviceInfo.percentage || 50
} }
@@ -173,11 +165,10 @@ Singleton {
} }
function getCurrentDeviceInfo() { function getCurrentDeviceInfo() {
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice( const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
) : (lastIpcDevice if (!deviceToUse) {
|| currentDevice)
if (!deviceToUse)
return null return null
}
for (const device of devices) { for (const device of devices) {
if (device.name === deviceToUse) { if (device.name === deviceToUse) {
@@ -188,11 +179,10 @@ Singleton {
} }
function isCurrentDeviceReady() { function isCurrentDeviceReady() {
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice( const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
) : (lastIpcDevice if (!deviceToUse) {
|| currentDevice)
if (!deviceToUse)
return false return false
}
if (ddcPendingInit[deviceToUse]) { if (ddcPendingInit[deviceToUse]) {
return false return false
@@ -202,8 +192,9 @@ Singleton {
} }
function getCurrentDeviceInfoByName(deviceName) { function getCurrentDeviceInfoByName(deviceName) {
if (!deviceName) if (!deviceName) {
return null return null
}
for (const device of devices) { for (const device of devices) {
if (device.name === deviceName) { if (device.name === deviceName) {
@@ -219,8 +210,7 @@ Singleton {
} }
const displayId = ddcInitQueue.shift() const displayId = ddcInitQueue.shift()
ddcInitialBrightnessProcess.command = ["ddcutil", "getvcp", "-d", String( ddcInitialBrightnessProcess.command = ["ddcutil", "getvcp", "-d", String(displayId), "10", "--brief"]
displayId), "10", "--brief"]
ddcInitialBrightnessProcess.running = true ddcInitialBrightnessProcess.running = true
} }
@@ -233,7 +223,7 @@ Singleton {
nightModeEnabled = true nightModeEnabled = true
SessionData.setNightModeEnabled(true) SessionData.setNightModeEnabled(true)
// Apply immediately or start automation // Apply immediately or start automation
if (SessionData.nightModeAutoEnabled) { if (SessionData.nightModeAutoEnabled) {
startAutomation() startAutomation()
@@ -266,10 +256,7 @@ Singleton {
function applyNightModeDirectly() { function applyNightModeDirectly() {
const temperature = SessionData.nightModeTemperature || 4500 const temperature = SessionData.nightModeTemperature || 4500
gammaStepProcess.command = buildGammastepCommand([ gammaStepProcess.command = buildGammastepCommand(["-m", "wayland", "-O", String(temperature)])
"-m", "wayland",
"-O", String(temperature)
])
gammaStepProcess.running = true gammaStepProcess.running = true
} }
@@ -279,17 +266,19 @@ Singleton {
} }
function startAutomation() { function startAutomation() {
if (!automationAvailable) return if (!automationAvailable) {
return
}
const mode = SessionData.nightModeAutoMode || "time" const mode = SessionData.nightModeAutoMode || "time"
switch (mode) { switch (mode) {
case "time": case "time":
startTimeBasedMode() startTimeBasedMode()
break break
case "location": case "location":
startLocationBasedMode() startLocationBasedMode()
break break
} }
} }
@@ -310,29 +299,19 @@ Singleton {
function startLocationBasedMode() { function startLocationBasedMode() {
const temperature = SessionData.nightModeTemperature || 4500 const temperature = SessionData.nightModeTemperature || 4500
const dayTemp = 6500 const dayTemp = 6500
if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) { if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
automationProcess.command = buildGammastepCommand([ automationProcess.command = buildGammastepCommand(["-m", "wayland", "-l", `${SessionData.latitude.toFixed(6)}:${SessionData.longitude.toFixed(6)}`, "-t", `${dayTemp}:${temperature}`, "-v"])
"-m", "wayland",
"-l", `${SessionData.latitude.toFixed(6)}:${SessionData.longitude.toFixed(6)}`,
"-t", `${dayTemp}:${temperature}`,
"-v"
])
automationProcess.running = true automationProcess.running = true
return return
} }
if (SessionData.nightModeLocationProvider === "geoclue2") { if (SessionData.nightModeLocationProvider === "geoclue2") {
automationProcess.command = buildGammastepCommand([ automationProcess.command = buildGammastepCommand(["-m", "wayland", "-l", "geoclue2", "-t", `${dayTemp}:${temperature}`, "-v"])
"-m", "wayland",
"-l", "geoclue2",
"-t", `${dayTemp}:${temperature}`,
"-v"
])
automationProcess.running = true automationProcess.running = true
return return
} }
console.warn("DisplayService: Location mode selected but no coordinates or geoclue provider set") console.warn("DisplayService: Location mode selected but no coordinates or geoclue provider set")
} }
@@ -347,7 +326,7 @@ Singleton {
const endMinutes = SessionData.nightModeEndHour * 60 + SessionData.nightModeEndMinute const endMinutes = SessionData.nightModeEndHour * 60 + SessionData.nightModeEndMinute
let shouldBeNight = false let shouldBeNight = false
if (startMinutes > endMinutes) { if (startMinutes > endMinutes) {
shouldBeNight = (currentTime >= startMinutes) || (currentTime < endMinutes) shouldBeNight = (currentTime >= startMinutes) || (currentTime < endMinutes)
} else { } else {
@@ -356,7 +335,7 @@ Singleton {
if (shouldBeNight !== isAutomaticNightTime) { if (shouldBeNight !== isAutomaticNightTime) {
isAutomaticNightTime = shouldBeNight isAutomaticNightTime = shouldBeNight
if (shouldBeNight) { if (shouldBeNight) {
applyNightModeDirectly() applyNightModeDirectly()
} else { } else {
@@ -373,14 +352,14 @@ Singleton {
SessionData.setNightModeAutoMode(mode) SessionData.setNightModeAutoMode(mode)
} }
function evaluateNightMode() { function evaluateNightMode() {
// Always stop all processes first to clean slate // Always stop all processes first to clean slate
stopAutomation() stopAutomation()
if (!nightModeEnabled) { if (!nightModeEnabled) {
return return
} }
if (SessionData.nightModeAutoEnabled) { if (SessionData.nightModeAutoEnabled) {
restartTimer.nextAction = "automation" restartTimer.nextAction = "automation"
restartTimer.start() restartTimer.start()
@@ -399,7 +378,7 @@ Singleton {
property string nextAction: "" property string nextAction: ""
interval: 100 interval: 100
repeat: false repeat: false
onTriggered: { onTriggered: {
if (nextAction === "automation") { if (nextAction === "automation") {
startAutomation() startAutomation()
@@ -476,8 +455,7 @@ Singleton {
} }
ddcDevices = newDdcDevices ddcDevices = newDdcDevices
console.log("DisplayService: Found", ddcDevices.length, console.log("DisplayService: Found", ddcDevices.length, "DDC displays")
"DDC displays")
// Queue initial brightness readings for DDC devices // Queue initial brightness readings for DDC devices
ddcInitQueue = [] ddcInitQueue = []
@@ -496,16 +474,13 @@ Singleton {
// Retry setting last device now that DDC devices are available // Retry setting last device now that DDC devices are available
const lastDevice = SessionData.lastBrightnessDevice || "" const lastDevice = SessionData.lastBrightnessDevice || ""
if (lastDevice) { if (lastDevice) {
const deviceExists = devices.some( const deviceExists = devices.some(d => d.name === lastDevice)
d => d.name === lastDevice) if (deviceExists && (!currentDevice || currentDevice !== lastDevice)) {
if (deviceExists && (!currentDevice
|| currentDevice !== lastDevice)) {
setCurrentDevice(lastDevice, false) setCurrentDevice(lastDevice, false)
} }
} }
} catch (error) { } catch (error) {
console.warn("DisplayService: Failed to parse DDC devices:", console.warn("DisplayService: Failed to parse DDC devices:", error)
error)
ddcDevices = [] ddcDevices = []
} }
} }
@@ -513,8 +488,7 @@ Singleton {
onExited: function (exitCode) { onExited: function (exitCode) {
if (exitCode !== 0) { if (exitCode !== 0) {
console.warn("DisplayService: Failed to detect DDC displays:", console.warn("DisplayService: Failed to detect DDC displays:", exitCode)
exitCode)
ddcDevices = [] ddcDevices = []
} }
} }
@@ -526,8 +500,7 @@ Singleton {
command: ["brightnessctl", "-m", "-l"] command: ["brightnessctl", "-m", "-l"]
onExited: function (exitCode) { onExited: function (exitCode) {
if (exitCode !== 0) { if (exitCode !== 0) {
console.warn("DisplayService: Failed to list devices:", console.warn("DisplayService: Failed to list devices:", exitCode)
exitCode)
brightnessAvailable = false brightnessAvailable = false
} }
} }
@@ -542,7 +515,7 @@ Singleton {
const newDevices = [] const newDevices = []
for (const line of lines) { for (const line of lines) {
const parts = line.split(",") const parts = line.split(",")
if (parts.length >= 5) if (parts.length >= 5) {
newDevices.push({ newDevices.push({
"name": parts[0], "name": parts[0],
"class": parts[1], "class": parts[1],
@@ -550,10 +523,11 @@ Singleton {
"percentage": parseInt(parts[3]), "percentage": parseInt(parts[3]),
"max": parseInt(parts[4]) "max": parseInt(parts[4])
}) })
}
} }
// Store brightnessctl devices separately // Store brightnessctl devices separately
devices = newDevices devices = newDevices
// Always refresh to combine with DDC devices and set up device selection // Always refresh to combine with DDC devices and set up device selection
refreshDevicesInternal() refreshDevicesInternal()
} }
@@ -565,9 +539,9 @@ Singleton {
running: false running: false
onExited: function (exitCode) { onExited: function (exitCode) {
if (exitCode !== 0) if (exitCode !== 0) {
console.warn("DisplayService: Failed to set brightness:", console.warn("DisplayService: Failed to set brightness:", exitCode)
exitCode) }
} }
} }
@@ -576,10 +550,9 @@ Singleton {
running: false running: false
onExited: function (exitCode) { onExited: function (exitCode) {
if (exitCode !== 0) if (exitCode !== 0) {
console.warn( console.warn("DisplayService: Failed to set DDC brightness:", exitCode)
"DisplayService: Failed to set DDC brightness:", }
exitCode)
} }
} }
@@ -588,9 +561,9 @@ Singleton {
running: false running: false
onExited: function (exitCode) { onExited: function (exitCode) {
if (exitCode !== 0) if (exitCode !== 0) {
console.warn("DisplayService: Failed to get initial DDC brightness:", console.warn("DisplayService: Failed to get initial DDC brightness:", exitCode)
exitCode) }
processNextDdcInit() processNextDdcInit()
} }
@@ -598,7 +571,7 @@ Singleton {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (!text.trim()) if (!text.trim())
return return
const parts = text.trim().split(" ") const parts = text.trim().split(" ")
if (parts.length >= 5) { if (parts.length >= 5) {
@@ -619,8 +592,7 @@ Singleton {
delete newPending[deviceName] delete newPending[deviceName]
ddcPendingInit = newPending ddcPendingInit = newPending
console.log("DisplayService: Initial DDC Device", console.log("DisplayService: Initial DDC Device", deviceName, "brightness:", brightness + "%")
deviceName, "brightness:", brightness + "%")
} }
} }
} }
@@ -632,15 +604,15 @@ Singleton {
running: false running: false
onExited: function (exitCode) { onExited: function (exitCode) {
if (exitCode !== 0) if (exitCode !== 0) {
console.warn("DisplayService: Failed to get brightness:", console.warn("DisplayService: Failed to get brightness:", exitCode)
exitCode) }
} }
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (!text.trim()) if (!text.trim())
return return
const parts = text.trim().split(",") const parts = text.trim().split(",")
if (parts.length >= 5) { if (parts.length >= 5) {
@@ -657,8 +629,7 @@ Singleton {
} }
brightnessInitialized = true brightnessInitialized = true
console.log("DisplayService: Device", currentDevice, console.log("DisplayService: Device", currentDevice, "brightness:", brightness + "%")
"brightness:", brightness + "%")
brightnessChanged() brightnessChanged()
} }
} }
@@ -670,16 +641,15 @@ Singleton {
running: false running: false
onExited: function (exitCode) { onExited: function (exitCode) {
if (exitCode !== 0) if (exitCode !== 0) {
console.warn( console.warn("DisplayService: Failed to get DDC brightness:", exitCode)
"DisplayService: Failed to get DDC brightness:", }
exitCode)
} }
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (!text.trim()) if (!text.trim())
return return
// Parse ddcutil getvcp output format: "VCP 10 C 50 100" // Parse ddcutil getvcp output format: "VCP 10 C 50 100"
const parts = text.trim().split(" ") const parts = text.trim().split(" ")
@@ -697,8 +667,7 @@ Singleton {
} }
brightnessInitialized = true brightnessInitialized = true
console.log("DisplayService: DDC Device", currentDevice, console.log("DisplayService: DDC Device", currentDevice, "brightness:", brightness + "%")
"brightness:", brightness + "%")
brightnessChanged() brightnessChanged()
} }
} }
@@ -709,12 +678,12 @@ Singleton {
id: gammastepAvailabilityProcess id: gammastepAvailabilityProcess
command: ["which", "gammastep"] command: ["which", "gammastep"]
running: false running: false
onExited: function(exitCode) { onExited: function (exitCode) {
automationAvailable = (exitCode === 0) automationAvailable = (exitCode === 0)
if (automationAvailable) { if (automationAvailable) {
detectLocationProviders() detectLocationProviders()
// If night mode should be enabled on startup // If night mode should be enabled on startup
if (nightModeEnabled && SessionData.nightModeAutoEnabled) { if (nightModeEnabled && SessionData.nightModeAutoEnabled) {
startAutomation() startAutomation()
@@ -748,7 +717,7 @@ Singleton {
automationAvailable = true automationAvailable = true
nightModeEnabled = true nightModeEnabled = true
SessionData.setNightModeEnabled(true) SessionData.setNightModeEnabled(true)
if (SessionData.nightModeAutoEnabled) { if (SessionData.nightModeAutoEnabled) {
startAutomation() startAutomation()
} else { } else {
@@ -789,121 +758,149 @@ Singleton {
// Session Data Connections // Session Data Connections
Connections { Connections {
target: SessionData target: SessionData
function onNightModeEnabledChanged() { function onNightModeEnabledChanged() {
nightModeEnabled = SessionData.nightModeEnabled nightModeEnabled = SessionData.nightModeEnabled
evaluateNightMode() evaluateNightMode()
} }
function onNightModeAutoEnabledChanged() { evaluateNightMode() } function onNightModeAutoEnabledChanged() {
function onNightModeAutoModeChanged() { evaluateNightMode() } evaluateNightMode()
function onNightModeStartHourChanged() { evaluateNightMode() } }
function onNightModeStartMinuteChanged() { evaluateNightMode() } function onNightModeAutoModeChanged() {
function onNightModeEndHourChanged() { evaluateNightMode() } evaluateNightMode()
function onNightModeEndMinuteChanged() { evaluateNightMode() } }
function onNightModeTemperatureChanged() { evaluateNightMode() } function onNightModeStartHourChanged() {
function onLatitudeChanged() { evaluateNightMode() } evaluateNightMode()
function onLongitudeChanged() { evaluateNightMode() } }
function onNightModeLocationProviderChanged() { evaluateNightMode() } function onNightModeStartMinuteChanged() {
evaluateNightMode()
}
function onNightModeEndHourChanged() {
evaluateNightMode()
}
function onNightModeEndMinuteChanged() {
evaluateNightMode()
}
function onNightModeTemperatureChanged() {
evaluateNightMode()
}
function onLatitudeChanged() {
evaluateNightMode()
}
function onLongitudeChanged() {
evaluateNightMode()
}
function onNightModeLocationProviderChanged() {
evaluateNightMode()
}
} }
// IPC Handler for external control // IPC Handler for external control
IpcHandler { IpcHandler {
function set(percentage: string, device: string): string { function set(percentage: string, device: string): string {
if (!root.brightnessAvailable) if (!root.brightnessAvailable) {
return "Brightness control not available" return "Brightness control not available"
}
const value = parseInt(percentage) const value = parseInt(percentage)
if (isNaN(value)) { if (isNaN(value)) {
return "Invalid brightness value: " + percentage return "Invalid brightness value: " + percentage
} }
const clampedValue = Math.max(1, Math.min(100, value)) const clampedValue = Math.max(1, Math.min(100, value))
const targetDevice = device || "" const targetDevice = device || ""
// Ensure device exists if specified // Ensure device exists if specified
if (targetDevice && !root.devices.some(d => d.name === targetDevice)) { if (targetDevice && !root.devices.some(d => d.name === targetDevice)) {
return "Device not found: " + targetDevice return "Device not found: " + targetDevice
} }
root.lastIpcDevice = targetDevice root.lastIpcDevice = targetDevice
if (targetDevice && targetDevice !== root.currentDevice) { if (targetDevice && targetDevice !== root.currentDevice) {
root.setCurrentDevice(targetDevice, false) root.setCurrentDevice(targetDevice, false)
} }
root.setBrightness(clampedValue, targetDevice) root.setBrightness(clampedValue, targetDevice)
if (targetDevice) if (targetDevice) {
return "Brightness set to " + clampedValue + "% on " + targetDevice return "Brightness set to " + clampedValue + "% on " + targetDevice
else } else {
return "Brightness set to " + clampedValue + "%" return "Brightness set to " + clampedValue + "%"
}
} }
function increment(step: string, device: string): string { function increment(step: string, device: string): string {
if (!root.brightnessAvailable) if (!root.brightnessAvailable) {
return "Brightness control not available" return "Brightness control not available"
}
const targetDevice = device || "" const targetDevice = device || ""
const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice
// Ensure device exists // Ensure device exists
if (actualDevice && !root.devices.some(d => d.name === actualDevice)) { if (actualDevice && !root.devices.some(d => d.name === actualDevice)) {
return "Device not found: " + actualDevice return "Device not found: " + actualDevice
} }
const currentLevel = actualDevice ? root.getDeviceBrightness(actualDevice) : root.brightnessLevel const currentLevel = actualDevice ? root.getDeviceBrightness(actualDevice) : root.brightnessLevel
const stepValue = parseInt(step || "10") const stepValue = parseInt(step || "10")
const newLevel = Math.max(1, Math.min(100, currentLevel + stepValue)) const newLevel = Math.max(1, Math.min(100, currentLevel + stepValue))
root.lastIpcDevice = targetDevice root.lastIpcDevice = targetDevice
if (targetDevice && targetDevice !== root.currentDevice) { if (targetDevice && targetDevice !== root.currentDevice) {
root.setCurrentDevice(targetDevice, false) root.setCurrentDevice(targetDevice, false)
} }
root.setBrightness(newLevel, targetDevice) root.setBrightness(newLevel, targetDevice)
if (targetDevice) if (targetDevice) {
return "Brightness increased to " + newLevel + "% on " + targetDevice return "Brightness increased to " + newLevel + "% on " + targetDevice
else } else {
return "Brightness increased to " + newLevel + "%" return "Brightness increased to " + newLevel + "%"
}
} }
function decrement(step: string, device: string): string { function decrement(step: string, device: string): string {
if (!root.brightnessAvailable) if (!root.brightnessAvailable) {
return "Brightness control not available" return "Brightness control not available"
}
const targetDevice = device || "" const targetDevice = device || ""
const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice
// Ensure device exists // Ensure device exists
if (actualDevice && !root.devices.some(d => d.name === actualDevice)) { if (actualDevice && !root.devices.some(d => d.name === actualDevice)) {
return "Device not found: " + actualDevice return "Device not found: " + actualDevice
} }
const currentLevel = actualDevice ? root.getDeviceBrightness(actualDevice) : root.brightnessLevel const currentLevel = actualDevice ? root.getDeviceBrightness(actualDevice) : root.brightnessLevel
const stepValue = parseInt(step || "10") const stepValue = parseInt(step || "10")
const newLevel = Math.max(1, Math.min(100, currentLevel - stepValue)) const newLevel = Math.max(1, Math.min(100, currentLevel - stepValue))
root.lastIpcDevice = targetDevice root.lastIpcDevice = targetDevice
if (targetDevice && targetDevice !== root.currentDevice) { if (targetDevice && targetDevice !== root.currentDevice) {
root.setCurrentDevice(targetDevice, false) root.setCurrentDevice(targetDevice, false)
} }
root.setBrightness(newLevel, targetDevice) root.setBrightness(newLevel, targetDevice)
if (targetDevice) if (targetDevice) {
return "Brightness decreased to " + newLevel + "% on " + targetDevice return "Brightness decreased to " + newLevel + "% on " + targetDevice
else } else {
return "Brightness decreased to " + newLevel + "%" return "Brightness decreased to " + newLevel + "%"
}
} }
function status(): string { function status(): string {
if (!root.brightnessAvailable) if (!root.brightnessAvailable) {
return "Brightness control not available" return "Brightness control not available"
}
return "Device: " + root.currentDevice + " - Brightness: " + root.brightnessLevel + "%" return "Device: " + root.currentDevice + " - Brightness: " + root.brightnessLevel + "%"
} }
function list(): string { function list(): string {
if (!root.brightnessAvailable) if (!root.brightnessAvailable) {
return "No brightness devices available" return "No brightness devices available"
}
let result = "Available devices:\\n" let result = "Available devices:\\n"
for (const device of root.devices) { for (const device of root.devices) {
@@ -974,4 +971,4 @@ Singleton {
target: "night" target: "night"
} }
} }

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell

View File

@@ -1,56 +1,60 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import Quickshell.Widgets
Singleton { Singleton {
id: root id: root
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying) property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null
?? availablePlayers.find(
p => p.canControl
&& p.canPlay) ?? null
IpcHandler { IpcHandler {
target: "mpris" target: "mpris"
function list(): string { function list(): string {
return root.availablePlayers.map(p => p.identity).join("") return root.availablePlayers.map(p => p.identity).join("\n")
} }
function play(): void { function play(): void {
if (root.activePlayer?.canPlay) if (root.activePlayer && root.activePlayer.canPlay) {
root.activePlayer.play() root.activePlayer.play()
}
} }
function pause(): void { function pause(): void {
if (root.activePlayer?.canPause) if (root.activePlayer && root.activePlayer.canPause) {
root.activePlayer.pause() root.activePlayer.pause()
}
} }
function playPause(): void { function playPause(): void {
if (root.activePlayer?.canTogglePlaying) if (root.activePlayer && root.activePlayer.canTogglePlaying) {
root.activePlayer.togglePlaying() root.activePlayer.togglePlaying()
}
} }
function previous(): void { function previous(): void {
if (root.activePlayer?.canGoPrevious) if (root.activePlayer && root.activePlayer.canGoPrevious) {
root.activePlayer.previous() root.activePlayer.previous()
}
} }
function next(): void { function next(): void {
if (root.activePlayer?.canGoNext) if (root.activePlayer && root.activePlayer.canGoNext) {
root.activePlayer.next() root.activePlayer.next()
}
} }
function stop(): void { function stop(): void {
root.activePlayer?.stop() if (root.activePlayer) {
root.activePlayer.stop()
}
} }
} }
} }

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -9,35 +10,32 @@ import qs.Common
Singleton { Singleton {
id: root id: root
// Core network state
property int refCount: 0 property int refCount: 0
property string networkStatus: "disconnected" // "ethernet", "wifi", "disconnected" property string networkStatus: "disconnected"
property string primaryConnection: "" // Active connection UUID property string primaryConnection: ""
// Ethernet properties
property string ethernetIP: "" property string ethernetIP: ""
property string ethernetInterface: "" property string ethernetInterface: ""
property bool ethernetConnected: false property bool ethernetConnected: false
property string ethernetConnectionUuid: "" property string ethernetConnectionUuid: ""
// WiFi properties
property string wifiIP: "" property string wifiIP: ""
property string wifiInterface: "" property string wifiInterface: ""
property bool wifiConnected: false property bool wifiConnected: false
property bool wifiEnabled: true property bool wifiEnabled: true
property string wifiConnectionUuid: "" property string wifiConnectionUuid: ""
// WiFi details
property string currentWifiSSID: "" property string currentWifiSSID: ""
property int wifiSignalStrength: 0 property int wifiSignalStrength: 0
property var wifiNetworks: [] property var wifiNetworks: []
property var savedConnections: [] property var savedConnections: []
property var ssidToConnectionName: {} property var ssidToConnectionName: {
}
property var wifiSignalIcon: { property var wifiSignalIcon: {
if (!wifiConnected || networkStatus !== "wifi") { if (!wifiConnected || networkStatus !== "wifi") {
return "signal_wifi_off" return "signal_wifi_off"
} }
// Use nmcli signal strength percentage
if (wifiSignalStrength >= 70) { if (wifiSignalStrength >= 70) {
return "signal_wifi_4_bar" return "signal_wifi_4_bar"
} }
@@ -53,17 +51,14 @@ Singleton {
return "signal_wifi_bad" return "signal_wifi_bad"
} }
// Connection management
property string userPreference: "auto" // "auto", "wifi", "ethernet" property string userPreference: "auto" // "auto", "wifi", "ethernet"
property bool isConnecting: false property bool isConnecting: false
property string connectingSSID: "" property string connectingSSID: ""
property string connectionError: "" property string connectionError: ""
// Scanning
property bool isScanning: false property bool isScanning: false
property bool autoScan: false property bool autoScan: false
// Legacy compatibility properties
property bool wifiAvailable: true property bool wifiAvailable: true
property bool wifiToggling: false property bool wifiToggling: false
property bool changingPreference: false property bool changingPreference: false
@@ -76,7 +71,6 @@ Singleton {
property string wifiPassword: "" property string wifiPassword: ""
property string forgetSSID: "" property string forgetSSID: ""
// Network info properties
property string networkInfoSSID: "" property string networkInfoSSID: ""
property string networkInfoDetails: "" property string networkInfoDetails: ""
property bool networkInfoLoading: false property bool networkInfoLoading: false
@@ -84,15 +78,13 @@ Singleton {
signal networksUpdated signal networksUpdated
signal connectionChanged signal connectionChanged
// Helper: split nmcli -t output respecting escaped colons (\:)
function splitNmcliFields(line) { function splitNmcliFields(line) {
let parts = [] const parts = []
let cur = "" let cur = ""
let escape = false let escape = false
for (let i = 0; i < line.length; i++) { for (var i = 0; i < line.length; i++) {
const ch = line[i] const ch = line[i]
if (escape) { if (escape) {
// Keep literal for escaped colon and other sequences
cur += ch cur += ch
escape = false escape = false
} else if (ch === '\\') { } else if (ch === '\\') {
@@ -140,11 +132,7 @@ Singleton {
stdout: SplitParser { stdout: SplitParser {
splitMarker: "\n" splitMarker: "\n"
onRead: line => { onRead: line => {
if (line.includes("StateChanged") || line.includes( if (line.includes("StateChanged") || line.includes("PrimaryConnectionChanged") || line.includes("WirelessEnabled") || line.includes("ActiveConnection") || line.includes("PropertiesChanged")) {
"PrimaryConnectionChanged") || line.includes(
"WirelessEnabled") || line.includes(
"ActiveConnection") || line.includes(
"PropertiesChanged")) {
refreshNetworkState() refreshNetworkState()
} }
} }
@@ -316,8 +304,9 @@ Singleton {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
const match = text.match(/inet (\d+\.\d+\.\d+\.\d+)/) const match = text.match(/inet (\d+\.\d+\.\d+\.\d+)/)
if (match) if (match) {
root.ethernetIP = match[1] root.ethernetIP = match[1]
}
} }
} }
} }
@@ -415,22 +404,21 @@ Singleton {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
const match = text.match(/inet (\d+\.\d+\.\d+\.\d+)/) const match = text.match(/inet (\d+\.\d+\.\d+\.\d+)/)
if (match) if (match) {
root.wifiIP = match[1] root.wifiIP = match[1]
}
} }
} }
} }
Process { Process {
id: getCurrentWifiInfo id: getCurrentWifiInfo
// Prefer IN-USE,SIGNAL,SSID, but we'll also parse legacy ACTIVE format
command: root.wifiInterface ? ["nmcli", "-t", "-f", "IN-USE,SIGNAL,SSID", "device", "wifi", "list", "ifname", root.wifiInterface] : [] command: root.wifiInterface ? ["nmcli", "-t", "-f", "IN-USE,SIGNAL,SSID", "device", "wifi", "list", "ifname", root.wifiInterface] : []
running: false running: false
stdout: SplitParser { stdout: SplitParser {
splitMarker: "\n" splitMarker: "\n"
onRead: line => { onRead: line => {
// IN-USE format: "*:SIGNAL:SSID"
if (line.startsWith("*:")) { if (line.startsWith("*:")) {
const rest = line.substring(2) const rest = line.substring(2)
const parts = root.splitNmcliFields(rest) const parts = root.splitNmcliFields(rest)
@@ -455,7 +443,6 @@ Singleton {
} }
} }
function updateActiveConnections() { function updateActiveConnections() {
getActiveConnections.running = true getActiveConnections.running = true
} }
@@ -475,11 +462,9 @@ Singleton {
const type = parts[1] const type = parts[1]
const device = parts[2] const device = parts[2]
const state = parts[3] const state = parts[3]
if (type === "802-3-ethernet" if (type === "802-3-ethernet" && state === "activated") {
&& state === "activated") {
root.ethernetConnectionUuid = uuid root.ethernetConnectionUuid = uuid
} else if (type === "802-11-wireless" } else if (type === "802-11-wireless" && state === "activated") {
&& state === "activated") {
root.wifiConnectionUuid = uuid root.wifiConnectionUuid = uuid
} }
} }
@@ -514,8 +499,9 @@ Singleton {
onStreamFinished: { onStreamFinished: {
if (!root.currentWifiSSID) { if (!root.currentWifiSSID) {
const name = text.trim() const name = text.trim()
if (name) if (name) {
root.currentWifiSSID = name root.currentWifiSSID = name
}
} }
} }
} }
@@ -539,8 +525,9 @@ Singleton {
} }
function scanWifi() { function scanWifi() {
if (root.isScanning || !root.wifiEnabled) if (root.isScanning || !root.wifiEnabled) {
return return
}
root.isScanning = true root.isScanning = true
requestWifiScan.running = true requestWifiScan.running = true
@@ -578,7 +565,7 @@ Singleton {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
let networks = [] const networks = []
const lines = text.trim().split('\n') const lines = text.trim().split('\n')
const seen = new Set() const seen = new Set()
@@ -596,7 +583,7 @@ Singleton {
"secured": parts[2] !== "", "secured": parts[2] !== "",
"bssid": parts[3], "bssid": parts[3],
"connected": ssid === root.currentWifiSSID, "connected": ssid === root.currentWifiSSID,
"saved": false // Will be updated by saved connections check "saved": false
}) })
} }
} }
@@ -617,8 +604,8 @@ Singleton {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
let saved = [] const saved = []
let mapping = {} const mapping = {}
const lines = text.trim().split('\n') const lines = text.trim().split('\n')
for (const line of lines) { for (const line of lines) {
@@ -640,8 +627,8 @@ Singleton {
root.savedWifiNetworks = saved root.savedWifiNetworks = saved
root.ssidToConnectionName = mapping root.ssidToConnectionName = mapping
let updated = [...root.wifiNetworks] const updated = [...root.wifiNetworks]
for (let network of updated) { for (const network of updated) {
network.saved = saved.some(s => s.ssid === network.ssid) network.saved = saved.some(s => s.ssid === network.ssid)
} }
root.wifiNetworks = updated root.wifiNetworks = updated
@@ -650,15 +637,15 @@ Singleton {
} }
function connectToWifi(ssid, password = "") { function connectToWifi(ssid, password = "") {
if (root.isConnecting) if (root.isConnecting) {
return return
}
root.isConnecting = true root.isConnecting = true
root.connectingSSID = ssid root.connectingSSID = ssid
root.connectionError = "" root.connectionError = ""
root.connectionStatus = "connecting" root.connectionStatus = "connecting"
// For saved networks without password, try connection up first
if (!password && root.ssidToConnectionName[ssid]) { if (!password && root.ssidToConnectionName[ssid]) {
const connectionName = root.ssidToConnectionName[ssid] const connectionName = root.ssidToConnectionName[ssid]
wifiConnector.command = ["nmcli", "connection", "up", connectionName] wifiConnector.command = ["nmcli", "connection", "up", connectionName]
@@ -670,10 +657,6 @@ Singleton {
wifiConnector.running = true wifiConnector.running = true
} }
function connectToWifiWithPassword(ssid, password) {
connectToWifi(ssid, password)
}
Process { Process {
id: wifiConnector id: wifiConnector
running: false running: false
@@ -688,8 +671,7 @@ Singleton {
root.connectionError = "" root.connectionError = ""
root.connectionStatus = "connected" root.connectionStatus = "connected"
if (root.userPreference === "wifi" if (root.userPreference === "wifi" || root.userPreference === "auto") {
|| root.userPreference === "auto") {
setConnectionPriority("wifi") setConnectionPriority("wifi")
} }
} }
@@ -701,8 +683,7 @@ Singleton {
root.connectionError = text root.connectionError = text
root.lastConnectionError = text root.lastConnectionError = text
if (!wifiConnector.connectionSucceeded && text.trim() !== "") { if (!wifiConnector.connectionSucceeded && text.trim() !== "") {
if (text.includes("password") || text.includes( if (text.includes("password") || text.includes("authentication")) {
"authentication")) {
root.connectionStatus = "invalid_password" root.connectionStatus = "invalid_password"
root.passwordDialogShouldReopen = true root.passwordDialogShouldReopen = true
} else { } else {
@@ -715,7 +696,6 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
if (exitCode === 0 || wifiConnector.connectionSucceeded) { if (exitCode === 0 || wifiConnector.connectionSucceeded) {
if (!wifiConnector.connectionSucceeded) { if (!wifiConnector.connectionSucceeded) {
// Command succeeded but we didn't see "successfully" - still mark as success
ToastService.showInfo(`Connected to ${root.connectingSSID}`) ToastService.showInfo(`Connected to ${root.connectingSSID}`)
root.connectionStatus = "connected" root.connectionStatus = "connected"
} }
@@ -724,11 +704,9 @@ Singleton {
root.connectionStatus = "failed" root.connectionStatus = "failed"
} }
if (root.connectionStatus === "invalid_password") { if (root.connectionStatus === "invalid_password") {
ToastService.showError( ToastService.showError(`Invalid password for ${root.connectingSSID}`)
`Invalid password for ${root.connectingSSID}`)
} else { } else {
ToastService.showError( ToastService.showError(`Failed to connect to ${root.connectingSSID}`)
`Failed to connect to ${root.connectingSSID}`)
} }
} }
@@ -740,8 +718,9 @@ Singleton {
} }
function disconnectWifi() { function disconnectWifi() {
if (!root.wifiInterface) if (!root.wifiInterface) {
return return
}
wifiDisconnector.command = ["nmcli", "dev", "disconnect", root.wifiInterface] wifiDisconnector.command = ["nmcli", "dev", "disconnect", root.wifiInterface]
wifiDisconnector.running = true wifiDisconnector.running = true
@@ -776,13 +755,11 @@ Singleton {
if (exitCode === 0) { if (exitCode === 0) {
ToastService.showInfo(`Forgot network ${root.forgetSSID}`) ToastService.showInfo(`Forgot network ${root.forgetSSID}`)
root.savedConnections = root.savedConnections.filter( root.savedConnections = root.savedConnections.filter(s => s.ssid !== root.forgetSSID)
s => s.ssid !== root.forgetSSID) root.savedWifiNetworks = root.savedWifiNetworks.filter(s => s.ssid !== root.forgetSSID)
root.savedWifiNetworks = root.savedWifiNetworks.filter(
s => s.ssid !== root.forgetSSID)
let updated = [...root.wifiNetworks] const updated = [...root.wifiNetworks]
for (let network of updated) { for (const network of updated) {
if (network.ssid === root.forgetSSID) { if (network.ssid === root.forgetSSID) {
network.saved = false network.saved = false
if (network.connected) { if (network.connected) {
@@ -800,8 +777,9 @@ Singleton {
} }
function toggleWifiRadio() { function toggleWifiRadio() {
if (root.wifiToggling) if (root.wifiToggling) {
return return
}
root.wifiToggling = true root.wifiToggling = true
const targetState = root.wifiEnabled ? "off" : "on" const targetState = root.wifiEnabled ? "off" : "on"
@@ -819,15 +797,12 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
root.wifiToggling = false root.wifiToggling = false
if (exitCode === 0) { if (exitCode === 0) {
// Don't manually toggle wifiEnabled - let DBus monitoring handle it ToastService.showInfo(targetState === "on" ? "WiFi enabled" : "WiFi disabled")
ToastService.showInfo(
targetState === "on" ? "WiFi enabled" : "WiFi disabled")
} }
refreshNetworkState() refreshNetworkState()
} }
} }
// ===== Network Preference Management =====
function setNetworkPreference(preference) { function setNetworkPreference(preference) {
root.userPreference = preference root.userPreference = preference
root.changingPreference = true root.changingPreference = true
@@ -839,7 +814,6 @@ Singleton {
} else if (preference === "ethernet") { } else if (preference === "ethernet") {
setConnectionPriority("ethernet") setConnectionPriority("ethernet")
} }
// "auto" uses default NetworkManager behavior
} }
function setConnectionPriority(type) { function setConnectionPriority(type) {
@@ -865,9 +839,7 @@ Singleton {
Process { Process {
id: restartConnections id: restartConnections
command: ["bash", "-c", "nmcli -t -f UUID,TYPE connection show --active | " command: ["bash", "-c", "nmcli -t -f UUID,TYPE connection show --active | " + "grep -E '802-11-wireless|802-3-ethernet' | cut -d: -f1 | " + "xargs -I {} sh -c 'nmcli connection down {} && nmcli connection up {}'"]
+ "grep -E '802-11-wireless|802-3-ethernet' | cut -d: -f1 | "
+ "xargs -I {} sh -c 'nmcli connection down {} && nmcli connection up {}'"]
running: false running: false
onExited: { onExited: {
@@ -890,7 +862,6 @@ Singleton {
root.autoRefreshEnabled = false root.autoRefreshEnabled = false
} }
// ===== Network Info =====
function fetchNetworkInfo(ssid) { function fetchNetworkInfo(ssid) {
root.networkInfoSSID = ssid root.networkInfoSSID = ssid
root.networkInfoLoading = true root.networkInfoLoading = true
@@ -907,23 +878,21 @@ Singleton {
onStreamFinished: { onStreamFinished: {
let details = "" let details = ""
if (text.trim()) { if (text.trim()) {
let lines = text.trim().split('\n') const lines = text.trim().split('\n')
let bands = [] const bands = []
// Collect all access points for this SSID for (const line of lines) {
for (let line of lines) { const parts = line.split(':')
let parts = line.split(':')
if (parts.length >= 11 && parts[0] === root.networkInfoSSID) { if (parts.length >= 11 && parts[0] === root.networkInfoSSID) {
let signal = parts[1] || "0" const signal = parts[1] || "0"
let security = parts[2] || "Open" const security = parts[2] || "Open"
let freq = parts[3] || "Unknown" const freq = parts[3] || "Unknown"
let rate = parts[4] || "Unknown" const rate = parts[4] || "Unknown"
let channel = parts[6] || "Unknown" const channel = parts[6] || "Unknown"
let isActive = parts[9] === "yes" const isActive = parts[9] === "yes"
// BSSID is the last field, find it by counting colons
let colonCount = 0 let colonCount = 0
let bssidStart = -1 let bssidStart = -1
for (let i = 0; i < line.length; i++) { for (var i = 0; i < line.length; i++) {
if (line[i] === ':') { if (line[i] === ':') {
colonCount++ colonCount++
if (colonCount === 10) { if (colonCount === 10) {
@@ -932,10 +901,10 @@ Singleton {
} }
} }
} }
let bssid = bssidStart >= 0 ? line.substring(bssidStart).replace(/\\:/g, ":") : "" const bssid = bssidStart >= 0 ? line.substring(bssidStart).replace(/\\:/g, ":") : ""
let band = "Unknown" let band = "Unknown"
let freqNum = parseInt(freq) const freqNum = parseInt(freq)
if (freqNum >= 2400 && freqNum <= 2500) { if (freqNum >= 2400 && freqNum <= 2500) {
band = "2.4 GHz" band = "2.4 GHz"
} else if (freqNum >= 5000 && freqNum <= 6000) { } else if (freqNum >= 5000 && freqNum <= 6000) {
@@ -945,28 +914,31 @@ Singleton {
} }
bands.push({ bands.push({
band: band, "band": band,
freq: freq, "freq": freq,
channel: channel, "channel": channel,
signal: signal, "signal": signal,
rate: rate, "rate": rate,
security: security, "security": security,
isActive: isActive, "isActive": isActive,
bssid: bssid "bssid": bssid
}) })
} }
} }
if (bands.length > 0) { if (bands.length > 0) {
// Sort bands: active first, then by signal strength
bands.sort((a, b) => { bands.sort((a, b) => {
if (a.isActive && !b.isActive) return -1 if (a.isActive && !b.isActive) {
if (!a.isActive && b.isActive) return 1 return -1
return parseInt(b.signal) - parseInt(a.signal) }
}) if (!a.isActive && b.isActive) {
return 1
for (let i = 0; i < bands.length; i++) { }
let b = bands[i] return parseInt(b.signal) - parseInt(a.signal)
})
for (var i = 0; i < bands.length; i++) {
const b = bands[i]
if (b.isActive) { if (b.isActive) {
details += "● " + b.band + " (Connected) - " + b.signal + "%\\n" details += "● " + b.band + " (Connected) - " + b.signal + "%\\n"
} else { } else {
@@ -998,18 +970,6 @@ Singleton {
} }
} }
function refreshNetworkStatus() {
refreshNetworkState()
}
function delayedRefreshNetworkStatus() {
refreshNetworkState()
}
function updateCurrentWifiInfo() {
getCurrentWifiInfo.running = true
}
function enableWifiDevice() { function enableWifiDevice() {
wifiDeviceEnabler.running = true wifiDeviceEnabler.running = true
} }
@@ -1030,7 +990,7 @@ Singleton {
} }
function connectToWifiAndSetPreference(ssid, password) { function connectToWifiAndSetPreference(ssid, password) {
connectToWifiWithPassword(ssid, password) connectToWifi(ssid, password)
setNetworkPreference("wifi") setNetworkPreference("wifi")
} }
@@ -1066,8 +1026,9 @@ Singleton {
function getNetworkInfo(ssid) { function getNetworkInfo(ssid) {
const network = root.wifiNetworks.find(n => n.ssid === ssid) const network = root.wifiNetworks.find(n => n.ssid === ssid)
if (!network) if (!network) {
return null return null
}
return { return {
"ssid": network.ssid, "ssid": network.ssid,

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -9,7 +10,6 @@ import Quickshell.Wayland
Singleton { Singleton {
id: root id: root
// Workspace management
property var workspaces: ({}) property var workspaces: ({})
property var allWorkspaces: [] property var allWorkspaces: []
property int focusedWorkspaceIndex: 0 property int focusedWorkspaceIndex: 0
@@ -17,24 +17,18 @@ Singleton {
property var currentOutputWorkspaces: [] property var currentOutputWorkspaces: []
property string currentOutput: "" property string currentOutput: ""
// Output/Monitor management property var outputs: ({})
property var outputs: ({}) // Map of output name to output info with positions
// Window management
property var windows: [] property var windows: []
// Overview state
property bool inOverview: false property bool inOverview: false
// Keyboard layout state
property int currentKeyboardLayoutIndex: 0 property int currentKeyboardLayoutIndex: 0
property var keyboardLayoutNames: [] property var keyboardLayoutNames: []
// Internal state (not exposed to external components)
property string configValidationOutput: "" property string configValidationOutput: ""
property bool hasInitialConnection: false property bool hasInitialConnection: false
readonly property string socketPath: Quickshell.env("NIRI_SOCKET") readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
Component.onCompleted: { Component.onCompleted: {
@@ -54,11 +48,9 @@ Singleton {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
try { try {
var outputsData = JSON.parse(text) const outputsData = JSON.parse(text)
outputs = outputsData outputs = outputsData
console.log("NiriService: Loaded", console.log("NiriService: Loaded", Object.keys(outputsData).length, "outputs")
Object.keys(outputsData).length, "outputs")
// Re-sort windows with monitor positions
if (windows.length > 0) { if (windows.length > 0) {
windows = sortWindowsByLayout(windows) windows = sortWindowsByLayout(windows)
} }
@@ -70,9 +62,7 @@ Singleton {
onExited: exitCode => { onExited: exitCode => {
if (exitCode !== 0) { if (exitCode !== 0) {
console.warn( console.warn("NiriService: Failed to fetch outputs, exit code:", exitCode)
"NiriService: Failed to fetch outputs, exit code:",
exitCode)
} }
} }
} }
@@ -108,59 +98,40 @@ Singleton {
function sortWindowsByLayout(windowList) { function sortWindowsByLayout(windowList) {
return [...windowList].sort((a, b) => { return [...windowList].sort((a, b) => {
// Get workspace info for both windows const aWorkspace = workspaces[a.workspace_id]
var aWorkspace = workspaces[a.workspace_id] const bWorkspace = workspaces[b.workspace_id]
var bWorkspace = workspaces[b.workspace_id]
if (aWorkspace && bWorkspace) { if (aWorkspace && bWorkspace) {
var aOutput = aWorkspace.output const aOutput = aWorkspace.output
var bOutput = bWorkspace.output const bOutput = bWorkspace.output
// 1. First, sort by monitor position (left to right, top to bottom) const aOutputInfo = outputs[aOutput]
var aOutputInfo = outputs[aOutput] const bOutputInfo = outputs[bOutput]
var bOutputInfo = outputs[bOutput]
if (aOutputInfo && bOutputInfo if (aOutputInfo && bOutputInfo && aOutputInfo.logical && bOutputInfo.logical) {
&& aOutputInfo.logical if (aOutputInfo.logical.x !== bOutputInfo.logical.x) {
&& bOutputInfo.logical) { return aOutputInfo.logical.x - bOutputInfo.logical.x
// Sort by monitor X position (left to right)
if (aOutputInfo.logical.x
!== bOutputInfo.logical.x) {
return aOutputInfo.logical.x
- bOutputInfo.logical.x
} }
// If same X, sort by Y position (top to bottom) if (aOutputInfo.logical.y !== bOutputInfo.logical.y) {
if (aOutputInfo.logical.y return aOutputInfo.logical.y - bOutputInfo.logical.y
!== bOutputInfo.logical.y) {
return aOutputInfo.logical.y
- bOutputInfo.logical.y
} }
} }
// 2. If same monitor, sort by workspace index if (aOutput === bOutput && aWorkspace.idx !== bWorkspace.idx) {
if (aOutput === bOutput
&& aWorkspace.idx !== bWorkspace.idx) {
return aWorkspace.idx - bWorkspace.idx return aWorkspace.idx - bWorkspace.idx
} }
} }
// 3. If same workspace, sort by actual position within workspace if (a.workspace_id === b.workspace_id && a.layout && b.layout) {
if (a.workspace_id === b.workspace_id
&& a.layout && b.layout) {
// Use pos_in_scrolling_layout [x, y] coordinates if (a.layout.pos_in_scrolling_layout && b.layout.pos_in_scrolling_layout) {
if (a.layout.pos_in_scrolling_layout const aPos = a.layout.pos_in_scrolling_layout
&& b.layout.pos_in_scrolling_layout) { const bPos = b.layout.pos_in_scrolling_layout
var aPos = a.layout.pos_in_scrolling_layout
var bPos = b.layout.pos_in_scrolling_layout
if (aPos.length > 1 if (aPos.length > 1 && bPos.length > 1) {
&& bPos.length > 1) {
// Sort by X (horizontal) position first
if (aPos[0] !== bPos[0]) { if (aPos[0] !== bPos[0]) {
return aPos[0] - bPos[0] return aPos[0] - bPos[0]
} }
// Then sort by Y (vertical) position
if (aPos[1] !== bPos[1]) { if (aPos[1] !== bPos[1]) {
return aPos[1] - bPos[1] return aPos[1] - bPos[1]
} }
@@ -168,37 +139,50 @@ Singleton {
} }
} }
// 4. Fallback to window ID for consistent ordering
return a.id - b.id return a.id - b.id
}) })
} }
function handleNiriEvent(event) { function handleNiriEvent(event) {
if (event.WorkspacesChanged) { const eventType = Object.keys(event)[0];
handleWorkspacesChanged(event.WorkspacesChanged)
} else if (event.WorkspaceActivated) { switch (eventType) {
handleWorkspaceActivated(event.WorkspaceActivated) case 'WorkspacesChanged':
} else if (event.WorkspaceActiveWindowChanged) { handleWorkspacesChanged(event.WorkspacesChanged);
handleWorkspaceActiveWindowChanged( break;
event.WorkspaceActiveWindowChanged) case 'WorkspaceActivated':
} else if (event.WindowsChanged) { handleWorkspaceActivated(event.WorkspaceActivated);
handleWindowsChanged(event.WindowsChanged) break;
} else if (event.WindowClosed) { case 'WorkspaceActiveWindowChanged':
handleWindowClosed(event.WindowClosed) handleWorkspaceActiveWindowChanged(event.WorkspaceActiveWindowChanged);
} else if (event.WindowOpenedOrChanged) { break;
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged) case 'WindowsChanged':
} else if (event.WindowLayoutsChanged) { handleWindowsChanged(event.WindowsChanged);
handleWindowLayoutsChanged(event.WindowLayoutsChanged) break;
} else if (event.OutputsChanged) { case 'WindowClosed':
handleOutputsChanged(event.OutputsChanged) handleWindowClosed(event.WindowClosed);
} else if (event.OverviewOpenedOrClosed) { break;
handleOverviewChanged(event.OverviewOpenedOrClosed) case 'WindowOpenedOrChanged':
} else if (event.ConfigLoaded) { handleWindowOpenedOrChanged(event.WindowOpenedOrChanged);
handleConfigLoaded(event.ConfigLoaded) break;
} else if (event.KeyboardLayoutsChanged) { case 'WindowLayoutsChanged':
handleKeyboardLayoutsChanged(event.KeyboardLayoutsChanged) handleWindowLayoutsChanged(event.WindowLayoutsChanged);
} else if (event.KeyboardLayoutSwitched) { break;
handleKeyboardLayoutSwitched(event.KeyboardLayoutSwitched) case 'OutputsChanged':
handleOutputsChanged(event.OutputsChanged);
break;
case 'OverviewOpenedOrClosed':
handleOverviewChanged(event.OverviewOpenedOrClosed);
break;
case 'ConfigLoaded':
handleConfigLoaded(event.ConfigLoaded);
break;
case 'KeyboardLayoutsChanged':
handleKeyboardLayoutsChanged(event.KeyboardLayoutsChanged);
break;
case 'KeyboardLayoutSwitched':
handleKeyboardLayoutSwitched(event.KeyboardLayoutSwitched);
break;
} }
} }
@@ -214,7 +198,7 @@ Singleton {
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused) focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused)
if (focusedWorkspaceIndex >= 0) { if (focusedWorkspaceIndex >= 0) {
var focusedWs = allWorkspaces[focusedWorkspaceIndex] const focusedWs = allWorkspaces[focusedWorkspaceIndex]
focusedWorkspaceId = focusedWs.id focusedWorkspaceId = focusedWs.id
currentOutput = focusedWs.output || "" currentOutput = focusedWs.output || ""
} else { } else {
@@ -227,8 +211,9 @@ Singleton {
function handleWorkspaceActivated(data) { function handleWorkspaceActivated(data) {
const ws = root.workspaces[data.id] const ws = root.workspaces[data.id]
if (!ws) if (!ws) {
return return
}
const output = ws.output const output = ws.output
for (const id in root.workspaces) { for (const id in root.workspaces) {
@@ -251,23 +236,18 @@ Singleton {
currentOutput = allWorkspaces[focusedWorkspaceIndex].output || "" currentOutput = allWorkspaces[focusedWorkspaceIndex].output || ""
} }
allWorkspaces = Object.values(root.workspaces).sort( allWorkspaces = Object.values(root.workspaces).sort((a, b) => a.idx - b.idx)
(a, b) => a.idx - b.idx)
updateCurrentOutputWorkspaces() updateCurrentOutputWorkspaces()
workspacesChanged() workspacesChanged()
} }
function handleWorkspaceActiveWindowChanged(data) { function handleWorkspaceActiveWindowChanged(data) {
// Update the focused window when workspace's active window changes if (data.active_window_id !== null && data.active_window_id !== undefined) {
// This is crucial for handling floating window close scenarios const updatedWindows = []
if (data.active_window_id !== null
&& data.active_window_id !== undefined) {
// Create new windows array with updated focus states to trigger property change
let updatedWindows = []
for (var i = 0; i < windows.length; i++) { for (var i = 0; i < windows.length; i++) {
let w = windows[i] const w = windows[i]
let updatedWindow = {} const updatedWindow = {}
for (let prop in w) { for (let prop in w) {
updatedWindow[prop] = w[prop] updatedWindow[prop] = w[prop]
} }
@@ -276,17 +256,14 @@ Singleton {
} }
windows = updatedWindows windows = updatedWindows
} else { } else {
// No active window in this workspace const updatedWindows = []
// Create new windows array with cleared focus states for this workspace
let updatedWindows = []
for (var i = 0; i < windows.length; i++) { for (var i = 0; i < windows.length; i++) {
let w = windows[i] const w = windows[i]
let updatedWindow = {} const updatedWindow = {}
for (let prop in w) { for (let prop in w) {
updatedWindow[prop] = w[prop] updatedWindow[prop] = w[prop]
} }
updatedWindow.is_focused = w.workspace_id updatedWindow.is_focused = w.workspace_id == data.workspace_id ? false : w.is_focused
== data.workspace_id ? false : w.is_focused
updatedWindows.push(updatedWindow) updatedWindows.push(updatedWindow)
} }
windows = updatedWindows windows = updatedWindows
@@ -302,28 +279,28 @@ Singleton {
} }
function handleWindowOpenedOrChanged(data) { function handleWindowOpenedOrChanged(data) {
if (!data.window) if (!data.window) {
return return
}
const window = data.window const window = data.window
const existingIndex = windows.findIndex(w => w.id === window.id) const existingIndex = windows.findIndex(w => w.id === window.id)
if (existingIndex >= 0) { if (existingIndex >= 0) {
let updatedWindows = [...windows] const updatedWindows = [...windows]
updatedWindows[existingIndex] = window updatedWindows[existingIndex] = window
windows = sortWindowsByLayout(updatedWindows) windows = sortWindowsByLayout(updatedWindows)
} else { } else {
windows = sortWindowsByLayout([...windows, window]) windows = sortWindowsByLayout([...windows, window])
} }
} }
function handleWindowLayoutsChanged(data) { function handleWindowLayoutsChanged(data) {
// Update layout positions for windows that have changed if (!data.changes) {
if (!data.changes)
return return
}
let updatedWindows = [...windows] const updatedWindows = [...windows]
let hasChanges = false let hasChanges = false
for (const change of data.changes) { for (const change of data.changes) {
@@ -332,8 +309,7 @@ Singleton {
const windowIndex = updatedWindows.findIndex(w => w.id === windowId) const windowIndex = updatedWindows.findIndex(w => w.id === windowId)
if (windowIndex >= 0) { if (windowIndex >= 0) {
// Create a new object with updated layout const updatedWindow = {}
var updatedWindow = {}
for (var prop in updatedWindows[windowIndex]) { for (var prop in updatedWindows[windowIndex]) {
updatedWindow[prop] = updatedWindows[windowIndex][prop] updatedWindow[prop] = updatedWindows[windowIndex][prop]
} }
@@ -345,7 +321,6 @@ Singleton {
if (hasChanges) { if (hasChanges) {
windows = sortWindowsByLayout(updatedWindows) windows = sortWindowsByLayout(updatedWindows)
// Trigger update in dock and widgets
windowsChanged() windowsChanged()
} }
} }
@@ -353,7 +328,6 @@ Singleton {
function handleOutputsChanged(data) { function handleOutputsChanged(data) {
if (data.outputs) { if (data.outputs) {
outputs = data.outputs outputs = data.outputs
// Re-sort windows with new monitor positions
windows = sortWindowsByLayout(windows) windows = sortWindowsByLayout(windows)
} }
} }
@@ -367,8 +341,7 @@ Singleton {
validateProcess.running = true validateProcess.running = true
} else { } else {
configValidationOutput = "" configValidationOutput = ""
if (ToastService.toastVisible if (ToastService.toastVisible && ToastService.currentLevel === ToastService.levelError) {
&& ToastService.currentLevel === ToastService.levelError) {
ToastService.hideToast() ToastService.hideToast()
} }
if (hasInitialConnection) { if (hasInitialConnection) {
@@ -398,13 +371,10 @@ Singleton {
stderr: StdioCollector { stderr: StdioCollector {
onStreamFinished: { onStreamFinished: {
const lines = text.split('\n') const lines = text.split('\n')
const trimmedLines = lines.map(line => line.replace(/\s+$/, const trimmedLines = lines.map(line => line.replace(/\s+$/, '')).filter(line => line.length > 0)
'')).filter(
line => line.length > 0)
configValidationOutput = trimmedLines.join('\n').trim() configValidationOutput = trimmedLines.join('\n').trim()
if (hasInitialConnection) { if (hasInitialConnection) {
ToastService.showError("niri: failed to load config", ToastService.showError("niri: failed to load config", configValidationOutput)
configValidationOutput)
} }
} }
} }
@@ -422,13 +392,14 @@ Singleton {
return return
} }
var outputWs = allWorkspaces.filter(w => w.output === currentOutput) const outputWs = allWorkspaces.filter(w => w.output === currentOutput)
currentOutputWorkspaces = outputWs currentOutputWorkspaces = outputWs
} }
function send(request) { function send(request) {
if (!CompositorService.isNiri || !requestSocket.connected) if (!CompositorService.isNiri || !requestSocket.connected) {
return false return false
}
requestSocket.write(JSON.stringify(request) + "\n") requestSocket.write(JSON.stringify(request) + "\n")
return true return true
} }
@@ -444,41 +415,36 @@ Singleton {
} }
}) })
} }
function focusWindow(windowId) { function focusWindow(windowId) {
return send({ return send({
"Action": { "Action": {
"FocusWindow": { "FocusWindow": {
"id": windowId "id": windowId
} }
} }
}) })
} }
function getCurrentOutputWorkspaceNumbers() { function getCurrentOutputWorkspaceNumbers() {
return currentOutputWorkspaces.map( return currentOutputWorkspaces.map(w => w.idx + 1)
w => w.idx + 1) // niri uses 0-based, UI shows 1-based
} }
function getCurrentWorkspaceNumber() { function getCurrentWorkspaceNumber() {
if (focusedWorkspaceIndex >= 0 if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) {
&& focusedWorkspaceIndex < allWorkspaces.length) {
return allWorkspaces[focusedWorkspaceIndex].idx + 1 return allWorkspaces[focusedWorkspaceIndex].idx + 1
} }
return 1 return 1
} }
function getCurrentKeyboardLayoutName() { function getCurrentKeyboardLayoutName() {
if (currentKeyboardLayoutIndex >= 0 if (currentKeyboardLayoutIndex >= 0 && currentKeyboardLayoutIndex < keyboardLayoutNames.length) {
&& currentKeyboardLayoutIndex < keyboardLayoutNames.length) {
return keyboardLayoutNames[currentKeyboardLayoutIndex] return keyboardLayoutNames[currentKeyboardLayoutIndex]
} }
return "" return ""
} }
function cycleKeyboardLayout() { function cycleKeyboardLayout() {
return send({ return send({
"Action": { "Action": {
@@ -499,16 +465,19 @@ Singleton {
}) })
} }
function findNiriWindow(toplevel) { function findNiriWindow(toplevel) {
if (!toplevel.appId) return null if (!toplevel.appId) {
return null
}
for (var j = 0; j < windows.length; j++) { for (var j = 0; j < windows.length; j++) {
var niriWindow = windows[j] const niriWindow = windows[j]
if (niriWindow.app_id === toplevel.appId) { if (niriWindow.app_id === toplevel.appId) {
if (!niriWindow.title || niriWindow.title === toplevel.title) { if (!niriWindow.title || niriWindow.title === toplevel.title) {
return { niriIndex: j, niriWindow: niriWindow } return {
"niriIndex": j,
"niriWindow": niriWindow
}
} }
} }
} }
@@ -519,69 +488,71 @@ Singleton {
if (!toplevels || toplevels.length === 0 || !CompositorService.isNiri || windows.length === 0) { if (!toplevels || toplevels.length === 0 || !CompositorService.isNiri || windows.length === 0) {
return [...toplevels] return [...toplevels]
} }
return [...toplevels].sort((a, b) => { return [...toplevels].sort((a, b) => {
var aNiri = findNiriWindow(a) const aNiri = findNiriWindow(a)
var bNiri = findNiriWindow(b) const bNiri = findNiriWindow(b)
if (!aNiri && !bNiri) return 0 if (!aNiri && !bNiri) {
if (!aNiri) return 1 return 0
if (!bNiri) return -1 }
if (!aNiri) {
var aWindow = aNiri.niriWindow return 1
var bWindow = bNiri.niriWindow }
var aWorkspace = allWorkspaces.find(ws => ws.id === aWindow.workspace_id) if (!bNiri) {
var bWorkspace = allWorkspaces.find(ws => ws.id === bWindow.workspace_id) return -1
}
if (aWorkspace && bWorkspace) {
if (aWorkspace.output !== bWorkspace.output) { const aWindow = aNiri.niriWindow
return aWorkspace.output.localeCompare(bWorkspace.output) const bWindow = bNiri.niriWindow
} const aWorkspace = allWorkspaces.find(ws => ws.id === aWindow.workspace_id)
const bWorkspace = allWorkspaces.find(ws => ws.id === bWindow.workspace_id)
if (aWorkspace.output === bWorkspace.output && aWorkspace.idx !== bWorkspace.idx) {
return aWorkspace.idx - bWorkspace.idx if (aWorkspace && bWorkspace) {
} if (aWorkspace.output !== bWorkspace.output) {
} return aWorkspace.output.localeCompare(bWorkspace.output)
}
if (aWindow.workspace_id === bWindow.workspace_id &&
aWindow.layout && bWindow.layout && if (aWorkspace.output === bWorkspace.output && aWorkspace.idx !== bWorkspace.idx) {
aWindow.layout.pos_in_scrolling_layout && return aWorkspace.idx - bWorkspace.idx
bWindow.layout.pos_in_scrolling_layout) { }
var aPos = aWindow.layout.pos_in_scrolling_layout }
var bPos = bWindow.layout.pos_in_scrolling_layout
if (aWindow.workspace_id === bWindow.workspace_id && aWindow.layout && bWindow.layout && aWindow.layout.pos_in_scrolling_layout && bWindow.layout.pos_in_scrolling_layout) {
if (aPos.length > 1 && bPos.length > 1) { const aPos = aWindow.layout.pos_in_scrolling_layout
if (aPos[0] !== bPos[0]) { const bPos = bWindow.layout.pos_in_scrolling_layout
return aPos[0] - bPos[0]
} if (aPos.length > 1 && bPos.length > 1) {
if (aPos[1] !== bPos[1]) { if (aPos[0] !== bPos[0]) {
return aPos[1] - bPos[1] return aPos[0] - bPos[0]
} }
} if (aPos[1] !== bPos[1]) {
} return aPos[1] - bPos[1]
}
return aWindow.id - bWindow.id }
}) }
return aWindow.id - bWindow.id
})
} }
function filterCurrentWorkspace(toplevels, screenName){ function filterCurrentWorkspace(toplevels, screenName) {
var currentWorkspaceId = null let currentWorkspaceId = null
for (var i = 0; i < allWorkspaces.length; i++) { for (var i = 0; i < allWorkspaces.length; i++) {
var ws = allWorkspaces[i] const ws = allWorkspaces[i]
if (ws.output === screenName && ws.is_active){ if (ws.output === screenName && ws.is_active) {
currentWorkspaceId = ws.id currentWorkspaceId = ws.id
break break
} }
} }
if (currentWorkspaceId === null) { if (currentWorkspaceId === null) {
return toplevels return toplevels
} }
return toplevels.filter(toplevel => { return toplevels.filter(toplevel => {
var niriMatch = findNiriWindow(toplevel) const niriMatch = findNiriWindow(toplevel)
return niriMatch && niriMatch.niriWindow.workspace_id === currentWorkspaceId return niriMatch && niriMatch.niriWindow.workspace_id === currentWorkspaceId
}) })
} }
} }

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -13,8 +14,7 @@ Singleton {
readonly property list<NotifWrapper> notifications: [] readonly property list<NotifWrapper> notifications: []
readonly property list<NotifWrapper> allWrappers: [] readonly property list<NotifWrapper> allWrappers: []
readonly property list<NotifWrapper> popups: allWrappers.filter( readonly property list<NotifWrapper> popups: allWrappers.filter(n => n && n.popup)
n => n && n.popup)
property list<NotifWrapper> notificationQueue: [] property list<NotifWrapper> notificationQueue: []
property list<NotifWrapper> visibleNotifications: [] property list<NotifWrapper> visibleNotifications: []
@@ -34,14 +34,19 @@ Singleton {
property int _dismissBatchSize: 8 property int _dismissBatchSize: 8
property int _dismissTickMs: 8 property int _dismissTickMs: 8
property bool _suspendGrouping: false property bool _suspendGrouping: false
property var _groupCache: ({"notifications": [], "popups": []}) property var _groupCache: ({
"notifications": [],
"popups": []
})
property bool _groupsDirty: false property bool _groupsDirty: false
Component.onCompleted: { Component.onCompleted: {
_recomputeGroups() _recomputeGroups()
} }
function _nowSec() { return Date.now() / 1000.0 } function _nowSec() {
return Date.now() / 1000.0
}
function _ingressAllowed(notif) { function _ingressAllowed(notif) {
const t = _nowSec() const t = _nowSec()
@@ -50,22 +55,26 @@ Singleton {
_ingressCountThisSec = 0 _ingressCountThisSec = 0
} }
_ingressCountThisSec += 1 _ingressCountThisSec += 1
if (notif.urgency === NotificationUrgency.Critical) if (notif.urgency === NotificationUrgency.Critical) {
return true return true
}
return _ingressCountThisSec <= maxIngressPerSecond return _ingressCountThisSec <= maxIngressPerSecond
} }
function _enqueuePopup(wrapper) { function _enqueuePopup(wrapper) {
if (notificationQueue.length >= maxQueueSize) { if (notificationQueue.length >= maxQueueSize) {
const gk = getGroupKey(wrapper) const gk = getGroupKey(wrapper)
let idx = notificationQueue.findIndex(w => let idx = notificationQueue.findIndex(w => w && getGroupKey(w) === gk && w.urgency !== NotificationUrgency.Critical)
w && getGroupKey(w) === gk && w.urgency !== NotificationUrgency.Critical)
if (idx === -1) { if (idx === -1) {
idx = notificationQueue.findIndex(w => w && w.urgency !== NotificationUrgency.Critical) idx = notificationQueue.findIndex(w => w && w.urgency !== NotificationUrgency.Critical)
} }
if (idx === -1) idx = 0 if (idx === -1) {
idx = 0
}
const victim = notificationQueue[idx] const victim = notificationQueue[idx]
if (victim) victim.popup = false if (victim) {
victim.popup = false
}
notificationQueue.splice(idx, 1) notificationQueue.splice(idx, 1)
} }
notificationQueue = [...notificationQueue, wrapper] notificationQueue = [...notificationQueue, wrapper]
@@ -80,18 +89,26 @@ Singleton {
function _trimStored() { function _trimStored() {
if (notifications.length > maxStoredNotifications) { if (notifications.length > maxStoredNotifications) {
const overflow = notifications.length - maxStoredNotifications const overflow = notifications.length - maxStoredNotifications
let toDrop = [] const toDrop = []
for (let i = notifications.length - 1; i >= 0 && toDrop.length < overflow; --i) { for (var i = notifications.length - 1; i >= 0 && toDrop.length < overflow; --i) {
const w = notifications[i] const w = notifications[i]
if (w && w.notification && w.urgency !== NotificationUrgency.Critical) if (w && w.notification && w.urgency !== NotificationUrgency.Critical) {
toDrop.push(w) toDrop.push(w)
}
} }
for (let i = notifications.length - 1; i >= 0 && toDrop.length < overflow; --i) { for (var i = notifications.length - 1; i >= 0 && toDrop.length < overflow; --i) {
const w = notifications[i] const w = notifications[i]
if (w && w.notification && toDrop.indexOf(w) === -1) if (w && w.notification && toDrop.indexOf(w) === -1) {
toDrop.push(w) toDrop.push(w)
}
}
for (const w of toDrop) {
try {
w.notification.dismiss()
} catch (e) {
}
} }
for (const w of toDrop) { try { w.notification.dismiss() } catch(e) {} }
} }
} }
@@ -144,11 +161,15 @@ Singleton {
running: false running: false
onTriggered: { onTriggered: {
let n = Math.min(_dismissBatchSize, _dismissQueue.length) let n = Math.min(_dismissBatchSize, _dismissQueue.length)
for (let i = 0; i < n; ++i) { for (var i = 0; i < n; ++i) {
const w = _dismissQueue.pop() const w = _dismissQueue.pop()
try { try {
if (w && w.notification) w.notification.dismiss() if (w && w.notification) {
} catch (e) {} w.notification.dismiss()
}
} catch (e) {
}
} }
if (_dismissQueue.length === 0) { if (_dismissQueue.length === 0) {
dismissPump.stop() dismissPump.stop()
@@ -195,7 +216,11 @@ Singleton {
if (!_ingressAllowed(notif)) { if (!_ingressAllowed(notif)) {
if (notif.urgency !== NotificationUrgency.Critical) { if (notif.urgency !== NotificationUrgency.Critical) {
try { notif.dismiss() } catch(e) {} try {
notif.dismiss()
} catch (e) {
}
return return
} }
} }
@@ -212,8 +237,8 @@ Singleton {
_trimStored() _trimStored()
Qt.callLater(() => { Qt.callLater(() => {
_initWrapperPersistence(wrapper) _initWrapperPersistence(wrapper)
}) })
if (shouldShowPopup) { if (shouldShowPopup) {
_enqueuePopup(wrapper) _enqueuePopup(wrapper)
@@ -241,8 +266,9 @@ Singleton {
readonly property Timer timer: Timer { readonly property Timer timer: Timer {
interval: { interval: {
if (!wrapper.notification) if (!wrapper.notification) {
return 5000 return 5000
}
switch (wrapper.notification.urgency) { switch (wrapper.notification.urgency) {
case NotificationUrgency.Low: case NotificationUrgency.Low:
@@ -273,17 +299,15 @@ Singleton {
const hours = Math.floor(minutes / 60) const hours = Math.floor(minutes / 60)
if (hours < 1) { if (hours < 1) {
if (minutes < 1) if (minutes < 1) {
return "now" return "now"
}
return `${minutes}m ago` return `${minutes}m ago`
} }
const nowDate = new Date(now.getFullYear(), now.getMonth(), const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate())
now.getDate()) const timeDate = new Date(time.getFullYear(), time.getMonth(), time.getDate())
const timeDate = new Date(time.getFullYear(), time.getMonth(), const daysDiff = Math.floor((nowDate - timeDate) / (1000 * 60 * 60 * 24))
time.getDate())
const daysDiff = Math.floor(
(nowDate - timeDate) / (1000 * 60 * 60 * 24))
if (daysDiff === 0) { if (daysDiff === 0) {
return formatTime(time) return formatTime(time)
@@ -299,8 +323,7 @@ Singleton {
function formatTime(date) { function formatTime(date) {
let use24Hour = true let use24Hour = true
try { try {
if (typeof SettingsData !== "undefined" if (typeof SettingsData !== "undefined" && SettingsData.use24HourClock !== undefined) {
&& SettingsData.use24HourClock !== undefined) {
use24Hour = SettingsData.use24HourClock use24Hour = SettingsData.use24HourClock
} }
} catch (e) { } catch (e) {
@@ -318,7 +341,9 @@ Singleton {
readonly property string summary: notification.summary readonly property string summary: notification.summary
readonly property string body: notification.body readonly property string body: notification.body
readonly property string htmlBody: { readonly property string htmlBody: {
if (!popup && !root.popupsDisabled) return "" if (!popup && !root.popupsDisabled) {
return ""
}
if (body && (body.includes('<') && body.includes('>'))) { if (body && (body.includes('<') && body.includes('>'))) {
return body return body
} }
@@ -337,8 +362,9 @@ Singleton {
readonly property string desktopEntry: notification.desktopEntry readonly property string desktopEntry: notification.desktopEntry
readonly property string image: notification.image readonly property string image: notification.image
readonly property string cleanImage: { readonly property string cleanImage: {
if (!image) if (!image) {
return "" return ""
}
if (image.startsWith("file://")) { if (image.startsWith("file://")) {
return image.substring(7) return image.substring(7)
} }
@@ -354,12 +380,12 @@ Singleton {
root.allWrappers = root.allWrappers.filter(w => w !== wrapper) root.allWrappers = root.allWrappers.filter(w => w !== wrapper)
root.notifications = root.notifications.filter(w => w !== wrapper) root.notifications = root.notifications.filter(w => w !== wrapper)
if (root.bulkDismissing) if (root.bulkDismissing) {
return return
}
const groupKey = getGroupKey(wrapper) const groupKey = getGroupKey(wrapper)
const remainingInGroup = root.notifications.filter( const remainingInGroup = root.notifications.filter(n => getGroupKey(n) === groupKey)
n => getGroupKey(n) === groupKey)
if (remainingInGroup.length <= 1) { if (remainingInGroup.length <= 1) {
clearGroupExpansionState(groupKey) clearGroupExpansionState(groupKey)
@@ -392,20 +418,23 @@ Singleton {
visibleNotifications = [] visibleNotifications = []
_dismissQueue = notifications.slice() _dismissQueue = notifications.slice()
if (notifications.length) if (notifications.length) {
notifications = [] notifications = []
}
expandedGroups = {} expandedGroups = {}
expandedMessages = {} expandedMessages = {}
_suspendGrouping = true _suspendGrouping = true
if (!dismissPump.running && _dismissQueue.length) if (!dismissPump.running && _dismissQueue.length) {
dismissPump.start() dismissPump.start()
}
} }
function dismissNotification(wrapper) { function dismissNotification(wrapper) {
if (!wrapper || !wrapper.notification) if (!wrapper || !wrapper.notification) {
return return
}
wrapper.popup = false wrapper.popup = false
wrapper.notification.dismiss() wrapper.notification.dismiss()
} }
@@ -422,14 +451,18 @@ Singleton {
} }
function processQueue() { function processQueue() {
if (addGateBusy) if (addGateBusy) {
return return
if (popupsDisabled) }
if (popupsDisabled) {
return return
if (SessionData.doNotDisturb) }
if (SessionData.doNotDisturb) {
return return
if (notificationQueue.length === 0) }
if (notificationQueue.length === 0) {
return return
}
const activePopupCount = visibleNotifications.filter(n => n && n.popup).length const activePopupCount = visibleNotifications.filter(n => n && n.popup).length
if (activePopupCount >= 4) { if (activePopupCount >= 4) {
@@ -461,10 +494,12 @@ Singleton {
if (w && w.destroy && !w.isPersistent && notifications.indexOf(w) === -1) { if (w && w.destroy && !w.isPersistent && notifications.indexOf(w) === -1) {
Qt.callLater(() => { Qt.callLater(() => {
try { try {
w.destroy() w.destroy()
} catch (e) {} } catch (e) {
})
}
})
} }
} }
@@ -490,8 +525,9 @@ Singleton {
function _recomputeGroupsLater() { function _recomputeGroupsLater() {
_groupsDirty = true _groupsDirty = true
if (!groupsDebounce.running) if (!groupsDebounce.running) {
groupsDebounce.start() groupsDebounce.start()
}
} }
function _calcGroupedNotifications() { function _calcGroupedNotifications() {
@@ -520,15 +556,12 @@ Singleton {
} }
return Object.values(groups).sort((a, b) => { return Object.values(groups).sort((a, b) => {
const aUrgency = a.latestNotification.urgency const aUrgency = a.latestNotification.urgency || NotificationUrgency.Low
|| NotificationUrgency.Low const bUrgency = b.latestNotification.urgency || NotificationUrgency.Low
const bUrgency = b.latestNotification.urgency
|| NotificationUrgency.Low
if (aUrgency !== bUrgency) { if (aUrgency !== bUrgency) {
return bUrgency - aUrgency return bUrgency - aUrgency
} }
return b.latestNotification.time.getTime( return b.latestNotification.time.getTime() - a.latestNotification.time.getTime()
) - a.latestNotification.time.getTime()
}) })
} }
@@ -558,8 +591,7 @@ Singleton {
} }
return Object.values(groups).sort((a, b) => { return Object.values(groups).sort((a, b) => {
return b.latestNotification.time.getTime( return b.latestNotification.time.getTime() - a.latestNotification.time.getTime()
) - a.latestNotification.time.getTime()
}) })
} }
@@ -582,8 +614,7 @@ Singleton {
} }
} else { } else {
for (const notif of allWrappers) { for (const notif of allWrappers) {
if (notif && notif.notification && getGroupKey( if (notif && notif.notification && getGroupKey(notif) === groupKey) {
notif) === groupKey) {
notif.notification.dismiss() notif.notification.dismiss()
} }
} }
@@ -617,8 +648,7 @@ Singleton {
expandedGroups = newExpandedGroups expandedGroups = newExpandedGroups
let newExpandedMessages = {} let newExpandedMessages = {}
for (const messageId in expandedMessages) { for (const messageId in expandedMessages) {
if (currentMessageIds.has(messageId) if (currentMessageIds.has(messageId) && expandedMessages[messageId]) {
&& expandedMessages[messageId]) {
newExpandedMessages[messageId] = true newExpandedMessages[messageId] = true
} }
} }

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -14,9 +15,7 @@ Singleton {
property bool settingsPortalAvailable: false property bool settingsPortalAvailable: false
property int systemColorScheme: 0 // 0=default, 1=prefer-dark, 2=prefer-light property int systemColorScheme: 0 // 0=default, 1=prefer-dark, 2=prefer-light
function init() { function init() {}
// Stub just to force IPC registration
}
function getSystemProfileImage() { function getSystemProfileImage() {
systemProfileCheckProcess.running = true systemProfileCheckProcess.running = true
@@ -40,22 +39,23 @@ Singleton {
} }
function setSystemColorScheme(isLightMode) { function setSystemColorScheme(isLightMode) {
if (!settingsPortalAvailable) if (!settingsPortalAvailable) {
return return
}
var colorScheme = isLightMode ? "prefer-light" : "prefer-dark" const colorScheme = isLightMode ? "prefer-light" : "prefer-dark"
var script = "gsettings set org.gnome.desktop.interface color-scheme '" + colorScheme + "'" const script = `gsettings set org.gnome.desktop.interface color-scheme '${colorScheme}'`
systemColorSchemeSetProcess.command = ["bash", "-c", script] systemColorSchemeSetProcess.command = ["bash", "-c", script]
systemColorSchemeSetProcess.running = true systemColorSchemeSetProcess.running = true
} }
function setSystemProfileImage(imagePath) { function setSystemProfileImage(imagePath) {
if (!accountsServiceAvailable || !imagePath) if (!accountsServiceAvailable || !imagePath) {
return return
}
var script = ["dbus-send --system --print-reply --dest=org.freedesktop.Accounts", "/org/freedesktop/Accounts/User$(id -u)", "org.freedesktop.Accounts.User.SetIconFile", "string:'" + imagePath + "'"].join( const script = `dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.Accounts.User.SetIconFile string:'${imagePath}'`
" ")
systemProfileSetProcess.command = ["bash", "-c", script] systemProfileSetProcess.command = ["bash", "-c", script]
systemProfileSetProcess.running = true systemProfileSetProcess.running = true
@@ -94,9 +94,8 @@ Singleton {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
var match = text.match(/string\s+"([^"]+)"/) const match = text.match(/string\s+"([^"]+)"/)
if (match && match[1] && match[1] !== "" if (match && match[1] && match[1] !== "" && match[1] !== "/var/lib/AccountsService/icons/") {
&& match[1] !== "/var/lib/AccountsService/icons/") {
root.systemProfileImage = match[1] root.systemProfileImage = match[1]
if (!root.profileImage || root.profileImage === "") { if (!root.profileImage || root.profileImage === "") {
@@ -144,12 +143,12 @@ Singleton {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
var match = text.match(/uint32 (\d+)/) const match = text.match(/uint32 (\d+)/)
if (match && match[1]) { if (match && match[1]) {
root.systemColorScheme = parseInt(match[1]) root.systemColorScheme = parseInt(match[1])
if (typeof Theme !== "undefined") { if (typeof Theme !== "undefined") {
var shouldBeLightMode = (root.systemColorScheme === 2) const shouldBeLightMode = (root.systemColorScheme === 2)
if (Theme.isLightMode !== shouldBeLightMode) { if (Theme.isLightMode !== shouldBeLightMode) {
Theme.isLightMode = shouldBeLightMode Theme.isLightMode = shouldBeLightMode
if (typeof SessionData !== "undefined") { if (typeof SessionData !== "undefined") {
@@ -193,9 +192,7 @@ Singleton {
return "ERROR: No path provided" return "ERROR: No path provided"
} }
var absolutePath = path.startsWith( const absolutePath = path.startsWith("/") ? path : `${StandardPaths.writableLocation(StandardPaths.HomeLocation)}/${path}`
"/") ? path : StandardPaths.writableLocation(
StandardPaths.HomeLocation) + "/" + path
try { try {
root.setProfileImage(absolutePath) root.setProfileImage(absolutePath)

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -10,13 +11,15 @@ Singleton {
id: root id: root
readonly property bool microphoneActive: { readonly property bool microphoneActive: {
if (!Pipewire.ready || !Pipewire.nodes?.values) if (!Pipewire.ready || !Pipewire.nodes?.values) {
return false return false
}
for (var i = 0; i < Pipewire.nodes.values.length; i++) { for (let i = 0; i < Pipewire.nodes.values.length; i++) {
const node = Pipewire.nodes.values[i] const node = Pipewire.nodes.values[i]
if (!node) if (!node) {
continue continue
}
if ((node.type & PwNodeType.AudioInStream) === PwNodeType.AudioInStream) { if ((node.type & PwNodeType.AudioInStream) === PwNodeType.AudioInStream) {
if (!looksLikeSystemVirtualMic(node)) { if (!looksLikeSystemVirtualMic(node)) {
@@ -32,22 +35,21 @@ Singleton {
} }
PwObjectTracker { PwObjectTracker {
objects: Pipewire.nodes.values.filter( objects: Pipewire.nodes.values.filter(node => node.audio && !node.isStream)
node => node.audio && !node.isStream
)
} }
readonly property bool cameraActive: { readonly property bool cameraActive: {
if (!Pipewire.ready || !Pipewire.nodes?.values) if (!Pipewire.ready || !Pipewire.nodes?.values) {
return false return false
}
for (var i = 0; i < Pipewire.nodes.values.length; i++) { for (let i = 0; i < Pipewire.nodes.values.length; i++) {
const node = Pipewire.nodes.values[i] const node = Pipewire.nodes.values[i]
if (!node || !node.ready) if (!node || !node.ready) {
continue continue
}
if (node.properties if (node.properties && node.properties["media.class"] === "Stream/Input/Video") {
&& node.properties["media.class"] === "Stream/Input/Video") {
if (node.properties["stream.is-live"] === "true") { if (node.properties["stream.is-live"] === "true") {
return true return true
} }
@@ -57,13 +59,15 @@ Singleton {
} }
readonly property bool screensharingActive: { readonly property bool screensharingActive: {
if (!Pipewire.ready || !Pipewire.nodes?.values) if (!Pipewire.ready || !Pipewire.nodes?.values) {
return false return false
}
for (var i = 0; i < Pipewire.nodes.values.length; i++) { for (let i = 0; i < Pipewire.nodes.values.length; i++) {
const node = Pipewire.nodes.values[i] const node = Pipewire.nodes.values[i]
if (!node || !node.ready) if (!node || !node.ready) {
continue continue
}
if ((node.type & PwNodeType.VideoSource) === PwNodeType.VideoSource) { if ((node.type & PwNodeType.VideoSource) === PwNodeType.VideoSource) {
if (looksLikeScreencast(node)) { if (looksLikeScreencast(node)) {
@@ -71,15 +75,11 @@ Singleton {
} }
} }
if (node.properties if (node.properties && node.properties["media.class"] === "Stream/Input/Audio") {
&& node.properties["media.class"] === "Stream/Input/Audio") { const mediaName = (node.properties["media.name"] || "").toLowerCase()
const mediaName = (node.properties["media.name"] const appName = (node.properties["application.name"] || "").toLowerCase()
|| "").toLowerCase()
const appName = (node.properties["application.name"]
|| "").toLowerCase()
if (mediaName.includes("desktop") || appName.includes("screen") if (mediaName.includes("desktop") || appName.includes("screen") || appName === "obs") {
|| appName === "obs") {
if (node.properties["stream.is-live"] === "true") { if (node.properties["stream.is-live"] === "true") {
if (node.audio && node.audio.muted) { if (node.audio && node.audio.muted) {
return false return false
@@ -92,30 +92,27 @@ Singleton {
return false return false
} }
readonly property bool anyPrivacyActive: microphoneActive || cameraActive readonly property bool anyPrivacyActive: microphoneActive || cameraActive || screensharingActive
|| screensharingActive
function looksLikeSystemVirtualMic(node) { function looksLikeSystemVirtualMic(node) {
if (!node) if (!node) {
return false return false
}
const name = (node.name || "").toLowerCase() const name = (node.name || "").toLowerCase()
const mediaName = (node.properties && node.properties["media.name"] const mediaName = (node.properties && node.properties["media.name"] || "").toLowerCase()
|| "").toLowerCase() const appName = (node.properties && node.properties["application.name"] || "").toLowerCase()
const appName = (node.properties && node.properties["application.name"]
|| "").toLowerCase()
const combined = name + " " + mediaName + " " + appName const combined = name + " " + mediaName + " " + appName
return /cava|monitor|system/.test(combined) return /cava|monitor|system/.test(combined)
} }
function looksLikeScreencast(node) { function looksLikeScreencast(node) {
if (!node) if (!node) {
return false return false
const appName = (node.properties && node.properties["application.name"] }
|| "").toLowerCase() const appName = (node.properties && node.properties["application.name"] || "").toLowerCase()
const nodeName = (node.name || "").toLowerCase() const nodeName = (node.name || "").toLowerCase()
const combined = appName + " " + nodeName const combined = appName + " " + nodeName
return /xdg-desktop-portal|xdpw|screencast|screen|gnome shell|kwin|obs/.test( return /xdg-desktop-portal|xdpw|screencast|screen|gnome shell|kwin|obs/.test(combined)
combined)
} }
function getMicrophoneStatus() { function getMicrophoneStatus() {
@@ -132,14 +129,16 @@ Singleton {
function getPrivacySummary() { function getPrivacySummary() {
const active = [] const active = []
if (microphoneActive) if (microphoneActive) {
active.push("microphone") active.push("microphone")
if (cameraActive) }
if (cameraActive) {
active.push("camera") active.push("camera")
if (screensharingActive) }
if (screensharingActive) {
active.push("screensharing") active.push("screensharing")
}
return active.length > 0 ? "Privacy active: " + active.join( return active.length > 0 ? `Privacy active: ${active.join(", ")}` : "No privacy concerns detected"
", ") : "No privacy concerns detected"
} }
} }

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -41,11 +42,6 @@ Singleton {
} }
} }
// ! TODO - hacky because uwsm doesnt behave as expected
// uwsm idk, always passes the is-active check even if it's not a session
// It reutrns exit code 0 when uwsm stop fails
// They have flaws in their system, so we need to be hacky to just try it and
// detect random text
Process { Process {
id: uwsmLogout id: uwsmLogout
command: ["uwsm", "stop"] command: ["uwsm", "stop"]
@@ -53,14 +49,14 @@ Singleton {
stdout: SplitParser { stdout: SplitParser {
splitMarker: "\n" splitMarker: "\n"
onRead: (data) => { onRead: data => {
if (data.trim().toLowerCase().includes("not running")) { if (data.trim().toLowerCase().includes("not running")) {
_logout() _logout()
} }
} }
} }
onExited: function(exitCode) { onExited: function (exitCode) {
if (exitCode === 0) { if (exitCode === 0) {
return return
} }
@@ -69,8 +65,9 @@ Singleton {
} }
function logout() { function logout() {
if (hasUwsm) if (hasUwsm) {
uwsmLogout.running = true uwsmLogout.running = true
}
_logout() _logout()
} }
@@ -100,15 +97,17 @@ Singleton {
signal inhibitorChanged signal inhibitorChanged
function enableIdleInhibit() { function enableIdleInhibit() {
if (idleInhibited) if (idleInhibited) {
return return
}
idleInhibited = true idleInhibited = true
inhibitorChanged() inhibitorChanged()
} }
function disableIdleInhibit() { function disableIdleInhibit() {
if (!idleInhibited) if (!idleInhibited) {
return return
}
idleInhibited = false idleInhibited = false
inhibitorChanged() inhibitorChanged()
} }
@@ -129,8 +128,9 @@ Singleton {
idleInhibited = false idleInhibited = false
Qt.callLater(() => { Qt.callLater(() => {
if (wasActive) if (wasActive) {
idleInhibited = true idleInhibited = true
}
}) })
} }
} }
@@ -143,16 +143,14 @@ Singleton {
return ["true"] return ["true"]
} }
return [isElogind ? "elogind-inhibit" : "systemd-inhibit", "--what=idle", "--who=quickshell", "--why=" return [isElogind ? "elogind-inhibit" : "systemd-inhibit", "--what=idle", "--who=quickshell", `--why=${inhibitReason}`, "--mode=block", "sleep", "infinity"]
+ inhibitReason, "--mode=block", "sleep", "infinity"]
} }
running: idleInhibited running: idleInhibited
onExited: function (exitCode) { onExited: function (exitCode) {
if (idleInhibited && exitCode !== 0) { if (idleInhibited && exitCode !== 0) {
console.warn("SessionService: Inhibitor process crashed with exit code:", console.warn("SessionService: Inhibitor process crashed with exit code:", exitCode)
exitCode)
idleInhibited = false idleInhibited = false
ToastService.showWarning("Idle inhibitor failed") ToastService.showWarning("Idle inhibitor failed")
} }
@@ -181,11 +179,11 @@ Singleton {
function reason(newReason: string): string { function reason(newReason: string): string {
if (!newReason) { if (!newReason) {
return "Current reason: " + root.inhibitReason return `Current reason: ${root.inhibitReason}`
} }
root.setInhibitReason(newReason) root.setInhibitReason(newReason)
return "Inhibit reason set to: " + newReason return `Inhibit reason set to: ${newReason}`
} }
target: "inhibit" target: "inhibit"

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -24,8 +25,9 @@ Singleton {
"level": level, "level": level,
"details": details "details": details
}) })
if (!toastVisible) if (!toastVisible) {
processQueue() processQueue()
}
} }
function showInfo(message, details = "") { function showInfo(message, details = "") {
@@ -48,13 +50,15 @@ Singleton {
currentLevel = levelInfo currentLevel = levelInfo
toastTimer.stop() toastTimer.stop()
resetToastState() resetToastState()
if (toastQueue.length > 0) if (toastQueue.length > 0) {
processQueue() processQueue()
}
} }
function processQueue() { function processQueue() {
if (toastQueue.length === 0) if (toastQueue.length === 0) {
return return
}
const toast = toastQueue.shift() const toast = toastQueue.shift()
currentMessage = toast.message currentMessage = toast.message
@@ -68,8 +72,7 @@ Singleton {
toastTimer.interval = 8000 toastTimer.interval = 8000
toastTimer.start() toastTimer.start()
} else { } else {
toastTimer.interval = toast.level toastTimer.interval = toast.level === levelError ? 5000 : toast.level === levelWarn ? 4000 : 3000
=== levelError ? 5000 : toast.level === levelWarn ? 4000 : 3000
toastTimer.start() toastTimer.start()
} }
} }

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -33,7 +34,6 @@ Singleton {
getUptime() getUptime()
} }
// Get username and full name
Process { Process {
id: userInfoProcess id: userInfoProcess
@@ -60,7 +60,6 @@ Singleton {
} }
} }
// Get system uptime
Process { Process {
id: uptimeProcess id: uptimeProcess
@@ -81,17 +80,21 @@ Singleton {
const minutes = Math.floor((seconds % 3600) / 60) const minutes = Math.floor((seconds % 3600) / 60)
const parts = [] const parts = []
if (days > 0) if (days > 0) {
parts.push(`${days} day${days === 1 ? "" : "s"}`) parts.push(`${days} day${days === 1 ? "" : "s"}`)
if (hours > 0) }
parts.push(`${hours} hour${hours === 1 ? "" : "s"}`) if (hours > 0) {
if (minutes > 0) parts.push(`${hours} hour${hours === 1 ? "" : "s"}`)
parts.push(`${minutes} minute${minutes === 1 ? "" : "s"}`) }
if (minutes > 0) {
parts.push(`${minutes} minute${minutes === 1 ? "" : "s"}`)
}
if (parts.length > 0) if (parts.length > 0) {
root.uptime = "up " + parts.join(", ") root.uptime = `up ${parts.join(", ")}`
else } else {
root.uptime = `up ${seconds} seconds` root.uptime = `up ${seconds} seconds`
}
} }
} }
} }

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -34,7 +35,6 @@ Singleton {
property int minFetchInterval: 30000 // 30 seconds minimum between fetches property int minFetchInterval: 30000 // 30 seconds minimum between fetches
property int persistentRetryCount: 0 // Track persistent retry attempts for backoff property int persistentRetryCount: 0 // Track persistent retry attempts for backoff
// Weather icon mapping (based on wttr.in weather codes)
property var weatherIcons: ({ property var weatherIcons: ({
"113": "clear_day", "113": "clear_day",
"116": "partly_cloudy_day", "116": "partly_cloudy_day",
@@ -105,9 +105,7 @@ Singleton {
function addRef() { function addRef() {
refCount++ refCount++
if (refCount === 1 && !weather.available if (refCount === 1 && !weather.available && SettingsData.weatherEnabled) {
&& SettingsData.weatherEnabled) {
// Start fetching when first consumer appears and weather is enabled
fetchWeather() fetchWeather()
} }
} }
@@ -117,7 +115,6 @@ Singleton {
} }
function fetchWeather() { function fetchWeather() {
// Only fetch if someone is consuming the data and weather is enabled
if (root.refCount === 0 || !SettingsData.weatherEnabled) { if (root.refCount === 0 || !SettingsData.weatherEnabled) {
return return
} }
@@ -127,7 +124,6 @@ Singleton {
return return
} }
// Check if we've fetched recently to prevent spam
const now = Date.now() const now = Date.now()
if (now - root.lastFetchTime < root.minFetchInterval) { if (now - root.lastFetchTime < root.minFetchInterval) {
console.log("Weather fetch throttled, too soon since last fetch") console.log("Weather fetch throttled, too soon since last fetch")
@@ -137,9 +133,7 @@ Singleton {
console.log("Fetching weather from:", getWeatherUrl()) console.log("Fetching weather from:", getWeatherUrl())
root.lastFetchTime = now root.lastFetchTime = now
root.weather.loading = true root.weather.loading = true
weatherFetcher.command weatherFetcher.command = ["bash", "-c", `curl -s --connect-timeout 10 --max-time 30 '${getWeatherUrl()}'`]
= ["bash", "-c", `curl -s --connect-timeout 10 --max-time 30 '${getWeatherUrl(
)}'`]
weatherFetcher.running = true weatherFetcher.running = true
} }
@@ -151,12 +145,10 @@ Singleton {
function handleWeatherSuccess() { function handleWeatherSuccess() {
root.retryAttempts = 0 root.retryAttempts = 0
root.persistentRetryCount = 0 // Reset persistent retry count on success root.persistentRetryCount = 0
// Stop any persistent retry timer if running
if (persistentRetryTimer.running) { if (persistentRetryTimer.running) {
persistentRetryTimer.stop() persistentRetryTimer.stop()
} }
// Don't restart the timer - let it continue its normal interval
if (updateTimer.interval !== root.updateInterval) { if (updateTimer.interval !== root.updateInterval) {
updateTimer.interval = root.updateInterval updateTimer.interval = root.updateInterval
} }
@@ -165,18 +157,14 @@ Singleton {
function handleWeatherFailure() { function handleWeatherFailure() {
root.retryAttempts++ root.retryAttempts++
if (root.retryAttempts < root.maxRetryAttempts) { if (root.retryAttempts < root.maxRetryAttempts) {
console.log(`Weather fetch failed, retrying in ${root.retryDelay console.log(`Weather fetch failed, retrying in ${root.retryDelay / 1000}s (attempt ${root.retryAttempts}/${root.maxRetryAttempts})`)
/ 1000}s (attempt ${root.retryAttempts}/${root.maxRetryAttempts})`)
retryTimer.start() retryTimer.start()
} else { } else {
console.warn("Weather fetch failed after maximum retry attempts, will keep trying...") console.warn("Weather fetch failed after maximum retry attempts, will keep trying...")
root.weather.available = false root.weather.available = false
root.weather.loading = false root.weather.loading = false
// Reset retry count but keep trying with exponential backoff
root.retryAttempts = 0 root.retryAttempts = 0
// Use exponential backoff: 1min, 2min, 4min, then cap at 5min const backoffDelay = Math.min(60000 * Math.pow(2, persistentRetryCount), 300000)
const backoffDelay = Math.min(60000 * Math.pow(
2, persistentRetryCount), 300000)
persistentRetryCount++ persistentRetryCount++
console.log(`Scheduling persistent retry in ${backoffDelay / 1000}s`) console.log(`Scheduling persistent retry in ${backoffDelay / 1000}s`)
persistentRetryTimer.interval = backoffDelay persistentRetryTimer.interval = backoffDelay
@@ -186,8 +174,7 @@ Singleton {
Process { Process {
id: weatherFetcher id: weatherFetcher
command: ["bash", "-c", `curl -s --connect-timeout 10 --max-time 30 '${root.getWeatherUrl( command: ["bash", "-c", `curl -s --connect-timeout 10 --max-time 30 '${root.getWeatherUrl()}'`]
)}'`]
running: false running: false
stdout: StdioCollector { stdout: StdioCollector {
@@ -206,8 +193,7 @@ Singleton {
const location = data.nearest_area[0] || {} const location = data.nearest_area[0] || {}
const astronomy = data.weather[0]?.astronomy[0] || {} const astronomy = data.weather[0]?.astronomy[0] || {}
if (!Object.keys(current).length || !Object.keys( if (!Object.keys(current).length || !Object.keys(location).length) {
location).length) {
throw new Error("Required fields missing") throw new Error("Required fields missing")
} }
@@ -226,8 +212,7 @@ Singleton {
"pressure": Number(current.pressure) || 0 "pressure": Number(current.pressure) || 0
} }
console.log("Weather updated:", root.weather.city, console.log("Weather updated:", root.weather.city, `${root.weather.temp}°C`)
`${root.weather.temp}°C`)
root.handleWeatherSuccess() root.handleWeatherSuccess()
} catch (e) { } catch (e) {
@@ -268,7 +253,7 @@ Singleton {
Timer { Timer {
id: persistentRetryTimer id: persistentRetryTimer
interval: 60000 // Will be dynamically set interval: 60000
running: false running: false
repeat: false repeat: false
onTriggered: { onTriggered: {
@@ -279,8 +264,7 @@ Singleton {
Component.onCompleted: { Component.onCompleted: {
SettingsData.weatherCoordinatesChanged.connect(() => { SettingsData.weatherCoordinatesChanged.connect(() => {
console.log( console.log("Weather location changed, force refreshing weather")
"Weather location changed, force refreshing weather")
root.weather = { root.weather = {
"available": false, "available": false,
"loading": true, "loading": true,
@@ -300,16 +284,13 @@ Singleton {
}) })
SettingsData.weatherLocationChanged.connect(() => { SettingsData.weatherLocationChanged.connect(() => {
console.log( console.log("Weather location display name changed")
"Weather location display name changed") const currentWeather = Object.assign({}, root.weather)
const currentWeather = Object.assign(
{}, root.weather)
root.weather = currentWeather root.weather = currentWeather
}) })
SettingsData.useAutoLocationChanged.connect(() => { SettingsData.useAutoLocationChanged.connect(() => {
console.log( console.log("Auto location setting changed, force refreshing weather")
"Auto location setting changed, force refreshing weather")
root.weather = { root.weather = {
"available": false, "available": false,
"loading": true, "loading": true,
@@ -329,16 +310,10 @@ Singleton {
}) })
SettingsData.weatherEnabledChanged.connect(() => { SettingsData.weatherEnabledChanged.connect(() => {
console.log( console.log("Weather enabled setting changed:", SettingsData.weatherEnabled)
"Weather enabled setting changed:", if (SettingsData.weatherEnabled && root.refCount > 0 && !root.weather.available) {
SettingsData.weatherEnabled)
if (SettingsData.weatherEnabled
&& root.refCount > 0
&& !root.weather.available) {
// Start fetching when weather is re-enabled
root.forceRefresh() root.forceRefresh()
} else if (!SettingsData.weatherEnabled) { } else if (!SettingsData.weatherEnabled) {
// Stop all timers when weather is disabled
updateTimer.stop() updateTimer.stop()
retryTimer.stop() retryTimer.stop()
persistentRetryTimer.stop() persistentRetryTimer.stop()

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# https://github.com/jesperhh/qmlfmt # https://github.com/jesperhh/qmlfmt
find . -name "*.qml" -exec qmlfmt -t 4 -i 4 -w {} \; find . -name "*.qml" -exec qmlfmt -t 4 -i 4 -b 250 -w {} \;