1740 lines
90 KiB
QML
1740 lines
90 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: "Claude usage",
|
|
detail: "Show how much of the Claude subscription has been used, beside the other vitals"
|
|
},
|
|
|
|
// ── 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: []
|
|
}
|
|
]
|
|
},
|
|
// Grouped with the workspaces rather than with focus, because a group is
|
|
// where a setting is found rather than what it is about: the slider that
|
|
// sets this is a schema-bound row on WorkspacesPage. The focus group
|
|
// routes to Notifications, which is where focusModes renders.
|
|
{
|
|
key: "focusDurationMinutes", type: "int", def: 45, min: 5, max: 180, step: 5,
|
|
unit: "min",
|
|
group: "workspaces",
|
|
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" }
|
|
},
|
|
|
|
// ── 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" }
|
|
},
|
|
|
|
// 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,
|
|
group: "accessibility",
|
|
label: "Magnifier",
|
|
detail: "Magnifies the screen around the pointer. 1.0 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" }
|
|
},
|
|
|
|
// ── 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"
|
|
},
|
|
|
|
// ── 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"
|
|
},
|
|
|
|
// ── 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"
|
|
},
|
|
|
|
// ── 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 } } -- lock-screen fields from older rules are
|
|
// dropped at normalization; hyprlock cannot render notifications.
|
|
//
|
|
// 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 and lock-screen visibility 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;
|
|
}
|
|
}
|