greeter test
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import qs.Config
|
||||
import qs.Components
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool appleDisplayPresent: false
|
||||
property list<var> ddcMonitors: []
|
||||
property list<var> ddcServiceMon: []
|
||||
readonly property list<Monitor> monitors: variants.instances
|
||||
|
||||
function decreaseBrightness(): void {
|
||||
const monitor = getMonitor("active");
|
||||
if (monitor)
|
||||
monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement);
|
||||
}
|
||||
|
||||
function getMonitor(query: string): var {
|
||||
if (query === "active") {
|
||||
return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused);
|
||||
}
|
||||
|
||||
if (query.startsWith("model:")) {
|
||||
const model = query.slice(6);
|
||||
return monitors.find(m => m.modelData.model === model);
|
||||
}
|
||||
|
||||
if (query.startsWith("serial:")) {
|
||||
const serial = query.slice(7);
|
||||
return monitors.find(m => m.modelData.serialNumber === serial);
|
||||
}
|
||||
|
||||
if (query.startsWith("id:")) {
|
||||
const id = parseInt(query.slice(3), 10);
|
||||
return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id);
|
||||
}
|
||||
|
||||
return monitors.find(m => m.modelData.name === query);
|
||||
}
|
||||
|
||||
function getMonitorForScreen(screen: ShellScreen): var {
|
||||
return monitors.find(m => m.modelData === screen);
|
||||
}
|
||||
|
||||
function increaseBrightness(): void {
|
||||
const monitor = getMonitor("active");
|
||||
if (monitor)
|
||||
monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement);
|
||||
}
|
||||
|
||||
onMonitorsChanged: {
|
||||
ddcMonitors = [];
|
||||
ddcServiceMon = [];
|
||||
ddcServiceProc.running = true;
|
||||
ddcProc.running = true;
|
||||
}
|
||||
|
||||
Variants {
|
||||
id: variants
|
||||
|
||||
model: Quickshell.screens
|
||||
|
||||
Monitor {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
command: ["sh", "-c", "asdbctl get"]
|
||||
running: true
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.appleDisplayPresent = text.trim().length > 0
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ddcProc
|
||||
|
||||
command: ["ddcutil", "detect", "--brief"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.ddcMonitors = text.trim().split("\n\n").filter(d => d.startsWith("Display ")).map(d => ({
|
||||
busNum: d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)[1],
|
||||
connector: d.match(/DRM connector:\s+(.*)/)[1].replace(/^card\d+-/, "") // strip "card1-"
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ddcServiceProc
|
||||
|
||||
command: ["ddcutil-client", "detect"]
|
||||
|
||||
// running: true
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const t = text.replace(/\r\n/g, "\n").trim();
|
||||
|
||||
const output = ("\n" + t).split(/\n(?=display:\s*\d+\s*\n)/).filter(b => b.startsWith("display:")).map(b => ({
|
||||
display: Number(b.match(/^display:\s*(\d+)/m)?.[1] ?? -1),
|
||||
name: (b.match(/^\s*product_name:\s*(.*)$/m)?.[1] ?? "").trim()
|
||||
})).filter(d => d.display > 0);
|
||||
root.ddcServiceMon = output;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
description: "Increase brightness"
|
||||
name: "brightnessUp"
|
||||
|
||||
onPressed: root.increaseBrightness()
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
description: "Decrease brightness"
|
||||
name: "brightnessDown"
|
||||
|
||||
onPressed: root.decreaseBrightness()
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function get(): real {
|
||||
return getFor("active");
|
||||
}
|
||||
|
||||
// Allows searching by active/model/serial/id/name
|
||||
function getFor(query: string): real {
|
||||
return root.getMonitor(query)?.brightness ?? -1;
|
||||
}
|
||||
|
||||
function set(value: string): string {
|
||||
return setFor("active", value);
|
||||
}
|
||||
|
||||
// Handles brightness value like brightnessctl: 0.1, +0.1, 0.1-, 10%, +10%, 10%-
|
||||
function setFor(query: string, value: string): string {
|
||||
const monitor = root.getMonitor(query);
|
||||
if (!monitor)
|
||||
return "Invalid monitor: " + query;
|
||||
|
||||
let targetBrightness;
|
||||
if (value.endsWith("%-")) {
|
||||
const percent = parseFloat(value.slice(0, -2));
|
||||
targetBrightness = monitor.brightness - (percent / 100);
|
||||
} else if (value.startsWith("+") && value.endsWith("%")) {
|
||||
const percent = parseFloat(value.slice(1, -1));
|
||||
targetBrightness = monitor.brightness + (percent / 100);
|
||||
} else if (value.endsWith("%")) {
|
||||
const percent = parseFloat(value.slice(0, -1));
|
||||
targetBrightness = percent / 100;
|
||||
} else if (value.startsWith("+")) {
|
||||
const increment = parseFloat(value.slice(1));
|
||||
targetBrightness = monitor.brightness + increment;
|
||||
} else if (value.endsWith("-")) {
|
||||
const decrement = parseFloat(value.slice(0, -1));
|
||||
targetBrightness = monitor.brightness - decrement;
|
||||
} else if (value.includes("%") || value.includes("-") || value.includes("+")) {
|
||||
return `Invalid brightness format: ${value}\nExpected: 0.1, +0.1, 0.1-, 10%, +10%, 10%-`;
|
||||
} else {
|
||||
targetBrightness = parseFloat(value);
|
||||
}
|
||||
|
||||
if (isNaN(targetBrightness))
|
||||
return `Failed to parse value: ${value}\nExpected: 0.1, +0.1, 0.1-, 10%, +10%, 10%-`;
|
||||
|
||||
monitor.setBrightness(targetBrightness);
|
||||
|
||||
return `Set monitor ${monitor.modelData.name} brightness to ${+monitor.brightness.toFixed(2)}`;
|
||||
}
|
||||
|
||||
target: "brightness"
|
||||
}
|
||||
|
||||
component Monitor: QtObject {
|
||||
id: monitor
|
||||
|
||||
property real brightness
|
||||
readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? ""
|
||||
readonly property string displayNum: root.ddcServiceMon.find(m => m.name === modelData.model)?.display ?? ""
|
||||
readonly property Process initProc: Process {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (monitor.isDdcService) {
|
||||
const output = text.split("\n").filter(o => o.startsWith("vcp_current_value:"))[0].split(":")[1];
|
||||
const val = parseInt(output.trim());
|
||||
monitor.brightness = val / 100;
|
||||
} else if (monitor.isAppleDisplay) {
|
||||
const val = parseInt(text.trim());
|
||||
monitor.brightness = val / 101;
|
||||
} else {
|
||||
const [, , , cur, max] = text.split(" ");
|
||||
monitor.brightness = parseInt(cur) / parseInt(max);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
|
||||
readonly property bool isDdc: root.ddcMonitors.some(m => m.connector === modelData.name)
|
||||
readonly property bool isDdcService: Config.services.ddcutilService
|
||||
required property ShellScreen modelData
|
||||
property real queuedBrightness: NaN
|
||||
readonly property Timer timer: Timer {
|
||||
interval: 500
|
||||
|
||||
onTriggered: {
|
||||
if (!isNaN(monitor.queuedBrightness)) {
|
||||
monitor.setBrightness(monitor.queuedBrightness);
|
||||
monitor.queuedBrightness = NaN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initBrightness(): void {
|
||||
if (isDdcService)
|
||||
initProc.command = ["ddcutil-client", "-d", displayNum, "getvcp", "10"];
|
||||
else if (isAppleDisplay)
|
||||
initProc.command = ["asdbctl", "get"];
|
||||
else if (isDdc)
|
||||
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"];
|
||||
else
|
||||
initProc.command = ["sh", "-c", "echo a b c $(brightnessctl g) $(brightnessctl m)"];
|
||||
|
||||
initProc.running = true;
|
||||
}
|
||||
|
||||
function setBrightness(value: real): void {
|
||||
value = Math.max(0, Math.min(1, value));
|
||||
const rounded = Math.round(value * 100);
|
||||
if (Math.round(brightness * 100) === rounded)
|
||||
return;
|
||||
|
||||
if ((isDdc || isDdcService) && timer.running) {
|
||||
queuedBrightness = value;
|
||||
return;
|
||||
}
|
||||
|
||||
brightness = value;
|
||||
|
||||
if (isDdcService)
|
||||
Quickshell.execDetached(["ddcutil-client", "-d", displayNum, "setvcp", "10", rounded]);
|
||||
else if (isAppleDisplay)
|
||||
Quickshell.execDetached(["asdbctl", "set", rounded]);
|
||||
else if (isDdc)
|
||||
Quickshell.execDetached(["ddcutil", "--disable-dynamic-sleep", "--sleep-multiplier", ".1", "--skip-ddc-checks", "-b", busNum, "setvcp", "10", rounded]);
|
||||
else
|
||||
Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]);
|
||||
|
||||
if (isDdc || isDdcService)
|
||||
timer.restart();
|
||||
}
|
||||
|
||||
Component.onCompleted: initBrightness()
|
||||
onBusNumChanged: initBrightness()
|
||||
onDisplayNumChanged: initBrightness()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import ZShell.Internal
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import qs.Paths
|
||||
|
||||
Image {
|
||||
id: root
|
||||
|
||||
property alias path: manager.path
|
||||
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
|
||||
Connections {
|
||||
function onDevicePixelRatioChanged(): void {
|
||||
manager.updateSource();
|
||||
}
|
||||
|
||||
target: QsWindow.window
|
||||
}
|
||||
|
||||
CachingImageManager {
|
||||
id: manager
|
||||
|
||||
cacheDir: Qt.resolvedUrl(Paths.imagecache)
|
||||
item: root
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function getTrayIcon(id: string, icon: string): string {
|
||||
if (icon.includes("?path=")) {
|
||||
const [name, path] = icon.split("?path=");
|
||||
icon = Qt.resolvedUrl(`${path}/${name.slice(name.lastIndexOf("/") + 1)}`);
|
||||
} else if (icon.includes("qspixmap") && id === "chrome_status_icon_1") {
|
||||
icon = icon.replace("qspixmap", "icon/discord-tray");
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
pragma Singleton
|
||||
|
||||
import qs.Config
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var categoryIcons: ({
|
||||
WebBrowser: "web",
|
||||
Printing: "print",
|
||||
Security: "security",
|
||||
Network: "chat",
|
||||
Archiving: "archive",
|
||||
Compression: "archive",
|
||||
Development: "code",
|
||||
IDE: "code",
|
||||
TextEditor: "edit_note",
|
||||
Audio: "music_note",
|
||||
Music: "music_note",
|
||||
Player: "music_note",
|
||||
Recorder: "mic",
|
||||
Game: "sports_esports",
|
||||
FileTools: "files",
|
||||
FileManager: "files",
|
||||
Filesystem: "files",
|
||||
FileTransfer: "files",
|
||||
Settings: "settings",
|
||||
DesktopSettings: "settings",
|
||||
HardwareSettings: "settings",
|
||||
TerminalEmulator: "terminal",
|
||||
ConsoleOnly: "terminal",
|
||||
Utility: "build",
|
||||
Monitor: "monitor_heart",
|
||||
Midi: "graphic_eq",
|
||||
Mixer: "graphic_eq",
|
||||
AudioVideoEditing: "video_settings",
|
||||
AudioVideo: "music_video",
|
||||
Video: "videocam",
|
||||
Building: "construction",
|
||||
Graphics: "photo_library",
|
||||
"2DGraphics": "photo_library",
|
||||
RasterGraphics: "photo_library",
|
||||
TV: "tv",
|
||||
System: "host",
|
||||
Office: "content_paste"
|
||||
})
|
||||
readonly property var weatherIcons: ({
|
||||
"0": "clear_day",
|
||||
"1": "clear_day",
|
||||
"2": "partly_cloudy_day",
|
||||
"3": "cloud",
|
||||
"45": "foggy",
|
||||
"48": "foggy",
|
||||
"51": "rainy",
|
||||
"53": "rainy",
|
||||
"55": "rainy",
|
||||
"56": "rainy",
|
||||
"57": "rainy",
|
||||
"61": "rainy",
|
||||
"63": "rainy",
|
||||
"65": "rainy",
|
||||
"66": "rainy",
|
||||
"67": "rainy",
|
||||
"71": "cloudy_snowing",
|
||||
"73": "cloudy_snowing",
|
||||
"75": "snowing_heavy",
|
||||
"77": "cloudy_snowing",
|
||||
"80": "rainy",
|
||||
"81": "rainy",
|
||||
"82": "rainy",
|
||||
"85": "cloudy_snowing",
|
||||
"86": "snowing_heavy",
|
||||
"95": "thunderstorm",
|
||||
"96": "thunderstorm",
|
||||
"99": "thunderstorm"
|
||||
})
|
||||
|
||||
function getAppCategoryIcon(name: string, fallback: string): string {
|
||||
const categories = DesktopEntries.heuristicLookup(name)?.categories;
|
||||
|
||||
if (categories)
|
||||
for (const [key, value] of Object.entries(categoryIcons))
|
||||
if (categories.includes(key))
|
||||
return value;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function getAppIcon(name: string, fallback: string): string {
|
||||
const icon = DesktopEntries.heuristicLookup(name)?.icon;
|
||||
if (fallback !== "undefined")
|
||||
return Quickshell.iconPath(icon, fallback);
|
||||
return Quickshell.iconPath(icon);
|
||||
}
|
||||
|
||||
function getBluetoothIcon(icon: string): string {
|
||||
if (icon.includes("headset") || icon.includes("headphones"))
|
||||
return "headphones";
|
||||
if (icon.includes("audio"))
|
||||
return "speaker";
|
||||
if (icon.includes("phone"))
|
||||
return "smartphone";
|
||||
if (icon.includes("mouse"))
|
||||
return "mouse";
|
||||
if (icon.includes("keyboard"))
|
||||
return "keyboard";
|
||||
return "bluetooth";
|
||||
}
|
||||
|
||||
function getMicVolumeIcon(volume: real, isMuted: bool): string {
|
||||
if (!isMuted && volume > 0)
|
||||
return "mic";
|
||||
return "mic_off";
|
||||
}
|
||||
|
||||
function getNetworkIcon(strength: int, isSecure = false): string {
|
||||
if (isSecure) {
|
||||
if (strength >= 80)
|
||||
return "network_wifi_locked";
|
||||
if (strength >= 60)
|
||||
return "network_wifi_3_bar_locked";
|
||||
if (strength >= 40)
|
||||
return "network_wifi_2_bar_locked";
|
||||
if (strength >= 20)
|
||||
return "network_wifi_1_bar_locked";
|
||||
return "signal_wifi_0_bar";
|
||||
} else {
|
||||
if (strength >= 80)
|
||||
return "network_wifi";
|
||||
if (strength >= 60)
|
||||
return "network_wifi_3_bar";
|
||||
if (strength >= 40)
|
||||
return "network_wifi_2_bar";
|
||||
if (strength >= 20)
|
||||
return "network_wifi_1_bar";
|
||||
return "signal_wifi_0_bar";
|
||||
}
|
||||
}
|
||||
|
||||
function getNotifIcon(summary: string, urgency: int): string {
|
||||
summary = summary.toLowerCase();
|
||||
if (summary.includes("reboot"))
|
||||
return "restart_alt";
|
||||
if (summary.includes("recording"))
|
||||
return "screen_record";
|
||||
if (summary.includes("battery"))
|
||||
return "power";
|
||||
if (summary.includes("screenshot"))
|
||||
return "screenshot_monitor";
|
||||
if (summary.includes("welcome"))
|
||||
return "waving_hand";
|
||||
if (summary.includes("time") || summary.includes("a break"))
|
||||
return "schedule";
|
||||
if (summary.includes("installed"))
|
||||
return "download";
|
||||
if (summary.includes("update"))
|
||||
return "update";
|
||||
if (summary.includes("unable to"))
|
||||
return "deployed_code_alert";
|
||||
if (summary.includes("profile"))
|
||||
return "person";
|
||||
if (summary.includes("file"))
|
||||
return "folder_copy";
|
||||
if (urgency === NotificationUrgency.Critical)
|
||||
return "release_alert";
|
||||
return "chat";
|
||||
}
|
||||
|
||||
function getVolumeIcon(volume: real, isMuted: bool): string {
|
||||
if (isMuted)
|
||||
return "no_sound";
|
||||
if (volume >= 0.5)
|
||||
return "volume_up";
|
||||
if (volume > 0)
|
||||
return "volume_down";
|
||||
return "volume_mute";
|
||||
}
|
||||
|
||||
function getWeatherIcon(code: string): string {
|
||||
if (weatherIcons.hasOwnProperty(code))
|
||||
return weatherIcons[code];
|
||||
return "air";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property Item activeMenu: null
|
||||
property Item activeTrigger: null
|
||||
|
||||
function close(menu) {
|
||||
if (!menu)
|
||||
return;
|
||||
|
||||
if (activeMenu === menu) {
|
||||
activeMenu = null;
|
||||
activeTrigger = null;
|
||||
}
|
||||
|
||||
menu.expanded = false;
|
||||
}
|
||||
|
||||
function closeActive() {
|
||||
if (activeMenu)
|
||||
activeMenu.expanded = false;
|
||||
|
||||
activeMenu = null;
|
||||
activeTrigger = null;
|
||||
}
|
||||
|
||||
function forget(menu) {
|
||||
if (activeMenu === menu) {
|
||||
activeMenu = null;
|
||||
activeTrigger = null;
|
||||
}
|
||||
}
|
||||
|
||||
function hit(item, scenePos) {
|
||||
if (!item || !item.visible)
|
||||
return false;
|
||||
|
||||
const p = item.mapFromItem(null, scenePos.x, scenePos.y);
|
||||
return item.contains(p);
|
||||
}
|
||||
|
||||
function open(menu, trigger) {
|
||||
if (activeMenu && activeMenu !== menu)
|
||||
activeMenu.expanded = false;
|
||||
|
||||
activeMenu = menu;
|
||||
activeTrigger = trigger || null;
|
||||
menu.expanded = true;
|
||||
}
|
||||
|
||||
function toggle(menu, trigger) {
|
||||
if (activeMenu === menu && menu.expanded)
|
||||
close(menu);
|
||||
else
|
||||
open(menu, trigger);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
pragma Singleton
|
||||
|
||||
import qs.Config
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool isDefaultLogo: true
|
||||
property string osId
|
||||
property list<string> osIdLike
|
||||
property string osLogo
|
||||
property string osName
|
||||
property string osPrettyName
|
||||
readonly property string shell: Quickshell.env("SHELL").split("/").pop()
|
||||
property string uptime
|
||||
readonly property string user: Quickshell.env("USER")
|
||||
readonly property string wm: Quickshell.env("XDG_CURRENT_DESKTOP") || Quickshell.env("XDG_SESSION_DESKTOP")
|
||||
|
||||
FileView {
|
||||
id: osRelease
|
||||
|
||||
path: "/etc/os-release"
|
||||
|
||||
onLoaded: {
|
||||
const lines = text().split("\n");
|
||||
|
||||
const fd = key => lines.find(l => l.startsWith(`${key}=`))?.split("=")[1].replace(/"/g, "") ?? "";
|
||||
|
||||
root.osName = fd("NAME");
|
||||
root.osPrettyName = fd("PRETTY_NAME");
|
||||
root.osId = fd("ID");
|
||||
root.osIdLike = fd("ID_LIKE").split(" ");
|
||||
|
||||
const logo = Quickshell.iconPath(fd("LOGO"), true);
|
||||
if (Config.general.logo) {
|
||||
root.osLogo = Quickshell.iconPath(Config.general.logo, true) || "file://" + Paths.absolutePath(Config.general.logo);
|
||||
root.isDefaultLogo = false;
|
||||
} else if (logo) {
|
||||
root.osLogo = logo;
|
||||
root.isDefaultLogo = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onLogoChanged(): void {
|
||||
osRelease.reload();
|
||||
}
|
||||
|
||||
target: Config.general
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 15000
|
||||
repeat: true
|
||||
running: true
|
||||
|
||||
onTriggered: fileUptime.reload()
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: fileUptime
|
||||
|
||||
path: "/proc/uptime"
|
||||
|
||||
onLoaded: {
|
||||
const up = parseInt(text().split(" ")[0] ?? 0);
|
||||
|
||||
const hours = Math.floor(up / 3600);
|
||||
const minutes = Math.floor((up % 3600) / 60);
|
||||
|
||||
let str = "";
|
||||
if (hours > 0)
|
||||
str += `${hours} hour${hours === 1 ? "" : "s"}`;
|
||||
if (minutes > 0 || !str)
|
||||
str += `${str ? ", " : ""}${minutes} minute${minutes === 1 ? "" : "s"}`;
|
||||
root.uptime = str;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import qs.Config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string autoGpuType: "NONE"
|
||||
property string cpuName: ""
|
||||
property real cpuPerc
|
||||
property real cpuTemp
|
||||
|
||||
// Individual disks: Array of { mount, used, total, free, perc }
|
||||
property var disks: []
|
||||
property real gpuMemTotal: 0
|
||||
property real gpuMemUsed
|
||||
property real gpuPerc
|
||||
property real gpuTemp
|
||||
readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType
|
||||
property real lastCpuIdle
|
||||
property real lastCpuTotal
|
||||
readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0
|
||||
property real memTotal
|
||||
property real memUsed
|
||||
property int refCount
|
||||
readonly property real storagePerc: {
|
||||
let totalUsed = 0;
|
||||
let totalSize = 0;
|
||||
for (const disk of disks) {
|
||||
totalUsed += disk.used;
|
||||
totalSize += disk.total;
|
||||
}
|
||||
return totalSize > 0 ? totalUsed / totalSize : 0;
|
||||
}
|
||||
|
||||
function cleanCpuName(name: string): string {
|
||||
return name.replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/CPU/gi, "").replace(/\d+th Gen /gi, "").replace(/\d+nd Gen /gi, "").replace(/\d+rd Gen /gi, "").replace(/\d+st Gen /gi, "").replace(/Core /gi, "").replace(/Processor/gi, "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function cleanGpuName(name: string): string {
|
||||
return name.replace(/NVIDIA GeForce /gi, "").replace(/NVIDIA /gi, "").replace(/AMD Radeon /gi, "").replace(/AMD /gi, "").replace(/Intel /gi, "").replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/Graphics/gi, "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function formatKib(kib: real): var {
|
||||
const mib = 1024;
|
||||
const gib = 1024 ** 2;
|
||||
const tib = 1024 ** 3;
|
||||
|
||||
if (kib >= tib)
|
||||
return {
|
||||
value: kib / tib,
|
||||
unit: "TiB"
|
||||
};
|
||||
if (kib >= gib)
|
||||
return {
|
||||
value: kib / gib,
|
||||
unit: "GiB"
|
||||
};
|
||||
if (kib >= mib)
|
||||
return {
|
||||
value: kib / mib,
|
||||
unit: "MiB"
|
||||
};
|
||||
return {
|
||||
value: kib,
|
||||
unit: "KiB"
|
||||
};
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: Config.dashboard.resourceUpdateInterval
|
||||
repeat: true
|
||||
running: root.refCount > 0
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
stat.reload();
|
||||
meminfo.reload();
|
||||
if (root.gpuType === "GENERIC")
|
||||
gpuUsage.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 60000 * 120
|
||||
repeat: true
|
||||
running: true
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
storage.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: Config.dashboard.resourceUpdateInterval * 5
|
||||
repeat: true
|
||||
running: root.refCount > 0
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
sensors.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: cpuinfoInit
|
||||
|
||||
path: "/proc/cpuinfo"
|
||||
|
||||
onLoaded: {
|
||||
const nameMatch = text().match(/model name\s*:\s*(.+)/);
|
||||
if (nameMatch)
|
||||
root.cpuName = root.cleanCpuName(nameMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: stat
|
||||
|
||||
path: "/proc/stat"
|
||||
|
||||
onLoaded: {
|
||||
const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/);
|
||||
if (data) {
|
||||
const stats = data.slice(1).map(n => parseInt(n, 10));
|
||||
const total = stats.reduce((a, b) => a + b, 0);
|
||||
const idle = stats[3] + (stats[4] ?? 0);
|
||||
|
||||
const totalDiff = total - root.lastCpuTotal;
|
||||
const idleDiff = idle - root.lastCpuIdle;
|
||||
const newCpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0;
|
||||
|
||||
root.lastCpuTotal = total;
|
||||
root.lastCpuIdle = idle;
|
||||
|
||||
if (Math.abs(newCpuPerc - root.cpuPerc) >= 0.01)
|
||||
root.cpuPerc = newCpuPerc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: meminfo
|
||||
|
||||
path: "/proc/meminfo"
|
||||
|
||||
onLoaded: {
|
||||
const data = text();
|
||||
const total = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1;
|
||||
const used = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0;
|
||||
|
||||
if (root.memTotal !== total)
|
||||
root.memTotal = total;
|
||||
|
||||
if (Math.abs(used - root.memUsed) >= 16384)
|
||||
root.memUsed = used;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: storage
|
||||
|
||||
command: ["lsblk", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE", "-P"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const diskMap = {}; // Map disk name -> { name, totalSize, used, fsTotal }
|
||||
const lines = text.trim().split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim() === "")
|
||||
continue;
|
||||
const nameMatch = line.match(/NAME="([^"]+)"/);
|
||||
const sizeMatch = line.match(/SIZE="([^"]+)"/);
|
||||
const typeMatch = line.match(/TYPE="([^"]+)"/);
|
||||
const fsusedMatch = line.match(/FSUSED="([^"]*)"/);
|
||||
const fssizeMatch = line.match(/FSSIZE="([^"]*)"/);
|
||||
|
||||
if (!nameMatch || !typeMatch)
|
||||
continue;
|
||||
|
||||
const name = nameMatch[1];
|
||||
const type = typeMatch[1];
|
||||
const size = parseInt(sizeMatch?.[1] || "0", 10);
|
||||
const fsused = parseInt(fsusedMatch?.[1] || "0", 10);
|
||||
const fssize = parseInt(fssizeMatch?.[1] || "0", 10);
|
||||
|
||||
if (type === "disk") {
|
||||
// Skip zram (swap) devices
|
||||
if (name.startsWith("zram"))
|
||||
continue;
|
||||
|
||||
// Initialize disk entry
|
||||
if (!diskMap[name]) {
|
||||
diskMap[name] = {
|
||||
name: name,
|
||||
totalSize: size,
|
||||
used: 0,
|
||||
fsTotal: 0
|
||||
};
|
||||
}
|
||||
} else if (type === "part") {
|
||||
// Find parent disk (remove trailing numbers/p+numbers)
|
||||
let parentDisk = name.replace(/p?\d+$/, "");
|
||||
// For nvme devices like nvme0n1p1, parent is nvme0n1
|
||||
if (name.match(/nvme\d+n\d+p\d+/))
|
||||
parentDisk = name.replace(/p\d+$/, "");
|
||||
|
||||
// Aggregate partition usage to parent disk
|
||||
if (diskMap[parentDisk]) {
|
||||
diskMap[parentDisk].used += fsused;
|
||||
diskMap[parentDisk].fsTotal += fssize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const diskList = [];
|
||||
let totalUsed = 0;
|
||||
let totalSize = 0;
|
||||
|
||||
for (const diskName of Object.keys(diskMap).sort()) {
|
||||
const disk = diskMap[diskName];
|
||||
// Use filesystem total if available, otherwise use disk size
|
||||
const total = disk.fsTotal > 0 ? disk.fsTotal : disk.totalSize;
|
||||
const used = disk.used;
|
||||
const perc = total > 0 ? used / total : 0;
|
||||
|
||||
// Convert bytes to KiB for consistency with formatKib
|
||||
diskList.push({
|
||||
mount: disk.name // Using 'mount' property for compatibility
|
||||
,
|
||||
used: used / 1024,
|
||||
total: total / 1024,
|
||||
free: (total - used) / 1024,
|
||||
perc: perc
|
||||
});
|
||||
|
||||
totalUsed += used;
|
||||
totalSize += total;
|
||||
}
|
||||
|
||||
root.disks = diskList;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuNameDetect
|
||||
|
||||
command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || lspci 2>/dev/null | grep -i 'vga\\|3d\\|display' | head -1"]
|
||||
running: true
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const output = text.trim();
|
||||
if (!output)
|
||||
return;
|
||||
|
||||
// Check if it's from nvidia-smi (clean GPU name)
|
||||
if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) {
|
||||
root.gpuName = root.cleanGpuName(output);
|
||||
} else {
|
||||
// Parse lspci output: extract name from brackets or after colon
|
||||
const bracketMatch = output.match(/\[([^\]]+)\]/);
|
||||
if (bracketMatch) {
|
||||
root.gpuName = root.cleanGpuName(bracketMatch[1]);
|
||||
} else {
|
||||
const colonMatch = output.match(/:\s*(.+)/);
|
||||
if (colonMatch)
|
||||
root.gpuName = root.cleanGpuName(colonMatch[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuTypeCheck
|
||||
|
||||
command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"]
|
||||
running: !Config.services.gpuType
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.autoGpuType = text.trim()
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: oneshotMem
|
||||
|
||||
command: ["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"]
|
||||
running: root.gpuType === "NVIDIA" && root.gpuMemTotal === 0
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.gpuMemTotal = Number(this.text.trim());
|
||||
oneshotMem.running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuUsageNvidia
|
||||
|
||||
command: ["/usr/bin/nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits", "-lms", "1000"]
|
||||
running: root.refCount > 0 && root.gpuType === "NVIDIA"
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
const parts = String(data).trim().split(/\s*,\s*/);
|
||||
if (parts.length < 3)
|
||||
return;
|
||||
|
||||
const usageRaw = parseInt(parts[0], 10);
|
||||
const tempRaw = parseInt(parts[1], 10);
|
||||
const memRaw = parseInt(parts[2], 10);
|
||||
|
||||
if (!Number.isFinite(usageRaw) || !Number.isFinite(tempRaw) || !Number.isFinite(memRaw))
|
||||
return;
|
||||
|
||||
const newGpuPerc = Math.max(0, Math.min(1, usageRaw / 100));
|
||||
const newGpuTemp = tempRaw;
|
||||
const newGpuMemUsed = root.gpuMemTotal > 0 ? Math.max(0, Math.min(1, memRaw / root.gpuMemTotal)) : 0;
|
||||
|
||||
// Only publish meaningful changes to avoid needless binding churn / repaints
|
||||
if (Math.abs(root.gpuPerc - newGpuPerc) >= 0.01)
|
||||
root.gpuPerc = newGpuPerc;
|
||||
|
||||
if (Math.abs(root.gpuTemp - newGpuTemp) >= 1)
|
||||
root.gpuTemp = newGpuTemp;
|
||||
|
||||
if (Math.abs(root.gpuMemUsed - newGpuMemUsed) >= 0.01)
|
||||
root.gpuMemUsed = newGpuMemUsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuUsage
|
||||
|
||||
command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : ["echo"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
console.log("this is running");
|
||||
if (root.gpuType === "GENERIC") {
|
||||
const percs = text.trim().split("\n");
|
||||
const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0);
|
||||
root.gpuPerc = sum / percs.length / 100;
|
||||
} else {
|
||||
root.gpuPerc = 0;
|
||||
root.gpuTemp = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: sensors
|
||||
|
||||
command: ["sensors"]
|
||||
environment: ({
|
||||
LANG: "C.UTF-8",
|
||||
LC_ALL: "C.UTF-8"
|
||||
})
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/);
|
||||
if (!cpuTemp)
|
||||
// If AMD Tdie pattern failed, try fallback on Tctl
|
||||
cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/);
|
||||
|
||||
if (cpuTemp && Math.abs(parseFloat(cpuTemp[1]) - root.cpuTemp) >= 0.5)
|
||||
root.cpuTemp = parseFloat(cpuTemp[1]);
|
||||
|
||||
if (root.gpuType !== "GENERIC")
|
||||
return;
|
||||
|
||||
let eligible = false;
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
|
||||
for (const line of text.trim().split("\n")) {
|
||||
if (line === "Adapter: PCI adapter")
|
||||
eligible = true;
|
||||
else if (line === "")
|
||||
eligible = false;
|
||||
else if (eligible) {
|
||||
let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
|
||||
if (!match)
|
||||
// Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs)
|
||||
match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
|
||||
|
||||
if (match) {
|
||||
sum += parseFloat(match[2]);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root.gpuTemp = count > 0 ? sum / count : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import QtQuick
|
||||
|
||||
Text {
|
||||
renderType: Text.NativeRendering
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property list<DesktopEntry> entryList: []
|
||||
property var preppedIcons: []
|
||||
property var preppedIds: []
|
||||
property var preppedNames: []
|
||||
|
||||
// Dynamic fixups
|
||||
property var regexSubstitutions: [
|
||||
{
|
||||
"regex": /^steam_app_(\d+)$/,
|
||||
"replace": "steam_icon_$1"
|
||||
},
|
||||
{
|
||||
"regex": /Minecraft.*/,
|
||||
"replace": "minecraft-launcher"
|
||||
},
|
||||
{
|
||||
"regex": /.*polkit.*/,
|
||||
"replace": "system-lock-screen"
|
||||
},
|
||||
{
|
||||
"regex": /gcr.prompter/,
|
||||
"replace": "system-lock-screen"
|
||||
}
|
||||
]
|
||||
property real scoreThreshold: 0.2
|
||||
|
||||
// Manual overrides for tricky apps
|
||||
property var substitutions: ({
|
||||
"code-url-handler": "visual-studio-code",
|
||||
"Code": "visual-studio-code",
|
||||
"gnome-tweaks": "org.gnome.tweaks",
|
||||
"pavucontrol-qt": "pavucontrol",
|
||||
"wps": "wps-office2019-kprometheus",
|
||||
"wpsoffice": "wps-office2019-kprometheus",
|
||||
"footclient": "foot"
|
||||
})
|
||||
|
||||
function checkCleanMatch(str) {
|
||||
if (!str || str.length <= 3)
|
||||
return null;
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return null;
|
||||
|
||||
// Aggressive fallback: strip all separators
|
||||
const cleanStr = str.toLowerCase().replace(/[\.\-_]/g, '');
|
||||
const list = Array.from(entryList);
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const entry = list[i];
|
||||
const cleanId = (entry.id || "").toLowerCase().replace(/[\.\-_]/g, '');
|
||||
if (cleanId.includes(cleanStr) || cleanStr.includes(cleanId)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkFuzzySearch(str) {
|
||||
if (typeof FuzzySort === 'undefined')
|
||||
return null;
|
||||
|
||||
// Check filenames (IDs) first
|
||||
if (preppedIds.length > 0) {
|
||||
let results = fuzzyQuery(str, preppedIds);
|
||||
if (results.length === 0) {
|
||||
const underscored = str.replace(/-/g, '_').toLowerCase();
|
||||
if (underscored !== str)
|
||||
results = fuzzyQuery(underscored, preppedIds);
|
||||
}
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
// Then icons
|
||||
if (preppedIcons.length > 0) {
|
||||
const results = fuzzyQuery(str, preppedIcons);
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
// Then names
|
||||
if (preppedNames.length > 0) {
|
||||
const results = fuzzyQuery(str, preppedNames);
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Lookup Helpers ---
|
||||
|
||||
function checkHeuristic(str) {
|
||||
if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) {
|
||||
const entry = DesktopEntries.heuristicLookup(str);
|
||||
if (entry)
|
||||
return entry;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkRegex(str) {
|
||||
for (let i = 0; i < regexSubstitutions.length; i++) {
|
||||
const sub = regexSubstitutions[i];
|
||||
const replaced = str.replace(sub.regex, sub.replace);
|
||||
if (replaced !== str) {
|
||||
return findAppEntry(replaced);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkSimpleTransforms(str) {
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return null;
|
||||
|
||||
const lower = str.toLowerCase();
|
||||
|
||||
const variants = [str, lower, getFromReverseDomain(str), getFromReverseDomain(str)?.toLowerCase(), normalizeWithHyphens(str), str.replace(/_/g, '-').toLowerCase(), str.replace(/-/g, '_').toLowerCase()];
|
||||
|
||||
for (let i = 0; i < variants.length; i++) {
|
||||
const variant = variants[i];
|
||||
if (variant) {
|
||||
const entry = DesktopEntries.byId(variant);
|
||||
if (entry)
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkSubstitutions(str) {
|
||||
let effectiveStr = substitutions[str];
|
||||
if (!effectiveStr)
|
||||
effectiveStr = substitutions[str.toLowerCase()];
|
||||
|
||||
if (effectiveStr && effectiveStr !== str) {
|
||||
return findAppEntry(effectiveStr);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function distroLogoPath() {
|
||||
try {
|
||||
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : "";
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Robust lookup strategy
|
||||
function findAppEntry(str) {
|
||||
if (!str || str.length === 0)
|
||||
return null;
|
||||
|
||||
let result = null;
|
||||
|
||||
if (result = checkHeuristic(str))
|
||||
return result;
|
||||
if (result = checkSubstitutions(str))
|
||||
return result;
|
||||
if (result = checkRegex(str))
|
||||
return result;
|
||||
if (result = checkSimpleTransforms(str))
|
||||
return result;
|
||||
if (result = checkFuzzySearch(str))
|
||||
return result;
|
||||
if (result = checkCleanMatch(str))
|
||||
return result;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function fuzzyQuery(search, preppedData) {
|
||||
if (!search || !preppedData || preppedData.length === 0)
|
||||
return [];
|
||||
return FuzzySort.go(search, preppedData, {
|
||||
all: true,
|
||||
key: "name"
|
||||
}).map(r => r.obj.entry);
|
||||
}
|
||||
|
||||
function getFromReverseDomain(str) {
|
||||
if (!str)
|
||||
return "";
|
||||
return str.split('.').slice(-1)[0];
|
||||
}
|
||||
|
||||
// Deprecated shim
|
||||
function guessIcon(str) {
|
||||
const entry = findAppEntry(str);
|
||||
return entry ? entry.icon : "image-missing";
|
||||
}
|
||||
|
||||
function iconExists(iconName) {
|
||||
if (!iconName || iconName.length === 0)
|
||||
return false;
|
||||
if (iconName.startsWith("/"))
|
||||
return true;
|
||||
|
||||
const path = Quickshell.iconPath(iconName, true);
|
||||
return path && path.length > 0 && !path.includes("image-missing");
|
||||
}
|
||||
|
||||
function iconForAppId(appId, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable";
|
||||
if (!appId)
|
||||
return iconFromName(fallback, fallback);
|
||||
|
||||
const entry = findAppEntry(appId);
|
||||
if (entry) {
|
||||
return iconFromName(entry.icon, fallback);
|
||||
}
|
||||
|
||||
return iconFromName(appId, fallback);
|
||||
}
|
||||
|
||||
function iconFromName(iconName, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable";
|
||||
try {
|
||||
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
|
||||
const p = Quickshell.iconPath(iconName, fallback);
|
||||
if (p && p !== "")
|
||||
return p;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : "";
|
||||
} catch (e2) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWithHyphens(str) {
|
||||
if (!str)
|
||||
return "";
|
||||
return str.toLowerCase().replace(/\s+/g, "-");
|
||||
}
|
||||
|
||||
function refreshEntries() {
|
||||
if (typeof DesktopEntries === 'undefined')
|
||||
return;
|
||||
|
||||
const values = Array.from(DesktopEntries.applications.values);
|
||||
if (values) {
|
||||
entryList = values.sort((a, b) => a.name.localeCompare(b.name));
|
||||
updatePreppedData();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreppedData() {
|
||||
if (typeof FuzzySort === 'undefined')
|
||||
return;
|
||||
|
||||
const list = Array.from(entryList);
|
||||
preppedNames = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.name} `),
|
||||
entry: a
|
||||
}));
|
||||
preppedIcons = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.icon} `),
|
||||
entry: a
|
||||
}));
|
||||
preppedIds = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.id} `),
|
||||
entry: a
|
||||
}));
|
||||
}
|
||||
|
||||
Component.onCompleted: refreshEntries()
|
||||
|
||||
Connections {
|
||||
function onValuesChanged() {
|
||||
refreshEntries();
|
||||
}
|
||||
|
||||
target: DesktopEntries.applications
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
readonly property string amPmStr: timeComponents[2] ?? ""
|
||||
readonly property date date: clock.date
|
||||
property alias enabled: clock.enabled
|
||||
readonly property string hourStr: timeComponents[0] ?? ""
|
||||
readonly property int hours: clock.hours
|
||||
readonly property string minuteStr: timeComponents[1] ?? ""
|
||||
readonly property int minutes: clock.minutes
|
||||
readonly property int seconds: clock.seconds
|
||||
readonly property list<string> timeComponents: timeStr.split(":")
|
||||
readonly property string timeStr: format("hh:mm")
|
||||
|
||||
function format(fmt: string): string {
|
||||
return Qt.formatDateTime(clock.date, fmt);
|
||||
}
|
||||
|
||||
SystemClock {
|
||||
id: clock
|
||||
|
||||
precision: SystemClock.Seconds
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Paths
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property alias currentWallpaperPath: adapter.currentWallpaperPath
|
||||
property alias lockscreenBg: adapter.lockscreenBg
|
||||
|
||||
FileView {
|
||||
id: fileView
|
||||
|
||||
path: `${Paths.state}/wallpaper_path.json`
|
||||
watchChanges: true
|
||||
|
||||
onAdapterUpdated: writeAdapter()
|
||||
onFileChanged: reload()
|
||||
|
||||
JsonAdapter {
|
||||
id: adapter
|
||||
|
||||
property string currentWallpaperPath: ""
|
||||
property string lockscreenBg: `${Paths.state}/lockscreen_bg.png`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import ZShell
|
||||
import qs.Config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var cachedCities: new Map()
|
||||
property var cc
|
||||
property string city
|
||||
readonly property string description: cc?.weatherDesc ?? qsTr("No weather")
|
||||
readonly property string feelsLike: `${cc?.feelsLikeC ?? 0}°C`
|
||||
property list<var> forecast
|
||||
property list<var> hourlyForecast
|
||||
readonly property int humidity: cc?.humidity ?? 0
|
||||
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
|
||||
property string loc
|
||||
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), "h:mm") : "--:--"
|
||||
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), "h:mm") : "--:--"
|
||||
readonly property string temp: `${cc?.tempC ?? 0}°C`
|
||||
readonly property real windSpeed: cc?.windSpeed ?? 0
|
||||
|
||||
function fetchCityFromCoords(coords: string): void {
|
||||
if (cachedCities.has(coords)) {
|
||||
city = cachedCities.get(coords);
|
||||
return;
|
||||
}
|
||||
|
||||
const [lat, lon] = coords.split(",");
|
||||
const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`;
|
||||
Requests.get(url, text => {
|
||||
const geo = JSON.parse(text).features?.[0]?.properties.geocoding;
|
||||
if (geo) {
|
||||
const geoCity = geo.type === "city" ? geo.name : geo.city;
|
||||
city = geoCity;
|
||||
cachedCities.set(coords, geoCity);
|
||||
} else {
|
||||
city = "Unknown City";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fetchCoordsFromCity(cityName: string): void {
|
||||
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=en&format=json`;
|
||||
|
||||
Requests.get(url, text => {
|
||||
const json = JSON.parse(text);
|
||||
if (json.results && json.results.length > 0) {
|
||||
const result = json.results[0];
|
||||
loc = result.latitude + "," + result.longitude;
|
||||
city = result.name;
|
||||
} else {
|
||||
loc = "";
|
||||
reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fetchWeatherData(): void {
|
||||
const url = getWeatherUrl();
|
||||
if (url === "")
|
||||
return;
|
||||
|
||||
Requests.get(url, text => {
|
||||
const json = JSON.parse(text);
|
||||
if (!json.current || !json.daily)
|
||||
return;
|
||||
|
||||
cc = {
|
||||
weatherCode: json.current.weather_code,
|
||||
weatherDesc: getWeatherCondition(json.current.weather_code),
|
||||
tempC: Math.round(json.current.temperature_2m),
|
||||
tempF: Math.round(toFahrenheit(json.current.temperature_2m)),
|
||||
feelsLikeC: Math.round(json.current.apparent_temperature),
|
||||
feelsLikeF: Math.round(toFahrenheit(json.current.apparent_temperature)),
|
||||
humidity: json.current.relative_humidity_2m,
|
||||
windSpeed: json.current.wind_speed_10m,
|
||||
isDay: json.current.is_day,
|
||||
sunrise: json.daily.sunrise[0],
|
||||
sunset: json.daily.sunset[0]
|
||||
};
|
||||
|
||||
const forecastList = [];
|
||||
for (let i = 0; i < json.daily.time.length; i++)
|
||||
forecastList.push({
|
||||
date: json.daily.time[i],
|
||||
maxTempC: Math.round(json.daily.temperature_2m_max[i]),
|
||||
maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])),
|
||||
minTempC: Math.round(json.daily.temperature_2m_min[i]),
|
||||
minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])),
|
||||
weatherCode: json.daily.weather_code[i],
|
||||
icon: Icons.getWeatherIcon(json.daily.weather_code[i])
|
||||
});
|
||||
forecast = forecastList;
|
||||
|
||||
const hourlyList = [];
|
||||
const now = new Date();
|
||||
for (let i = 0; i < json.hourly.time.length; i++) {
|
||||
const time = new Date(json.hourly.time[i]);
|
||||
if (time < now)
|
||||
continue;
|
||||
|
||||
hourlyList.push({
|
||||
timestamp: json.hourly.time[i],
|
||||
hour: time.getHours(),
|
||||
tempC: Math.round(json.hourly.temperature_2m[i]),
|
||||
tempF: Math.round(toFahrenheit(json.hourly.temperature_2m[i])),
|
||||
weatherCode: json.hourly.weather_code[i],
|
||||
icon: Icons.getWeatherIcon(json.hourly.weather_code[i])
|
||||
});
|
||||
}
|
||||
hourlyForecast = hourlyList;
|
||||
});
|
||||
}
|
||||
|
||||
function getWeatherCondition(code: string): string {
|
||||
const conditions = {
|
||||
"0": "Clear",
|
||||
"1": "Clear",
|
||||
"2": "Partly cloudy",
|
||||
"3": "Overcast",
|
||||
"45": "Fog",
|
||||
"48": "Fog",
|
||||
"51": "Drizzle",
|
||||
"53": "Drizzle",
|
||||
"55": "Drizzle",
|
||||
"56": "Freezing drizzle",
|
||||
"57": "Freezing drizzle",
|
||||
"61": "Light rain",
|
||||
"63": "Rain",
|
||||
"65": "Heavy rain",
|
||||
"66": "Light rain",
|
||||
"67": "Heavy rain",
|
||||
"71": "Light snow",
|
||||
"73": "Snow",
|
||||
"75": "Heavy snow",
|
||||
"77": "Snow",
|
||||
"80": "Light rain",
|
||||
"81": "Rain",
|
||||
"82": "Heavy rain",
|
||||
"85": "Light snow showers",
|
||||
"86": "Heavy snow showers",
|
||||
"95": "Thunderstorm",
|
||||
"96": "Thunderstorm with hail",
|
||||
"99": "Thunderstorm with hail"
|
||||
};
|
||||
return conditions[code] || "Unknown";
|
||||
}
|
||||
|
||||
function getWeatherUrl(): string {
|
||||
if (!loc || loc.indexOf(",") === -1)
|
||||
return "";
|
||||
|
||||
const [lat, lon] = loc.split(",");
|
||||
const baseUrl = "https://api.open-meteo.com/v1/forecast";
|
||||
const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"];
|
||||
|
||||
return baseUrl + "?" + params.join("&");
|
||||
}
|
||||
|
||||
function reload(): void {
|
||||
const configLocation = Config.services.weatherLocation;
|
||||
|
||||
if (configLocation) {
|
||||
if (configLocation.indexOf(",") !== -1 && !isNaN(parseFloat(configLocation.split(",")[0]))) {
|
||||
loc = configLocation;
|
||||
fetchCityFromCoords(configLocation);
|
||||
} else {
|
||||
fetchCoordsFromCity(configLocation);
|
||||
}
|
||||
} else if (!loc || timer.elapsed() > 900) {
|
||||
Requests.get("https://ipinfo.io/json", text => {
|
||||
const response = JSON.parse(text);
|
||||
if (response.loc) {
|
||||
loc = response.loc;
|
||||
city = response.city ?? "";
|
||||
timer.restart();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toFahrenheit(celcius: real): real {
|
||||
return celcius * 9 / 5 + 32;
|
||||
}
|
||||
|
||||
onLocChanged: fetchWeatherData()
|
||||
|
||||
// Refresh current location hourly
|
||||
Timer {
|
||||
interval: 3600000 // 1 hour
|
||||
repeat: true
|
||||
running: true
|
||||
|
||||
onTriggered: fetchWeatherData()
|
||||
}
|
||||
|
||||
ElapsedTimer {
|
||||
id: timer
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user