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
@@ -8,9 +8,16 @@
// Inside a volume, the timeline is the Time Machine view: points in time,
// newest first, each one openable as a folder tree you can take a file out of.
//
// Rollback is deliberately absent. snapper's rollback changes the btrfs default
// subvolume, and this system's fstab pins subvol= explicitly, which overrides
// it -- so it would report success and change nothing after a reboot.
// The browser is its own card, not a section inside a volume card. It used to
// live inside, behind `volumeCard.open` -- so opening a point in time from a
// collapsed card set the browsing state, drew nothing, and hid the timeline as
// well. Whether a volume's older points are showing has nothing to do with
// whether you are looking inside one of them.
//
// Going back to a previous state of the whole machine is deliberately absent.
// snapper's version of it changes the btrfs default subvolume, and this
// system's fstab pins subvol= explicitly, which overrides it -- so it would
// report success and change nothing after a reboot.
import Quickshell
import QtQuick
@@ -34,7 +41,38 @@ SettingsPage {
property string confirmingDelete: ""
property string confirmingRestore: ""
readonly property bool browsingOpen: Snapshots.browsingConfig !== ""
// The service's own answer, kept under the page's name so every `visible:`
// below reads as "because something is being browsed" and never as
// "because a volume card happens to be expanded".
readonly property bool browsingOpen: Snapshots.browserOpen
readonly property var browsingConfigRecord: Snapshots.configs.find(
config => String(config.name ?? "") === Snapshots.browsingConfig) ?? null
// How many of each horizon the timeline keeps. A subset of the 0-50 the
// service accepts, because a dropdown of fifty-one numbers is a list, not a
// choice -- and whatever the configuration is actually set to is added to
// it, so a snapper default of 24 hourly is a row you can see rather than a
// value the picker cannot display.
readonly property var retentionCounts: [0, 1, 2, 3, 5, 7, 10, 14, 21, 30, 50]
function retentionOptions(unit: string, current: int): var {
const counts = root.retentionCounts.slice();
if (counts.indexOf(current) < 0 && current >= 0 && current <= Snapshots.retentionMax)
counts.push(current);
counts.sort((left, right) => left - right);
return counts.map(count => ({
value: count,
label: count === 0 ? "None kept" : count + " " + unit,
detail: count === 0
? "Nothing is kept on this horizon"
: ""
}));
}
function retentionLimit(config: var, horizon: string): int {
return Number((config?.limits ?? ({}))[horizon] ?? 0);
}
Component.onCompleted: Snapshots.refresh()
@@ -96,6 +134,8 @@ SettingsPage {
readonly property string configName: String(volumeCard.modelData.name ?? "")
readonly property var snapshots: volumeCard.modelData.snapshots ?? []
readonly property bool open: root.openConfig === volumeCard.configName
readonly property bool editable: !Snapshots.busy
&& volumeCard.modelData.readable === true
readonly property int shownCount: volumeCard.open
? volumeCard.snapshots.length
: Math.min(root.previewCount, volumeCard.snapshots.length)
@@ -109,21 +149,75 @@ SettingsPage {
? Snapshots.describe(volumeCard.modelData)
: "Nothing is being taken for this volume"
checked: volumeCard.modelData.timelineEnabled === true
enabled: !Snapshots.busy && volumeCard.modelData.readable === true
enabled: volumeCard.editable
onToggled: value => Snapshots.setTimeline(volumeCard.configName, value)
}
// The three horizons, editable. This was a read-only summary of
// numbers only snapper's config file could change, sitting one
// line above a service function nothing ever called.
TextRow {
label: "Keep"
detail: "Older points are removed automatically once these counts are exceeded"
value: Snapshots.retentionSummary(volumeCard.modelData)
}
OptionPickerRow {
id: hourlyRow
width: parent.width
label: "Hourly"
detail: "Points taken on the hour, and the first to be removed"
options: root.retentionOptions(
"hourly", root.retentionLimit(volumeCard.modelData, "hourly"))
current: root.retentionLimit(volumeCard.modelData, "hourly")
enabled: volumeCard.editable
onPicked: value => Snapshots.setRetention(
volumeCard.configName,
Number(value),
root.retentionLimit(volumeCard.modelData, "daily"),
root.retentionLimit(volumeCard.modelData, "weekly"))
}
OptionPickerRow {
id: dailyRow
width: parent.width
label: "Daily"
detail: "One point kept per day, for this many days"
options: root.retentionOptions(
"daily", root.retentionLimit(volumeCard.modelData, "daily"))
current: root.retentionLimit(volumeCard.modelData, "daily")
enabled: volumeCard.editable
onPicked: value => Snapshots.setRetention(
volumeCard.configName,
root.retentionLimit(volumeCard.modelData, "hourly"),
Number(value),
root.retentionLimit(volumeCard.modelData, "weekly"))
}
OptionPickerRow {
id: weeklyRow
width: parent.width
label: "Weekly"
detail: "One point kept per week, for this many weeks — the longest reach back"
options: root.retentionOptions(
"weekly", root.retentionLimit(volumeCard.modelData, "weekly"))
current: root.retentionLimit(volumeCard.modelData, "weekly")
enabled: volumeCard.editable
onPicked: value => Snapshots.setRetention(
volumeCard.configName,
root.retentionLimit(volumeCard.modelData, "hourly"),
root.retentionLimit(volumeCard.modelData, "daily"),
Number(value))
}
ActionRow {
label: "Take one now"
detail: "Kept until you remove it, unlike the automatic ones"
action: "Take snapshot"
enabled: !Snapshots.busy && volumeCard.modelData.readable === true
enabled: volumeCard.editable
onTriggered: Snapshots.take(volumeCard.configName, "Taken from Settings")
}
@@ -139,7 +233,6 @@ SettingsPage {
Column {
width: parent.width
visible: !root.browsingOpen
Repeater {
model: volumeCard.open
@@ -154,6 +247,9 @@ SettingsPage {
readonly property string token: volumeCard.configName + ":" + pointRow.modelData.number
readonly property bool confirming: root.confirmingDelete === pointRow.token
readonly property bool browsingThis: root.browsingOpen
&& Snapshots.browsingConfig === volumeCard.configName
&& Snapshots.browsingSnapshot === Number(pointRow.modelData.number)
width: parent.width
// A kept snapshot is one the timeline will not remove,
@@ -164,6 +260,7 @@ SettingsPage {
detail: String(pointRow.modelData.description ?? "")
+ " · #" + pointRow.modelData.number
+ (pointRow.modelData.kept ? " · kept" : "")
+ (pointRow.browsingThis ? " · open below" : "")
controlWidth: 250
divider: pointRow.index < volumeCard.shownCount - 1
@@ -173,7 +270,7 @@ SettingsPage {
spacing: 8
SettingsButton {
text: "Open"
text: pointRow.browsingThis ? "Open again" : "Open"
enabled: !Snapshots.browsing
onClicked: Snapshots.browse(volumeCard.configName,
Number(pointRow.modelData.number), "")
@@ -224,119 +321,128 @@ SettingsPage {
Text {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: volumeCard.open ? "Show fewer \u25B4" : "Show all \u25BE"
text: volumeCard.open ? "Show fewer " : "Show all "
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
}
}
}
// ── Inside one point in time ─────────────────────────────────────
// ── Inside one point in time ─────────────────────────────────────────────
//
// Its own card, rendered on the browsing state alone. Nothing about which
// volume card happens to be expanded can hide it.
SettingsCard {
visible: root.browsingOpen
title: "Browsing " + (root.browsingConfigRecord
? Snapshots.labelFor(root.browsingConfigRecord)
: Snapshots.browsingConfig)
+ " · #" + Snapshots.browsingSnapshot
subtitle: Snapshots.browsingPath === ""
? "The top of that point in time."
: "Inside " + Snapshots.browsingPath + "."
ActionRow {
width: parent.width
label: Snapshots.browsingPath === ""
? "Close this point in time"
: "…/" + Snapshots.browsingPath
detail: "Choosing Restore puts a copy back where it came from, keeping whatever is there now"
action: Snapshots.browsingPath === "" ? "Close" : "Back"
enabled: !Snapshots.browsing
onTriggered: Snapshots.browseUp()
}
TextRow {
width: parent.width
visible: Snapshots.browsing
label: "Reading the snapshot…"
detail: "Listing a folder from a point in time"
value: ""
divider: false
}
Repeater {
model: Snapshots.browsing ? [] : Snapshots.browseEntries
delegate: SettingRow {
id: entryRow
required property var modelData
required property int index
readonly property string entryPath: Snapshots.browsingPath === ""
? String(entryRow.modelData.name)
: Snapshots.browsingPath + "/" + String(entryRow.modelData.name)
readonly property bool confirming: root.confirmingRestore === entryRow.entryPath
Column {
width: parent.width
visible: volumeCard.open && root.browsingOpen
&& Snapshots.browsingConfig === volumeCard.configName
icon: entryRow.modelData.directory ? "\u{F024B}" : "\u{F0214}"
label: String(entryRow.modelData.name ?? "")
detail: entryRow.modelData.directory
? "Folder"
: Snapshots.formatBytes(entryRow.modelData.bytes ?? 0)
controlWidth: 230
divider: entryRow.index < Snapshots.browseEntries.length - 1
ActionRow {
width: parent.width
label: Snapshots.browsingPath === ""
? "Snapshot #" + Snapshots.browsingSnapshot
: "…/" + Snapshots.browsingPath
detail: "Choosing Restore puts a copy back where it came from, keeping whatever is there now"
action: "Back"
enabled: !Snapshots.browsing
onTriggered: Snapshots.browseUp()
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 8
TextRow {
width: parent.width
visible: Snapshots.browsing
label: "Reading the snapshot…"
detail: "Listing a folder from a point in time"
value: ""
}
SettingsButton {
visible: entryRow.modelData.directory === true
text: "Open"
enabled: !Snapshots.browsing
onClicked: Snapshots.browse(Snapshots.browsingConfig,
Snapshots.browsingSnapshot,
entryRow.entryPath)
}
Repeater {
model: Snapshots.browsing ? [] : Snapshots.browseEntries
SettingsButton {
text: entryRow.confirming ? "Cancel" : "Restore"
enabled: !Snapshots.busy
onClicked: root.confirmingRestore =
entryRow.confirming ? "" : entryRow.entryPath
}
delegate: SettingRow {
id: entryRow
required property var modelData
required property int index
readonly property string entryPath: Snapshots.browsingPath === ""
? String(entryRow.modelData.name)
: Snapshots.browsingPath + "/" + String(entryRow.modelData.name)
readonly property bool confirming: root.confirmingRestore === entryRow.entryPath
width: parent.width
icon: entryRow.modelData.directory ? "\u{F024B}" : "\u{F0214}"
label: String(entryRow.modelData.name ?? "")
detail: entryRow.modelData.directory
? "Folder"
: Snapshots.formatBytes(entryRow.modelData.bytes ?? 0)
controlWidth: 230
divider: entryRow.index < Snapshots.browseEntries.length - 1
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 8
SettingsButton {
visible: entryRow.modelData.directory === true
text: "Open"
enabled: !Snapshots.browsing
onClicked: Snapshots.browse(Snapshots.browsingConfig,
Snapshots.browsingSnapshot,
entryRow.entryPath)
}
SettingsButton {
text: entryRow.confirming ? "Cancel" : "Restore"
enabled: !Snapshots.busy
onClicked: root.confirmingRestore =
entryRow.confirming ? "" : entryRow.entryPath
}
SettingsButton {
visible: entryRow.confirming
text: "Put it back"
tone: "danger"
enabled: !Snapshots.busy
onClicked: {
root.confirmingRestore = "";
Snapshots.restore(Snapshots.browsingConfig,
Snapshots.browsingSnapshot,
entryRow.entryPath);
}
}
SettingsButton {
visible: entryRow.confirming
text: "Put it back"
tone: "danger"
enabled: !Snapshots.busy
onClicked: {
root.confirmingRestore = "";
Snapshots.restore(Snapshots.browsingConfig,
Snapshots.browsingSnapshot,
entryRow.entryPath);
}
}
}
TextRow {
width: parent.width
visible: !Snapshots.browsing && Snapshots.browseTruncated
label: "Only the first entries are shown"
detail: "This folder holds more than this list can usefully show"
value: ""
divider: false
}
TextRow {
width: parent.width
visible: !Snapshots.browsing && Snapshots.browseEntries.length === 0
label: "Nothing here"
detail: "This folder was empty at that point in time"
value: ""
divider: false
}
}
}
TextRow {
width: parent.width
visible: !Snapshots.browsing && Snapshots.browseTruncated
label: "Only the first entries are shown"
detail: "This folder holds more than this list can usefully show"
value: ""
divider: false
}
TextRow {
width: parent.width
visible: !Snapshots.browsing && Snapshots.browseEntries.length === 0
label: "Nothing here"
detail: "This folder was empty at that point in time"
value: ""
divider: false
}
}
// ── What it costs ────────────────────────────────────────────────────────