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
+23 -14
View File
@@ -73,18 +73,21 @@ the comments of the components themselves, because knowing what a thing was
modelled on explains why it behaves the way it does. modelled on explains why it behaves the way it does.
GNOME is not gone from the machine: `gnome-control-center` is a declared GNOME is not gone from the machine: `gnome-control-center` is a declared
dependency, and Panama's own Settings hands off to it for the panels it dependency, and two rows in Panama's own Settings still open it. Adding an
deliberately does not own — Online Accounts, Color, Sound, Network, Keyboard, online account goes through the provider's dialog, because the OAuth sign-in
Privacy, Wellbeing, Accessibility, and System for users, date and time, region runs inside a library Fedora ships without a binding anything else can call.
and remote desktop. The allow-list in And Digital wellbeing — screen time and break reminders — is the one panel of
[`services/SystemSettings.qml`](config/dot/quickshell/services/SystemSettings.qml) GNOME's that still does something Panama does not. Everything else it used to
is what decides; anything not on it is a panel Panama owns itself. hand over is a page here now, and
[`tests/quickshell/gnome-handoff-contract`](tests/quickshell/gnome-handoff-contract)
fails the build if a door back opens onto a panel Panama owns, because those
rows kept working perfectly long after they stopped being true.
Displays is one of the panels it owns, and the only one where a wrong answer Displays is one of the panels it owns, and the only one where a wrong answer
can leave you unable to see well enough to undo it. So every change there is can leave you unable to see well enough to undo it. So every change there is
applied as one complete layout, read back from the compositor, and reverted applied as one complete layout, read back from the compositor, and reverted
after fifteen seconds unless you keep it — resolution, scale, rotation, after fifteen seconds unless you keep it — resolution, scale, rotation,
position and primary display, and now colour profile, bit depth, SDR position and primary display, and now color profile, bit depth, SDR
brightness and saturation, and mirroring with them. Two of those opt out of brightness and saturation, and mirroring with them. Two of those opt out of
part of that, for reasons rather than convenience. A per-display variable part of that, for reasons rather than convenience. A per-display variable
refresh rate override is applied but never verified, because `hyprctl` reports refresh rate override is applied but never verified, because `hyprctl` reports
@@ -100,7 +103,7 @@ DDC, and the buttons on the bezel change it behind our back.
| `config/dot/hypr/` | Compositor config. **Lua, not hyprlang** — see its README | | `config/dot/hypr/` | Compositor config. **Lua, not hyprlang** — see its README |
| `config/dot/quickshell/` | The shell: bar, dock, Continuum overview, Settings, Screen Intelligence, focus sessions, quick settings, notifications, screenshot UI | | `config/dot/quickshell/` | The shell: bar, dock, Continuum overview, Settings, Screen Intelligence, focus sessions, quick settings, notifications, screenshot UI |
| `config/containers/` | Container definitions systemd runs as units — currently the speech-to-text server behind dictation | | `config/containers/` | Container definitions systemd runs as units — currently the speech-to-text server behind dictation |
| `config/dot/vicinae/` | Raycast-style launcher, themed. Its commands live in `config/local/share/vicinae/` — script commands (settings deep links, power menu, reminders, window switcher, kill process, SSH hosts, recent files, color picker), and one compiled extension that adds web search with live suggestions. File search, calculator, clipboard, and emoji are Vicinae's own | | `config/dot/vicinae/` | Raycast-style launcher, themed. Its commands live in `config/local/share/vicinae/` — script commands (settings deep links, power actions, projects, reminders, window switcher, kill process, SSH hosts, recent files, color picker), and one compiled extension that adds web search with live suggestions. File search, calculator, clipboard, and emoji are Vicinae's own |
| `config/dot/uwsm/` | Session environment (see the uwsm caveat in the hypr README) | | `config/dot/uwsm/` | Session environment (see the uwsm caveat in the hypr README) |
| `config/dot/wofi/` | Fallback launcher, in case the shell fails to start | | `config/dot/wofi/` | Fallback launcher, in case the shell fails to start |
| `config/dot/xdg-desktop-portal/` | Portal backend routing | | `config/dot/xdg-desktop-portal/` | Portal backend routing |
@@ -108,7 +111,11 @@ DDC, and the buttons on the bezel change it behind our back.
New machines get a welcome card on first start, teaching the handful of keys New machines get a welcome card on first start, teaching the handful of keys
that matter and reachable afterwards from the launcher. `SUPER + /` shows every that matter and reachable afterwards from the launcher. `SUPER + /` shows every
shortcut the machine has, read from the live keymap so a rebind appears there shortcut the machine has, read from the live keymap so a rebind appears there
without anything being kept in sync. Settings carries a without anything being kept in sync. Rebinding is Settings' job, and so is
adding a shortcut of your own or assigning the four-finger touchpad swipes: an
entry names an application, a shell action or a window move rather than
carrying a command, so a hand-edited settings file can move a key but cannot
make one run something arbitrary. Settings carries a
[manual](config/dot/quickshell/manual/) written for the person using the [manual](config/dot/quickshell/manual/) written for the person using the
desktop rather than the person building it, opening with a chapter for people desktop rather than the person building it, opening with a chapter for people
arriving from GNOME, macOS or Windows. arriving from GNOME, macOS or Windows.
@@ -132,7 +139,8 @@ migrations/ One repair per file, for machines installed before a change
config/ config/
bash/ .bashrc, aliases, env (env is gitignored) bash/ .bashrc, aliases, env (env is gitignored)
copy/ Files copied verbatim over / (needs sudo) copy/ Files copied verbatim over / (needs sudo)
dot/ Symlinked into ~/.config dot/ Symlinked into ~/.config; quickshell/manual/ is the manual
Settings renders
firefox/ Vendored Firefox chrome, linked into the browser profile firefox/ Vendored Firefox chrome, linked into the browser profile
containers/ Quadlets, linked into ~/.config/containers/systemd containers/ Quadlets, linked into ~/.config/containers/systemd
local/ Icons, the cursor theme, and the launcher's commands and local/ Icons, the cursor theme, and the launcher's commands and
@@ -145,7 +153,6 @@ setup/
packages/ One package per line; extras/ holds the optional categories packages/ One package per line; extras/ holds the optional categories
scripts/ Run in order by ./install scripts/ Run in order by ./install
tests/ Contracts. See below tests/ Contracts. See below
manual/ The user manual, rendered in Settings
docs/ Settings reference, and the design specs behind the work docs/ Settings reference, and the design specs behind the work
``` ```
@@ -210,7 +217,7 @@ readable, and an indented line belongs to the entry above it — which is how OB
carries its sixteen plugin extensions as one thing to tick. carries its sixteen plugin extensions as one thing to tick.
Hooks are the extension point: drop a script at `~/.config/panama/hooks/theme-set` Hooks are the extension point: drop a script at `~/.config/panama/hooks/theme-set`
and it runs whenever the colour scheme changes, with the scheme and accent as and it runs whenever the color scheme changes, with the scheme and accent as
arguments. Same for `post-upgrade` and `post-migrate`, and a `<name>.d/` arguments. Same for `post-upgrade` and `post-migrate`, and a `<name>.d/`
directory beside each so several things can react without fighting over one directory beside each so several things can react without fighting over one
file. A broken hook is reported and stepped over, never fatal. Samples are file. A broken hook is reported and stepped over, never fatal. Samples are
@@ -236,6 +243,8 @@ the current upstream and reports a failure rather than working around it.
Adding one is adding a file to `setup/apps/`, and the file has to say why the Adding one is adding a file to `setup/apps/`, and the file has to say why the
exception exists. exception exists.
None of the scripts in this repository carry a `.sh` extension. A shebang and No script in this repository carries a `.sh` extension, with one deliberate
the executable bit already select the interpreter, and the extension only exception: each repair in `migrations/` is named `<timestamp>.sh`, because
`panama-migrate` finds them by globbing exactly that. Everywhere else a shebang
and the executable bit already select the interpreter, and the extension only
becomes something to keep in sync — which it did not stay. becomes something to keep in sync — which it did not stay.
@@ -49,5 +49,32 @@ clock in the middle and status on the right. There is still a dock. Volume
and brightness keys still work, and still work on the lock screen. Copy and and brightness keys still work, and still work on the lock screen. Copy and
paste are unchanged. Your files are where you left them. paste are unchanged. Your files are where you left them.
GNOME Settings is still installed for the few panels Panama deliberately does ## Where the settings went
not own, and Panama's own Settings hands off to it where that applies.
`Super + I` opens Panama's own Settings. The sidebar is where GNOME's panels
ended up, and it is worth one read so you never have to hunt:
| If you are looking for | It is under |
|---|---|
| Themes, wallpaper, fonts, text size, titlebars | Appearance |
| The bar, the dock, Control Center, tiling, workspaces | Shell |
| Resolution, scale, arrangement, night light | Displays |
| Output and input devices, per-application volume | Sound |
| Do Not Disturb, per-application rules, focus modes | Notifications & Focus |
| Shortcuts, mouse and touchpad, dictation | Input |
| Wi-Fi and VPN, the firewall, sharing, printers | Network & Sharing |
| Default applications, autostart, gaming, Screen Intelligence | Applications |
| The accounts on this machine, and online ones | Users & Accounts |
| Permissions, saved passwords, what the machine remembers, SSH keys | Privacy & Security |
| Battery, the lid, the power button, idle and lock timings | Power & Lock |
| Magnifier, color filters, reduced motion, screen reader | Accessibility |
| Updates, storage, snapshots, date and time, backups | System |
Home is the page `Super + I` lands on. The search box above the sidebar
searches the settings themselves rather than the page names, so typing what you
want to change is usually faster than remembering which of those it lives in.
GNOME Settings is still installed, and Panama's Settings opens it in exactly
two places: adding an online account, which has to go through the provider's
own dialog, and Digital wellbeing, which is the one panel of GNOME's that does
something Panama does not.
@@ -58,3 +58,9 @@ one I have".
Settings has a [Keyboard page](panama://settings/shortcuts), under Input, listing every bind, each of which Settings has a [Keyboard page](panama://settings/shortcuts), under Input, listing every bind, each of which
can be reassigned. A rebind moves the shortcut and cannot change what it does, so can be reassigned. A rebind moves the shortcut and cannot change what it does, so
there is no way to make a key do something unexpected by editing it. there is no way to make a key do something unexpected by editing it.
Shortcuts you invent live in the same list, in a **Custom** group above the
rest. One of those names an application, a shell action or a window move rather
than storing a command — for the same reason: what you are choosing is which
key, not what it runs. A chord something already holds is refused rather than
stolen, so a new shortcut can never quietly cost you an old one.
@@ -28,9 +28,11 @@ follows you between workspaces. A floating window can be moved by holding
## Workspaces ## Workspaces
Ten of them, on `Alt + 1` through `Alt + 0`. `Alt + H` and `Alt + L` step Ten you can jump straight to, on `Alt + 1` through `Alt + 0`, though they are
between neighbours, and `Alt + Shift` plus a number sends the current window created and destroyed as you use them rather than sitting there empty. `Alt + H`
somewhere without following it. and `Alt + L` step between neighbours — right past the last one makes a new one,
left from the first stays put — and `Alt + Shift` plus a number sends the
current window somewhere without following it.
On a machine with more than one display, workspaces belong to whichever On a machine with more than one display, workspaces belong to whichever
screen has focus. If you would rather pin workspaces one through ten to your screen has focus. If you would rather pin workspaces one through ten to your
@@ -41,6 +43,29 @@ How windows share the space — the layout, the master area, gaps and window
edges — is Shell Tiling. The workspace switches themselves, and the projects edges — is Shell Tiling. The workspace switches themselves, and the projects
below, are Shell Workspaces. below, are Shell Workspaces.
## The touchpad
Three fingers sideways moves between workspaces and follows your fingers as it
goes; three fingers up opens the overview and three down closes it. Those are
GNOME's gestures, reproduced, and they do not move.
Four fingers are yours. Each of the four directions can be given a job — a shell
action, a window or workspace move — from the same list your own shortcuts pick
from, on Input Mouse & Touchpad. Assigning one reloads the compositor, so
expect a beat of black before it works. Unassigned directions are left alone
rather than bound to nothing.
## Rules for one application
Some windows want the same treatment every time: the password manager should
float, chat should open on workspace four, a picture viewer should not be
resized to fill half the screen. **Application window rules** on Shell Tiling is where that
is written down. You pick an application and tick what should happen to it —
float, center it, pin it to every workspace, give it a fixed size or a
workspace to open on — rather than writing a match pattern. Panama's own
surfaces cannot be targeted, and a rule with nothing ticked is refused rather
than saved as a row you would wonder about later.
## Projects ## Projects
A project is the set of windows you open together: which applications, which A project is the set of windows you open together: which applications, which
@@ -10,8 +10,8 @@ From a terminal, the same check is `panama doctor`.
## The shell is gone ## The shell is gone
If the bar and dock disappear, the Quickshell process died. **Restart Shell** If the bar and dock disappear, the Quickshell process died. **Restart Desktop
from the launcher brings it back. If the launcher is also gone, press Shell** from the launcher brings it back. If the launcher is also gone, press
`Super + Shift + R` for the fallback launcher, which depends on nothing but `Super + Shift + R` for the fallback launcher, which depends on nothing but
itself and exists for exactly this. itself and exists for exactly this.
@@ -32,7 +32,7 @@ Settings has a [Displays page](panama://settings/displays). It opens on a pictur
one display or several — and everything under that picture belongs to one display or several — and everything under that picture belongs to
whichever one you have selected. Every change there reverts itself after whichever one you have selected. Every change there reverts itself after
fifteen seconds unless you confirm it, so a mode your monitor cannot show fifteen seconds unless you confirm it, so a mode your monitor cannot show
cannot strand you, and that covers colour, bit depth and mirroring as well as cannot strand you, and that covers color, bit depth and mirroring as well as
resolution, scale and rotation. If you are already stranded, `hyprctl resolution, scale and rotation. If you are already stranded, `hyprctl
monitors` from a terminal shows what is applied. monitors` from a terminal shows what is applied.
@@ -45,6 +45,29 @@ labelled as an unverified claim, beside the real action.
If you did not expect it at all, cancel. Nothing that needs privileges is If you did not expect it at all, cancel. Nothing that needs privileges is
harmed by being refused. harmed by being refused.
## A button did nothing except change its wording
That is the button working. Anything in Settings that removes something takes
two presses: the first arms it, and the row then shows what is about to happen
beside a **Keep** that backs out. The second press is the one that does it.
Only one button in Settings is armed at a time, so arming a second one disarms
the first, wherever it was.
## The network I need is not in the list
The Wi-Fi list is what is in range. **Saved networks**, further down
[Connections](panama://settings/connectivity), is what this machine remembers,
including the ones nowhere near you — which is where to tidy up a profile you
want rid of, rather than waiting until you are standing next to it again. A
network that broadcasts no name is joined with **Join a hidden network…**, at
the bottom of the list.
Opening a connection shows what it actually got — address, gateway, DNS — and
is where you give it a fixed address instead when what it got does not work,
or mark it metered so updates and large downloads wait until you are somewhere
else.
## After an update, something is not right ## After an update, something is not right
`panama migrate` applies repairs this machine has not had yet. It is safe to `panama migrate` applies repairs this machine has not had yet. It is safe to
@@ -1,4 +1,4 @@
// "Add app to dock", reachable without opening Settings. // "Add application to dock", reachable without opening Settings.
// //
// The Settings page is the place to curate the whole dock -- reorder it, unpin // The Settings page is the place to curate the whole dock -- reorder it, unpin
// things, change how it hides. Adding one application is a single decision made // things, change how it hides. Adding one application is a single decision made
@@ -7,7 +7,7 @@
// //
// It embeds the same DockAppPicker the Dock page uses rather than growing a // It embeds the same DockAppPicker the Dock page uses rather than growing a
// second search: the exclusion of already-pinned applications, the icon lookup // second search: the exclusion of already-pinned applications, the icon lookup
// and the "nothing until you type" behaviour are all already there, and two // and the "nothing until you type" behavior are all already there, and two
// copies of them would drift. // copies of them would drift.
import Quickshell import Quickshell
@@ -125,7 +125,7 @@ PanelWindow {
Text { Text {
id: title id: title
text: "Add app to dock" text: "Add application to dock"
color: Theme.fg color: Theme.fg
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
@@ -3,7 +3,7 @@
// This is the fine-tune behind a disclosure, not the fast path: the four wells // 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 // 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 // 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. // deficiency nothing.
// //
// Committed on a debounce rather than per move. Each move used to write BOTH // Committed on a debounce rather than per move. Each move used to write BOTH
@@ -140,27 +140,27 @@ Column {
HsvRow { HsvRow {
target: "primary"; channel: "h"; channelValue: editor.primaryHsv.h; channelMaximum: 360 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 { HsvRow {
target: "primary"; channel: "s"; channelValue: editor.primaryHsv.s; channelMaximum: 100 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 { HsvRow {
target: "primary"; channel: "v"; channelValue: editor.primaryHsv.v; channelMaximum: 100 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 { HsvRow {
target: "secondary"; channel: "h"; channelValue: editor.secondaryHsv.h; channelMaximum: 360 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 { HsvRow {
target: "secondary"; channel: "s"; channelValue: editor.secondaryHsv.s; channelMaximum: 100 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 { HsvRow {
target: "secondary"; channel: "v"; channelValue: editor.secondaryHsv.v; channelMaximum: 100 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 divider: false
} }
@@ -176,7 +176,7 @@ Column {
} }
// Hands the sliders back to the stored pair. If the write was refused they // 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. // nothing accepted.
Timer { Timer {
id: releaseTimer id: releaseTimer
@@ -1,6 +1,6 @@
// Accessibility. // 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 // 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 // than by which subsystem happens to implement each control. The old page was
// grouped by mechanism (Pointer / Text / Motion / Magnifier / Contrast), which // 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 // the preference at launch, and re-applying the value it is already
// running would be a hyprctl call for nothing every time this page // running would be a hyprctl call for nothing every time this page
// opens. // 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 id: colorFilterRow
property string appliedFilter: "" property string appliedFilter: ""
@@ -164,26 +168,21 @@ SettingsPage {
title: "Hearing" title: "Hearing"
subtitle: "What the desktop does instead of making a sound." subtitle: "What the desktop does instead of making a sound."
ToggleRow { setting: "visualAlerts" } ToggleRow { setting: "visualAlerts"; divider: false }
SettingRow { Item { width: 1; height: 10 }
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
SoundBadge { // Said the same way the keyboard card says its absence, rather than as
anchors.right: parent.right // a row wearing a badge that stands for a control it never had.
anchors.verticalCenter: parent.verticalCenter SettingsNote {
text: "Not yet" headline: "Mono audio is not offered yet"
tone: Theme.fgMuted 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 { SettingsCard {
title: "Keyboard & pointer" 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 { ActionRow {
label: "Key repeat" label: "Key repeat"
@@ -219,7 +218,7 @@ SettingsPage {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 9 spacing: 9
SoundBadge { StatusBadge {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: Accessibility.orcaRunning ? "Running" : "Stopped" text: Accessibility.orcaRunning ? "Running" : "Stopped"
tone: Accessibility.orcaRunning ? Theme.ok : Theme.fgMuted tone: Accessibility.orcaRunning ? Theme.ok : Theme.fgMuted
@@ -239,12 +238,12 @@ SettingsPage {
} }
SettingRow { 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" 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 controlWidth: 90
divider: false divider: false
SoundBadge { StatusBadge {
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Built in" text: "Built in"
@@ -20,7 +20,7 @@ SettingsPage {
property string expandedPicker: "" property string expandedPicker: ""
// Whether the accent's HSV fine-tune is open. Collapsed by default: it is // 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. // usually already answered.
property bool fineTuneOpen: false property bool fineTuneOpen: false
@@ -270,7 +270,7 @@ SettingsPage {
Text { Text {
width: parent.width 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 color: Theme.fgDim
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
@@ -316,15 +316,10 @@ SettingsPage {
: "Based on " + ThemeProfiles.activeProfile.name : "Based on " + ThemeProfiles.activeProfile.name
+ " · every change previews live on the real desktop" + " · every change previews live on the real desktop"
Text { ErrorRow {
width: parent.width label: "The theme needs attention"
visible: wells.lastError !== "" || ColorScheme.lastError !== "" message: wells.lastError !== "" ? wells.lastError : ColorScheme.lastError
text: wells.lastError !== "" ? wells.lastError : ColorScheme.lastError divider: true
color: Theme.warn
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
bottomPadding: 10
} }
ThemeStartChips { ThemeStartChips {
@@ -422,7 +417,7 @@ SettingsPage {
SettingRow { SettingRow {
label: "Application" label: "Application"
detail: "GTK and libadwaita apps that follow the desktop default" detail: "GTK and libadwaita applications that follow the desktop default"
divider: false divider: false
controlWidth: 8 controlWidth: 8
} }
@@ -268,24 +268,20 @@ SettingsPage {
// a process each and only this page wants them, so a shell start where // a process each and only this page wants them, so a shell start where
// nobody opens Applications pays for neither. // nobody opens Applications pays for neither.
Component.onCompleted: { Component.onCompleted: {
AppLibrary.refresh(); if (!AppLibrary.flatpaksLoaded)
AppLibrary.refreshCatalog(); AppLibrary.refresh();
if (!AppLibrary.catalogLoaded)
AppLibrary.refreshCatalog();
} }
TextRow { ErrorRow {
visible: DefaultApps.lastError !== ""
label: "Application settings need attention" label: "Application settings need attention"
detail: DefaultApps.lastError message: DefaultApps.lastError
value: ""
divider: false
} }
TextRow { ErrorRow {
visible: String(AppLibrary.lastError ?? "") !== ""
label: "The application list needs attention" label: "The application list needs attention"
detail: String(AppLibrary.lastError ?? "") message: String(AppLibrary.lastError ?? "")
value: ""
divider: false
} }
// ── What is installed ──────────────────────────────────────────────────── // ── What is installed ────────────────────────────────────────────────────
@@ -361,7 +357,7 @@ SettingsPage {
TextRow { TextRow {
visible: root.installedApps.length > root.installedLimit visible: root.installedApps.length > root.installedLimit
label: (root.installedApps.length - root.installedLimit) + " more" 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: "" value: ""
divider: false divider: false
} }
@@ -369,7 +365,7 @@ SettingsPage {
TextRow { TextRow {
visible: (AppLibrary.apps ?? []).length > 0 && root.installedApps.length === 0 visible: (AppLibrary.apps ?? []).length > 0 && root.installedApps.length === 0
label: "Nothing matches that" 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: "" value: ""
divider: false divider: false
} }
@@ -449,7 +445,7 @@ SettingsPage {
label: AppLibrary.catalogLoaded label: AppLibrary.catalogLoaded
? "The catalog is empty" ? "The catalog is empty"
: "Reading the catalog…" : "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: "" value: ""
divider: false divider: false
} }
@@ -547,7 +543,7 @@ SettingsPage {
TextRow { TextRow {
visible: !DefaultApps.busy && DefaultApps.autostartEntries.length === 0 visible: !DefaultApps.busy && DefaultApps.autostartEntries.length === 0
label: "No user autostart entries" label: "No user autostart entries"
detail: "Applications can add entries to ~/.config/autostart." detail: "Applications can add entries to ~/.config/autostart"
value: "" value: ""
} }
@@ -612,7 +608,7 @@ SettingsPage {
// once at launch and give no sign that nothing had happened. // once at launch and give no sign that nothing had happened.
TextRow { TextRow {
label: "Compositor autostart" 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 value: DefaultApps.luaAutostartEntries.length > 0
? DefaultApps.luaAutostartEntries.length ? DefaultApps.luaAutostartEntries.length
+ (DefaultApps.luaAutostartEntries.length === 1 ? " entry" : " entries") + (DefaultApps.luaAutostartEntries.length === 1 ? " entry" : " entries")
@@ -622,7 +618,7 @@ SettingsPage {
TextRow { TextRow {
visible: !DefaultApps.busy && DefaultApps.luaAutostartEntries.length === 0 visible: !DefaultApps.busy && DefaultApps.luaAutostartEntries.length === 0
label: "No compositor entries found" 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: "" value: ""
divider: false divider: false
} }
@@ -175,7 +175,7 @@ Rectangle {
clip: true clip: true
Accessible.role: Accessible.EditableText 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 // Seeded once, then written only by the guarded handlers
// below and by onSwatchColorChanged. A binding here would // below and by onSwatchColorChanged. A binding here would
@@ -212,7 +212,7 @@ Rectangle {
IconButton { IconButton {
glyph: "◐" glyph: "◐"
title: "Open the colour wheel for " + root.role title: "Open the color wheel for " + root.role
onTriggered: root.wheelRequested() onTriggered: root.wheelRequested()
} }
@@ -118,7 +118,8 @@ SettingsPage {
NetworkTools.active = true; NetworkTools.active = true;
if (!WifiShare.scanned) if (!WifiShare.scanned)
WifiShare.refresh(); WifiShare.refresh();
Vpn.refresh(); if (!Vpn.scanned)
Vpn.refresh();
} }
Component.onDestruction: { Component.onDestruction: {
Connectivity.active = false; Connectivity.active = false;
@@ -137,12 +138,9 @@ SettingsPage {
NetworkTools.refreshDetails(root.wiredConnection); NetworkTools.refreshDetails(root.wiredConnection);
} }
TextRow { ErrorRow {
visible: NetworkTools.lastError !== ""
label: "The network needs attention" label: "The network needs attention"
detail: NetworkTools.lastError message: NetworkTools.lastError
value: ""
divider: false
} }
// ── Wired ──────────────────────────────────────────────────────────────── // ── Wired ────────────────────────────────────────────────────────────────
@@ -406,12 +404,11 @@ SettingsPage {
title: "VPN" title: "VPN"
subtitle: "WireGuard and OpenVPN profiles NetworkManager holds for you." subtitle: "WireGuard and OpenVPN profiles NetworkManager holds for you."
TextRow { ErrorRow {
width: parent.width width: parent.width
visible: Vpn.lastError !== ""
label: "The last VPN action did not finish" label: "The last VPN action did not finish"
detail: Vpn.lastError message: Vpn.lastError
value: "" divider: true
} }
Repeater { Repeater {
@@ -61,22 +61,19 @@ SettingsPage {
}).join(", "); }).join(", ");
} }
Component.onCompleted: Containers.refresh() Component.onCompleted: if (!Containers.scanned) Containers.refresh()
// ── the list ──────────────────────────────────────────────────────────── // ── the list ────────────────────────────────────────────────────────────
TextRow { ErrorRow {
visible: Containers.lastError !== ""
label: "That did not work" label: "That did not work"
detail: Containers.lastError message: Containers.lastError
value: ""
divider: false
} }
TextRow { TextRow {
visible: Containers.scanned && !Containers.available visible: Containers.scanned && !Containers.available
label: "podman is not 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: "" value: ""
divider: false divider: false
} }
@@ -449,7 +446,7 @@ SettingsPage {
TextRow { TextRow {
visible: Containers.scanned && Containers.available && Containers.total === 0 visible: Containers.scanned && Containers.available && Containers.total === 0
label: "No containers" label: "No containers"
detail: "Nothing has been created on this machine yet." detail: "Nothing has been created on this machine yet"
value: "" value: ""
divider: false divider: false
} }
@@ -20,7 +20,7 @@ Item {
// A Canvas reads Theme inside onPaint, where nothing records a dependency // 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 // 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. // 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 readonly property color ringColor: Theme.warn
onSecondsLeftChanged: ring.requestPaint() onSecondsLeftChanged: ring.requestPaint()
@@ -7,7 +7,7 @@
// one of the compositor's window verbs. // one of the compositor's window verbs.
// //
// Nothing typed here becomes a command. The editor reports an enum kind and a // 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 // 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 // is the whole reason there is no "run this command" option: settings.json has
// to stay a file it is safe to hand somebody. // to stay a file it is safe to hand somebody.
@@ -116,7 +116,7 @@ Column {
ChoiceGrid { ChoiceGrid {
width: parent.width width: parent.width
label: "What it does" 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 current: root.kind
options: [ options: [
{ value: "app", label: "Launch an application" }, { value: "app", label: "Launch an application" },
@@ -185,7 +185,7 @@ Column {
width: parent.width width: parent.width
visible: root.kind === "shell" visible: root.kind === "shell"
label: "Shell action" 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 current: root.target
options: root.shellOptions options: root.shellOptions
divider: false divider: false
@@ -199,7 +199,7 @@ Column {
width: parent.width width: parent.width
visible: root.kind === "window" visible: root.kind === "window"
label: "Window & workspace" 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 current: root.target
options: root.windowOptions options: root.windowOptions
divider: false divider: false
@@ -385,10 +385,9 @@ SettingsPage {
// measurement system and not its page size, and deriving Letter from a // measurement system and not its page size, and deriving Letter from a
// country list would be Panama inventing an answer glibc already holds // country list would be Panama inventing an answer glibc already holds
// -- a preview that guesses is worse than a preview that stops. // -- a preview that guesses is worse than a preview that stops.
TextRow { NotMeasuredRow {
label: "Paper size is not previewed" 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." 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"
value: ""
divider: false divider: false
} }
} }
@@ -507,12 +507,9 @@ SettingsPage {
divider: Dictation.lastError !== "" divider: Dictation.lastError !== ""
} }
TextRow { ErrorRow {
visible: Dictation.lastError !== ""
label: "Problem" label: "Problem"
detail: Dictation.lastError message: Dictation.lastError
value: ""
divider: false
} }
} }
@@ -10,7 +10,7 @@
// picture, and whether this is the one the panel was built for. // 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 // 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 // rides the same keep-or-revert transaction; this reports which resolution was
// asked for and lets that funnel do the rest. // asked for and lets that funnel do the rest.
@@ -516,17 +516,7 @@ SettingsPage {
} }
// ── Everything that belongs to no display in particular ───────────────── // ── Everything that belongs to no display in particular ─────────────────
Text { SectionLabel { text: "All displays" }
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
}
// Two cards side by side while there is room for two, and one above the // 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 // other when there is not. This window is tiled: its width is anywhere from
@@ -1,7 +1,7 @@
// Dock. // Dock.
// //
// The pinned applications come first because they are what the Dock is; the // 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 // be shown here as text -- "Instant", "250 ms" -- even though they were already
// stored, mutable integers. They are controls now. // stored, mutable integers. They are controls now.
@@ -80,35 +80,18 @@ SettingsPage {
Repeater { Repeater {
model: Quickshell.screens model: Quickshell.screens
delegate: Rectangle { delegate: SettingsChip {
id: screenPill id: screenPill
required property var modelData required property var modelData
readonly property string screenName: String(screenPill.modelData.name ?? "") 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 || Settings.dockScreens.indexOf(screenPill.screenName) >= 0
onClicked: root.toggleDockScreen(screenPill.screenName)
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) }
} }
} }
} }
@@ -122,7 +122,7 @@ Column {
width: parent.width width: parent.width
visible: !root.busy && root.truncated visible: !root.busy && root.truncated
label: "Only the closest matches are shown" label: "Only the closest matches are shown"
detail: "Narrow the search to reach the rest." detail: "Narrow the search to reach the rest"
value: "" value: ""
divider: false divider: false
} }
@@ -132,7 +132,7 @@ Column {
visible: !root.busy && root.query.length >= root.minimumQuery visible: !root.busy && root.query.length >= root.minimumQuery
&& root.matches.length === 0 && root.matches.length === 0
label: "No file type matches that" 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: "" value: ""
divider: false divider: false
} }
@@ -12,7 +12,6 @@
import Quickshell import Quickshell
import QtQuick import QtQuick
import qs.config
import qs.services import qs.services
SettingsPage { SettingsPage {
@@ -133,14 +132,11 @@ SettingsPage {
return text; return text;
} }
Component.onCompleted: Firewall.refresh() Component.onCompleted: if (!Firewall.scanned) Firewall.refresh()
TextRow { ErrorRow {
visible: Firewall.lastError !== ""
label: "The firewall needs attention" label: "The firewall needs attention"
detail: Firewall.lastError message: Firewall.lastError
value: ""
divider: false
} }
// ── The finding, when there is one ─────────────────────────────────────── // ── The finding, when there is one ───────────────────────────────────────
@@ -378,7 +374,7 @@ SettingsPage {
: (root.addKind === "service" : (root.addKind === "service"
? "That is not a service name firewalld would accept" ? "That is not a service name firewalld would accept"
: "That is not a port 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" action: "Allow"
enabled: root.addValid && !Firewall.busy enabled: root.addValid && !Firewall.busy
divider: false divider: false
@@ -565,43 +561,16 @@ SettingsPage {
Repeater { Repeater {
model: Firewall.allZones ?? [] model: Firewall.allZones ?? []
delegate: Rectangle { delegate: SettingsChip {
id: chip id: chip
required property var modelData required property var modelData
readonly property string zoneName: String(chip.modelData) readonly property string zoneName: String(chip.modelData)
readonly property bool current: root.browsingZone === chip.zoneName
width: chipLabel.implicitWidth + 20 text: chip.zoneName
height: 26 active: root.browsingZone === chip.zoneName
radius: Theme.pillRadius onClicked: root.browsingZone = chip.active ? "" : chip.zoneName
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
}
} }
} }
} }
@@ -85,7 +85,7 @@ Column {
Text { Text {
width: parent.width 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 color: Theme.fgDim
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
@@ -156,37 +156,15 @@ Column {
} }
} }
Rectangle { SettingsChip {
id: addChip id: addChip
height: 26 text: root.adding ? "Cancel" : "+ Add application"
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 { onClicked: {
id: addLabel root.adding = !root.adding;
anchors.centerIn: parent if (!root.adding)
text: root.adding ? "Cancel" : "+ Add app" search.text = "";
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 = "";
}
} }
} }
} }
@@ -215,7 +193,7 @@ Column {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
placeholder: "Search apps" placeholder: "Search applications"
} }
} }
@@ -90,7 +90,7 @@ Column {
case "workspace": case "workspace":
return "on workspace " + String(root.trigger.id ?? 1); return "on workspace " + String(root.trigger.id ?? 1);
case "fullscreen": case "fullscreen":
return "when an app is fullscreen"; return "when an application is fullscreen";
default: default:
return "manually"; return "manually";
} }
@@ -111,7 +111,7 @@ Column {
+ (root.allow.length === 0 + (root.allow.length === 0
? "" ? ""
: " except " + root.allow.length : " except " + root.allow.length
+ (root.allow.length === 1 ? " app" : " apps"))); + (root.allow.length === 1 ? " application" : " applications")));
} }
if (root.mode.keepAwake === true) if (root.mode.keepAwake === true)
effects.push(hypothetical ? "would keep the screen awake" : "keeps the screen awake"); effects.push(hypothetical ? "would keep the screen awake" : "keeps the screen awake");
@@ -349,7 +349,7 @@ Column {
}, },
{ {
value: "fullscreen", 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" detail: "On while a window is fullscreen on the focused display"
} }
] ]
@@ -398,41 +398,30 @@ Column {
spacing: 5 spacing: 5
Repeater { Repeater {
// `name` is what a screen reader says: four of the seven
// initials repeat, so the letter alone is not a day.
model: [ model: [
{ value: 1, label: "M" }, { value: 2, label: "T" }, { value: 1, label: "M", name: "Monday" },
{ value: 3, label: "W" }, { value: 4, label: "T" }, { value: 2, label: "T", name: "Tuesday" },
{ value: 5, label: "F" }, { value: 6, label: "S" }, { value: 3, label: "W", name: "Wednesday" },
{ value: 0, label: "S" } { 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 id: dayPill
required property var modelData required property var modelData
readonly property bool on:
(root.trigger.days ?? []).indexOf(dayPill.modelData.value) >= 0
width: 30 width: 30
height: 28 text: String(dayPill.modelData.label)
radius: 8 active: (root.trigger.days ?? []).indexOf(dayPill.modelData.value) >= 0
color: dayPill.on ? Theme.alpha(Theme.accent, 0.22) Accessible.name: String(dayPill.modelData.name)
: Theme.alpha(Theme.fg, 0.06) onClicked: root.toggleDay(dayPill.modelData.value)
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) }
} }
} }
} }
@@ -42,7 +42,7 @@ SettingsPage {
TextRow { TextRow {
visible: FocusModes.modes.length === 0 visible: FocusModes.modes.length === 0
label: "No focus modes" 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 divider: false
} }
@@ -25,17 +25,15 @@ SettingsPage {
// Polling only while this page is on screen. // Polling only while this page is on screen.
Component.onCompleted: { Component.onCompleted: {
Gaming.refresh(); if (!Gaming.scanned)
Gaming.refresh();
Gaming.watching = true; Gaming.watching = true;
} }
Component.onDestruction: Gaming.watching = false Component.onDestruction: Gaming.watching = false
TextRow { ErrorRow {
visible: Gaming.lastError !== ""
label: "Gaming needs attention" label: "Gaming needs attention"
detail: Gaming.lastError message: Gaming.lastError
value: ""
divider: false
} }
// ── Right now ──────────────────────────────────────────────────────────── // ── Right now ────────────────────────────────────────────────────────────
@@ -46,8 +46,8 @@ Item {
// nothing in common but the digits. // nothing in common but the digits.
property string readout: "" property string readout: ""
// Two or three colours, left to right. Painted across the whole track when // 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 colour // `fullTrack` is set -- for a value whose range is the point, like color
// temperature -- and as the fill alone otherwise. // temperature -- and as the fill alone otherwise.
property var trackColors: [Theme.accent, Theme.accentSecondary] property var trackColors: [Theme.accent, Theme.accentSecondary]
property bool fullTrack: false property bool fullTrack: false
@@ -12,7 +12,7 @@ SettingsPage {
readonly property string ledeText: { readonly property string ledeText: {
if (Health.checks.length === 0) 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"); const count = Health.checks.length + (Health.checks.length === 1 ? " check" : " checks");
if (root.issueChecks.length === 0) if (root.issueChecks.length === 0)
return count + " · everything healthy."; return count + " · everything healthy.";
@@ -43,7 +43,7 @@ SettingsPage {
{ {
group: "panama-tools", group: "panama-tools",
title: "Desktop tools", title: "Desktop tools",
subtitle: "Tracked links, launcher commands, apps, and inhibitors." subtitle: "Tracked links, launcher commands, applications, and inhibitors."
} }
] ]
readonly property var applicationTargets: ({ readonly property var applicationTargets: ({
@@ -432,14 +432,14 @@ SettingsPage {
// umbrella button reading "Open GNOME Settings", which landed on the System // 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 // 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 // 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 // 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 // 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. // 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 // 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. // 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 // pressing the button changed nothing, because the colord daemon that
// applies an ICC profile is not running under Hyprland. A handoff 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, // documents its own uselessness is a dead button with an apology attached,
@@ -18,7 +18,7 @@ SettingsCard {
case "unavailable": case "unavailable":
return Health.lastError || "The latest health check could not be completed."; return Health.lastError || "The latest health check could not be completed.";
case "checking": 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": case "error":
return root.observationCount === 1 return root.observationCount === 1
? "One part of the desktop needs action." ? "One part of the desktop needs action."
@@ -126,27 +126,13 @@ Rectangle {
elide: removeConfirm.armed ? Text.ElideNone : Text.ElideRight elide: removeConfirm.armed ? Text.ElideNone : Text.ElideRight
} }
Rectangle { StatusBadge {
anchors.left: aliasFrame.left anchors.left: aliasFrame.left
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 10 anchors.bottomMargin: 10
width: badgeCopy.implicitWidth + 14
height: 21
radius: Theme.pillRadius
visible: root.featured visible: root.featured
color: Theme.alpha(Theme.accent, 0.1) text: "Control Center"
border.width: 1 tone: Theme.accent
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
}
} }
ConfirmAction { ConfirmAction {
@@ -93,25 +93,11 @@ Rectangle {
elide: Text.ElideRight elide: Text.ElideRight
} }
Rectangle { StatusBadge {
visible: root.featured visible: root.featured
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: featuredCaption.implicitWidth + 16 text: "Control Center"
height: 19 tone: Theme.accent
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
}
} }
} }
@@ -247,7 +247,7 @@ SettingsPage {
TextRow { TextRow {
visible: root.findings.length === 0 visible: root.findings.length === 0
label: "Nothing needs your attention" label: "Nothing needs your attention"
detail: "Anything worth acting on would appear here." detail: "Anything worth acting on would appear here"
value: "" value: ""
divider: false divider: false
} }
@@ -103,7 +103,7 @@ Column {
return name === "" ? "" : Quickshell.iconPath(name, true); 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. // list re-sorts or the search narrows it.
readonly property color tileColor: { readonly property color tileColor: {
const palette = [Theme.accent, Theme.teal, Theme.magenta, Theme.cyan, 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 // Where it came from, said once and in the same place on every
// row -- the difference decides what the body can offer. // row -- the difference decides what the body can offer.
Rectangle { StatusBadge {
id: badge id: badge
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: badgeText.implicitWidth + 14 text: root.flatpak ? "Flatpak" : "System"
height: 17 tone: root.flatpak ? Theme.cyan : Theme.fgMuted
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
}
} }
} }
@@ -91,7 +91,7 @@ SettingsPage {
// The scheme is Panama's own, so a chapter can never accidentally // The scheme is Panama's own, so a chapter can never accidentally
// hand an http link to openSettings, and an unknown page cannot be // hand an http link to openSettings, and an unknown page cannot be
// smuggled in either: SettingsRoutes.resolve turns anything it does // 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. // where there is one, is the section within the page.
onLinkActivated: link => { onLinkActivated: link => {
const target = String(link ?? ""); const target = String(link ?? "");
@@ -18,8 +18,10 @@
// knobs come back up with it: they are about gestures, not about the touchpad. // knobs come back up with it: they are about gestures, not about the touchpad.
import QtQuick import QtQuick
import Quickshell
import qs.config import qs.config
import qs.services import qs.services
import qs.modules.clipboard
SettingsPage { SettingsPage {
id: root id: root
@@ -99,7 +101,7 @@ SettingsPage {
ToggleRow { setting: "leftHanded" } ToggleRow { setting: "leftHanded" }
ToggleRow { ToggleRow {
setting: "middleClickPaste" 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 { OptionPickerRow {
@@ -170,14 +172,19 @@ SettingsPage {
Keybinds.applyReload(); Keybinds.applyReload();
} }
// Nothing, then the shell's own actions, then the compositor's window // Nothing, then an application, then the shell's own actions, then the
// verbs -- the same lists the shortcut editor offers, read from Keybinds so // compositor's window verbs -- the same lists the shortcut editor offers,
// this page never becomes a second opinion on what resolves. // read from Keybinds so this page never becomes a second opinion on what
// resolves.
// //
// Applications are deliberately absent: assigning one means searching // Applications cannot be listed inline: a normal machine has several
// several hundred desktop entries, which a picker of this shape cannot do. // hundred desktop entries and a picker of this shape would be a page nobody
// A gesture that already holds an application (written by the shortcut // can read. So the option is a door rather than a list -- picking it opens
// vocabulary elsewhere) still shows, so it can be read and cleared. // 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 { function gestureOptions(key: string): var {
const out = [{ value: "", label: "Nothing", detail: "The swipe passes through to whatever is under it" }]; const out = [{ value: "", label: "Nothing", detail: "The swipe passes through to whatever is under it" }];
const stored = DesktopPreferences.get(key); const stored = DesktopPreferences.get(key);
@@ -187,6 +194,11 @@ SettingsPage {
label: String(stored.label), label: String(stored.label),
detail: "Application · launch-or-focus" 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) for (const action of Keybinds.shellActions)
out.push({ value: "shell:" + action.target, label: action.label, detail: "Shell action" }); out.push({ value: "shell:" + action.target, label: action.label, detail: "Shell action" });
for (const action of Keybinds.windowActions) for (const action of Keybinds.windowActions)
@@ -194,6 +206,49 @@ SettingsPage {
return out; 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 { SettingsCard {
visible: InputDevices.hasTouchpad visible: InputDevices.hasTouchpad
title: "Gestures" title: "Gestures"
@@ -236,7 +291,67 @@ SettingsPage {
current: root.gestureValue(String(gestureRow.modelData.key)) current: root.gestureValue(String(gestureRow.modelData.key))
enabled: !Keybinds.reloading enabled: !Keybinds.reloading
divider: gestureRow.index < root.gestureKeys.length - 1 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 width: parent.width
spacing: 8 spacing: 8
Text { SectionLabel {
visible: String(roomSection.modelData.name ?? "") !== "" visible: String(roomSection.modelData.name ?? "") !== ""
text: String(roomSection.modelData.name ?? "").toUpperCase() text: String(roomSection.modelData.name ?? "")
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
font.letterSpacing: 0.8
topPadding: 6
} }
Grid { Grid {
@@ -382,15 +376,10 @@ SettingsPage {
} }
} }
Text { ErrorRow {
width: parent.width label: "Home Assistant needs attention"
visible: HomeAssistantConfig.lastError !== "" message: HomeAssistantConfig.lastError
text: HomeAssistantConfig.lastError divider: true
color: Theme.danger
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
bottomPadding: 9
} }
SettingRow { SettingRow {
@@ -432,7 +421,7 @@ SettingsPage {
SettingRow { SettingRow {
label: "Light catalog" 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 divider: false
controlWidth: 176 controlWidth: 176
@@ -3,8 +3,8 @@
// by "because" -- a bare dash invites the reader to assume zero, and zero is // by "because" -- a bare dash invites the reader to assume zero, and zero is
// a measurement. // a measurement.
// //
// NotMeasuredRow { because: "The desktop portal's permission store is not answering, so what has asked for this cannot be read." } // 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 { 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 // `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, // 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 // 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 // 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. // muted or redirected application is obvious without opening it.
// //
// Every write leaves through `edited`/`forgotten`. The page owns the service // Every write leaves through `edited`/`forgotten`. The page owns the service
@@ -66,8 +66,8 @@ Column {
return name === "" ? "" : Quickshell.iconPath(name, true); return name === "" ? "" : Quickshell.iconPath(name, true);
} }
// A stable colour per application, so the tile is a recognisable shape // A stable color per application, so the tile is a recognizable shape
// rather than a different colour every time the list re-sorts. // rather than a different color every time the list re-sorts.
readonly property color tileColor: { readonly property color tileColor: {
const palette = [Theme.accent, Theme.teal, Theme.magenta, Theme.cyan, const palette = [Theme.accent, Theme.teal, Theme.magenta, Theme.cyan,
Theme.green, Theme.orange, Theme.pink]; Theme.green, Theme.orange, Theme.pink];
@@ -263,11 +263,11 @@ Column {
OptionPickerRow { OptionPickerRow {
width: parent.width width: parent.width
label: "Urgency" label: "Urgency"
detail: "Override what the app claims its notifications are" detail: "Override what the application claims its notifications are"
options: [ options: [
{ {
value: "auto", value: "auto",
label: "App decides", label: "Application decides",
detail: "Whatever urgency the notification arrives with" detail: "Whatever urgency the notification arrives with"
}, },
{ {
@@ -287,7 +287,7 @@ Column {
ActionRow { ActionRow {
width: parent.width width: parent.width
label: "Forget this app" label: "Forget this application"
detail: "Remove its rule; it returns on its next notification" detail: "Remove its rule; it returns on its next notification"
action: "Forget" action: "Forget"
divider: false divider: false
@@ -278,32 +278,18 @@ SettingsPage {
SettingsCard { SettingsCard {
title: "Applications" 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 { TextRow {
visible: Notifs.applications.length === 0 visible: Notifs.applications.length === 0
label: "No applications remembered yet" 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 divider: false
} }
Item { SectionLabel {
width: parent.width visible: root.recentApps.length > 0
height: root.recentApps.length > 0 ? 28 : 0 text: "Recent"
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
}
} }
Repeater { Repeater {
@@ -329,23 +315,9 @@ SettingsPage {
} }
} }
Item { SectionLabel {
width: parent.width visible: root.customizedApps.length > 0
height: root.customizedApps.length > 0 ? 28 : 0 text: "Customized"
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
}
} }
Repeater { Repeater {
@@ -400,7 +372,7 @@ SettingsPage {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
placeholder: "Search apps" placeholder: "Search applications"
} }
} }
@@ -168,27 +168,11 @@ SettingsPage {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 8 spacing: 8
Rectangle { StatusBadge {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: HomeAssistant.phase === "ready" visible: HomeAssistant.phase === "ready"
width: homeBadge.implicitWidth + 14 text: "Connected"
height: 17 tone: Theme.ok
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
}
} }
SettingsButton { SettingsButton {
@@ -211,27 +195,11 @@ SettingsPage {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 8 spacing: 8
Rectangle { StatusBadge {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: KdeConnect.pairedCount > 0 visible: KdeConnect.pairedCount > 0
width: phoneBadge.implicitWidth + 14 text: "Paired"
height: 17 tone: Theme.ok
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
}
} }
SettingsButton { SettingsButton {
@@ -259,7 +227,7 @@ SettingsPage {
TextRow { TextRow {
visible: OnlineAccounts.scanned && OnlineAccounts.accounts.length === 0 visible: OnlineAccounts.scanned && OnlineAccounts.accounts.length === 0
label: "No accounts yet" 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: "" value: ""
divider: false divider: false
} }
@@ -372,10 +372,10 @@ SettingsPage {
controlWidth: 160 controlWidth: 160
divider: false divider: false
// SoundBadge is the pill Panama already has for "how this thing is // The state, in the pill the whole settings app uses for one. It
// attached, or why it is not here"; the name is where it was first // is a fact about the session, not a control, which is why there
// needed rather than what it is. // is a badge here and no switch.
SoundBadge { StatusBadge {
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: LidPolicy.inhibited ? "Docked · staying awake" : "Will suspend" 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 { ErrorRow {
visible: Printers.lastError !== ""
label: "Printing needs attention" label: "Printing needs attention"
detail: Printers.lastError message: Printers.lastError
value: ""
divider: false
} }
// ── Which printers exist ───────────────────────────────────────────────── // ── Which printers exist ─────────────────────────────────────────────────
@@ -255,7 +252,7 @@ SettingsPage {
// was built around: holding keeps the job, cancelling throws it // was built around: holding keeps the job, cancelling throws it
// away and the document has to be sent from the app again. // away and the document has to be sent from the app again.
detail: cancelJob.armed 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.printer ?? "") + " · "
+ String(jobRow.modelData.state ?? "") + String(jobRow.modelData.state ?? "")
+ (Number(jobRow.modelData.pages ?? 0) > 0 + (Number(jobRow.modelData.pages ?? 0) > 0
@@ -42,7 +42,7 @@ Rectangle {
return root.measured ? "Idle" : "Not measured"; 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. // dim means idle, and "not measured" must not read as either.
readonly property color toneColor: root.active readonly property color toneColor: root.active
? Theme.warn ? Theme.warn
@@ -58,25 +58,25 @@ SettingsPage {
table: "camera", table: "camera",
label: "Camera", label: "Camera",
detail: "", 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", table: "microphone",
label: "Microphone", label: "Microphone",
detail: "", 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", table: "screencast",
label: "Screen sharing", label: "Screen sharing",
detail: "Can capture the screen", detail: "Can capture the screen",
empty: "No app holds a screen-capture grant." empty: "No application holds a screen-capture grant."
}, },
{ {
table: "remote-desktop", table: "remote-desktop",
label: "Remote desktop", label: "Remote desktop",
detail: "Can move the pointer and type", 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", table: "location",
@@ -89,7 +89,7 @@ SettingsPage {
function askedCount(count: int): string { function askedCount(count: int): string {
if (count === 0) if (count === 0)
return ""; 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. // 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 // An empty table means "nothing has asked" only once the store
// has been read and answered. Before that, and when the // has been read and answered. Before that, and when the
// permission store is not running at all, every table is empty // 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. // is then a measurement nobody took.
TextRow { TextRow {
width: section.width width: section.width
@@ -246,7 +246,7 @@ SettingsPage {
// session, so an app that has shared four times is four // session, so an app that has shared four times is four
// entries and one question. // entries and one question.
detail: grantRow.confirming 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 : (!section.simple && !section.revokeOnly
? (grantRow.modelData.allowed === true ? "Allowed" : "Withheld") ? (grantRow.modelData.allowed === true ? "Allowed" : "Withheld")
: String(section.modelData.detail) : 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 // The limit, stated rather than left to be discovered. Saying "your
// camera is protected" when a system package can open /dev/video0 // camera is protected" when a system package can open /dev/video0
// would be a claim this page cannot back up. // would be a claim this page cannot back up.
TextRow { SettingsNote {
label: "Only sandboxed apps are covered" headline: "Only sandboxed applications 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." 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."
divider: false
} }
} }
@@ -334,8 +335,8 @@ SettingsPage {
visible: Permissions.available visible: Permissions.available
title: "Run in the background" title: "Run in the background"
subtitle: backgroundCard.rows.length === 0 subtitle: backgroundCard.rows.length === 0
? "No app has asked to keep running with no window open." ? "No application has asked to keep running with no window open."
: backgroundCard.rows.length + " app" : backgroundCard.rows.length + " application"
+ (backgroundCard.rows.length === 1 ? " is" : "s are") + (backgroundCard.rows.length === 1 ? " is" : "s are")
+ " allowed to keep running with no window open." + " allowed to keep running with no window open."
@@ -412,10 +413,9 @@ SettingsPage {
divider: Keyring.available || Keyring.lastError !== "" divider: Keyring.available || Keyring.lastError !== ""
} }
SettingRow { ErrorRow {
visible: Keyring.lastError !== ""
label: "Keyring problem" label: "Keyring problem"
detail: Keyring.lastError message: Keyring.lastError
divider: Keyring.available divider: Keyring.available
} }
@@ -544,7 +544,7 @@ SettingsPage {
TextRow { TextRow {
visible: root.showingSecrets && Keyring.listed && Keyring.storedCount === 0 visible: root.showingSecrets && Keyring.listed && Keyring.storedCount === 0
label: "Nothing stored yet" 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: "" value: ""
divider: false divider: false
} }
@@ -553,7 +553,7 @@ SettingsPage {
// ── Traces ─────────────────────────────────────────────────────────────── // ── Traces ───────────────────────────────────────────────────────────────
// //
// Panama's own, now. Each row clears one thing, once, when asked -- there // 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 // 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. // remembered, with a way to stop remembering it.
@@ -569,7 +569,7 @@ SettingsPage {
label: "Recent files" label: "Recent files"
detail: recentsRow.confirming detail: recentsRow.confirming
? "This empties the list. Applications start filling it again as you open things." ? "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.measured
? " · " + Traces.recentsEntries + " entr" ? " · " + Traces.recentsEntries + " entr"
+ (Traces.recentsEntries === 1 ? "y" : "ies") + (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 // A pointer, not a control: the history belongs to Vicinae, which owns
// the database Super+V browses. Offering a Clear here would mean // the database Super+V browses. Offering a Clear here would mean
// reaching into another program's storage behind its back. // reaching into another program's storage behind its back.
TextRow { SettingsNote {
label: "Clipboard history" headline: "Clipboard history is kept by Vicinae"
detail: "Kept by Vicinae, which owns the database Super+V browses. Panama reads that history and does not clear it." body: "Vicinae owns the database Super+V browses. Panama reads that history and does not clear it."
value: ""
divider: Traces.lastError !== ""
} }
SettingRow { ErrorRow {
visible: Traces.lastError !== ""
label: "That did not work" label: "That did not work"
detail: Traces.lastError message: Traces.lastError
divider: false
} }
} }
@@ -787,8 +785,8 @@ SettingsPage {
} }
ActionRow { ActionRow {
label: "Per-app notifications" label: "Per-application notifications"
detail: "Which apps may notify, and how much they may show on a locked screen" detail: "Which applications may notify, and how much they may show on a locked screen"
action: "Open Notifications" action: "Open Notifications"
divider: false divider: false
onTriggered: ShellState.openSettings("notifications") 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 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. 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 the HSV sliders — ends in `ThemeProfiles.commitActive`, which also recomputes
`accentName` as the nearest curated accent. That is what keeps GNOME's accent `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. 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 `SettingsNote` is deliberately quiet and must stay that way. It explains
absences and boundaries — what this session cannot do, and why — and a 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". "something is wrong here".
### The rows are keyboard-reachable ### 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 // 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 // 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 // four headers, four subtitles and four borders to one subject. This is the
// lighter divider: it groups rows without pretending they are separate topics. // 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 QtQuick
import qs.config import qs.config
@@ -41,6 +46,9 @@ Item {
text: root.count text: root.count
color: Theme.alpha(Theme.fgMuted, 0.75) color: Theme.alpha(Theme.fgMuted, 0.75)
font.family: Theme.fontFamily 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.pixelSize: Math.max(9, Theme.fontSizeSmall - 1)
font.letterSpacing: 0.7 font.letterSpacing: 0.7
} }
@@ -7,9 +7,14 @@ Item {
default property alias trailingData: trailing.data default property alias trailingData: trailing.data
property string icon: "" property string icon: ""
property string label: "" 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. // it to Theme.danger so a failure reads as one at a glance.
property color labelColor: Theme.fg 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 detail: ""
property string value: "" property string value: ""
property bool divider: true property bool divider: true
@@ -7,7 +7,7 @@
// } // }
// //
// Deliberately quiet. This is not a warning banner and must never grow one's // 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 // 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 // "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. // 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. // 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 -- // 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 // 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. // that the order stops being a suggestion and starts being a burial.
// //
// The strip scrolls horizontally when it overflows: the Settings window tiles // The strip scrolls horizontally when it overflows: the Settings window tiles
@@ -20,7 +20,7 @@ SettingsPage {
title: "Sharing" title: "Sharing"
lede: "What this machine offers to other machines on the network." lede: "What this machine offers to other machines on the network."
Component.onCompleted: Sharing.refresh() Component.onCompleted: if (!Sharing.scanned) Sharing.refresh()
SettingsCard { SettingsCard {
title: "This machine" title: "This machine"
@@ -29,12 +29,11 @@ SettingsPage {
// The error belongs to the machine, not to a floating banner above the // 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 // title: a bare row above the first card read as part of the page
// furniture, which is exactly what an error must not do. // furniture, which is exactly what an error must not do.
TextRow { ErrorRow {
width: parent.width width: parent.width
visible: Sharing.lastError !== ""
label: "Sharing needs attention" label: "Sharing needs attention"
detail: Sharing.lastError message: Sharing.lastError
value: "" divider: true
} }
TextFieldRow { TextFieldRow {
@@ -78,7 +77,7 @@ SettingsPage {
TextRow { TextRow {
visible: Sharing.remoteLoginOn && Sharing.remoteSessions.length === 0 visible: Sharing.remoteLoginOn && Sharing.remoteSessions.length === 0
label: "Nobody is signed in remotely" 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: "" value: ""
divider: false divider: false
} }
@@ -219,7 +218,7 @@ SettingsPage {
TextRow { TextRow {
visible: Sharing.mediaSharing?.installed !== true visible: Sharing.mediaSharing?.installed !== true
label: "Share music and video to devices" label: "Share music and video to devices"
detail: "Needs Rygel, which is not installed." detail: "Needs Rygel, which is not installed"
value: "Not installed" value: "Not installed"
divider: false divider: false
} }
@@ -88,26 +88,11 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
} }
Rectangle { StatusBadge {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.overridden visible: root.overridden
width: badge.implicitWidth + 14 text: "Changed"
height: 18 tone: Theme.warn
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
}
} }
SettingsButton { SettingsButton {
@@ -433,41 +433,12 @@ SettingsPage {
width: parent ? parent.width : 620 width: parent ? parent.width : 620
spacing: 0 spacing: 0
Item { SectionLabel {
width: parent.width text: groupColumn.modelData.name
height: 30 count: root.filtering
? "· showing " + groupColumn.modelData.binds.length
Row { + " of " + groupColumn.modelData.total
anchors.left: parent.left : "· " + groupColumn.modelData.total
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
}
}
} }
Repeater { Repeater {
@@ -74,14 +74,11 @@ SettingsPage {
return Number((config?.limits ?? ({}))[horizon] ?? 0); return Number((config?.limits ?? ({}))[horizon] ?? 0);
} }
Component.onCompleted: Snapshots.refresh() Component.onCompleted: if (!Snapshots.scanned) Snapshots.refresh()
TextRow { ErrorRow {
visible: Snapshots.lastError !== ""
label: "Snapshots need attention" label: "Snapshots need attention"
detail: Snapshots.lastError message: Snapshots.lastError
value: ""
divider: false
} }
// What just happened to the file that was already there. // What just happened to the file that was already there.
@@ -224,7 +221,7 @@ SettingsPage {
TextRow { TextRow {
visible: volumeCard.snapshots.length === 0 visible: volumeCard.snapshots.length === 0
label: "No points in time yet" 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: "" value: ""
divider: false divider: false
} }
@@ -285,7 +282,7 @@ SettingsPage {
} }
SettingsButton { SettingsButton {
text: pointRow.confirming ? "Keep" : "Delete" text: pointRow.confirming ? "Keep" : "Delete"
enabled: !Snapshots.busy enabled: !Snapshots.busy
onClicked: root.confirmingDelete = onClicked: root.confirmingDelete =
pointRow.confirming ? "" : pointRow.token pointRow.confirming ? "" : pointRow.token
@@ -476,10 +473,10 @@ SettingsPage {
// Honest about what cannot be measured: per-snapshot size needs btrfs // Honest about what cannot be measured: per-snapshot size needs btrfs
// quota groups, which cost performance on every write. Reporting a // quota groups, which cost performance on every write. Reporting a
// made-up number would be worse than saying so. // made-up number would be worse than saying so.
TextRow { NotMeasuredRow {
label: "Space used by snapshots" 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" 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 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) readonly property bool muted: AudioDevices.applicationMuted(chip.modelData)
implicitWidth: name.implicitWidth + muteButton.width + 20 implicitWidth: name.implicitWidth + muteButton.width + 20
implicitHeight: 28 implicitHeight: 26
radius: Theme.pillRadius radius: Theme.pillRadius
color: Theme.alpha(Theme.fg, 0.06) color: Theme.alpha(Theme.fg, 0.06)
border.width: 1 border.width: 1
@@ -1,6 +1,6 @@
// One chip per channel the selected output actually has. // 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 // 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 // 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 // "is the rear left speaker actually rear left" is one click rather than a
@@ -183,7 +183,7 @@ Item {
clip: true clip: true
// Full-width gradient behind a clipped window, so a // 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. // squashed copy of the whole ramp.
Rectangle { Rectangle {
width: meter.width width: meter.width
@@ -298,19 +298,19 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 6 spacing: 6
SoundBadge { StatusBadge {
visible: root.airplay && !root.ghost visible: root.airplay && !root.ghost
text: "AirPlay" text: "AirPlay"
tone: Theme.cyan tone: Theme.cyan
} }
SoundBadge { StatusBadge {
visible: root.bluetooth && !root.ghost visible: root.bluetooth && !root.ghost
text: "Bluetooth" text: "Bluetooth"
tone: Theme.accent tone: Theme.accent
} }
SoundBadge { StatusBadge {
visible: root.ghost visible: root.ghost
text: "Returns when connected" text: "Returns when connected"
tone: Theme.fgMuted tone: Theme.fgMuted
@@ -27,12 +27,20 @@ SettingsPage {
// moves and the region past 100% is marked. // moves and the region past 100% is marked.
readonly property real volumeMaximum: Settings.overAmplification ? 1.5 : 1 readonly property real volumeMaximum: Settings.overAmplification ? 1.5 : 1
// The three services that read the machine rather than the graph do it once // The three services that read the machine rather than the graph are asked
// per page open, the way the Displays page refreshes hardware brightness. // 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: { Component.onCompleted: {
SoundFeedback.refresh(); SoundFeedback.refresh();
SoundDefaults.refresh(); if (!SoundDefaults.loaded)
SoundCards.refresh(); SoundDefaults.refresh();
if (!SoundCards.loaded)
SoundCards.refresh();
} }
SettingsCard { SettingsCard {
@@ -136,17 +144,7 @@ SettingsPage {
} }
// ── What the machine says when it wants attention ─────────────────────── // ── What the machine says when it wants attention ───────────────────────
Text { SectionLabel { text: "Alerts & feedback" }
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
}
SettingsCard { SettingsCard {
title: "Alerts" title: "Alerts"
@@ -189,17 +187,7 @@ SettingsPage {
} }
// ── The hardware underneath the devices ───────────────────────────────── // ── The hardware underneath the devices ─────────────────────────────────
Text { SectionLabel { text: "Advanced" }
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
}
SettingsCard { SettingsCard {
title: "Device profiles" title: "Device profiles"
@@ -157,7 +157,7 @@ SettingsPage {
TextRow { TextRow {
visible: SshKeys.scanned && SshKeys.keys.length === 0 visible: SshKeys.scanned && SshKeys.keys.length === 0
label: "Nothing here yet" 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: "" value: ""
} }
@@ -346,7 +346,7 @@ SettingsPage {
TextRow { TextRow {
visible: SshKeys.agent?.available === true && root.heldKeys.length === 0 visible: SshKeys.agent?.available === true && root.heldKeys.length === 0
label: "Nothing loaded" 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: "" value: ""
} }
@@ -390,12 +390,14 @@ SettingsPage {
// moment later, because it is read back off disk. The Remove button // moment later, because it is read back off disk. The Remove button
// above stays -- the helper refuses and says this, which is a better // above stays -- the helper refuses and says this, which is a better
// answer than a button that is not there. // 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 visible: SshKeys.agent?.available === true && !SshKeys.durableRemoval
label: "Removing a key from this agent does not stick" headline: "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." 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."
value: ""
divider: false
} }
} }
@@ -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) 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() Component.onCompleted: Disks.refresh()
TextRow { ErrorRow {
visible: Disks.lastError !== ""
label: "Storage needs attention" label: "Storage needs attention"
detail: Disks.lastError message: Disks.lastError
value: ""
divider: false
} }
// ── Space ──────────────────────────────────────────────────────────────── // ── Space ────────────────────────────────────────────────────────────────
@@ -113,7 +115,7 @@ SettingsPage {
visible: Disks.breakdownMeasured && Disks.breakdown visible: Disks.breakdownMeasured && Disks.breakdown
&& Disks.breakdown.complete === false && Disks.breakdown.complete === false
label: "The walk ran out of time" 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: "" value: ""
divider: false divider: false
} }
@@ -249,11 +251,10 @@ SettingsPage {
} }
} }
TextRow { NotMeasuredRow {
visible: Disks.foldersMeasured && Disks.folderScanTruncated visible: Disks.foldersMeasured && Disks.folderScanTruncated
label: "Some folders were not measured" label: "Some folders were not measured"
detail: "The walk ran out of time before reaching them, so this list is incomplete." because: "The walk ran out of time before reaching them, so this list is incomplete"
value: ""
divider: false divider: false
} }
} }
@@ -343,7 +344,7 @@ SettingsPage {
TextRow { TextRow {
visible: Disks.cleanablesMeasured && (Disks.cleanables ?? []).length === 0 visible: Disks.cleanablesMeasured && (Disks.cleanables ?? []).length === 0
label: "Nothing itemized" 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: "" value: ""
divider: false divider: false
} }
@@ -473,7 +474,7 @@ SettingsPage {
TextRow { TextRow {
visible: root.removableDrives.length === 0 visible: root.removableDrives.length === 0
label: "None connected" 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: "" value: ""
divider: false divider: false
} }
@@ -13,7 +13,7 @@
// and restoring defaults both used to happen on a single press, which is the // 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 // 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 // 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 QtQuick
import qs.config import qs.config
@@ -219,7 +219,7 @@ Rectangle {
? "Delete " + root.theme.name + ", press again to confirm" ? "Delete " + root.theme.name + ", press again to confirm"
: "Delete " + root.theme.name : "Delete " + root.theme.name
Accessible.description: root.removeArmed 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.focusable: true
Accessible.focused: remove.activeFocus Accessible.focused: remove.activeFocus
@@ -7,7 +7,7 @@
// whole family with it rather than leaving twelve tokens pointing at the old // whole family with it rather than leaving twelve tokens pointing at the old
// one. // 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. // the target is remembered rather than duplicated four times.
import QtQuick import QtQuick
@@ -46,7 +46,7 @@ Grid {
function apply(which: string, hex: string): void { function apply(which: string, hex: string): void {
const value = String(hex).trim().toLowerCase(); const value = String(hex).trim().toLowerCase();
if (!/^#[0-9a-f]{6}$/.test(value)) { 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; return;
} }
root.lastError = ""; root.lastError = "";
@@ -97,7 +97,7 @@ Grid {
ColorDialog { ColorDialog {
id: wheel id: wheel
title: "Choose a colour" title: "Choose a color"
// Qt hands back a color value, not a string; toString() on it is // Qt hands back a color value, not a string; toString() on it is
// "#aarrggbb" when there is alpha, so the channels are read directly. // "#aarrggbb" when there is alpha, so the channels are read directly.
onAccepted: root.apply(root.target, Qt.rgba( onAccepted: root.apply(root.target, Qt.rgba(
@@ -113,7 +113,7 @@ Grid {
} }
onExited: (exitCode, exitStatus) => { onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) 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. // 900px is narrower than its own caption.
import QtQuick import QtQuick
import qs.config
import qs.services import qs.services
Column { Column {
@@ -25,15 +24,7 @@ Column {
readonly property real cellWidth: root.columns > 0 readonly property real cellWidth: root.columns > 0
? (root.width - (root.columns - 1) * 12) / root.columns : 200 ? (root.width - (root.columns - 1) * 12) / root.columns : 200
Text { SectionLabel { text: root.heading }
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
}
Grid { Grid {
width: parent.width width: parent.width
@@ -1,6 +1,6 @@
// Naming what you built. // 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 // 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 // accent hexes. Delete appears only for a saved theme, because a shipped one
// cannot be removed -- it is read from the catalog every launch. // cannot be removed -- it is read from the catalog every launch.
@@ -16,10 +16,10 @@ SettingRow {
label: "Save as" label: "Save as"
// Armed, the row says what the delete actually costs: a saved theme is the // 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 detail: deleteConfirm.armed
? "Deletes “" + String(ThemeProfiles.activeProfile.name ?? "this theme") ? "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 : (root.custom
? "Saving again under a new name keeps both" ? "Saving again under a new name keeps both"
: "Editing a shipped theme forks it; give the fork a name to keep it") : "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 readonly property bool active: chip.modelData.id === ThemeProfiles.activeId
implicitWidth: caption.implicitWidth + dot.width + 26 implicitWidth: caption.implicitWidth + dot.width + 26
implicitHeight: 31 implicitHeight: 26
radius: Theme.pillRadius radius: Theme.pillRadius
color: chip.active color: chip.active
? Theme.alpha(Theme.accent, 0.12) ? Theme.alpha(Theme.accent, 0.12)
@@ -32,7 +32,7 @@ SettingsPage {
// one application is what somebody came here to write, and the gaps and // one application is what somebody came here to write, and the gaps and
// borders below are set once and left alone. // borders below are set once and left alone.
SettingsCard { 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." 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 { Repeater {
@@ -40,7 +40,8 @@ SettingsPage {
property string expandedPackage: "" property string expandedPackage: ""
Component.onCompleted: { Component.onCompleted: {
Updates.refresh(); if (!Updates.scanned)
Updates.refresh();
// A first visit with nothing cached should not show a confident "up to // 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. // date" it has no basis for, so it goes and finds out.
if (!Updates.everChecked && !Updates.checking) if (!Updates.everChecked && !Updates.checking)
@@ -103,12 +104,9 @@ SettingsPage {
// ── What just happened ─────────────────────────────────────────────────── // ── What just happened ───────────────────────────────────────────────────
TextRow { ErrorRow {
visible: Updates.lastError !== ""
label: "Updates need attention" label: "Updates need attention"
detail: Updates.lastError message: Updates.lastError
value: ""
divider: false
} }
TextRow { TextRow {
@@ -288,17 +286,18 @@ SettingsPage {
// A check that failed is not a machine that is current. "Current" is // A check that failed is not a machine that is current. "Current" is
// only ever said about a list that was actually read. // only ever said about a list that was actually read.
TextRow { NotMeasuredRow {
visible: Updates.sourceError("flatpak") !== "" visible: Updates.sourceError("flatpak") !== ""
label: "Flatpak" label: "Flatpak"
detail: Updates.sourceError("flatpak")
value: "Not checked" value: "Not checked"
because: Updates.sourceError("flatpak")
divider: true
} }
TextRow { TextRow {
visible: Updates.sourceError("flatpak") === "" && Updates.flatpak?.available === false visible: Updates.sourceError("flatpak") === "" && Updates.flatpak?.available === false
label: "Flatpak" label: "Flatpak"
detail: "Flatpak is not installed." detail: "Flatpak is not installed"
value: "Unavailable" value: "Unavailable"
} }
@@ -376,18 +375,18 @@ SettingsPage {
onTriggered: Updates.apply("flatpak") onTriggered: Updates.apply("flatpak")
} }
TextRow { NotMeasuredRow {
visible: Updates.sourceError("firmware") !== "" visible: Updates.sourceError("firmware") !== ""
label: "Firmware" label: "Firmware"
detail: Updates.sourceError("firmware")
value: "Not checked" value: "Not checked"
because: Updates.sourceError("firmware")
divider: false divider: false
} }
TextRow { TextRow {
visible: Updates.sourceError("firmware") === "" && Updates.firmware?.available === false visible: Updates.sourceError("firmware") === "" && Updates.firmware?.available === false
label: "Firmware" label: "Firmware"
detail: "Firmware updating is not available on this machine." detail: "Firmware updating is not available on this machine"
value: "Unavailable" value: "Unavailable"
divider: false divider: false
} }
@@ -460,7 +459,7 @@ SettingsPage {
TextRow { TextRow {
visible: Updates.historyLoaded && Updates.history.length === 0 visible: Updates.historyLoaded && Updates.history.length === 0
label: "Nothing recorded" label: "Nothing recorded"
detail: "Neither dnf nor flatpak has a transaction history here." detail: "Neither dnf nor flatpak has a transaction history here"
value: "" value: ""
divider: false divider: false
} }
@@ -155,20 +155,19 @@ SettingsPage {
} }
Component.onCompleted: { Component.onCompleted: {
UserAccounts.refresh(); if (!UserAccounts.scanned)
UserAccounts.refresh();
// Probing fprintd bus-activates it, so it waits for the page rather // Probing fprintd bus-activates it, so it waits for the page rather
// than costing every shell launch. // 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, // 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. // because the thing that refused is almost never the thing being read.
TextRow { ErrorRow {
visible: UserAccounts.lastError !== ""
label: "That change did not go through" label: "That change did not go through"
detail: UserAccounts.lastError message: UserAccounts.lastError
value: ""
divider: false
} }
// ── You ────────────────────────────────────────────────────────────────── // ── You ──────────────────────────────────────────────────────────────────
@@ -466,26 +465,10 @@ SettingsPage {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 8 spacing: 8
Rectangle { StatusBadge {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: noReader.implicitWidth + 14 text: "No reader"
height: 17 tone: Theme.warn
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
}
} }
SettingsButton { SettingsButton {
@@ -513,17 +496,7 @@ SettingsPage {
onToggled: value => Fingerprint.setUnlockEnabled(value) onToggled: value => Fingerprint.setUnlockEnabled(value)
} }
Text { SectionLabel { text: "Enrolled fingers" }
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
}
TextRow { TextRow {
width: parent.width width: parent.width
@@ -620,12 +593,9 @@ SettingsPage {
} }
} }
TextRow { ErrorRow {
visible: Fingerprint.lastError !== ""
label: "The reader had something to say" label: "The reader had something to say"
detail: Fingerprint.lastError message: Fingerprint.lastError
value: ""
divider: false
} }
} }
@@ -110,7 +110,7 @@ Column {
width: parent.width width: parent.width
visible: root.openWindows.length > 0 visible: root.openWindows.length > 0
label: "Open right now" 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 current: root.windowClass
options: root.openWindows options: root.openWindows
divider: false divider: false
@@ -248,7 +248,7 @@ Column {
width: parent.width width: parent.width
visible: root.workspaceOn visible: root.workspaceOn
label: "Workspace" label: "Workspace"
detail: "The ten the keymap reaches." detail: "The ten the keymap reaches"
current: root.workspace current: root.workspace
options: root.workspaceOptions options: root.workspaceOptions
divider: false divider: false
@@ -96,11 +96,9 @@ SettingsPage {
} }
} }
TextRow { ErrorRow {
visible: Projects.lastError !== ""
label: "Problem" label: "Problem"
detail: Projects.lastError message: Projects.lastError
divider: false
} }
} }
} }
@@ -97,7 +97,6 @@ PasswordField 1.0 PasswordField.qml
AudioBalance 1.0 AudioBalance.qml AudioBalance 1.0 AudioBalance.qml
SoundDeviceList 1.0 SoundDeviceList.qml SoundDeviceList 1.0 SoundDeviceList.qml
SoundDeviceRow 1.0 SoundDeviceRow.qml SoundDeviceRow 1.0 SoundDeviceRow.qml
SoundBadge 1.0 SoundBadge.qml
SoundChannelStrip 1.0 SoundChannelStrip.qml SoundChannelStrip 1.0 SoundChannelStrip.qml
SoundVolumeRow 1.0 SoundVolumeRow.qml SoundVolumeRow 1.0 SoundVolumeRow.qml
SoundThemeRow 1.0 SoundThemeRow.qml SoundThemeRow 1.0 SoundThemeRow.qml
@@ -117,6 +116,7 @@ TextEntryRow 1.0 TextEntryRow.qml
PrivacyPage 1.0 PrivacyPage.qml PrivacyPage 1.0 PrivacyPage.qml
PrivacyLiveTile 1.0 PrivacyLiveTile.qml PrivacyLiveTile 1.0 PrivacyLiveTile.qml
SectionLabel 1.0 SectionLabel.qml SectionLabel 1.0 SectionLabel.qml
StatusBadge 1.0 StatusBadge.qml
SearchPicker 1.0 SearchPicker.qml SearchPicker 1.0 SearchPicker.qml
OnlineAccountsPage 1.0 OnlineAccountsPage.qml OnlineAccountsPage 1.0 OnlineAccountsPage.qml
TextFieldRow 1.0 TextFieldRow.qml TextFieldRow 1.0 TextFieldRow.qml
@@ -52,19 +52,19 @@ PanelWindow {
find: "Overview", find: "Overview",
fallback: "Super + `", fallback: "Super + `",
title: "See every window", title: "See every window",
detail: "All your workspaces at once, and search across them." detail: "All your workspaces at once, and search across them"
}, },
{ {
find: "Settings", find: "Settings",
fallback: "Super + I", fallback: "Super + I",
title: "Change anything", title: "Change anything",
detail: "Displays, sound, network, appearance, and everything else." detail: "Displays, sound, network, appearance, and everything else"
}, },
{ {
find: "Keyboard shortcuts", find: "Keyboard shortcuts",
fallback: "Super + /", fallback: "Super + /",
title: "The rest of the keys", title: "The rest of the keys",
detail: "Every shortcut this desktop has, whenever you want it." detail: "Every shortcut this desktop has, whenever you want it"
} }
] ]
@@ -61,7 +61,7 @@ Singleton {
// //
// The theme id alone is not enough: editing a custom theme's background // The theme id alone is not enough: editing a custom theme's background
// keeps the same id, and the shell would repaint from the binding while // keeps the same id, and the shell would repaint from the binding while
// kitty, GTK and the lock screen kept the old colours -- the change would // kitty, GTK and the lock screen kept the old colors -- the change would
// look like it had only half landed. The values are in the key too, so any // look like it had only half landed. The values are in the key too, so any
// edit to the active palette is a change worth pushing. // edit to the active palette is a change worth pushing.
readonly property string palettePrint: { readonly property string palettePrint: {
@@ -192,7 +192,7 @@ Singleton {
// A theme change is a palette change: selecting another theme, or // A theme change is a palette change: selecting another theme, or
// editing the active one. accentName is the NEAREST CURATED name, so // editing the active one. accentName is the NEAREST CURATED name, so
// it can hold still across both -- Moon to Moon Rose keeps "blue" and // it can hold still across both -- Moon to Moon Rose keeps "blue" and
// moves every other colour. // moves every other color.
const themeChanged = force || themeId !== root.appliedThemeId const themeChanged = force || themeId !== root.appliedThemeId
|| palettePrint !== root.appliedPalettePrint; || palettePrint !== root.appliedPalettePrint;
+5 -5
View File
@@ -1,7 +1,7 @@
pragma Singleton pragma Singleton
// Display configuration: resolution, refresh rate, scale, rotation, position, // Display configuration: resolution, refresh rate, scale, rotation, position,
// colour management, variable refresh rate, and mirroring. // color management, variable refresh rate, and mirroring.
// //
// This is the only page in Panama Settings where a wrong value can leave you // This is the only page in Panama Settings where a wrong value can leave you
// unable to SEE the screen well enough to undo it. A mode the display cannot // unable to SEE the screen well enough to undo it. A mode the display cannot
@@ -85,7 +85,7 @@ Singleton {
{ value: 3, label: "Portrait (flipped)" } { value: 3, label: "Portrait (flipped)" }
] ]
// Colour management presets Hyprland accepts as `cm`. "auto" is a policy, // Color management presets Hyprland accepts as `cm`. "auto" is a policy,
// not a state: the compositor resolves it to a concrete preset and reports // not a state: the compositor resolves it to a concrete preset and reports
// that one back, so it is never verified against readback. // that one back, so it is never verified against readback.
readonly property var colorProfiles: [ readonly property var colorProfiles: [
@@ -510,7 +510,7 @@ Singleton {
} }
// Everything past `primary` is optional so that arrangements stored before // Everything past `primary` is optional so that arrangements stored before
// the colour and mirror fields existed still load. Present but invalid is // the color and mirror fields existed still load. Present but invalid is
// not optional: a half-written record is one Panama refuses rather than // not optional: a half-written record is one Panama refuses rather than
// guesses at, exactly as it treats a half-written position. // guesses at, exactly as it treats a half-written position.
function isPersistedLayoutEntry(entry: var): bool { function isPersistedLayoutEntry(entry: var): bool {
@@ -675,7 +675,7 @@ Singleton {
return root.formatBitdepth(String(monitor.currentFormat ?? "")); return root.formatBitdepth(String(monitor.currentFormat ?? ""));
} }
// The colour half of the readback comparison. Each field is asserted only // The color half of the readback comparison. Each field is asserted only
// where the compositor reports something to compare against; vrrMode is // where the compositor reports something to compare against; vrrMode is
// absent by design, since `vrr` reads back live state rather than policy. // absent by design, since `vrr` reads back live state rather than policy.
function matchesColor(monitor: var, requested: var): bool { function matchesColor(monitor: var, requested: var): bool {
@@ -732,7 +732,7 @@ Singleton {
// One-field controls remain callers of the complete-layout transaction. // One-field controls remain callers of the complete-layout transaction.
// Their edit is cloned into the current layout so every output's position, // Their edit is cloned into the current layout so every output's position,
// colour and mirror state participates in apply, verification, and // color and mirror state participates in apply, verification, and
// rollback. `partial` carries only the fields being changed; anything it // rollback. `partial` carries only the fields being changed; anything it
// leaves out keeps the value currentLayout just read from the compositor. // leaves out keeps the value currentLayout just read from the compositor.
function applyRecord(output: string, partial: var): bool { function applyRecord(output: string, partial: var): bool {
@@ -204,7 +204,7 @@ Singleton {
effects.push("keeps the screen awake"); effects.push("keeps the screen awake");
const allow = Array.isArray(mode.allow) ? mode.allow : []; const allow = Array.isArray(mode.allow) ? mode.allow : [];
if (allow.length > 0) if (allow.length > 0)
effects.push(allow.length + (allow.length === 1 ? " app may interrupt" : " apps may interrupt")); effects.push(allow.length + (allow.length === 1 ? " application may interrupt" : " applications may interrupt"));
const when = triggers.length > 0 ? "When " + triggers.join(" or ") : "Never turns on"; const when = triggers.length > 0 ? "When " + triggers.join(" or ") : "Never turns on";
return effects.length > 0 ? when + " · " + effects.join(", ") : when; return effects.length > 0 ? when + " · " + effects.join(", ") : when;
} }
+1 -1
View File
@@ -67,7 +67,7 @@ Singleton {
} }
} }
// "danger" | "warn" | "muted" | "ok". Named rather than a colour: Theme is // "danger" | "warn" | "muted" | "ok". Named rather than a color: Theme is
// a UI concern and this is a service. // a UI concern and this is a service.
readonly property string tone: { readonly property string tone: {
switch (root.headlineState) { switch (root.headlineState) {
@@ -77,7 +77,7 @@ Singleton {
} }
// The devices half of the store, in the shape the applications list has // The devices half of the store, in the shape the applications list has
// always read it in. Kept so that "this app has a privacy rule" keeps // always read it in. Kept so that "this application has a privacy rule" keeps
// working there without that page having to learn about tables. // working there without that page having to learn about tables.
readonly property var devices: { readonly property var devices: {
const known = root.tables; const known = root.tables;
@@ -310,14 +310,14 @@ Singleton {
applyRestoredState.restart(); applyRestoredState.restart();
} }
// The colour, depth and mirror fields a stored arrangement may carry beyond // The color, depth and mirror fields a stored arrangement may carry beyond
// its geometry. Optional exactly as Displays.isPersistedLayoutEntry has // its geometry. Optional exactly as Displays.isPersistedLayoutEntry has
// them: an arrangement written before these existed still restores, and a // them: an arrangement written before these existed still restores, and a
// field that is present but invalid refuses the whole entry rather than // field that is present but invalid refuses the whole entry rather than
// being guessed at. // being guessed at.
// //
// Leaving them out was a real loss, not a cosmetic one. The restored record // Leaving them out was a real loss, not a cosmetic one. The restored record
// is built on top of the LIVE layout, so a snapshot's colour profile, // is built on top of the LIVE layout, so a snapshot's color profile,
// bit depth and SDR levels were silently replaced by whatever the display // bit depth and SDR levels were silently replaced by whatever the display
// is showing right now -- and layoutsEqual, comparing only geometry, then // is showing right now -- and layoutsEqual, comparing only geometry, then
// judged the two identical and skipped the apply that would have put them // judged the two identical and skipped the apply that would have put them
@@ -223,7 +223,7 @@ Singleton {
// permissions" appears twice on purpose: the Applications page answers // permissions" appears twice on purpose: the Applications page answers
// what a Flatpak's sandbox exposes, this one answers what the portal // what a Flatpak's sandbox exposes, this one answers what the portal
// recorded, and they are different questions with the same name. // recorded, and they are different questions with the same name.
{ label: "Background apps", detail: "Which applications may keep running after you close them", page: "privacy" }, { label: "Background applications", detail: "Which applications may keep running after you close them", page: "privacy" },
{ label: "Screen sharing permission", detail: "Take back a screen-capture grant an application was given", page: "privacy" }, { label: "Screen sharing permission", detail: "Take back a screen-capture grant an application was given", page: "privacy" },
{ label: "Remote desktop permission", detail: "Take back a grant to control this desktop's pointer and keyboard", page: "privacy" }, { label: "Remote desktop permission", detail: "Take back a grant to control this desktop's pointer and keyboard", page: "privacy" },
{ label: "Clear recent files", detail: "Empty the list of documents this desktop remembers you opening", page: "privacy" }, { label: "Clear recent files", detail: "Empty the list of documents this desktop remembers you opening", page: "privacy" },
@@ -295,8 +295,8 @@ Singleton {
{ label: "Quiet hours", detail: "The schedule the Sleep focus mode keeps", page: "notifications" }, { label: "Quiet hours", detail: "The schedule the Sleep focus mode keeps", page: "notifications" },
{ label: "Critical alerts break through", detail: "Let urgent notifications past Do Not Disturb", page: "notifications" }, { label: "Critical alerts break through", detail: "Let urgent notifications past Do Not Disturb", page: "notifications" },
{ label: "Application notification rules", detail: "Which applications may notify you, and how", page: "notifications" }, { label: "Application notification rules", detail: "Which applications may notify you, and how", page: "notifications" },
{ label: "Forget an app's notifications", detail: "Remove its rule; it returns on its next notification", page: "notifications" }, { label: "Forget an application's notifications", detail: "Remove its rule; it returns on its next notification", page: "notifications" },
{ label: "Per-app notification sound", detail: "Turn the chime off for one application", page: "notifications" }, { label: "Per-application notification sound", detail: "Turn the chime off for one application", page: "notifications" },
{ label: "Banners or history", detail: "Send one application straight to history without a popup", page: "notifications" }, { label: "Banners or history", detail: "Send one application straight to history without a popup", page: "notifications" },
{ label: "Focus session duration", detail: "How long a focus session runs before it ends itself", page: "focus" }, { label: "Focus session duration", detail: "How long a focus session runs before it ends itself", page: "focus" },
{ label: "Keyboard shortcuts", detail: "Every shortcut the compositor has bound", page: "shortcuts" }, { label: "Keyboard shortcuts", detail: "Every shortcut the compositor has bound", page: "shortcuts" },
@@ -309,8 +309,8 @@ Singleton {
// of the three is a preference with a label to find it by. // of the three is a preference with a label to find it by.
{ label: "Custom shortcut", detail: "Bind your own keys to an application, a shell action, or a window action", page: "shortcuts" }, { label: "Custom shortcut", detail: "Bind your own keys to an application, a shell action, or a window action", page: "shortcuts" },
{ label: "Add a shortcut", detail: "Press the chord you want, then pick what it should do", page: "shortcuts" }, { label: "Add a shortcut", detail: "Press the chord you want, then pick what it should do", page: "shortcuts" },
{ label: "Launch an app with a shortcut", detail: "Give an application its own key combination", page: "shortcuts" }, { label: "Launch an application with a shortcut", detail: "Give an application its own key combination", page: "shortcuts" },
{ label: "App rules", detail: "Per-application window rules: float, centre, size, workspace, and no dimming", page: "tiling" }, { label: "Application window rules", detail: "Per-application window rules: float, center, size, workspace, and no dimming", page: "tiling" },
{ label: "Window rules", detail: "Make one application always float, open on a workspace, or skip the animations", page: "tiling" }, { label: "Window rules", detail: "Make one application always float, open on a workspace, or skip the animations", page: "tiling" },
{ label: "Always float a window", detail: "A per-application rule, so one application stops being tiled", page: "tiling" }, { label: "Always float a window", detail: "A per-application rule, so one application stops being tiled", page: "tiling" },
{ label: "Gestures", detail: "Three-finger swipes as shipped, and four-finger swipes you assign yourself", page: "mouse" }, { label: "Gestures", detail: "Three-finger swipes as shipped, and four-finger swipes you assign yourself", page: "mouse" },
@@ -333,10 +333,10 @@ Singleton {
{ label: "Screen reader", detail: "Start Orca and see whether the accessibility bus is up", page: "accessibility" }, { label: "Screen reader", detail: "Start Orca and see whether the accessibility bus is up", page: "accessibility" },
{ label: "Orca", detail: "The screen reader: whether it is running, and starting or stopping it", page: "accessibility" }, { label: "Orca", detail: "The screen reader: whether it is running, and starting or stopping it", page: "accessibility" },
{ label: "Sticky keys", detail: "Why sticky, slow and bounce keys are not offered in this session", page: "accessibility" }, { label: "Sticky keys", detail: "Why sticky, slow and bounce keys are not offered in this session", page: "accessibility" },
// The colour filter is a compositor shader rather than a switch, so it // The color filter is a compositor shader rather than a switch, so it
// is findable by the condition rather than only by the word "filter". // is findable by the condition rather than only by the word "filter".
{ label: "Color filter", detail: "A whole-screen filter the compositor renders: grayscale, or one for each kind of colour blindness", page: "accessibility" }, { label: "Color filter", detail: "A whole-screen filter the compositor renders: grayscale, or one for each kind of color blindness", page: "accessibility" },
{ label: "Grayscale", detail: "Drain the colour out of the whole screen", page: "accessibility" }, { label: "Grayscale", detail: "Drain the color out of the whole screen", page: "accessibility" },
{ label: "Color blindness", detail: "Protanopia, deuteranopia and tritanopia filters applied to the whole screen", page: "accessibility" }, { label: "Color blindness", detail: "Protanopia, deuteranopia and tritanopia filters applied to the whole screen", page: "accessibility" },
{ label: "Dictation", detail: "Speech to text with Super+D, typed where the cursor is", page: "dictation" }, { label: "Dictation", detail: "Speech to text with Super+D, typed where the cursor is", page: "dictation" },
{ label: "Speech to text", detail: "Set up the local speech server and model", page: "dictation" }, { label: "Speech to text", detail: "Set up the local speech server and model", page: "dictation" },
@@ -354,8 +354,8 @@ Singleton {
{ label: "Rotation", detail: "Turn a display a quarter, half, or three-quarter turn", page: "displays" }, { label: "Rotation", detail: "Turn a display a quarter, half, or three-quarter turn", page: "displays" },
{ label: "HDR", detail: "High dynamic range for a display that supports it", page: "displays" }, { label: "HDR", detail: "High dynamic range for a display that supports it", page: "displays" },
{ label: "Color profile", detail: "Automatic, sRGB, wide gamut, or HDR per display", page: "displays" }, { label: "Color profile", detail: "Automatic, sRGB, wide gamut, or HDR per display", page: "displays" },
{ label: "Bit depth", detail: "8-bit or 10-bit colour, per display", page: "displays" }, { label: "Bit depth", detail: "8-bit or 10-bit color, per display", page: "displays" },
{ label: "10-bit", detail: "Deeper colour with less gradient banding", page: "displays" }, { label: "10-bit", detail: "Deeper color with less gradient banding", page: "displays" },
{ label: "SDR brightness", detail: "How bright ordinary content sits inside HDR", page: "displays" }, { label: "SDR brightness", detail: "How bright ordinary content sits inside HDR", page: "displays" },
{ label: "Mirror displays", detail: "Show the same picture on a second display", page: "displays" }, { label: "Mirror displays", detail: "Show the same picture on a second display", page: "displays" },
{ label: "Variable refresh rate", detail: "Override the gaming policy for one display", page: "displays" }, { label: "Variable refresh rate", detail: "Override the gaming policy for one display", page: "displays" },
@@ -372,7 +372,7 @@ Singleton {
{ label: "Tokyo Night", detail: "Moon and Day, the shipped defaults", page: "appearance", section: "themes" }, { label: "Tokyo Night", detail: "Moon and Day, the shipped defaults", page: "appearance", section: "themes" },
{ label: "Video wallpaper", detail: "A looping video as the desktop background", page: "appearance", section: "background" }, { label: "Video wallpaper", detail: "A looping video as the desktop background", page: "appearance", section: "background" },
{ label: "Advanced accent", detail: "Adjust primary and secondary hue, saturation, and value", page: "appearance", section: "editor" }, { label: "Advanced accent", detail: "Adjust primary and secondary hue, saturation, and value", page: "appearance", section: "editor" },
{ label: "Pick colour from screen", detail: "Sample an accent colour with hyprpicker", page: "appearance" } { label: "Pick color from screen", detail: "Sample an accent color with hyprpicker", page: "appearance" }
] ]
function pageFor(group: string): string { function pageFor(group: string): string {
@@ -112,7 +112,7 @@ Singleton {
// node, and the node is gone -- so the stored name is all there is. These // node, and the node is gone -- so the stored name is all there is. These
// names are structured, though, and the structure carries the useful part: // names are structured, though, and the structure carries the useful part:
// the Bluetooth address, the AirPlay speaker's hostname, the USB device's // the Bluetooth address, the AirPlay speaker's hostname, the USB device's
// product string. Anything this does not recognise is tidied rather than // product string. Anything this does not recognize is tidied rather than
// guessed at, because a wrong name is worse than an ugly one. // guessed at, because a wrong name is worse than an ugly one.
function label(name: string): string { function label(name: string): string {
const raw = String(name ?? "").trim(); const raw = String(name ?? "").trim();
+10 -7
View File
@@ -73,7 +73,7 @@ reachable afterwards. Panama's reads the chords from the live keymap so it
cannot teach keys the machine does not have. cannot teach keys the machine does not have.
**HyDE** ([wallbash templates](https://github.com/HyDE-Project/HyDE)) and **HyDE** ([wallbash templates](https://github.com/HyDE-Project/HyDE)) and
omarchy's `default/themed/`: colour values as data consumed by templates rather omarchy's `default/themed/`: color values as data consumed by templates rather
than restated per consumer. Panama took the principle and not the template than restated per consumer. Panama took the principle and not the template
engine: `config/palette.json` is now the single source for everything outside engine: `config/palette.json` is now the single source for everything outside
QML, which removed six hand-synced copies of the eight accents. A per-app QML, which removed six hand-synced copies of the eight accents. A per-app
@@ -83,28 +83,31 @@ waiting beneficiary.
**caelestia** ([caelestia-dots/shell](https://github.com/caelestia-dots/shell)): **caelestia** ([caelestia-dots/shell](https://github.com/caelestia-dots/shell)):
lightweight system toasts as a category distinct from the OSD and the lightweight system toasts as a category distinct from the OSD and the
notification centre. Panama already had this as `StatusEvents`, so what was notification center. Panama already had this as `StatusEvents`, so what was
taken was the observation that a charger belongs in it; the layer itself needed taken was the observation that a charger belongs in it; the layer itself needed
no work. no work.
**DankMaterialShell** ([repo](https://github.com/AvengeMedia/DankMaterialShell)): **DankMaterialShell** ([repo](https://github.com/AvengeMedia/DankMaterialShell)):
surfacing agent usage in the bar, and a plugin registry with a lockfile. The surfacing agent usage in the bar, and a plugin registry with a lockfile. The
first is planned; the second remains deferred, see below. first shipped, as a bar widget with a switch of its own on Shell Bar; the
second remains deferred, see below.
### Declined, and why ### Declined, and why
- **Wallpaper-derived dynamic colour** (HyDE wallbash, matugen). The headline - **Wallpaper-derived dynamic color** (HyDE wallbash, matugen). The headline
feature of this whole genre, and declined on identity: Tokyo Night Moon is a feature of this whole genre, and declined on identity: Tokyo Night Moon is a
deliberate choice, and dynamic colour is how rices end up looking like each deliberate choice, and dynamic color is how rices end up looking like each
other. Reconsider as one more theme profile, never as the default. other. Reconsider as one more theme profile, never as the default.
- **A per-app template engine for theming.** See above. - **A per-app template engine for theming.** See above.
- **Audio visualisers.** A continuously repainting equaliser is exactly the - **Audio visualizers.** A continuously repainting equalizer is exactly the
GPU-pegging animation this project avoids on high-refresh displays. GPU-pegging animation this project avoids on high-refresh displays.
- **Theming applications by patching their internals** (Spotify, Discord). - **Theming applications by patching their internals** (Spotify, Discord).
Breaks on every application update; the maintenance is unbounded and falls on Breaks on every application update; the maintenance is unbounded and falls on
whoever is holding the desktop that day. whoever is holding the desktop that day.
- **A plugin host.** Still premature, for the reason already recorded below. - **A plugin host.** Still premature, for the reason already recorded below.
User hooks are the thirty-line version and are the next thing to build. User hooks were the thirty-line version and shipped instead: `panama-hook`
runs `~/.config/panama/hooks/<name>` and its `<name>.d/` directory for
`theme-set`, `post-upgrade` and `post-migrate`.
- **Package channels and pacman hooks.** Arch-specific, and blocking - **Package channels and pacman hooks.** Arch-specific, and blocking
`dnf upgrade` on a Fedora workstation would be hostile. `dnf upgrade` on a Fedora workstation would be hostile.
+1 -1
View File
@@ -50,7 +50,7 @@ grep -qE 'function measureBreakdown|function measureCleanables' "$service" \
# Both walks are asked for, never done on open: they cost the same as the # Both walks are asked for, never done on open: they cost the same as the
# folder scan the page already refuses to start by itself. # folder scan the page already refuses to start by itself.
grep -qE 'Component\.onCompleted:.*Disks\.(scan|measureBreakdown|measureCleanables)' "$page" \ grep -qE 'Component\.onCompleted:.*Disks\.(scan|measureBreakdown|measureCleanables)\b' "$page" \
&& fail 'the page starts an expensive walk the moment it opens' && fail 'the page starts an expensive walk the moment it opens'
# The expensive read must not run on open; that is the entire reason it is a # The expensive read must not run on open; that is the entire reason it is a
@@ -347,10 +347,10 @@ for (const [required, why] of [
} }
// Wording the spec pins, because it is the only place the consequence of // Wording the spec pins, because it is the only place the consequence of
// forgetting is explained. // forgetting is explained.
if (!familyText.includes("Forget this app")) if (!familyText.includes("Forget this application"))
fail("the expanded row has no Forget this app action"); fail("the expanded row has no Forget this application action");
if (!/returns on its next notification/.test(familyText)) if (!/returns on its next notification/.test(familyText))
fail("Forget this app does not say the rule comes back when the application notifies again"); fail("Forget this application does not say the rule comes back when the application notifies again");
// The Quiet card owns the breakthrough switch, and it belongs on the page the // The Quiet card owns the breakthrough switch, and it belongs on the page the
// notifications schema group routes to. // notifications schema group routes to.
+1 -1
View File
@@ -110,7 +110,7 @@ hidden network|Hidden network|connectivity
static ip|Static IP address|connectivity static ip|Static IP address|connectivity
color filter|Color filter|accessibility color filter|Color filter|accessibility
custom shortcut|Custom shortcut|shortcuts custom shortcut|Custom shortcut|shortcuts
app rules|App rules|tiling application window rules|Application window rules|tiling
window rules|Window rules|tiling window rules|Window rules|tiling
do not disturb|Do Not Disturb|notifications do not disturb|Do Not Disturb|notifications
four-finger swipe|Four-finger swipe|mouse four-finger swipe|Four-finger swipe|mouse
+1 -1
View File
@@ -32,7 +32,7 @@ device_row="$settings/SoundDeviceRow.qml"
application_mixer="$settings/ApplicationMixer.qml" application_mixer="$settings/ApplicationMixer.qml"
application_row="$settings/ApplicationVolumeRow.qml" application_row="$settings/ApplicationVolumeRow.qml"
balance="$settings/AudioBalance.qml" balance="$settings/AudioBalance.qml"
badge="$settings/SoundBadge.qml" badge="$settings/StatusBadge.qml"
capture_row="$settings/SoundCaptureRow.qml" capture_row="$settings/SoundCaptureRow.qml"
channel_strip="$settings/SoundChannelStrip.qml" channel_strip="$settings/SoundChannelStrip.qml"
theme_row="$settings/SoundThemeRow.qml" theme_row="$settings/SoundThemeRow.qml"