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:
@@ -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 ────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user