diff --git a/config/dot/quickshell/scripts/panama-settings-docs b/config/dot/quickshell/scripts/panama-settings-docs new file mode 100755 index 0000000..b4a6b34 --- /dev/null +++ b/config/dot/quickshell/scripts/panama-settings-docs @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 + +"""Generate the settings reference from PreferenceSchema.qml. + +Every other form of documentation in this repository has drifted at least once: +routing that pointed at a page not containing the setting, contracts that pinned +the bug they were meant to prevent, and a comment that failed to stop the person +who read it making the exact mistake it warned about. Prose describing 137 +settings would drift the day after it was written. + +So this is generated, and a contract fails when the committed copy no longer +matches what the schema says. The document cannot be wrong for longer than it +takes to run the suite. + +Usage: + panama-settings-docs write docs/settings.md + panama-settings-docs --check exit 1 if the committed copy is stale + panama-settings-docs --stdout print without writing + +Parsing rather than importing: the schema is QML, there is no QML interpreter +here, and a regex reader that FAILS LOUDLY when it stops recognising the file is +better than a dependency on a shell that has to start a compositor. +""" + +import argparse +import pathlib +import re +import sys + +# scripts/ -> quickshell/ -> dot/ -> config/ -> repo root. resolve() first, so +# this still works when invoked through the ~/.config/quickshell symlink. +ROOT = pathlib.Path(__file__).resolve().parents[4] +SCHEMA = ROOT / "config/dot/quickshell/config/PreferenceSchema.qml" +SEARCH = ROOT / "config/dot/quickshell/services/SettingsSearch.qml" +OUTPUT = ROOT / "docs/settings.md" + +# Page id -> the name a person sees in the sidebar. +PAGE_TITLES = { + "home": "Home", "appearance": "Appearance", "displays": "Displays", + "connectivity": "Network & Devices", "home-phone": "Home & Phone", + "desktop": "Desktop & Dock", "sound": "Sound", + "notifications": "Notifications & Focus", + "screen-intelligence": "Screen Intelligence", "shortcuts": "Keyboard", + "mouse": "Mouse & Touchpad", "privacy": "Privacy & Security", + "region": "Region & Language", "accounts": "Online Accounts", + "accessibility": "Accessibility", "power": "Power & Lock", + "datetime": "Date & Time", "applications": "Applications", + "services": "System Health", "about": "About", +} + + +class SchemaError(RuntimeError): + """The schema stopped looking the way this reader expects.""" + + +def read_routes(): + """group -> page id, from SettingsSearch.""" + text = SEARCH.read_text() + block = re.search(r"groupPages:\s*\(\{(.*?)\}\)", text, re.S) + if not block: + raise SchemaError("could not find groupPages in SettingsSearch.qml") + routes = dict(re.findall(r'"([a-zA-Z]+)"\s*:\s*"([a-z-]+)"', block.group(1))) + if not routes: + raise SchemaError("groupPages matched but contained no routes") + return routes + + +def read_entries(): + """Every schema entry, as dicts, in declaration order.""" + text = SCHEMA.read_text() + + # Each entry begins at `key:` and ends at the closing brace of its block. + # Nested braces (options, hypr) are skipped by counting depth. + entries = [] + for match in re.finditer(r'\{\s*\n\s*key:\s*"([^"]+)"', text): + name = match.group(1) + start = match.start() + depth = 0 + end = None + for index in range(start, len(text)): + if text[index] == "{": + depth += 1 + elif text[index] == "}": + depth -= 1 + if depth == 0: + end = index + break + if end is None: + raise SchemaError(f"entry {name!r} is not brace-balanced") + body = text[start:end] + + def scalar(field): + found = re.search(rf'\b{field}:\s*("([^"]*)"|[^,\n]+)', body) + if not found: + return None + return (found.group(2) if found.group(2) is not None + else found.group(1).strip()) + + entry = { + "key": name, + "type": scalar("type"), + "default": scalar("def"), + "group": scalar("group"), + "label": scalar("label"), + "detail": scalar("detail"), + "unit": scalar("unit"), + "min": scalar("min"), + "max": scalar("max"), + "internal": scalar("internal") == "true", + "option": scalar("option"), + "options": re.findall(r'\{\s*value:\s*("?[^",]+)"?,\s*label:\s*"([^"]+)"', body), + } + if not entry["type"] or not entry["group"]: + raise SchemaError(f"entry {name!r} is missing a type or group") + entries.append(entry) + + if len(entries) < 50: + raise SchemaError( + f"only {len(entries)} entries parsed; the schema has far more, so " + "this reader no longer recognises the file" + ) + return entries + + +def render(entries, routes): + lines = [ + "# Settings reference", + "", + "**Generated from `config/dot/quickshell/config/PreferenceSchema.qml`.**", + "Do not edit this file. Run `quickshell/scripts/panama-settings-docs`", + "after changing the schema; a contract fails when this copy is stale.", + "", + f"{len([e for e in entries if not e['internal']])} settings across " + f"{len({e['group'] for e in entries if not e['internal']})} groups. " + f"{len([e for e in entries if e['option']])} of them are applied to the " + "compositor and confirmed by reading the value back.", + "", + ] + + by_group = {} + for entry in entries: + by_group.setdefault(entry["group"], []).append(entry) + + for group in sorted(by_group): + visible = [e for e in by_group[group] if not e["internal"]] + if not visible: + continue + page = routes.get(group) + title = PAGE_TITLES.get(page, page or "—") + lines += [f"## {group}", "", f"Found on **{title}**.", ""] + lines += ["| Setting | Default | What it does |", "|---|---|---|"] + for entry in visible: + default = entry["default"] or "—" + if entry["unit"]: + default = f"{default} {entry['unit']}" + if entry["options"]: + choices = ", ".join(label for _value, label in entry["options"]) + detail = f"{entry['detail'] or ''} Choices: {choices}." + else: + detail = entry["detail"] or "" + if entry["min"] is not None and entry["max"] is not None: + # Schema details are written without trailing punctuation, so + # one is added before appending a second sentence. + if detail and not detail.rstrip().endswith((".", "!", "?")): + detail = detail.rstrip() + "." + detail += f" Range {entry['min']}–{entry['max']}." + label = entry["label"] or entry["key"] + compositor = f" `{entry['option']}`" if entry["option"] else "" + lines.append( + f"| **{label}**
`{entry['key']}`{compositor} | {default} | " + f"{detail.strip()} |" + ) + lines.append("") + + return "\n".join(lines) + "\n" + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--check", action="store_true") + parser.add_argument("--stdout", action="store_true") + args = parser.parse_args() + + try: + rendered = render(read_entries(), read_routes()) + except SchemaError as error: + # Loudly, and without writing. A partial reference is worse than a stale + # one: stale is caught by --check, partial reads as complete. + print(f"panama-settings-docs: {error}", file=sys.stderr) + return 2 + + if args.stdout: + print(rendered, end="") + return 0 + + if args.check: + if not OUTPUT.exists(): + print("panama-settings-docs: docs/settings.md has never been generated", + file=sys.stderr) + return 1 + if OUTPUT.read_text() != rendered: + print("panama-settings-docs: docs/settings.md is stale; re-run this " + "script and commit the result", file=sys.stderr) + return 1 + return 0 + + OUTPUT.parent.mkdir(parents=True, exist_ok=True) + OUTPUT.write_text(rendered) + print(f"wrote {OUTPUT.relative_to(ROOT)}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/settings.md b/docs/settings.md new file mode 100644 index 0000000..917defc --- /dev/null +++ b/docs/settings.md @@ -0,0 +1,317 @@ +# Settings reference + +**Generated from `config/dot/quickshell/config/PreferenceSchema.qml`.** +Do not edit this file. Run `quickshell/scripts/panama-settings-docs` +after changing the schema; a contract fails when this copy is stale. + +127 settings across 26 groups. 67 of them are applied to the compositor and confirmed by reading the value back. + +## accessibility + +Found on **Accessibility**. + +| Setting | Default | What it does | +|---|---|---| +| **Magnifier**
`magnifierFactor` `cursor:zoom_factor` | 1.0 | Magnifies the screen around the pointer. 1.0 is off. Range 1.0–5.0. | +| **Magnifier follows in steps**
`magnifierRigid` `cursor:zoom_rigid` | false | Moves the magnified view in increments rather than gliding with the pointer | +| **Dim inactive windows**
`dimInactive` `decoration:dim_inactive` | false | Darkens every window except the focused one, so the active window is unmistakable | +| **Dim amount**
`dimStrength` `decoration:dim_strength` | 0.5 | How much darker unfocused windows are. Range 0.05–0.9. | +| **Pointer size**
`cursorSize` | 24 px | Applies to the compositor and to applications. Range 16–64. | +| **Text size**
`textScale` | 1.0 | Scales interface text everywhere; 1.00 is the design size. Range 0.75–2.0. | + +## appearance + +Found on **Appearance**. + +| Setting | Default | What it does | +|---|---|---| +| **Appearance**
`colorScheme` | dark | Light and dark share one identity, not two themes Choices: Dark, Light. | +| **Accent color**
`accentName` | blue | Drives the focused window border, the bar hairline, and every active state Choices: Prism blue, Orchid, Teal, Green, Amber, Orange, Rose, Slate. | + +## capture + +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. | +| **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 + +Found on **Appearance**. + +| Setting | Default | What it does | +|---|---|---| +| **24-hour time**
`use24Hour` | false | Use 18:30 instead of 6:30 PM | +| **Show seconds**
`showSeconds` | true | Keep a precise clock in the center of the bar | +| **Show weekday**
`showWeekday` | true | Include the abbreviated weekday before the date | + +## display + +Found on **Displays**. + +| Setting | Default | What it does | +|---|---|---| +| **Game-aware HDR**
`autoHdr` `render:cm_auto_hdr` | true | Hand HDR to fullscreen games while the desktop stays SDR | +| **Variable refresh rate**
`vrrPolicy` `misc:vrr` | 3 | Matches the display's refresh rate to what is on screen Choices: Off, Always on, Fullscreen only, Fullscreen games. | +| **Direct scanout**
`directScanoutPolicy` `render:direct_scanout` | 2 | Lets fullscreen content bypass compositing Choices: Off, Always on, Automatic. | + +## dock + +Found on **Desktop & Dock**. + +| Setting | Default | What it does | +|---|---|---| +| **Automatically hide the Dock**
`dockAutohide` | true | Reveal it at the bottom edge when a workspace is occupied | +| **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. | +| **Pinned applications**
`dockPinned` | [ | Applications that stay in the Dock whether or not they are running | + +## edges + +Found on **Desktop & Dock**. + +| Setting | Default | What it does | +|---|---|---| +| **Resize by dragging the border**
`resizeOnBorder` `general:resize_on_border` | true | Drag a window's edge to resize it, instead of only with the keyboard | +| **Border grab area**
`borderGrabArea` `general:extend_border_grab_area` | 15 px | How far outside the border still counts as grabbing it. Larger is easier to hit. Range 0–40. | +| **Show the resize cursor**
`hoverIconOnBorder` `general:hover_icon_on_border` | true | Change the pointer when it is over a resizable border | +| **Snap distance between windows**
`snapWindowGap` `general:snap:window_gap` | 10 px | How close two floating windows must be before they snap together. Range 0–60. | +| **Snap distance to screen edges**
`snapMonitorGap` `general:snap:monitor_gap` | 10 px | How close a floating window must be to an edge before it snaps to it. Range 0–60. | +| **Snapping respects gaps**
`snapRespectGaps` `general:snap:respect_gaps` | false | Snapped windows keep the configured gap instead of touching | + +## effects + +Found on **Appearance**. + +| Setting | Default | What it does | +|---|---|---| +| **Blur**
`blurEnabled` `decoration:blur:enabled` | true | Blur the desktop behind translucent surfaces | +| **Blur radius**
`blurSize` `decoration:blur:size` | 8 | Larger is softer and costs more frame time. Range 1–20. | +| **Blur passes**
`blurPasses` `decoration:blur:passes` | 3 | More passes look smoother and cost more frame time. Range 1–5. | +| **Window shadows**
`shadowEnabled` `decoration:shadow:enabled` | true | Lift windows off the wallpaper with a soft shadow | +| **Shadow size**
`shadowRange` `decoration:shadow:range` | 20 px | How far the shadow spreads from the window edge. Range 0–60. | +| **Hard-edged shadow**
`shadowSharp` `decoration:shadow:sharp` | false | A crisp shadow instead of a soft falloff | +| **Shadow falloff**
`shadowRenderPower` `decoration:shadow:render_power` | 3 | How sharply the shadow fades out. Higher is tighter to the window. Range 1–4. | +| **Focus glow**
`glowEnabled` `decoration:glow:enabled` | true | A faint halo behind the focused window | +| **Glow size**
`glowRange` `decoration:glow:range` | 8 px | Kept small deliberately: the gradient border is the signature. Range 0–30. | +| **Animations**
`animationsEnabled` `animations:enabled` | true | Window, workspace, and panel motion | + +## focus + +Found on **Desktop & Dock**. + +| Setting | Default | What it does | +|---|---|---| +| **Focus session length**
`focusDurationMinutes` | 45 min | How long a focus session runs before it ends itself. Range 5–180. | + +## idle + +Found on **Power & Lock**. + +| Setting | Default | What it does | +|---|---|---| +| **Turn the screen off after**
`screenBlankMinutes` | 5 min | Blanks the display; nothing is locked yet. Range 0–120. | +| **Lock the screen after**
`lockMinutes` | 10 min | Counted from when the session went idle, not from blanking. Range 0–240. | +| **Suspend after**
`suspendMinutes` | 0 min | This is a desktop, so Panama ships with automatic suspend off. Range 0–480. | +| **Lock before sleeping**
`lockOnSleep` | true | Requires your password when the machine wakes | + +## input + +Found on **Keyboard**. + +| Setting | Default | What it does | +|---|---|---| +| **Keyboard layout**
`keyboardLayout` `input:kb_layout` | us | XKB layout name, or a comma-separated list to switch between | +| **Layout variant**
`keyboardVariant` `input:kb_variant` | — | XKB variant, such as dvorak or colemak. Empty for the standard layout | +| **Keyboard options**
`keyboardOptions` `input:kb_options` | caps:escape_shifted_capslock | XKB options, such as compose:ralt to make right Alt a compose key | +| **Num Lock on login**
`numlockByDefault` `input:numlock_by_default` | true | Turn Num Lock on when the session starts | +| **Repeat delay**
`keyRepeatDelay` `input:repeat_delay` | 500 ms | How long a key is held before it starts repeating. Range 150–1000. | +| **Repeat rate**
`keyRepeatRate` `input:repeat_rate` | 33 /s | How many characters a second a held key produces. Range 5–100. | + +## lockAppearance + +Found on **Appearance**. + +| Setting | Default | What it does | +|---|---|---| +| **Background**
`lockBackgroundMode` | screenshot | What appears behind the lock screen Choices: Blurred desktop, Current wallpaper, Solid color. | +| **Background blur**
`lockBlurLevel` | 3 | Softens what is behind the password field. Range 0–5. | +| **Show clock**
`lockShowClock` | true | Use the desktop's 12 or 24-hour format | +| **Show date**
`lockShowDate` | true | Show the weekday and full date | +| **Show user name**
`lockShowUser` | true | Identify the signed-in account | +| **Hide password field until typing**
`lockFadeOnEmpty` | false | Keep the empty field out of the way | + +## master + +Found on **Desktop & Dock**. + +| Setting | Default | What it does | +|---|---|---| +| **Master area size**
`masterFactor` `master:mfact` | 0.55 | How much of the screen the master window takes. Range 0.1–0.9. | +| **Master area position**
`masterOrientation` `master:orientation` | left | Which side of the screen the master window occupies Choices: Left, Right, Top, Bottom, Center. | +| **New windows become**
`masterNewStatus` `master:new_status` | slave | Whether a new window takes the master area or joins the stack Choices: The master window, Part of the stack, Whatever the focused window is. | +| **Add new windows at the top**
`masterNewOnTop` `master:new_on_top` | false | New stack windows go above the others rather than below | + +## multitasking + +Found on **Desktop & Dock**. + +| Setting | Default | What it does | +|---|---|---| +| **Tiling layout**
`windowLayout` `general:layout` | dwindle | Dwindle splits the focused window; master keeps one large window beside a stack Choices: Dwindle, Master and stack. | +| **Keep split direction**
`preserveSplit` `dwindle:preserve_split` | true | New windows follow the split of the window they replace, instead of always halving the longer side | +| **New windows open**
`forceSplit` `dwindle:force_split` | 0 | Where a new window lands relative to the one that was focused Choices: Where the pointer is, Always left or above, Always right or below. | +| **Snap floating windows**
`windowSnapping` `general:snap:enabled` | true | Floating windows stick to screen edges and to each other as you drag them | +| **Switch back and forth**
`workspaceBackAndForth` `binds:workspace_back_and_forth` | false | Selecting the workspace you are already on returns you to the previous one | +| **Wrap around at the ends**
`allowWorkspaceCycles` `binds:allow_workspace_cycles` | false | Moving past the last workspace continues from the first | +| **Let applications take focus**
`focusOnActivate` `misc:focus_on_activate` | false | An application asking for attention is switched to, rather than only highlighted | +| **Pointer changes active display**
`mouseMoveFocusesMonitor` `misc:mouse_move_focuses_monitor` | true | Moving the pointer to another display makes it the active one | + +## nightLight + +Found on **Displays**. + +| Setting | Default | What it does | +|---|---|---| +| **Night Light**
`nightLightEnabled` | false | Shift the display warmer to reduce blue light | +| **Schedule automatically**
`nightLightAutomatic` | false | Turn Night Light on and off at the scheduled hours | +| **Color temperature**
`nightLightTemperature` | 3500 K | Lower is warmer. Range 2000–6500. | +| **Turns on at**
`nightLightFrom` | 17.0 | Only used when Night Light follows a schedule. Range 0–23.5. | +| **Turns off at**
`nightLightTo` | 10.0 | A time earlier than the start simply means the next morning. Range 0–23.5. | + +## notices + +Found on **Desktop & Dock**. + +| Setting | Default | What it does | +|---|---|---| +| **Hyprland wallpaper**
`hyprlandLogo` `misc:disable_hyprland_logo` | false | The stock background Hyprland draws when no wallpaper is set | +| **Splash text**
`hyprlandSplash` `misc:disable_splash_rendering` | false | The line of text Hyprland renders over the stock background | +| **Update announcements**
`hyprlandUpdateNews` `ecosystem:no_update_news` | false | The window Hyprland opens after an update to describe what changed | +| **Donation reminders**
`hyprlandDonationNag` `ecosystem:no_donation_nag` | false | The prompt Hyprland shows twice a year asking for support | + +## notifications + +Found on **Notifications & Focus**. + +| Setting | Default | What it does | +|---|---|---| +| **Notification duration**
`notificationTimeoutMs` | 5000 ms | How long ordinary notification banners remain visible. Range 1000–30000. | +| **Critical notification duration**
`notificationTimeoutCriticalMs` | 0 ms | Zero keeps critical notification banners visible until dismissed. Range 0–60000. | +| **Notification history**
`notificationHistoryLimit` | 100 | Maximum notifications retained in the notification center. Range 10–500. | +| **Visible banners**
`maxVisibleToasts` | 4 | Maximum notification banners shown at once. Range 1–8. | + +## pointer + +Found on **Mouse & Touchpad**. + +| Setting | Default | What it does | +|---|---|---| +| **Pointer focus**
`followMouse` `input:follow_mouse` | 1 | What moving the pointer does to which window is focused Choices: Click to focus, Focus follows pointer, Pointer detached, Pointer fully separate. | +| **Pointer speed**
`pointerSensitivity` `input:sensitivity` | 0.0 | Zero is flat, unaccelerated response. Range -1.0–1.0. | +| **Hide pointer after**
`cursorInactiveTimeout` `cursor:inactive_timeout` | 4 s | Seconds of stillness before the pointer fades out; 0 never hides it. Range 0–60. | +| **Natural scrolling**
`naturalScroll` `input:natural_scroll` | false | Content follows the direction of your fingers, as on a phone | +| **Acceleration**
`accelProfile` `input:accel_profile` | flat | Flat moves the pointer the same distance however fast you move Choices: Flat, Adaptive. | +| **Scroll speed**
`scrollFactor` `input:scroll_factor` | 1.0 | Multiplies how far one notch of the wheel scrolls. Range 0.1–4.0. | +| **Left-handed**
`leftHanded` `input:left_handed` | false | Swap the primary and secondary buttons | +| **Middle-click paste**
`middleClickPaste` `misc:middle_click_paste` | true | Paste the primary selection in GTK and native Wayland applications | + +## themes + +Found on **Appearance**. + +| Setting | Default | What it does | +|---|---|---| +| **Pointer theme**
`cursorTheme` | oreo_blue_cursors | The pointer design used by applications and Hyprland | +| **Application icons**
`iconTheme` | Adwaita | The icon set used by GTK applications | + +## titlebar + +Found on **Appearance**. + +| Setting | Default | What it does | +|---|---|---| +| **Button side**
`titlebarButtonSide` | right | Place application titlebar buttons on the left or right Choices: Left, Right. | +| **Maximize button**
`titlebarMaximizeButton` | false | Show a maximize button in application titlebars that support it | +| **Double-click titlebar**
`titlebarDoubleClick` | toggle-maximize | Choose what a double-click on an application titlebar does Choices: Toggle maximize, Do nothing. | + +## touchpad + +Found on **Mouse & Touchpad**. + +| Setting | Default | What it does | +|---|---|---| +| **Tap to click**
`touchpadTapToClick` `input:touchpad:tap-to-click` | true | A tap counts as a click without pressing down | +| **Natural scrolling**
`touchpadNaturalScroll` `input:touchpad:natural_scroll` | true | Content follows the direction of your fingers | +| **Disable while typing**
`touchpadDisableWhileTyping` `input:touchpad:disable_while_typing` | true | Ignore the touchpad briefly after a keystroke, so a palm cannot move the pointer | +| **Scroll speed**
`touchpadScrollFactor` `input:touchpad:scroll_factor` | 1.0 | Multiplies how far a two-finger scroll travels. Range 0.1–4.0. | +| **Drag lock**
`touchpadDragLock` `input:touchpad:drag_lock` | 0 | Keeps a tap-and-drag active when you lift a finger mid-drag Choices: Off, On, On, until you tap again. | +| **Middle-click by pressing both buttons**
`touchpadMiddleButtonEmulation` `input:touchpad:middle_button_emulation` | false | Pressing left and right together acts as a middle click | + +## typography + +Found on **Appearance**. + +| Setting | Default | What it does | +|---|---|---| +| **Interface font**
`interfaceFont` | Adwaita Sans | Used for every piece of text in the shell | +| **Icon font**
`iconFont` | VictorMono Nerd Font | Draws the shell's glyphs, so it must be a Nerd Font | +| **Interface text size**
`interfaceFontSize` | 13 px | The base size the rest of the shell's type scales from. Range 10–18. | +| **Application font**
`applicationFont` | Adwaita Sans | Used by menus, controls, and labels in applications | +| **Application text size**
`applicationFontSize` | 11 pt | The base text size used by applications. Range 6–32. | +| **Document font**
`documentFont` | Adwaita Sans | Used for document content when an application follows the system choice | +| **Document text size**
`documentFontSize` | 12 pt | The default text size for document content. Range 6–32. | +| **Monospace font**
`monospaceFont` | VictorMono Nerd Font | Used by terminals, editors, and code fields that follow the system choice | +| **Monospace text size**
`monospaceFontSize` | 10 pt | The default text size for terminals and code. Range 6–32. | +| **Font hinting**
`fontHinting` | slight | How strongly text aligns to the pixel grid Choices: None, Slight, Medium, Full. | +| **Text smoothing**
`fontAntialiasing` | rgba | How application text softens its edges Choices: None, Grayscale, Subpixel. | + +## vitals + +Found on **Appearance**. + +| Setting | Default | What it does | +|---|---|---| +| **Processor**
`showCpu` | true | Show processor usage beside the workspace indicator | +| **Memory**
`showMemory` | true | Show memory usage beside the workspace indicator | +| **Graphics**
`showGpu` | true | Show graphics usage beside the workspace indicator | +| **Vitals refresh**
`vitalsIntervalMs` | 2000 ms | How often processor, memory, and graphics usage update. Range 500–10000. | + +## wallpaper + +Found on **Appearance**. + +| Setting | Default | What it does | +|---|---|---| +| **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. | +| **Shuffle**
`wallpaperShuffle` | true | Show every selected image before repeating | + +## weather + +Found on **Home**. + +| Setting | Default | What it does | +|---|---|---| +| **Temperature unit**
`temperatureUnit` | fahrenheit | Choose Fahrenheit or Celsius for the weather card Choices: Fahrenheit, Celsius. | +| **Weather refresh**
`weatherRefreshMinutes` | 20 min | How often Panama updates the current conditions. Range 5–120. | + +## windows + +Found on **Appearance**. + +| Setting | Default | What it does | +|---|---|---| +| **Inner gaps**
`gapsIn` `general:gaps_in` | 5 px | Space between neighboring tiled windows. Range 0–40. | +| **Outer gaps**
`gapsOut` `general:gaps_out` | 10 px | Space between the tiled area and the screen edge. Range 0–80. | +| **Border width**
`borderSize` `general:border_size` | 2 px | Thickness of the gradient border on the focused window. Range 0–10. | +| **Corner radius**
`windowRounding` `decoration:rounding` | 18 px | Matches the shell's popover radius so windows and panels agree. Range 0–40. | +| **Unfocused window opacity**
`inactiveOpacity` `decoration:inactive_opacity` | 1.0 | Fade windows that do not have focus. Range 0.5–1.0. | +| **Focused window opacity**
`activeOpacity` `decoration:active_opacity` | 1.0 | Fade even the focused window; 1.0 is fully opaque. Range 0.5–1.0. | +| **Fullscreen opacity**
`fullscreenOpacity` `decoration:fullscreen_opacity` | 1.0 | Applied instead of the focused opacity when a window is fullscreen. Range 0.5–1.0. | +| **Corner shape**
`roundingPower` `decoration:rounding_power` | 2.0 | 2 is a circular corner; higher values approach a squircle. Range 2.0–10.0. | + diff --git a/tests/quickshell/settings-docs-contract.sh b/tests/quickshell/settings-docs-contract.sh new file mode 100755 index 0000000..8a7d57f --- /dev/null +++ b/tests/quickshell/settings-docs-contract.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +# The settings reference is generated, and must not be stale. +# +# Every other kind of documentation here has drifted at least once: routing that +# pointed at a page not containing the setting, contracts that pinned the bug +# they were meant to prevent, a comment that failed to stop the person who read +# it from making the exact mistake it described. Prose describing 127 settings +# would drift the day after it was written. +# +# So docs/settings.md is generated from the schema, and this fails the moment +# the committed copy stops matching. That is the whole mechanism: the document +# cannot be wrong for longer than it takes to run the suite. + +set -uo pipefail + +repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +generator="$repo_dir/config/dot/quickshell/scripts/panama-settings-docs" +doc="$repo_dir/docs/settings.md" + +fail() { + printf 'settings docs contract: %s\n' "$1" >&2 + exit 1 +} + +[[ -x "$generator" ]] || fail 'the generator is missing or not executable' + +"$generator" --check || fail 'docs/settings.md is stale -- run quickshell/scripts/panama-settings-docs and commit the result' + +# A generator that silently emitted nothing would also pass --check against an +# equally empty file, so the output is checked for substance too. +[[ -s "$doc" ]] || fail 'docs/settings.md is empty' + +settings="$(grep -c '^| \*\*' "$doc")" +schema_keys="$(grep -c 'key: "' "$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml")" + +# Internal keys are deliberately omitted, so the document is smaller than the +# schema -- but not by much. A large gap means the reader stopped recognising +# entries and quietly documented a fraction of them. +(( settings > schema_keys * 8 / 10 )) \ + || fail "the reference documents only $settings of $schema_keys schema keys, which suggests the generator stopped parsing partway" + +# The compositor-backed settings are the ones worth naming precisely, since +# that is the string someone would search the Hyprland docs for. +grep -q 'cursor:zoom_factor' "$doc" \ + || fail 'compositor option names are missing from the reference' + +# A stale copy must be detectable, not merely regenerable. Prove the check +# actually compares content rather than always returning success. +scratch="$(mktemp -d /tmp/panama-docs.XXXXXX)" +trap 'rm -rf "$scratch"' EXIT +cp "$doc" "$scratch/settings.md" +printf '\n\n' >>"$doc" +if "$generator" --check >/dev/null 2>&1; then + cp "$scratch/settings.md" "$doc" + fail '--check reported success on a modified file, so staleness would never be caught' +fi +cp "$scratch/settings.md" "$doc" + +printf 'settings docs contract: PASS (%d settings documented)\n' "$settings"