Merge branch 'main' into module/wifi
This commit is contained in:
+30
-84
@@ -1,19 +1,13 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "../scripts/levendist.js" as Levendist
|
||||
import qs.Helpers
|
||||
import "../scripts/fuzzysort.js" as Fuzzy
|
||||
import qs.Config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<DesktopEntry> list: Array.from(DesktopEntries.applications.values).filter((app, index, self) => index === self.findIndex(t => (t.id === app.id)))
|
||||
readonly property var preppedIcons: list.map(a => ({
|
||||
name: Fuzzy.prepare(`${a.icon} `),
|
||||
entry: a
|
||||
}))
|
||||
readonly property list<DesktopEntry> list: Array.from(DesktopEntries.applications.values)
|
||||
readonly property var preppedNames: list.map(a => ({
|
||||
name: Fuzzy.prepare(`${a.name} `),
|
||||
entry: a
|
||||
@@ -36,8 +30,7 @@ Singleton {
|
||||
"replace": "system-lock-screen"
|
||||
}
|
||||
]
|
||||
readonly property real scoreGapThreshold: 0.1
|
||||
readonly property real scoreThreshold: 0.6
|
||||
property real scoreThreshold: 0.2
|
||||
property var substitutions: ({
|
||||
"code-url-handler": "visual-studio-code",
|
||||
"Code": "visual-studio-code",
|
||||
@@ -45,64 +38,27 @@ Singleton {
|
||||
"pavucontrol-qt": "pavucontrol",
|
||||
"wps": "wps-office2019-kprometheus",
|
||||
"wpsoffice": "wps-office2019-kprometheus",
|
||||
"footclient": "foot"
|
||||
"footclient": "foot",
|
||||
"zen": "zen-browser"
|
||||
})
|
||||
|
||||
function bestFuzzyEntry(search: string, preppedList: list<var>, key: string): var {
|
||||
const results = Fuzzy.go(search, preppedList, {
|
||||
key: key,
|
||||
threshold: root.scoreThreshold,
|
||||
limit: 2
|
||||
signal reload
|
||||
|
||||
function fuzzyQuery(search: string): var {
|
||||
return Fuzzy.go(search, preppedNames, {
|
||||
all: true,
|
||||
key: "name"
|
||||
}).map(r => {
|
||||
return r.obj.entry;
|
||||
});
|
||||
|
||||
if (!results || results.length === 0)
|
||||
return null;
|
||||
|
||||
const best = results[0];
|
||||
const second = results.length > 1 ? results[1] : null;
|
||||
|
||||
if (second && (best.score - second.score) < root.scoreGapThreshold)
|
||||
return null;
|
||||
|
||||
return best.obj.entry;
|
||||
}
|
||||
|
||||
function fuzzyQuery(search: string, preppedList: list<var>): var {
|
||||
const entry = bestFuzzyEntry(search, preppedList, "name");
|
||||
return entry ? [entry] : [];
|
||||
}
|
||||
|
||||
function getKebabNormalizedAppName(str: string): string {
|
||||
return str.toLowerCase().replace(/\s+/g, "-");
|
||||
}
|
||||
|
||||
function getReverseDomainNameAppName(str: string): string {
|
||||
return str.split('.').slice(-1)[0];
|
||||
}
|
||||
|
||||
function getUndescoreToKebabAppName(str: string): string {
|
||||
return str.toLowerCase().replace(/_/g, "-");
|
||||
}
|
||||
|
||||
function guessIcon(str) {
|
||||
if (!str || str.length == 0)
|
||||
return "image-missing";
|
||||
|
||||
if (iconExists(str))
|
||||
return str;
|
||||
|
||||
const entry = DesktopEntries.byId(str);
|
||||
if (entry)
|
||||
return entry.icon;
|
||||
|
||||
const heuristicEntry = DesktopEntries.heuristicLookup(str);
|
||||
if (heuristicEntry)
|
||||
return heuristicEntry.icon;
|
||||
|
||||
if (substitutions[str])
|
||||
return substitutions[str];
|
||||
if (substitutions[str.toLowerCase()])
|
||||
return substitutions[str.toLowerCase()];
|
||||
|
||||
for (let i = 0; i < regexSubstitutions.length; i++) {
|
||||
const substitution = regexSubstitutions[i];
|
||||
@@ -111,35 +67,25 @@ Singleton {
|
||||
return replacedName;
|
||||
}
|
||||
|
||||
const lowercased = str.toLowerCase();
|
||||
if (iconExists(lowercased))
|
||||
return lowercased;
|
||||
if (iconExists(str))
|
||||
return str;
|
||||
|
||||
const reverseDomainNameAppName = getReverseDomainNameAppName(str);
|
||||
if (iconExists(reverseDomainNameAppName))
|
||||
return reverseDomainNameAppName;
|
||||
let guessStr = str;
|
||||
guessStr = str.split('.').slice(-1)[0].toLowerCase();
|
||||
if (iconExists(guessStr))
|
||||
return guessStr;
|
||||
guessStr = str.toLowerCase().replace(/\s+/g, "-");
|
||||
if (iconExists(guessStr))
|
||||
return guessStr;
|
||||
const searchResults = root.fuzzyQuery(str);
|
||||
if (searchResults.length > 0) {
|
||||
const firstEntry = searchResults[0];
|
||||
guessStr = firstEntry.icon;
|
||||
if (iconExists(guessStr))
|
||||
return guessStr;
|
||||
}
|
||||
|
||||
const lowercasedDomainNameAppName = reverseDomainNameAppName.toLowerCase();
|
||||
if (iconExists(lowercasedDomainNameAppName))
|
||||
return lowercasedDomainNameAppName;
|
||||
|
||||
const kebabNormalizedGuess = getKebabNormalizedAppName(str);
|
||||
if (iconExists(kebabNormalizedGuess))
|
||||
return kebabNormalizedGuess;
|
||||
|
||||
const undescoreToKebabGuess = getUndescoreToKebabAppName(str);
|
||||
if (iconExists(undescoreToKebabGuess))
|
||||
return undescoreToKebabGuess;
|
||||
|
||||
const iconSearchResult = fuzzyQuery(str, preppedIcons);
|
||||
if (iconSearchResult && iconExists(iconSearchResult.icon))
|
||||
return iconSearchResult.icon;
|
||||
|
||||
const nameSearchResult = root.fuzzyQuery(str, preppedNames);
|
||||
if (nameSearchResult && iconExists(nameSearchResult.icon))
|
||||
return nameSearchResult.icon;
|
||||
|
||||
return "application-x-executable";
|
||||
return str;
|
||||
}
|
||||
|
||||
function iconExists(iconName) {
|
||||
|
||||
@@ -29,4 +29,6 @@ Singleton {
|
||||
readonly property bool isLaptop: UPower.displayDevice.isLaptopBattery
|
||||
readonly property bool onBattery: UPower.onBattery
|
||||
readonly property bool ready: UPower.displayDevice.ready
|
||||
readonly property real timeToEmpty: UPower.displayDevice.timeToEmpty
|
||||
readonly property real timeToFull: UPower.displayDevice.timeToFull
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function setBrightness(value: real): void {
|
||||
value = Math.max(0, Math.min(1, value));
|
||||
value = Math.max(Config.services.minBrightness, Math.min(1, value));
|
||||
const rounded = Math.round(value * 100);
|
||||
if (Math.round(brightness * 100) === rounded)
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Config
|
||||
import "../scripts/fuzzysort.js" as Fuzzy
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string cliphistBinary: "cliphist"
|
||||
property string currentEntry: ""
|
||||
property list<string> entries: []
|
||||
property real pasteDelay: 0.05
|
||||
readonly property var preparedEntries: entries.map(a => ({
|
||||
name: Fuzzy.prepare(displayText(a)),
|
||||
entry: a
|
||||
}))
|
||||
property string previewImageFile: "/tmp/qs-cliphist-preview.img"
|
||||
property string previewImageSource: ""
|
||||
property bool previewIsImage: false
|
||||
property list<var> previewText: []
|
||||
property int previewToken: 0
|
||||
property real scoreThreshold: 0.2
|
||||
|
||||
function copy(entry): void {
|
||||
Quickshell.execDetached(["bash", "-c", `printf '${shellSingleQuoteEscape(entry)}' | ${root.cliphistBinary} decode | wl-copy`]);
|
||||
}
|
||||
|
||||
function deleteEntry(entry): void {
|
||||
deleteProc.deleteEntry(entry);
|
||||
}
|
||||
|
||||
function displayText(entry): string {
|
||||
return entry.replace(/^\s*\d+\s+/, "");
|
||||
}
|
||||
|
||||
function entryIsImage(entry): bool {
|
||||
return !!(/^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/.test(entry));
|
||||
}
|
||||
|
||||
function escapeHtml(str): string {
|
||||
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function fuzzyQuery(search: string): var {
|
||||
if (search.trim() === "") {
|
||||
return entries;
|
||||
}
|
||||
return Fuzzy.go(search, preparedEntries, {
|
||||
all: true,
|
||||
key: "name"
|
||||
}).map(r => {
|
||||
return r.obj.entry;
|
||||
});
|
||||
}
|
||||
|
||||
function paste(entry): void {
|
||||
Quickshell.execDetached(["bash", "-c", `printf '${shellSingleQuoteEscape(entry)}' | ${root.cliphistBinary} decode | wl-copy && wl-paste`]);
|
||||
}
|
||||
|
||||
function processPreviewLines(text: string): var {
|
||||
if (text === "")
|
||||
return [];
|
||||
const lines = text.split("\n");
|
||||
let minIndent = Infinity;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const m = lines[i].match(/^(\s*)/);
|
||||
const indent = m ? m[1].length : 0;
|
||||
if (lines[i].trim().length > 0 && indent < minIndent)
|
||||
minIndent = indent;
|
||||
}
|
||||
if (minIndent === Infinity)
|
||||
minIndent = 0;
|
||||
const result = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const indentMatch = line.match(/^(\s*)/);
|
||||
const indentLen = indentMatch ? indentMatch[1].length : 0;
|
||||
const stripped = line.slice(Math.min(minIndent, indentLen));
|
||||
result.push({
|
||||
line: i + 1,
|
||||
text: stripped
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function refresh(): void {
|
||||
readProc.buffer = [];
|
||||
readProc.running = true;
|
||||
}
|
||||
|
||||
function refreshPreview(): void {
|
||||
previewToken += 1;
|
||||
const token = previewToken;
|
||||
|
||||
if (!currentEntry) {
|
||||
previewText = [];
|
||||
previewImageSource = "";
|
||||
previewIsImage = false;
|
||||
return;
|
||||
}
|
||||
|
||||
previewImageSource = "";
|
||||
previewIsImage = entryIsImage(currentEntry);
|
||||
|
||||
if (previewIsImage) {
|
||||
previewImageProc.token = token;
|
||||
previewImageProc.running = true;
|
||||
} else {
|
||||
previewTextProc.token = token;
|
||||
previewTextProc.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
function shellSingleQuoteEscape(str): string {
|
||||
return String(str).replace(/'/g, "'\\''");
|
||||
}
|
||||
|
||||
function wipe(): void {
|
||||
wipeProc.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: deleteProc
|
||||
|
||||
property string entry: ""
|
||||
|
||||
function deleteEntry(entry) {
|
||||
deleteProc.entry = entry;
|
||||
deleteProc.running = true;
|
||||
deleteProc.entry = "";
|
||||
}
|
||||
|
||||
command: ["bash", "-c", `echo '${root.shellSingleQuoteEscape(deleteProc.entry)}' | ${root.cliphistBinary} delete`]
|
||||
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: wipeProc
|
||||
|
||||
command: [root.cliphistBinary, "wipe"]
|
||||
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onClipboardTextChanged() {
|
||||
delayedUpdateTimer.restart();
|
||||
}
|
||||
|
||||
target: Quickshell
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: delayedUpdateTimer
|
||||
|
||||
interval: 50
|
||||
repeat: false
|
||||
|
||||
onTriggered: {
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: previewTextProc
|
||||
|
||||
property int token: 0
|
||||
|
||||
command: ["bash", "-c", `
|
||||
printf '%s' '${root.shellSingleQuoteEscape(root.currentEntry)}' | ${root.cliphistBinary} decode
|
||||
`]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (previewTextProc.token !== root.previewToken)
|
||||
return;
|
||||
root.previewText = root.processPreviewLines(this.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: previewImageProc
|
||||
|
||||
property int token: 0
|
||||
|
||||
command: ["bash", "-c", `
|
||||
set -euo pipefail
|
||||
tmp='${root.shellSingleQuoteEscape(root.previewImageFile)}'
|
||||
printf '%s' '${root.shellSingleQuoteEscape(root.currentEntry)}' | ${root.cliphistBinary} decode > "$tmp"
|
||||
`]
|
||||
running: false
|
||||
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (token !== root.previewToken)
|
||||
return;
|
||||
if (exitCode !== 0) {
|
||||
console.error("[Cliphist] image preview failed", exitCode, exitStatus);
|
||||
return;
|
||||
}
|
||||
|
||||
root.previewImageSource = "";
|
||||
Qt.callLater(() => {
|
||||
root.previewImageSource = `file://${root.previewImageFile}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: readProc
|
||||
|
||||
property list<string> buffer: []
|
||||
|
||||
command: [root.cliphistBinary, "list"]
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: line => {
|
||||
readProc.buffer.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (exitCode === 0) {
|
||||
root.entries = readProc.buffer;
|
||||
} else {
|
||||
console.error("[Cliphist] Failed to refresh with code", exitCode, "and status", exitStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function update(): void {
|
||||
root.refresh();
|
||||
}
|
||||
|
||||
target: "cliphistService"
|
||||
}
|
||||
}
|
||||
@@ -42,11 +42,24 @@ Singleton {
|
||||
function checkStartup() {
|
||||
if (!root.enabled)
|
||||
return;
|
||||
|
||||
var now = new Date();
|
||||
if (now.getHours() >= darkStart || now.getHours() < darkEnd) {
|
||||
applyDarkMode();
|
||||
var nowMinutes = now.getHours() * 60 + now.getMinutes();
|
||||
|
||||
var isDarkTime;
|
||||
|
||||
if (root.darkStart <= root.darkEnd) {
|
||||
isDarkTime = (nowMinutes >= root.darkStart && nowMinutes < root.darkEnd);
|
||||
} else {
|
||||
applyLightMode();
|
||||
isDarkTime = (nowMinutes >= root.darkStart || nowMinutes < root.darkEnd);
|
||||
}
|
||||
|
||||
if (isDarkTime) {
|
||||
if (DynamicColors.light)
|
||||
root.applyDarkMode();
|
||||
} else {
|
||||
if (!DynamicColors.light)
|
||||
root.applyLightMode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,8 +73,19 @@ Singleton {
|
||||
onTriggered: {
|
||||
if (!root.enabled)
|
||||
return;
|
||||
|
||||
var now = new Date();
|
||||
if (now.getHours() >= root.darkStart || now.getHours() < root.darkEnd) {
|
||||
var nowMinutes = now.getHours() * 60 + now.getMinutes();
|
||||
|
||||
var isDarkTime;
|
||||
|
||||
if (root.darkStart <= root.darkEnd) {
|
||||
isDarkTime = (nowMinutes >= root.darkStart && nowMinutes < root.darkEnd);
|
||||
} else {
|
||||
isDarkTime = (nowMinutes >= root.darkStart || nowMinutes < root.darkEnd);
|
||||
}
|
||||
|
||||
if (isDarkTime) {
|
||||
if (DynamicColors.light)
|
||||
root.applyDarkMode();
|
||||
} else {
|
||||
|
||||
@@ -39,6 +39,21 @@ Singleton {
|
||||
readonly property real uploadSpeed: _uploadSpeed
|
||||
readonly property real uploadTotal: _uploadTotal
|
||||
|
||||
function resetSampling(): void {
|
||||
netDevFile.reload();
|
||||
|
||||
const content = netDevFile.text();
|
||||
if (!content)
|
||||
return;
|
||||
|
||||
const data = root.parseNetDev(content);
|
||||
const now = Date.now();
|
||||
|
||||
root._prevRxBytes = data.rx;
|
||||
root._prevTxBytes = data.tx;
|
||||
root._prevTimestamp = now;
|
||||
}
|
||||
|
||||
function formatBytes(bytes: real): var {
|
||||
// Handle negative or invalid values
|
||||
if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) {
|
||||
@@ -156,10 +171,15 @@ Singleton {
|
||||
Timer {
|
||||
interval: Config.dashboard.resourceUpdateInterval
|
||||
repeat: true
|
||||
running: root.refCount > 0
|
||||
running: true
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
if (root.refCount <= 0) {
|
||||
root.resetSampling();
|
||||
return;
|
||||
}
|
||||
|
||||
netDevFile.reload();
|
||||
const content = netDevFile.text();
|
||||
if (!content)
|
||||
|
||||
@@ -1,464 +0,0 @@
|
||||
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 string gpuName
|
||||
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;
|
||||
|
||||
if (root.gpuType === "GENERIC" && root.gpuMemTotal === 0)
|
||||
oneshotMemAmd.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: oneshotMemAmd
|
||||
|
||||
command: ["sh", "-c", "cat /sys/class/drm/card*/device/mem_info_vram_total"]
|
||||
running: root.gpuType === "GENERIC" && root.gpuMemTotal === 0
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const values = text.trim().split("\n").map(v => parseInt(v, 10)).filter(v => Number.isFinite(v));
|
||||
|
||||
if (values.length > 0) {
|
||||
const totalBytes = values.reduce((a, b) => a + b, 0);
|
||||
root.gpuMemTotal = totalBytes / (1024 * 1024);
|
||||
}
|
||||
|
||||
oneshotMemAmd.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", "paste -d ' ' /sys/class/drm/card*/device/gpu_busy_percent /sys/class/drm/card*/device/mem_info_vram_used"] : ["echo"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (root.gpuType === "GENERIC") {
|
||||
const lines = text.trim().split("\n");
|
||||
|
||||
let percSum = 0;
|
||||
let memSum = 0;
|
||||
let count = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length < 2)
|
||||
continue;
|
||||
|
||||
const gpuBusy = parseInt(parts[0], 10);
|
||||
const memUsed = parseInt(parts[1], 10);
|
||||
|
||||
if (!Number.isFinite(gpuBusy) || !Number.isFinite(memUsed))
|
||||
continue;
|
||||
|
||||
percSum += gpuBusy;
|
||||
memSum += memUsed;
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
// GPU usage %
|
||||
root.gpuPerc = (percSum / count) / 100;
|
||||
|
||||
// VRAM usage (bytes → MiB → normalized)
|
||||
const memUsedMiB = memSum / (1024 * 1024);
|
||||
|
||||
const newGpuMemUsed = root.gpuMemTotal > 0 ? Math.max(0, Math.min(1, memUsedMiB / root.gpuMemTotal)) : 0;
|
||||
|
||||
if (Math.abs(root.gpuMemUsed - newGpuMemUsed) >= 0.01)
|
||||
root.gpuMemUsed = newGpuMemUsed;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-12
@@ -32,33 +32,26 @@ Searcher {
|
||||
showPreview = true;
|
||||
}
|
||||
|
||||
function setCrop(screen: string, rect: rect, scaledRect: rect, zoom: real): void {
|
||||
let updated = Object.assign({}, root.crops);
|
||||
|
||||
function setCrop(screen: string, rect: rect, zoom: real): void {
|
||||
if (zoom <= 0)
|
||||
zoom = 1.0;
|
||||
else if (zoom > 5.0)
|
||||
zoom = 5.0;
|
||||
|
||||
updated[screen] = {
|
||||
root.crops[screen] = {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
scaledX: scaledRect.x,
|
||||
scaledY: scaledRect.y,
|
||||
scaledWidth: scaledRect.width,
|
||||
scaledHeight: scaledRect.height,
|
||||
zoom: zoom
|
||||
};
|
||||
|
||||
root.crops = updated;
|
||||
// root.crops = updated;
|
||||
}
|
||||
|
||||
function setWallpaper(path: string): void {
|
||||
actualCurrent = path;
|
||||
WallpaperPath.currentWallpaperPath = path;
|
||||
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), Qt.rect(0, 0, 0, 0), 1.0));
|
||||
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), 1.0));
|
||||
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
|
||||
if (Config.general.color.schemeGeneration)
|
||||
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]);
|
||||
@@ -91,7 +84,7 @@ Searcher {
|
||||
path: `${Paths.state}/wallpaper-crops.json`
|
||||
watchChanges: true
|
||||
|
||||
onAdapterUpdated: writeAdapter()
|
||||
onAdapterUpdated: cropWriteDelay.restart()
|
||||
onFileChanged: reload()
|
||||
|
||||
JsonAdapter {
|
||||
@@ -101,6 +94,16 @@ Searcher {
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: cropWriteDelay
|
||||
|
||||
interval: 100
|
||||
repeat: false
|
||||
running: false
|
||||
|
||||
onTriggered: monitorCrops.writeAdapter()
|
||||
}
|
||||
|
||||
FileSystemModel {
|
||||
id: wallpapers
|
||||
|
||||
|
||||
Reference in New Issue
Block a user