Coherence: one component per idea, one spelling per word, one truth per claim

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-25 02:21:45 -04:00
parent 06c53d6c21
commit bd2fd12de2
87 changed files with 660 additions and 766 deletions
@@ -3,7 +3,7 @@
// This is the fine-tune behind a disclosure, not the fast path: the four wells
// above cover what most edits are, and these six rows are for the last few
// degrees. Each row is a labelled, keyboard-operable slider with a numeric
// readout, because a hue ring alone tells someone with a colour vision
// readout, because a hue ring alone tells someone with a color vision
// deficiency nothing.
//
// Committed on a debounce rather than per move. Each move used to write BOTH
@@ -140,27 +140,27 @@ Column {
HsvRow {
target: "primary"; channel: "h"; channelValue: editor.primaryHsv.h; channelMaximum: 360
suffix: "°"; label: "Primary hue"; detail: "Colour family, measured from 0 to 360 degrees"
suffix: "°"; label: "Primary hue"; detail: "Color family, measured from 0 to 360 degrees"
}
HsvRow {
target: "primary"; channel: "s"; channelValue: editor.primaryHsv.s; channelMaximum: 100
label: "Primary saturation"; detail: "Colour intensity from grey to vivid"
label: "Primary saturation"; detail: "Color intensity from grey to vivid"
}
HsvRow {
target: "primary"; channel: "v"; channelValue: editor.primaryHsv.v; channelMaximum: 100
label: "Primary value"; detail: "Brightness from black to full colour"
label: "Primary value"; detail: "Brightness from black to full color"
}
HsvRow {
target: "secondary"; channel: "h"; channelValue: editor.secondaryHsv.h; channelMaximum: 360
suffix: "°"; label: "Secondary hue"; detail: "Colour family at the far end of the Prism gradient"
suffix: "°"; label: "Secondary hue"; detail: "Color family at the far end of the Prism gradient"
}
HsvRow {
target: "secondary"; channel: "s"; channelValue: editor.secondaryHsv.s; channelMaximum: 100
label: "Secondary saturation"; detail: "Colour intensity from grey to vivid"
label: "Secondary saturation"; detail: "Color intensity from grey to vivid"
}
HsvRow {
target: "secondary"; channel: "v"; channelValue: editor.secondaryHsv.v; channelMaximum: 100
label: "Secondary value"; detail: "Brightness from black to full colour"
label: "Secondary value"; detail: "Brightness from black to full color"
divider: false
}
@@ -176,7 +176,7 @@ Column {
}
// Hands the sliders back to the stored pair. If the write was refused they
// snap back to what is really in effect rather than showing a colour
// snap back to what is really in effect rather than showing a color
// nothing accepted.
Timer {
id: releaseTimer
@@ -1,6 +1,6 @@
// Accessibility.
//
// Organised by what a person came here unable to do -- see it, tolerate the
// Organized by what a person came here unable to do -- see it, tolerate the
// motion, hear it, reach it from the keyboard, have it read aloud -- rather
// than by which subsystem happens to implement each control. The old page was
// grouped by mechanism (Pointer / Text / Motion / Magnifier / Contrast), which
@@ -112,7 +112,11 @@ SettingsPage {
// the preference at launch, and re-applying the value it is already
// running would be a hyprctl call for nothing every time this page
// opens.
ChoiceRow {
//
// A picker rather than a segmented control: five options with a
// sentence under each get ninety pixels apiece in a strip, which is
// room for neither.
OptionPickerRow {
id: colorFilterRow
property string appliedFilter: ""
@@ -164,26 +168,21 @@ SettingsPage {
title: "Hearing"
subtitle: "What the desktop does instead of making a sound."
ToggleRow { setting: "visualAlerts" }
ToggleRow { setting: "visualAlerts"; divider: false }
SettingRow {
label: "Mono audio"
detail: "Not offered yet — folding stereo into one channel is a real change to the PipeWire graph, and a switch that only looked like it did that would be worse than its absence. Balance lives on the Sound page meanwhile."
controlWidth: 90
divider: false
Item { width: 1; height: 10 }
SoundBadge {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: "Not yet"
tone: Theme.fgMuted
}
// Said the same way the keyboard card says its absence, rather than as
// a row wearing a badge that stands for a control it never had.
SettingsNote {
headline: "Mono audio is not offered yet"
body: "Folding stereo into one channel is a real change to the PipeWire graph, and a switch that only looked like it did that would be worse than its absence. Balance lives on the Sound page meanwhile."
}
}
SettingsCard {
title: "Keyboard & pointer"
subtitle: "Key behaviour lives with the Keyboard settings; what the pointer does when you stop moving it lives here."
subtitle: "Key behavior lives with the Keyboard settings; what the pointer does when you stop moving it lives here."
ActionRow {
label: "Key repeat"
@@ -219,7 +218,7 @@ SettingsPage {
anchors.verticalCenter: parent.verticalCenter
spacing: 9
SoundBadge {
StatusBadge {
anchors.verticalCenter: parent.verticalCenter
text: Accessibility.orcaRunning ? "Running" : "Stopped"
tone: Accessibility.orcaRunning ? Theme.ok : Theme.fgMuted
@@ -239,12 +238,12 @@ SettingsPage {
}
SettingRow {
label: "This app, read aloud"
label: "This application, read aloud"
detail: "Every settings row carries a spoken name and takes keyboard focus — Tab walks the page, Space flips a switch, the arrows move a slider or step a choice"
controlWidth: 90
divider: false
SoundBadge {
StatusBadge {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: "Built in"
@@ -20,7 +20,7 @@ SettingsPage {
property string expandedPicker: ""
// Whether the accent's HSV fine-tune is open. Collapsed by default: it is
// six sliders that answer a question the four colour wells above have
// six sliders that answer a question the four color wells above have
// usually already answered.
property bool fineTuneOpen: false
@@ -270,7 +270,7 @@ SettingsPage {
Text {
width: parent.width
text: "Primary, secondary, background, foreground, effects — every colour yours, saved as a theme that lives in the galleries above"
text: "Primary, secondary, background, foreground, effects — every color yours, saved as a theme that lives in the galleries above"
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
@@ -316,15 +316,10 @@ SettingsPage {
: "Based on " + ThemeProfiles.activeProfile.name
+ " · every change previews live on the real desktop"
Text {
width: parent.width
visible: wells.lastError !== "" || ColorScheme.lastError !== ""
text: wells.lastError !== "" ? wells.lastError : ColorScheme.lastError
color: Theme.warn
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
bottomPadding: 10
ErrorRow {
label: "The theme needs attention"
message: wells.lastError !== "" ? wells.lastError : ColorScheme.lastError
divider: true
}
ThemeStartChips {
@@ -422,7 +417,7 @@ SettingsPage {
SettingRow {
label: "Application"
detail: "GTK and libadwaita apps that follow the desktop default"
detail: "GTK and libadwaita applications that follow the desktop default"
divider: false
controlWidth: 8
}
@@ -268,24 +268,20 @@ SettingsPage {
// a process each and only this page wants them, so a shell start where
// nobody opens Applications pays for neither.
Component.onCompleted: {
AppLibrary.refresh();
AppLibrary.refreshCatalog();
if (!AppLibrary.flatpaksLoaded)
AppLibrary.refresh();
if (!AppLibrary.catalogLoaded)
AppLibrary.refreshCatalog();
}
TextRow {
visible: DefaultApps.lastError !== ""
ErrorRow {
label: "Application settings need attention"
detail: DefaultApps.lastError
value: ""
divider: false
message: DefaultApps.lastError
}
TextRow {
visible: String(AppLibrary.lastError ?? "") !== ""
ErrorRow {
label: "The application list needs attention"
detail: String(AppLibrary.lastError ?? "")
value: ""
divider: false
message: String(AppLibrary.lastError ?? "")
}
// ── What is installed ────────────────────────────────────────────────────
@@ -361,7 +357,7 @@ SettingsPage {
TextRow {
visible: root.installedApps.length > root.installedLimit
label: (root.installedApps.length - root.installedLimit) + " more"
detail: "Search finds any of them by name or by application id."
detail: "Search finds any of them by name or by application id"
value: ""
divider: false
}
@@ -369,7 +365,7 @@ SettingsPage {
TextRow {
visible: (AppLibrary.apps ?? []).length > 0 && root.installedApps.length === 0
label: "Nothing matches that"
detail: "Both the application's name and its id are searched."
detail: "Both the application's name and its id are searched"
value: ""
divider: false
}
@@ -449,7 +445,7 @@ SettingsPage {
label: AppLibrary.catalogLoaded
? "The catalog is empty"
: "Reading the catalog…"
detail: "It is parsed from setup/packages/extras, the same files the command line uses."
detail: "It is parsed from setup/packages/extras, the same files the command line uses"
value: ""
divider: false
}
@@ -547,7 +543,7 @@ SettingsPage {
TextRow {
visible: !DefaultApps.busy && DefaultApps.autostartEntries.length === 0
label: "No user autostart entries"
detail: "Applications can add entries to ~/.config/autostart."
detail: "Applications can add entries to ~/.config/autostart"
value: ""
}
@@ -612,7 +608,7 @@ SettingsPage {
// once at launch and give no sign that nothing had happened.
TextRow {
label: "Compositor autostart"
detail: "Started from the Hyprland configuration, and read-only here."
detail: "Started from the Hyprland configuration, and read-only here"
value: DefaultApps.luaAutostartEntries.length > 0
? DefaultApps.luaAutostartEntries.length
+ (DefaultApps.luaAutostartEntries.length === 1 ? " entry" : " entries")
@@ -622,7 +618,7 @@ SettingsPage {
TextRow {
visible: !DefaultApps.busy && DefaultApps.luaAutostartEntries.length === 0
label: "No compositor entries found"
detail: "No hl.exec_cmd entries were found in config/dot/hypr/autostart.lua."
detail: "No hl.exec_cmd entries were found in config/dot/hypr/autostart.lua"
value: ""
divider: false
}
@@ -175,7 +175,7 @@ Rectangle {
clip: true
Accessible.role: Accessible.EditableText
Accessible.name: root.role + " colour, hex"
Accessible.name: root.role + " color, hex"
// Seeded once, then written only by the guarded handlers
// below and by onSwatchColorChanged. A binding here would
@@ -212,7 +212,7 @@ Rectangle {
IconButton {
glyph: "◐"
title: "Open the colour wheel for " + root.role
title: "Open the color wheel for " + root.role
onTriggered: root.wheelRequested()
}
@@ -118,7 +118,8 @@ SettingsPage {
NetworkTools.active = true;
if (!WifiShare.scanned)
WifiShare.refresh();
Vpn.refresh();
if (!Vpn.scanned)
Vpn.refresh();
}
Component.onDestruction: {
Connectivity.active = false;
@@ -137,12 +138,9 @@ SettingsPage {
NetworkTools.refreshDetails(root.wiredConnection);
}
TextRow {
visible: NetworkTools.lastError !== ""
ErrorRow {
label: "The network needs attention"
detail: NetworkTools.lastError
value: ""
divider: false
message: NetworkTools.lastError
}
// ── Wired ────────────────────────────────────────────────────────────────
@@ -406,12 +404,11 @@ SettingsPage {
title: "VPN"
subtitle: "WireGuard and OpenVPN profiles NetworkManager holds for you."
TextRow {
ErrorRow {
width: parent.width
visible: Vpn.lastError !== ""
label: "The last VPN action did not finish"
detail: Vpn.lastError
value: ""
message: Vpn.lastError
divider: true
}
Repeater {
@@ -61,22 +61,19 @@ SettingsPage {
}).join(", ");
}
Component.onCompleted: Containers.refresh()
Component.onCompleted: if (!Containers.scanned) Containers.refresh()
// ── the list ────────────────────────────────────────────────────────────
TextRow {
visible: Containers.lastError !== ""
ErrorRow {
label: "That did not work"
detail: Containers.lastError
value: ""
divider: false
message: Containers.lastError
}
TextRow {
visible: Containers.scanned && !Containers.available
label: "podman is not available"
detail: "Nothing here can be shown until podman is installed."
detail: "Nothing here can be shown until podman is installed"
value: ""
divider: false
}
@@ -449,7 +446,7 @@ SettingsPage {
TextRow {
visible: Containers.scanned && Containers.available && Containers.total === 0
label: "No containers"
detail: "Nothing has been created on this machine yet."
detail: "Nothing has been created on this machine yet"
value: ""
divider: false
}
@@ -20,7 +20,7 @@ Item {
// A Canvas reads Theme inside onPaint, where nothing records a dependency
// on it, so a palette change would leave the ring drawn in the old warn
// tone until the next tick -- or for good, on a countdown sitting still.
// Naming the colour as a property gives the repaint something to watch.
// Naming the color as a property gives the repaint something to watch.
readonly property color ringColor: Theme.warn
onSecondsLeftChanged: ring.requestPaint()
@@ -7,7 +7,7 @@
// one of the compositor's window verbs.
//
// Nothing typed here becomes a command. The editor reports an enum kind and a
// target that Keybinds.describeAction() already recognises; hypr/actions.lua
// target that Keybinds.describeAction() already recognizes; hypr/actions.lua
// resolves that to something runnable through whitelist tables of its own. That
// is the whole reason there is no "run this command" option: settings.json has
// to stay a file it is safe to hand somebody.
@@ -116,7 +116,7 @@ Column {
ChoiceGrid {
width: parent.width
label: "What it does"
detail: "Three kinds, and only three: Panama resolves the name you pick when you press the keys, so no shortcut ever stores a command."
detail: "Three kinds, and only three: Panama resolves the name you pick when you press the keys, so no shortcut ever stores a command"
current: root.kind
options: [
{ value: "app", label: "Launch an application" },
@@ -185,7 +185,7 @@ Column {
width: parent.width
visible: root.kind === "shell"
label: "Shell action"
detail: "The shell's own surfaces, each one something Panama already answers over IPC."
detail: "The shell's own surfaces, each one something Panama already answers over IPC"
current: root.target
options: root.shellOptions
divider: false
@@ -199,7 +199,7 @@ Column {
width: parent.width
visible: root.kind === "window"
label: "Window & workspace"
detail: "Compositor verbs: the first three act on the focused window, the rest go to a workspace."
detail: "Compositor verbs: the first three act on the focused window, the rest go to a workspace"
current: root.target
options: root.windowOptions
divider: false
@@ -385,10 +385,9 @@ SettingsPage {
// measurement system and not its page size, and deriving Letter from a
// country list would be Panama inventing an answer glibc already holds
// -- a preview that guesses is worse than a preview that stops.
TextRow {
NotMeasuredRow {
label: "Paper size is not previewed"
detail: "Qt can report a locale's measurement system but not its page size, and guessing it from the country would be this page making something up."
value: ""
because: "Qt can report a locale's measurement system but not its page size, and guessing it from the country would be this page making something up"
divider: false
}
}
@@ -507,12 +507,9 @@ SettingsPage {
divider: Dictation.lastError !== ""
}
TextRow {
visible: Dictation.lastError !== ""
ErrorRow {
label: "Problem"
detail: Dictation.lastError
value: ""
divider: false
message: Dictation.lastError
}
}
@@ -10,7 +10,7 @@
// picture, and whether this is the one the panel was built for.
//
// Applying is the page's job. Every per-display edit on the Displays page goes
// through one funnel so the whole record -- position, colour, mirror state --
// through one funnel so the whole record -- position, color, mirror state --
// rides the same keep-or-revert transaction; this reports which resolution was
// asked for and lets that funnel do the rest.
@@ -516,17 +516,7 @@ SettingsPage {
}
// ── Everything that belongs to no display in particular ─────────────────
Text {
width: parent.width
text: "All displays"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
font.capitalization: Font.AllUppercase
font.letterSpacing: 0.7
topPadding: 6
}
SectionLabel { text: "All displays" }
// Two cards side by side while there is room for two, and one above the
// other when there is not. This window is tiled: its width is anywhere from
@@ -1,7 +1,7 @@
// Dock.
//
// The pinned applications come first because they are what the Dock is; the
// behaviour card below is how it gets out of the way. The dock timings used to
// behavior card below is how it gets out of the way. The dock timings used to
// be shown here as text -- "Instant", "250 ms" -- even though they were already
// stored, mutable integers. They are controls now.
@@ -80,35 +80,18 @@ SettingsPage {
Repeater {
model: Quickshell.screens
delegate: Rectangle {
delegate: SettingsChip {
id: screenPill
required property var modelData
readonly property string screenName: String(screenPill.modelData.name ?? "")
// An empty list means all, so every pill reads as on.
readonly property bool on: Settings.dockScreens.length === 0
text: screenPill.screenName
// An empty list means all, so every chip reads as on.
active: Settings.dockScreens.length === 0
|| Settings.dockScreens.indexOf(screenPill.screenName) >= 0
width: pillLabel.implicitWidth + 20
height: 28
radius: 8
color: screenPill.on ? Theme.alpha(Theme.accent, 0.22)
: Theme.alpha(Theme.fg, 0.06)
border.width: screenPill.on ? 1 : 0
border.color: Theme.alpha(Theme.accent, 0.5)
Text {
id: pillLabel
anchors.centerIn: parent
text: screenPill.screenName
color: screenPill.on ? Theme.fg : Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
HoverHandler { cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: root.toggleDockScreen(screenPill.screenName) }
onClicked: root.toggleDockScreen(screenPill.screenName)
}
}
}
@@ -122,7 +122,7 @@ Column {
width: parent.width
visible: !root.busy && root.truncated
label: "Only the closest matches are shown"
detail: "Narrow the search to reach the rest."
detail: "Narrow the search to reach the rest"
value: ""
divider: false
}
@@ -132,7 +132,7 @@ Column {
visible: !root.busy && root.query.length >= root.minimumQuery
&& root.matches.length === 0
label: "No file type matches that"
detail: "Only types this system knows about can be given a handler."
detail: "Only types this system knows about can be given a handler"
value: ""
divider: false
}
@@ -12,7 +12,6 @@
import Quickshell
import QtQuick
import qs.config
import qs.services
SettingsPage {
@@ -133,14 +132,11 @@ SettingsPage {
return text;
}
Component.onCompleted: Firewall.refresh()
Component.onCompleted: if (!Firewall.scanned) Firewall.refresh()
TextRow {
visible: Firewall.lastError !== ""
ErrorRow {
label: "The firewall needs attention"
detail: Firewall.lastError
value: ""
divider: false
message: Firewall.lastError
}
// ── The finding, when there is one ───────────────────────────────────────
@@ -378,7 +374,7 @@ SettingsPage {
: (root.addKind === "service"
? "That is not a service name firewalld would accept"
: "That is not a port firewalld would accept")
detail: "This writes a permanent rule — the system will ask for your password."
detail: "This writes a permanent rule — the system will ask for your password"
action: "Allow"
enabled: root.addValid && !Firewall.busy
divider: false
@@ -565,43 +561,16 @@ SettingsPage {
Repeater {
model: Firewall.allZones ?? []
delegate: Rectangle {
delegate: SettingsChip {
id: chip
required property var modelData
readonly property string zoneName: String(chip.modelData)
readonly property bool current: root.browsingZone === chip.zoneName
width: chipLabel.implicitWidth + 20
height: 26
radius: Theme.pillRadius
color: chip.current
? Theme.alpha(Theme.accent, 0.18)
: Theme.alpha(Theme.fg, chipHover.hovered ? 0.11 : 0.055)
border.width: 1
border.color: chip.current
? Theme.alpha(Theme.accent, 0.45)
: Theme.alpha(Theme.fg, 0.09)
Text {
id: chipLabel
anchors.centerIn: parent
text: chip.zoneName
color: chip.current ? Theme.fg : Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: chip.current ? Font.DemiBold : Font.Normal
}
HoverHandler {
id: chipHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: root.browsingZone = chip.current ? "" : chip.zoneName
}
text: chip.zoneName
active: root.browsingZone === chip.zoneName
onClicked: root.browsingZone = chip.active ? "" : chip.zoneName
}
}
}
@@ -85,7 +85,7 @@ Column {
Text {
width: parent.width
text: "Apps that break through while this mode is on"
text: "Applications that break through while this mode is on"
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
@@ -156,37 +156,15 @@ Column {
}
}
Rectangle {
SettingsChip {
id: addChip
height: 26
width: addLabel.implicitWidth + 20
radius: Theme.pillRadius
color: Theme.alpha(Theme.fg, addHover.hovered ? 0.1 : 0.05)
border.width: 1
border.color: Theme.alpha(Theme.fg, 0.2)
text: root.adding ? "Cancel" : "+ Add application"
Text {
id: addLabel
anchors.centerIn: parent
text: root.adding ? "Cancel" : "+ Add app"
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
HoverHandler {
id: addHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: {
root.adding = !root.adding;
if (!root.adding)
search.text = "";
}
onClicked: {
root.adding = !root.adding;
if (!root.adding)
search.text = "";
}
}
}
@@ -215,7 +193,7 @@ Column {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
placeholder: "Search apps"
placeholder: "Search applications"
}
}
@@ -90,7 +90,7 @@ Column {
case "workspace":
return "on workspace " + String(root.trigger.id ?? 1);
case "fullscreen":
return "when an app is fullscreen";
return "when an application is fullscreen";
default:
return "manually";
}
@@ -111,7 +111,7 @@ Column {
+ (root.allow.length === 0
? ""
: " except " + root.allow.length
+ (root.allow.length === 1 ? " app" : " apps")));
+ (root.allow.length === 1 ? " application" : " applications")));
}
if (root.mode.keepAwake === true)
effects.push(hypothetical ? "would keep the screen awake" : "keeps the screen awake");
@@ -349,7 +349,7 @@ Column {
},
{
value: "fullscreen",
label: "When an app is fullscreen",
label: "When an application is fullscreen",
detail: "On while a window is fullscreen on the focused display"
}
]
@@ -398,41 +398,30 @@ Column {
spacing: 5
Repeater {
// `name` is what a screen reader says: four of the seven
// initials repeat, so the letter alone is not a day.
model: [
{ value: 1, label: "M" }, { value: 2, label: "T" },
{ value: 3, label: "W" }, { value: 4, label: "T" },
{ value: 5, label: "F" }, { value: 6, label: "S" },
{ value: 0, label: "S" }
{ value: 1, label: "M", name: "Monday" },
{ value: 2, label: "T", name: "Tuesday" },
{ value: 3, label: "W", name: "Wednesday" },
{ value: 4, label: "T", name: "Thursday" },
{ value: 5, label: "F", name: "Friday" },
{ value: 6, label: "S", name: "Saturday" },
{ value: 0, label: "S", name: "Sunday" }
]
delegate: Rectangle {
// Seven of them across, so they share one width rather than
// each taking the width of its own letter.
delegate: SettingsChip {
id: dayPill
required property var modelData
readonly property bool on:
(root.trigger.days ?? []).indexOf(dayPill.modelData.value) >= 0
width: 30
height: 28
radius: 8
color: dayPill.on ? Theme.alpha(Theme.accent, 0.22)
: Theme.alpha(Theme.fg, 0.06)
border.width: 1
border.color: dayPill.on ? Theme.alpha(Theme.accent, 0.5)
: Theme.alpha(Theme.fg, 0.08)
Text {
anchors.centerIn: parent
text: String(dayPill.modelData.label)
color: dayPill.on ? Theme.fg : Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: dayPill.on ? Font.DemiBold : Font.Medium
}
HoverHandler { cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: root.toggleDay(dayPill.modelData.value) }
text: String(dayPill.modelData.label)
active: (root.trigger.days ?? []).indexOf(dayPill.modelData.value) >= 0
Accessible.name: String(dayPill.modelData.name)
onClicked: root.toggleDay(dayPill.modelData.value)
}
}
}
@@ -42,7 +42,7 @@ SettingsPage {
TextRow {
visible: FocusModes.modes.length === 0
label: "No focus modes"
detail: "Add one below and give it something to react to."
detail: "Add one below and give it something to react to"
divider: false
}
@@ -25,17 +25,15 @@ SettingsPage {
// Polling only while this page is on screen.
Component.onCompleted: {
Gaming.refresh();
if (!Gaming.scanned)
Gaming.refresh();
Gaming.watching = true;
}
Component.onDestruction: Gaming.watching = false
TextRow {
visible: Gaming.lastError !== ""
ErrorRow {
label: "Gaming needs attention"
detail: Gaming.lastError
value: ""
divider: false
message: Gaming.lastError
}
// ── Right now ────────────────────────────────────────────────────────────
@@ -46,8 +46,8 @@ Item {
// nothing in common but the digits.
property string readout: ""
// Two or three colours, left to right. Painted across the whole track when
// `fullTrack` is set -- for a value whose range is the point, like colour
// Two or three colors, left to right. Painted across the whole track when
// `fullTrack` is set -- for a value whose range is the point, like color
// temperature -- and as the fill alone otherwise.
property var trackColors: [Theme.accent, Theme.accentSecondary]
property bool fullTrack: false
@@ -12,7 +12,7 @@ SettingsPage {
readonly property string ledeText: {
if (Health.checks.length === 0)
return "Checks the parts of the desktop this app owns, and explains what needs attention.";
return "Checks the parts of the desktop this application owns, and explains what needs attention.";
const count = Health.checks.length + (Health.checks.length === 1 ? " check" : " checks");
if (root.issueChecks.length === 0)
return count + " · everything healthy.";
@@ -43,7 +43,7 @@ SettingsPage {
{
group: "panama-tools",
title: "Desktop tools",
subtitle: "Tracked links, launcher commands, apps, and inhibitors."
subtitle: "Tracked links, launcher commands, applications, and inhibitors."
}
]
readonly property var applicationTargets: ({
@@ -432,14 +432,14 @@ SettingsPage {
// umbrella button reading "Open GNOME Settings", which landed on the System
// panel. That button was the last door of its kind, and by the end it was
// pointing at a house Panama had bought: Users, Sharing, Printers, Online
// Accounts, Privacy, Region, Colour and the whole of Connections are pages
// Accounts, Privacy, Region, Color and the whole of Connections are pages
// here now. A generic front door to a settings app you no longer need is
// not a boundary, it is a habit -- so it is gone, and gnome-handoff-contract
// holds the door shut by naming `system` in its OWNED map.
//
// Screen time is the exception, and it is a real one: GNOME's wellbeing
// panel does something Panama does not, and that button genuinely works.
// Colour profiles used to sit here too, with a detail line explaining that
// Color profiles used to sit here too, with a detail line explaining that
// pressing the button changed nothing, because the colord daemon that
// applies an ICC profile is not running under Hyprland. A handoff that
// documents its own uselessness is a dead button with an apology attached,
@@ -18,7 +18,7 @@ SettingsCard {
case "unavailable":
return Health.lastError || "The latest health check could not be completed.";
case "checking":
return "Checking the desktop services, tools, and integrations this app owns.";
return "Checking the desktop services, tools, and integrations this application owns.";
case "error":
return root.observationCount === 1
? "One part of the desktop needs action."
@@ -126,27 +126,13 @@ Rectangle {
elide: removeConfirm.armed ? Text.ElideNone : Text.ElideRight
}
Rectangle {
StatusBadge {
anchors.left: aliasFrame.left
anchors.bottom: parent.bottom
anchors.bottomMargin: 10
width: badgeCopy.implicitWidth + 14
height: 21
radius: Theme.pillRadius
visible: root.featured
color: Theme.alpha(Theme.accent, 0.1)
border.width: 1
border.color: Theme.alpha(Theme.accent, 0.2)
Text {
id: badgeCopy
anchors.centerIn: parent
text: "Control Center"
color: Theme.accent
font.family: Theme.fontFamily
font.pixelSize: 9
font.weight: Font.DemiBold
}
text: "Control Center"
tone: Theme.accent
}
ConfirmAction {
@@ -93,25 +93,11 @@ Rectangle {
elide: Text.ElideRight
}
Rectangle {
StatusBadge {
visible: root.featured
anchors.verticalCenter: parent.verticalCenter
width: featuredCaption.implicitWidth + 16
height: 19
radius: Theme.pillRadius
color: Theme.alpha(Theme.accent, 0.1)
border.width: 1
border.color: Theme.alpha(Theme.accent, 0.2)
Text {
id: featuredCaption
anchors.centerIn: parent
text: "Control Center"
color: Theme.accent
font.family: Theme.fontFamily
font.pixelSize: 9
font.weight: Font.DemiBold
}
text: "Control Center"
tone: Theme.accent
}
}
@@ -247,7 +247,7 @@ SettingsPage {
TextRow {
visible: root.findings.length === 0
label: "Nothing needs your attention"
detail: "Anything worth acting on would appear here."
detail: "Anything worth acting on would appear here"
value: ""
divider: false
}
@@ -103,7 +103,7 @@ Column {
return name === "" ? "" : Quickshell.iconPath(name, true);
}
// A stable colour per application, so the tile keeps its identity as the
// A stable color per application, so the tile keeps its identity as the
// list re-sorts or the search narrows it.
readonly property color tileColor: {
const palette = [Theme.accent, Theme.teal, Theme.magenta, Theme.cyan,
@@ -190,31 +190,12 @@ Column {
// Where it came from, said once and in the same place on every
// row -- the difference decides what the body can offer.
Rectangle {
StatusBadge {
id: badge
anchors.verticalCenter: parent.verticalCenter
width: badgeText.implicitWidth + 14
height: 17
radius: Theme.pillRadius
color: root.flatpak
? Theme.alpha(Theme.cyan, 0.09)
: Theme.alpha(Theme.fgMuted, 0.12)
border.width: 1
border.color: root.flatpak
? Theme.alpha(Theme.cyan, 0.28)
: Theme.alpha(Theme.fgMuted, 0.3)
Text {
id: badgeText
anchors.centerIn: parent
text: root.flatpak ? "FLATPAK" : "SYSTEM"
color: root.flatpak ? Theme.cyan : Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Math.max(8, Theme.fontSizeSmall - 2)
font.weight: Font.DemiBold
}
text: root.flatpak ? "Flatpak" : "System"
tone: root.flatpak ? Theme.cyan : Theme.fgMuted
}
}
@@ -91,7 +91,7 @@ SettingsPage {
// The scheme is Panama's own, so a chapter can never accidentally
// hand an http link to openSettings, and an unknown page cannot be
// smuggled in either: SettingsRoutes.resolve turns anything it does
// not recognise into Home rather than a blank loader. A fragment,
// not recognize into Home rather than a blank loader. A fragment,
// where there is one, is the section within the page.
onLinkActivated: link => {
const target = String(link ?? "");
@@ -18,8 +18,10 @@
// knobs come back up with it: they are about gestures, not about the touchpad.
import QtQuick
import Quickshell
import qs.config
import qs.services
import qs.modules.clipboard
SettingsPage {
id: root
@@ -99,7 +101,7 @@ SettingsPage {
ToggleRow { setting: "leftHanded" }
ToggleRow {
setting: "middleClickPaste"
detail: "Paste the primary selection in GTK and native Wayland applications; individual apps may choose not to support it"
detail: "Paste the primary selection in GTK and native Wayland applications; individual applications may choose not to support it"
}
OptionPickerRow {
@@ -170,14 +172,19 @@ SettingsPage {
Keybinds.applyReload();
}
// Nothing, then the shell's own actions, then the compositor's window
// verbs -- the same lists the shortcut editor offers, read from Keybinds so
// this page never becomes a second opinion on what resolves.
// Nothing, then an application, then the shell's own actions, then the
// compositor's window verbs -- the same lists the shortcut editor offers,
// read from Keybinds so this page never becomes a second opinion on what
// resolves.
//
// Applications are deliberately absent: assigning one means searching
// several hundred desktop entries, which a picker of this shape cannot do.
// A gesture that already holds an application (written by the shortcut
// vocabulary elsewhere) still shows, so it can be read and cleared.
// Applications cannot be listed inline: a normal machine has several
// hundred desktop entries and a picker of this shape would be a page nobody
// can read. So the option is a door rather than a list -- picking it opens
// the same search the shortcut editor uses, below the row. A gesture that
// already holds an application shows it too, so it can be read and cleared
// without going back through the search.
readonly property string appPickerValue: "pick-application"
function gestureOptions(key: string): var {
const out = [{ value: "", label: "Nothing", detail: "The swipe passes through to whatever is under it" }];
const stored = DesktopPreferences.get(key);
@@ -187,6 +194,11 @@ SettingsPage {
label: String(stored.label),
detail: "Application · launch-or-focus"
});
out.push({
value: root.appPickerValue,
label: "Launch an application…",
detail: "Search everything installed, the way a custom shortcut does"
});
for (const action of Keybinds.shellActions)
out.push({ value: "shell:" + action.target, label: action.label, detail: "Shell action" });
for (const action of Keybinds.windowActions)
@@ -194,6 +206,49 @@ SettingsPage {
return out;
}
// Which gesture is currently choosing an application, "" while none is.
property string pickingAppFor: ""
// Capped at eight and only ever searched, never browsed -- the same rule
// and the same reason as CustomShortcutEditor's list.
readonly property var appMatches: {
const needle = gestureAppSearch.text.trim().toLowerCase();
if (needle === "")
return [];
const out = [];
for (const entry of DesktopEntries.applications.values) {
if (entry.noDisplay)
continue;
if (String(entry.name).toLowerCase().indexOf(needle) >= 0)
out.push(entry);
if (out.length >= 8)
break;
}
return out;
}
// The one path that writes an application onto a gesture. It builds the
// same three-field record assignGesture() writes for the named actions --
// an entry id is never a command, and hypr/actions.lua resolves it the same
// way it resolves one written by a custom shortcut.
function assignGestureApp(key: string, entry: var): void {
const target = String(entry?.id ?? "");
if (Keybinds.describeAction({ kind: "app", target: target }) === "")
return;
DesktopPreferences.set(key, {
kind: "app",
target: target,
label: "Launch " + String(entry?.name ?? target)
});
root.closeAppPicker();
Keybinds.applyReload();
}
function closeAppPicker(): void {
root.pickingAppFor = "";
gestureAppSearch.text = "";
}
SettingsCard {
visible: InputDevices.hasTouchpad
title: "Gestures"
@@ -236,7 +291,67 @@ SettingsPage {
current: root.gestureValue(String(gestureRow.modelData.key))
enabled: !Keybinds.reloading
divider: gestureRow.index < root.gestureKeys.length - 1
onPicked: value => root.assignGesture(String(gestureRow.modelData.key), String(value))
onPicked: value => {
if (String(value) === root.appPickerValue) {
root.pickingAppFor = String(gestureRow.modelData.key);
gestureAppSearch.text = "";
return;
}
root.closeAppPicker();
root.assignGesture(String(gestureRow.modelData.key), String(value));
}
}
}
// The search, unfolded under the four rows rather than inside one of
// them: the picker has already collapsed by the time this appears, and
// a list of eight results inside a collapsed row has nowhere to go.
Column {
width: parent.width
visible: root.pickingAppFor !== ""
spacing: 0
Item {
width: parent.width
height: 40
SearchField {
id: gestureAppSearch
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
placeholder: "Search installed applications"
}
}
Repeater {
model: root.appMatches
SettingRow {
id: candidate
required property var modelData
required property int index
width: parent.width
label: String(candidate.modelData.name ?? "")
detail: String(candidate.modelData.id ?? "")
controlWidth: 96
divider: candidate.index < root.appMatches.length - 1
activatable: true
onActivated: root.assignGestureApp(root.pickingAppFor, candidate.modelData)
}
}
TextRow {
visible: root.appMatches.length === 0
label: gestureAppSearch.text.trim() === ""
? "Type to find an application"
: "Nothing installed matches that"
detail: "The gesture stores the application, never a command — the same three fields a custom shortcut stores"
divider: false
}
}
@@ -111,15 +111,9 @@ SettingsPage {
width: parent.width
spacing: 8
Text {
SectionLabel {
visible: String(roomSection.modelData.name ?? "") !== ""
text: String(roomSection.modelData.name ?? "").toUpperCase()
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
font.letterSpacing: 0.8
topPadding: 6
text: String(roomSection.modelData.name ?? "")
}
Grid {
@@ -382,15 +376,10 @@ SettingsPage {
}
}
Text {
width: parent.width
visible: HomeAssistantConfig.lastError !== ""
text: HomeAssistantConfig.lastError
color: Theme.danger
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
bottomPadding: 9
ErrorRow {
label: "Home Assistant needs attention"
message: HomeAssistantConfig.lastError
divider: true
}
SettingRow {
@@ -432,7 +421,7 @@ SettingsPage {
SettingRow {
label: "Light catalog"
detail: "Light state is read through the Home Assistant helper."
detail: "Light state is read through the Home Assistant helper"
divider: false
controlWidth: 176
@@ -3,8 +3,8 @@
// by "because" -- a bare dash invites the reader to assume zero, and zero is
// a measurement.
//
// NotMeasuredRow { because: "The desktop portal's permission store is not answering, so what has asked for this cannot be read." }
// NotMeasuredRow { label: "Not read yet"; because: "Reading what the battery firmware has recorded." }
// NotMeasuredRow { because: "The desktop portal's permission store is not answering, so what has asked for this cannot be read" }
// NotMeasuredRow { label: "Not read yet"; because: "Reading what the battery firmware has recorded" }
//
// `because` is required in spirit: the row renders without one, but a consumer
// leaving it empty is exactly the dishonesty this component exists to prevent,
@@ -3,7 +3,7 @@
//
// The head is hand-built rather than a SettingRow because it needs two things
// SettingRow has no room for -- the application's own icon, and a subtitle
// whose second half is coloured when the rule is no longer the default, so a
// whose second half is colored when the rule is no longer the default, so a
// muted or redirected application is obvious without opening it.
//
// Every write leaves through `edited`/`forgotten`. The page owns the service
@@ -66,8 +66,8 @@ Column {
return name === "" ? "" : Quickshell.iconPath(name, true);
}
// A stable colour per application, so the tile is a recognisable shape
// rather than a different colour every time the list re-sorts.
// A stable color per application, so the tile is a recognizable shape
// rather than a different color every time the list re-sorts.
readonly property color tileColor: {
const palette = [Theme.accent, Theme.teal, Theme.magenta, Theme.cyan,
Theme.green, Theme.orange, Theme.pink];
@@ -263,11 +263,11 @@ Column {
OptionPickerRow {
width: parent.width
label: "Urgency"
detail: "Override what the app claims its notifications are"
detail: "Override what the application claims its notifications are"
options: [
{
value: "auto",
label: "App decides",
label: "Application decides",
detail: "Whatever urgency the notification arrives with"
},
{
@@ -287,7 +287,7 @@ Column {
ActionRow {
width: parent.width
label: "Forget this app"
label: "Forget this application"
detail: "Remove its rule; it returns on its next notification"
action: "Forget"
divider: false
@@ -278,32 +278,18 @@ SettingsPage {
SettingsCard {
title: "Applications"
subtitle: "Apps appear after their first notification. Recent senders and apps you have customized stay up top; the rest wait in All apps."
subtitle: "Applications appear after their first notification. Recent senders and applications you have customized stay up top; the rest wait in All applications."
TextRow {
visible: Notifs.applications.length === 0
label: "No applications remembered yet"
detail: "Application controls will appear after the first notification arrives."
detail: "Application controls will appear after the first notification arrives"
divider: false
}
Item {
width: parent.width
height: root.recentApps.length > 0 ? 28 : 0
visible: height > 0
Text {
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.bottomMargin: 4
text: "Recent"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 1)
font.weight: Font.Bold
font.capitalization: Font.AllUppercase
font.letterSpacing: 0.8
}
SectionLabel {
visible: root.recentApps.length > 0
text: "Recent"
}
Repeater {
@@ -329,23 +315,9 @@ SettingsPage {
}
}
Item {
width: parent.width
height: root.customizedApps.length > 0 ? 28 : 0
visible: height > 0
Text {
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.bottomMargin: 4
text: "Customized"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 1)
font.weight: Font.Bold
font.capitalization: Font.AllUppercase
font.letterSpacing: 0.8
}
SectionLabel {
visible: root.customizedApps.length > 0
text: "Customized"
}
Repeater {
@@ -400,7 +372,7 @@ SettingsPage {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
placeholder: "Search apps"
placeholder: "Search applications"
}
}
@@ -168,27 +168,11 @@ SettingsPage {
anchors.verticalCenter: parent.verticalCenter
spacing: 8
Rectangle {
StatusBadge {
anchors.verticalCenter: parent.verticalCenter
visible: HomeAssistant.phase === "ready"
width: homeBadge.implicitWidth + 14
height: 17
radius: Theme.pillRadius
color: Theme.alpha(Theme.ok, 0.1)
border.width: 1
border.color: Theme.alpha(Theme.ok, 0.3)
Text {
id: homeBadge
anchors.centerIn: parent
text: "CONNECTED"
color: Theme.ok
font.family: Theme.fontFamily
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 2)
font.weight: Font.DemiBold
font.letterSpacing: 0.5
}
text: "Connected"
tone: Theme.ok
}
SettingsButton {
@@ -211,27 +195,11 @@ SettingsPage {
anchors.verticalCenter: parent.verticalCenter
spacing: 8
Rectangle {
StatusBadge {
anchors.verticalCenter: parent.verticalCenter
visible: KdeConnect.pairedCount > 0
width: phoneBadge.implicitWidth + 14
height: 17
radius: Theme.pillRadius
color: Theme.alpha(Theme.ok, 0.1)
border.width: 1
border.color: Theme.alpha(Theme.ok, 0.3)
Text {
id: phoneBadge
anchors.centerIn: parent
text: "PAIRED"
color: Theme.ok
font.family: Theme.fontFamily
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 2)
font.weight: Font.DemiBold
font.letterSpacing: 0.5
}
text: "Paired"
tone: Theme.ok
}
SettingsButton {
@@ -259,7 +227,7 @@ SettingsPage {
TextRow {
visible: OnlineAccounts.scanned && OnlineAccounts.accounts.length === 0
label: "No accounts yet"
detail: "Adding one lets mail, calendar, contacts and file managers share a single sign-in."
detail: "Adding one lets mail, calendar, contacts and file managers share a single sign-in"
value: ""
divider: false
}
@@ -372,10 +372,10 @@ SettingsPage {
controlWidth: 160
divider: false
// SoundBadge is the pill Panama already has for "how this thing is
// attached, or why it is not here"; the name is where it was first
// needed rather than what it is.
SoundBadge {
// The state, in the pill the whole settings app uses for one. It
// is a fact about the session, not a control, which is why there
// is a badge here and no switch.
StatusBadge {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: LidPolicy.inhibited ? "Docked · staying awake" : "Will suspend"
@@ -66,14 +66,11 @@ SettingsPage {
});
}
Component.onCompleted: Printers.refresh()
Component.onCompleted: if (!Printers.scanned) Printers.refresh()
TextRow {
visible: Printers.lastError !== ""
ErrorRow {
label: "Printing needs attention"
detail: Printers.lastError
value: ""
divider: false
message: Printers.lastError
}
// ── Which printers exist ─────────────────────────────────────────────────
@@ -255,7 +252,7 @@ SettingsPage {
// was built around: holding keeps the job, cancelling throws it
// away and the document has to be sent from the app again.
detail: cancelJob.armed
? "The job is thrown away — printing it means sending it from the app again. Hold keeps it in the queue instead."
? "The job is thrown away — printing it means sending it from the application again. Hold keeps it in the queue instead."
: String(jobRow.modelData.printer ?? "") + " · "
+ String(jobRow.modelData.state ?? "")
+ (Number(jobRow.modelData.pages ?? 0) > 0
@@ -42,7 +42,7 @@ Rectangle {
return root.measured ? "Idle" : "Not measured";
}
// The neutral third state is neutral in the colour too: warn means in use,
// The neutral third state is neutral in the color too: warn means in use,
// dim means idle, and "not measured" must not read as either.
readonly property color toneColor: root.active
? Theme.warn
@@ -58,25 +58,25 @@ SettingsPage {
table: "camera",
label: "Camera",
detail: "",
empty: "No app has asked for the camera. The portal records an answer the first time each one asks."
empty: "No application has asked for the camera. The portal records an answer the first time each one asks."
},
{
table: "microphone",
label: "Microphone",
detail: "",
empty: "No app has asked for the microphone. The portal records an answer the first time each one asks."
empty: "No application has asked for the microphone. The portal records an answer the first time each one asks."
},
{
table: "screencast",
label: "Screen sharing",
detail: "Can capture the screen",
empty: "No app holds a screen-capture grant."
empty: "No application holds a screen-capture grant."
},
{
table: "remote-desktop",
label: "Remote desktop",
detail: "Can move the pointer and type",
empty: "No app holds a remote-control grant."
empty: "No application holds a remote-control grant."
},
{
table: "location",
@@ -89,7 +89,7 @@ SettingsPage {
function askedCount(count: int): string {
if (count === 0)
return "";
return count + (count === 1 ? " app has asked" : " apps have asked");
return count + (count === 1 ? " application has asked" : " applications have asked");
}
// What a stored secret is FOR, from its attributes. Never its value.
@@ -210,7 +210,7 @@ SettingsPage {
// An empty table means "nothing has asked" only once the store
// has been read and answered. Before that, and when the
// permission store is not running at all, every table is empty
// for the same reason -- and "no app has asked for the camera"
// for the same reason -- and "no application has asked for the camera"
// is then a measurement nobody took.
TextRow {
width: section.width
@@ -246,7 +246,7 @@ SettingsPage {
// session, so an app that has shared four times is four
// entries and one question.
detail: grantRow.confirming
? "The recorded answer goes away, and this app is asked again the next time it tries."
? "The recorded answer goes away, and this application is asked again the next time it tries."
: (!section.simple && !section.revokeOnly
? (grantRow.modelData.allowed === true ? "Allowed" : "Withheld")
: String(section.modelData.detail)
@@ -311,13 +311,14 @@ SettingsPage {
}
}
Item { width: 1; height: 10 }
// The limit, stated rather than left to be discovered. Saying "your
// camera is protected" when a system package can open /dev/video0
// would be a claim this page cannot back up.
TextRow {
label: "Only sandboxed apps are covered"
detail: "These are the answers Flatpak apps gave the permission portal. Programs installed as system packages are not sandboxed, never ask, and reach these devices directly."
divider: false
SettingsNote {
headline: "Only sandboxed applications are covered"
body: "These are the answers Flatpak applications gave the permission portal. Programs installed as system packages are not sandboxed, never ask, and reach these devices directly."
}
}
@@ -334,8 +335,8 @@ SettingsPage {
visible: Permissions.available
title: "Run in the background"
subtitle: backgroundCard.rows.length === 0
? "No app has asked to keep running with no window open."
: backgroundCard.rows.length + " app"
? "No application has asked to keep running with no window open."
: backgroundCard.rows.length + " application"
+ (backgroundCard.rows.length === 1 ? " is" : "s are")
+ " allowed to keep running with no window open."
@@ -412,10 +413,9 @@ SettingsPage {
divider: Keyring.available || Keyring.lastError !== ""
}
SettingRow {
visible: Keyring.lastError !== ""
ErrorRow {
label: "Keyring problem"
detail: Keyring.lastError
message: Keyring.lastError
divider: Keyring.available
}
@@ -544,7 +544,7 @@ SettingsPage {
TextRow {
visible: root.showingSecrets && Keyring.listed && Keyring.storedCount === 0
label: "Nothing stored yet"
detail: "Applications that save a password or token will appear here."
detail: "Applications that save a password or token will appear here"
value: ""
divider: false
}
@@ -553,7 +553,7 @@ SettingsPage {
// ── Traces ───────────────────────────────────────────────────────────────
//
// Panama's own, now. Each row clears one thing, once, when asked -- there
// is no "clean up your machine" button here and no number coloured red,
// is no "clean up your machine" button here and no number colored red,
// because none of this is a problem to be solved. It is a list of what is
// remembered, with a way to stop remembering it.
@@ -569,7 +569,7 @@ SettingsPage {
label: "Recent files"
detail: recentsRow.confirming
? "This empties the list. Applications start filling it again as you open things."
: "The recents list GTK apps show"
: "The recents list GTK applications show"
+ (Traces.measured
? " · " + Traces.recentsEntries + " entr"
+ (Traces.recentsEntries === 1 ? "y" : "ies")
@@ -710,21 +710,19 @@ SettingsPage {
}
}
Item { width: 1; height: 10 }
// A pointer, not a control: the history belongs to Vicinae, which owns
// the database Super+V browses. Offering a Clear here would mean
// reaching into another program's storage behind its back.
TextRow {
label: "Clipboard history"
detail: "Kept by Vicinae, which owns the database Super+V browses. Panama reads that history and does not clear it."
value: ""
divider: Traces.lastError !== ""
SettingsNote {
headline: "Clipboard history is kept by Vicinae"
body: "Vicinae owns the database Super+V browses. Panama reads that history and does not clear it."
}
SettingRow {
visible: Traces.lastError !== ""
ErrorRow {
label: "That did not work"
detail: Traces.lastError
divider: false
message: Traces.lastError
}
}
@@ -787,8 +785,8 @@ SettingsPage {
}
ActionRow {
label: "Per-app notifications"
detail: "Which apps may notify, and how much they may show on a locked screen"
label: "Per-application notifications"
detail: "Which applications may notify, and how much they may show on a locked screen"
action: "Open Notifications"
divider: false
onTriggered: ShellState.openSettings("notifications")
@@ -116,7 +116,7 @@ disclosure. Background and foreground are never stored alone: the five surfaces
and two text tints are mixed from them, so changing the ground moves the whole
family instead of leaving twelve tokens pointing at the old one.
Every route that writes color — typed hex, the colour wheel, the eyedropper,
Every route that writes color — typed hex, the color wheel, the eyedropper,
the HSV sliders — ends in `ThemeProfiles.commitActive`, which also recomputes
`accentName` as the nearest curated accent. That is what keeps GNOME's accent
enum, kitty's border and the lock screen from going stale after a custom edit.
@@ -339,7 +339,7 @@ erase a user-selected accent or a theme switch to leave the border behind.
`SettingsNote` is deliberately quiet and must stay that way. It explains
absences and boundaries — what this session cannot do, and why — and a
warning-coloured box would turn "not offered yet, here is the reason" into
warning-colored box would turn "not offered yet, here is the reason" into
"something is wrong here".
### The rows are keyboard-reachable
@@ -1,11 +1,16 @@
// A small uppercase heading inside a card, with an optional count beside it.
// A small uppercase heading, with an optional count beside it.
//
// SectionLabel { text: "Camera"; count: "2 apps have asked" }
// SectionLabel { text: "Camera"; count: "2 applications have asked" }
//
// A card whose rows come from four different permission tables needs to say
// which table a row belongs to, and a second SettingsCard per table would give
// four headers, four subtitles and four borders to one subject. This is the
// lighter divider: it groups rows without pretending they are separate topics.
//
// It does the same job one level up, between cards, where a page has more
// cards than a reader can hold at once and they fall into two or three
// stretches -- Sound's "Alerts & feedback", Displays' "All displays". Same
// shape either way, which is the point of it being one component.
import QtQuick
import qs.config
@@ -41,6 +46,9 @@ Item {
text: root.count
color: Theme.alpha(Theme.fgMuted, 0.75)
font.family: Theme.fontFamily
// Counts are the usual thing to put here, and a count that changes
// as a filter narrows should not shuffle the digits beside it.
font.features: Theme.tabularFigures
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 1)
font.letterSpacing: 0.7
}
@@ -7,9 +7,14 @@ Item {
default property alias trailingData: trailing.data
property string icon: ""
property string label: ""
// The headline's colour. Foreground for every ordinary row; ErrorRow sets
// The headline's color. Foreground for every ordinary row; ErrorRow sets
// it to Theme.danger so a failure reads as one at a glance.
property color labelColor: Theme.fg
// The line under the headline. One sentence takes no full stop -- it is a
// caption, not a paragraph, and a lone trailing dot under two hundred rows
// is a speck. More than one sentence takes them throughout, last one
// included, because at that length the reader is reading prose and prose
// that stops without punctuation reads as truncated.
property string detail: ""
property string value: ""
property bool divider: true
@@ -7,7 +7,7 @@
// }
//
// Deliberately quiet. This is not a warning banner and must never grow one's
// colouring: the things it explains are absences and boundaries, not problems,
// coloring: the things it explains are absences and boundaries, not problems,
// and a red-edged box would make "this desktop cannot do that yet" read as
// "something is wrong here". It sits darker than the card it is in, which is
// the whole visual signal it needs -- a note, set into the surface.
@@ -9,7 +9,7 @@
// Only for pages long enough that scrolling hides the control someone came for.
// Appearance was six cards deep, so light and dark -- the thing reached most --
// sat below a wallpaper grid and an entire lock screen. Tabs are not a way to
// make a short page look organised; they are for when the page is long enough
// make a short page look organized; they are for when the page is long enough
// that the order stops being a suggestion and starts being a burial.
//
// The strip scrolls horizontally when it overflows: the Settings window tiles
@@ -20,7 +20,7 @@ SettingsPage {
title: "Sharing"
lede: "What this machine offers to other machines on the network."
Component.onCompleted: Sharing.refresh()
Component.onCompleted: if (!Sharing.scanned) Sharing.refresh()
SettingsCard {
title: "This machine"
@@ -29,12 +29,11 @@ SettingsPage {
// The error belongs to the machine, not to a floating banner above the
// title: a bare row above the first card read as part of the page
// furniture, which is exactly what an error must not do.
TextRow {
ErrorRow {
width: parent.width
visible: Sharing.lastError !== ""
label: "Sharing needs attention"
detail: Sharing.lastError
value: ""
message: Sharing.lastError
divider: true
}
TextFieldRow {
@@ -78,7 +77,7 @@ SettingsPage {
TextRow {
visible: Sharing.remoteLoginOn && Sharing.remoteSessions.length === 0
label: "Nobody is signed in remotely"
detail: "Remote login is on, and no one is connected from another machine."
detail: "Remote login is on, and no one is connected from another machine"
value: ""
divider: false
}
@@ -219,7 +218,7 @@ SettingsPage {
TextRow {
visible: Sharing.mediaSharing?.installed !== true
label: "Share music and video to devices"
detail: "Needs Rygel, which is not installed."
detail: "Needs Rygel, which is not installed"
value: "Not installed"
divider: false
}
@@ -88,26 +88,11 @@ Item {
font.pixelSize: Theme.fontSizeSmall
}
Rectangle {
StatusBadge {
anchors.verticalCenter: parent.verticalCenter
visible: root.overridden
width: badge.implicitWidth + 14
height: 18
radius: Theme.pillRadius
color: Theme.alpha(Theme.warn, 0.10)
border.width: 1
border.color: Theme.alpha(Theme.warn, 0.32)
Text {
id: badge
anchors.centerIn: parent
text: "CHANGED"
color: Theme.warn
font.family: Theme.fontFamily
font.pixelSize: Math.max(8, Theme.fontSizeSmall - 2)
font.weight: Font.DemiBold
}
text: "Changed"
tone: Theme.warn
}
SettingsButton {
@@ -433,41 +433,12 @@ SettingsPage {
width: parent ? parent.width : 620
spacing: 0
Item {
width: parent.width
height: 30
Row {
anchors.left: parent.left
anchors.leftMargin: 6
anchors.bottom: parent.bottom
anchors.bottomMargin: 4
spacing: 8
Text {
anchors.baseline: groupCount.baseline
text: groupColumn.modelData.name
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
font.capitalization: Font.AllUppercase
font.letterSpacing: 0.8
}
Text {
id: groupCount
text: root.filtering
? "· showing " + groupColumn.modelData.binds.length
+ " of " + groupColumn.modelData.total
: "· " + groupColumn.modelData.total
color: Theme.fgMuted
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
}
}
SectionLabel {
text: groupColumn.modelData.name
count: root.filtering
? "· showing " + groupColumn.modelData.binds.length
+ " of " + groupColumn.modelData.total
: "· " + groupColumn.modelData.total
}
Repeater {
@@ -74,14 +74,11 @@ SettingsPage {
return Number((config?.limits ?? ({}))[horizon] ?? 0);
}
Component.onCompleted: Snapshots.refresh()
Component.onCompleted: if (!Snapshots.scanned) Snapshots.refresh()
TextRow {
visible: Snapshots.lastError !== ""
ErrorRow {
label: "Snapshots need attention"
detail: Snapshots.lastError
value: ""
divider: false
message: Snapshots.lastError
}
// What just happened to the file that was already there.
@@ -224,7 +221,7 @@ SettingsPage {
TextRow {
visible: volumeCard.snapshots.length === 0
label: "No points in time yet"
detail: "One is taken automatically on the schedule above."
detail: "One is taken automatically on the schedule above"
value: ""
divider: false
}
@@ -285,7 +282,7 @@ SettingsPage {
}
SettingsButton {
text: pointRow.confirming ? "Keep" : "Delete"
text: pointRow.confirming ? "Keep" : "Delete"
enabled: !Snapshots.busy
onClicked: root.confirmingDelete =
pointRow.confirming ? "" : pointRow.token
@@ -476,10 +473,10 @@ SettingsPage {
// Honest about what cannot be measured: per-snapshot size needs btrfs
// quota groups, which cost performance on every write. Reporting a
// made-up number would be worse than saying so.
TextRow {
NotMeasuredRow {
label: "Space used by snapshots"
detail: "Measuring this per snapshot needs btrfs quotas, which slow down every write. Free space above is the number that matters."
value: "Not measured"
because: "Measuring this per snapshot needs btrfs quotas, which slow down every write. Free space above is the number that matters."
divider: false
}
}
@@ -1,39 +0,0 @@
// A pill beside a device name: how the device is attached, or why it is not
// here at all.
//
// Nine outputs named after their chipsets look alike in a list. Which of them
// arrives over the network, over Bluetooth, or is simply away in its case is
// the fact that tells them apart, so it is said next to the name rather than
// left to be inferred from a subtitle.
import QtQuick
import qs.config
Rectangle {
id: root
property string text: ""
// The token the badge is drawn from -- cyan for network devices, the accent
// for Bluetooth, muted for a device that is not present.
property color tone: Theme.accent
implicitWidth: label.implicitWidth + 14
implicitHeight: 17
radius: Theme.pillRadius
color: Theme.alpha(root.tone, 0.10)
border.width: 1
border.color: Theme.alpha(root.tone, 0.28)
Text {
id: label
anchors.centerIn: parent
text: root.text
color: root.tone
font.family: Theme.fontFamily
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 2)
font.weight: Font.DemiBold
font.capitalization: Font.AllUppercase
font.letterSpacing: 0.5
}
}
@@ -47,7 +47,7 @@ SettingRow {
readonly property bool muted: AudioDevices.applicationMuted(chip.modelData)
implicitWidth: name.implicitWidth + muteButton.width + 20
implicitHeight: 28
implicitHeight: 26
radius: Theme.pillRadius
color: Theme.alpha(Theme.fg, 0.06)
border.width: 1
@@ -1,6 +1,6 @@
// One chip per channel the selected output actually has.
//
// "Test" used to play a single front-centre sample, which answers "is this the
// "Test" used to play a single front-center sample, which answers "is this the
// right device" and nothing else. A surround receiver wired to the wrong socket
// passes that test. Each chip plays its own channel's sample, so the answer to
// "is the rear left speaker actually rear left" is one click rather than a
@@ -183,7 +183,7 @@ Item {
clip: true
// Full-width gradient behind a clipped window, so a
// given level is always the same colour rather than a
// given level is always the same color rather than a
// squashed copy of the whole ramp.
Rectangle {
width: meter.width
@@ -298,19 +298,19 @@ Item {
anchors.verticalCenter: parent.verticalCenter
spacing: 6
SoundBadge {
StatusBadge {
visible: root.airplay && !root.ghost
text: "AirPlay"
tone: Theme.cyan
}
SoundBadge {
StatusBadge {
visible: root.bluetooth && !root.ghost
text: "Bluetooth"
tone: Theme.accent
}
SoundBadge {
StatusBadge {
visible: root.ghost
text: "Returns when connected"
tone: Theme.fgMuted
@@ -27,12 +27,20 @@ SettingsPage {
// moves and the region past 100% is marked.
readonly property real volumeMaximum: Settings.overAmplification ? 1.5 : 1
// The three services that read the machine rather than the graph do it once
// per page open, the way the Displays page refreshes hardware brightness.
// The three services that read the machine rather than the graph are asked
// when the page opens, the way the Displays page probes hardware
// brightness. Two of them read hardware that does not change while Settings
// is open, so they are asked once a session and not once a visit.
//
// SoundFeedback is not one of them: it reads GSettings keys any GNOME
// control panel can rewrite behind this page's back, so it is re-read every
// open rather than trusted from the first one.
Component.onCompleted: {
SoundFeedback.refresh();
SoundDefaults.refresh();
SoundCards.refresh();
if (!SoundDefaults.loaded)
SoundDefaults.refresh();
if (!SoundCards.loaded)
SoundCards.refresh();
}
SettingsCard {
@@ -136,17 +144,7 @@ SettingsPage {
}
// ── What the machine says when it wants attention ───────────────────────
Text {
width: parent.width
text: "Alerts & feedback"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
font.capitalization: Font.AllUppercase
font.letterSpacing: 0.7
topPadding: 6
}
SectionLabel { text: "Alerts & feedback" }
SettingsCard {
title: "Alerts"
@@ -189,17 +187,7 @@ SettingsPage {
}
// ── The hardware underneath the devices ─────────────────────────────────
Text {
width: parent.width
text: "Advanced"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
font.capitalization: Font.AllUppercase
font.letterSpacing: 0.7
topPadding: 6
}
SectionLabel { text: "Advanced" }
SettingsCard {
title: "Device profiles"
@@ -157,7 +157,7 @@ SettingsPage {
TextRow {
visible: SshKeys.scanned && SshKeys.keys.length === 0
label: "Nothing here yet"
detail: "A key you generate below shows up here, with its fingerprint and whether the agent is holding it."
detail: "A key you generate below shows up here, with its fingerprint and whether the agent is holding it"
value: ""
}
@@ -346,7 +346,7 @@ SettingsPage {
TextRow {
visible: SshKeys.agent?.available === true && root.heldKeys.length === 0
label: "Nothing loaded"
detail: "Add a key above and the agent will offer it without asking for its passphrase again."
detail: "Add a key above and the agent will offer it without asking for its passphrase again"
value: ""
}
@@ -390,12 +390,14 @@ SettingsPage {
// moment later, because it is read back off disk. The Remove button
// above stays -- the helper refuses and says this, which is a better
// answer than a button that is not there.
TextRow {
Item { width: 1; height: agentNote.visible ? 10 : 0 }
SettingsNote {
id: agentNote
visible: SshKeys.agent?.available === true && !SshKeys.durableRemoval
label: "Removing a key from this agent does not stick"
detail: "It lists every key in ~/.ssh, so one removed comes straight back at the next sign-in. That is the keyring agent's design, not a bug — move the file out of ~/.ssh to stop it being offered."
value: ""
divider: false
headline: "Removing a key from this agent does not stick"
body: "It lists every key in ~/.ssh, so one removed comes straight back at the next sign-in. That is the keyring agent's design, not a bug — move the file out of ~/.ssh to stop it being offered."
}
}
@@ -0,0 +1,47 @@
// A pill beside a name: the one fact about that thing which decides how to
// read the rest of the row.
//
// StatusBadge { text: "Bluetooth"; tone: Theme.accent }
// StatusBadge { text: "No reader"; tone: Theme.warn }
//
// It was born on the Sound page as SoundBadge, where nine outputs named after
// their chipsets look alike in a list and which of them arrives over the
// network, over Bluetooth, or is simply away in its case is what tells them
// apart. That turned out to be the general shape -- an account that is
// connected, a package that came from Flatpak, a shortcut that has been
// changed -- so it lost the "Sound" and became the one pill the settings use.
//
// Always a single short token, always upper-cased here rather than by the
// caller. A badge carrying a sentence is a detail line wearing a border.
import QtQuick
import qs.config
Rectangle {
id: root
property string text: ""
// The token the badge is drawn from -- cyan for network devices, the accent
// for Bluetooth, muted for a device that is not present.
property color tone: Theme.accent
implicitWidth: label.implicitWidth + 14
implicitHeight: 17
radius: Theme.pillRadius
color: Theme.alpha(root.tone, 0.10)
border.width: 1
border.color: Theme.alpha(root.tone, 0.28)
Text {
id: label
anchors.centerIn: parent
text: root.text
color: root.tone
font.family: Theme.fontFamily
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 2)
font.weight: Font.DemiBold
font.capitalization: Font.AllUppercase
font.letterSpacing: 0.5
}
}
@@ -51,14 +51,16 @@ SettingsPage {
readonly property var removableDrives: Disks.drives.filter(entry => entry.removable)
// Unguarded on purpose, unlike the other refresh-on-open pages: this is the
// cheap snapshot, not the walk. Free space moves while Settings is open --
// that is most of the reason someone comes back to this page -- and
// disks-contract pins the bare call so a later sweep cannot quietly turn it
// into a once-a-session read. The expensive walks are asked for by hand.
Component.onCompleted: Disks.refresh()
TextRow {
visible: Disks.lastError !== ""
ErrorRow {
label: "Storage needs attention"
detail: Disks.lastError
value: ""
divider: false
message: Disks.lastError
}
// ── Space ────────────────────────────────────────────────────────────────
@@ -113,7 +115,7 @@ SettingsPage {
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."
detail: "Home, applications and caches are floors rather than totals, so the remainder is larger than it should be"
value: ""
divider: false
}
@@ -249,11 +251,10 @@ SettingsPage {
}
}
TextRow {
NotMeasuredRow {
visible: Disks.foldersMeasured && Disks.folderScanTruncated
label: "Some folders were not measured"
detail: "The walk ran out of time before reaching them, so this list is incomplete."
value: ""
because: "The walk ran out of time before reaching them, so this list is incomplete"
divider: false
}
}
@@ -343,7 +344,7 @@ SettingsPage {
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."
detail: "Caches, the trash, unused Flatpak runtimes and the package download cache were all measured and none of them holds anything"
value: ""
divider: false
}
@@ -473,7 +474,7 @@ SettingsPage {
TextRow {
visible: root.removableDrives.length === 0
label: "None connected"
detail: "Drives you plug in appear here, with a way to unmount them safely."
detail: "Drives you plug in appear here, with a way to unmount them safely"
value: ""
divider: false
}
@@ -13,7 +13,7 @@
// and restoring defaults both used to happen on a single press, which is the
// one interaction on this page nobody can take back: the two-stage confirm is
// the same one Snapshots and Privacy use, where the first press only arms the
// second and the second is the one wearing the danger colour.
// second and the second is the one wearing the danger color.
import QtQuick
import qs.config
@@ -219,7 +219,7 @@ Rectangle {
? "Delete " + root.theme.name + ", press again to confirm"
: "Delete " + root.theme.name
Accessible.description: root.removeArmed
? "This theme's palette, terminal colours and effects go with it"
? "This theme's palette, terminal colors and effects go with it"
: ""
Accessible.focusable: true
Accessible.focused: remove.activeFocus
@@ -7,7 +7,7 @@
// whole family with it rather than leaving twelve tokens pointing at the old
// one.
//
// One eyedropper process and one colour wheel are shared by all four wells --
// One eyedropper process and one color wheel are shared by all four wells --
// the target is remembered rather than duplicated four times.
import QtQuick
@@ -46,7 +46,7 @@ Grid {
function apply(which: string, hex: string): void {
const value = String(hex).trim().toLowerCase();
if (!/^#[0-9a-f]{6}$/.test(value)) {
root.lastError = "That is not a colour this theme can use.";
root.lastError = "That is not a color this theme can use.";
return;
}
root.lastError = "";
@@ -97,7 +97,7 @@ Grid {
ColorDialog {
id: wheel
title: "Choose a colour"
title: "Choose a color"
// Qt hands back a color value, not a string; toString() on it is
// "#aarrggbb" when there is alpha, so the channels are read directly.
onAccepted: root.apply(root.target, Qt.rgba(
@@ -113,7 +113,7 @@ Grid {
}
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0)
root.lastError = "Screen colour picking was cancelled or unavailable.";
root.lastError = "Screen color picking was cancelled or unavailable.";
}
}
}
@@ -6,7 +6,6 @@
// 900px is narrower than its own caption.
import QtQuick
import qs.config
import qs.services
Column {
@@ -25,15 +24,7 @@ Column {
readonly property real cellWidth: root.columns > 0
? (root.width - (root.columns - 1) * 12) / root.columns : 200
Text {
text: root.heading
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
font.capitalization: Font.AllUppercase
font.letterSpacing: 0.7
}
SectionLabel { text: root.heading }
Grid {
width: parent.width
@@ -1,6 +1,6 @@
// Naming what you built.
//
// Saving snapshots the palette, the terminal colours and the ten effect values
// Saving snapshots the palette, the terminal colors and the ten effect values
// as they stand, so a saved theme restores the whole look rather than two
// accent hexes. Delete appears only for a saved theme, because a shipped one
// cannot be removed -- it is read from the catalog every launch.
@@ -16,10 +16,10 @@ SettingRow {
label: "Save as"
// Armed, the row says what the delete actually costs: a saved theme is the
// only copy of its palette, terminal colours and effect values.
// only copy of its palette, terminal colors and effect values.
detail: deleteConfirm.armed
? "Deletes “" + String(ThemeProfiles.activeProfile.name ?? "this theme")
+ "” — its palette, terminal colours and effects go with it"
+ "” — its palette, terminal colors and effects go with it"
: (root.custom
? "Saving again under a new name keeps both"
: "Editing a shipped theme forks it; give the fork a name to keep it")
@@ -30,7 +30,7 @@ Flow {
readonly property bool active: chip.modelData.id === ThemeProfiles.activeId
implicitWidth: caption.implicitWidth + dot.width + 26
implicitHeight: 31
implicitHeight: 26
radius: Theme.pillRadius
color: chip.active
? Theme.alpha(Theme.accent, 0.12)
@@ -32,7 +32,7 @@ SettingsPage {
// one application is what somebody came here to write, and the gaps and
// borders below are set once and left alone.
SettingsCard {
title: "App rules"
title: "Application window rules"
subtitle: "How specific applications behave when they open. Behaviors, not regexes: pick an application, tick what it should do. Panama's own surfaces cannot be matched."
Repeater {
@@ -40,7 +40,8 @@ SettingsPage {
property string expandedPackage: ""
Component.onCompleted: {
Updates.refresh();
if (!Updates.scanned)
Updates.refresh();
// A first visit with nothing cached should not show a confident "up to
// date" it has no basis for, so it goes and finds out.
if (!Updates.everChecked && !Updates.checking)
@@ -103,12 +104,9 @@ SettingsPage {
// ── What just happened ───────────────────────────────────────────────────
TextRow {
visible: Updates.lastError !== ""
ErrorRow {
label: "Updates need attention"
detail: Updates.lastError
value: ""
divider: false
message: Updates.lastError
}
TextRow {
@@ -288,17 +286,18 @@ SettingsPage {
// A check that failed is not a machine that is current. "Current" is
// only ever said about a list that was actually read.
TextRow {
NotMeasuredRow {
visible: Updates.sourceError("flatpak") !== ""
label: "Flatpak"
detail: Updates.sourceError("flatpak")
value: "Not checked"
because: Updates.sourceError("flatpak")
divider: true
}
TextRow {
visible: Updates.sourceError("flatpak") === "" && Updates.flatpak?.available === false
label: "Flatpak"
detail: "Flatpak is not installed."
detail: "Flatpak is not installed"
value: "Unavailable"
}
@@ -376,18 +375,18 @@ SettingsPage {
onTriggered: Updates.apply("flatpak")
}
TextRow {
NotMeasuredRow {
visible: Updates.sourceError("firmware") !== ""
label: "Firmware"
detail: Updates.sourceError("firmware")
value: "Not checked"
because: Updates.sourceError("firmware")
divider: false
}
TextRow {
visible: Updates.sourceError("firmware") === "" && Updates.firmware?.available === false
label: "Firmware"
detail: "Firmware updating is not available on this machine."
detail: "Firmware updating is not available on this machine"
value: "Unavailable"
divider: false
}
@@ -460,7 +459,7 @@ SettingsPage {
TextRow {
visible: Updates.historyLoaded && Updates.history.length === 0
label: "Nothing recorded"
detail: "Neither dnf nor flatpak has a transaction history here."
detail: "Neither dnf nor flatpak has a transaction history here"
value: ""
divider: false
}
@@ -155,20 +155,19 @@ SettingsPage {
}
Component.onCompleted: {
UserAccounts.refresh();
if (!UserAccounts.scanned)
UserAccounts.refresh();
// Probing fprintd bus-activates it, so it waits for the page rather
// than costing every shell launch.
Fingerprint.refresh();
if (!Fingerprint.scanned)
Fingerprint.refresh();
}
// A write that failed is a row, not a page. Everything below stays usable,
// because the thing that refused is almost never the thing being read.
TextRow {
visible: UserAccounts.lastError !== ""
ErrorRow {
label: "That change did not go through"
detail: UserAccounts.lastError
value: ""
divider: false
message: UserAccounts.lastError
}
// ── You ──────────────────────────────────────────────────────────────────
@@ -466,26 +465,10 @@ SettingsPage {
anchors.verticalCenter: parent.verticalCenter
spacing: 8
Rectangle {
StatusBadge {
anchors.verticalCenter: parent.verticalCenter
width: noReader.implicitWidth + 14
height: 17
radius: Theme.pillRadius
color: Theme.alpha(Theme.warn, 0.1)
border.width: 1
border.color: Theme.alpha(Theme.warn, 0.3)
Text {
id: noReader
anchors.centerIn: parent
text: "NO READER"
color: Theme.warn
font.family: Theme.fontFamily
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 2)
font.weight: Font.DemiBold
font.letterSpacing: 0.5
}
text: "No reader"
tone: Theme.warn
}
SettingsButton {
@@ -513,17 +496,7 @@ SettingsPage {
onToggled: value => Fingerprint.setUnlockEnabled(value)
}
Text {
width: parent.width
text: "ENROLLED FINGERS"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Math.max(9, Theme.fontSizeSmall - 1)
font.weight: Font.DemiBold
font.letterSpacing: 0.8
topPadding: 12
bottomPadding: 4
}
SectionLabel { text: "Enrolled fingers" }
TextRow {
width: parent.width
@@ -620,12 +593,9 @@ SettingsPage {
}
}
TextRow {
visible: Fingerprint.lastError !== ""
ErrorRow {
label: "The reader had something to say"
detail: Fingerprint.lastError
value: ""
divider: false
message: Fingerprint.lastError
}
}
@@ -110,7 +110,7 @@ Column {
width: parent.width
visible: root.openWindows.length > 0
label: "Open right now"
detail: "The class comes from the compositor, so these are exact."
detail: "The class comes from the compositor, so these are exact"
current: root.windowClass
options: root.openWindows
divider: false
@@ -248,7 +248,7 @@ Column {
width: parent.width
visible: root.workspaceOn
label: "Workspace"
detail: "The ten the keymap reaches."
detail: "The ten the keymap reaches"
current: root.workspace
options: root.workspaceOptions
divider: false
@@ -96,11 +96,9 @@ SettingsPage {
}
}
TextRow {
visible: Projects.lastError !== ""
ErrorRow {
label: "Problem"
detail: Projects.lastError
divider: false
message: Projects.lastError
}
}
}
@@ -97,7 +97,6 @@ PasswordField 1.0 PasswordField.qml
AudioBalance 1.0 AudioBalance.qml
SoundDeviceList 1.0 SoundDeviceList.qml
SoundDeviceRow 1.0 SoundDeviceRow.qml
SoundBadge 1.0 SoundBadge.qml
SoundChannelStrip 1.0 SoundChannelStrip.qml
SoundVolumeRow 1.0 SoundVolumeRow.qml
SoundThemeRow 1.0 SoundThemeRow.qml
@@ -117,6 +116,7 @@ TextEntryRow 1.0 TextEntryRow.qml
PrivacyPage 1.0 PrivacyPage.qml
PrivacyLiveTile 1.0 PrivacyLiveTile.qml
SectionLabel 1.0 SectionLabel.qml
StatusBadge 1.0 StatusBadge.qml
SearchPicker 1.0 SearchPicker.qml
OnlineAccountsPage 1.0 OnlineAccountsPage.qml
TextFieldRow 1.0 TextFieldRow.qml