The Dock's pinned applications were a sixteen-entry literal in Settings.qml, so changing what sits in the Dock meant editing QML. They are now an ordered list in the shared store, with move up, move down, unpin, and a filtered picker for adding installed applications. Keeping them in the shared store rather than a file of their own means they are covered by Restore defaults like everything else. This needed a "json" schema type for values the schema stores and resets but does not validate field by field. It exists so structured settings can live in the one file rather than growing a fourth preference store; the owning service validates the contents. Snapshots make the settings app safe to experiment with. The whole configuration is one file, so a backup is a copy and a restore is an overwrite, and restoring snapshots what it replaces so it is itself undoable. A snapshot is validated as JSON before it can be restored over a working configuration, and a name that is not a plain snapshot filename from the backup directory is refused. Snapshot names carry milliseconds. At one-second resolution a save followed promptly by a restore produced the same filename twice, and the restore's own safety snapshot overwrote the file it was about to read -- found by the contract, which restores immediately after saving. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
593 lines
29 KiB
QML
593 lines
29 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 ───────────────────────────────────────────────────────────
|
|
{
|
|
key: "use24Hour", type: "bool", def: false, group: "clock",
|
|
label: "24-hour time",
|
|
detail: "Use 18:30 instead of 6:30 PM"
|
|
},
|
|
{
|
|
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"
|
|
},
|
|
|
|
// ── 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: "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 ───────────────────────────────────────────────────────────
|
|
{
|
|
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.
|
|
{
|
|
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: "Content-aware matches the display to what is on screen",
|
|
options: [
|
|
{ value: 0, label: "Off" },
|
|
{ value: 3, label: "Content-aware" }
|
|
],
|
|
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" },
|
|
{ value: 2, label: "Automatic" }
|
|
],
|
|
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 neighbouring 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" }
|
|
},
|
|
|
|
// ── 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: "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: "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: "input",
|
|
label: "Focus follows pointer",
|
|
detail: "Click to focus matches GNOME; sloppy focus follows the pointer",
|
|
options: [
|
|
{ value: 0, label: "Never" },
|
|
{ value: 1, label: "Click to focus" },
|
|
{ value: 2, label: "Sloppy focus" }
|
|
],
|
|
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: "input",
|
|
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: "input",
|
|
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" }
|
|
},
|
|
|
|
// ── 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.
|
|
{
|
|
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"
|
|
},
|
|
|
|
// ── 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"
|
|
},
|
|
|
|
// ── 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"
|
|
},
|
|
|
|
// ── 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"
|
|
},
|
|
|
|
// ── 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.
|
|
{
|
|
key: "screenshotDir", type: "enum", def: "Pictures/Screenshots", group: "capture",
|
|
label: "Screenshot folder",
|
|
detail: "Folder under your home directory for screenshots",
|
|
options: [
|
|
{ value: "Pictures/Screenshots", label: "Pictures / Screenshots" },
|
|
{ value: "Pictures", label: "Pictures" },
|
|
{ value: "Desktop", label: "Desktop" }
|
|
]
|
|
},
|
|
{
|
|
key: "recordingDir", type: "enum", def: "Videos/Recordings", group: "capture",
|
|
label: "Recording folder",
|
|
detail: "Folder under your home directory for screen recordings",
|
|
options: [
|
|
{ value: "Videos/Recordings", label: "Videos / Recordings" },
|
|
{ value: "Videos", label: "Videos" },
|
|
{ value: "Desktop", label: "Desktop" }
|
|
]
|
|
},
|
|
{
|
|
key: "recorderArgs", type: "enum", def: "-c h264_vaapi -d /dev/dri/renderD128",
|
|
group: "capture",
|
|
label: "Recording encoder",
|
|
detail: "Hardware encoding keeps recording off the processor while gaming",
|
|
options: [
|
|
{ value: "-c h264_vaapi -d /dev/dri/renderD128", label: "VAAPI H.264" },
|
|
{ value: "-c hevc_vaapi -d /dev/dri/renderD128", 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.
|
|
{
|
|
key: "dockPinned", type: "json", group: "dock",
|
|
label: "Pinned applications",
|
|
detail: "Applications that stay in the Dock whether or not they are running",
|
|
def: [
|
|
"org.gnome.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", "claude-desktop",
|
|
"codex-desktop", "md.obsidian.Obsidian",
|
|
"com.obsproject.Studio", "steam"
|
|
]
|
|
},
|
|
|
|
// ── Internal ────────────────────────────────────────────────────────
|
|
{
|
|
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 serialised 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;
|
|
}
|
|
}
|