From 538c0a887c781240ba8fffe019710ddf11972391 Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Fri, 21 Aug 2026 12:57:47 -0400 Subject: [PATCH] Save things where the rest of the software already saves them Screenshots and recordings offered three folders to choose between, and three guesses cannot include the folder somebody's other software already writes to -- which is the only folder that matters. This machine has had ~/Pictures/Screenshots and ~/Videos/Screencasts since long before Panama, and Panama was writing recordings to a Videos/Recordings it invented. Both are free text now, and the recording default is the folder that was already there. Wallpapers were swept from four directories at once, so the distribution's stock images arrived mixed in with the user's own and there was no way to ask for just one. Where wallpapers live is something somebody knows about their own machine. It is a setting, not a search. All three accept an absolute path as well as one relative to home, which meant fixing Capture: it prefixed $HOME unconditionally, so naming /mnt/captures would have written screenshots to ~/mnt/captures and left nobody able to find them. The generator turned out to skip any entry whose comment sits inside the braces rather than above them -- it looks for `key:` immediately after `{`. Three settings were invisible in the reference because of it, one of them dockScreens, which has never appeared there at all. The staleness contract could not see it either: regenerating reproduced the same omission, so the copy was current and incomplete at once. It now counts what was declared against what it could read and refuses rather than quietly documenting less than exists. Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1 --- .../quickshell/config/PreferenceSchema.qml | 48 ++++++++++++------- .../modules/settings/AppearancePage.qml | 4 +- .../settings/ScreenIntelligencePage.qml | 4 +- .../modules/settings/WallpaperPicker.qml | 2 +- .../quickshell/scripts/panama-settings-docs | 16 +++++++ config/dot/quickshell/services/Capture.qml | 20 +++++++- config/dot/quickshell/services/Wallpaper.qml | 20 +++++--- docs/settings.md | 8 ++-- tests/quickshell/settings-pages-contract | 7 ++- 9 files changed, 94 insertions(+), 35 deletions(-) diff --git a/config/dot/quickshell/config/PreferenceSchema.qml b/config/dot/quickshell/config/PreferenceSchema.qml index de3ef43..5efa352 100644 --- a/config/dot/quickshell/config/PreferenceSchema.qml +++ b/config/dot/quickshell/config/PreferenceSchema.qml @@ -101,11 +101,12 @@ Singleton { { value: "right", label: "Right" } ] }, + + // A "json" value: the screen names the Dock appears on. Empty means + // every screen, which is both the sensible default and the right + // answer for the common single-monitor case -- storing a list of + // names there would go stale the moment a display is unplugged. { - // A "json" value: the screen names the Dock appears on. Empty means - // every screen, which is both the sensible default and the right - // answer for the common single-monitor case -- storing a list of - // names there would go stale the moment a display is unplugged. key: "dockScreens", type: "json", def: [], group: "dock", label: "Screens", detail: "Which displays show the Dock" @@ -859,6 +860,19 @@ Singleton { // ── Desktop background ────────────────────────────────────────────── // Applied through hyprpaper's IPC. Not an hl.config option, so it has // no `hypr` block; services/Wallpaper.qml owns applying it. + + // One folder, not four. The picker used to sweep ~/Pictures/Wallpapers, + // ~/Pictures/Backgrounds, ~/.local/share/backgrounds and + // /usr/share/backgrounds, which meant the distribution's stock + // images turned up mixed in with the user's own and there was no + // way to say "only mine". Where wallpapers live is a thing somebody + // knows about their own machine; it is a setting, not a search. + { + key: "wallpaperDir", type: "string", def: "Pictures/Wallpapers", group: "wallpaper", + pattern: "^~?/?[A-Za-z0-9 ._/+@'-]{1,160}$", + label: "Wallpaper folder", + detail: "Where the picker looks. Relative to your home folder unless it starts with /" + }, { key: "wallpaperPath", type: "string", def: "", group: "wallpaper", // Reaches hyprpaper as the "," argument form, so a @@ -1271,25 +1285,23 @@ Singleton { // Directories and encoder arguments are enums rather than free text: // both are handed to a recorder process, and an arbitrary string there // is a much larger surface than a settings page needs to expose. + + // Free text rather than three choices. The three were a guess at + // where somebody keeps screenshots, and a guess cannot include the + // folder the rest of their software already writes to -- which is + // the only folder that matters. A path starting with / is taken as + // absolute, so a drive that is not under home works too. { - key: "screenshotDir", type: "enum", def: "Pictures/Screenshots", group: "capture", + key: "screenshotDir", type: "string", def: "Pictures/Screenshots", group: "capture", + pattern: "^~?/?[A-Za-z0-9 ._/+@'-]{1,160}$", label: "Screenshot folder", - detail: "Folder under your home directory for screenshots", - options: [ - { value: "Pictures/Screenshots", label: "Pictures / Screenshots" }, - { value: "Pictures", label: "Pictures" }, - { value: "Desktop", label: "Desktop" } - ] + detail: "Where screenshots are saved. Relative to your home folder unless it starts with /" }, { - key: "recordingDir", type: "enum", def: "Videos/Recordings", group: "capture", + key: "recordingDir", type: "string", def: "Videos/Screencasts", group: "capture", + pattern: "^~?/?[A-Za-z0-9 ._/+@'-]{1,160}$", label: "Recording folder", - detail: "Folder under your home directory for screen recordings", - options: [ - { value: "Videos/Recordings", label: "Videos / Recordings" }, - { value: "Videos", label: "Videos" }, - { value: "Desktop", label: "Desktop" } - ] + detail: "Where screen recordings are saved. Relative to your home folder unless it starts with /" }, { key: "recorderArgs", type: "enum", def: "-c h264_vaapi -d /dev/dri/renderD128", diff --git a/config/dot/quickshell/modules/settings/AppearancePage.qml b/config/dot/quickshell/modules/settings/AppearancePage.qml index de6ab49..f590ea0 100644 --- a/config/dot/quickshell/modules/settings/AppearancePage.qml +++ b/config/dot/quickshell/modules/settings/AppearancePage.qml @@ -74,7 +74,9 @@ SettingsPage { title: "Background" subtitle: Wallpaper.lastError !== "" ? Wallpaper.lastError - : "Applied to every display. Looked for in ~/Pictures/Wallpapers, ~/Pictures/Backgrounds, ~/.local/share/backgrounds, and /usr/share/backgrounds." + : "Applied to every display, from the folder below." + + TextEntryRow { setting: "wallpaperDir"; placeholder: "Pictures/Wallpapers" } WallpaperControls { id: wallpaperControls diff --git a/config/dot/quickshell/modules/settings/ScreenIntelligencePage.qml b/config/dot/quickshell/modules/settings/ScreenIntelligencePage.qml index 4af33c3..e68f74c 100644 --- a/config/dot/quickshell/modules/settings/ScreenIntelligencePage.qml +++ b/config/dot/quickshell/modules/settings/ScreenIntelligencePage.qml @@ -45,8 +45,8 @@ SettingsPage { title: "Capture preferences" subtitle: "Choose where captures go and how recordings are encoded." - ChoiceRow { setting: "screenshotDir" } - ChoiceRow { setting: "recordingDir" } + TextEntryRow { setting: "screenshotDir"; placeholder: "Pictures/Screenshots" } + TextEntryRow { setting: "recordingDir"; placeholder: "Videos/Screencasts" } ChoiceRow { setting: "recorderArgs"; divider: false } } diff --git a/config/dot/quickshell/modules/settings/WallpaperPicker.qml b/config/dot/quickshell/modules/settings/WallpaperPicker.qml index d2f892e..dcec498 100644 --- a/config/dot/quickshell/modules/settings/WallpaperPicker.qml +++ b/config/dot/quickshell/modules/settings/WallpaperPicker.qml @@ -218,7 +218,7 @@ Item { width: parent.width - 40 horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap - text: "No images found. Looked in ~/Pictures/Wallpapers, ~/Pictures/Backgrounds, ~/.local/share/backgrounds, and /usr/share/backgrounds." + text: "No images in " + Wallpaper.searchRoots[0] + ". Change the folder above, or put some pictures there." color: Theme.fgMuted font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall diff --git a/config/dot/quickshell/scripts/panama-settings-docs b/config/dot/quickshell/scripts/panama-settings-docs index 68d6333..3811825 100755 --- a/config/dot/quickshell/scripts/panama-settings-docs +++ b/config/dot/quickshell/scripts/panama-settings-docs @@ -73,6 +73,22 @@ def read_entries(): # Each entry begins at `key:` and ends at the closing brace of its block. # Nested braces (options, hypr) are skipped by counting depth. entries = [] + # An entry is only found when `key:` is the first thing inside its brace, so + # a comment written INSIDE the literal rather than above it makes the whole + # setting invisible here -- silently, and the staleness contract cannot see + # it either, because regenerating reproduces the same omission. Three + # settings were undocumented this way before anyone noticed, one of them for + # weeks. Counting what was declared against what was parsed is what turns + # that from silence into an error. + declared = {m.group(1) for m in re.finditer(r'\bkey:\s*"([^"]+)"', text)} + parsed = {m.group(1) for m in re.finditer(r'\{\s*\n\s*key:\s*"([^"]+)"', text)} + invisible = sorted(declared - parsed) + if invisible: + raise SchemaError( + "these settings are declared but cannot be read, which means they would " + "be silently missing from the reference: " + ", ".join(invisible) + + ". Move the comment above the entry's opening brace.") + for match in re.finditer(r'\{\s*\n\s*key:\s*"([^"]+)"', text): name = match.group(1) start = match.start() diff --git a/config/dot/quickshell/services/Capture.qml b/config/dot/quickshell/services/Capture.qml index 48606b6..d99e1e0 100644 --- a/config/dot/quickshell/services/Capture.qml +++ b/config/dot/quickshell/services/Capture.qml @@ -77,8 +77,24 @@ Singleton { // ── Paths ─────────────────────────────────────────────────────────────── readonly property string _home: Quickshell.env("HOME") || "/home/gib" - readonly property string shotDir: root._home + "/" + Settings.screenshotDir - readonly property string recDir: root._home + "/" + Settings.recordingDir + + // Both folders are settings now rather than a choice of three, so both can + // name somewhere outside home -- a second drive, a network mount. Prefixing + // $HOME unconditionally, as this did, would have turned /mnt/captures into + // ~/mnt/captures and written screenshots somewhere nobody looks. + function _resolve(folder: string): string { + const trimmed = String(folder ?? "").trim(); + if (trimmed === "") + return root._home; + if (trimmed.startsWith("/")) + return trimmed; + if (trimmed.startsWith("~/")) + return root._home + trimmed.slice(1); + return root._home + "/" + trimmed; + } + + readonly property string shotDir: root._resolve(Settings.screenshotDir) + readonly property string recDir: root._resolve(Settings.recordingDir) property string _freezePath: "" property string _prevFreezePath: "" diff --git a/config/dot/quickshell/services/Wallpaper.qml b/config/dot/quickshell/services/Wallpaper.qml index ec4f15e..8f27527 100644 --- a/config/dot/quickshell/services/Wallpaper.qml +++ b/config/dot/quickshell/services/Wallpaper.qml @@ -51,12 +51,20 @@ Singleton { return root.primaryFirstOutputs(outputs); } - readonly property var searchRoots: [ - `${Quickshell.env("HOME")}/Pictures/Wallpapers`, - `${Quickshell.env("HOME")}/Pictures/Backgrounds`, - `${Quickshell.env("HOME")}/.local/share/backgrounds`, - "/usr/share/backgrounds" - ] + // One folder, chosen in Settings, rather than four swept blindly. A leading + // slash means an absolute path -- a drive that is not under home -- and + // anything else is read relative to it, which is how the capture folders + // work too. + readonly property var searchRoots: { + const configured = String(DesktopPreferences.get("wallpaperDir") ?? "").trim(); + const folder = configured === "" ? "Pictures/Wallpapers" : configured; + const home = Quickshell.env("HOME"); + if (folder.startsWith("/")) + return [folder]; + if (folder.startsWith("~/")) + return [home + folder.slice(1)]; + return [`${home}/${folder}`]; + } Process { id: scan diff --git a/docs/settings.md b/docs/settings.md index 67307ff..a4887d3 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -4,7 +4,7 @@ Do not edit this file. Run `quickshell/scripts/panama-settings-docs` after changing the schema; a contract fails when this copy is stale. -137 settings across 27 groups. 70 of them are applied to the compositor and confirmed by reading the value back. +139 settings across 27 groups. 70 of them are applied to the compositor and confirmed by reading the value back. ## accessibility @@ -35,8 +35,8 @@ Found on **Screen Intelligence**. | Setting | Default | What it does | |---|---|---| -| **Screenshot folder**
`screenshotDir` | Pictures/Screenshots | Folder under your home directory for screenshots Choices: Pictures / Screenshots, Pictures, Desktop. | -| **Recording folder**
`recordingDir` | Videos/Recordings | Folder under your home directory for screen recordings Choices: Videos / Recordings, Videos, Desktop. | +| **Screenshot folder**
`screenshotDir` | Pictures/Screenshots | Where screenshots are saved. Relative to your home folder unless it starts with / | +| **Recording folder**
`recordingDir` | Videos/Screencasts | Where screen recordings are saved. Relative to your home folder unless it starts with / | | **Recording encoder**
`recorderArgs` | -c h264_vaapi -d /dev/dri/renderD128 | Hardware encoding keeps recording off the processor while gaming Choices: VAAPI H.264, VAAPI HEVC, CPU x264. | ## clock @@ -68,6 +68,7 @@ Found on **Desktop & Dock**. |---|---|---| | **Automatically hide the Dock**
`dockAutohide` | true | Reveal it at the bottom edge when a workspace is occupied | | **Position**
`dockPosition` | bottom | Which edge the Dock lives on Choices: Bottom, Left, Right. | +| **Screens**
`dockScreens` | [] | Which displays show the Dock | | **Icon size**
`dockIconSize` | 48 px | How large the Dock's application icons are drawn. Range 32–80. | | **Reveal delay**
`dockRevealDelayMs` | 0 ms | Zero reveals the Dock the instant the pointer reaches the edge. Range 0–1000. | | **Hide delay**
`dockHideDelayMs` | 250 ms | Prevents flicker when crossing between icons. Range 0–2000. | @@ -303,6 +304,7 @@ Found on **Appearance**. | Setting | Default | What it does | |---|---|---| +| **Wallpaper folder**
`wallpaperDir` | Pictures/Wallpapers | Where the picker looks. Relative to your home folder unless it starts with / | | **Wallpaper**
`wallpaperPath` | — | Shown on every output | | **Wallpaper mode**
`wallpaperMode` | single | Use one image, rotate a collection, or choose per display Choices: Single, Slideshow, Per display. | | **Change background every**
`wallpaperIntervalMinutes` | 30 min | Time between slideshow images. Range 5–1440. | diff --git a/tests/quickshell/settings-pages-contract b/tests/quickshell/settings-pages-contract index 1d9780e..4a1f54a 100755 --- a/tests/quickshell/settings-pages-contract +++ b/tests/quickshell/settings-pages-contract @@ -76,8 +76,11 @@ raise SystemExit(0 if block and re.search(r'zeroLabel\s*:\s*"Never"', block.grou PY intelligence_page="$repo_dir/config/dot/quickshell/modules/settings/ScreenIntelligencePage.qml" -require_row "$intelligence_page" ChoiceRow screenshotDir -require_row "$intelligence_page" ChoiceRow recordingDir +# Free text, not a choice. Three preset folders could not include the one the +# rest of somebody's software already writes to, which is the only folder that +# matters -- so these became editable and the row type changed with them. +require_row "$intelligence_page" TextEntryRow screenshotDir +require_row "$intelligence_page" TextEntryRow recordingDir require_row "$intelligence_page" ChoiceRow recorderArgs # The source-only contract is safe during a shared Quickshell quiet window.