1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-09 23:15:38 -05:00

replace qmlformat with a better tool

still not perfect, but well - what can ya do
This commit is contained in:
bbedward
2025-08-08 15:55:37 -04:00
parent 8dc6e2805d
commit 4d408c65f2
137 changed files with 30315 additions and 29625 deletions

View File

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick
import Quickshell
@@ -8,147 +9,149 @@ import Quickshell.Widgets
import "../Common/fuzzysort.js" as Fuzzy
Singleton {
id: root
property var applications: DesktopEntries.applications.values
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
}
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
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
}
return finalScore
},
limit: 50
})
return results.map(r => r.obj.entry)
}
id: root
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)
property var applications: DesktopEntries.applications.values
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
}
// 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"
if (preppedApps.length === 0) {
return []
}
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()
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 (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"
}
function getAppsInCategory(category) {
if (category === "All") {
return applications
}
return applications.filter(app => {
var appCategories = getCategoriesForApp(app)
return appCategories.includes(category)
})
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"
})
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

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick
import Quickshell
@@ -7,170 +8,174 @@ import Quickshell.Io
import Quickshell.Services.Pipewire
Singleton {
id: root
id: root
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode source: Pipewire.defaultAudioSource
signal volumeChanged()
signal micMuteChanged()
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode source: Pipewire.defaultAudioSource
function displayName(node) {
if (!node) return ""
if (node.properties && node.properties["device.description"]) {
return node.properties["device.description"]
}
signal volumeChanged
signal micMuteChanged
if (node.description && node.description !== node.name) {
return node.description
}
function displayName(node) {
if (!node)
return ""
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
if (node.properties && node.properties["device.description"]) {
return node.properties["device.description"]
}
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 ""
if (node.description && node.description !== node.name) {
return node.description
}
PwObjectTracker {
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
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"
// 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";
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]
}
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";
// 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 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 increment(step: string): string {
if (root.sink && root.sink.audio) {
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 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";
function decrement(step: string): string {
if (root.sink && root.sink.audio) {
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"
}
// 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) {
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) {
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;
}
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,57 +1,72 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick
import Quickshell
import Quickshell.Services.UPower
Singleton {
id: root
id: root
readonly property UPowerDevice device: UPower.displayDevice
readonly property bool batteryAvailable: device && device.ready && device.isLaptopBattery
readonly property int batteryLevel: batteryAvailable ? device.percentage * 100 : 0
readonly property bool isCharging: batteryAvailable && device.state === UPowerDeviceState.Charging
readonly property bool isLowBattery: batteryAvailable && batteryLevel <= 20
readonly property string batteryHealth: batteryAvailable && device.healthSupported ? Math.round(device.healthPercentage * 100) + "%" : "N/A"
readonly property real batteryCapacity: batteryAvailable && device.energyCapacity > 0 ? device.energyCapacity : 0
readonly property string batteryStatus: {
if (!batteryAvailable)
return "No Battery";
readonly property UPowerDevice device: UPower.displayDevice
readonly property bool batteryAvailable: device && device.ready
&& device.isLaptopBattery
readonly property int batteryLevel: batteryAvailable ? device.percentage * 100 : 0
readonly property bool isCharging: batteryAvailable
&& device.state === UPowerDeviceState.Charging
readonly property bool isLowBattery: batteryAvailable && batteryLevel <= 20
readonly property string batteryHealth: batteryAvailable
&& device.healthSupported ? Math.round(
device.healthPercentage
* 100) + "%" : "N/A"
readonly property real batteryCapacity: batteryAvailable
&& device.energyCapacity > 0 ? device.energyCapacity : 0
readonly property string batteryStatus: {
if (!batteryAvailable)
return "No Battery"
return UPowerDeviceState.toString(device.state);
return UPowerDeviceState.toString(device.state)
}
readonly property int timeRemaining: {
if (!batteryAvailable)
return 0
return isCharging ? (device.timeToFull || 0) : (device.timeToEmpty || 0)
}
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))
btDevices.push({
"name": dev.model || UPowerDeviceType.toString(dev.type),
"percentage": Math.round(dev.percentage),
"type": dev.type
})
}
readonly property int timeRemaining: {
if (!batteryAvailable)
return 0;
return btDevices
}
return isCharging ? (device.timeToFull || 0) : (device.timeToEmpty || 0);
}
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))
btDevices.push({
"name": dev.model || UPowerDeviceType.toString(dev.type),
"percentage": Math.round(dev.percentage),
"type": dev.type
});
function formatTimeRemaining() {
if (!batteryAvailable || timeRemaining <= 0)
return "Unknown"
}
return btDevices;
}
function formatTimeRemaining() {
if (!batteryAvailable || timeRemaining <= 0)
return "Unknown";
const hours = Math.floor(timeRemaining / 3600);
const minutes = Math.floor((timeRemaining % 3600) / 60);
if (hours > 0)
return hours + "h " + minutes + "m";
else
return minutes + "m";
}
const hours = Math.floor(timeRemaining / 3600)
const minutes = Math.floor((timeRemaining % 3600) / 60)
if (hours > 0)
return hours + "h " + minutes + "m"
else
return minutes + "m"
}
}

View File

@@ -1,142 +1,155 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick
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 || "";
var aHasRealName = aName.includes(" ") && aName.length > 3;
var bHasRealName = bName.includes(" ") && bName.length > 3;
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;
});
}
function sortDevices(devices) {
return devices.sort((a, b) => {
var aName = a.name || a.deviceName || ""
var bName = b.name || b.deviceName || ""
function getDeviceIcon(device) {
if (!device)
return "bluetooth";
var aHasRealName = aName.includes(" ")
&& aName.length > 3
var bHasRealName = bName.includes(" ")
&& bName.length > 3
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 (aHasRealName && !bHasRealName)
return -1
if (!aHasRealName && bHasRealName)
return 1
if (icon.includes("mouse") || name.includes("mouse"))
return "mouse";
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
})
}
if (icon.includes("keyboard") || name.includes("keyboard"))
return "keyboard";
function getDeviceIcon(device) {
if (!device)
return "bluetooth"
if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android") || name.includes("samsung"))
return "smartphone";
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("watch") || name.includes("watch"))
return "watch";
if (icon.includes("mouse") || name.includes("mouse"))
return "mouse"
if (icon.includes("speaker") || name.includes("speaker"))
return "speaker";
if (icon.includes("keyboard") || name.includes("keyboard"))
return "keyboard"
if (icon.includes("display") || name.includes("tv"))
return "tv";
if (icon.includes("phone") || name.includes("phone") || name.includes(
"iphone") || name.includes("android") || name.includes("samsung"))
return "smartphone"
return "bluetooth";
}
if (icon.includes("watch") || name.includes("watch"))
return "watch"
if (icon.includes("speaker") || name.includes("speaker"))
return "speaker"
if (icon.includes("display") || name.includes("tv"))
return "tv"
function canConnect(device) {
if (!device)
return false;
return "bluetooth"
}
return !device.paired && !device.pairing && !device.blocked;
}
function canConnect(device) {
if (!device)
return false
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 connectDeviceWithTrust(device) {
if (!device) return;
device.trusted = true;
device.connect();
}
function isDeviceBusy(device) {
if (!device)
return false
return device.pairing || device.state === BluetoothDeviceState.Disconnecting
|| device.state === BluetoothDeviceState.Connecting
}
function connectDeviceWithTrust(device) {
if (!device)
return
device.trusted = true
device.connect()
}
}

