2028 lines
107 KiB
QML
2028 lines
107 KiB
QML
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 "<output>,<path>" 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",
|
||
"codex-desktop", "md.obsidian.Obsidian",
|
||
"com.obsproject.Studio", "steam"
|
||
]
|
||
},
|
||
|
||
// ── Keyboard shortcut overrides ─────────────────────────────────────
|
||
// { "<bind description>": "<chord>" }. 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 ───────────────────────────────────────────
|
||
// { "<output>": { 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 ──────────────────────────────
|
||
// { "<appId>": { 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;
|
||
}
|
||
}
|