1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-11 07:52:50 -05:00

qmlfmt with 4 space

This commit is contained in:
bbedward
2025-08-20 00:05:14 -04:00
parent 6e0977c719
commit b688bbfe83
154 changed files with 28809 additions and 27639 deletions

View File

@@ -9,149 +9,153 @@ import Quickshell.Widgets
import "../Common/fuzzysort.js" as Fuzzy
Singleton {
id: root
id: root
property var applications: DesktopEntries.applications.values
property var applications: DesktopEntries.applications.values
property var preppedApps: applications.map(app => ({
"name": Fuzzy.prepare(
app.name
|| ""),
"comment": Fuzzy.prepare(
app.comment
|| ""),
"entry": app
}))
property var preppedApps: applications.map(app => ({
"name": Fuzzy.prepare(
app.name
|| ""),
"comment": Fuzzy.prepare(
app.comment
|| ""),
"entry": app
}))
function searchApplications(query) {
if (!query || query.length === 0) {
return applications
}
function searchApplications(query) {
if (!query || query.length === 0) {
return applications
}
if (preppedApps.length === 0) {
return []
}
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
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
if (nameScore > 0) {
var queryLower = query.toLowerCase()
var nameLower = appName.toLowerCase()
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 (nameLower === queryLower) {
finalScore = nameScore * 100
} else if (nameLower.startsWith(
queryLower)) {
finalScore = nameScore * 50
} else if (nameLower.includes(
" " + queryLower)
|| nameLower.includes(
queryLower + " ")
|| nameLower.endsWith(
" " + queryLower)) {
finalScore = nameScore * 25
} else if (nameLower.includes(
queryLower)) {
finalScore = nameScore * 10
} else {
finalScore = nameScore * 2 + commentScore * 0.1
}
} else {
finalScore = commentScore * 0.1
}
return finalScore
},
"limit": 50
})
return results.map(r => r.obj.entry)
}
function getCategoriesForApp(app) {
if (!app || !app.categories)
return []
var categoryMap = {
"AudioVideo": "Media",
"Audio": "Media",
"Video": "Media",
"Development": "Development",
"TextEditor": "Development",
"IDE": "Development",
"Education": "Education",
"Game": "Games",
"Graphics": "Graphics",
"Photography": "Graphics",
"Network": "Internet",
"WebBrowser": "Internet",
"Email": "Internet",
"Office": "Office",
"WordProcessor": "Office",
"Spreadsheet": "Office",
"Presentation": "Office",
"Science": "Science",
"Settings": "Settings",
"System": "System",
"Utility": "Utilities",
"Accessories": "Utilities",
"FileManager": "Utilities",
"TerminalEmulator": "Utilities"
}
var mappedCategories = new Set()
for (var i = 0; i < app.categories.length; i++) {
var cat = app.categories[i]
if (categoryMap[cat]) {
mappedCategories.add(categoryMap[cat])
}
}
return Array.from(mappedCategories)
}
// Category icon mappings
property var categoryIcons: ({
"All": "apps",
"Media": "music_video",
"Development": "code",
"Games": "sports_esports",
"Graphics": "photo_library",
"Internet": "web",
"Office": "content_paste",
"Settings": "settings",
"System": "host",
"Utilities": "build"
return finalScore
},
"limit": 50
})
function getCategoryIcon(category) {
return categoryIcons[category] || "folder"
}
function getAllCategories() {
var categories = new Set(["All"])
for (var i = 0; i < applications.length; i++) {
var appCategories = getCategoriesForApp(applications[i])
appCategories.forEach(cat => categories.add(cat))
return results.map(r => r.obj.entry)
}
return Array.from(categories).sort()
}
function getCategoriesForApp(app) {
if (!app || !app.categories)
return []
function getAppsInCategory(category) {
if (category === "All") {
return applications
var categoryMap = {
"AudioVideo": "Media",
"Audio": "Media",
"Video": "Media",
"Development": "Development",
"TextEditor": "Development",
"IDE": "Development",
"Education": "Education",
"Game": "Games",
"Graphics": "Graphics",
"Photography": "Graphics",
"Network": "Internet",
"WebBrowser": "Internet",
"Email": "Internet",
"Office": "Office",
"WordProcessor": "Office",
"Spreadsheet": "Office",
"Presentation": "Office",
"Science": "Science",
"Settings": "Settings",
"System": "System",
"Utility": "Utilities",
"Accessories": "Utilities",
"FileManager": "Utilities",
"TerminalEmulator": "Utilities"
}
var mappedCategories = new Set()
for (var i = 0; i < app.categories.length; i++) {
var cat = app.categories[i]
if (categoryMap[cat]) {
mappedCategories.add(categoryMap[cat])
}
}
return Array.from(mappedCategories)
}
return applications.filter(app => {
var appCategories = getCategoriesForApp(app)
return appCategories.includes(category)
})
}
// Category icon mappings
property var categoryIcons: ({
"All": "apps",
"Media": "music_video",
"Development": "code",
"Games": "sports_esports",
"Graphics": "photo_library",
"Internet": "web",
"Office": "content_paste",
"Settings": "settings",
"System": "host",
"Utilities": "build"
})
function getCategoryIcon(category) {
return categoryIcons[category] || "folder"
}
function getAllCategories() {
var categories = new Set(["All"])
for (var i = 0; i < applications.length; i++) {
var appCategories = getCategoriesForApp(applications[i])
appCategories.forEach(cat => categories.add(cat))
}
return Array.from(categories).sort()
}
function getAppsInCategory(category) {
if (category === "All") {
return applications
}
return applications.filter(app => {
var appCategories = getCategoriesForApp(
app)
return appCategories.includes(category)
})
}
}

View File

@@ -8,180 +8,183 @@ import Quickshell.Io
import Quickshell.Services.Pipewire
Singleton {
id: root
id: root
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode source: Pipewire.defaultAudioSource
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode source: Pipewire.defaultAudioSource
signal volumeChanged
signal micMuteChanged
signal volumeChanged
signal micMuteChanged
function displayName(node) {
if (!node)
return ""
function displayName(node) {
if (!node)
return ""
if (node.properties && node.properties["device.description"]) {
return node.properties["device.description"]
}
if (node.description && node.description !== node.name) {
return node.description
}
if (node.nickname && node.nickname !== node.name) {
return node.nickname
}
if (node.name.includes("analog-stereo"))
return "Built-in Speakers"
else if (node.name.includes("bluez"))
return "Bluetooth Audio"
else if (node.name.includes("usb"))
return "USB Audio"
else if (node.name.includes("hdmi"))
return "HDMI Audio"
return node.name
}
function subtitle(name) {
if (!name)
return ""
if (name.includes('usb-')) {
if (name.includes('SteelSeries')) {
return "USB Gaming Headset"
} else if (name.includes('Generic')) {
return "USB Audio Device"
}
return "USB Audio"
} else 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')) {
return "Bluetooth Audio"
} else if (name.includes('analog')) {
return "Built-in Audio"
} else if (name.includes('hdmi')) {
return "HDMI Audio"
}
return ""
}
PwObjectTracker {
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
}
// 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 + "%"
}
return "No audio sink available"
}
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"
}
return "No audio sink available"
}
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 + "%"
}
return "No audio source available"
}
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"
}
return "No audio source available"
}
// IPC Handler for external control
IpcHandler {
target: "audio"
function setvolume(percentage: string): string {
return root.setVolume(parseInt(percentage))
}
function increment(step: string): string {
if (root.sink && root.sink.audio) {
if (root.sink.audio.muted) {
root.sink.audio.muted = false
if (node.properties && node.properties["device.description"]) {
return node.properties["device.description"]
}
const currentVolume = Math.round(root.sink.audio.volume * 100)
const newVolume = Math.max(0, Math.min(100, currentVolume + parseInt(
step || "5")))
root.sink.audio.volume = newVolume / 100
root.volumeChanged()
return "Volume increased to " + newVolume + "%"
}
return "No audio sink available"
}
function decrement(step: string): string {
if (root.sink && root.sink.audio) {
if (root.sink.audio.muted) {
root.sink.audio.muted = false
if (node.description && node.description !== node.name) {
return node.description
}
const currentVolume = Math.round(root.sink.audio.volume * 100)
const newVolume = Math.max(0, Math.min(100, currentVolume - parseInt(
step || "5")))
root.sink.audio.volume = newVolume / 100
root.volumeChanged()
return "Volume decreased to " + newVolume + "%"
}
return "No audio sink available"
if (node.nickname && node.nickname !== node.name) {
return node.nickname
}
if (node.name.includes("analog-stereo"))
return "Built-in Speakers"
else if (node.name.includes("bluez"))
return "Bluetooth Audio"
else if (node.name.includes("usb"))
return "USB Audio"
else if (node.name.includes("hdmi"))
return "HDMI Audio"
return node.name
}
function mute(): string {
const result = root.toggleMute()
root.volumeChanged()
return result
function subtitle(name) {
if (!name)
return ""
if (name.includes('usb-')) {
if (name.includes('SteelSeries')) {
return "USB Gaming Headset"
} else if (name.includes('Generic')) {
return "USB Audio Device"
}
return "USB Audio"
} else 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')) {
return "Bluetooth Audio"
} else if (name.includes('analog')) {
return "Built-in Audio"
} else if (name.includes('hdmi')) {
return "HDMI Audio"
}
return ""
}
function setmic(percentage: string): string {
return root.setMicVolume(parseInt(percentage))
PwObjectTracker {
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
}
function micmute(): string {
const result = root.toggleMicMute()
root.micMuteChanged()
return result
// 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 + "%"
}
return "No audio sink available"
}
function status(): string {
let result = "Audio Status:\n"
if (root.sink && root.sink.audio) {
const volume = Math.round(root.sink.audio.volume * 100)
result += "Output: " + volume + "%" + (root.sink.audio.muted ? " (muted)" : "") + "\n"
} else {
result += "Output: No sink available\n"
}
if (root.source && root.source.audio) {
const micVolume = Math.round(root.source.audio.volume * 100)
result += "Input: " + micVolume + "%" + (root.source.audio.muted ? " (muted)" : "")
} else {
result += "Input: No source available"
}
return result
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"
}
return "No audio sink available"
}
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 + "%"
}
return "No audio source available"
}
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"
}
return "No audio source available"
}
// IPC Handler for external control
IpcHandler {
target: "audio"
function setvolume(percentage: string): string {
return root.setVolume(parseInt(percentage))
}
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 + "%"
}
return "No audio sink available"
}
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 + "%"
}
return "No audio sink available"
}
function mute(): string {
const result = root.toggleMute()
root.volumeChanged()
return result
}
function setmic(percentage: string): string {
return root.setMicVolume(parseInt(percentage))
}
function micmute(): string {
const result = root.toggleMicMute()
root.micMuteChanged()
return result
}
function status(): string {
let result = "Audio Status:\n"
if (root.sink && root.sink.audio) {
const volume = Math.round(root.sink.audio.volume * 100)
result += "Output: " + volume + "%"
+ (root.sink.audio.muted ? " (muted)" : "") + "\n"
} else {
result += "Output: No sink available\n"
}
if (root.source && root.source.audio) {
const micVolume = Math.round(root.source.audio.volume * 100)
result += "Input: " + micVolume + "%" + (root.source.audio.muted ? " (muted)" : "")
} else {
result += "Input: No source available"
}
return result
}
}
}
}