View File

@@ -1,200 +1,211 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property bool brightnessAvailable: laptopBacklightAvailable || ddcAvailable
property bool laptopBacklightAvailable: false
property bool ddcAvailable: false
property int brightnessLevel: 50
property int maxBrightness: 100
property int currentRawBrightness: 0
property bool brightnessInitialized: false
signal brightnessChanged()
function setBrightnessInternal(percentage) {
brightnessLevel = Math.max(1, Math.min(100, percentage));
if (laptopBacklightAvailable) {
laptopBrightnessProcess.command = ["brightnessctl", "set", brightnessLevel + "%"];
laptopBrightnessProcess.running = true;
} else if (ddcAvailable) {
Quickshell.execDetached(["ddcutil", "setvcp", "10", brightnessLevel.toString()]);
}
}
function setBrightness(percentage) {
setBrightnessInternal(percentage);
brightnessChanged();
}
Component.onCompleted: {
ddcAvailabilityChecker.running = true;
laptopBacklightChecker.running = true;
}
onLaptopBacklightAvailableChanged: {
if (laptopBacklightAvailable && !brightnessInitialized) {
laptopBrightnessInitProcess.running = true;
}
}
onDdcAvailableChanged: {
if (ddcAvailable && !laptopBacklightAvailable && !brightnessInitialized) {
ddcBrightnessInitProcess.running = true;
}
}
Process {
id: ddcAvailabilityChecker
command: ["which", "ddcutil"]
onExited: function(exitCode) {
ddcAvailable = (exitCode === 0);
}
}
Process {
id: laptopBacklightChecker
command: ["brightnessctl", "--list"]
onExited: function(exitCode) {
laptopBacklightAvailable = (exitCode === 0);
}
}
Process {
id: laptopBrightnessProcess
running: false
onExited: function(exitCode) {
if (exitCode !== 0) {
}
}
}
Process {
id: laptopBrightnessInitProcess
command: ["brightnessctl", "get"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
currentRawBrightness = parseInt(text.trim());
laptopMaxBrightnessProcess.running = true;
}
}
}
onExited: function(exitCode) {
if (exitCode !== 0) {
console.warn("BrightnessService: Failed to read current brightness:", exitCode);
}
}
}
Process {
id: laptopMaxBrightnessProcess
command: ["brightnessctl", "max"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
maxBrightness = parseInt(text.trim());
brightnessLevel = Math.round((currentRawBrightness / maxBrightness) * 100);
brightnessInitialized = true;
console.log("BrightnessService: Initialized with brightness level:", brightnessLevel + "%");
}
}
}
onExited: function(exitCode) {
if (exitCode !== 0) {
console.warn("BrightnessService: Failed to read max brightness:", exitCode);
}
}
}
Process {
id: ddcBrightnessInitProcess
command: ["ddcutil", "getvcp", "10", "--brief"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
const parts = text.trim().split(" ");
if (parts.length >= 5) {
const current = parseInt(parts[3]) || 50;
const max = parseInt(parts[4]) || 100;
brightnessLevel = Math.round((current / max) * 100);
brightnessInitialized = true;
}
}
}
}
onExited: function(exitCode) {
if (exitCode !== 0) {
if (!laptopBacklightAvailable) {
console.warn("BrightnessService: DDC brightness read failed:", exitCode);
}
}
}
}
// IPC Handler for external control
IpcHandler {
target: "brightness"
id: root
function set(percentage: string): string {
if (!root.brightnessAvailable) {
return "Brightness control not available";
}
const value = parseInt(percentage);
const clampedValue = Math.max(1, Math.min(100, value));
root.setBrightness(clampedValue);
return "Brightness set to " + clampedValue + "%";
}
property bool brightnessAvailable: laptopBacklightAvailable || ddcAvailable
property bool laptopBacklightAvailable: false
property bool ddcAvailable: false
property int brightnessLevel: 50
property int maxBrightness: 100
property int currentRawBrightness: 0
property bool brightnessInitialized: false
function increment(step: string): string {
if (!root.brightnessAvailable) {
return "Brightness control not available";
}
const currentLevel = root.brightnessLevel;
const newLevel = Math.max(1, Math.min(100, currentLevel + parseInt(step || "10")));
root.setBrightness(newLevel);
return "Brightness increased to " + newLevel + "%";
}
signal brightnessChanged
function decrement(step: string): string {
if (!root.brightnessAvailable) {
return "Brightness control not available";
}
const currentLevel = root.brightnessLevel;
const newLevel = Math.max(1, Math.min(100, currentLevel - parseInt(step || "10")));
root.setBrightness(newLevel);
return "Brightness decreased to " + newLevel + "%";
}
function setBrightnessInternal(percentage) {
brightnessLevel = Math.max(1, Math.min(100, percentage))
function status(): string {
if (!root.brightnessAvailable) {
return "Brightness control not available";
}
return "Brightness: " + root.brightnessLevel + "% (" +
(root.laptopBacklightAvailable ? "laptop backlight" : "DDC") + ")";
}
if (laptopBacklightAvailable) {
laptopBrightnessProcess.command = ["brightnessctl", "set", brightnessLevel + "%"]
laptopBrightnessProcess.running = true
} else if (ddcAvailable) {
Quickshell.execDetached(
["ddcutil", "setvcp", "10", brightnessLevel.toString()])
}
}
}
function setBrightness(percentage) {
setBrightnessInternal(percentage)
brightnessChanged()
}
Component.onCompleted: {
ddcAvailabilityChecker.running = true
laptopBacklightChecker.running = true
}
onLaptopBacklightAvailableChanged: {
if (laptopBacklightAvailable && !brightnessInitialized) {
laptopBrightnessInitProcess.running = true
}
}
onDdcAvailableChanged: {
if (ddcAvailable && !laptopBacklightAvailable && !brightnessInitialized) {
ddcBrightnessInitProcess.running = true
}
}
Process {
id: ddcAvailabilityChecker
command: ["which", "ddcutil"]
onExited: function (exitCode) {
ddcAvailable = (exitCode === 0)
}
}
Process {
id: laptopBacklightChecker
command: ["brightnessctl", "--list"]
onExited: function (exitCode) {
laptopBacklightAvailable = (exitCode === 0)
}
}
Process {
id: laptopBrightnessProcess
running: false
onExited: function (exitCode) {
if (exitCode !== 0) {
}
}
}
Process {
id: laptopBrightnessInitProcess
command: ["brightnessctl", "get"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
currentRawBrightness = parseInt(text.trim())
laptopMaxBrightnessProcess.running = true
}
}
}
onExited: function (exitCode) {
if (exitCode !== 0) {
console.warn("BrightnessService: Failed to read current brightness:",
exitCode)
}
}
}
Process {
id: laptopMaxBrightnessProcess
command: ["brightnessctl", "max"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
maxBrightness = parseInt(text.trim())
brightnessLevel = Math.round(
(currentRawBrightness / maxBrightness) * 100)
brightnessInitialized = true
console.log("BrightnessService: Initialized with brightness level:",
brightnessLevel + "%")
}
}
}
onExited: function (exitCode) {
if (exitCode !== 0) {
console.warn("BrightnessService: Failed to read max brightness:",
exitCode)
}
}
}
Process {
id: ddcBrightnessInitProcess
command: ["ddcutil", "getvcp", "10", "--brief"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
const parts = text.trim().split(" ")
if (parts.length >= 5) {
const current = parseInt(parts[3]) || 50
const max = parseInt(parts[4]) || 100
brightnessLevel = Math.round((current / max) * 100)
brightnessInitialized = true
}
}
}
}
onExited: function (exitCode) {
if (exitCode !== 0) {
if (!laptopBacklightAvailable) {
console.warn("BrightnessService: DDC brightness read failed:",
exitCode)
}
}
}
}
// IPC Handler for external control
IpcHandler {
target: "brightness"
function set(percentage: string): string {
if (!root.brightnessAvailable) {
return "Brightness control not available"
}
const value = parseInt(percentage)
const clampedValue = Math.max(1, Math.min(100, value))
root.setBrightness(clampedValue)
return "Brightness set to " + clampedValue + "%"
}
function increment(step: string): string {
if (!root.brightnessAvailable) {
return "Brightness control not available"
}
const currentLevel = root.brightnessLevel
const newLevel = Math.max(1,
Math.min(100,
currentLevel + parseInt(step || "10")))
root.setBrightness(newLevel)
return "Brightness increased to " + newLevel + "%"
}
function decrement(step: string): string {
if (!root.brightnessAvailable) {
return "Brightness control not available"
}
const currentLevel = root.brightnessLevel
const newLevel = Math.max(1,
Math.min(100,
currentLevel - parseInt(step || "10")))
root.setBrightness(newLevel)
return "Brightness decreased to " + newLevel + "%"
}
function status(): string {
if (!root.brightnessAvailable) {
return "Brightness control not available"
}
return "Brightness: " + root.brightnessLevel + "% ("
+ (root.laptopBacklightAvailable ? "laptop backlight" : "DDC") + ")"
}
}
}

