Compare commits
2
Commits
| 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 {
|
function refresh(): void {
|
||||||
if (!listQuery.running)
|
if (!listQuery.running)
|
||||||
@@ -195,6 +212,7 @@ Singleton {
|
|||||||
actionRun.exec([root.helperPath, "save", root.serializeHomeState()]);
|
actionRun.exec([root.helperPath, "save", root.serializeHomeState()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function serializeHomeState(): string {
|
function serializeHomeState(): string {
|
||||||
const current = root.readHomeState();
|
const current = root.readHomeState();
|
||||||
const favorites = [];
|
const favorites = [];
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ Singleton {
|
|||||||
property var displayBusy: function() { return Displays.busy || Displays.awaitingConfirmation; }
|
property var displayBusy: function() { return Displays.busy || Displays.awaitingConfirmation; }
|
||||||
property var readDisplays: function() { return DesktopPreferences.get("displays"); }
|
property var readDisplays: function() { return DesktopPreferences.get("displays"); }
|
||||||
property var protectDisplays: function(value) { return DesktopPreferences.set("displays", value); }
|
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 setDisplayBlocked: function(blocked) { Displays.externalChangeBlocked = blocked; }
|
||||||
property var reloadKeybinds: function() { Keybinds.applyReload(); }
|
property var reloadKeybinds: function() { Keybinds.applyReload(); }
|
||||||
property var keybindsReloading: function() { return Keybinds.reloading; }
|
property var keybindsReloading: function() { return Keybinds.reloading; }
|
||||||
@@ -509,6 +516,17 @@ Singleton {
|
|||||||
return false;
|
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);
|
root.setDisplayBlocked(true);
|
||||||
DesktopPreferences.resetDesktopDefaults();
|
DesktopPreferences.resetDesktopDefaults();
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ ShellRoot {
|
|||||||
root.recordReset("display.block:" + blocked);
|
root.recordReset("display.block:" + blocked);
|
||||||
root.displayBlocked = 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.reloadKeybinds = function() { root.recordReset("keybinds.reload"); };
|
||||||
SystemSettings.keybindsReloading = function() { return false; };
|
SystemSettings.keybindsReloading = function() { return false; };
|
||||||
SystemSettings.applyWallpaper = function(path) { root.recordReset("wallpaper.set:" + path); };
|
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
|
sleep 0.6
|
||||||
|
|
||||||
reset_state="$(qs_for_harness ipc call settings-system-test resetState)"
|
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 == [
|
jq -e '.calls == [
|
||||||
|
"snapshot",
|
||||||
"display.block:true",
|
"display.block:true",
|
||||||
"keybinds.reload",
|
"keybinds.reload",
|
||||||
"wallpaper.set:",
|
"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