View File

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick
import Quickshell
@@ -9,50 +10,71 @@ 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"
return "N/A"
if (device.healthSupported && device.healthPercentage > 0)
return Math.round(device.healthPercentage) + "%"
return Math.round(device.healthPercentage) + "%"
// Calculate health from energy capacity vs design capacity
if (device.energyCapacity > 0 && device.energy > 0) {
// 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
var healthPercent = (device.energyCapacity / 90.0045)
* 100 // your design capacity from upower
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 (device.state === UPowerDeviceState.Charging && device.changeRate <= 0)
return "Plugged In"
return "No Battery"
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 = []
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)) {
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)) {
btDevices.push({
"name": dev.model || UPowerDeviceType.toString(dev.type),
"percentage": Math.round(dev.percentage),
"type": dev.type
})
"name": dev.model
|| UPowerDeviceType.toString(
dev.type),
"percentage": Math.round(dev.percentage),
"type": dev.type
})
}
}
return btDevices
@@ -63,13 +85,13 @@ Singleton {
return "Unknown"
var timeSeconds = isCharging ? device.timeToFull : device.timeToEmpty
if (!timeSeconds || timeSeconds <= 0 || timeSeconds > 86400)
return "Unknown"
var hours = Math.floor(timeSeconds / 3600)
var minutes = Math.floor((timeSeconds % 3600) / 60)
if (hours > 0)
return hours + "h " + minutes + "m"
else

View File

@@ -7,149 +7,153 @@ import Quickshell
import Quickshell.Bluetooth
Singleton {
id: root
id: root
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 var devices: adapter ? adapter.devices : null
readonly property var pairedDevices: {
if (!adapter || !adapter.devices)
return []
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 var devices: adapter ? adapter.devices : null
readonly property var pairedDevices: {
if (!adapter || !adapter.devices)
return []
return adapter.devices.values.filter(dev => {
return dev && (dev.paired
|| dev.trusted)
})
}
readonly property var allDevicesWithBattery: {
if (!adapter || !adapter.devices)
return []
return adapter.devices.values.filter(dev => {
return dev && (dev.paired
|| dev.trusted)
})
}
readonly property var allDevicesWithBattery: {
if (!adapter || !adapter.devices)
return []
return adapter.devices.values.filter(dev => {
return dev && dev.batteryAvailable
&& dev.battery > 0
})
}
return adapter.devices.values.filter(dev => {
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 || ""
function sortDevices(devices) {
return devices.sort((a, b) => {
var aName = a.name || a.deviceName || ""
var bName = b.name || b.deviceName || ""
var aHasRealName = aName.includes(" ")
&& aName.length > 3
var bHasRealName = bName.includes(" ")
&& bName.length > 3
var aHasRealName = aName.includes(" ")
&& aName.length > 3
var 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
return bSignal - aSignal
})
}
var aSignal = (a.signalStrength !== undefined
&& a.signalStrength > 0) ? a.signalStrength : 0
var bSignal = (b.signalStrength !== undefined
&& b.signalStrength > 0) ? b.signalStrength : 0
return bSignal - aSignal
})
}
function getDeviceIcon(device) {
if (!device)
return "bluetooth"
function getDeviceIcon(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"))
return "headset"
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"))
return "headset"
if (icon.includes("mouse") || name.includes("mouse"))
return "mouse"
if (icon.includes("mouse") || name.includes("mouse"))
return "mouse"
if (icon.includes("keyboard") || name.includes("keyboard"))
return "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"))
return "smartphone"
if (icon.includes("phone") || name.includes("phone") || name.includes(
"iphone") || name.includes("android") || name.includes(
"samsung"))
return "smartphone"
if (icon.includes("watch") || name.includes("watch"))
return "watch"
if (icon.includes("watch") || name.includes("watch"))
return "watch"
if (icon.includes("speaker") || name.includes("speaker"))
return "speaker"
if (icon.includes("speaker") || name.includes("speaker"))
return "speaker"
if (icon.includes("display") || name.includes("tv"))
return "tv"
if (icon.includes("display") || name.includes("tv"))
return "tv"
return "bluetooth"
}
return "bluetooth"
}
function canConnect(device) {
if (!device)
return false
function canConnect(device) {
if (!device)
return false
return !device.paired && !device.pairing && !device.blocked
}
return !device.paired && !device.pairing && !device.blocked
}
function getSignalStrength(device) {
if (!device || device.signalStrength === undefined
|| device.signalStrength <= 0)
return "Unknown"
function getSignalStrength(device) {
if (!device || device.signalStrength === undefined
|| device.signalStrength <= 0)
return "Unknown"
var signal = device.signalStrength
if (signal >= 80)
return "Excellent"
var signal = device.signalStrength
if (signal >= 80)
return "Excellent"
if (signal >= 60)
return "Good"
if (signal >= 60)
return "Good"
if (signal >= 40)
return "Fair"
if (signal >= 40)
return "Fair"
if (signal >= 20)
return "Poor"
if (signal >= 20)
return "Poor"
return "Very Poor"
}
return "Very Poor"
}
function getSignalIcon(device) {
if (!device || device.signalStrength === undefined
|| device.signalStrength <= 0)
return "signal_cellular_null"
function getSignalIcon(device) {
if (!device || device.signalStrength === undefined
|| device.signalStrength <= 0)
return "signal_cellular_null"
var signal = device.signalStrength
if (signal >= 80)
return "signal_cellular_4_bar"
var signal = device.signalStrength
if (signal >= 80)
return "signal_cellular_4_bar"
if (signal >= 60)
return "signal_cellular_3_bar"
if (signal >= 60)
return "signal_cellular_3_bar"
if (signal >= 40)
return "signal_cellular_2_bar"
if (signal >= 40)
return "signal_cellular_2_bar"
if (signal >= 20)
return "signal_cellular_1_bar"
if (signal >= 20)
return "signal_cellular_1_bar"
return "signal_cellular_0_bar"
}
return "signal_cellular_0_bar"
}
function isDeviceBusy(device) {
if (!device)
return false
return device.pairing || device.state === BluetoothDeviceState.Disconnecting
|| device.state === BluetoothDeviceState.Connecting
}
function isDeviceBusy(device) {
if (!device)
return false
return device.pairing
|| device.state === BluetoothDeviceState.Disconnecting
|| device.state === BluetoothDeviceState.Connecting
}
function connectDeviceWithTrust(device) {
if (!device)
return
function connectDeviceWithTrust(device) {
if (!device)
return
device.trusted = true
device.connect()
}
device.trusted = true
device.connect()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,238 +7,245 @@ import Quickshell
import Quickshell.Io
Singleton {
id: root
id: root
property bool khalAvailable: false
property var eventsByDate: ({})
property bool isLoading: false
property string lastError: ""
property date lastStartDate
property date lastEndDate
property bool khalAvailable: false
property var eventsByDate: ({})
property bool isLoading: false
property string lastError: ""
property date lastStartDate
property date lastEndDate
function checkKhalAvailability() {
if (!khalCheckProcess.running)
khalCheckProcess.running = true
}
function loadCurrentMonth() {
if (!root.khalAvailable)
return
let today = new Date()
let firstDay = new Date(today.getFullYear(), today.getMonth(), 1)
let lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0)
// Add padding
let startDate = new Date(firstDay)
startDate.setDate(startDate.getDate() - firstDay.getDay() - 7)
let endDate = new Date(lastDay)
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7)
loadEvents(startDate, endDate)
}
function loadEvents(startDate, endDate) {
if (!root.khalAvailable) {
return
function checkKhalAvailability() {
if (!khalCheckProcess.running)
khalCheckProcess.running = true
}
if (eventsProcess.running) {
return
function loadCurrentMonth() {
if (!root.khalAvailable)
return
let today = new Date()
let firstDay = new Date(today.getFullYear(), today.getMonth(), 1)
let lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0)
// Add padding
let startDate = new Date(firstDay)
startDate.setDate(startDate.getDate() - firstDay.getDay() - 7)
let endDate = new Date(lastDay)
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7)
loadEvents(startDate, endDate)
}
// Store last requested date range for refresh timer
root.lastStartDate = startDate
root.lastEndDate = endDate
root.isLoading = true
// Format dates for khal (MM/dd/yyyy based on printformats)
let startDateStr = Qt.formatDate(startDate, "MM/dd/yyyy")
let endDateStr = Qt.formatDate(endDate, "MM/dd/yyyy")
eventsProcess.requestStartDate = startDate
eventsProcess.requestEndDate = endDate
eventsProcess.command = ["khal", "list", "--json", "title", "--json", "description", "--json", "start-date", "--json", "start-time", "--json", "end-date", "--json", "end-time", "--json", "all-day", "--json", "location", "--json", "url", startDateStr, endDateStr]
eventsProcess.running = true
}
function getEventsForDate(date) {
let dateKey = Qt.formatDate(date, "yyyy-MM-dd")
return root.eventsByDate[dateKey] || []
}
function hasEventsForDate(date) {
let events = getEventsForDate(date)
return events.length > 0
}
// Initialize on component completion
Component.onCompleted: {
checkKhalAvailability()
}
// Process for checking khal configuration
Process {
id: khalCheckProcess
command: ["khal", "list", "today"]
running: false
onExited: exitCode => {
root.khalAvailable = (exitCode === 0)
if (exitCode === 0) {
loadCurrentMonth()
}
function loadEvents(startDate, endDate) {
if (!root.khalAvailable) {
return
}
if (eventsProcess.running) {
return
}
// Store last requested date range for refresh timer
root.lastStartDate = startDate
root.lastEndDate = endDate
root.isLoading = true
// Format dates for khal (MM/dd/yyyy based on printformats)
let startDateStr = Qt.formatDate(startDate, "MM/dd/yyyy")
let endDateStr = Qt.formatDate(endDate, "MM/dd/yyyy")
eventsProcess.requestStartDate = startDate
eventsProcess.requestEndDate = endDate
eventsProcess.command = ["khal", "list", "--json", "title", "--json", "description", "--json", "start-date", "--json", "start-time", "--json", "end-date", "--json", "end-time", "--json", "all-day", "--json", "location", "--json", "url", startDateStr, endDateStr]
eventsProcess.running = true
}
}
// Process for loading events
Process {
id: eventsProcess
function getEventsForDate(date) {
let dateKey = Qt.formatDate(date, "yyyy-MM-dd")
return root.eventsByDate[dateKey] || []
}
property date requestStartDate
property date requestEndDate
property string rawOutput: ""
function hasEventsForDate(date) {
let events = getEventsForDate(date)
return events.length > 0
}
running: false
onExited: exitCode => {
root.isLoading = false
if (exitCode !== 0) {
root.lastError = "Failed to load events (exit code: " + exitCode + ")"
return
}
try {
let newEventsByDate = {}
let lines = eventsProcess.rawOutput.split('\n')
for (let line of lines) {
line = line.trim()
if (!line || line === "[]")
continue
// Initialize on component completion
Component.onCompleted: {
checkKhalAvailability()
}
// Parse JSON line
let dayEvents = JSON.parse(line)
// Process each event in this day's array
for (let event of dayEvents) {
if (!event.title)
continue
// Process for checking khal configuration
Process {
id: khalCheckProcess
// Parse start and end dates
let startDate, endDate
if (event['start-date']) {
let startParts = event['start-date'].split('/')
startDate = new Date(parseInt(startParts[2]),
parseInt(startParts[0]) - 1,
parseInt(startParts[1]))
} else {
startDate = new Date()
command: ["khal", "list", "today"]
running: false
onExited: exitCode => {
root.khalAvailable = (exitCode === 0)
if (exitCode === 0) {
loadCurrentMonth()
}
if (event['end-date']) {
let endParts = event['end-date'].split('/')
endDate = new Date(parseInt(endParts[2]),
parseInt(endParts[0]) - 1,
parseInt(endParts[1]))
} else {
endDate = new Date(startDate)
}
}
// Process for loading events
Process {
id: eventsProcess
property date requestStartDate
property date requestEndDate
property string rawOutput: ""
running: false
onExited: exitCode => {
root.isLoading = false
if (exitCode !== 0) {
root.lastError = "Failed to load events (exit code: " + exitCode + ")"
return
}
// Create start/end times
let startTime = new Date(startDate)
let endTime = new Date(endDate)
if (event['start-time'] && event['all-day'] !== "True") {
// Parse time if available and not all-day
let timeStr = event['start-time']
if (timeStr) {
let timeParts = timeStr.match(/(\d+):(\d+)/)
if (timeParts) {
startTime.setHours(parseInt(timeParts[1]),
parseInt(timeParts[2]))
if (event['end-time']) {
let endTimeParts = event['end-time'].match(/(\d+):(\d+)/)
if (endTimeParts)
endTime.setHours(parseInt(endTimeParts[1]),
parseInt(endTimeParts[2]))
} else {
// Default to 1 hour duration on same day
endTime = new Date(startTime)
endTime.setHours(startTime.getHours() + 1)
}
try {
let newEventsByDate = {}
let lines = eventsProcess.rawOutput.split('\n')
for (let line of lines) {
line = line.trim()
if (!line || line === "[]")
continue
// Parse JSON line
let dayEvents = JSON.parse(line)
// Process each event in this day's array
for (let event of dayEvents) {
if (!event.title)
continue
// Parse start and end dates
let startDate, endDate
if (event['start-date']) {
let startParts = event['start-date'].split('/')
startDate = new Date(parseInt(startParts[2]),
parseInt(startParts[0]) - 1,
parseInt(startParts[1]))
} else {
startDate = new Date()
}
if (event['end-date']) {
let endParts = event['end-date'].split('/')
endDate = new Date(parseInt(endParts[2]),
parseInt(endParts[0]) - 1,
parseInt(endParts[1]))
} else {
endDate = new Date(startDate)
}
// Create start/end times
let startTime = new Date(startDate)
let endTime = new Date(endDate)
if (event['start-time']
&& event['all-day'] !== "True") {
// Parse time if available and not all-day
let timeStr = event['start-time']
if (timeStr) {
let timeParts = timeStr.match(/(\d+):(\d+)/)
if (timeParts) {
startTime.setHours(parseInt(timeParts[1]),
parseInt(timeParts[2]))
if (event['end-time']) {
let endTimeParts = event['end-time'].match(
/(\d+):(\d+)/)
if (endTimeParts)
endTime.setHours(
parseInt(endTimeParts[1]),
parseInt(endTimeParts[2]))
} else {
// Default to 1 hour duration on same day
endTime = new Date(startTime)
endTime.setHours(
startTime.getHours() + 1)
}
}
}
}
// Create unique ID for this event (to track multi-day events)
let eventId = event.title + "_" + event['start-date']
+ "_" + (event['start-time'] || 'allday')
// Create event object template
let eventTemplate = {
"id": eventId,
"title": event.title || "Untitled Event",
"start": startTime,
"end": endTime,
"location": event.location || "",
"description": event.description || "",
"url": event.url || "",
"calendar": "",
"color": "",
"allDay": event['all-day'] === "True",
"isMultiDay": startDate.toDateString(
) !== endDate.toDateString()
}
// Add event to each day it spans
let currentDate = new Date(startDate)
while (currentDate <= endDate) {
let dateKey = Qt.formatDate(currentDate,
"yyyy-MM-dd")
if (!newEventsByDate[dateKey])
newEventsByDate[dateKey] = []
// Check if this exact event is already added to this date (prevent duplicates)
let existingEvent = newEventsByDate[dateKey].find(
e => {
return e.id === eventId
})
if (existingEvent) {
// Move to next day without adding duplicate
currentDate.setDate(currentDate.getDate() + 1)
continue
}
// Create a copy of the event for this date
let dayEvent = Object.assign({}, eventTemplate)
// For multi-day events, adjust the display time for this specific day
if (currentDate.getTime() === startDate.getTime()) {
// First day - use original start time
dayEvent.start = new Date(startTime)
} else {
// Subsequent days - start at beginning of day for all-day events
dayEvent.start = new Date(currentDate)
if (!dayEvent.allDay)
dayEvent.start.setHours(0, 0, 0, 0)
}
if (currentDate.getTime() === endDate.getTime()) {
// Last day - use original end time
dayEvent.end = new Date(endTime)
} else {
// Earlier days - end at end of day for all-day events
dayEvent.end = new Date(currentDate)
if (!dayEvent.allDay)
dayEvent.end.setHours(23, 59, 59, 999)
}
newEventsByDate[dateKey].push(dayEvent)
// Move to next day
currentDate.setDate(currentDate.getDate() + 1)
}
}
}
}
// Sort events by start time within each date
for (let dateKey in newEventsByDate) {
newEventsByDate[dateKey].sort((a, b) => {
return a.start.getTime(
) - b.start.getTime()
})
}
root.eventsByDate = newEventsByDate
root.lastError = ""
} catch (error) {
root.lastError = "Failed to parse events JSON: " + error.toString()
root.eventsByDate = {}
}
// Create unique ID for this event (to track multi-day events)
let eventId = event.title + "_" + event['start-date'] + "_" + (event['start-time']
|| 'allday')
// Create event object template
let eventTemplate = {
"id": eventId,
"title": event.title || "Untitled Event",
"start": startTime,
"end": endTime,
"location": event.location || "",
"description": event.description || "",
"url": event.url || "",
"calendar": "",
"color": "",
"allDay": event['all-day'] === "True",
"isMultiDay": startDate.toDateString() !== endDate.toDateString()
}
// Add event to each day it spans
let currentDate = new Date(startDate)
while (currentDate <= endDate) {
let dateKey = Qt.formatDate(currentDate, "yyyy-MM-dd")
if (!newEventsByDate[dateKey])
newEventsByDate[dateKey] = []
// Check if this exact event is already added to this date (prevent duplicates)
let existingEvent = newEventsByDate[dateKey].find(e => {
return e.id === eventId
})
if (existingEvent) {
// Move to next day without adding duplicate
currentDate.setDate(currentDate.getDate() + 1)
continue
}
// Create a copy of the event for this date
let dayEvent = Object.assign({}, eventTemplate)
// For multi-day events, adjust the display time for this specific day
if (currentDate.getTime() === startDate.getTime()) {
// First day - use original start time
dayEvent.start = new Date(startTime)
} else {
// Subsequent days - start at beginning of day for all-day events
dayEvent.start = new Date(currentDate)
if (!dayEvent.allDay)
dayEvent.start.setHours(0, 0, 0, 0)
}
if (currentDate.getTime() === endDate.getTime()) {
// Last day - use original end time
dayEvent.end = new Date(endTime)
} else {
// Earlier days - end at end of day for all-day events
dayEvent.end = new Date(currentDate)
if (!dayEvent.allDay)
dayEvent.end.setHours(23, 59, 59, 999)
}
newEventsByDate[dateKey].push(dayEvent)
// Move to next day
currentDate.setDate(currentDate.getDate() + 1)
}
}
// Reset for next run
eventsProcess.rawOutput = ""
}
// Sort events by start time within each date
for (let dateKey in newEventsByDate) {
newEventsByDate[dateKey].sort((a, b) => {
return a.start.getTime(
) - b.start.getTime()
})
}
root.eventsByDate = newEventsByDate
root.lastError = ""
} catch (error) {
root.lastError = "Failed to parse events JSON: " + error.toString()
root.eventsByDate = {}
}
// Reset for next run
eventsProcess.rawOutput = ""
}
stdout: SplitParser {
splitMarker: "\n"
onRead: data => {
eventsProcess.rawOutput += data + "\n"
}
stdout: SplitParser {
splitMarker: "\n"
onRead: data => {
eventsProcess.rawOutput += data + "\n"
}
}
}
}
}

View File

@@ -7,52 +7,54 @@ import Quickshell
import Quickshell.Io
Singleton {
id: root
id: root
property list<int> values: Array(6)
property int refCount: 0
property bool cavaAvailable: false
property list<int> values: Array(6)
property int refCount: 0
property bool cavaAvailable: false
Process {
id: cavaCheck
Process {
id: cavaCheck
command: ["which", "cava"]
running: false
onExited: exitCode => {
root.cavaAvailable = exitCode === 0
}
}
Component.onCompleted: {
cavaCheck.running = true
}
Process {
id: cavaProcess
running: root.cavaAvailable && root.refCount > 0
command: ["sh", "-c", `printf '[general]\\nmode=normal\\nframerate=25\\nautosens=0\\nsensitivity=30\\nbars=6\\nlower_cutoff_freq=50\\nhigher_cutoff_freq=12000\\n[output]\\nmethod=raw\\nraw_target=/dev/stdout\\ndata_format=ascii\\nchannels=mono\\nmono_option=average\\n[smoothing]\\nnoise_reduction=35\\nintegral=90\\ngravity=95\\nignore=2\\nmonstercat=1.5' | cava -p /dev/stdin`]
onRunningChanged: {
if (!running) {
root.values = Array(6).fill(0)
}
}
stdout: SplitParser {
splitMarker: "\n"
onRead: data => {
if (root.refCount > 0 && data.trim()) {
let points = data.split(";").map(p => {
return parseInt(p.trim(), 10)
}).filter(p => {
return !isNaN(p)
})
if (points.length >= 6) {
root.values = points.slice(0, 6)
}
command: ["which", "cava"]
running: false
onExited: exitCode => {
root.cavaAvailable = exitCode === 0
}
}
Component.onCompleted: {
cavaCheck.running = true
}
Process {
id: cavaProcess
running: root.cavaAvailable && root.refCount > 0
command: ["sh", "-c", `printf '[general]\\nmode=normal\\nframerate=25\\nautosens=0\\nsensitivity=30\\nbars=6\\nlower_cutoff_freq=50\\nhigher_cutoff_freq=12000\\n[output]\\nmethod=raw\\nraw_target=/dev/stdout\\ndata_format=ascii\\nchannels=mono\\nmono_option=average\\n[smoothing]\\nnoise_reduction=35\\nintegral=90\\ngravity=95\\nignore=2\\nmonstercat=1.5' | cava -p /dev/stdin`]
onRunningChanged: {
if (!running) {
root.values = Array(6).fill(0)
}
}
stdout: SplitParser {
splitMarker: "\n"
onRead: data => {
if (root.refCount > 0 && data.trim()) {
let points = data.split(";").map(p => {
return parseInt(
p.trim(), 10)
}).filter(p => {
return !isNaN(
p)
})
if (points.length >= 6) {
root.values = points.slice(0, 6)
}
}
}
}
}
}
}
}

View File

@@ -3,7 +3,8 @@ import Quickshell
import Quickshell.Io
import qs.Common
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
Singleton {
id: root
@@ -74,21 +75,28 @@ Singleton {
property int historySize: 60
property var cpuHistory: []
property var memoryHistory: []
property var networkHistory: ({ "rx": [], "tx": [] })
property var diskHistory: ({ "read": [], "write": [] })
property var networkHistory: ({
"rx": [],
"tx": []
})
property var diskHistory: ({
"read": [],
"write": []
})
function addRef(modules = null) {
refCount++
let modulesChanged = false
if (modules) {
const modulesToAdd = Array.isArray(modules) ? modules : [modules]
for (const module of modulesToAdd) {
// 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) {
enabledModules.push(module)
@@ -99,7 +107,8 @@ 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
@@ -111,7 +120,7 @@ Singleton {
function removeRef(modules = null) {
refCount = Math.max(0, refCount - 1)
let modulesChanged = false
if (modules) {
const modulesToRemove = Array.isArray(modules) ? modules : [modules]
for (const module of modulesToRemove) {
@@ -119,7 +128,8 @@ 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]
@@ -127,7 +137,8 @@ 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)")
}
}
}
@@ -135,8 +146,9 @@ 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")) {
cpuCursor = ""
@@ -156,13 +168,14 @@ Singleton {
function addGpuPciId(pciId) {
const currentCount = gpuPciIdRefCounts[pciId] || 0
gpuPciIdRefCounts[pciId] = currentCount + 1
// Add to gpuPciIds array if not already there
if (!gpuPciIds.includes(pciId)) {
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)
}
@@ -172,7 +185,8 @@ 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]
@@ -181,21 +195,23 @@ Singleton {
gpuPciIds = gpuPciIds.slice()
gpuPciIds.splice(index, 1)
}
// Clear temperature data for this GPU when no longer monitored
if (availableGpus && availableGpus.length > 0) {
const updatedGpus = availableGpus.slice()
for (let i = 0; i < updatedGpus.length; i++) {
for (var i = 0; i < updatedGpus.length; i++) {
if (updatedGpus[i].pciId === pciId) {
updatedGpus[i] = Object.assign({}, updatedGpus[i], { temperature: 0 })
updatedGpus[i] = Object.assign({}, updatedGpus[i], {
"temperature": 0
})
}
}
availableGpus = updatedGpus
}
console.log("Removing GPU PCI ID completely:", pciId)
}
// Force property change notification
gpuPciIdRefCounts = Object.assign({}, gpuPciIdRefCounts)
}
@@ -216,19 +232,20 @@ Singleton {
}
function initializeGpuMetadata() {
if (!dgopAvailable) return
if (!dgopAvailable)
return
// Load GPU metadata once at startup for basic info
gpuInitProcess.running = true
}
function buildDgopCommand() {
const cmd = ["dgop", "meta", "--json"]
if (enabledModules.length === 0) {
// Don't run if no modules are needed
return []
}
// Replace 'gpu' with 'gpu-temp' when we have PCI IDs to monitor
const finalModules = []
for (const module of enabledModules) {
@@ -238,12 +255,12 @@ Singleton {
finalModules.push(module)
}
}
// Add gpu-temp module automatically when we have PCI IDs to monitor
if (gpuPciIds.length > 0 && finalModules.indexOf("gpu-temp") === -1) {
finalModules.push("gpu-temp")
}
if (enabledModules.indexOf("all") !== -1) {
cmd.push("--modules", "all")
} else if (finalModules.length > 0) {
@@ -254,10 +271,12 @@ 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)
}
@@ -265,7 +284,8 @@ 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) {
@@ -280,7 +300,7 @@ Singleton {
if (data.cpu) {
const cpu = data.cpu
cpuSampleCount++
// Use dgop CPU numbers directly without modification
cpuUsage = cpu.usage || 0
cpuFrequency = cpu.frequency || 0
@@ -301,34 +321,34 @@ Singleton {
const totalKB = mem.total || 0
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
usedSwapKB = (mem.swaptotal || 0) - (mem.swapfree || 0)
addToHistory(memoryHistory, memoryUsage)
}
if (data.network && Array.isArray(data.network)) {
// Store raw network interface data
networkInterfaces = data.network
let totalRx = 0
let totalTx = 0
for (const iface of data.network) {
totalRx += iface.rx || 0
totalTx += iface.tx || 0
}
if (lastNetworkStats) {
const timeDiff = updateInterval / 1000
const rxDiff = totalRx - lastNetworkStats.rx
@@ -338,20 +358,23 @@ Singleton {
addToHistory(networkHistory.rx, networkRxRate / 1024)
addToHistory(networkHistory.tx, networkTxRate / 1024)
}
lastNetworkStats = { "rx": totalRx, "tx": totalTx }
lastNetworkStats = {
"rx": totalRx,
"tx": totalTx
}
}
if (data.disk && Array.isArray(data.disk)) {
// Store raw disk device data
diskDevices = data.disk
let totalRead = 0
let totalWrite = 0
for (const disk of data.disk) {
totalRead += (disk.read || 0) * 512
totalWrite += (disk.write || 0) * 512
}
if (lastDiskStats) {
const timeDiff = updateInterval / 1000
const readDiff = totalRead - lastDiskStats.read
@@ -361,7 +384,10 @@ Singleton {
addToHistory(diskHistory.read, diskReadRate / (1024 * 1024))
addToHistory(diskHistory.write, diskWriteRate / (1024 * 1024))
}
lastDiskStats = { "read": totalRead, "write": totalWrite }
lastDiskStats = {
"read": totalRead,
"write": totalWrite
}
}
if (data.diskmounts) {
@@ -371,26 +397,31 @@ Singleton {
if (data.processes && Array.isArray(data.processes)) {
const newProcesses = []
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,
"command": proc.command || "",
"fullCommand": proc.fullCommand || "",
"displayName": (proc.command && proc.command.length > 15) ?
proc.command.substring(0, 15) + "..." : (proc.command || "")
})
"pid": proc.pid || 0,
"ppid": proc.ppid || 0,
"cpu": cpuUsage,
"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 || "")
})
}
allProcesses = newProcesses
applySorting()
// Store the single opaque cursor string for the entire process list
if (data.cursor) {
procCursor = data.cursor
@@ -398,18 +429,24 @@ Singleton {
}
// 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 // Handle both meta format and direct gpu command format
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 (let i = 0; i < updatedGpus.length; i++) {
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 })
updatedGpus[i] = Object.assign({}, existingGpu, {
"temperature": tempGpu.temperature
|| 0
})
}
}
availableGpus = updatedGpus
@@ -417,17 +454,18 @@ 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({
"driver": gpu.driver || "",
"vendor": gpu.vendor || "",
"displayName": displayName,
"fullName": fullName,
"pciId": gpu.pciId || "",
"temperature": gpu.temperature || 0
})
"driver": gpu.driver || "",
"vendor": gpu.vendor || "",
"displayName": displayName,
"fullName": fullName,
"pciId": gpu.pciId || "",
"temperature": gpu.temperature || 0
})
}
availableGpus = gpuList
}
@@ -463,17 +501,22 @@ Singleton {
function getProcessIcon(command) {
const cmd = command.toLowerCase()
if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes("browser"))
if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes(
"browser"))
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"))
return "play_circle"
if (cmd.includes("systemd") || cmd.includes("kernel") || cmd.includes("kthread"))
if (cmd.includes("systemd") || cmd.includes("kernel") || cmd.includes(
"kthread"))
return "settings"
return "memory"
}
@@ -514,43 +557,45 @@ Singleton {
applySorting()
}
}
function applySorting() {
if (!allProcesses || allProcesses.length === 0) return
if (!allProcesses || allProcesses.length === 0)
return
const sorted = allProcesses.slice()
sorted.sort((a, b) => {
let valueA, valueB
switch (currentSort) {
case "cpu":
valueA = a.cpu || 0
valueB = b.cpu || 0
return valueB - valueA
case "memory":
valueA = a.memoryKB || 0
valueB = b.memoryKB || 0
return valueB - valueA
case "name":
valueA = (a.command || "").toLowerCase()
valueB = (b.command || "").toLowerCase()
return valueA.localeCompare(valueB)
case "pid":
valueA = a.pid || 0
valueB = b.pid || 0
return valueA - valueB
default:
return 0
}
})
let valueA, valueB
switch (currentSort) {
case "cpu":
valueA = a.cpu || 0
valueB = b.cpu || 0
return valueB - valueA
case "memory":
valueA = a.memoryKB || 0
valueB = b.memoryKB || 0
return valueB - valueA
case "name":
valueA = (a.command || "").toLowerCase()
valueB = (b.command || "").toLowerCase()
return valueA.localeCompare(valueB)
case "pid":
valueA = a.pid || 0
valueB = b.pid || 0
return valueA - valueB
default:
return 0
}
})
processes = sorted.slice(0, processLimit)
}
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()
@@ -561,14 +606,16 @@ Singleton {
command: root.buildDgopCommand()
running: false
onCommandChanged: {
//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()) {
@@ -590,10 +637,12 @@ 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()) {
@@ -613,23 +662,24 @@ 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 {
@@ -637,10 +687,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()) {
@@ -648,16 +698,18 @@ Singleton {
const lines = text.trim().split('\n')
let prettyName = ""
let name = ""
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, '')
}
}
// Prefer PRETTY_NAME, fallback to NAME
const distroName = prettyName || name || "Linux"
distribution = distroName
@@ -675,4 +727,4 @@ Singleton {
dgopCheckProcess.running = true
osReleaseProcess.running = true
}
}
}

View File

@@ -3,7 +3,8 @@ import Quickshell
import Quickshell.Io
import qs.Common
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
Singleton {
id: root
@@ -11,100 +12,98 @@ Singleton {
property bool inhibitorAvailable: true
property bool idleInhibited: false
property string inhibitReason: "Keep system awake"
signal inhibitorChanged()
signal inhibitorChanged
function enableIdleInhibit() {
if (idleInhibited) return;
idleInhibited = true;
inhibitorChanged();
if (idleInhibited)
return
idleInhibited = true
inhibitorChanged()
}
function disableIdleInhibit() {
if (!idleInhibited) return;
idleInhibited = false;
inhibitorChanged();
if (!idleInhibited)
return
idleInhibited = false
inhibitorChanged()
}
function toggleIdleInhibit() {
if (idleInhibited) {
disableIdleInhibit();
disableIdleInhibit()
} else {
enableIdleInhibit();
enableIdleInhibit()
}
}
function setInhibitReason(reason) {
inhibitReason = reason;
inhibitReason = reason
if (idleInhibited) {
const wasActive = idleInhibited;
idleInhibited = false;
const wasActive = idleInhibited
idleInhibited = false
Qt.callLater(() => {
if (wasActive) idleInhibited = true;
});
if (wasActive)
idleInhibited = true
})
}
}
Process {
id: idleInhibitProcess
command: {
if (!idleInhibited) {
return ["true"];
return ["true"]
}
return [
"systemd-inhibit",
"--what=idle",
"--who=quickshell",
"--why=" + inhibitReason,
"--mode=block",
"sleep", "infinity"
];
return ["systemd-inhibit", "--what=idle", "--who=quickshell", "--why="
+ inhibitReason, "--mode=block", "sleep", "infinity"]
}
running: idleInhibited
onExited: function(exitCode) {
onExited: function (exitCode) {
if (idleInhibited && exitCode !== 0) {
console.warn("IdleInhibitorService: Inhibitor process crashed with exit code:", exitCode);
idleInhibited = false;
ToastService.showWarning("Idle inhibitor failed");
console.warn("IdleInhibitorService: Inhibitor process crashed with exit code:",
exitCode)
idleInhibited = false
ToastService.showWarning("Idle inhibitor failed")
}
}
}
IpcHandler {
function toggle() : string {
root.toggleIdleInhibit();
return root.idleInhibited ? "Idle inhibit enabled" : "Idle inhibit disabled";
function toggle(): string {
root.toggleIdleInhibit()
return root.idleInhibited ? "Idle inhibit enabled" : "Idle inhibit disabled"
}
function enable() : string {
root.enableIdleInhibit();
return "Idle inhibit enabled";
function enable(): string {
root.enableIdleInhibit()
return "Idle inhibit enabled"
}
function disable() : string {
root.disableIdleInhibit();
return "Idle inhibit disabled";
function disable(): string {
root.disableIdleInhibit()
return "Idle inhibit disabled"
}
function status() : string {
return root.idleInhibited ? "Idle inhibit is enabled" : "Idle inhibit is disabled";
function status(): string {
return root.idleInhibited ? "Idle inhibit is enabled" : "Idle inhibit is disabled"
}
function reason(newReason: string) : string {
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;
root.setInhibitReason(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
@@ -13,93 +14,55 @@ Singleton {
property bool powerDialogVisible: false
property bool rebootDialogVisible: false
property bool logoutDialogVisible: false
property var facts: [
"A photon takes 100,000 to 200,000 years bouncing through the Sun's dense core, then races to Earth in just 8 minutes 20 seconds.",
"A teaspoon of neutron star matter would weigh a billion metric tons here on Earth.",
"Right now, 100 trillion solar neutrinos are passing through your body every second.",
"The Sun converts 4 million metric tons of matter into pure energy every second—enough to power Earth for 500,000 years.",
"The universe still glows with leftover heat from the Big Bang—just 2.7 degrees above absolute zero.",
"There's a nebula out there that's actually colder than empty space itself.",
"We've detected black holes crashing together by measuring spacetime stretch by less than 1/10,000th the width of a proton.",
"Fast radio bursts can release more energy in 5 milliseconds than our Sun produces in 3 days.",
"Our galaxy might be crawling with billions of rogue planets drifting alone in the dark.",
"Distant galaxies can move away from us faster than light because space itself is stretching.",
"The edge of what we can see is 46.5 billion light-years away, even though the universe is only 13.8 billion years old.",
"The universe is mostly invisible: 5% regular matter, 27% dark matter, 68% dark energy.",
"A day on Venus lasts longer than its entire year around the Sun.",
"On Mercury, the time between sunrises is 176 Earth days long.",
"In about 4.5 billion years, our galaxy will smash into Andromeda.",
"Most of the gold in your jewelry was forged when neutron stars collided somewhere in space.",
"PSR J1748-2446ad, the fastest spinning star, rotates 716 times per second—its equator moves at 24% the speed of light.",
"Cosmic rays create particles that shouldn't make it to Earth's surface, but time dilation lets them sneak through.",
"Jupiter's magnetic field is so huge that if we could see it, it would look bigger than the Moon in our sky.",
"Interstellar space is so empty it's like a cube 32 kilometers wide containing just a single grain of sand.",
"Voyager 1 is 24 billion kilometers away but won't leave the Sun's gravitational influence for another 30,000 years.",
"Counting to a billion at one number per second would take over 31 years.",
"Space is so vast, even speeding at light-speed, you'd never return past the cosmic horizon.",
"Astronauts on the ISS age about 0.01 seconds less each year than people on Earth.",
"Sagittarius B2, a dust cloud near our galaxy's center, contains ethyl formate—the compound that gives raspberries their flavor and rum its smell.",
"Beyond 16 billion light-years, the cosmic event horizon marks where space expands too fast for light to ever reach us again.",
"Even at light-speed, you'd never catch up to most galaxies—space expands faster.",
"Only around 5% of galaxies are ever reachable—even at light-speed.",
"If the Sun vanished, we'd still orbit it for 8 minutes before drifting away.",
"If a planet 65 million light-years away looked at Earth now, it'd see dinosaurs.",
"Our oldest radio signals will reach the Milky Way's center in 26,000 years.",
"Every atom in your body heavier than hydrogen was forged in the nuclear furnace of a dying star.",
"The Moon moves 3.8 centimeters farther from Earth every year.",
"The universe creates 275 million new stars every single day.",
"Jupiter's Great Red Spot is a storm twice the size of Earth that has been raging for at least 350 years.",
"If you watched someone fall into a black hole, they'd appear frozen at the event horizon forever—time effectively stops from your perspective.",
"The Boötes Supervoid is a cosmic desert 1.8 billion light-years across with 60% fewer galaxies than it should have."
]
property var facts: ["A photon takes 100,000 to 200,000 years bouncing through the Sun's dense core, then races to Earth in just 8 minutes 20 seconds.", "A teaspoon of neutron star matter would weigh a billion metric tons here on Earth.", "Right now, 100 trillion solar neutrinos are passing through your body every second.", "The Sun converts 4 million metric tons of matter into pure energy every second—enough to power Earth for 500,000 years.", "The universe still glows with leftover heat from the Big Bang—just 2.7 degrees above absolute zero.", "There's a nebula out there that's actually colder than empty space itself.", "We've detected black holes crashing together by measuring spacetime stretch by less than 1/10,000th the width of a proton.", "Fast radio bursts can release more energy in 5 milliseconds than our Sun produces in 3 days.", "Our galaxy might be crawling with billions of rogue planets drifting alone in the dark.", "Distant galaxies can move away from us faster than light because space itself is stretching.", "The edge of what we can see is 46.5 billion light-years away, even though the universe is only 13.8 billion years old.", "The universe is mostly invisible: 5% regular matter, 27% dark matter, 68% dark energy.", "A day on Venus lasts longer than its entire year around the Sun.", "On Mercury, the time between sunrises is 176 Earth days long.", "In about 4.5 billion years, our galaxy will smash into Andromeda.", "Most of the gold in your jewelry was forged when neutron stars collided somewhere in space.", "PSR J1748-2446ad, the fastest spinning star, rotates 716 times per second—its equator moves at 24% the speed of light.", "Cosmic rays create particles that shouldn't make it to Earth's surface, but time dilation lets them sneak through.", "Jupiter's magnetic field is so huge that if we could see it, it would look bigger than the Moon in our sky.", "Interstellar space is so empty it's like a cube 32 kilometers wide containing just a single grain of sand.", "Voyager 1 is 24 billion kilometers away but won't leave the Sun's gravitational influence for another 30,000 years.", "Counting to a billion at one number per second would take over 31 years.", "Space is so vast, even speeding at light-speed, you'd never return past the cosmic horizon.", "Astronauts on the ISS age about 0.01 seconds less each year than people on Earth.", "Sagittarius B2, a dust cloud near our galaxy's center, contains ethyl formate—the compound that gives raspberries their flavor and rum its smell.", "Beyond 16 billion light-years, the cosmic event horizon marks where space expands too fast for light to ever reach us again.", "Even at light-speed, you'd never catch up to most galaxies—space expands faster.", "Only around 5% of galaxies are ever reachable—even at light-speed.", "If the Sun vanished, we'd still orbit it for 8 minutes before drifting away.", "If a planet 65 million light-years away looked at Earth now, it'd see dinosaurs.", "Our oldest radio signals will reach the Milky Way's center in 26,000 years.", "Every atom in your body heavier than hydrogen was forged in the nuclear furnace of a dying star.", "The Moon moves 3.8 centimeters farther from Earth every year.", "The universe creates 275 million new stars every single day.", "Jupiter's Great Red Spot is a storm twice the size of Earth that has been raging for at least 350 years.", "If you watched someone fall into a black hole, they'd appear frozen at the event horizon forever—time effectively stops from your perspective.", "The Boötes Supervoid is a cosmic desert 1.8 billion light-years across with 60% fewer galaxies than it should have."]
function pickRandomFact() {
randomFact = facts[Math.floor(Math.random() * facts.length)];
randomFact = facts[Math.floor(Math.random() * facts.length)]
}
function resetState() {
unlocking = false;
pamState = "";
powerDialogVisible = false;
rebootDialogVisible = false;
logoutDialogVisible = false;
pickRandomFact();
unlocking = false
pamState = ""
powerDialogVisible = false
rebootDialogVisible = false
logoutDialogVisible = false
pickRandomFact()
}
function setPamState(state) {
pamState = state;
pamState = state
}
function setUnlocking(value) {
unlocking = value;
unlocking = value
}
function showPowerDialog() {
powerDialogVisible = true;
powerDialogVisible = true
}
function hidePowerDialog() {
powerDialogVisible = false;
powerDialogVisible = false
}
function showRebootDialog() {
rebootDialogVisible = true;
rebootDialogVisible = true
}
function hideRebootDialog() {
rebootDialogVisible = false;
rebootDialogVisible = false
}
function showLogoutDialog() {
logoutDialogVisible = true;
logoutDialogVisible = true
}
function hideLogoutDialog() {
logoutDialogVisible = false;
logoutDialogVisible = false
}
Component.onCompleted: {
pickRandomFact();
pickRandomFact()
}
}
}

View File

@@ -9,48 +9,49 @@ import Quickshell.Services.Mpris
import Quickshell.Widgets
Singleton {
id: root
id: root
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying)
?? 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"
IpcHandler {
target: "mpris"
function list(): string {
return root.availablePlayers.map(p => p.identity).join("")
function list(): string {
return root.availablePlayers.map(p => p.identity).join("")
}
function play(): void {
if (root.activePlayer?.canPlay)
root.activePlayer.play()
}
function pause(): void {
if (root.activePlayer?.canPause)
root.activePlayer.pause()
}
function playPause(): void {
if (root.activePlayer?.canTogglePlaying)
root.activePlayer.togglePlaying()
}
function previous(): void {
if (root.activePlayer?.canGoPrevious)
root.activePlayer.previous()
}
function next(): void {
if (root.activePlayer?.canGoNext)
root.activePlayer.next()
}
function stop(): void {
root.activePlayer?.stop()
}
}
function play(): void {
if (root.activePlayer?.canPlay)
root.activePlayer.play()
}
function pause(): void {
if (root.activePlayer?.canPause)
root.activePlayer.pause()
}
function playPause(): void {
if (root.activePlayer?.canTogglePlaying)
root.activePlayer.togglePlaying()
}
function previous(): void {
if (root.activePlayer?.canGoPrevious)
root.activePlayer.previous()
}
function next(): void {
if (root.activePlayer?.canGoNext)
root.activePlayer.next()
}
function stop(): void {
root.activePlayer?.stop()
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -10,514 +10,519 @@ import qs.Common
import "../Common/markdown2html.js" as Markdown2Html
Singleton {
id: root
id: root
readonly property list<NotifWrapper> notifications: []
readonly property list<NotifWrapper> allWrappers: []
readonly property list<NotifWrapper> popups: allWrappers.filter(n => n.popup)
readonly property list<NotifWrapper> notifications: []
readonly property list<NotifWrapper> allWrappers: []
readonly property list<NotifWrapper> popups: allWrappers.filter(
n => n.popup)
property list<NotifWrapper> notificationQueue: []
property list<NotifWrapper> visibleNotifications: []
property int maxVisibleNotifications: 3
property bool addGateBusy: false
property int enterAnimMs: 400
property int seqCounter: 0
property bool bulkDismissing: false
property list<NotifWrapper> notificationQueue: []
property list<NotifWrapper> visibleNotifications: []
property int maxVisibleNotifications: 3
property bool addGateBusy: false
property int enterAnimMs: 400
property int seqCounter: 0
property bool bulkDismissing: false
Timer {
id: addGate
interval: enterAnimMs + 50
running: false
repeat: false
onTriggered: {
addGateBusy = false
processQueue()
}
}
Timer {
id: timeUpdateTimer
interval: 30000
repeat: true
running: root.allWrappers.length > 0
triggeredOnStart: false
onTriggered: {
root.timeUpdateTick = !root.timeUpdateTick
}
}
property bool timeUpdateTick: false
property bool clockFormatChanged: false
readonly property var groupedNotifications: getGroupedNotifications()
readonly property var groupedPopups: getGroupedPopups()
property var expandedGroups: ({})
property var expandedMessages: ({})
property bool popupsDisabled: false
NotificationServer {
id: server
keepOnReload: false
actionsSupported: true
actionIconsSupported: true
bodyHyperlinksSupported: true
bodyImagesSupported: true
bodyMarkupSupported: true
imageSupported: true
inlineReplySupported: true
persistenceSupported: true
onNotification: notif => {
notif.tracked = true
const shouldShowPopup = !root.popupsDisabled && !SessionData.doNotDisturb
const wrapper = notifComponent.createObject(root, {
"popup": shouldShowPopup,
"notification": notif
})
if (wrapper) {
root.allWrappers.push(wrapper)
root.notifications.push(wrapper)
if (shouldShowPopup) {
notificationQueue = [...notificationQueue, wrapper]
processQueue()
Timer {
id: addGate
interval: enterAnimMs + 50
running: false
repeat: false
onTriggered: {
addGateBusy = false
processQueue()
}
}
}
}
component NotifWrapper: QtObject {
id: wrapper
property bool popup: false
property bool removedByLimit: false
property bool isPersistent: true
property int seq: 0
onPopupChanged: {
if (!popup) {
removeFromVisibleNotifications(wrapper)
}
}
readonly property Timer timer: Timer {
interval: {
if (!wrapper.notification) return 5000
switch (wrapper.notification.urgency) {
case NotificationUrgency.Low:
return SettingsData.notificationTimeoutLow
case NotificationUrgency.Critical:
return SettingsData.notificationTimeoutCritical
default:
return SettingsData.notificationTimeoutNormal
Timer {
id: timeUpdateTimer
interval: 30000
repeat: true
running: root.allWrappers.length > 0
triggeredOnStart: false
onTriggered: {
root.timeUpdateTick = !root.timeUpdateTick
}
}
repeat: false
running: false
onTriggered: {
if (interval > 0) {
wrapper.popup = false
}
property bool timeUpdateTick: false
property bool clockFormatChanged: false
readonly property var groupedNotifications: getGroupedNotifications()
readonly property var groupedPopups: getGroupedPopups()
property var expandedGroups: ({})
property var expandedMessages: ({})
property bool popupsDisabled: false
NotificationServer {
id: server
keepOnReload: false
actionsSupported: true
actionIconsSupported: true
bodyHyperlinksSupported: true
bodyImagesSupported: true
bodyMarkupSupported: true
imageSupported: true
inlineReplySupported: true
persistenceSupported: true
onNotification: notif => {
notif.tracked = true
const shouldShowPopup = !root.popupsDisabled
&& !SessionData.doNotDisturb
const wrapper = notifComponent.createObject(root, {
"popup": shouldShowPopup,
"notification": notif
})
if (wrapper) {
root.allWrappers.push(wrapper)
root.notifications.push(wrapper)
if (shouldShowPopup) {
notificationQueue = [...notificationQueue, wrapper]
processQueue()
}
}
}
}
}
readonly property date time: new Date()
readonly property string timeStr: {
root.timeUpdateTick
root.clockFormatChanged
const now = new Date()
const diff = now.getTime() - time.getTime()
const minutes = Math.floor(diff / 60000)
const hours = Math.floor(minutes / 60)
if (hours < 1) {
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))
if (daysDiff === 0) {
return formatTime(time)
}
if (daysDiff === 1) {
return `yesterday, ${formatTime(time)}`
}
return `${daysDiff} days ago`
}
function formatTime(date) {
let use24Hour = true
try {
if (typeof SettingsData !== "undefined" && SettingsData.use24HourClock !== undefined) {
use24Hour = SettingsData.use24HourClock
}
} catch (e) {
use24Hour = true
}
if (use24Hour) {
return date.toLocaleTimeString(Qt.locale(), "HH:mm")
} else {
return date.toLocaleTimeString(Qt.locale(), "h:mm AP")
}
}
required property Notification notification
readonly property string summary: notification.summary
readonly property string body: notification.body
readonly property string htmlBody: {
if (body && (body.includes('<') && body.includes('>'))) {
return body
}
return Markdown2Html.markdownToHtml(body)
}
readonly property string appIcon: notification.appIcon
readonly property string appName: {
if (notification.appName == "") {
const entry = DesktopEntries.byId(notification.desktopEntry)
if (entry && entry.name) {
return entry.name.toLowerCase()
}
}
return notification.appName || "app"
}
readonly property string desktopEntry: notification.desktopEntry
readonly property string image: notification.image
readonly property string cleanImage: {
if (!image)
return ""
if (image.startsWith("file://")) {
return image.substring(7)
}
return image
}
readonly property int urgency: notification.urgency
readonly property list<NotificationAction> actions: notification.actions
component NotifWrapper: QtObject {
id: wrapper
property bool popup: false
property bool removedByLimit: false
property bool isPersistent: true
property int seq: 0
readonly property Connections conn: Connections {
target: wrapper.notification.Retainable
function onDropped(): void {
const notifIndex = root.notifications.indexOf(wrapper)
const allIndex = root.allWrappers.indexOf(wrapper)
if (allIndex !== -1)
root.allWrappers.splice(allIndex, 1)
if (notifIndex !== -1)
root.notifications.splice(notifIndex, 1)
if (root.bulkDismissing)
return
const groupKey = getGroupKey(wrapper)
const remainingInGroup = root.notifications.filter(n => getGroupKey(
n) === groupKey)
if (remainingInGroup.length <= 1) {
clearGroupExpansionState(groupKey)
onPopupChanged: {
if (!popup) {
removeFromVisibleNotifications(wrapper)
}
}
cleanupExpansionStates()
}
function onAboutToDestroy(): void {
wrapper.destroy()
}
}
}
Component {
id: notifComponent
NotifWrapper {}
}
function clearAllNotifications() {
bulkDismissing = true
popupsDisabled = true
addGate.stop()
addGateBusy = false
notificationQueue = []
for (const w of visibleNotifications)
w.popup = false
visibleNotifications = []
const toDismiss = notifications.slice()
if (notifications.length)
notifications.splice(0, notifications.length)
expandedGroups = {}
expandedMessages = {}
for (var i = 0; i < toDismiss.length; ++i) {
const w = toDismiss[i]
if (w && w.notification) {
try {
w.notification.dismiss()
} catch (e) {
readonly property Timer timer: Timer {
interval: {
if (!wrapper.notification)
return 5000
switch (wrapper.notification.urgency) {
case NotificationUrgency.Low:
return SettingsData.notificationTimeoutLow
case NotificationUrgency.Critical:
return SettingsData.notificationTimeoutCritical
default:
return SettingsData.notificationTimeoutNormal
}
}
repeat: false
running: false
onTriggered: {
if (interval > 0) {
wrapper.popup = false
}
}
}
}
}
bulkDismissing = false
popupsDisabled = false
}
readonly property date time: new Date()
readonly property string timeStr: {
root.timeUpdateTick
root.clockFormatChanged
function dismissNotification(wrapper) {
if (!wrapper || !wrapper.notification)
return
wrapper.popup = false
wrapper.notification.dismiss()
}
const now = new Date()
const diff = now.getTime() - time.getTime()
const minutes = Math.floor(diff / 60000)
const hours = Math.floor(minutes / 60)
function disablePopups(disable) {
popupsDisabled = disable
if (disable) {
notificationQueue = []
visibleNotifications = []
for (const notif of root.allWrappers) {
notif.popup = false
}
}
}
if (hours < 1) {
if (minutes < 1)
return "now"
return `${minutes}m ago`
}
function processQueue() {
if (addGateBusy)
return
if (popupsDisabled)
return
if (SessionData.doNotDisturb)
return
if (notificationQueue.length === 0)
return
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 next = notificationQueue.shift()
if (daysDiff === 0) {
return formatTime(time)
}
next.seq = ++seqCounter
visibleNotifications = [...visibleNotifications, next]
next.popup = true
if (daysDiff === 1) {
return `yesterday, ${formatTime(time)}`
}
if (next.timer.interval > 0) {
next.timer.start()
}
addGateBusy = true
addGate.restart()
}
function removeFromVisibleNotifications(wrapper) {
const i = visibleNotifications.findIndex(n => n === wrapper)
if (i !== -1) {
const v = [...visibleNotifications]
v.splice(i, 1)
visibleNotifications = v
processQueue()
}
}
function releaseWrapper(w) {
let v = visibleNotifications.slice()
const vi = v.indexOf(w)
if (vi !== -1) {
v.splice(vi, 1)
visibleNotifications = v
}
let q = notificationQueue.slice()
const qi = q.indexOf(w)
if (qi !== -1) {
q.splice(qi, 1)
notificationQueue = q
}
if (w && w.destroy && !w.isPersistent) {
w.destroy()
}
}
function getGroupKey(wrapper) {
if (wrapper.desktopEntry && wrapper.desktopEntry !== "") {
return wrapper.desktopEntry.toLowerCase()
}
return wrapper.appName.toLowerCase()
}
function getGroupedNotifications() {
const groups = {}
for (const notif of notifications) {
const groupKey = getGroupKey(notif)
if (!groups[groupKey]) {
groups[groupKey] = {
"key": groupKey,
"appName": notif.appName,
"notifications": [],
"latestNotification": null,
"count": 0,
"hasInlineReply": false
return `${daysDiff} days ago`
}
}
groups[groupKey].notifications.unshift(notif)
groups[groupKey].latestNotification = groups[groupKey].notifications[0]
groups[groupKey].count = groups[groupKey].notifications.length
function formatTime(date) {
let use24Hour = true
try {
if (typeof SettingsData !== "undefined"
&& SettingsData.use24HourClock !== undefined) {
use24Hour = SettingsData.use24HourClock
}
} catch (e) {
use24Hour = true
}
if (notif.notification.hasInlineReply) {
groups[groupKey].hasInlineReply = true
}
}
return Object.values(groups).sort((a, b) => {
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(
)
})
}
function getGroupedPopups() {
const groups = {}
for (const notif of popups) {
const groupKey = getGroupKey(notif)
if (!groups[groupKey]) {
groups[groupKey] = {
"key": groupKey,
"appName": notif.appName,
"notifications": [],
"latestNotification": null,
"count": 0,
"hasInlineReply": false
if (use24Hour) {
return date.toLocaleTimeString(Qt.locale(), "HH:mm")
} else {
return date.toLocaleTimeString(Qt.locale(), "h:mm AP")
}
}
}
groups[groupKey].notifications.unshift(notif)
groups[groupKey].latestNotification = groups[groupKey].notifications[0]
groups[groupKey].count = groups[groupKey].notifications.length
if (notif.notification.hasInlineReply) {
groups[groupKey].hasInlineReply = true
}
}
return Object.values(groups).sort((a, b) => {
return b.latestNotification.time.getTime(
) - a.latestNotification.time.getTime(
)
})
}
function toggleGroupExpansion(groupKey) {
let newExpandedGroups = {}
for (const key in expandedGroups) {
newExpandedGroups[key] = expandedGroups[key]
}
newExpandedGroups[groupKey] = !newExpandedGroups[groupKey]
expandedGroups = newExpandedGroups
}
function dismissGroup(groupKey) {
const group = groupedNotifications.find(g => g.key === groupKey)
if (group) {
for (const notif of group.notifications) {
if (notif && notif.notification) {
notif.notification.dismiss()
required property Notification notification
readonly property string summary: notification.summary
readonly property string body: notification.body
readonly property string htmlBody: {
if (body && (body.includes('<') && body.includes('>'))) {
return body
}
return Markdown2Html.markdownToHtml(body)
}
}
} else {
for (const notif of allWrappers) {
if (notif && notif.notification && getGroupKey(notif) === groupKey) {
notif.notification.dismiss()
readonly property string appIcon: notification.appIcon
readonly property string appName: {
if (notification.appName == "") {
const entry = DesktopEntries.byId(notification.desktopEntry)
if (entry && entry.name) {
return entry.name.toLowerCase()
}
}
return notification.appName || "app"
}
}
}
}
function clearGroupExpansionState(groupKey) {
let newExpandedGroups = {}
for (const key in expandedGroups) {
if (key !== groupKey && expandedGroups[key]) {
newExpandedGroups[key] = true
}
}
expandedGroups = newExpandedGroups
}
function cleanupExpansionStates() {
const currentGroupKeys = new Set(groupedNotifications.map(g => g.key))
const currentMessageIds = new Set()
for (const group of groupedNotifications) {
for (const notif of group.notifications) {
currentMessageIds.add(notif.notification.id)
}
}
let newExpandedGroups = {}
for (const key in expandedGroups) {
if (currentGroupKeys.has(key) && expandedGroups[key]) {
newExpandedGroups[key] = true
}
}
expandedGroups = newExpandedGroups
let newExpandedMessages = {}
for (const messageId in expandedMessages) {
if (currentMessageIds.has(messageId) && expandedMessages[messageId]) {
newExpandedMessages[messageId] = true
}
}
expandedMessages = newExpandedMessages
}
function toggleMessageExpansion(messageId) {
let newExpandedMessages = {}
for (const key in expandedMessages) {
newExpandedMessages[key] = expandedMessages[key]
}
newExpandedMessages[messageId] = !newExpandedMessages[messageId]
expandedMessages = newExpandedMessages
}
Connections {
target: SessionData
function onDoNotDisturbChanged() {
if (SessionData.doNotDisturb) {
// Hide all current popups when DND is enabled
for (const notif of visibleNotifications) {
notif.popup = false
readonly property string desktopEntry: notification.desktopEntry
readonly property string image: notification.image
readonly property string cleanImage: {
if (!image)
return ""
if (image.startsWith("file://")) {
return image.substring(7)
}
return image
}
visibleNotifications = []
readonly property int urgency: notification.urgency
readonly property list<NotificationAction> actions: notification.actions
readonly property Connections conn: Connections {
target: wrapper.notification.Retainable
function onDropped(): void {
const notifIndex = root.notifications.indexOf(wrapper)
const allIndex = root.allWrappers.indexOf(wrapper)
if (allIndex !== -1)
root.allWrappers.splice(allIndex, 1)
if (notifIndex !== -1)
root.notifications.splice(notifIndex, 1)
if (root.bulkDismissing)
return
const groupKey = getGroupKey(wrapper)
const remainingInGroup = root.notifications.filter(
n => getGroupKey(n) === groupKey)
if (remainingInGroup.length <= 1) {
clearGroupExpansionState(groupKey)
}
cleanupExpansionStates()
}
function onAboutToDestroy(): void {
wrapper.destroy()
}
}
}
Component {
id: notifComponent
NotifWrapper {}
}
function clearAllNotifications() {
bulkDismissing = true
popupsDisabled = true
addGate.stop()
addGateBusy = false
notificationQueue = []
} else {
// Re-enable popup processing when DND is disabled
processQueue()
}
}
}
Connections {
target: typeof SettingsData !== "undefined" ? SettingsData : null
function onUse24HourClockChanged() {
root.clockFormatChanged = !root.clockFormatChanged
for (const w of visibleNotifications)
w.popup = false
visibleNotifications = []
const toDismiss = notifications.slice()
if (notifications.length)
notifications.splice(0, notifications.length)
expandedGroups = {}
expandedMessages = {}
for (var i = 0; i < toDismiss.length; ++i) {
const w = toDismiss[i]
if (w && w.notification) {
try {
w.notification.dismiss()
} catch (e) {
}
}
}
bulkDismissing = false
popupsDisabled = false
}
function dismissNotification(wrapper) {
if (!wrapper || !wrapper.notification)
return
wrapper.popup = false
wrapper.notification.dismiss()
}
function disablePopups(disable) {
popupsDisabled = disable
if (disable) {
notificationQueue = []
visibleNotifications = []
for (const notif of root.allWrappers) {
notif.popup = false
}
}
}
function processQueue() {
if (addGateBusy)
return
if (popupsDisabled)
return
if (SessionData.doNotDisturb)
return
if (notificationQueue.length === 0)
return
const next = notificationQueue.shift()
next.seq = ++seqCounter
visibleNotifications = [...visibleNotifications, next]
next.popup = true
if (next.timer.interval > 0) {
next.timer.start()
}
addGateBusy = true
addGate.restart()
}
function removeFromVisibleNotifications(wrapper) {
const i = visibleNotifications.findIndex(n => n === wrapper)
if (i !== -1) {
const v = [...visibleNotifications]
v.splice(i, 1)
visibleNotifications = v
processQueue()
}
}
function releaseWrapper(w) {
let v = visibleNotifications.slice()
const vi = v.indexOf(w)
if (vi !== -1) {
v.splice(vi, 1)
visibleNotifications = v
}
let q = notificationQueue.slice()
const qi = q.indexOf(w)
if (qi !== -1) {
q.splice(qi, 1)
notificationQueue = q
}
if (w && w.destroy && !w.isPersistent) {
w.destroy()
}
}
function getGroupKey(wrapper) {
if (wrapper.desktopEntry && wrapper.desktopEntry !== "") {
return wrapper.desktopEntry.toLowerCase()
}
return wrapper.appName.toLowerCase()
}
function getGroupedNotifications() {
const groups = {}
for (const notif of notifications) {
const groupKey = getGroupKey(notif)
if (!groups[groupKey]) {
groups[groupKey] = {
"key": groupKey,
"appName": notif.appName,
"notifications": [],
"latestNotification": null,
"count": 0,
"hasInlineReply": false
}
}
groups[groupKey].notifications.unshift(notif)
groups[groupKey].latestNotification = groups[groupKey].notifications[0]
groups[groupKey].count = groups[groupKey].notifications.length
if (notif.notification.hasInlineReply) {
groups[groupKey].hasInlineReply = true
}
}
return Object.values(groups).sort((a, b) => {
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()
})
}
function getGroupedPopups() {
const groups = {}
for (const notif of popups) {
const groupKey = getGroupKey(notif)
if (!groups[groupKey]) {
groups[groupKey] = {
"key": groupKey,
"appName": notif.appName,
"notifications": [],
"latestNotification": null,
"count": 0,
"hasInlineReply": false
}
}
groups[groupKey].notifications.unshift(notif)
groups[groupKey].latestNotification = groups[groupKey].notifications[0]
groups[groupKey].count = groups[groupKey].notifications.length
if (notif.notification.hasInlineReply) {
groups[groupKey].hasInlineReply = true
}
}
return Object.values(groups).sort((a, b) => {
return b.latestNotification.time.getTime(
) - a.latestNotification.time.getTime()
})
}
function toggleGroupExpansion(groupKey) {
let newExpandedGroups = {}
for (const key in expandedGroups) {
newExpandedGroups[key] = expandedGroups[key]
}
newExpandedGroups[groupKey] = !newExpandedGroups[groupKey]
expandedGroups = newExpandedGroups
}
function dismissGroup(groupKey) {
const group = groupedNotifications.find(g => g.key === groupKey)
if (group) {
for (const notif of group.notifications) {
if (notif && notif.notification) {
notif.notification.dismiss()
}
}
} else {
for (const notif of allWrappers) {
if (notif && notif.notification && getGroupKey(
notif) === groupKey) {
notif.notification.dismiss()
}
}
}
}
function clearGroupExpansionState(groupKey) {
let newExpandedGroups = {}
for (const key in expandedGroups) {
if (key !== groupKey && expandedGroups[key]) {
newExpandedGroups[key] = true
}
}
expandedGroups = newExpandedGroups
}
function cleanupExpansionStates() {
const currentGroupKeys = new Set(groupedNotifications.map(g => g.key))
const currentMessageIds = new Set()
for (const group of groupedNotifications) {
for (const notif of group.notifications) {
currentMessageIds.add(notif.notification.id)
}
}
let newExpandedGroups = {}
for (const key in expandedGroups) {
if (currentGroupKeys.has(key) && expandedGroups[key]) {
newExpandedGroups[key] = true
}
}
expandedGroups = newExpandedGroups
let newExpandedMessages = {}
for (const messageId in expandedMessages) {
if (currentMessageIds.has(messageId)
&& expandedMessages[messageId]) {
newExpandedMessages[messageId] = true
}
}
expandedMessages = newExpandedMessages
}
function toggleMessageExpansion(messageId) {
let newExpandedMessages = {}
for (const key in expandedMessages) {
newExpandedMessages[key] = expandedMessages[key]
}
newExpandedMessages[messageId] = !newExpandedMessages[messageId]
expandedMessages = newExpandedMessages
}
Connections {
target: SessionData
function onDoNotDisturbChanged() {
if (SessionData.doNotDisturb) {
// Hide all current popups when DND is enabled
for (const notif of visibleNotifications) {
notif.popup = false
}
visibleNotifications = []
notificationQueue = []
} else {
// Re-enable popup processing when DND is disabled
processQueue()
}
}
}
Connections {
target: typeof SettingsData !== "undefined" ? SettingsData : null
function onUse24HourClockChanged() {
root.clockFormatChanged = !root.clockFormatChanged
}
}
}
}

View File

@@ -7,174 +7,174 @@ import Quickshell
import Quickshell.Io
Singleton {
id: root
id: root
property bool accountsServiceAvailable: false
property string systemProfileImage: ""
property string profileImage: ""
property bool settingsPortalAvailable: false
property int systemColorScheme: 0 // 0=default, 1=prefer-dark, 2=prefer-light
property bool accountsServiceAvailable: false
property string systemProfileImage: ""
property string profileImage: ""
property bool settingsPortalAvailable: false
property int systemColorScheme: 0 // 0=default, 1=prefer-dark, 2=prefer-light
function getSystemProfileImage() {
systemProfileCheckProcess.running = true
}
function setProfileImage(imagePath) {
profileImage = imagePath
if (accountsServiceAvailable && imagePath) {
setSystemProfileImage(imagePath)
function getSystemProfileImage() {
systemProfileCheckProcess.running = true
}
}
function getSystemColorScheme() {
systemColorSchemeCheckProcess.running = true
}
function setLightMode(isLightMode) {
if (settingsPortalAvailable) {
setSystemColorScheme(isLightMode)
}
}
function setSystemColorScheme(isLightMode) {
if (!settingsPortalAvailable)
return
var colorScheme = isLightMode ? "prefer-light" : "prefer-dark"
var script = "gsettings set org.gnome.desktop.interface color-scheme '" + colorScheme + "'"
systemColorSchemeSetProcess.command = ["bash", "-c", script]
systemColorSchemeSetProcess.running = true
}
function setSystemProfileImage(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(
" ")
systemProfileSetProcess.command = ["bash", "-c", script]
systemProfileSetProcess.running = true
}
Component.onCompleted: {
checkAccountsService()
checkSettingsPortal()
}
function checkAccountsService() {
accountsServiceCheckProcess.running = true
}
function checkSettingsPortal() {
settingsPortalCheckProcess.running = true
}
Process {
id: accountsServiceCheckProcess
command: ["bash", "-c", "dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts org.freedesktop.Accounts.FindUserByName string:\"$USER\""]
running: false
onExited: exitCode => {
root.accountsServiceAvailable = (exitCode === 0)
if (root.accountsServiceAvailable) {
root.getSystemProfileImage()
}
}
}
Process {
id: systemProfileCheckProcess
command: ["bash", "-c", "dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile"]
running: false
stdout: StdioCollector {
onStreamFinished: {
var match = text.match(/string\s+"([^"]+)"/)
if (match && match[1] && match[1] !== ""
&& match[1] !== "/var/lib/AccountsService/icons/") {
root.systemProfileImage = match[1]
if (!root.profileImage || root.profileImage === "") {
root.profileImage = root.systemProfileImage
}
function setProfileImage(imagePath) {
profileImage = imagePath
if (accountsServiceAvailable && imagePath) {
setSystemProfileImage(imagePath)
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
root.systemProfileImage = ""
}
function getSystemColorScheme() {
systemColorSchemeCheckProcess.running = true
}
}
Process {
id: systemProfileSetProcess
running: false
onExited: exitCode => {
if (exitCode === 0) {
root.getSystemProfileImage()
}
function setLightMode(isLightMode) {
if (settingsPortalAvailable) {
setSystemColorScheme(isLightMode)
}
}
}
Process {
id: settingsPortalCheckProcess
command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"]
running: false
function setSystemColorScheme(isLightMode) {
if (!settingsPortalAvailable)
return
onExited: exitCode => {
root.settingsPortalAvailable = (exitCode === 0)
if (root.settingsPortalAvailable) {
root.getSystemColorScheme()
}
var colorScheme = isLightMode ? "prefer-light" : "prefer-dark"
var script = "gsettings set org.gnome.desktop.interface color-scheme '" + colorScheme + "'"
systemColorSchemeSetProcess.command = ["bash", "-c", script]
systemColorSchemeSetProcess.running = true
}
}
Process {
id: systemColorSchemeCheckProcess
command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"]
running: false
function setSystemProfileImage(imagePath) {
if (!accountsServiceAvailable || !imagePath)
return
stdout: StdioCollector {
onStreamFinished: {
var match = text.match(/uint32 (\d+)/)
if (match && match[1]) {
root.systemColorScheme = parseInt(match[1])
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(
" ")
if (typeof Theme !== "undefined") {
var shouldBeLightMode = (root.systemColorScheme === 2)
if (Theme.isLightMode !== shouldBeLightMode) {
Theme.isLightMode = shouldBeLightMode
if (typeof SessionData !== "undefined") {
SessionData.setLightMode(shouldBeLightMode)
}
systemProfileSetProcess.command = ["bash", "-c", script]
systemProfileSetProcess.running = true
}
Component.onCompleted: {
checkAccountsService()
checkSettingsPortal()
}
function checkAccountsService() {
accountsServiceCheckProcess.running = true
}
function checkSettingsPortal() {
settingsPortalCheckProcess.running = true
}
Process {
id: accountsServiceCheckProcess
command: ["bash", "-c", "dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts org.freedesktop.Accounts.FindUserByName string:\"$USER\""]
running: false
onExited: exitCode => {
root.accountsServiceAvailable = (exitCode === 0)
if (root.accountsServiceAvailable) {
root.getSystemProfileImage()
}
}
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
root.systemColorScheme = 0
}
}
}
Process {
id: systemProfileCheckProcess
command: ["bash", "-c", "dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile"]
running: false
Process {
id: systemColorSchemeSetProcess
running: false
stdout: StdioCollector {
onStreamFinished: {
var match = text.match(/string\s+"([^"]+)"/)
if (match && match[1] && match[1] !== ""
&& match[1] !== "/var/lib/AccountsService/icons/") {
root.systemProfileImage = match[1]
onExited: exitCode => {
if (exitCode === 0) {
Qt.callLater(() => {
root.getSystemColorScheme()
})
}
if (!root.profileImage || root.profileImage === "") {
root.profileImage = root.systemProfileImage
}
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
root.systemProfileImage = ""
}
}
}
Process {
id: systemProfileSetProcess
running: false
onExited: exitCode => {
if (exitCode === 0) {
root.getSystemProfileImage()
}
}
}
Process {
id: settingsPortalCheckProcess
command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"]
running: false
onExited: exitCode => {
root.settingsPortalAvailable = (exitCode === 0)
if (root.settingsPortalAvailable) {
root.getSystemColorScheme()
}
}
}
Process {
id: systemColorSchemeCheckProcess
command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"]
running: false
stdout: StdioCollector {
onStreamFinished: {
var match = text.match(/uint32 (\d+)/)
if (match && match[1]) {
root.systemColorScheme = parseInt(match[1])
if (typeof Theme !== "undefined") {
var shouldBeLightMode = (root.systemColorScheme === 2)
if (Theme.isLightMode !== shouldBeLightMode) {
Theme.isLightMode = shouldBeLightMode
if (typeof SessionData !== "undefined") {
SessionData.setLightMode(shouldBeLightMode)
}
}
}
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
root.systemColorScheme = 0
}
}
}
Process {
id: systemColorSchemeSetProcess
running: false
onExited: exitCode => {
if (exitCode === 0) {
Qt.callLater(() => {
root.getSystemColorScheme()
})
}
}
}
}
}

View File

@@ -8,136 +8,137 @@ import Quickshell.Io
import Quickshell.Services.Pipewire
Singleton {
id: root
id: root
readonly property bool microphoneActive: {
if (!Pipewire.ready || !Pipewire.nodes?.values)
return false
readonly property bool microphoneActive: {
if (!Pipewire.ready || !Pipewire.nodes?.values)
return false
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
const node = Pipewire.nodes.values[i]
if (!node)
continue
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
const node = Pipewire.nodes.values[i]
if (!node)
continue
if ((node.type & PwNodeType.AudioInStream) === PwNodeType.AudioInStream) {
if (!looksLikeSystemVirtualMic(node)) {
console.log(node.audio)
if (node.audio && node.audio.muted) {
return false
}
return true
}
}
}
return false
}
PwObjectTracker {
objects: Pipewire.nodes.values
}
readonly property bool cameraActive: {
if (!Pipewire.ready || !Pipewire.nodes?.values)
return false
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
const node = Pipewire.nodes.values[i]
if (!node || !node.ready)
continue
if (node.properties
&& node.properties["media.class"] === "Stream/Input/Video") {
if (node.properties["stream.is-live"] === "true") {
return true
}
}
}
return false
}
readonly property bool screensharingActive: {
if (!Pipewire.ready || !Pipewire.nodes?.values)
return false
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
const node = Pipewire.nodes.values[i]
if (!node || !node.ready)
continue
if ((node.type & PwNodeType.VideoSource) === PwNodeType.VideoSource) {
if (looksLikeScreencast(node)) {
return true
}
}
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 (node.properties["stream.is-live"] === "true") {
if (node.audio && node.audio.muted) {
return false
if ((node.type & PwNodeType.AudioInStream) === PwNodeType.AudioInStream) {
if (!looksLikeSystemVirtualMic(node)) {
console.log(node.audio)
if (node.audio && node.audio.muted) {
return false
}
return true
}
}
return true
}
}
}
return false
}
return false
}
readonly property bool anyPrivacyActive: microphoneActive || cameraActive
|| screensharingActive
PwObjectTracker {
objects: Pipewire.nodes.values
}
function looksLikeSystemVirtualMic(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 combined = name + " " + mediaName + " " + appName
return /cava|monitor|system/.test(combined)
}
readonly property bool cameraActive: {
if (!Pipewire.ready || !Pipewire.nodes?.values)
return false
function looksLikeScreencast(node) {
if (!node)
return false
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)
}
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
const node = Pipewire.nodes.values[i]
if (!node || !node.ready)
continue
function getMicrophoneStatus() {
return microphoneActive ? "active" : "inactive"
}
if (node.properties
&& node.properties["media.class"] === "Stream/Input/Video") {
if (node.properties["stream.is-live"] === "true") {
return true
}
}
}
return false
}
function getCameraStatus() {
return cameraActive ? "active" : "inactive"
}
readonly property bool screensharingActive: {
if (!Pipewire.ready || !Pipewire.nodes?.values)
return false
function getScreensharingStatus() {
return screensharingActive ? "active" : "inactive"
}
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
const node = Pipewire.nodes.values[i]
if (!node || !node.ready)
continue
function getPrivacySummary() {
const active = []
if (microphoneActive)
active.push("microphone")
if (cameraActive)
active.push("camera")
if (screensharingActive)
active.push("screensharing")
if ((node.type & PwNodeType.VideoSource) === PwNodeType.VideoSource) {
if (looksLikeScreencast(node)) {
return true
}
}
return active.length > 0 ? "Privacy active: " + active.join(
", ") : "No privacy concerns detected"
}
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 (node.properties["stream.is-live"] === "true") {
if (node.audio && node.audio.muted) {
return false
}
return true
}
}
}
}
return false
}
readonly property bool anyPrivacyActive: microphoneActive || cameraActive
|| screensharingActive
function looksLikeSystemVirtualMic(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 combined = name + " " + mediaName + " " + appName
return /cava|monitor|system/.test(combined)
}
function looksLikeScreencast(node) {
if (!node)
return false
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)
}
function getMicrophoneStatus() {
return microphoneActive ? "active" : "inactive"
}
function getCameraStatus() {
return cameraActive ? "active" : "inactive"
}
function getScreensharingStatus() {
return screensharingActive ? "active" : "inactive"
}
function getPrivacySummary() {
const active = []
if (microphoneActive)
active.push("microphone")
if (cameraActive)
active.push("camera")
if (screensharingActive)
active.push("screensharing")
return active.length > 0 ? "Privacy active: " + active.join(
", ") : "No privacy concerns detected"
}
}

View File

@@ -6,97 +6,98 @@ import QtQuick
import Quickshell
Singleton {
id: root
id: root
readonly property int levelInfo: 0
readonly property int levelWarn: 1
readonly property int levelError: 2
property string currentMessage: ""
property int currentLevel: levelInfo
property bool toastVisible: false
property var toastQueue: []
property string currentDetails: ""
property bool hasDetails: false
property string wallpaperErrorStatus: ""
readonly property int levelInfo: 0
readonly property int levelWarn: 1
readonly property int levelError: 2
property string currentMessage: ""
property int currentLevel: levelInfo
property bool toastVisible: false
property var toastQueue: []
property string currentDetails: ""
property bool hasDetails: false
property string wallpaperErrorStatus: ""
function showToast(message, level = levelInfo, details = "") {
toastQueue.push({
"message": message,
"level": level,
"details": details
})
if (!toastVisible)
processQueue()
}
function showInfo(message, details = "") {
showToast(message, levelInfo, details)
}
function showWarning(message, details = "") {
showToast(message, levelWarn, details)
}
function showError(message, details = "") {
showToast(message, levelError, details)
}
function hideToast() {
toastVisible = false
currentMessage = ""
currentDetails = ""
hasDetails = false
currentLevel = levelInfo
toastTimer.stop()
resetToastState()
if (toastQueue.length > 0)
processQueue()
}
function processQueue() {
if (toastQueue.length === 0)
return
const toast = toastQueue.shift()
currentMessage = toast.message
currentLevel = toast.level
currentDetails = toast.details || ""
hasDetails = currentDetails.length > 0
toastVisible = true
resetToastState()
if (toast.level === levelError && hasDetails) {
toastTimer.interval = 8000
toastTimer.start()
} else {
toastTimer.interval = toast.level === levelError ? 5000 : toast.level === levelWarn ? 4000 : 3000
toastTimer.start()
function showToast(message, level = levelInfo, details = "") {
toastQueue.push({
"message": message,
"level": level,
"details": details
})
if (!toastVisible)
processQueue()
}
}
signal resetToastState
function stopTimer() {
toastTimer.stop()
}
function restartTimer() {
if (hasDetails && currentLevel === levelError) {
toastTimer.interval = 8000
toastTimer.restart()
function showInfo(message, details = "") {
showToast(message, levelInfo, details)
}
}
function clearWallpaperError() {
wallpaperErrorStatus = ""
}
function showWarning(message, details = "") {
showToast(message, levelWarn, details)
}
Timer {
id: toastTimer
function showError(message, details = "") {
showToast(message, levelError, details)
}
interval: 5000
running: false
repeat: false
onTriggered: hideToast()
}
function hideToast() {
toastVisible = false
currentMessage = ""
currentDetails = ""
hasDetails = false
currentLevel = levelInfo
toastTimer.stop()
resetToastState()
if (toastQueue.length > 0)
processQueue()
}
function processQueue() {
if (toastQueue.length === 0)
return
const toast = toastQueue.shift()
currentMessage = toast.message
currentLevel = toast.level
currentDetails = toast.details || ""
hasDetails = currentDetails.length > 0
toastVisible = true
resetToastState()
if (toast.level === levelError && hasDetails) {
toastTimer.interval = 8000
toastTimer.start()
} else {
toastTimer.interval = toast.level
=== levelError ? 5000 : toast.level === levelWarn ? 4000 : 3000
toastTimer.start()
}
}
signal resetToastState
function stopTimer() {
toastTimer.stop()
}
function restartTimer() {
if (hasDetails && currentLevel === levelError) {
toastTimer.interval = 8000
toastTimer.restart()
}
}
function clearWallpaperError() {
wallpaperErrorStatus = ""
}
Timer {
id: toastTimer
interval: 5000
running: false
repeat: false
onTriggered: hideToast()
}
}

View File

@@ -7,93 +7,93 @@ import Quickshell
import Quickshell.Io
Singleton {
id: root
id: root
property string username: ""
property string fullName: ""
property string profilePicture: ""
property string uptime: ""
property string hostname: ""
property bool profileAvailable: false
property string username: ""
property string fullName: ""
property string profilePicture: ""
property string uptime: ""
property string hostname: ""
property bool profileAvailable: false
function getUserInfo() {
userInfoProcess.running = true
}
function getUptime() {
uptimeProcess.running = true
}
function refreshUserInfo() {
getUserInfo()
getUptime()
}
Component.onCompleted: {
getUserInfo()
getUptime()
}
// Get username and full name
Process {
id: userInfoProcess
command: ["bash", "-c", "echo \"$USER|$(getent passwd $USER | cut -d: -f5 | cut -d, -f1)|$(hostname)\""]
running: false
onExited: exitCode => {
if (exitCode !== 0) {
root.username = "User"
root.fullName = "User"
root.hostname = "System"
}
function getUserInfo() {
userInfoProcess.running = true
}
stdout: StdioCollector {
onStreamFinished: {
const parts = text.trim().split("|")
if (parts.length >= 3) {
root.username = parts[0] || ""
root.fullName = parts[1] || parts[0] || ""
root.hostname = parts[2] || ""
function getUptime() {
uptimeProcess.running = true
}
function refreshUserInfo() {
getUserInfo()
getUptime()
}
Component.onCompleted: {
getUserInfo()
getUptime()
}
// Get username and full name
Process {
id: userInfoProcess
command: ["bash", "-c", "echo \"$USER|$(getent passwd $USER | cut -d: -f5 | cut -d, -f1)|$(hostname)\""]
running: false
onExited: exitCode => {
if (exitCode !== 0) {
root.username = "User"
root.fullName = "User"
root.hostname = "System"
}
}
}
}
}
// Get system uptime
Process {
id: uptimeProcess
command: ["cat", "/proc/uptime"]
running: false
onExited: exitCode => {
if (exitCode !== 0) {
root.uptime = "Unknown"
}
stdout: StdioCollector {
onStreamFinished: {
const parts = text.trim().split("|")
if (parts.length >= 3) {
root.username = parts[0] || ""
root.fullName = parts[1] || parts[0] || ""
root.hostname = parts[2] || ""
}
}
}
}
stdout: StdioCollector {
onStreamFinished: {
const seconds = parseInt(text.split(" ")[0])
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
// Get system uptime
Process {
id: uptimeProcess
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"}`)
command: ["cat", "/proc/uptime"]
running: false
if (parts.length > 0)
root.uptime = "up " + parts.join(", ")
else
root.uptime = `up ${seconds} seconds`
}
onExited: exitCode => {
if (exitCode !== 0) {
root.uptime = "Unknown"
}
}
stdout: StdioCollector {
onStreamFinished: {
const seconds = parseInt(text.split(" ")[0])
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
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 (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
@@ -7,189 +8,203 @@ import Quickshell.Io
import qs.Common
Singleton {
id: root
id: root
property bool cyclingActive: false
property string cachedCyclingTime: SessionData.wallpaperCyclingTime
property int cachedCyclingInterval: SessionData.wallpaperCyclingInterval
property string lastTimeCheck: ""
property bool cyclingActive: false
property string cachedCyclingTime: SessionData.wallpaperCyclingTime
property int cachedCyclingInterval: SessionData.wallpaperCyclingInterval
property string lastTimeCheck: ""
Component.onCompleted: {
updateCyclingState()
}
Connections {
target: SessionData
function onWallpaperCyclingEnabledChanged() {
updateCyclingState()
}
function onWallpaperCyclingModeChanged() {
updateCyclingState()
}
function onWallpaperCyclingIntervalChanged() {
cachedCyclingInterval = SessionData.wallpaperCyclingInterval
if (SessionData.wallpaperCyclingMode === "interval") {
Component.onCompleted: {
updateCyclingState()
}
}
function onWallpaperCyclingTimeChanged() {
cachedCyclingTime = SessionData.wallpaperCyclingTime
if (SessionData.wallpaperCyclingMode === "time") {
updateCyclingState()
}
Connections {
target: SessionData
function onWallpaperCyclingEnabledChanged() {
updateCyclingState()
}
function onWallpaperCyclingModeChanged() {
updateCyclingState()
}
function onWallpaperCyclingIntervalChanged() {
cachedCyclingInterval = SessionData.wallpaperCyclingInterval
if (SessionData.wallpaperCyclingMode === "interval") {
updateCyclingState()
}
}
function onWallpaperCyclingTimeChanged() {
cachedCyclingTime = SessionData.wallpaperCyclingTime
if (SessionData.wallpaperCyclingMode === "time") {
updateCyclingState()
}
}
}
}
function updateCyclingState() {
if (SessionData.wallpaperCyclingEnabled && SessionData.wallpaperPath) {
startCycling()
} else {
stopCycling()
function updateCyclingState() {
if (SessionData.wallpaperCyclingEnabled && SessionData.wallpaperPath) {
startCycling()
} else {
stopCycling()
}
}
}
function startCycling() {
if (SessionData.wallpaperCyclingMode === "interval") {
intervalTimer.interval = cachedCyclingInterval * 1000
intervalTimer.start()
cyclingActive = true
} else if (SessionData.wallpaperCyclingMode === "time") {
cyclingActive = true
checkTimeBasedCycling()
}
}
function stopCycling() {
intervalTimer.stop()
cyclingActive = false
}
function cycleToNextWallpaper() {
if (!SessionData.wallpaperPath) return
const wallpaperDir = SessionData.wallpaperPath.substring(0, SessionData.wallpaperPath.lastIndexOf('/'))
cyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`]
cyclingProcess.running = true
}
function cycleToPrevWallpaper() {
if (!SessionData.wallpaperPath) return
const wallpaperDir = SessionData.wallpaperPath.substring(0, SessionData.wallpaperPath.lastIndexOf('/'))
prevCyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`]
prevCyclingProcess.running = true
}
function cycleNextManually() {
if (SessionData.wallpaperPath) {
cycleToNextWallpaper()
// Restart timers if cycling is active
if (cyclingActive && SessionData.wallpaperCyclingEnabled) {
function startCycling() {
if (SessionData.wallpaperCyclingMode === "interval") {
intervalTimer.interval = cachedCyclingInterval * 1000
intervalTimer.restart()
intervalTimer.interval = cachedCyclingInterval * 1000
intervalTimer.start()
cyclingActive = true
} else if (SessionData.wallpaperCyclingMode === "time") {
cyclingActive = true
checkTimeBasedCycling()
}
}
}
}
function cyclePrevManually() {
if (SessionData.wallpaperPath) {
cycleToPrevWallpaper()
// Restart timers if cycling is active
if (cyclingActive && SessionData.wallpaperCyclingEnabled) {
if (SessionData.wallpaperCyclingMode === "interval") {
intervalTimer.interval = cachedCyclingInterval * 1000
intervalTimer.restart()
function stopCycling() {
intervalTimer.stop()
cyclingActive = false
}
function cycleToNextWallpaper() {
if (!SessionData.wallpaperPath)
return
const wallpaperDir = SessionData.wallpaperPath.substring(
0, SessionData.wallpaperPath.lastIndexOf('/'))
cyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`]
cyclingProcess.running = true
}
function cycleToPrevWallpaper() {
if (!SessionData.wallpaperPath)
return
const wallpaperDir = SessionData.wallpaperPath.substring(
0, SessionData.wallpaperPath.lastIndexOf('/'))
prevCyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`]
prevCyclingProcess.running = true
}
function cycleNextManually() {
if (SessionData.wallpaperPath) {
cycleToNextWallpaper()
// Restart timers if cycling is active
if (cyclingActive && SessionData.wallpaperCyclingEnabled) {
if (SessionData.wallpaperCyclingMode === "interval") {
intervalTimer.interval = cachedCyclingInterval * 1000
intervalTimer.restart()
}
}
}
}
}
}
function checkTimeBasedCycling() {
const currentTime = Qt.formatTime(systemClock.date, "hh:mm")
if (currentTime === cachedCyclingTime && currentTime !== lastTimeCheck) {
lastTimeCheck = currentTime
cycleToNextWallpaper()
} else if (currentTime !== cachedCyclingTime) {
lastTimeCheck = ""
}
}
Timer {
id: intervalTimer
interval: cachedCyclingInterval * 1000
running: false
repeat: true
onTriggered: cycleToNextWallpaper()
}
SystemClock {
id: systemClock
precision: SystemClock.Minutes
onDateChanged: {
if (SessionData.wallpaperCyclingMode === "time" && cyclingActive) {
checkTimeBasedCycling()
}
}
}
Process {
id: cyclingProcess
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
const files = text.trim().split('\n').filter(file => file.length > 0)
if (files.length <= 1) return
const wallpaperList = files.sort()
const currentPath = SessionData.wallpaperPath
let currentIndex = wallpaperList.findIndex(path => path === currentPath)
if (currentIndex === -1) currentIndex = 0
// Get next wallpaper
const nextIndex = (currentIndex + 1) % wallpaperList.length
const nextWallpaper = wallpaperList[nextIndex]
if (nextWallpaper && nextWallpaper !== currentPath) {
SessionData.setWallpaper(nextWallpaper)
}
function cyclePrevManually() {
if (SessionData.wallpaperPath) {
cycleToPrevWallpaper()
// Restart timers if cycling is active
if (cyclingActive && SessionData.wallpaperCyclingEnabled) {
if (SessionData.wallpaperCyclingMode === "interval") {
intervalTimer.interval = cachedCyclingInterval * 1000
intervalTimer.restart()
}
}
}
}
}
}
Process {
id: prevCyclingProcess
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
const files = text.trim().split('\n').filter(file => file.length > 0)
if (files.length <= 1) return
const wallpaperList = files.sort()
const currentPath = SessionData.wallpaperPath
let currentIndex = wallpaperList.findIndex(path => path === currentPath)
if (currentIndex === -1) currentIndex = 0
// Get previous wallpaper
const prevIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1
const prevWallpaper = wallpaperList[prevIndex]
if (prevWallpaper && prevWallpaper !== currentPath) {
SessionData.setWallpaper(prevWallpaper)
}
function checkTimeBasedCycling() {
const currentTime = Qt.formatTime(systemClock.date, "hh:mm")
if (currentTime === cachedCyclingTime
&& currentTime !== lastTimeCheck) {
lastTimeCheck = currentTime
cycleToNextWallpaper()
} else if (currentTime !== cachedCyclingTime) {
lastTimeCheck = ""
}
}
}
}
}
Timer {
id: intervalTimer
interval: cachedCyclingInterval * 1000
running: false
repeat: true
onTriggered: cycleToNextWallpaper()
}
SystemClock {
id: systemClock
precision: SystemClock.Minutes
onDateChanged: {
if (SessionData.wallpaperCyclingMode === "time" && cyclingActive) {
checkTimeBasedCycling()
}
}
}
Process {
id: cyclingProcess
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
const files = text.trim().split('\n').filter(
file => file.length > 0)
if (files.length <= 1)
return
const wallpaperList = files.sort()
const currentPath = SessionData.wallpaperPath
let currentIndex = wallpaperList.findIndex(
path => path === currentPath)
if (currentIndex === -1)
currentIndex = 0
// Get next wallpaper
const nextIndex = (currentIndex + 1) % wallpaperList.length
const nextWallpaper = wallpaperList[nextIndex]
if (nextWallpaper && nextWallpaper !== currentPath) {
SessionData.setWallpaper(nextWallpaper)
}
}
}
}
}
Process {
id: prevCyclingProcess
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
const files = text.trim().split('\n').filter(
file => file.length > 0)
if (files.length <= 1)
return
const wallpaperList = files.sort()
const currentPath = SessionData.wallpaperPath
let currentIndex = wallpaperList.findIndex(
path => path === currentPath)
if (currentIndex === -1)
currentIndex = 0
// Get previous wallpaper
const prevIndex = currentIndex
=== 0 ? wallpaperList.length - 1 : currentIndex - 1
const prevWallpaper = wallpaperList[prevIndex]
if (prevWallpaper && prevWallpaper !== currentPath) {
SessionData.setWallpaper(prevWallpaper)
}
}
}
}
}
}

View File

@@ -8,346 +8,346 @@ import Quickshell.Io
import qs.Common
Singleton {
id: root
id: root
property int refCount: 0
property int refCount: 0
property var weather: ({
"available": false,
"loading": true,
"temp": 0,
"tempF": 0,
"city": "",
"wCode": "113",
"humidity": 0,
"wind": "",
"sunrise": "06:00",
"sunset": "18:00",
"uv": 0,
"pressure": 0
})
property var weather: ({
"available": false,
"loading": true,
"temp": 0,
"tempF": 0,
"city": "",
"wCode": "113",
"humidity": 0,
"wind": "",
"sunrise": "06:00",
"sunset": "18:00",
"uv": 0,
"pressure": 0
})
property int updateInterval: 600000 // 10 minutes
property int retryAttempts: 0
property int maxRetryAttempts: 3
property int retryDelay: 30000 // 30 seconds
property int lastFetchTime: 0
property int minFetchInterval: 30000 // 30 seconds minimum between fetches
property int persistentRetryCount: 0 // Track persistent retry attempts for backoff
property int updateInterval: 600000 // 10 minutes
property int retryAttempts: 0
property int maxRetryAttempts: 3
property int retryDelay: 30000 // 30 seconds
property int lastFetchTime: 0
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",
"119": "cloud",
"122": "cloud",
"143": "foggy",
"176": "rainy",
"179": "rainy",
"182": "rainy",
"185": "rainy",
"200": "thunderstorm",
"227": "cloudy_snowing",
"230": "snowing_heavy",
"248": "foggy",
"260": "foggy",
"263": "rainy",
"266": "rainy",
"281": "rainy",
"284": "rainy",
"293": "rainy",
"296": "rainy",
"299": "rainy",
"302": "weather_hail",
"305": "rainy",
"308": "weather_hail",
"311": "rainy",
"314": "rainy",
"317": "rainy",
"320": "cloudy_snowing",
"323": "cloudy_snowing",
"326": "cloudy_snowing",
"329": "snowing_heavy",
"332": "snowing_heavy",
"335": "snowing_heavy",
"338": "snowing_heavy",
"350": "rainy",
"353": "rainy",
"356": "weather_hail",
"359": "weather_hail",
"362": "rainy",
"365": "weather_hail",
"368": "cloudy_snowing",
"371": "snowing_heavy",
"374": "weather_hail",
"377": "weather_hail",
"386": "thunderstorm",
"389": "thunderstorm",
"392": "snowing_heavy",
"395": "snowing_heavy"
})
// Weather icon mapping (based on wttr.in weather codes)
property var weatherIcons: ({
"113": "clear_day",
"116": "partly_cloudy_day",
"119": "cloud",
"122": "cloud",
"143": "foggy",
"176": "rainy",
"179": "rainy",
"182": "rainy",
"185": "rainy",
"200": "thunderstorm",
"227": "cloudy_snowing",
"230": "snowing_heavy",
"248": "foggy",
"260": "foggy",
"263": "rainy",
"266": "rainy",
"281": "rainy",
"284": "rainy",
"293": "rainy",
"296": "rainy",
"299": "rainy",
"302": "weather_hail",
"305": "rainy",
"308": "weather_hail",
"311": "rainy",
"314": "rainy",
"317": "rainy",
"320": "cloudy_snowing",
"323": "cloudy_snowing",
"326": "cloudy_snowing",
"329": "snowing_heavy",
"332": "snowing_heavy",
"335": "snowing_heavy",
"338": "snowing_heavy",
"350": "rainy",
"353": "rainy",
"356": "weather_hail",
"359": "weather_hail",
"362": "rainy",
"365": "weather_hail",
"368": "cloudy_snowing",
"371": "snowing_heavy",
"374": "weather_hail",
"377": "weather_hail",
"386": "thunderstorm",
"389": "thunderstorm",
"392": "snowing_heavy",
"395": "snowing_heavy"
})
function getWeatherIcon(code) {
return weatherIcons[code] || "cloud"
}
function getWeatherUrl() {
if (SettingsData.useAutoLocation) {
const url = "wttr.in/?format=j1"
console.log("Using auto location, URL:", url)
return url
function getWeatherIcon(code) {
return weatherIcons[code] || "cloud"
}
const location = SettingsData.weatherCoordinates || "40.7128,-74.0060"
const url = `wttr.in/${encodeURIComponent(location)}?format=j1`
return url
}
function addRef() {
refCount++
if (refCount === 1 && !weather.available && SettingsData.weatherEnabled) {
// Start fetching when first consumer appears and weather is enabled
fetchWeather()
}
}
function removeRef() {
refCount = Math.max(0, refCount - 1)
}
function fetchWeather() {
// Only fetch if someone is consuming the data and weather is enabled
if (root.refCount === 0 || !SettingsData.weatherEnabled) {
return
}
if (weatherFetcher.running) {
console.log("Weather fetch already in progress, skipping")
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")
return
}
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.running = true
}
function forceRefresh() {
console.log("Force refreshing weather")
root.lastFetchTime = 0 // Reset throttle
fetchWeather()
}
function handleWeatherSuccess() {
root.retryAttempts = 0
root.persistentRetryCount = 0 // Reset persistent retry count on success
// Stop any persistent retry timer if running
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
}
}
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})`)
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)
persistentRetryCount++
console.log(`Scheduling persistent retry in ${backoffDelay / 1000}s`)
persistentRetryTimer.interval = backoffDelay
persistentRetryTimer.start()
}
}
Process {
id: weatherFetcher
command: ["bash", "-c", `curl -s --connect-timeout 10 --max-time 30 '${root.getWeatherUrl(
)}'`]
running: false
stdout: StdioCollector {
onStreamFinished: {
const raw = text.trim()
if (!raw || raw[0] !== "{") {
console.warn("No valid weather data received")
root.handleWeatherFailure()
return
function getWeatherUrl() {
if (SettingsData.useAutoLocation) {
const url = "wttr.in/?format=j1"
console.log("Using auto location, URL:", url)
return url
}
try {
const data = JSON.parse(raw)
const location = SettingsData.weatherCoordinates || "40.7128,-74.0060"
const url = `wttr.in/${encodeURIComponent(location)}?format=j1`
return url
}
const current = data.current_condition[0] || {}
const location = data.nearest_area[0] || {}
const astronomy = data.weather[0]?.astronomy[0] || {}
function addRef() {
refCount++
if (!Object.keys(current).length || !Object.keys(location).length) {
throw new Error("Required fields missing")
}
root.weather = {
"available": true,
"loading": false,
"temp": Number(current.temp_C) || 0,
"tempF": Number(current.temp_F) || 0,
"city": location.areaName[0]?.value || "Unknown",
"wCode": current.weatherCode || "113",
"humidity": Number(current.humidity) || 0,
"wind": `${current.windspeedKmph || 0} km/h`,
"sunrise": astronomy.sunrise || "06:00",
"sunset": astronomy.sunset || "18:00",
"uv": Number(current.uvIndex) || 0,
"pressure": Number(current.pressure) || 0
}
console.log("Weather updated:", root.weather.city,
`${root.weather.temp}°C`)
root.handleWeatherSuccess()
} catch (e) {
console.warn("Failed to parse weather data:", e.message)
root.handleWeatherFailure()
if (refCount === 1 && !weather.available
&& SettingsData.weatherEnabled) {
// Start fetching when first consumer appears and weather is enabled
fetchWeather()
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
console.warn("Weather fetch failed with exit code:", exitCode)
root.handleWeatherFailure()
}
function removeRef() {
refCount = Math.max(0, refCount - 1)
}
}
Timer {
id: updateTimer
interval: root.updateInterval
running: root.refCount > 0 && SettingsData.weatherEnabled
repeat: true
triggeredOnStart: true
onTriggered: {
root.fetchWeather()
function fetchWeather() {
// Only fetch if someone is consuming the data and weather is enabled
if (root.refCount === 0 || !SettingsData.weatherEnabled) {
return
}
if (weatherFetcher.running) {
console.log("Weather fetch already in progress, skipping")
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")
return
}
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.running = true
}
}
Timer {
id: retryTimer
interval: root.retryDelay
running: false
repeat: false
onTriggered: {
root.fetchWeather()
function forceRefresh() {
console.log("Force refreshing weather")
root.lastFetchTime = 0 // Reset throttle
fetchWeather()
}
}
Timer {
id: persistentRetryTimer
interval: 60000 // Will be dynamically set
running: false
repeat: false
onTriggered: {
console.log("Persistent retry attempt...")
root.fetchWeather()
function handleWeatherSuccess() {
root.retryAttempts = 0
root.persistentRetryCount = 0 // Reset persistent retry count on success
// Stop any persistent retry timer if running
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
}
}
}
Component.onCompleted: {
SettingsData.weatherCoordinatesChanged.connect(() => {
console.log(
"Weather location changed, force refreshing weather")
root.weather = {
"available": false,
"loading": true,
"temp": 0,
"tempF": 0,
"city": "",
"wCode": "113",
"humidity": 0,
"wind": "",
"sunrise": "06:00",
"sunset": "18:00",
"uv": 0,
"pressure": 0
}
root.lastFetchTime = 0
root.forceRefresh()
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})`)
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)
persistentRetryCount++
console.log(`Scheduling persistent retry in ${backoffDelay / 1000}s`)
persistentRetryTimer.interval = backoffDelay
persistentRetryTimer.start()
}
}
Process {
id: weatherFetcher
command: ["bash", "-c", `curl -s --connect-timeout 10 --max-time 30 '${root.getWeatherUrl(
)}'`]
running: false
stdout: StdioCollector {
onStreamFinished: {
const raw = text.trim()
if (!raw || raw[0] !== "{") {
console.warn("No valid weather data received")
root.handleWeatherFailure()
return
}
try {
const data = JSON.parse(raw)
const current = data.current_condition[0] || {}
const location = data.nearest_area[0] || {}
const astronomy = data.weather[0]?.astronomy[0] || {}
if (!Object.keys(current).length || !Object.keys(
location).length) {
throw new Error("Required fields missing")
}
root.weather = {
"available": true,
"loading": false,
"temp": Number(current.temp_C) || 0,
"tempF": Number(current.temp_F) || 0,
"city": location.areaName[0]?.value || "Unknown",
"wCode": current.weatherCode || "113",
"humidity": Number(current.humidity) || 0,
"wind": `${current.windspeedKmph || 0} km/h`,
"sunrise": astronomy.sunrise || "06:00",
"sunset": astronomy.sunset || "18:00",
"uv": Number(current.uvIndex) || 0,
"pressure": Number(current.pressure) || 0
}
console.log("Weather updated:", root.weather.city,
`${root.weather.temp}°C`)
root.handleWeatherSuccess()
} catch (e) {
console.warn("Failed to parse weather data:", e.message)
root.handleWeatherFailure()
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
console.warn("Weather fetch failed with exit code:", exitCode)
root.handleWeatherFailure()
}
}
}
Timer {
id: updateTimer
interval: root.updateInterval
running: root.refCount > 0 && SettingsData.weatherEnabled
repeat: true
triggeredOnStart: true
onTriggered: {
root.fetchWeather()
}
}
Timer {
id: retryTimer
interval: root.retryDelay
running: false
repeat: false
onTriggered: {
root.fetchWeather()
}
}
Timer {
id: persistentRetryTimer
interval: 60000 // Will be dynamically set
running: false
repeat: false
onTriggered: {
console.log("Persistent retry attempt...")
root.fetchWeather()
}
}
Component.onCompleted: {
SettingsData.weatherCoordinatesChanged.connect(() => {
console.log(
"Weather location changed, force refreshing weather")
root.weather = {
"available": false,
"loading": true,
"temp": 0,
"tempF": 0,
"city": "",
"wCode": "113",
"humidity": 0,
"wind": "",
"sunrise": "06:00",
"sunset": "18:00",
"uv": 0,
"pressure": 0
}
root.lastFetchTime = 0
root.forceRefresh()
})
SettingsData.weatherLocationChanged.connect(() => {
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")
root.weather = {
"available": false,
"loading": true,
"temp": 0,
"tempF": 0,
"city": "",
"wCode": "113",
"humidity": 0,
"wind": "",
"sunrise": "06:00",
"sunset": "18:00",
"uv": 0,
"pressure": 0
}
root.lastFetchTime = 0
root.forceRefresh()
})
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
root.forceRefresh()
} else if (!SettingsData.weatherEnabled) {
// Stop all timers when weather is disabled
updateTimer.stop()
retryTimer.stop()
persistentRetryTimer.stop()
if (weatherFetcher.running) {
weatherFetcher.running = false
}
}
})
SettingsData.weatherLocationChanged.connect(() => {
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")
root.weather = {
"available": false,
"loading": true,
"temp": 0,
"tempF": 0,
"city": "",
"wCode": "113",
"humidity": 0,
"wind": "",
"sunrise": "06:00",
"sunset": "18:00",
"uv": 0,
"pressure": 0
}
root.lastFetchTime = 0
root.forceRefresh()
})
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
root.forceRefresh()
} else if (!SettingsData.weatherEnabled) {
// Stop all timers when weather is disabled
updateTimer.stop()
retryTimer.stop()
persistentRetryTimer.stop()
if (weatherFetcher.running) {
weatherFetcher.running = false
}
}
})
}
}
}