View File

@@ -1,245 +1,244 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick
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 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 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);
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
}
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;
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 getEventsForDate(date) {
let dateKey = Qt.formatDate(date, "yyyy-MM-dd");
return root.eventsByDate[dateKey] || [];
}
// Process for loading events
Process {
id: eventsProcess
function hasEventsForDate(date) {
let events = getEventsForDate(date);
return events.length > 0;
}
property date requestStartDate
property date requestEndDate
property string rawOutput: ""
// Initialize on component completion
Component.onCompleted: {
checkKhalAvailability();
}
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
// Process for checking khal configuration
Process {
id: khalCheckProcess
// 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
command: ["khal", "list", "today"]
running: false
onExited: (exitCode) => {
root.khalAvailable = (exitCode === 0);
if (exitCode === 0) {
loadCurrentMonth();
// 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()
}
}
}
// 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 ;
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)
}
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);
}
}
// 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)
}
}
// 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";
// 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 = {}
}
// Reset for next run
eventsProcess.rawOutput = ""
}
stdout: SplitParser {
splitMarker: "\n"
onRead: data => {
eventsProcess.rawOutput += data + "\n"
}
}
}
}

View File

@@ -1,53 +1,54 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick
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: true
onExited: (exitCode) => {
root.cavaAvailable = exitCode === 0;
}
command: ["which", "cava"]
running: true
onExited: exitCode => {
root.cavaAvailable = exitCode === 0
}
}
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)
}
}
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);
}
}
}
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

