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.