Compare commits
2
Commits
d96863b687
...
2fcaada7e8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fcaada7e8 | ||
|
|
588dec4adc |
+214
@@ -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}**<br>`{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())
|
||||
@@ -181,7 +181,24 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: root.refresh()
|
||||
Component.onCompleted: {
|
||||
root.refresh();
|
||||
|
||||
// Restoring defaults is the only irreversible action Panama offers, and
|
||||
// it lives in SystemSettings -- which must not reference this singleton,
|
||||
// since this one already references it. So the capability is PUSHED
|
||||
// there rather than pulled from here.
|
||||
//
|
||||
// Returns false when a snapshot is already running rather than queueing:
|
||||
// the caller is about to wipe the stores, and a snapshot landing after
|
||||
// that would record the wiped state as if it were the user's.
|
||||
SystemSettings.takeSafetySnapshot = function() {
|
||||
if (actionRun.running)
|
||||
return false;
|
||||
root.save();
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
function refresh(): void {
|
||||
if (!listQuery.running)
|
||||
@@ -195,6 +212,7 @@ Singleton {
|
||||
actionRun.exec([root.helperPath, "save", root.serializeHomeState()]);
|
||||
}
|
||||
|
||||
|
||||
function serializeHomeState(): string {
|
||||
const current = root.readHomeState();
|
||||
const favorites = [];
|
||||
|
||||
@@ -38,6 +38,13 @@ Singleton {
|
||||
property var displayBusy: function() { return Displays.busy || Displays.awaitingConfirmation; }
|
||||
property var readDisplays: function() { return DesktopPreferences.get("displays"); }
|
||||
property var protectDisplays: function(value) { return DesktopPreferences.set("displays", value); }
|
||||
|
||||
// Installed by SettingsBackup at startup. A default no-op rather than a
|
||||
// direct reference, because SettingsBackup already references this
|
||||
// singleton and a mutual reference between two singletons is an
|
||||
// initialisation-order problem waiting to happen. Tests override it the
|
||||
// same way they override the seams above.
|
||||
property var takeSafetySnapshot: function() { return false; }
|
||||
property var setDisplayBlocked: function(blocked) { Displays.externalChangeBlocked = blocked; }
|
||||
property var reloadKeybinds: function() { Keybinds.applyReload(); }
|
||||
property var keybindsReloading: function() { return Keybinds.reloading; }
|
||||
@@ -509,6 +516,17 @@ Singleton {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Snapshot before wiping. Restoring defaults clears every preference
|
||||
// and the Home accessory store, and there is no undo for it anywhere in
|
||||
// the app -- so the one automatic snapshot Panama takes is the one taken
|
||||
// immediately before the only irreversible action it offers.
|
||||
//
|
||||
// Deliberately not fatal if it fails: a user who asked to reset should
|
||||
// get their reset, and a snapshot that could not be written is reported
|
||||
// rather than allowed to block the thing they asked for.
|
||||
if (!root.takeSafetySnapshot())
|
||||
console.warn("SystemSettings: could not snapshot before restoring defaults");
|
||||
|
||||
root.setDisplayBlocked(true);
|
||||
DesktopPreferences.resetDesktopDefaults();
|
||||
|
||||
|
||||
@@ -43,6 +43,12 @@ ShellRoot {
|
||||
root.recordReset("display.block:" + blocked);
|
||||
root.displayBlocked = blocked;
|
||||
};
|
||||
// Recorded so the contract can prove the snapshot happens BEFORE the
|
||||
// stores are cleared, not merely that it happens.
|
||||
SystemSettings.takeSafetySnapshot = function() {
|
||||
root.recordReset("snapshot");
|
||||
return true;
|
||||
};
|
||||
SystemSettings.reloadKeybinds = function() { root.recordReset("keybinds.reload"); };
|
||||
SystemSettings.keybindsReloading = function() { return false; };
|
||||
SystemSettings.applyWallpaper = function(path) { root.recordReset("wallpaper.set:" + path); };
|
||||
|
||||
@@ -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**<br>`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**<br>`magnifierRigid` `cursor:zoom_rigid` | false | Moves the magnified view in increments rather than gliding with the pointer |
|
||||
| **Dim inactive windows**<br>`dimInactive` `decoration:dim_inactive` | false | Darkens every window except the focused one, so the active window is unmistakable |
|
||||
| **Dim amount**<br>`dimStrength` `decoration:dim_strength` | 0.5 | How much darker unfocused windows are. Range 0.05–0.9. |
|
||||
| **Pointer size**<br>`cursorSize` | 24 px | Applies to the compositor and to applications. Range 16–64. |
|
||||
| **Text size**<br>`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**<br>`colorScheme` | dark | Light and dark share one identity, not two themes Choices: Dark, Light. |
|
||||
| **Accent color**<br>`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**<br>`screenshotDir` | Pictures/Screenshots | Folder under your home directory for screenshots Choices: Pictures / Screenshots, Pictures, Desktop. |
|
||||
| **Recording folder**<br>`recordingDir` | Videos/Recordings | Folder under your home directory for screen recordings Choices: Videos / Recordings, Videos, Desktop. |
|
||||
| **Recording encoder**<br>`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**<br>`use24Hour` | false | Use 18:30 instead of 6:30 PM |
|
||||
| **Show seconds**<br>`showSeconds` | true | Keep a precise clock in the center of the bar |
|
||||
| **Show weekday**<br>`showWeekday` | true | Include the abbreviated weekday before the date |
|
||||
|
||||
## display
|
||||
|
||||
Found on **Displays**.
|
||||
|
||||
| Setting | Default | What it does |
|
||||
|---|---|---|
|
||||
| **Game-aware HDR**<br>`autoHdr` `render:cm_auto_hdr` | true | Hand HDR to fullscreen games while the desktop stays SDR |
|
||||
| **Variable refresh rate**<br>`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**<br>`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**<br>`dockAutohide` | true | Reveal it at the bottom edge when a workspace is occupied |
|
||||
| **Reveal delay**<br>`dockRevealDelayMs` | 0 ms | Zero reveals the Dock the instant the pointer reaches the edge. Range 0–1000. |
|
||||
| **Hide delay**<br>`dockHideDelayMs` | 250 ms | Prevents flicker when crossing between icons. Range 0–2000. |
|
||||
| **Pinned applications**<br>`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**<br>`resizeOnBorder` `general:resize_on_border` | true | Drag a window's edge to resize it, instead of only with the keyboard |
|
||||
| **Border grab area**<br>`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**<br>`hoverIconOnBorder` `general:hover_icon_on_border` | true | Change the pointer when it is over a resizable border |
|
||||
| **Snap distance between windows**<br>`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**<br>`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**<br>`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**<br>`blurEnabled` `decoration:blur:enabled` | true | Blur the desktop behind translucent surfaces |
|
||||
| **Blur radius**<br>`blurSize` `decoration:blur:size` | 8 | Larger is softer and costs more frame time. Range 1–20. |
|
||||
| **Blur passes**<br>`blurPasses` `decoration:blur:passes` | 3 | More passes look smoother and cost more frame time. Range 1–5. |
|
||||
| **Window shadows**<br>`shadowEnabled` `decoration:shadow:enabled` | true | Lift windows off the wallpaper with a soft shadow |
|
||||
| **Shadow size**<br>`shadowRange` `decoration:shadow:range` | 20 px | How far the shadow spreads from the window edge. Range 0–60. |
|
||||
| **Hard-edged shadow**<br>`shadowSharp` `decoration:shadow:sharp` | false | A crisp shadow instead of a soft falloff |
|
||||
| **Shadow falloff**<br>`shadowRenderPower` `decoration:shadow:render_power` | 3 | How sharply the shadow fades out. Higher is tighter to the window. Range 1–4. |
|
||||
| **Focus glow**<br>`glowEnabled` `decoration:glow:enabled` | true | A faint halo behind the focused window |
|
||||
| **Glow size**<br>`glowRange` `decoration:glow:range` | 8 px | Kept small deliberately: the gradient border is the signature. Range 0–30. |
|
||||
| **Animations**<br>`animationsEnabled` `animations:enabled` | true | Window, workspace, and panel motion |
|
||||
|
||||
## focus
|
||||
|
||||
Found on **Desktop & Dock**.
|
||||
|
||||
| Setting | Default | What it does |
|
||||
|---|---|---|
|
||||
| **Focus session length**<br>`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**<br>`screenBlankMinutes` | 5 min | Blanks the display; nothing is locked yet. Range 0–120. |
|
||||
| **Lock the screen after**<br>`lockMinutes` | 10 min | Counted from when the session went idle, not from blanking. Range 0–240. |
|
||||
| **Suspend after**<br>`suspendMinutes` | 0 min | This is a desktop, so Panama ships with automatic suspend off. Range 0–480. |
|
||||
| **Lock before sleeping**<br>`lockOnSleep` | true | Requires your password when the machine wakes |
|
||||
|
||||
## input
|
||||
|
||||
Found on **Keyboard**.
|
||||
|
||||
| Setting | Default | What it does |
|
||||
|---|---|---|
|
||||
| **Keyboard layout**<br>`keyboardLayout` `input:kb_layout` | us | XKB layout name, or a comma-separated list to switch between |
|
||||
| **Layout variant**<br>`keyboardVariant` `input:kb_variant` | — | XKB variant, such as dvorak or colemak. Empty for the standard layout |
|
||||
| **Keyboard options**<br>`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**<br>`numlockByDefault` `input:numlock_by_default` | true | Turn Num Lock on when the session starts |
|
||||
| **Repeat delay**<br>`keyRepeatDelay` `input:repeat_delay` | 500 ms | How long a key is held before it starts repeating. Range 150–1000. |
|
||||
| **Repeat rate**<br>`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**<br>`lockBackgroundMode` | screenshot | What appears behind the lock screen Choices: Blurred desktop, Current wallpaper, Solid color. |
|
||||
| **Background blur**<br>`lockBlurLevel` | 3 | Softens what is behind the password field. Range 0–5. |
|
||||
| **Show clock**<br>`lockShowClock` | true | Use the desktop's 12 or 24-hour format |
|
||||
| **Show date**<br>`lockShowDate` | true | Show the weekday and full date |
|
||||
| **Show user name**<br>`lockShowUser` | true | Identify the signed-in account |
|
||||
| **Hide password field until typing**<br>`lockFadeOnEmpty` | false | Keep the empty field out of the way |
|
||||
|
||||
## master
|
||||
|
||||
Found on **Desktop & Dock**.
|
||||
|
||||
| Setting | Default | What it does |
|
||||
|---|---|---|
|
||||
| **Master area size**<br>`masterFactor` `master:mfact` | 0.55 | How much of the screen the master window takes. Range 0.1–0.9. |
|
||||
| **Master area position**<br>`masterOrientation` `master:orientation` | left | Which side of the screen the master window occupies Choices: Left, Right, Top, Bottom, Center. |
|
||||
| **New windows become**<br>`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**<br>`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**<br>`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**<br>`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**<br>`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**<br>`windowSnapping` `general:snap:enabled` | true | Floating windows stick to screen edges and to each other as you drag them |
|
||||
| **Switch back and forth**<br>`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**<br>`allowWorkspaceCycles` `binds:allow_workspace_cycles` | false | Moving past the last workspace continues from the first |
|
||||
| **Let applications take focus**<br>`focusOnActivate` `misc:focus_on_activate` | false | An application asking for attention is switched to, rather than only highlighted |
|
||||
| **Pointer changes active display**<br>`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**<br>`nightLightEnabled` | false | Shift the display warmer to reduce blue light |
|
||||
| **Schedule automatically**<br>`nightLightAutomatic` | false | Turn Night Light on and off at the scheduled hours |
|
||||
| **Color temperature**<br>`nightLightTemperature` | 3500 K | Lower is warmer. Range 2000–6500. |
|
||||
| **Turns on at**<br>`nightLightFrom` | 17.0 | Only used when Night Light follows a schedule. Range 0–23.5. |
|
||||
| **Turns off at**<br>`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**<br>`hyprlandLogo` `misc:disable_hyprland_logo` | false | The stock background Hyprland draws when no wallpaper is set |
|
||||
| **Splash text**<br>`hyprlandSplash` `misc:disable_splash_rendering` | false | The line of text Hyprland renders over the stock background |
|
||||
| **Update announcements**<br>`hyprlandUpdateNews` `ecosystem:no_update_news` | false | The window Hyprland opens after an update to describe what changed |
|
||||
| **Donation reminders**<br>`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**<br>`notificationTimeoutMs` | 5000 ms | How long ordinary notification banners remain visible. Range 1000–30000. |
|
||||
| **Critical notification duration**<br>`notificationTimeoutCriticalMs` | 0 ms | Zero keeps critical notification banners visible until dismissed. Range 0–60000. |
|
||||
| **Notification history**<br>`notificationHistoryLimit` | 100 | Maximum notifications retained in the notification center. Range 10–500. |
|
||||
| **Visible banners**<br>`maxVisibleToasts` | 4 | Maximum notification banners shown at once. Range 1–8. |
|
||||
|
||||
## pointer
|
||||
|
||||
Found on **Mouse & Touchpad**.
|
||||
|
||||
| Setting | Default | What it does |
|
||||
|---|---|---|
|
||||
| **Pointer focus**<br>`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**<br>`pointerSensitivity` `input:sensitivity` | 0.0 | Zero is flat, unaccelerated response. Range -1.0–1.0. |
|
||||
| **Hide pointer after**<br>`cursorInactiveTimeout` `cursor:inactive_timeout` | 4 s | Seconds of stillness before the pointer fades out; 0 never hides it. Range 0–60. |
|
||||
| **Natural scrolling**<br>`naturalScroll` `input:natural_scroll` | false | Content follows the direction of your fingers, as on a phone |
|
||||
| **Acceleration**<br>`accelProfile` `input:accel_profile` | flat | Flat moves the pointer the same distance however fast you move Choices: Flat, Adaptive. |
|
||||
| **Scroll speed**<br>`scrollFactor` `input:scroll_factor` | 1.0 | Multiplies how far one notch of the wheel scrolls. Range 0.1–4.0. |
|
||||
| **Left-handed**<br>`leftHanded` `input:left_handed` | false | Swap the primary and secondary buttons |
|
||||
| **Middle-click paste**<br>`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**<br>`cursorTheme` | oreo_blue_cursors | The pointer design used by applications and Hyprland |
|
||||
| **Application icons**<br>`iconTheme` | Adwaita | The icon set used by GTK applications |
|
||||
|
||||
## titlebar
|
||||
|
||||
Found on **Appearance**.
|
||||
|
||||
| Setting | Default | What it does |
|
||||
|---|---|---|
|
||||
| **Button side**<br>`titlebarButtonSide` | right | Place application titlebar buttons on the left or right Choices: Left, Right. |
|
||||
| **Maximize button**<br>`titlebarMaximizeButton` | false | Show a maximize button in application titlebars that support it |
|
||||
| **Double-click titlebar**<br>`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**<br>`touchpadTapToClick` `input:touchpad:tap-to-click` | true | A tap counts as a click without pressing down |
|
||||
| **Natural scrolling**<br>`touchpadNaturalScroll` `input:touchpad:natural_scroll` | true | Content follows the direction of your fingers |
|
||||
| **Disable while typing**<br>`touchpadDisableWhileTyping` `input:touchpad:disable_while_typing` | true | Ignore the touchpad briefly after a keystroke, so a palm cannot move the pointer |
|
||||
| **Scroll speed**<br>`touchpadScrollFactor` `input:touchpad:scroll_factor` | 1.0 | Multiplies how far a two-finger scroll travels. Range 0.1–4.0. |
|
||||
| **Drag lock**<br>`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**<br>`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**<br>`interfaceFont` | Adwaita Sans | Used for every piece of text in the shell |
|
||||
| **Icon font**<br>`iconFont` | VictorMono Nerd Font | Draws the shell's glyphs, so it must be a Nerd Font |
|
||||
| **Interface text size**<br>`interfaceFontSize` | 13 px | The base size the rest of the shell's type scales from. Range 10–18. |
|
||||
| **Application font**<br>`applicationFont` | Adwaita Sans | Used by menus, controls, and labels in applications |
|
||||
| **Application text size**<br>`applicationFontSize` | 11 pt | The base text size used by applications. Range 6–32. |
|
||||
| **Document font**<br>`documentFont` | Adwaita Sans | Used for document content when an application follows the system choice |
|
||||
| **Document text size**<br>`documentFontSize` | 12 pt | The default text size for document content. Range 6–32. |
|
||||
| **Monospace font**<br>`monospaceFont` | VictorMono Nerd Font | Used by terminals, editors, and code fields that follow the system choice |
|
||||
| **Monospace text size**<br>`monospaceFontSize` | 10 pt | The default text size for terminals and code. Range 6–32. |
|
||||
| **Font hinting**<br>`fontHinting` | slight | How strongly text aligns to the pixel grid Choices: None, Slight, Medium, Full. |
|
||||
| **Text smoothing**<br>`fontAntialiasing` | rgba | How application text softens its edges Choices: None, Grayscale, Subpixel. |
|
||||
|
||||
## vitals
|
||||
|
||||
Found on **Appearance**.
|
||||
|
||||
| Setting | Default | What it does |
|
||||
|---|---|---|
|
||||
| **Processor**<br>`showCpu` | true | Show processor usage beside the workspace indicator |
|
||||
| **Memory**<br>`showMemory` | true | Show memory usage beside the workspace indicator |
|
||||
| **Graphics**<br>`showGpu` | true | Show graphics usage beside the workspace indicator |
|
||||
| **Vitals refresh**<br>`vitalsIntervalMs` | 2000 ms | How often processor, memory, and graphics usage update. Range 500–10000. |
|
||||
|
||||
## wallpaper
|
||||
|
||||
Found on **Appearance**.
|
||||
|
||||
| Setting | Default | What it does |
|
||||
|---|---|---|
|
||||
| **Wallpaper**<br>`wallpaperPath` | — | Shown on every output |
|
||||
| **Wallpaper mode**<br>`wallpaperMode` | single | Use one image, rotate a collection, or choose per display Choices: Single, Slideshow, Per display. |
|
||||
| **Change background every**<br>`wallpaperIntervalMinutes` | 30 min | Time between slideshow images. Range 5–1440. |
|
||||
| **Shuffle**<br>`wallpaperShuffle` | true | Show every selected image before repeating |
|
||||
|
||||
## weather
|
||||
|
||||
Found on **Home**.
|
||||
|
||||
| Setting | Default | What it does |
|
||||
|---|---|---|
|
||||
| **Temperature unit**<br>`temperatureUnit` | fahrenheit | Choose Fahrenheit or Celsius for the weather card Choices: Fahrenheit, Celsius. |
|
||||
| **Weather refresh**<br>`weatherRefreshMinutes` | 20 min | How often Panama updates the current conditions. Range 5–120. |
|
||||
|
||||
## windows
|
||||
|
||||
Found on **Appearance**.
|
||||
|
||||
| Setting | Default | What it does |
|
||||
|---|---|---|
|
||||
| **Inner gaps**<br>`gapsIn` `general:gaps_in` | 5 px | Space between neighboring tiled windows. Range 0–40. |
|
||||
| **Outer gaps**<br>`gapsOut` `general:gaps_out` | 10 px | Space between the tiled area and the screen edge. Range 0–80. |
|
||||
| **Border width**<br>`borderSize` `general:border_size` | 2 px | Thickness of the gradient border on the focused window. Range 0–10. |
|
||||
| **Corner radius**<br>`windowRounding` `decoration:rounding` | 18 px | Matches the shell's popover radius so windows and panels agree. Range 0–40. |
|
||||
| **Unfocused window opacity**<br>`inactiveOpacity` `decoration:inactive_opacity` | 1.0 | Fade windows that do not have focus. Range 0.5–1.0. |
|
||||
| **Focused window opacity**<br>`activeOpacity` `decoration:active_opacity` | 1.0 | Fade even the focused window; 1.0 is fully opaque. Range 0.5–1.0. |
|
||||
| **Fullscreen opacity**<br>`fullscreenOpacity` `decoration:fullscreen_opacity` | 1.0 | Applied instead of the focused opacity when a window is fullscreen. Range 0.5–1.0. |
|
||||
| **Corner shape**<br>`roundingPower` `decoration:rounding_power` | 2.0 | 2 is a circular corner; higher values approach a squircle. Range 2.0–10.0. |
|
||||
|
||||
@@ -136,7 +136,14 @@ jq -e '.count == 1 and .initialized == true' <<<"$home_before" >/dev/null \
|
||||
sleep 0.6
|
||||
|
||||
reset_state="$(qs_for_harness ipc call settings-system-test resetState)"
|
||||
# The snapshot must come FIRST. Restoring defaults is the only irreversible
|
||||
# action Panama offers, and a snapshot taken after the stores were cleared would
|
||||
# faithfully record the wiped state as if it were the user's.
|
||||
jq -e '.calls[0] == "snapshot"' <<<"$reset_state" >/dev/null \
|
||||
|| fail "reset did not snapshot before wiping the stores: $reset_state"
|
||||
|
||||
jq -e '.calls == [
|
||||
"snapshot",
|
||||
"display.block:true",
|
||||
"keybinds.reload",
|
||||
"wallpaper.set:",
|
||||
|
||||
Executable
+60
@@ -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<!-- drift -->\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"
|
||||
Reference in New Issue
Block a user