@@ -1,101 +1,96 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
id: root
property bool niriAvailable: false
property string focusedAppId: ""
property string focusedAppName: ""
property string focusedWindowTitle: ""
property int focusedWindowId: -1
property bool niriAvailable: false
property string focusedAppId: ""
property string focusedAppName: ""
property string focusedWindowTitle: ""
property int focusedWindowId: -1
function updateFromNiriData() {
if (!root.niriAvailable) {
clearFocusedWindow();
return;
}
let focusedWindow = NiriService.windows.find(w => w.is_focused);
if (focusedWindow) {
root.focusedAppId = focusedWindow.app_id || "";
root.focusedWindowTitle = focusedWindow.title || "";
root.focusedAppName = getDisplayName(focusedWindow.app_id || "");
root.focusedWindowId = parseInt(focusedWindow.id) || -1;
} else {
clearFocusedWindow();
}
function updateFromNiriData() {
if (!root.niriAvailable) {
clearFocusedWindow()
return
}
let focusedWindow = NiriService.windows.find(w => w.is_focused)
function clearFocusedWindow() {
root.focusedAppId = "";
root.focusedAppName = "";
root.focusedWindowTitle = "";
if (focusedWindow) {
root.focusedAppId = focusedWindow.app_id || ""
root.focusedWindowTitle = focusedWindow.title || ""
root.focusedAppName = getDisplayName(focusedWindow.app_id || "")
root.focusedWindowId = parseInt(focusedWindow.id) || -1
} else {
clearFocusedWindow()
}
}
function clearFocusedWindow() {
root.focusedAppId = ""
root.focusedAppName = ""
root.focusedWindowTitle = ""
}
// Convert app_id to a more user-friendly display name
function getDisplayName(appId) {
if (!appId)
return ""
const desktopEntry = DesktopEntries.byId(appId)
return desktopEntry && desktopEntry.name ? desktopEntry.name : ""
}
Component.onCompleted: {
root.niriAvailable = NiriService.niriAvailable
NiriService.onNiriAvailableChanged.connect(() => {
root.niriAvailable = NiriService.niriAvailable
if (root.niriAvailable)
updateFromNiriData()
})
if (root.niriAvailable)
updateFromNiriData()
}
Connections {
function onFocusedWindowIdChanged() {
const focusedWindowId = NiriService.focusedWindowId
if (!focusedWindowId) {
clearFocusedWindow()
return
}
const focusedWindow = NiriService.windows.find(
w => w.id == focusedWindowId)
if (focusedWindow) {
root.focusedAppId = focusedWindow.app_id || ""
root.focusedWindowTitle = focusedWindow.title || ""
root.focusedAppName = getDisplayName(focusedWindow.app_id || "")
root.focusedWindowId = parseInt(focusedWindow.id) || -1
} else {
clearFocusedWindow()
}
}
// Convert app_id to a more user-friendly display name
function getDisplayName(appId) {
if (!appId)
return "";
const desktopEntry = DesktopEntries.byId(appId);
return desktopEntry && desktopEntry.name ? desktopEntry.name : "";
function onWindowsChanged() {
updateFromNiriData()
}
Component.onCompleted: {
root.niriAvailable = NiriService.niriAvailable;
NiriService.onNiriAvailableChanged.connect(() => {
root.niriAvailable = NiriService.niriAvailable;
if (root.niriAvailable)
updateFromNiriData();
});
if (root.niriAvailable)
updateFromNiriData();
function onWindowOpenedOrChanged(windowData) {
if (windowData.is_focused) {
root.focusedAppId = windowData.app_id || ""
root.focusedWindowTitle = windowData.title || ""
root.focusedAppName = getDisplayName(windowData.app_id || "")
root.focusedWindowId = parseInt(windowData.id) || -1
}
}
Connections {
function onFocusedWindowIdChanged() {
const focusedWindowId = NiriService.focusedWindowId;
if (!focusedWindowId) {
clearFocusedWindow();
return;
}
const focusedWindow = NiriService.windows.find(w => w.id == focusedWindowId);
if (focusedWindow) {
root.focusedAppId = focusedWindow.app_id || "";
root.focusedWindowTitle = focusedWindow.title || "";
root.focusedAppName = getDisplayName(focusedWindow.app_id || "");
root.focusedWindowId = parseInt(focusedWindow.id) || -1;
} else {
clearFocusedWindow();
}
}
function onWindowsChanged() {
updateFromNiriData();
}
function onWindowOpenedOrChanged(windowData) {
if (windowData.is_focused) {
root.focusedAppId = windowData.app_id || "";
root.focusedWindowTitle = windowData.title || "";
root.focusedAppName = getDisplayName(windowData.app_id || "");
root.focusedWindowId = parseInt(windowData.id) || -1;
}
}
target: NiriService
}
target: NiriService
}
}

View File

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick
import Quickshell
@@ -8,48 +9,48 @@ import Quickshell.Services.Mpris
import Quickshell.Widgets
Singleton {
id: root
id: root
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying)
?? availablePlayers.find(p => p.canControl && p.canPlay)
?? null
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
IpcHandler {
target: "mpris"
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying)
?? availablePlayers.find(
p => p.canControl && p.canPlay) ?? null
function list(): string {
return root.availablePlayers.map(p => p.identity).join("");
}
IpcHandler {
target: "mpris"
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 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()
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,299 +1,303 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
id: root
// Workspace management
property var workspaces: ({})
property var allWorkspaces: []
property int focusedWorkspaceIndex: 0
property string focusedWorkspaceId: ""
property var currentOutputWorkspaces: []
property string currentOutput: ""
// Workspace management
property var workspaces: ({})
property var allWorkspaces: []
property int focusedWorkspaceIndex: 0
property string focusedWorkspaceId: ""
property var currentOutputWorkspaces: []
property string currentOutput: ""
// Window management
property var windows: []
property int focusedWindowIndex: -1
property string focusedWindowTitle: "(No active window)"
property string focusedWindowId: ""
// Window management
property var windows: []
property int focusedWindowIndex: -1
property string focusedWindowTitle: "(No active window)"
property string focusedWindowId: ""
// Overview state
property bool inOverview: false
// Overview state
property bool inOverview: false
signal windowOpenedOrChanged(var windowData)
signal windowOpenedOrChanged(var windowData)
// Feature availability
property bool niriAvailable: false
// Feature availability
property bool niriAvailable: false
readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
Component.onCompleted: checkNiriAvailability()
Component.onCompleted: checkNiriAvailability()
Process {
id: niriCheck
command: ["test", "-S", root.socketPath]
Process {
id: niriCheck
command: ["test", "-S", root.socketPath]
onExited: exitCode => {
root.niriAvailable = exitCode === 0;
if (root.niriAvailable) {
eventStreamSocket.connected = true;
}
onExited: exitCode => {
root.niriAvailable = exitCode === 0
if (root.niriAvailable) {
eventStreamSocket.connected = true
}
}
}
function checkNiriAvailability() {
niriCheck.running = true
}
Socket {
id: eventStreamSocket
path: root.socketPath
connected: false
onConnectionStateChanged: {
if (connected) {
write('"EventStream"\n')
}
}
parser: SplitParser {
onRead: line => {
try {
const event = JSON.parse(line)
handleNiriEvent(event)
} catch (e) {
console.warn("NiriService: Failed to parse event:", line, e)
}
}
}
}
Socket {
id: requestSocket
path: root.socketPath
connected: root.niriAvailable
}
function handleNiriEvent(event) {
if (event.WorkspacesChanged) {
handleWorkspacesChanged(event.WorkspacesChanged)
} else if (event.WorkspaceActivated) {
handleWorkspaceActivated(event.WorkspaceActivated)
} else if (event.WindowsChanged) {
handleWindowsChanged(event.WindowsChanged)
} else if (event.WindowClosed) {
handleWindowClosed(event.WindowClosed)
} else if (event.WindowFocusChanged) {
handleWindowFocusChanged(event.WindowFocusChanged)
} else if (event.WindowOpenedOrChanged) {
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged)
} else if (event.OverviewOpenedOrClosed) {
handleOverviewChanged(event.OverviewOpenedOrClosed)
}
}
function handleWorkspacesChanged(data) {
const workspaces = {}
for (const ws of data.workspaces) {
workspaces[ws.id] = ws
}
function checkNiriAvailability() {
niriCheck.running = true;
root.workspaces = workspaces
allWorkspaces = [...data.workspaces].sort((a, b) => a.idx - b.idx)
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused)
if (focusedWorkspaceIndex >= 0) {
var focusedWs = allWorkspaces[focusedWorkspaceIndex]
focusedWorkspaceId = focusedWs.id
currentOutput = focusedWs.output || ""
} else {
focusedWorkspaceIndex = 0
focusedWorkspaceId = ""
}
Socket {
id: eventStreamSocket
path: root.socketPath
connected: false
updateCurrentOutputWorkspaces()
}
onConnectionStateChanged: {
if (connected) {
write('"EventStream"\n');
}
}
function handleWorkspaceActivated(data) {
const ws = root.workspaces[data.id]
if (!ws)
return
const output = ws.output
parser: SplitParser {
onRead: line => {
try {
const event = JSON.parse(line);
handleNiriEvent(event);
} catch (e) {
console.warn("NiriService: Failed to parse event:", line, e);
}
}
}
for (const id in root.workspaces) {
const workspace = root.workspaces[id]
const got_activated = workspace.id === data.id
if (workspace.output === output) {
workspace.is_active = got_activated
}
if (data.focused) {
workspace.is_focused = got_activated
}
}
Socket {
id: requestSocket
path: root.socketPath
connected: root.niriAvailable
focusedWorkspaceId = data.id
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id)
if (focusedWorkspaceIndex >= 0) {
currentOutput = allWorkspaces[focusedWorkspaceIndex].output || ""
}
function handleNiriEvent(event) {
if (event.WorkspacesChanged) {
handleWorkspacesChanged(event.WorkspacesChanged);
} else if (event.WorkspaceActivated) {
handleWorkspaceActivated(event.WorkspaceActivated);
} else if (event.WindowsChanged) {
handleWindowsChanged(event.WindowsChanged);
} else if (event.WindowClosed) {
handleWindowClosed(event.WindowClosed);
} else if (event.WindowFocusChanged) {
handleWindowFocusChanged(event.WindowFocusChanged);
} else if (event.WindowOpenedOrChanged) {
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged);
} else if (event.OverviewOpenedOrClosed) {
handleOverviewChanged(event.OverviewOpenedOrClosed);
}
allWorkspaces = Object.values(root.workspaces).sort((a, b) => a.idx - b.idx)
updateCurrentOutputWorkspaces()
workspacesChanged()
}
function handleWindowsChanged(data) {
windows = [...data.windows].sort((a, b) => a.id - b.id)
updateFocusedWindow()
}
function handleWindowClosed(data) {
windows = windows.filter(w => w.id !== data.id)
updateFocusedWindow()
}
function handleWindowFocusChanged(data) {
if (data.id) {
focusedWindowId = data.id
focusedWindowIndex = windows.findIndex(w => w.id === data.id)
} else {
focusedWindowId = ""
focusedWindowIndex = -1
}
updateFocusedWindow()
}
function handleWindowOpenedOrChanged(data) {
if (!data.window)
return
const window = data.window
const existingIndex = windows.findIndex(w => w.id === window.id)
if (existingIndex >= 0) {
let updatedWindows = [...windows]
updatedWindows[existingIndex] = window
windows = updatedWindows.sort((a, b) => a.id - b.id)
} else {
windows = [...windows, window].sort((a, b) => a.id - b.id)
}
function handleWorkspacesChanged(data) {
const workspaces = {};
for (const ws of data.workspaces) {
workspaces[ws.id] = ws;
}
root.workspaces = workspaces;
allWorkspaces = [...data.workspaces].sort((a, b) => a.idx - b.idx);
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused);
if (focusedWorkspaceIndex >= 0) {
var focusedWs = allWorkspaces[focusedWorkspaceIndex];
focusedWorkspaceId = focusedWs.id;
currentOutput = focusedWs.output || "";
} else {
focusedWorkspaceIndex = 0;
focusedWorkspaceId = "";
}
updateCurrentOutputWorkspaces();
if (window.is_focused) {
focusedWindowId = window.id
focusedWindowIndex = windows.findIndex(w => w.id === window.id)
}
function handleWorkspaceActivated(data) {
const ws = root.workspaces[data.id];
if (!ws)
return;
const output = ws.output;
updateFocusedWindow()
for (const id in root.workspaces) {
const workspace = root.workspaces[id];
const got_activated = workspace.id === data.id;
windowOpenedOrChanged(window)
}
if (workspace.output === output) {
workspace.is_active = got_activated;
}
function handleOverviewChanged(data) {
inOverview = data.is_open
}
if (data.focused) {
workspace.is_focused = got_activated;
}
}
focusedWorkspaceId = data.id;
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id);
if (focusedWorkspaceIndex >= 0) {
currentOutput = allWorkspaces[focusedWorkspaceIndex].output || "";
}
allWorkspaces = Object.values(root.workspaces).sort((a, b) => a.idx - b.idx);
updateCurrentOutputWorkspaces();
workspacesChanged();
function updateCurrentOutputWorkspaces() {
if (!currentOutput) {
currentOutputWorkspaces = allWorkspaces
return
}
function handleWindowsChanged(data) {
windows = [...data.windows].sort((a, b) => a.id - b.id);
updateFocusedWindow();
var outputWs = allWorkspaces.filter(w => w.output === currentOutput)
currentOutputWorkspaces = outputWs
}
function updateFocusedWindow() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
var focusedWin = windows[focusedWindowIndex]
focusedWindowTitle = focusedWin.title || "(Unnamed window)"
} else {
focusedWindowTitle = "(No active window)"
}
}
function handleWindowClosed(data) {
windows = windows.filter(w => w.id !== data.id);
updateFocusedWindow();
}
function send(request) {
if (!niriAvailable || !requestSocket.connected)
return false
requestSocket.write(JSON.stringify(request) + "\n")
return true
}
function handleWindowFocusChanged(data) {
if (data.id) {
focusedWindowId = data.id;
focusedWindowIndex = windows.findIndex(w => w.id === data.id);
} else {
focusedWindowId = "";
focusedWindowIndex = -1;
}
updateFocusedWindow();
}
function handleWindowOpenedOrChanged(data) {
if (!data.window)
return;
const window = data.window;
const existingIndex = windows.findIndex(w => w.id === window.id);
if (existingIndex >= 0) {
let updatedWindows = [...windows];
updatedWindows[existingIndex] = window;
windows = updatedWindows.sort((a, b) => a.id - b.id);
} else {
windows = [...windows, window].sort((a, b) => a.id - b.id);
}
if (window.is_focused) {
focusedWindowId = window.id;
focusedWindowIndex = windows.findIndex(w => w.id === window.id);
}
updateFocusedWindow();
windowOpenedOrChanged(window);
}
function handleOverviewChanged(data) {
inOverview = data.is_open;
}
function updateCurrentOutputWorkspaces() {
if (!currentOutput) {
currentOutputWorkspaces = allWorkspaces;
return;
}
var outputWs = allWorkspaces.filter(w => w.output === currentOutput);
currentOutputWorkspaces = outputWs;
}
function updateFocusedWindow() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
var focusedWin = windows[focusedWindowIndex];
focusedWindowTitle = focusedWin.title || "(Unnamed window)";
} else {
focusedWindowTitle = "(No active window)";
}
}
function send(request) {
if (!niriAvailable || !requestSocket.connected)
return false;
requestSocket.write(JSON.stringify(request) + "\n");
return true;
}
function switchToWorkspace(workspaceIndex) {
return send({
Action: {
FocusWorkspace: {
reference: {
Index: workspaceIndex
function switchToWorkspace(workspaceIndex) {
return send({
"Action": {
"FocusWorkspace": {
"reference": {
"Index": workspaceIndex
}
}
}
}
});
}
}
})
}
function getCurrentOutputWorkspaceNumbers() {
return currentOutputWorkspaces.map(w => w.idx + 1); // niri uses 0-based, UI shows 1-based
}
function getCurrentOutputWorkspaceNumbers() {
return currentOutputWorkspaces.map(
w => w.idx + 1) // niri uses 0-based, UI shows 1-based
}
function getCurrentWorkspaceNumber() {
if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) {
return allWorkspaces[focusedWorkspaceIndex].idx + 1;
}
return 1;
function getCurrentWorkspaceNumber() {
if (focusedWorkspaceIndex >= 0
&& focusedWorkspaceIndex < allWorkspaces.length) {
return allWorkspaces[focusedWorkspaceIndex].idx + 1
}
return 1
}
function focusWindow(windowId) {
return send({
Action: {
FocusWindow: {
id: windowId
}
}
});
}
function focusWindow(windowId) {
return send({
"Action": {
"FocusWindow": {
"id": windowId
}
}
})
}
function closeWindow(windowId) {
return send({
Action: {
CloseWindow: {
id: windowId
}
}
});
}
function closeWindow(windowId) {
return send({
"Action": {
"CloseWindow": {
"id": windowId
}
}
})
}
function quit() {
return send({
Action: {
Quit: {
skip_confirmation: true
}
}
});
}
function quit() {
return send({
"Action": {
"Quit": {
"skip_confirmation": true
}
}
})
}
function getWindowsByAppId(appId) {
if (!appId)
return [];
return windows.filter(w => w.app_id && w.app_id.toLowerCase() === appId.toLowerCase());
}
function getWindowsByAppId(appId) {
if (!appId)
return []
return windows.filter(w => w.app_id && w.app_id.toLowerCase(
) === appId.toLowerCase())
}
function getRunningAppIds() {
var appIds = new Set();
windows.forEach(w => {
if (w.app_id) {
appIds.add(w.app_id.toLowerCase());
}
});
return Array.from(appIds);
}
function getRunningAppIds() {
var appIds = new Set()
windows.forEach(w => {
if (w.app_id) {
appIds.add(w.app_id.toLowerCase())
}
})
return Array.from(appIds)
}
}

View File

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick
import Quickshell
@@ -10,430 +11,449 @@ 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)
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
readonly property list<NotifWrapper> notifications: []
readonly property list<NotifWrapper> allWrappers: []
readonly property list<NotifWrapper> popups: allWrappers.filter(n => n.popup)
Timer {
id: addGate
interval: enterAnimMs + 50
running: false
repeat: false
onTriggered: { addGateBusy = false; processQueue(); }
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()
}
// Android 16-style grouped notifications
readonly property var groupedNotifications: getGroupedNotifications()
readonly property var groupedPopups: getGroupedPopups()
property var expandedGroups: ({})
property var expandedMessages: ({})
property bool popupsDisabled: false
}
NotificationServer {
id: server
// Android 16-style grouped notifications
readonly property var groupedNotifications: getGroupedNotifications()
readonly property var groupedPopups: getGroupedPopups()
keepOnReload: false
actionsSupported: true
actionIconsSupported: true
bodyHyperlinksSupported: true
bodyImagesSupported: true
bodyMarkupSupported: true
imageSupported: true
inlineReplySupported: true
property var expandedGroups: ({})
property var expandedMessages: ({})
property bool popupsDisabled: false
onNotification: notif => {
notif.tracked = true;
NotificationServer {
id: server
const shouldShowPopup = !root.popupsDisabled && !SessionData.doNotDisturb;
const wrapper = notifComponent.createObject(root, {
popup: shouldShowPopup,
notification: notif
});
keepOnReload: false
actionsSupported: true
actionIconsSupported: true
bodyHyperlinksSupported: true
bodyImagesSupported: true
bodyMarkupSupported: true
imageSupported: true
inlineReplySupported: true
if (wrapper) {
root.allWrappers.push(wrapper);
root.notifications.push(wrapper);
if (shouldShowPopup) {
notificationQueue = [...notificationQueue, wrapper];
processQueue();
}
}
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()
}
}
}
}
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)
}
}
component NotifWrapper: QtObject {
id: wrapper
readonly property Timer timer: Timer {
interval: 5000
repeat: false
running: false
onTriggered: {
wrapper.popup = false
}
}
readonly property date time: new Date()
readonly property string timeStr: {
const now = new Date()
const diff = now.getTime() - time.getTime()
const m = Math.floor(diff / 60000)
const h = Math.floor(m / 60)
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: 5000
repeat: false
running: false
onTriggered: {
wrapper.popup = false;
}
}
readonly property date time: new Date()
readonly property string timeStr: {
const now = new Date();
const diff = now.getTime() - time.getTime();
const m = Math.floor(diff / 60000);
const h = Math.floor(m / 60);
if (h < 1 && m < 1)
return "now";
if (h < 1)
return `${m}m`;
return `${h}h`;
}
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 == "") {
// try to get the app name from the desktop entry
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
readonly property bool hasImage: image && image.length > 0
readonly property bool hasAppIcon: appIcon && appIcon.length > 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);
}
cleanupExpansionStates();
}
function onAboutToDestroy(): void {
wrapper.destroy();
}
}
if (h < 1 && m < 1)
return "now"
if (h < 1)
return `${m}m`
return `${h}h`
}
Component {
id: notifComponent
NotifWrapper {}
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 == "") {
// try to get the app name from the desktop entry
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
readonly property bool hasImage: image && image.length > 0
readonly property bool hasAppIcon: appIcon && appIcon.length > 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)
}
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) {
}
}
}
function clearAllNotifications() {
bulkDismissing = true;
popupsDisabled = true;
addGate.stop();
addGateBusy = false;
notificationQueue = [];
bulkDismissing = false
popupsDisabled = false
}
for (const w of visibleNotifications) w.popup = false;
visibleNotifications = [];
function dismissNotification(wrapper) {
if (!wrapper || !wrapper.notification)
return
wrapper.popup = false
wrapper.notification.dismiss()
}
const toDismiss = notifications.slice();
function disablePopups(disable) {
popupsDisabled = disable
if (disable) {
notificationQueue = []
visibleNotifications = []
for (const notif of root.allWrappers) {
notif.popup = false
}
}
}
if (notifications.length) notifications.splice(0, notifications.length);
expandedGroups = {};
expandedMessages = {};
function processQueue() {
if (addGateBusy)
return
if (popupsDisabled)
return
if (SessionData.doNotDisturb)
return
if (notificationQueue.length === 0)
return
for (let i = 0; i < toDismiss.length; ++i) {
const w = toDismiss[i];
if (w && w.notification) {
try { w.notification.dismiss(); } catch (e) {}
}
}
const next = notificationQueue.shift()
bulkDismissing = false;
popupsDisabled = false;
next.seq = ++seqCounter
visibleNotifications = [...visibleNotifications, next]
next.popup = true
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) {
// Remove from visible
let v = visibleNotifications.slice()
const vi = v.indexOf(w)
if (vi !== -1) {
v.splice(vi, 1)
visibleNotifications = v
}
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, ...rest] = notificationQueue;
notificationQueue = rest;
next.seq = ++seqCounter;
visibleNotifications = [...visibleNotifications, next];
next.popup = true;
addGateBusy = true;
addGate.restart();
// Remove from queue
let q = notificationQueue.slice()
const qi = q.indexOf(w)
if (qi !== -1) {
q.splice(qi, 1)
notificationQueue = q
}
function removeFromVisibleNotifications(wrapper) {
const i = visibleNotifications.findIndex(n => n === wrapper);
if (i !== -1) {
const v = [...visibleNotifications]; v.splice(i, 1);
visibleNotifications = v;
processQueue();
}
// Destroy wrapper if non-persistent
if (w && w.destroy && !w.isPersistent) {
w.destroy()
}
}
// Android 16-style notification grouping functions
function getGroupKey(wrapper) {
// Priority 1: Use desktopEntry if available
if (wrapper.desktopEntry && wrapper.desktopEntry !== "") {
return wrapper.desktopEntry.toLowerCase()
}
function releaseWrapper(w) {
// Remove from visible
let v = visibleNotifications.slice();
const vi = v.indexOf(w);
if (vi !== -1) {
v.splice(vi, 1);
visibleNotifications = v;
}
// Priority 2: Use appName as fallback
return wrapper.appName.toLowerCase()
}
// Remove from queue
let q = notificationQueue.slice();
const qi = q.indexOf(w);
if (qi !== -1) {
q.splice(qi, 1);
notificationQueue = q;
}
function getGroupedNotifications() {
const groups = {}
// Destroy wrapper if non-persistent
if (w && w.destroy && !w.isPersistent) {
w.destroy();
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(
)
})
}
// Android 16-style notification grouping functions
function getGroupKey(wrapper) {
// Priority 1: Use desktopEntry if available
if (wrapper.desktopEntry && wrapper.desktopEntry !== "") {
return wrapper.desktopEntry.toLowerCase();
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
}
// Priority 2: Use appName as fallback
return wrapper.appName.toLowerCase();
}
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
}
}
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();
});
}
return Object.values(groups).sort((a, b) => {
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 toggleGroupExpansion(groupKey) {
let newExpandedGroups = {};
for (const key in expandedGroups) {
newExpandedGroups[key] = expandedGroups[key];
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()
}
newExpandedGroups[groupKey] = !newExpandedGroups[groupKey];
expandedGroups = newExpandedGroups;
}
} else {
for (const notif of allWrappers) {
if (notif && notif.notification && getGroupKey(notif) === groupKey) {
notif.notification.dismiss()
}
}
}
}
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
}
}
function clearGroupExpansionState(groupKey) {
let newExpandedGroups = {};
for (const key in expandedGroups) {
if (key !== groupKey && expandedGroups[key]) {
newExpandedGroups[key] = true;
}
}
expandedGroups = newExpandedGroups;
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)
}
}
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;
let newExpandedGroups = {}
for (const key in expandedGroups) {
if (currentGroupKeys.has(key) && expandedGroups[key]) {
newExpandedGroups[key] = true
}
}
function toggleMessageExpansion(messageId) {
let newExpandedMessages = {};
for (const key in expandedMessages) {
newExpandedMessages[key] = expandedMessages[key];
}
newExpandedMessages[messageId] = !newExpandedMessages[messageId];
expandedMessages = newExpandedMessages;
expandedGroups = newExpandedGroups
let newExpandedMessages = {}
for (const messageId in expandedMessages) {
if (currentMessageIds.has(messageId) && expandedMessages[messageId]) {
newExpandedMessages[messageId] = true
}
}
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();
}
}
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()
}
}
}
}

