Files
Panama/config/dot/quickshell/services/Traces.qml
T

115 lines
4.3 KiB
QML

pragma Singleton
// What this desktop remembers about what you opened.
//
// Two traces -- the recent-files list and the thumbnail cache -- both of them a
// normal and useful part of a desktop rather than a problem. This measures them
// when asked and clears them when asked, and does neither on its own.
//
// Nothing here measures on startup. Walking a thumbnail cache costs real time
// on a machine that has browsed a large picture library, and a settings page
// that has not been opened has no business spending it. `measured` says whether
// there is an answer yet, so the card can show its own state honestly instead of
// showing a confident zero.
//
// Trash is deliberately absent. It is measured and emptied by Disks, and one
// trash implementation is the right number to have.
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
readonly property string helperPath: Quickshell.env("PANAMA_PRIVACY_HELPER")
|| Quickshell.shellDir + "/scripts/panama-privacy"
// real rather than int: a thumbnail cache on a machine with a large picture
// library goes past what a 32-bit int holds, and a byte count that wrapped
// negative is worse than no byte count.
property real recentsBytes: 0
property int recentsEntries: 0
property string recentsPath: ""
property bool recentsPresent: false
property real thumbnailsBytes: 0
property string thumbnailsPath: ""
property bool thumbnailsPresent: false
// False when the walk ran out of time, which makes the byte count a floor
// rather than an answer.
property bool thumbnailsComplete: true
// Whether there is an answer at all yet. Distinct from thumbnailsComplete,
// which is about the quality of an answer that exists.
property bool measured: false
property string lastError: ""
readonly property bool busy: reader.running || mutation.running
function measure(): void {
if (reader.running)
return;
reader.command = [root.helperPath, "traces"];
reader.running = true;
}
function absorb(text: string): void {
try {
const parsed = JSON.parse(text);
const recents = parsed.recents ?? ({});
const thumbnails = parsed.thumbnails ?? ({});
root.recentsBytes = Number(recents.bytes ?? 0);
root.recentsEntries = Number(recents.entries ?? 0);
root.recentsPath = String(recents.path ?? "");
root.recentsPresent = recents.present === true;
root.thumbnailsBytes = Number(thumbnails.bytes ?? 0);
root.thumbnailsPath = String(thumbnails.path ?? "");
root.thumbnailsPresent = thumbnails.present === true;
root.thumbnailsComplete = thumbnails.measured !== false;
root.lastError = String(parsed.error ?? "");
} catch (error) {
root.lastError = "Could not read what the desktop has remembered.";
console.warn("Traces: could not parse helper output:", error);
}
root.measured = true;
}
function run(verb: string): void {
if (mutation.running)
return;
root.lastError = "";
mutation.command = [root.helperPath, verb];
mutation.running = true;
}
// The list is emptied, never deleted: GTK recreates the file the moment
// anything opens a file anyway, and an empty valid document takes effect in
// every running application immediately.
function clearRecents(): void { root.run("clear-recents"); }
// The folder stays; what is inside it goes. The helper refuses to follow a
// link out of it.
function clearThumbnails(): void { root.run("clear-thumbnails"); }
Process {
id: reader
stdout: StdioCollector { onStreamFinished: root.absorb(this.text) }
stderr: StdioCollector {
onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
}
}
// Both clears answer with a freshly measured snapshot, so the card updates
// without a second read.
Process {
id: mutation
stdout: StdioCollector { onStreamFinished: root.absorb(this.text) }
stderr: StdioCollector {
onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
}
}
}