pragma Singleton // ───────────────────────────────────────────────────────────────────────────── // The schema is the single source of truth for every user-changeable setting. // // One entry describes a setting completely: its name, type, default, valid // range, which group it belongs to, and how to label it. Persistence, // validation, reset, and (from Stage 3) the settings UI itself are all derived // from these entries rather than restated. // // Adding a setting means adding one entry here. It does not mean editing a // property alias, a JSON adapter, a change handler, and a reset function -- // which is what it used to mean, and why the settings app stalled at sixteen // knobs while forty comparable values stayed hardcoded one file away. // // Entry fields // key unique identifier; also the JSON key on disk // type "bool" | "int" | "real" | "string" | "enum" | "json" // def shipped default, used when the file is absent or a value is invalid // min/max inclusive bounds for int and real; values outside are clamped // step UI increment for int and real // options for "enum": [{ value, label }], the only accepted values // group grouping id, used to build settings pages // label short UI name // detail one line explaining what changing it does // internal true for state the shell keeps but the user never edits directly // pattern for "string": a regular expression the value must match in full // // "json" holds a structured value -- a list or an object -- that the schema // stores and resets but does not validate field by field. It exists so that // settings like the dock's pinned applications live in the same file, and are // covered by the same reset, as everything else rather than growing a fourth // preference store. The service that owns such a value is responsible for // validating it; see services/Dock-related consumers. // hypr present when the setting maps onto an Hyprland option: // path the hl.config table path, e.g. ["decoration","blur","size"] // option the getoption path used to read the value back // readAs which field getoption returns it in -- "int", "bool", // "float", "str", or "css" (gaps, returned as a box) // // Anything with a `hypr` block is applied live by services/SystemSettings.qml // and read at startup by config/dot/hypr/prefs.lua, using the same key. The Lua // keeps the shipped value as its fallback, so the config still works with no // settings file at all. // ───────────────────────────────────────────────────────────────────────────── import Quickshell import QtQuick Singleton { id: root readonly property var entries: [ // ── Clock ─────────────────────────────────────────────────────────── // Group datetime, not clock: this drives the date menu, notification // timestamps, and the lock screen — Date & Time owns it. { key: "use24Hour", type: "bool", def: false, group: "datetime", label: "24-hour time", detail: "Use 18:30 instead of 6:30 PM" }, // ── Bar ───────────────────────────────────────────────────────────── // The bar floats directly on the wallpaper; these keep it legible on // grounds the theme never met, and choose which widgets earn a place. { key: "barTextTone", type: "enum", def: "theme", group: "bar", label: "Bar text", detail: "Follow the theme, or force a light or dark tone for the wallpaper you actually use", options: [ { value: "theme", label: "Follow theme" }, { value: "light", label: "Light" }, { value: "dark", label: "Dark" } ] }, { key: "barTextShadow", type: "bool", def: false, group: "bar", label: "Bar text shadow", detail: "A soft dark halo under every glyph and label in the bar" }, { key: "barBackdrop", type: "bool", def: false, group: "bar", label: "Bar backdrop", detail: "A subtle scrim fading down from the top edge" }, { key: "showWeatherWidget", type: "bool", def: true, group: "bar", label: "Weather in the bar", detail: "Beside the clock, once a forecast has been fetched" }, { key: "showMediaWidget", type: "bool", def: true, group: "bar", label: "Media in the bar", detail: "Now playing, click to pause" }, { key: "showClipboardButton", type: "bool", def: true, group: "bar", label: "Clipboard button", detail: "The history stays on Super+V either way" }, { key: "showCalendarCountdown", type: "bool", def: true, group: "bar", label: "Calendar countdown", detail: "Appears in the bar fifteen minutes before an event" }, // ── Control Center ────────────────────────────────────────────────── { key: "ccShowFocus", type: "bool", def: true, group: "controlCenter", label: "Focus in Control Center", detail: "The session row at the top of the panel" }, { key: "ccShowHome", type: "bool", def: true, group: "controlCenter", label: "Home in Control Center", detail: "Your accessory shelf" }, { key: "ccShowPhone", type: "bool", def: true, group: "controlCenter", label: "Phone in Control Center", detail: "Vitals and reach-it actions" }, { key: "showSeconds", type: "bool", def: true, group: "clock", label: "Show seconds", detail: "Keep a precise clock in the center of the bar" }, { key: "showWeekday", type: "bool", def: true, group: "clock", label: "Show weekday", detail: "Include the abbreviated weekday before the date" }, // ── Vitals ────────────────────────────────────────────────────────── { key: "showCpu", type: "bool", def: true, group: "vitals", label: "Processor", detail: "Show processor usage beside the workspace indicator" }, { key: "showMemory", type: "bool", def: true, group: "vitals", label: "Memory", detail: "Show memory usage beside the workspace indicator" }, { key: "showGpu", type: "bool", def: true, group: "vitals", label: "Graphics", detail: "Show graphics usage beside the workspace indicator" }, // Only ever visible on a machine that has a battery: the indicator // gates on Battery.available as well as this, the way the graphics // field gates on Vitals.gpuAvailable. { key: "showBattery", type: "bool", def: true, group: "vitals", label: "Battery", detail: "Show the charge level in the bar, on machines that have a battery" }, // The number beside the icon, GNOME's "Show Battery Percentage". // Off by default for the same reason GNOME ships it off: the icon // already says what matters, and the number is for people who want it. { key: "showBatteryPercent", type: "bool", def: false, group: "vitals", label: "Battery percentage", detail: "Show the exact number beside the battery icon" }, // Off by default: this is a coding-tool readout, not something a // general-purpose desktop should show without being asked. { key: "showAgentUsage", type: "bool", def: false, group: "vitals", label: "Agent usage", detail: "Show how much of the busiest agent subscription has been used, beside the other vitals" }, // ── Agents ────────────────────────────────────────────────────────── // The escalation ladder and the usage collectors. `preferredAgent` is // deliberately "none" out of the box: until an agent is chosen, crash // notifications carry no action -- the desktop stays quiet rather than // volunteering a tool the user never asked for. { key: "preferredAgent", type: "enum", def: "none", group: "agents", label: "Preferred agent", detail: "Who answers when the desktop offers to investigate something", options: [ { value: "none", label: "None" }, { value: "claude", label: "Claude Code" }, { value: "codex", label: "Codex" } ] }, { key: "crashDiagnoseOffer", type: "bool", def: true, group: "agents", label: "Offer to diagnose crashes", detail: "When a program dumps core, the notification carries a click that opens the preferred agent mid-investigation with the crash details in hand" }, { key: "reloadFailureOffer", type: "bool", def: true, group: "agents", label: "Offer help when the shell fails to reload", detail: "A broken change to the shell's own configuration offers the failing log to the agent" }, { key: "healthAgentHandoff", type: "bool", def: true, group: "agents", label: "System Health hands off unrepairable checks", detail: "A red check with no repair, or whose repair failed, grows an Ask-the-agent button carrying the check's snapshot" }, { key: "agentAutoApprove", type: "bool", def: true, group: "agents", label: "Launched agents approve their own tools", detail: "Investigations run without permission prompts. The diagnose skill still holds agents to reading rather than fixing, and root still goes through panama-sudo, reason and all" }, { key: "agentUsageClaude", type: "bool", def: true, group: "agents", label: "Collect Claude Code usage", detail: "Limits from Anthropic's usage endpoint, tokens from the local transcripts" }, { key: "agentUsageCodex", type: "bool", def: true, group: "agents", label: "Collect Codex usage", detail: "Limits over the Codex app-server, sessions from its local files" }, { key: "agentUsageRefreshMinutes", type: "int", def: 15, min: 5, max: 60, step: 5, unit: " min", group: "agents", label: "Refresh interval", detail: "How often the usage collectors ask for fresh numbers, in minutes" }, // ── Battery ───────────────────────────────────────────────────────── // The two points at which the desktop starts telling you. Low is a // quiet mention; critical is the one that interrupts, so it is // published at a priority Do Not Disturb does not silence. { key: "batteryLowPercent", type: "int", def: 20, min: 5, max: 50, step: 5, unit: "%", group: "battery", label: "Warn at", detail: "Mention the battery once it drops this low" }, { key: "batteryCriticalPercent", type: "int", def: 5, min: 1, max: 25, step: 1, unit: "%", group: "battery", label: "Urgent at", detail: "Interrupt at this level, even during Do Not Disturb" }, // What the desktop DOES at the critical threshold, beyond interrupting. // Suspend by default, which is GNOME's behavior: sleep preserves the // session at a level the firmware can hold for days, and the // alternative -- a hard cut at 0% -- preserves nothing. { key: "batteryCriticalAction", type: "enum", def: "suspend", group: "battery", label: "At the urgent level", detail: "What happens when the battery reaches the urgent threshold while discharging", options: [ { value: "suspend", label: "Suspend" }, { value: "nothing", label: "Only warn" } ] }, // Only offered where the firmware exposes a ceiling; the Power page // hides the control entirely otherwise. 100 means charge to full. { key: "batteryChargeLimit", type: "int", def: 100, min: 50, max: 100, step: 5, unit: "%", group: "battery", label: "Stop charging at", detail: "Charging to less than full is easier on the battery over years" }, // ── Dock ──────────────────────────────────────────────────────────── { key: "dockAutohide", type: "bool", def: true, group: "dock", label: "Automatically hide the Dock", detail: "Reveal it at the bottom edge when a workspace is occupied" }, { key: "dockPosition", type: "enum", def: "bottom", group: "dock", label: "Position", detail: "Which edge the Dock lives on", options: [ { value: "bottom", label: "Bottom" }, { value: "left", label: "Left" }, { value: "right", label: "Right" } ] }, // A "json" value: the screen names the Dock appears on. Empty means // every screen, which is both the sensible default and the right // answer for the common single-monitor case -- storing a list of // names there would go stale the moment a display is unplugged. { key: "dockScreens", type: "json", def: [], group: "dock", label: "Screens", detail: "Which displays show the Dock" }, { key: "dockIconSize", type: "int", def: 48, min: 32, max: 80, step: 4, unit: "px", group: "dock", label: "Icon size", detail: "How large the Dock's application icons are drawn" }, { key: "dockRevealDelayMs", type: "int", def: 0, min: 0, max: 1000, step: 25, unit: "ms", group: "dock", label: "Reveal delay", detail: "Zero reveals the Dock the instant the pointer reaches the edge" }, { key: "dockHideDelayMs", type: "int", def: 250, min: 0, max: 2000, step: 25, unit: "ms", group: "dock", label: "Hide delay", detail: "Prevents flicker when crossing between icons" }, // ── Focus ─────────────────────────────────────────────────────────── // ── Focus modes ───────────────────────────────────────────────────── // A "json" value: named modes, each with what turns it on and what it // does. Triggers rather than a scheduler -- a mode is on because a // condition is true right now, which is re-evaluated rather than fired // once. A schedule is one of those conditions ("is now inside this // window?"), which is why suspend, a reboot mid-window, and a lid // opened after the start time all behave correctly without special // cases: there is no alarm to have missed. // // Gaming ships enabled because the behaviour already existed as // gamingSilenceNotifications; Sleep ships disabled, because a desktop // that starts silencing someone on first boot has overstepped. { key: "focusModes", type: "json", group: "focus", label: "Focus modes", detail: "What quiets this machine, and what turns it on", def: [ { id: "deep-work", name: "Deep work", enabled: true, triggers: [{ kind: "manual" }], durationMinutes: 45, silence: true, keepAwake: true, allow: [] }, { id: "gaming", name: "Gaming", enabled: true, triggers: [{ kind: "game" }], durationMinutes: 0, silence: true, keepAwake: true, allow: [] }, { id: "sleep", name: "Sleep", enabled: false, triggers: [{ kind: "schedule", start: "23:30", end: "07:00", days: [0, 1, 2, 3, 4, 5, 6] }], durationMinutes: 0, silence: true, keepAwake: false, allow: [] } ] }, // Set by the duration chips on the Focus tab, which is also where // focusModes renders and where the focus group routes -- one editor, // one page, so search and the docs point at the only place it exists. { key: "focusDurationMinutes", type: "int", def: 45, min: 5, max: 180, step: 5, unit: "min", group: "focus", label: "Focus session length", detail: "How long a focus session runs before it ends itself" }, // ── Display policy ────────────────────────────────────────────────── // These three are written to the compositor and verified by read-back. // See services/SystemSettings.qml for why the exit code cannot be // trusted for either hyprctl keyword or hyprctl eval. // GNOME's Multitasking panel had exactly this choice, and it is the one // worth reproducing: not which workspace goes on which screen, but // whether the second screen participates in workspaces at all. // // No `hypr` block, because this is not an option. It becomes workspace // rules in monitors.lua, and Hyprland reads those at config time and // will not let one be removed afterwards -- so applying a change is a // reload rather than a write, which is what Workspaces.qml owns. { key: "workspacesOnPrimaryOnly", type: "bool", def: false, group: "display", label: "Workspaces on the primary display only", detail: "Other screens keep one workspace of their own rather than switching along with it" }, { key: "autoHdr", type: "bool", def: true, group: "display", label: "Game-aware HDR", detail: "Hand HDR to fullscreen games while the desktop stays SDR", hypr: { path: ["render", "cm_auto_hdr"], option: "render:cm_auto_hdr", readAs: "int" } }, { key: "vrrPolicy", type: "enum", def: 3, group: "display", label: "Variable refresh rate", detail: "Matches the display's refresh rate to what is on screen", // All four the compositor publishes, rather than the two that were // here. Always-on VRR is a legitimate choice on a panel that // handles it well, and it was simply unreachable -- as was // fullscreen-only, which is what someone wanting VRR for video // rather than games wants. options: [ { value: 0, label: "Off", detail: "The display runs at a fixed refresh rate" }, { value: 1, label: "Always on", detail: "Best on panels that handle low refresh rates without flicker" }, { value: 2, label: "Fullscreen only", detail: "Any fullscreen window, including video" }, { value: 3, label: "Fullscreen games", detail: "Only fullscreen games, which is the safest default" } ], hypr: { path: ["misc", "vrr"], option: "misc:vrr", readAs: "int" } }, { key: "directScanoutPolicy", type: "enum", def: 2, group: "display", label: "Direct scanout", detail: "Lets fullscreen content bypass compositing", options: [ { value: 0, label: "Off", detail: "Everything goes through the compositor" }, { value: 1, label: "Always on", detail: "Forced rather than decided per surface; can drop frames on some drivers" }, { value: 2, label: "Automatic", detail: "The compositor decides per surface, which is the safe default" } ], hypr: { path: ["render", "direct_scanout"], option: "render:direct_scanout", readAs: "int" } }, // ── Window appearance ─────────────────────────────────────────────── // These adjust the parameters of the Prism identity -- how much space, // how soft, how much motion -- rather than replacing it. Shipped values // are duplicated as the fallbacks in config/dot/hypr/looks.lua so that // the Hyprland config still stands on its own. { key: "gapsIn", type: "int", def: 5, min: 0, max: 40, step: 1, unit: "px", group: "windows", label: "Inner gaps", detail: "Space between neighboring tiled windows", hypr: { path: ["general", "gaps_in"], option: "general:gaps_in", readAs: "css" } }, { key: "gapsOut", type: "int", def: 10, min: 0, max: 80, step: 1, unit: "px", group: "windows", label: "Outer gaps", detail: "Space between the tiled area and the screen edge", hypr: { path: ["general", "gaps_out"], option: "general:gaps_out", readAs: "css" } }, { key: "borderSize", type: "int", def: 2, min: 0, max: 10, step: 1, unit: "px", group: "windows", label: "Border width", detail: "Thickness of the gradient border on the focused window", hypr: { path: ["general", "border_size"], option: "general:border_size", readAs: "int" } }, { key: "windowRounding", type: "int", def: 18, min: 0, max: 40, step: 1, unit: "px", group: "windows", label: "Corner radius", detail: "Matches the shell's popover radius so windows and panels agree", hypr: { path: ["decoration", "rounding"], option: "decoration:rounding", readAs: "int" } }, { key: "inactiveOpacity", type: "real", def: 1.0, min: 0.5, max: 1.0, step: 0.05, group: "windows", label: "Unfocused window opacity", detail: "Fade windows that do not have focus", hypr: { path: ["decoration", "inactive_opacity"], option: "decoration:inactive_opacity", readAs: "float" } }, { key: "activeOpacity", type: "real", def: 1.0, min: 0.5, max: 1.0, step: 0.05, group: "windows", label: "Focused window opacity", detail: "Fade even the focused window; 1.0 is fully opaque", hypr: { path: ["decoration", "active_opacity"], option: "decoration:active_opacity", readAs: "float" } }, { key: "fullscreenOpacity", type: "real", def: 1.0, min: 0.5, max: 1.0, step: 0.05, group: "windows", label: "Fullscreen opacity", detail: "Applied instead of the focused opacity when a window is fullscreen", hypr: { path: ["decoration", "fullscreen_opacity"], option: "decoration:fullscreen_opacity", readAs: "float" } }, { key: "roundingPower", type: "real", def: 2.0, min: 2.0, max: 10.0, step: 0.5, group: "windows", label: "Corner shape", detail: "2 is a circular corner; higher values approach a squircle", hypr: { path: ["decoration", "rounding_power"], option: "decoration:rounding_power", readAs: "float" } }, // ── Window edges ──────────────────────────────────────────────────── // How the pointer interacts with a window's border, and how windows // behave near each other. All shipped by looks.lua with no way to // change any of it. { key: "resizeOnBorder", type: "bool", def: true, group: "edges", label: "Resize by dragging the border", detail: "Drag a window's edge to resize it, instead of only with the keyboard", hypr: { path: ["general", "resize_on_border"], option: "general:resize_on_border", readAs: "bool" } }, { key: "borderGrabArea", type: "int", def: 15, min: 0, max: 40, step: 1, unit: "px", group: "edges", label: "Border grab area", detail: "How far outside the border still counts as grabbing it. Larger is easier to hit", hypr: { path: ["general", "extend_border_grab_area"], option: "general:extend_border_grab_area", readAs: "int" } }, { key: "hoverIconOnBorder", type: "bool", def: true, group: "edges", label: "Show the resize cursor", detail: "Change the pointer when it is over a resizable border", hypr: { path: ["general", "hover_icon_on_border"], option: "general:hover_icon_on_border", readAs: "bool" } }, { key: "snapWindowGap", type: "int", def: 10, min: 0, max: 60, step: 1, unit: "px", group: "edges", label: "Snap distance between windows", detail: "How close two floating windows must be before they snap together", hypr: { path: ["general", "snap", "window_gap"], option: "general:snap:window_gap", readAs: "int" } }, { key: "snapMonitorGap", type: "int", def: 10, min: 0, max: 60, step: 1, unit: "px", group: "edges", label: "Snap distance to screen edges", detail: "How close a floating window must be to an edge before it snaps to it", hypr: { path: ["general", "snap", "monitor_gap"], option: "general:snap:monitor_gap", readAs: "int" } }, { key: "snapRespectGaps", type: "bool", def: false, group: "edges", label: "Snapping respects gaps", detail: "Snapped windows keep the configured gap instead of touching", hypr: { path: ["general", "snap", "respect_gaps"], option: "general:snap:respect_gaps", readAs: "bool" } }, // ── Master layout ─────────────────────────────────────────────────── // Only meaningful when the tiling layout is Master and stack. Offering // that layout with none of its options was an omission: it is the one // layout whose whole behavior is in these settings. { key: "masterFactor", type: "real", def: 0.55, min: 0.1, max: 0.9, step: 0.05, group: "master", label: "Master area size", detail: "How much of the screen the master window takes", hypr: { path: ["master", "mfact"], option: "master:mfact", readAs: "float" } }, { key: "masterOrientation", type: "enum", def: "left", group: "master", label: "Master area position", detail: "Which side of the screen the master window occupies", options: [ { value: "left", label: "Left" }, { value: "right", label: "Right" }, { value: "top", label: "Top" }, { value: "bottom", label: "Bottom" }, { value: "center", label: "Center" } ], hypr: { path: ["master", "orientation"], option: "master:orientation", readAs: "str" } }, { key: "masterNewStatus", type: "enum", def: "slave", group: "master", label: "New windows become", detail: "Whether a new window takes the master area or joins the stack", options: [ { value: "master", label: "The master window" }, { value: "slave", label: "Part of the stack" }, { value: "inherit", label: "Whatever the focused window is" } ], hypr: { path: ["master", "new_status"], option: "master:new_status", readAs: "str" } }, { key: "masterNewOnTop", type: "bool", def: false, group: "master", label: "Add new windows at the top", detail: "New stack windows go above the others rather than below", hypr: { path: ["master", "new_on_top"], option: "master:new_on_top", readAs: "bool" } }, // ── Hyprland's own notices ────────────────────────────────────────── // Panama turns all four off on the user's behalf. That is a defensible // default and was not a decision anyone could reverse without editing // looks.lua, which is precisely the kind of thing this app exists to // stop. { key: "hyprlandLogo", type: "bool", def: false, group: "notices", label: "Hyprland wallpaper", detail: "The stock background Hyprland draws when no wallpaper is set", hypr: { path: ["misc", "disable_hyprland_logo"], option: "misc:disable_hyprland_logo", readAs: "bool", invert: true } }, { key: "hyprlandSplash", type: "bool", def: false, group: "notices", label: "Splash text", detail: "The line of text Hyprland renders over the stock background", hypr: { path: ["misc", "disable_splash_rendering"], option: "misc:disable_splash_rendering", readAs: "bool", invert: true } }, { key: "hyprlandUpdateNews", type: "bool", def: false, group: "notices", label: "Update announcements", detail: "The window Hyprland opens after an update to describe what changed", hypr: { path: ["ecosystem", "no_update_news"], option: "ecosystem:no_update_news", readAs: "bool", invert: true } }, { key: "hyprlandDonationNag", type: "bool", def: false, group: "notices", label: "Donation reminders", detail: "The prompt Hyprland shows twice a year asking for support", hypr: { path: ["ecosystem", "no_donation_nag"], option: "ecosystem:no_donation_nag", readAs: "bool", invert: true } }, // ── Effects ───────────────────────────────────────────────────────── { key: "blurEnabled", type: "bool", def: true, group: "effects", label: "Blur", detail: "Blur the desktop behind translucent surfaces", hypr: { path: ["decoration", "blur", "enabled"], option: "decoration:blur:enabled", readAs: "bool" } }, { key: "blurSize", type: "int", def: 8, min: 1, max: 20, step: 1, group: "effects", label: "Blur radius", detail: "Larger is softer and costs more frame time", hypr: { path: ["decoration", "blur", "size"], option: "decoration:blur:size", readAs: "int" } }, { key: "blurPasses", type: "int", def: 3, min: 1, max: 5, step: 1, group: "effects", label: "Blur passes", detail: "More passes look smoother and cost more frame time", hypr: { path: ["decoration", "blur", "passes"], option: "decoration:blur:passes", readAs: "int" } }, { key: "shadowEnabled", type: "bool", def: true, group: "effects", label: "Window shadows", detail: "Lift windows off the wallpaper with a soft shadow", hypr: { path: ["decoration", "shadow", "enabled"], option: "decoration:shadow:enabled", readAs: "bool" } }, { key: "shadowRange", type: "int", def: 20, min: 0, max: 60, step: 1, unit: "px", group: "effects", label: "Shadow size", detail: "How far the shadow spreads from the window edge", hypr: { path: ["decoration", "shadow", "range"], option: "decoration:shadow:range", readAs: "int" } }, { key: "shadowSharp", type: "bool", def: false, group: "effects", label: "Hard-edged shadow", detail: "A crisp shadow instead of a soft falloff", hypr: { path: ["decoration", "shadow", "sharp"], option: "decoration:shadow:sharp", readAs: "bool" } }, { key: "shadowRenderPower", type: "int", def: 3, min: 1, max: 4, step: 1, group: "effects", label: "Shadow falloff", detail: "How sharply the shadow fades out. Higher is tighter to the window", hypr: { path: ["decoration", "shadow", "render_power"], option: "decoration:shadow:render_power", readAs: "int" } }, { key: "glowEnabled", type: "bool", def: true, group: "effects", label: "Focus glow", detail: "A faint halo behind the focused window", hypr: { path: ["decoration", "glow", "enabled"], option: "decoration:glow:enabled", readAs: "bool" } }, { key: "glowRange", type: "int", def: 8, min: 0, max: 30, step: 1, unit: "px", group: "effects", label: "Glow size", detail: "Kept small deliberately: the gradient border is the signature", hypr: { path: ["decoration", "glow", "range"], option: "decoration:glow:range", readAs: "int" } }, { key: "animationsEnabled", type: "bool", def: true, group: "effects", label: "Animations", detail: "Window, workspace, and panel motion", hypr: { path: ["animations", "enabled"], option: "animations:enabled", readAs: "bool" } }, // ── Input ─────────────────────────────────────────────────────────── { key: "keyboardLayout", type: "string", def: "us", group: "input", // Reaches an hl.config string, so it is constrained to the shape of // an XKB layout list and nothing else. pattern: "^[a-z]{2,8}(,[a-z]{2,8})*$", label: "Keyboard layout", detail: "XKB layout name, or a comma-separated list to switch between", hypr: { path: ["input", "kb_layout"], option: "input:kb_layout", readAs: "str" } }, { key: "keyboardVariant", type: "string", def: "", group: "input", // Same shape as the layout list and for the same reason: this is // serialized into an hl.config string. pattern: "^$|^[a-z0-9_]{1,24}(,[a-z0-9_]{1,24})*$", label: "Layout variant", detail: "XKB variant, such as dvorak or colemak. Empty for the standard layout", hypr: { path: ["input", "kb_variant"], option: "input:kb_variant", readAs: "str" } }, { key: "keyboardOptions", type: "string", def: "caps:escape_shifted_capslock", group: "input", // XKB option names are colon-separated pairs in a comma-separated // list, e.g. "compose:ralt,caps:escape". pattern: "^$|^[a-z0-9_]+:[a-z0-9_]+(,[a-z0-9_]+:[a-z0-9_]+)*$", label: "Keyboard options", detail: "XKB options, such as compose:ralt to make right Alt a compose key", hypr: { path: ["input", "kb_options"], option: "input:kb_options", readAs: "str" } }, { key: "numlockByDefault", type: "bool", def: true, group: "input", label: "Num Lock on login", detail: "Turn Num Lock on when the session starts", hypr: { path: ["input", "numlock_by_default"], option: "input:numlock_by_default", readAs: "bool" } }, { key: "keyRepeatDelay", type: "int", def: 500, min: 150, max: 1000, step: 25, unit: "ms", group: "input", label: "Repeat delay", detail: "How long a key is held before it starts repeating", hypr: { path: ["input", "repeat_delay"], option: "input:repeat_delay", readAs: "int" } }, { key: "keyRepeatRate", type: "int", def: 33, min: 5, max: 100, step: 1, unit: "/s", group: "input", label: "Repeat rate", detail: "How many characters a second a held key produces", hypr: { path: ["input", "repeat_rate"], option: "input:repeat_rate", readAs: "int" } }, { key: "followMouse", type: "enum", def: 1, group: "pointer", label: "Pointer focus", detail: "What moving the pointer does to which window is focused", // These labels were wrong, and wrong in the worst way: value 1 was // shown as "Click to focus" while Hyprland's 1 means the opposite. // The compositor publishes the authoritative mapping itself -- // `hyprctl descriptions` gives // map: [{"separate":3},{"detached":2},{"follow":1},{"disabled":0}] // -- so a desktop labeled "Click to focus" was in fact following // the pointer, and the way to actually get click-to-focus was to // choose "Never". Value 3 was missing entirely. // // enum-hypr-map-contract now pins every mapped enum against that // published map, so this cannot drift again. options: [ { value: 0, label: "Click to focus", detail: "Moving the pointer never changes focus" }, { value: 1, label: "Focus follows pointer", detail: "The window under the pointer takes focus as you move" }, { value: 2, label: "Pointer detached", detail: "The pointer highlights windows on its own; clicking moves keyboard focus" }, { value: 3, label: "Pointer fully separate", detail: "Clicking does not move keyboard focus at all" } ], hypr: { path: ["input", "follow_mouse"], option: "input:follow_mouse", readAs: "int" } }, { key: "pointerSensitivity", type: "real", def: 0.0, min: -1.0, max: 1.0, step: 0.05, group: "pointer", label: "Pointer speed", detail: "Zero is flat, unaccelerated response", hypr: { path: ["input", "sensitivity"], option: "input:sensitivity", readAs: "float" } }, { key: "cursorInactiveTimeout", type: "int", def: 4, min: 0, max: 60, step: 1, unit: "s", group: "pointer", label: "Hide pointer after", detail: "Seconds of stillness before the pointer fades out; 0 never hides it", // Reported as a float even though it is only ever set to whole // seconds. `readAs` describes what getoption answers with, not what // the setting means -- getting this wrong makes every write to it // look rejected. hypr: { path: ["cursor", "inactive_timeout"], option: "cursor:inactive_timeout", readAs: "float" } }, // ── Pointer ───────────────────────────────────────────────────────── // // Every `readAs` below was read back off the running compositor rather // than guessed. Two are not what they look like: touchpad drag lock is // an int with three states, not a switch, and scroll factors are floats // even at their default of exactly 1. { key: "naturalScroll", type: "bool", def: false, group: "pointer", label: "Natural scrolling", detail: "Content follows the direction of your fingers, as on a phone", hypr: { path: ["input", "natural_scroll"], option: "input:natural_scroll", readAs: "bool" } }, { key: "accelProfile", type: "enum", def: "flat", group: "pointer", label: "Acceleration", detail: "Flat moves the pointer the same distance however fast you move", options: [ { value: "flat", label: "Flat" }, { value: "adaptive", label: "Adaptive" } ], hypr: { path: ["input", "accel_profile"], option: "input:accel_profile", readAs: "str" } }, { key: "scrollFactor", type: "real", def: 1.0, min: 0.1, max: 4.0, step: 0.1, group: "pointer", label: "Scroll speed", detail: "Multiplies how far one notch of the wheel scrolls", hypr: { path: ["input", "scroll_factor"], option: "input:scroll_factor", readAs: "float" } }, { key: "leftHanded", type: "bool", def: false, group: "pointer", label: "Left-handed", detail: "Swap the primary and secondary buttons", hypr: { path: ["input", "left_handed"], option: "input:left_handed", readAs: "bool" } }, { key: "middleClickPaste", type: "bool", def: true, group: "pointer", label: "Middle-click paste", detail: "Paste the primary selection in GTK and native Wayland applications", hypr: { path: ["misc", "middle_click_paste"], option: "misc:middle_click_paste", readAs: "bool" } }, { key: "focusOnClose", type: "enum", def: 0, group: "pointer", label: "Focus after closing", detail: "Which window takes keyboard focus when the focused one goes away", // Designed as a two-way choice; the compositor publishes three. // map: [{"mru":2},{"cursor":1},{"next":0}] // and 0 -- the value this desktop runs on today -- is "next in the // stack", which is neither of the two the design named. Hiding it // would make the shipped default unreachable from its own dropdown, // and enum-hypr-map-contract refuses an enum that drops a published // value for exactly that reason. options: [ { value: 0, label: "Next in the stack", detail: "Whichever window Hyprland has next in the layout order" }, { value: 1, label: "Under the pointer", detail: "Whatever window the pointer happens to be over" }, { value: 2, label: "Most recently used", detail: "The window you were on before this one" } ], hypr: { path: ["input", "focus_on_close"], option: "input:focus_on_close", readAs: "int" } }, { key: "scrollMethod", type: "enum", def: "", group: "pointer", label: "Scroll method", detail: "How a pointing device turns movement into scrolling", // No `map` is published for this one -- it is a plain string option, // and the words it accepts live in its description instead: // [2fg/edge/on_button_down/no_scroll]. // // Unset is a real state rather than an absence, and it is the state // Panama ships: getoption answers "[[EMPTY]]" until something writes // the option, and an empty value means "whatever libinput picks for // this device", which is the branch every stock Hyprland takes. So // empty is offered as a choice of its own -- without it the setting // would be a one-way door, and its default would be unreachable. // Writing "" reads back as "" with set:true, the same round trip // input:kb_variant has made for as long as it has been empty. options: [ { value: "", label: "Whatever suits the device", detail: "Two fingers on a touchpad, the wheel on a mouse" }, { value: "2fg", label: "Two fingers" }, { value: "edge", label: "Along the edge of the touchpad" }, { value: "on_button_down", label: "While a button is held" }, { value: "no_scroll", label: "Never scroll" } ], hypr: { path: ["input", "scroll_method"], option: "input:scroll_method", readAs: "str" } }, { key: "scrollButton", type: "int", def: 0, min: 0, max: 300, step: 1, group: "pointer", label: "Scroll button", detail: "Which button is held to scroll, as an evdev code; 0 lets the device choose", // The range is the compositor's own rather than a guess: descriptions // gives min 0, max 300. Only meaningful while Scroll method is // "While a button is held", which is a UI condition, not a schema one // -- the value stays valid and stored either way. hypr: { path: ["input", "scroll_button"], option: "input:scroll_button", readAs: "int" } }, { key: "cursorHideWhileTyping", type: "bool", def: false, group: "pointer", label: "Hide pointer while typing", detail: "The pointer vanishes on the next keystroke and returns when you move it", // A `cursor:` option rather than an `input:` one, so its read-back in // the Lua sits in a cursor table of its own; see hypr/input.lua. hypr: { path: ["cursor", "hide_on_key_press"], option: "cursor:hide_on_key_press", readAs: "bool" } }, { key: "cursorWarpOnWorkspaceChange", type: "bool", def: false, group: "pointer", label: "Jump pointer to the focused display", detail: "Moves the pointer to the last focused window after switching workspace", // A switch here, an integer in the compositor -- the same shape // autoHdr has, and `readAs: "int"` is what keeps the two sides in // agreement. The published map is // map: [{"force":2},{"enable":1},{"disable":0}] // and "force" -- warp even when the pointer is already on that // display -- is deliberately not offered: a third state would turn a // switch into a dropdown for a distinction almost nobody wants. // enum-hypr-map-contract governs enums only, so this is a decision // rather than a violation, but it IS a decision: value 2 is not // reachable from Settings. hypr: { path: ["cursor", "warp_on_change_workspace"], option: "cursor:warp_on_change_workspace", readAs: "int" } }, // ── Touchpad ──────────────────────────────────────────────────────── // // Shown only on machines that have one. These are separate from the // pointer settings above because libinput keeps them separate: a mouse // and a touchpad on the same machine can scroll in opposite directions, // and usually should. { key: "touchpadTapToClick", type: "bool", def: true, group: "touchpad", label: "Tap to click", detail: "A tap counts as a click without pressing down", // The Lua config key and the hyprctl option name genuinely differ // here: hl.config wants input.touchpad.tap_to_click, getoption // answers to input:touchpad:tap-to-click. Using either spelling for // both fails -- a hyphen is not a Lua identifier, and the // underscored name is not a known option to getoption. This is what // the two separate fields are for. hypr: { path: ["input", "touchpad", "tap_to_click"], option: "input:touchpad:tap-to-click", readAs: "bool" } }, { key: "touchpadNaturalScroll", type: "bool", def: true, group: "touchpad", label: "Natural scrolling", detail: "Content follows the direction of your fingers", hypr: { path: ["input", "touchpad", "natural_scroll"], option: "input:touchpad:natural_scroll", readAs: "bool" } }, { key: "touchpadDisableWhileTyping", type: "bool", def: true, group: "touchpad", label: "Disable while typing", detail: "Ignore the touchpad briefly after a keystroke, so a palm cannot move the pointer", hypr: { path: ["input", "touchpad", "disable_while_typing"], option: "input:touchpad:disable_while_typing", readAs: "bool" } }, { key: "touchpadScrollFactor", type: "real", def: 1.0, min: 0.1, max: 4.0, step: 0.1, group: "touchpad", label: "Scroll speed", detail: "Multiplies how far a two-finger scroll travels", hypr: { path: ["input", "touchpad", "scroll_factor"], option: "input:touchpad:scroll_factor", readAs: "float" } }, { key: "touchpadDragLock", type: "enum", def: 0, group: "touchpad", label: "Drag lock", detail: "Keeps a tap-and-drag active when you lift a finger mid-drag", // An int with three states rather than a switch, which is why this // is an enum: reported as `int` by getoption, not `bool`. options: [ { value: 0, label: "Off" }, { value: 1, label: "On" }, { value: 2, label: "On, until you tap again" } ], hypr: { path: ["input", "touchpad", "drag_lock"], option: "input:touchpad:drag_lock", readAs: "int" } }, { key: "touchpadMiddleButtonEmulation", type: "bool", def: false, group: "touchpad", label: "Middle-click by pressing both buttons", detail: "Pressing left and right together acts as a middle click", hypr: { path: ["input", "touchpad", "middle_button_emulation"], option: "input:touchpad:middle_button_emulation", readAs: "bool" } }, { key: "touchpadClickfinger", type: "bool", def: false, group: "touchpad", label: "Two-finger right-click", detail: "One, two, or three fingers pressing down give left, right, and middle click, instead of clicking by which part of the pad you press", hypr: { path: ["input", "touchpad", "clickfinger_behavior"], option: "input:touchpad:clickfinger_behavior", readAs: "bool" } }, { key: "touchpadTapAndDrag", type: "bool", def: true, group: "touchpad", label: "Tap and drag", detail: "A tap followed straight away by a tap-and-hold starts a drag, with nothing pressed down", // Hyphens in the option name, underscores in the Lua path -- the same // split tap-to-click documents above, and the only other option in // the touchpad section spelled that way. // // `hyprctl descriptions` contradicts itself here: it reports current // false while `hyprctl getoption` answers bool true with set:false, // meaning nothing has ever written it and it is sitting on // Hyprland's own default of true. getoption is the authority, since // it is what the write path verifies against, so true is what ships // and nothing changes on a machine that has a touchpad. hypr: { path: ["input", "touchpad", "tap_and_drag"], option: "input:touchpad:tap-and-drag", readAs: "bool" } }, // Tuning for the three-finger gestures registered in hypr/input.lua. // The gestures themselves are not settings: Hyprland reads a gesture // registration at config time, so switching one on would need a reload, // and these two are the parts it will accept at runtime. { key: "swipeDistance", type: "int", def: 300, min: 100, max: 800, step: 20, unit: "px", group: "touchpad", label: "Swipe distance", detail: "How far a three-finger swipe must travel to change workspace", hypr: { path: ["gestures", "workspace_swipe_distance"], option: "gestures:workspace_swipe_distance", readAs: "int" } }, { key: "swipeInvert", type: "bool", def: true, group: "touchpad", label: "Natural swipe direction", detail: "Swiping left moves to the workspace on the right, as content follows your fingers", hypr: { path: ["gestures", "workspace_swipe_invert"], option: "gestures:workspace_swipe_invert", readAs: "bool" } }, // ── Multitasking ──────────────────────────────────────────────────── // // GNOME's Multitasking panel, in Hyprland's terms. The Desktop page // described the first two of these as read-only facts ("Layout: // Tiling"), which was never true -- they are ordinary settings that // happened not to have controls. // // Defaults here are Panama's shipped values from hypr/looks.lua, not // Hyprland's own, so restoring defaults returns the desktop to how it // ships rather than to how Hyprland would behave with no config. { key: "windowLayout", type: "enum", def: "dwindle", group: "multitasking", label: "Tiling layout", detail: "Dwindle splits the focused window; master keeps one large window beside a stack", options: [ { value: "dwindle", label: "Dwindle" }, { value: "master", label: "Master and stack" } ], hypr: { path: ["general", "layout"], option: "general:layout", readAs: "str" } }, { key: "preserveSplit", type: "bool", def: true, group: "multitasking", label: "Keep split direction", detail: "New windows follow the split of the window they replace, instead of always halving the longer side", hypr: { path: ["dwindle", "preserve_split"], option: "dwindle:preserve_split", readAs: "bool" } }, { key: "forceSplit", type: "enum", def: 0, group: "multitasking", label: "New windows open", detail: "Where a new window lands relative to the one that was focused", options: [ { value: 0, label: "Where the pointer is" }, { value: 1, label: "Always left or above" }, { value: 2, label: "Always right or below" } ], hypr: { path: ["dwindle", "force_split"], option: "dwindle:force_split", readAs: "int" } }, { key: "windowSnapping", type: "bool", def: true, group: "multitasking", label: "Snap floating windows", detail: "Floating windows stick to screen edges and to each other as you drag them", hypr: { path: ["general", "snap", "enabled"], option: "general:snap:enabled", readAs: "bool" } }, { key: "workspaceBackAndForth", type: "bool", def: false, group: "workspaces", label: "Switch back and forth", detail: "Selecting the workspace you are already on returns you to the previous one", hypr: { path: ["binds", "workspace_back_and_forth"], option: "binds:workspace_back_and_forth", readAs: "bool" } }, { key: "allowWorkspaceCycles", type: "bool", def: false, group: "workspaces", label: "Wrap around at the ends", detail: "Moving past the last workspace continues from the first", hypr: { path: ["binds", "allow_workspace_cycles"], option: "binds:allow_workspace_cycles", readAs: "bool" } }, { key: "focusOnActivate", type: "bool", def: false, group: "workspaces", label: "Let applications take focus", detail: "An application asking for attention is switched to, rather than only highlighted", hypr: { path: ["misc", "focus_on_activate"], option: "misc:focus_on_activate", readAs: "bool" } }, { key: "windowSwallow", type: "bool", def: false, group: "workspaces", label: "Hide the terminal that launched a window", detail: "A terminal disappears while an application started from it is open, and returns when it closes", hypr: { path: ["misc", "enable_swallow"], option: "misc:enable_swallow", readAs: "bool" } }, { key: "mouseMoveFocusesMonitor", type: "bool", def: true, group: "workspaces", label: "Pointer changes active display", detail: "Moving the pointer to another display makes it the active one", hypr: { path: ["misc", "mouse_move_focuses_monitor"], option: "misc:mouse_move_focuses_monitor", readAs: "bool" } }, // ── Accessibility ─────────────────────────────────────────────────── // // Only what Hyprland can actually deliver. GNOME's sticky keys, slow // keys, bounce keys and mouse keys are AccessX, an X11 server feature: // XKB under Wayland has no accessx option group at all (verified // against evdev.lst), and Hyprland does not implement it. The // compositor will happily STORE "accessx:enable" as a keyboard option // and nothing will ever act on it, which is exactly the kind of switch // this app refuses to ship. { key: "magnifierFactor", type: "real", def: 1.0, min: 1.0, max: 5.0, step: 0.1, unit: "×", group: "accessibility", label: "Magnifier", // The readout is "1.00 ×", so the detail says "1.00 ×" too. It used // to say "1.0 is off" beside a slider reading 1.00, and the page // carried a `zeroLabel: "Off"` that could never fire: the minimum // IS 1.0, so the value is never 0 and the zero label was dead copy. // Off is a magnification of one, and that is what both lines say. detail: "Magnifies the screen around the pointer. 1.00 × is off", hypr: { path: ["cursor", "zoom_factor"], option: "cursor:zoom_factor", readAs: "float" } }, { key: "magnifierRigid", type: "bool", def: false, group: "accessibility", label: "Magnifier follows in steps", detail: "Moves the magnified view in increments rather than gliding with the pointer", hypr: { path: ["cursor", "zoom_rigid"], option: "cursor:zoom_rigid", readAs: "bool" } }, { key: "highContrast", type: "bool", def: false, group: "accessibility", label: "High contrast", detail: "Increases contrast in applications that support it. Modern GTK applications read this from the desktop portal and restyle themselves; older ones need a high-contrast theme, which is not installed here.", // No hypr mapping: this is a GNOME interface setting the portal // republishes as org.freedesktop.appearance contrast, which is what // libadwaita actually reads. DesktopStyle applies it. }, { key: "dimInactive", type: "bool", def: false, group: "accessibility", label: "Dim inactive windows", detail: "Darkens every window except the focused one, so the active window is unmistakable", hypr: { path: ["decoration", "dim_inactive"], option: "decoration:dim_inactive", readAs: "bool" } }, { key: "dimStrength", type: "real", def: 0.5, min: 0.05, max: 0.9, step: 0.05, group: "accessibility", label: "Dim amount", detail: "How much darker unfocused windows are", hypr: { path: ["decoration", "dim_strength"], option: "decoration:dim_strength", readAs: "float" } }, { key: "colorFilter", type: "enum", def: "none", group: "accessibility", label: "Color filter", detail: "A whole-screen filter rendered by the compositor — grayscale, or a correction for one kind of color blindness. Costs nothing when off.", // No hypr mapping, deliberately: hyprctl stores decoration:screen_shader // as a shader *path*, not this enum, so a hypr: block would fail the // shape and sweep contracts on read-back. hypr/looks.lua maps the enum // to a shipped shader for reloads; SystemSettings.applyColorFilter does // the same mapping live. options: [ { value: "none", label: "None" }, { value: "grayscale", label: "Grayscale" }, { value: "protanopia", label: "Protanopia" }, { value: "deuteranopia", label: "Deuteranopia" }, { value: "tritanopia", label: "Tritanopia" } ] }, { key: "visualAlerts", type: "bool", def: false, group: "accessibility", label: "Flash the screen for notifications", // No hypr mapping and no gsettings mapping: the flash is drawn by // modules/notifications/VisualBell.qml, one per screen, and fires // on the same notifications the bell would ring for -- except that // it is deliberately NOT gated on the event-sounds switch, since a // visual alert exists for people who cannot hear the bell. detail: "A single flash at the edges of every screen when a notification arrives that would ring the bell" }, // ── Gaming ────────────────────────────────────────────────────────── // What Panama does while a game runs. gamemode tells us when that // starts and stops through its own hook scripts, so these are real // behaviours rather than hints -- and each one is undone afterwards to // whatever it was before, not to a default. { key: "gamingPerformanceProfile", type: "bool", def: true, group: "gaming", label: "Use the performance power profile", detail: "Switches while a game runs and switches back when it exits" }, { key: "gamingNotifyOnStart", type: "bool", def: false, group: "gaming", label: "Say when Game Mode engages", detail: "A notification when a game requests it, which is otherwise invisible" }, // ── Night light ───────────────────────────────────────────────────── { key: "nightLightEnabled", type: "bool", def: false, group: "nightLight", label: "Night Light", detail: "Shift the display warmer to reduce blue light" }, { key: "nightLightAutomatic", type: "bool", def: false, group: "nightLight", label: "Schedule automatically", detail: "Turn Night Light on and off at the scheduled hours" }, { key: "nightLightTemperature", type: "int", def: 3500, min: 2000, max: 6500, step: 100, unit: "K", group: "nightLight", label: "Color temperature", detail: "Lower is warmer" }, // ── Desktop background ────────────────────────────────────────────── // Applied through hyprpaper's IPC. Not an hl.config option, so it has // no `hypr` block; services/Wallpaper.qml owns applying it. // One folder, not four. The picker used to sweep ~/Pictures/Wallpapers, // ~/Pictures/Backgrounds, ~/.local/share/backgrounds and // /usr/share/backgrounds, which meant the distribution's stock // images turned up mixed in with the user's own and there was no // way to say "only mine". Where wallpapers live is a thing somebody // knows about their own machine; it is a setting, not a search. { key: "wallpaperDir", type: "string", def: "Pictures/Wallpapers", group: "wallpaper", pattern: "^~?/?[A-Za-z0-9 ._/+@'-]{1,160}$", label: "Wallpaper folder", detail: "Where the picker looks. Relative to your home folder unless it starts with /" }, { key: "wallpaperPath", type: "string", def: "", group: "wallpaper", // Reaches hyprpaper as the "," argument form, so a // comma would split it into a different request. Absolute paths // only, no commas, no newlines. pattern: "^(|/[^,\\n]+)$", label: "Wallpaper", detail: "Shown on every output" }, { key: "wallpaperMode", type: "enum", def: "single", group: "wallpaper", label: "Wallpaper mode", detail: "Use one image, rotate a collection, or choose per display", options: [ { value: "single", label: "Single" }, { value: "slideshow", label: "Slideshow" }, { value: "per-monitor", label: "Per display" } ] }, { key: "wallpaperSlideshowPaths", type: "json", def: ([]), group: "wallpaper", internal: true, label: "Slideshow collection", detail: "Backgrounds selected for rotation" }, { key: "wallpaperIntervalMinutes", type: "int", def: 30, min: 5, max: 1440, step: 5, unit: "min", group: "wallpaper", label: "Change background every", detail: "Time between slideshow images" }, { key: "wallpaperShuffle", type: "bool", def: true, group: "wallpaper", label: "Shuffle", detail: "Show every selected image before repeating" }, { key: "wallpaperPerMonitor", type: "json", def: ({}), group: "wallpaper", internal: true, label: "Per-display backgrounds", detail: "Background assigned to each connected display" }, // Video wallpapers play through mpvpaper (services/VideoWallpaper.qml); // the active video rides wallpaperPath like any still, routed by // extension. These two only shape discovery and the battery policy. { key: "videoWallpaperDir", type: "string", def: "Videos/Wallpapers", group: "wallpaper", pattern: "^~?/?[A-Za-z0-9 ._/+@'-]{1,160}$", label: "Video wallpaper folder", detail: "Where the picker looks for videos. Relative to your home folder unless it starts with /" }, // Its own key, not wallpaperPath: the still pipeline persists its // policy transactionally and once clobbered a stored video path. // Two owners, two keys. { key: "videoWallpaperPath", type: "string", def: "", group: "wallpaper", internal: true, pattern: "^(|/[^,\n]+)$", label: "Video wallpaper", detail: "The video playing as the desktop background" }, { key: "videoWallpaperPauseOnBattery", type: "bool", def: true, group: "wallpaper", label: "Pause video wallpaper on battery", detail: "Freezes on the current frame and resumes on wall power" }, // ── Lock-screen appearance ───────────────────────────────────────── // scripts/panama-lock validates these again before generating a state // config. The tracked hyprlock.conf remains the safe fallback. { key: "lockBackgroundMode", type: "enum", def: "screenshot", group: "lockAppearance", label: "Background", detail: "What appears behind the lock screen", options: [ { value: "screenshot", label: "Blurred desktop" }, { value: "wallpaper", label: "Current wallpaper" }, { value: "solid", label: "Solid color" } ] }, { key: "lockBlurLevel", type: "int", def: 3, min: 0, max: 5, step: 1, group: "lockAppearance", label: "Background blur", detail: "Softens what is behind the password field" }, { key: "lockShowClock", type: "bool", def: true, group: "lockAppearance", label: "Show clock", detail: "Use the desktop's 12 or 24-hour format" }, { key: "lockShowDate", type: "bool", def: true, group: "lockAppearance", label: "Show date", detail: "Show the weekday and full date" }, { key: "lockShowUser", type: "bool", def: true, group: "lockAppearance", label: "Show user name", detail: "Identify the signed-in account" }, { key: "lockFadeOnEmpty", type: "bool", def: false, group: "lockAppearance", label: "Hide password field until typing", detail: "Keep the empty field out of the way" }, // ── Idle, lock, and sleep ─────────────────────────────────────────── // Written into a generated hypridle config; see scripts/panama-idle. // Zero means never for all three. { key: "screenBlankMinutes", type: "int", def: 5, min: 0, max: 120, step: 1, unit: "min", group: "idle", label: "Turn the screen off after", detail: "Blanks the display; nothing is locked yet" }, { key: "lockMinutes", type: "int", def: 10, min: 0, max: 240, step: 1, unit: "min", group: "idle", label: "Lock the screen after", detail: "Counted from when the session went idle, not from blanking" }, { key: "suspendMinutes", type: "int", def: 0, min: 0, max: 480, step: 5, unit: "min", group: "idle", label: "Suspend after", detail: "This is a desktop, so Panama ships with automatic suspend off" }, { key: "lockOnSleep", type: "bool", def: true, group: "idle", label: "Lock before sleeping", detail: "Requires your password when the machine wakes" }, // The same three timings again, for when the machine is running on // its own battery. hypridle has no concept of a power source, so // there is one generated config and panama-idle rebuilds it from // whichever set applies when the charger comes or goes. // // Shorter defaults, because the cost of an idle screen differs: on // wall power it is a screen, on battery it is the rest of your // afternoon. A machine with no battery never reads these at all. { key: "screenBlankMinutesBattery", type: "int", def: 2, min: 0, max: 120, step: 1, unit: "min", group: "idleBattery", label: "Turn the screen off after", detail: "On battery. Blanks the display; nothing is locked yet" }, { key: "lockMinutesBattery", type: "int", def: 5, min: 0, max: 240, step: 1, unit: "min", group: "idleBattery", label: "Lock after", detail: "On battery. Requires your password to get back in" }, { key: "suspendMinutesBattery", type: "int", def: 20, min: 0, max: 480, step: 5, unit: "min", group: "idleBattery", label: "Suspend after", detail: "On battery, sleeping is what makes the charge last" }, // ── Power button ──────────────────────────────────────────────────── // logind is told to ignore the power key -- config/copy ships the // drop-in -- so what a press does is the compositor's decision rather // than the system's, and changing it needs no root. // // No `hypr` block: this is not a compositor option, it is read by // config/dot/hypr/keybinds.lua the way the workspace rules are. The // bind evaluates it AT PRESS TIME rather than at config time, so a // change here applies to the very next press and no reload is needed. { key: "powerButtonAction", type: "enum", def: "menu", group: "power", label: "Pressing the power button", detail: "The system ignores the key; Panama decides — so a bumped button never yanks the plug", options: [ { value: "menu", label: "Shows the power menu" }, { value: "suspend", label: "Suspends" }, { value: "poweroff", label: "Powers off (two-press)" }, { value: "nothing", label: "Does nothing" } ] }, // ── Night light schedule ──────────────────────────────────────────── // Hours as decimals, so 17.5 is half past five. Wrapping past midnight // is normal here and is what the shipped values do: on at 17:00, off at // 10:00 the following morning. { key: "nightLightFrom", type: "real", def: 17.0, min: 0, max: 23.5, step: 0.5, group: "nightLight", label: "Turns on at", detail: "Only used when Night Light follows a schedule" }, { key: "nightLightTo", type: "real", def: 10.0, min: 0, max: 23.5, step: 0.5, group: "nightLight", label: "Turns off at", detail: "A time earlier than the start simply means the next morning" }, // ── Color scheme ─────────────────────────────────────────────────── // Light is Tokyo Night Day, the official light variant, rather than a // palette invented to merely not be dark. Both share the same hues at // different lightness, which is what keeps the Prism identity intact // across the switch. // // services/ColorScheme.qml pushes the choice to GTK and to the // compositor's border colors, because an application toolbar or a // window border still wearing the other scheme is more jarring than // either scheme on its own. { key: "colorScheme", type: "enum", def: "dark", group: "appearance", label: "Appearance", detail: "Light and dark share one identity, not two themes", options: [ { value: "dark", label: "Dark" }, { value: "light", label: "Light" } ] }, { key: "accentName", type: "enum", def: "blue", group: "appearance", label: "Accent color", detail: "Drives the focused window border, the bar hairline, and every active state", // NAMED accents, not a free color. Each name carries a curated // pair per scheme, because one hex cannot serve both: a color // legible on the dark ground is usually illegible on the light one. // The palette and its measured contrast live in config/Theme.qml, // which is also what stops this list drifting from what is drawn. options: [ { value: "blue", label: "Prism blue" }, { value: "orchid", label: "Orchid" }, { value: "teal", label: "Teal" }, { value: "green", label: "Green" }, { value: "amber", label: "Amber" }, { value: "orange", label: "Orange" }, { value: "rose", label: "Rose" }, { value: "slate", label: "Slate" } ] }, { key: "themeProfileId", type: "string", def: "moon", group: "appearance", pattern: "^[a-z0-9][a-z0-9-]{0,63}$", label: "Selected theme profile", detail: "The shipped or saved theme currently applied to the desktop", internal: true }, { key: "themeProfiles", type: "json", def: [], group: "appearance", label: "Saved theme profiles", detail: "Named custom colour schemes and accent pairs", internal: true }, // The remembered theme per scheme: flipping light/dark lands on the // theme you last chose for that side, never a forced default. { key: "themeDark", type: "string", def: "moon", group: "appearance", pattern: "^[a-z0-9][a-z0-9-]{0,63}$", label: "Dark theme", detail: "The theme applied while the desktop is dark", internal: true }, { key: "themeLight", type: "string", def: "day", group: "appearance", pattern: "^[a-z0-9][a-z0-9-]{0,63}$", label: "Light theme", detail: "The theme applied while the desktop is light", internal: true }, // ── Application themes ───────────────────────────────────────────── // ColorScheme owns GTK's light/dark theme. These are the two theme // choices GNOME applications expose independently of that palette: // their icons and pointer. DesktopStyle only accepts names found in // the read-only XDG catalog before storing them. { key: "cursorTheme", type: "string", def: "oreo_blue_cursors", group: "themes", pattern: "^[A-Za-z0-9 ._+@'-]{1,96}$", label: "Pointer theme", detail: "The pointer design used by applications and Hyprland" }, { key: "iconTheme", type: "string", def: "Adwaita", group: "themes", pattern: "^[A-Za-z0-9 ._+@'-]{1,96}$", label: "Application icons", detail: "The icon set used by GTK applications" }, // ── Typography ────────────────────────────────────────────────────── // The single largest thing in this desktop that used to be changeable // only by editing Theme.qml. // // interfaceFont is every piece of text a person reads. iconFont is used // ONLY to draw glyphs -- workspace pills, the status cluster, search // icons -- so it must carry Nerd Font glyphs; a plain monospace family // there replaces every icon in the shell with tofu, which is why the // picker offers them as a separate, filtered list. { key: "interfaceFont", type: "string", def: "Adwaita Sans", group: "typography", // A family name, matched by fontconfig. Constrained because it also // reaches hl.config as a string in hypr/looks.lua. pattern: "^[A-Za-z0-9 ._-]{1,64}$", label: "Interface font", detail: "Used for every piece of text in the shell" }, { key: "iconFont", type: "string", def: "VictorMono Nerd Font", group: "typography", pattern: "^[A-Za-z0-9 ._-]{1,64}$", label: "Icon font", detail: "Draws the shell's glyphs, so it must be a Nerd Font" }, { key: "interfaceFontSize", type: "int", def: 13, min: 10, max: 18, step: 1, unit: "px", group: "typography", label: "Interface text size", detail: "The base size the rest of the shell's type scales from" }, { key: "applicationFont", type: "string", def: "Adwaita Sans", group: "typography", pattern: "^[A-Za-z0-9 ._+@'-]{1,96}$", label: "Application font", detail: "Used by menus, controls, and labels in applications" }, { key: "applicationFontSize", type: "int", def: 11, min: 6, max: 32, step: 1, unit: "pt", group: "typography", label: "Application text size", detail: "The base text size used by applications" }, { key: "documentFont", type: "string", def: "Adwaita Sans", group: "typography", pattern: "^[A-Za-z0-9 ._+@'-]{1,96}$", label: "Document font", detail: "Used for document content when an application follows the system choice" }, { key: "documentFontSize", type: "int", def: 12, min: 6, max: 32, step: 1, unit: "pt", group: "typography", label: "Document text size", detail: "The default text size for document content" }, { key: "monospaceFont", type: "string", def: "VictorMono Nerd Font", group: "typography", pattern: "^[A-Za-z0-9 ._+@'-]{1,96}$", label: "Monospace font", detail: "Used by terminals, editors, and code fields that follow the system choice" }, { key: "monospaceFontSize", type: "int", def: 10, min: 6, max: 32, step: 1, unit: "pt", group: "typography", label: "Monospace text size", detail: "The default text size for terminals and code" }, { key: "fontHinting", type: "enum", def: "slight", group: "typography", label: "Font hinting", detail: "How strongly text aligns to the pixel grid", options: [ { value: "none", label: "None" }, { value: "slight", label: "Slight" }, { value: "medium", label: "Medium" }, { value: "full", label: "Full" } ] }, { key: "fontAntialiasing", type: "enum", def: "rgba", group: "typography", label: "Text smoothing", detail: "How application text softens its edges", options: [ { value: "none", label: "None" }, { value: "grayscale", label: "Grayscale" }, { value: "rgba", label: "Subpixel" } ] }, // ── Application titlebars ────────────────────────────────────────── // These affect applications that honor GNOME's window preferences. // Hyprland itself has no server-side titlebar buttons, so minimize is // deliberately absent rather than presented as a switch that lies. { key: "titlebarButtonSide", type: "enum", def: "right", group: "titlebar", label: "Button side", detail: "Place application titlebar buttons on the left or right", options: [ { value: "left", label: "Left" }, { value: "right", label: "Right" } ] }, // Panama's own windows (Settings) draw their own titlebar and follow // the same rules as GNOME apps. Off is pure Hyprland: Super+Q closes, // Super+drag moves, Escape still works. { key: "panamaTitlebar", type: "bool", def: true, group: "titlebar", label: "Titlebar on Panama windows", detail: "Hide it and the window is pure Hyprland — Super+Q closes, Super+drag moves" }, // titlebarMaximizeButton and titlebarDoubleClick used to live here. // Removed on purpose: Hyprland has no minimize, maximize is noise in a // tiler, and DesktopStyle now pushes a close-only button-layout and // leaves GNOME's double-click default alone. // ── Accessibility ─────────────────────────────────────────────────── // Backed by gsettings so GTK applications agree with the shell, and // pushed to the compositor as well where it has its own notion. { key: "cursorSize", type: "int", def: 24, min: 16, max: 64, step: 4, unit: "px", group: "accessibility", label: "Pointer size", detail: "Applies to the compositor and to applications" }, { key: "textScale", type: "real", def: 1.0, min: 0.75, max: 2.0, step: 0.05, group: "accessibility", label: "Text size", detail: "Scales interface text everywhere; 1.00 is the design size" }, // ── Weather ───────────────────────────────────────────────────────── { key: "temperatureUnit", type: "enum", def: "fahrenheit", group: "weather", label: "Temperature unit", detail: "Choose Fahrenheit or Celsius for the weather card", options: [ { value: "fahrenheit", label: "Fahrenheit" }, { value: "celsius", label: "Celsius" } ] }, { key: "weatherRefreshMinutes", type: "int", def: 20, min: 5, max: 120, step: 5, unit: "min", group: "weather", label: "Weather refresh", detail: "How often Panama updates the current conditions" }, // ── Which GPU the vitals readout tracks ───────────────────────────── // A sysfs path rather than a card number, because the number is neither // stable across machines nor meaningful. Constrained to the one shape // that can be read for utilization; VitalsWidget hides itself when the // path is unreadable, so a stale value degrades to no readout rather // than a wrong one. { key: "gpuBusyPath", type: "string", def: "/sys/class/drm/card1/device/gpu_busy_percent", group: "vitals", internal: true, pattern: "^/sys/class/drm/card[0-9]+/device/gpu_busy_percent$", label: "Graphics device", detail: "Which GPU the graphics readout in the bar measures" }, // ── Search ────────────────────────────────────────────────────────── // The launcher's web search appends the query to this. It shipped // pointing at the author's personal bang redirector once; a stranger's // searches belong to no one's server but the engine they chose. { key: "webSearchUrl", type: "string", def: "https://duckduckgo.com/?q=", group: "search", pattern: "^https://[^\\s]{1,200}$", label: "Web search engine", detail: "Where the launcher's web search sends a query; the search text is appended" }, // ── Weather location ──────────────────────────────────────────────── // Coordinates rather than a place name, because that is what Open-Meteo // takes and it needs no API key. weatherLocation is only the label shown // in the UI; it is never sent anywhere, so it can say whatever makes the // reading recognizable. { key: "weatherLatitude", type: "real", def: 0, min: -90, max: 90, step: 0.0001, group: "weather", internal: true, label: "Latitude", detail: "Set by choosing a location" }, { key: "weatherLongitude", type: "real", def: 0, min: -180, max: 180, step: 0.0001, group: "weather", internal: true, label: "Longitude", detail: "Set by choosing a location" }, // Empty until a location is chosen. The shipped value was once the // author's home town, which confidently reported his weather on every // machine anywhere; an unset location fetches nothing and says so // instead. { key: "weatherLocation", type: "string", def: "", group: "weather", internal: true, // Display only -- never sent to the weather service. pattern: "^[^\\n]{0,64}$", label: "Weather location", detail: "The place the weather reading is for" }, // ── Vitals refresh ────────────────────────────────────────────────── { key: "vitalsIntervalMs", type: "int", def: 2000, min: 500, max: 10000, step: 500, unit: "ms", group: "vitals", label: "Vitals refresh", detail: "How often processor, memory, and graphics usage update" }, // ── Notifications ─────────────────────────────────────────────────── { key: "notificationTimeoutMs", type: "int", def: 5000, min: 1000, max: 30000, step: 500, unit: "ms", group: "notifications", label: "Notification duration", detail: "How long ordinary notification banners remain visible" }, { key: "notificationTimeoutCriticalMs", type: "int", def: 0, min: 0, max: 60000, step: 1000, unit: "ms", group: "notifications", label: "Critical notification duration", detail: "Zero keeps critical notification banners visible until dismissed" }, { key: "notificationHistoryLimit", type: "int", def: 100, min: 10, max: 500, step: 10, group: "notifications", label: "Notification history", detail: "Maximum notifications retained in the notification center" }, { key: "maxVisibleToasts", type: "int", def: 4, min: 1, max: 8, step: 1, group: "notifications", label: "Visible banners", detail: "Maximum notification banners shown at once" }, // The one hole anyone is allowed to punch in Do Not Disturb. Off by // default, because Do Not Disturb that lets something through anyway is // not the thing most people asked for -- and "critical" is the sender's // word, not yours, so an application that calls everything critical // would otherwise defeat the switch on its own say-so. Turned on, it // shows a banner for critical notifications while a mode or a manual Do // Not Disturb is silencing everything else; the per-application urgency // override is how you decide which senders get to claim it. { key: "criticalBreaksThrough", type: "bool", def: false, group: "notifications", label: "Critical alerts break through", detail: "Show critical notifications as banners even while Do Not Disturb is on" }, // ── Sound ─────────────────────────────────────────────────────────── // The two audio preferences that are Panama's own. Everything else on // the Sound page is live PipeWire or a GNOME desktop key, and belongs // to the system rather than to this file. // // Both are read by scripts/panama-osd as well as by the shell, so the // volume keys behave the same whether the panel is open or not. { key: "overAmplification", type: "bool", def: false, group: "sound", label: "Over-amplification", detail: "Lets the volume slider go to 150% — louder, at the cost of distortion on some hardware" }, { key: "volumeChangeBlip", type: "bool", def: true, group: "sound", label: "Volume-change blip", detail: "A short click each time the volume keys move the output level" }, // ── Capture ───────────────────────────────────────────────────────── // Directories and encoder arguments are enums rather than free text: // both are handed to a recorder process, and an arbitrary string there // is a much larger surface than a settings page needs to expose. // Free text rather than three choices. The three were a guess at // where somebody keeps screenshots, and a guess cannot include the // folder the rest of their software already writes to -- which is // the only folder that matters. A path starting with / is taken as // absolute, so a drive that is not under home works too. { key: "screenshotDir", type: "string", def: "Pictures/Screenshots", group: "capture", pattern: "^~?/?[A-Za-z0-9 ._/+@'-]{1,160}$", label: "Screenshot folder", detail: "Where screenshots are saved. Relative to your home folder unless it starts with /" }, { key: "recordingDir", type: "string", def: "Videos/Screencasts", group: "capture", pattern: "^~?/?[A-Za-z0-9 ._/+@'-]{1,160}$", label: "Recording folder", detail: "Where screen recordings are saved. Relative to your home folder unless it starts with /" }, // What a recording hears. Off by default, which is GNOME's default // too -- a screencast that silently captured the microphone would be // a privacy incident, not a feature. The @DEFAULT_*@ tokens are // PulseAudio's own always-current aliases, so the recording follows // the device Sound settings has selected rather than naming one. { key: "recorderAudio", type: "enum", def: "none", group: "capture", label: "Recording audio", detail: "What screen recordings capture alongside the video", options: [ { value: "none", label: "No audio" }, { value: "system", label: "System audio" }, { value: "microphone", label: "Microphone" } ] }, // "auto" stands in for the render node until record time: // /dev/dri/renderD128 was baked into every option once, which is one // machine's enumeration and frequently the wrong node on hybrid // graphics. Capture.qml resolves it when recording starts. { key: "recorderArgs", type: "enum", def: "-c h264_vaapi -d auto", group: "capture", label: "Recording encoder", detail: "Hardware encoding keeps recording off the processor while gaming", options: [ { value: "-c h264_vaapi -d auto", label: "VAAPI H.264" }, { value: "-c hevc_vaapi -d auto", label: "VAAPI HEVC" }, { value: "-c libx264", label: "CPU x264" } ] }, // ── Dock contents ─────────────────────────────────────────────────── // A "json" value: the ordered list of desktop entry ids pinned to the // dock. Kept in the shared store so that reordering the dock is covered // by Restore defaults like everything else, rather than living in its // own file. The shipped order is the GNOME dash it replaced, with one // deliberate substitution: Panama Settings takes the first slot rather // than GNOME Settings. Panama now covers what GNOME Settings did for // this desktop and delegates the remainder to it by name, so pinning // the thing it delegates TO put the fallback in front of the real one. // GNOME Settings stays installed and searchable in the launcher. { key: "dockPinned", type: "json", group: "dock", label: "Pinned applications", detail: "Applications that stay in the Dock whether or not they are running", def: [ "panama-settings", "kitty", "org.gnome.Nautilus", "com.bitwarden.desktop", "org.gnome.Software", "helium", "org.mozilla.thunderbird_esr", "com.slack.Slack", "app.bluebubbles.BlueBubbles", "rustdesk", "io.podman_desktop.PodmanDesktop", "com.anthropic.Claude", "chatgpt", "md.obsidian.Obsidian", "com.obsproject.Studio", "steam" ] }, // ── Keyboard shortcut overrides ───────────────────────────────────── // { "": "" }. Only the chord is stored: the // action always comes from hypr/keybinds.lua, so an override can move a // shortcut but can never make one do something else. The Lua validates // each chord and falls back to the shipped one, so a hand-edited file // cannot cost you a keymap. // // Edited through the Keyboard page (under Input) rather than as a row, hence // internal. { key: "keybindOverrides", type: "json", def: ({}), group: "input", internal: true, label: "Keyboard shortcut overrides", detail: "Shortcuts you have moved from their shipped chord" }, // ── Custom shortcuts ──────────────────────────────────────────────── // [ { chord, kind, target, label } ]. Data, never code: `kind` is one of // app | shell | window, `target` is a validated id resolved through the // whitelist tables in hypr/actions.lua, and an entry that fails any check // is silently not emitted. This is what keeps a user-editable file from // being executable even though it now describes shortcuts the user // invented. Edited through the Keyboard page, hence internal. { key: "customBinds", type: "json", def: [], group: "input", internal: true, label: "Custom shortcuts", detail: "Shortcuts you invented: each one launches an application, triggers a shell action, or moves a window" }, // ── Per-application window rules ──────────────────────────────────── // [ { class, label, float, center, size, workspace, noAnim, game, // noDim, pin } ]. `class` is matched literally (hypr/rules.lua escapes // it before Hyprland's RE2 sees it); `size` is [w, h] or null; // `workspace` is 1..10 or null; everything else is a boolean. Rules // matching the shell's own surfaces are refused at both ends. Edited // through the Windows page, hence internal. { key: "windowRules", type: "json", def: [], group: "multitasking", internal: true, label: "Application window rules", detail: "How specific applications behave when they open: floating, size, workspace, animations" }, // ── Four-finger gestures ──────────────────────────────────────────── // Each holds {} (unassigned) or a named action { kind, target, label }, // the same shape customBinds stores and the same whitelists resolve. // Registered at compositor config time, so assigning one reloads. { key: "gestureFourUp", type: "json", def: ({}), group: "touchpad", internal: true, label: "Four-finger swipe up", detail: "What a four-finger upward swipe does" }, { key: "gestureFourDown", type: "json", def: ({}), group: "touchpad", internal: true, label: "Four-finger swipe down", detail: "What a four-finger downward swipe does" }, { key: "gestureFourLeft", type: "json", def: ({}), group: "touchpad", internal: true, label: "Four-finger swipe left", detail: "What a four-finger leftward swipe does" }, { key: "gestureFourRight", type: "json", def: ({}), group: "touchpad", internal: true, label: "Four-finger swipe right", detail: "What a four-finger rightward swipe does" }, // ── Display configuration ─────────────────────────────────────────── // { "": { mode, scale, transform, x, y, primary, vrrMode, // colorProfile, bitdepth, sdrBrightness, sdrSaturation, mirrorOf } }, // applied by hypr/monitors.lua on top of the shipped values. Everything // past `primary` is optional, so records written before those fields // existed still load and the shipped defaults stand for them. { key: "displays", type: "json", def: ({}), group: "display", internal: true, label: "Display configuration", detail: "Resolution, scale, rotation, position, primary display, color, VRR override, and mirroring" }, // ── Per-application notification rules ────────────────────────────── // { "": { enabled, sound, display, urgency, lastSeenMs, name, // icon } -- every field past `enabled` is optional, so rules written // when this held only `enabled` still load. Lock-screen fields from // older rules are dropped at normalization; hyprlock cannot render // notifications. // // `name` and `icon` are a cache for the settings list, not the source: // resolution stays live-first through DesktopEntries, and these only // stand in for an application that is not installed (or not scanned // yet). `lastSeenMs` is stamped on every notification and is what puts // an application in the "Recent" section. // // Absent means "no rule", which is not the same as a rule that allows // everything: a new application must be able to notify without needing // an entry written for it first. services/Notifs.qml treats a missing // entry as permissive and Do Not Disturb remains an override on top, // rather than being duplicated per application. { key: "notificationAppRules", type: "json", def: ({}), group: "notifications", internal: true, label: "Application notification rules", detail: "Per-application notification sound, banner, and urgency preferences" }, // ── Internal ──────────────────────────────────────────────────────── { key: "welcomeSeen", type: "bool", def: false, group: "internal", internal: true, label: "Welcome shown", detail: "Set once the first-run welcome has been dismissed. Restoring defaults shows it again, which is intended: a reset machine is one somebody wants introduced to them" }, { key: "lastPage", type: "string", def: "home", group: "internal", internal: true, label: "Last settings page", detail: "Restores the page Settings was left on" } ] // key -> entry, built once. Every lookup below goes through this rather than // scanning `entries`, since get/set are called from bindings. readonly property var byKey: { const index = {}; for (const entry of root.entries) index[entry.key] = entry; return index; } readonly property var userKeys: root.entries.filter(entry => !entry.internal).map(entry => entry.key) function spec(key: string): var { return root.byKey[key] ?? null; } function has(key: string): bool { return root.byKey[key] !== undefined; } function defaultFor(key: string): var { const entry = root.byKey[key]; return entry ? entry.def : undefined; } function defaults(): var { const out = {}; for (const entry of root.entries) out[entry.key] = entry.def; return out; } function inGroup(group: string): var { return root.entries.filter(entry => entry.group === group && !entry.internal); } // Entries the compositor owns, used to build one hl.config{} payload. function hyprEntries(): var { return root.entries.filter(entry => entry.hypr !== undefined); } // Returns the value coerced into the entry's type and range, or `undefined` // if it cannot be represented at all. Out-of-range numbers are clamped // rather than rejected: a stale file with a since-narrowed bound should // still yield a usable desktop. function coerce(key: string, value: var): var { const entry = root.byKey[key]; if (!entry || value === undefined || value === null) return undefined; switch (entry.type) { case "bool": if (typeof value === "boolean") return value; if (value === "true") return true; if (value === "false") return false; return undefined; case "int": case "real": { const numeric = Number(value); if (!isFinite(numeric)) return undefined; const rounded = entry.type === "int" ? Math.round(numeric) : numeric; const lower = entry.min !== undefined ? Math.max(rounded, entry.min) : rounded; return entry.max !== undefined ? Math.min(lower, entry.max) : lower; } case "enum": return entry.options.some(option => option.value === value) ? value : undefined; case "json": // Accepted as-is. Anything JSON.parse produced is representable, // and per-field meaning belongs to the owning service rather than // here. A scalar is rejected so a corrupt file falls back to the // default instead of handing a list-shaped consumer a number. return (typeof value === "object") ? value : undefined; case "string": { const text = typeof value === "string" ? value : String(value); // A constrained string is rejected rather than sanitised. Several // of these are serialized into an hl.config payload, and quietly // stripping characters would turn a typo into a different setting // instead of an error the user can see. if (entry.pattern && !new RegExp(entry.pattern).test(text)) return undefined; return text; } } return undefined; } }