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:
@@ -9,6 +9,13 @@
|
||||
// be a terabyte -- so it happens on request rather than on open. The page says
|
||||
// so plainly instead of showing an empty list that reads as "nothing here".
|
||||
//
|
||||
// The cleanup card has rules, and they are the whole reason it can exist at
|
||||
// all. Every row names what it is and what is lost by clearing it. Every row
|
||||
// carries its size. Nothing is selected in advance, nothing is recommended,
|
||||
// nothing counts down, and a row with nothing in it says so and offers no
|
||||
// button. A "clean up your PC" panel that nags is a racket; this one is a list
|
||||
// of facts with a button next to each.
|
||||
//
|
||||
// Partitioning and formatting are deliberately absent; GNOME Disks is one row
|
||||
// away for that.
|
||||
|
||||
@@ -22,19 +29,25 @@ SettingsPage {
|
||||
|
||||
objectName: "storage"
|
||||
title: "Storage"
|
||||
lede: "How much space is left, what is using it, and whether the drive is healthy."
|
||||
lede: root.rootFs
|
||||
? Disks.formatBytes(root.rootFs.usedBytes) + " used of "
|
||||
+ Disks.formatBytes(root.rootFs.sizeBytes) + " — "
|
||||
+ Disks.formatBytes(root.rootFs.availBytes) + " free."
|
||||
: "How much space is left, what is using it, and whether the drive is healthy."
|
||||
|
||||
readonly property var rootFs: Disks.rootFilesystem
|
||||
readonly property var drive: Disks.primaryDrive
|
||||
|
||||
// The measured folders, as a share of the largest one, so the bars compare
|
||||
// against each other rather than against a total they do not sum to.
|
||||
readonly property real largestFolder: {
|
||||
let largest = 0;
|
||||
for (const folder of Disks.folders)
|
||||
largest = Math.max(largest, Number(folder.bytes ?? 0));
|
||||
return largest;
|
||||
}
|
||||
// Clearing something deletes it, so it never happens on a first press.
|
||||
// Holds the id of the cleanable that is one press away from running.
|
||||
property string pendingClean: ""
|
||||
|
||||
// Folder bars are drawn against the DISK, not against the largest folder.
|
||||
// Against the largest, the top row is always full and the bar says nothing
|
||||
// except which row is biggest -- which the sizes beside them already say.
|
||||
// Against the disk, the bar means the same thing as the free-space bar
|
||||
// above it, and a 212 GB folder on a 2 TB disk looks like what it is.
|
||||
readonly property real diskBytes: Number(root.rootFs?.sizeBytes ?? 0)
|
||||
|
||||
readonly property var removableDrives: Disks.drives.filter(entry => entry.removable)
|
||||
|
||||
@@ -51,12 +64,74 @@ SettingsPage {
|
||||
// ── Space ────────────────────────────────────────────────────────────────
|
||||
|
||||
SettingsCard {
|
||||
title: "Free space"
|
||||
title: "What is on this disk"
|
||||
subtitle: root.rootFs && (root.rootFs.mountpoints ?? []).length > 1
|
||||
? "One filesystem is mounted at " + Disks.mountLabel(root.rootFs)
|
||||
+ ", so they share the same space."
|
||||
: "The filesystem this session runs from."
|
||||
|
||||
// Measuring the segments walks the same folders the Folders card walks,
|
||||
// so it costs the same and is asked for the same way.
|
||||
ActionRow {
|
||||
visible: !Disks.breakdownMeasured || Disks.measuringBreakdown
|
||||
label: Disks.measuringBreakdown ? "Measuring…" : "Measure what is on this disk"
|
||||
detail: Disks.measuringBreakdown
|
||||
? "Walking your home folder, the flatpak installations, and the cache."
|
||||
: "Three of the four segments are measured by walking folders, which takes a minute."
|
||||
action: "Measure"
|
||||
enabled: !Disks.measuringBreakdown
|
||||
divider: false
|
||||
onTriggered: Disks.measureBreakdown()
|
||||
}
|
||||
|
||||
StorageBreakdownBar {
|
||||
visible: Disks.breakdownMeasured
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
// The caption belongs to the bar's arithmetic, so it lives with the
|
||||
// card rather than inside the component: home, applications and caches
|
||||
// are measured, and the fourth segment is what is left of the used
|
||||
// space once those three are subtracted. Calling that "System" would
|
||||
// blame the desktop for the user's own unscanned files.
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: Disks.breakdownMeasured
|
||||
topPadding: 12
|
||||
text: "Home, applications and caches are measured. \"System & everything else\" is"
|
||||
+ " the remainder — the packages this machine runs on, plus anything those"
|
||||
+ " three measurements did not reach."
|
||||
color: Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
// What the walk could not finish, said rather than absorbed into the
|
||||
// remainder without comment.
|
||||
TextRow {
|
||||
visible: Disks.breakdownMeasured && Disks.breakdown
|
||||
&& Disks.breakdown.complete === false
|
||||
label: "The walk ran out of time"
|
||||
detail: "Home, applications and caches are floors rather than totals, so the remainder is larger than it should be."
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Disks.breakdownMeasured && Disks.breakdown
|
||||
&& Disks.breakdown.exceedsUsed === true
|
||||
label: "The measured parts overlap"
|
||||
detail: "They add up to more than the used space, which means something was counted twice. The remainder is shown as zero rather than as a negative number."
|
||||
value: ""
|
||||
divider: false
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Free space"
|
||||
subtitle: "The number the rest of this page is about."
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 10
|
||||
@@ -118,9 +193,9 @@ SettingsPage {
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "What is using it"
|
||||
title: "Folders"
|
||||
subtitle: Disks.foldersMeasured
|
||||
? "Measured by walking each folder."
|
||||
? "Measured by walking each folder. Bars are drawn against the whole disk, so they mean the same thing as the free-space bar above."
|
||||
: "Measuring means walking every file, so it is not done automatically."
|
||||
|
||||
ActionRow {
|
||||
@@ -160,12 +235,9 @@ SettingsPage {
|
||||
radius: 2
|
||||
color: Theme.alpha(Theme.fg, 0.08)
|
||||
|
||||
// Relative to the LARGEST folder, not to the drive. At this
|
||||
// scale one folder holds almost everything, so bars drawn
|
||||
// against the total would leave every other row invisible.
|
||||
Rectangle {
|
||||
width: root.largestFolder > 0
|
||||
? Math.max(parent.width * (Number(folderBlock.modelData.bytes ?? 0) / root.largestFolder), 2)
|
||||
width: root.diskBytes > 0
|
||||
? Math.max(parent.width * (Number(folderBlock.modelData.bytes ?? 0) / root.diskBytes), 2)
|
||||
: 0
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
@@ -182,26 +254,98 @@ SettingsPage {
|
||||
label: "Some folders were not measured"
|
||||
detail: "The walk ran out of time before reaching them, so this list is incomplete."
|
||||
value: ""
|
||||
divider: Disks.containers !== null
|
||||
divider: false
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reclaiming space, without the racket ─────────────────────────────────
|
||||
|
||||
SettingsCard {
|
||||
title: "Clean up, honestly"
|
||||
subtitle: "Each row says what it is and what clearing it costs. Nothing here is chosen for you, and nothing on this page needs doing."
|
||||
|
||||
ActionRow {
|
||||
visible: Disks.containers !== null
|
||||
&& Number(Disks.containers?.reclaimableBytes ?? 0) > 0
|
||||
label: "Unused container images"
|
||||
detail: Disks.containers
|
||||
? Disks.formatBytes(Disks.containers.reclaimableBytes)
|
||||
+ " of " + Disks.formatBytes(Disks.containers.totalBytes)
|
||||
+ " is not used by any container"
|
||||
: ""
|
||||
action: "Show"
|
||||
visible: !Disks.cleanablesMeasured || Disks.measuringCleanables
|
||||
label: Disks.measuringCleanables ? "Measuring…" : "Measure what could be cleared"
|
||||
detail: "Measuring only counts bytes. Every row below is cleared by its own button, one at a time."
|
||||
action: "Measure"
|
||||
enabled: !Disks.measuringCleanables
|
||||
divider: Disks.cleanablesMeasured
|
||||
onTriggered: Disks.measureCleanables()
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Disks.cleanables ?? []
|
||||
|
||||
delegate: SettingRow {
|
||||
id: cleanRow
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property string cleanId: String(cleanRow.modelData.id ?? "")
|
||||
readonly property real bytes: Number(cleanRow.modelData.bytes ?? 0)
|
||||
readonly property bool empty: cleanRow.bytes <= 0
|
||||
readonly property bool privileged: cleanRow.modelData.privileged === true
|
||||
readonly property bool confirming: root.pendingClean !== ""
|
||||
&& root.pendingClean === cleanRow.cleanId
|
||||
|
||||
width: parent.width
|
||||
label: String(cleanRow.modelData.label ?? "")
|
||||
detail: cleanRow.empty
|
||||
? String(cleanRow.modelData.detail ?? "") + " · empty, nothing to do"
|
||||
: (cleanRow.confirming
|
||||
? "This deletes it now. Nothing here is recoverable from the trash afterwards."
|
||||
: String(cleanRow.modelData.detail ?? "")
|
||||
+ (cleanRow.privileged ? " · the system will ask for your password" : ""))
|
||||
value: cleanRow.empty ? Disks.formatBytes(0) : ""
|
||||
controlWidth: cleanRow.empty ? 90 : 210
|
||||
divider: cleanRow.index < (Disks.cleanables ?? []).length - 1
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: cleanRow.bytes > 0
|
||||
spacing: 9
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Disks.formatBytes(cleanRow.bytes)
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: cleanRow.confirming
|
||||
text: "Clear it"
|
||||
tone: "danger"
|
||||
enabled: Disks.cleaningId === ""
|
||||
onClicked: {
|
||||
root.pendingClean = "";
|
||||
Disks.clean(cleanRow.cleanId);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: cleanRow.confirming ? "Keep" : "Clear…"
|
||||
enabled: Disks.cleaningId === ""
|
||||
onClicked: root.pendingClean =
|
||||
cleanRow.confirming ? "" : cleanRow.cleanId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Disks.cleanablesMeasured && (Disks.cleanables ?? []).length === 0
|
||||
label: "Nothing itemized"
|
||||
detail: "Caches, the trash, unused Flatpak runtimes and the package download cache were all measured and none of them holds anything."
|
||||
value: ""
|
||||
divider: false
|
||||
// Reclaiming is not offered here on purpose: pruning images can
|
||||
// destroy work that lives outside this desktop, and a settings pane
|
||||
// should not put that one click deep. This opens a terminal showing
|
||||
// what would be reclaimed, and leaves the decision there.
|
||||
onTriggered: Quickshell.execDetached(
|
||||
["kitty", "--hold", "-e", "podman", "system", "df"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,6 +425,11 @@ SettingsPage {
|
||||
// Named here because this is the page somebody opens when space is short,
|
||||
// and container images are usually the largest thing nobody remembers
|
||||
// having. Only shown when there is actually something to reclaim.
|
||||
//
|
||||
// This is the ONLY container affordance on the page. There used to be a
|
||||
// second one above that opened a terminal running `podman system df`, which
|
||||
// meant two rows about the same gigabytes leading two different places --
|
||||
// and the terminal one could not actually reclaim anything.
|
||||
SettingsCard {
|
||||
visible: Containers.available && Containers.reclaimable > 0
|
||||
title: "Containers are holding " + Containers.formatBytes(Containers.reclaimable)
|
||||
|
||||
Reference in New Issue
Block a user