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() } } }