Make Applications a real app manager, and clean up storage without the racket

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 17:18:14 -04:00
parent b30bf40407
commit 5a0643357f
29 changed files with 5024 additions and 314 deletions
+117
View File
@@ -40,6 +40,32 @@ Singleton {
// are different answers.
property bool foldersMeasured: false
// What the used space is made of, and what could be freed.
//
// { segments: { home, applications, caches, system, free }, usedBytes,
// totalBytes, freeBytes, complete, exceedsUsed }
//
// `system` is the remainder -- used bytes minus the three measured segments
// -- and the page calls it "System & everything else" for that reason. It
// is not a measurement of the system; it is everything the walk did not
// reach. The segments never sum past used, and when a measurement would
// overshoot, `exceedsUsed` says so instead of a number being scaled to make
// the bar look tidy.
property var breakdown: null
property bool breakdownMeasured: false
property bool measuringBreakdown: false
// [{ id, label, detail, bytes, privileged }]
//
// Nothing here is selected, ordered by urgency, or acted on. Each row is
// freed only by its own id being passed to clean(), which is what keeps a
// mis-wired button from emptying four things at once.
property var cleanables: []
property bool cleanablesMeasured: false
property bool measuringCleanables: false
// Which row is mid-clean, so the page can disable that one row.
property string cleaningId: ""
readonly property var primaryDrive: root.drives.length > 0 ? root.drives[0] : null
// The filesystem the user means when they ask how full the machine is.
@@ -110,6 +136,49 @@ Singleton {
folderScan.running = true;
}
// The same walk `scan` does, so it costs the same and is asked for on
// demand rather than when the page opens.
function measureBreakdown(): void {
if (root.measuringBreakdown)
return;
root.measuringBreakdown = true;
breakdownScan.running = true;
}
function measureCleanables(): void {
if (root.measuringCleanables)
return;
root.measuringCleanables = true;
cleanableScan.running = true;
}
// Exactly one, named. An id this service has not been told about is
// refused here and refused again by the helper.
function clean(identifier: string): void {
if (cleaner.running || root.measuringCleanables)
return;
const known = root.cleanables.some(item => String(item.id) === String(identifier));
if (!known) {
root.lastError = "There is nothing by that name to clean up.";
return;
}
root.lastError = "";
root.cleaningId = String(identifier);
cleaner.command = [root.helperPath, "clean", String(identifier)];
cleaner.running = true;
}
function absorbCleanables(text: string): void {
try {
const parsed = JSON.parse(text);
root.cleanables = Array.isArray(parsed) ? parsed : [];
root.cleanablesMeasured = true;
} catch (error) {
root.lastError = "Could not measure what could be cleaned up.";
console.warn("Disks: could not parse cleanables output:", error);
}
}
function unmount(devicePath: string): void {
root.runMedia(["unmount", devicePath]);
}
@@ -171,6 +240,54 @@ Singleton {
onExited: root.scanning = false
}
Process {
id: breakdownScan
command: [root.helperPath, "breakdown"]
stdout: StdioCollector {
onStreamFinished: {
try {
root.breakdown = JSON.parse(this.text);
root.breakdownMeasured = true;
} catch (error) {
root.lastError = "Could not measure what is using the drive.";
console.warn("Disks: could not parse breakdown output:", error);
}
}
}
stderr: StdioCollector {
onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
}
onExited: root.measuringBreakdown = false
}
Process {
id: cleanableScan
command: [root.helperPath, "cleanables"]
stdout: StdioCollector { onStreamFinished: root.absorbCleanables(this.text) }
stderr: StdioCollector {
onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
}
onExited: root.measuringCleanables = false
}
Process {
id: cleaner
// The helper answers with the fresh list, so the sizes on screen are
// what is there now rather than what was there before the clean.
stdout: StdioCollector { onStreamFinished: root.absorbCleanables(this.text) }
stderr: StdioCollector {
onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
}
onExited: {
root.cleaningId = "";
// Freeing space changes the drive's usage and its breakdown, and
// both are on screen while this happens.
root.refresh();
if (root.breakdownMeasured)
root.measureBreakdown();
}
}
Process {
id: media
stderr: StdioCollector {