View File

@@ -1,180 +1,180 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick
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
function getSystemProfileImage() {
systemProfileCheckProcess.running = true
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 setProfileImage(imagePath) {
profileImage = imagePath
if (accountsServiceAvailable && imagePath) {
setSystemProfileImage(imagePath)
}
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 getSystemColorScheme() {
systemColorSchemeCheckProcess.running = true
onExited: exitCode => {
if (exitCode !== 0) {
root.systemProfileImage = ""
}
}
function setLightMode(isLightMode) {
if (settingsPortalAvailable) {
setSystemColorScheme(isLightMode)
}
}
Process {
id: systemProfileSetProcess
running: false
onExited: exitCode => {
if (exitCode === 0) {
root.getSystemProfileImage()
}
}
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
}
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()
}
}
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: 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)
}
}
}
}
}
}
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
}
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
root.systemProfileImage = ""
}
}
onExited: exitCode => {
if (exitCode !== 0) {
root.systemColorScheme = 0
}
}
Process {
id: systemProfileSetProcess
running: false
onExited: (exitCode) => {
if (exitCode === 0) {
root.getSystemProfileImage()
}
}
}
Process {
id: systemColorSchemeSetProcess
running: false
onExited: exitCode => {
if (exitCode === 0) {
Qt.callLater(() => {
root.getSystemColorScheme()
})
}
}
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

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick
import Quickshell
@@ -7,119 +8,136 @@ import Quickshell.Io
import Quickshell.Services.Pipewire
Singleton {
id: root
id: root
readonly property bool microphoneActive: {
if (!Pipewire.ready || !Pipewire.nodes?.values) return false
for (let 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
}
}
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
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
}
}
return false
}
PwObjectTracker {
objects: Pipewire.nodes.values
}
PwObjectTracker {
objects: Pipewire.nodes.values
}
readonly property bool cameraActive: {
if (!Pipewire.ready || !Pipewire.nodes?.values)
return false
readonly property bool cameraActive: {
if (!Pipewire.ready || !Pipewire.nodes?.values) return false
for (let 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
}
}
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
}
}
return false
}
readonly property bool screensharingActive: {
if (!Pipewire.ready || !Pipewire.nodes?.values) return false
for (let 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
}
return true
}
}
}
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
}
return false
}
readonly property bool anyPrivacyActive: microphoneActive || cameraActive || screensharingActive
}
if (node.properties
&& node.properties["media.class"] === "Stream/Input/Audio") {
const mediaName = (node.properties["media.name"] || "").toLowerCase()
const appName = (node.properties["application.name"]
|| "").toLowerCase()
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)
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
}
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)
}
readonly property bool anyPrivacyActive: microphoneActive || cameraActive
|| screensharingActive
function getMicrophoneStatus() {
return microphoneActive ? "active" : "inactive";
}
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 getCameraStatus() {
return cameraActive ? "active" : "inactive";
}
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 getScreensharingStatus() {
return screensharingActive ? "active" : "inactive";
}
function getMicrophoneStatus() {
return microphoneActive ? "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";
}
}
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"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,77 +1,75 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
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 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 wallpaperErrorStatus: ""
function showToast(message, level = levelInfo) {
toastQueue.push({
"message": message,
"level": level
});
if (!toastVisible)
processQueue();
function showToast(message, level = levelInfo) {
toastQueue.push({
"message": message,
"level": level
})
if (!toastVisible)
processQueue()
}
}
function showInfo(message) {
showToast(message, levelInfo)
}
function showInfo(message) {
showToast(message, levelInfo);
}
function showWarning(message) {
showToast(message, levelWarn)
}
function showWarning(message) {
showToast(message, levelWarn);
}
function showError(message) {
showToast(message, levelError)
}
function showError(message) {
showToast(message, levelError);
}
function hideToast() {
toastVisible = false
currentMessage = ""
currentLevel = levelInfo
toastTimer.stop()
if (toastQueue.length > 0)
processQueue()
}
function hideToast() {
toastVisible = false;
currentMessage = "";
currentLevel = levelInfo;
toastTimer.stop();
if (toastQueue.length > 0)
processQueue();
function processQueue() {
if (toastQueue.length === 0)
return
}
const toast = toastQueue.shift()
currentMessage = toast.message
currentLevel = toast.level
toastVisible = true
toastTimer.interval = toast.level
=== levelError ? 5000 : toast.level === levelWarn ? 4000 : 3000
toastTimer.start()
}
function processQueue() {
if (toastQueue.length === 0)
return ;
const toast = toastQueue.shift();
currentMessage = toast.message;
currentLevel = toast.level;
toastVisible = true;
toastTimer.interval = toast.level === levelError ? 5000 : toast.level === levelWarn ? 4000 : 3000;
toastTimer.start();
}
function clearWallpaperError() {
wallpaperErrorStatus = "";
}
Timer {
id: toastTimer
interval: 5000
running: false
repeat: false
onTriggered: hideToast()
}
function clearWallpaperError() {
wallpaperErrorStatus = ""
}
Timer {
id: toastTimer
interval: 5000
running: false
repeat: false
onTriggered: hideToast()
}
}

