1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -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
}
onAccepted: {
NetworkService.connectToWifiWithPassword(
NetworkService.connectToWifi(
wifiPasswordSSID, passwordInput.text)
close()
wifiPasswordInput = ""
@@ -286,7 +286,7 @@ DankModal {
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: {
NetworkService.connectToWifiWithPassword(
NetworkService.connectToWifi(
wifiPasswordSSID,
passwordInput.text)
close()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick
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 {
id: uwsmLogout
command: ["uwsm", "stop"]
@@ -53,14 +49,14 @@ Singleton {
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
onRead: data => {
if (data.trim().toLowerCase().includes("not running")) {
_logout()
}
}
}
onExited: function(exitCode) {
onExited: function (exitCode) {
if (exitCode === 0) {
return
}
@@ -69,8 +65,9 @@ Singleton {
}
function logout() {
if (hasUwsm)
if (hasUwsm) {
uwsmLogout.running = true
}
_logout()
}
@@ -100,15 +97,17 @@ Singleton {
signal inhibitorChanged
function enableIdleInhibit() {
if (idleInhibited)
if (idleInhibited) {
return
}
idleInhibited = true
inhibitorChanged()
}
function disableIdleInhibit() {
if (!idleInhibited)
if (!idleInhibited) {
return
}
idleInhibited = false
inhibitorChanged()
}
@@ -129,8 +128,9 @@ Singleton {
idleInhibited = false
Qt.callLater(() => {
if (wasActive)
idleInhibited = true
if (wasActive) {
idleInhibited = true
}
})
}
}
@@ -143,16 +143,14 @@ Singleton {
return ["true"]
}
return [isElogind ? "elogind-inhibit" : "systemd-inhibit", "--what=idle", "--who=quickshell", "--why="
+ inhibitReason, "--mode=block", "sleep", "infinity"]
return [isElogind ? "elogind-inhibit" : "systemd-inhibit", "--what=idle", "--who=quickshell", `--why=${inhibitReason}`, "--mode=block", "sleep", "infinity"]
}
running: idleInhibited
onExited: function (exitCode) {
if (idleInhibited && exitCode !== 0) {
console.warn("SessionService: Inhibitor process crashed with exit code:",
exitCode)
console.warn("SessionService: Inhibitor process crashed with exit code:", exitCode)
idleInhibited = false
ToastService.showWarning("Idle inhibitor failed")
}
@@ -181,11 +179,11 @@ Singleton {
function reason(newReason: string): string {
if (!newReason) {
return "Current reason: " + root.inhibitReason
return `Current reason: ${root.inhibitReason}`
}
root.setInhibitReason(newReason)
return "Inhibit reason set to: " + newReason
return `Inhibit reason set to: ${newReason}`
}
target: "inhibit"

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash
# 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 {} \;