View File

@@ -1,100 +1,99 @@
pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick
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 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 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";
}
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 parts = text.trim().split("|");
if (parts.length >= 3) {
root.username = parts[0] || "";
root.fullName = parts[1] || parts[0] || "";
root.hostname = parts[2] || "";
// Get system uptime
Process {
id: uptimeProcess
}
}
}
command: ["cat", "/proc/uptime"]
running: false
onExited: exitCode => {
if (exitCode !== 0) {
root.uptime = "Unknown"
}
}
// Get system uptime
Process {
id: uptimeProcess
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)
command: ["cat", "/proc/uptime"]
running: false
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"}`)
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`;
}
}
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
@@ -8,334 +9,347 @@ import qs.Common
import qs.Services
Singleton {
id: root
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 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"
})
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
}
const location = SettingsData.weatherCoordinates || "40.7128,-74.0060"
const url = `wttr.in/${encodeURIComponent(location)}?format=j1`
console.log("Using manual location:", location, "URL:", url)
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()
}
id: root
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 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"
})
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
}
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
}
const location = SettingsData.weatherCoordinates || "40.7128,-74.0060"
const url = `wttr.in/${encodeURIComponent(location)}?format=j1`
console.log("Using manual location:", location, "URL:", url)
return url
}
try {
const data = JSON.parse(raw)
function addRef() {
refCount++
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()
}
}
if (refCount === 1 && !weather.available && SettingsData.weatherEnabled) {
// Start fetching when first consumer appears and weather is enabled
fetchWeather()
}
Timer {
id: updateTimer
interval: root.updateInterval
running: root.refCount > 0 && SettingsData.weatherEnabled
repeat: true
triggeredOnStart: true
onTriggered: {
root.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
}
}
Timer {
id: retryTimer
interval: root.retryDelay
running: false
repeat: false
onTriggered: {
root.fetchWeather()
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()
}
}
}
Timer {
id: persistentRetryTimer
interval: 60000 // Will be dynamically set
running: false
repeat: false
onTriggered: {
console.log("Persistent retry attempt...")
root.fetchWeather()
}
onExited: exitCode => {
if (exitCode !== 0) {
console.warn("Weather fetch failed with exit code:", exitCode)
root.handleWeatherFailure()
}
}
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
}
}
})
}
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
}
}
})
}
}