The audit's first tier, in one change: every case found where the interface
asserted something the system did not do.
Twenty-one compositor-owned preferences -- the whole Mouse & Touchpad page,
plus window layout, snapping, dim-inactive and the magnifier -- had the live
half (hyprctl eval) and not the config-time half, so they quietly reverted on
every hyprctl reload. All 70 hypr-backed keys now have a prefs.get() in the
Lua, and hypr-prefs-contract pins both the presence and that the Lua fallback
equals the schema default, which is how the touchpad page misreported natural
scrolling on first boot.
The idle generator fell back from unwritten battery keys to the AC values
while the Power page displayed the schema defaults: a fresh laptop showed
"suspend at 20 minutes" and generated no suspend listener, then discharged to
zero in a bag. Unwritten keys now use the defaults the page shows
(idle-defaults-contract pins generator to schema; idle-config-contract
re-pinned to the new rule with the tradeoff recorded), and change-settings
enables managed idle on any machine with a battery -- without starting
hypridle in whatever session the installer runs under.
The per-app lock-screen notification switches wrote fields nothing read:
hyprlock cannot render notifications. Removed, with the rule model shrunk to
{enabled}, stale stored fields dropped at normalization, and the contract now
forbidding the page from growing lock-screen switches it cannot honor.
The battery warning thresholds were searchable, documented as "Found on
Power & Lock", and rendered nowhere -- and crossing the low threshold changed
only a glyph's color. Both sliders now exist where search was already sending
people, and low battery publishes a real notification at important priority.
Three handoffs opened GNOME panels that are inert in a Hyprland session. The
keyboard handoff is gone (that panel writes gsettings nothing here reads, and
the working controls sat on the same page); Connectivity gains a Wi-Fi row
that opens GNOME's actual Wi-Fi panel -- hidden SSIDs and 802.1X finally have
a road -- beside the network row that legitimately drives NetworkManager; the
universal-access handoff is gone, its few working toggles being controls this
app already owns. And the accessibility page now gives the true reason sticky
keys are missing: each Wayland compositor implements its own and Hyprland
does not yet -- not "an X11 feature with no Wayland equivalent," which sent
people to the wrong conclusion about the platform.
Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
310 lines
15 KiB
Lua
310 lines
15 KiB
Lua
-- ─────────────────────────────────────────────────────────────────────────────
|
|
-- Look and feel -- Tokyo Night Moon
|
|
--
|
|
-- Colors here must stay in sync with quickshell/config/Theme.qml.
|
|
-- accent user-selectable, see `accents` below -- blue (#82aaff) ships
|
|
-- bg #222436 base
|
|
--
|
|
-- Performance note: every animation below is event-driven. Nothing uses the
|
|
-- "loop" style on borderangle/shadowangle/glowangle -- that style forces
|
|
-- Hyprland to render at full refresh rate forever, even when nothing is
|
|
-- visible, which is exactly the kind of idle GPU burn to avoid on a 4500x3000
|
|
-- display.
|
|
-- ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
local prefs = require("prefs")
|
|
|
|
-- The eight accents, from config/palette.json -- the one place they are
|
|
-- written down for everything outside QML. They used to be restated here,
|
|
-- which made adding a ninth accent a five-file edit; the file that got
|
|
-- forgotten was always the one that fails silently.
|
|
--
|
|
-- Falls back to the shipped blue pair if the palette cannot be read, for the
|
|
-- same reason prefs never raises: a missing file costs the accent colour, not
|
|
-- the compositor config.
|
|
local palette = prefs.readJson(os.getenv("HOME") .. "/.config/quickshell/config/palette.json")
|
|
local accents = palette.accents or {
|
|
blue = { dark = "82aaff", darkSecondary = "b172b0", light = "2e7de9", lightSecondary = "9854f1" },
|
|
}
|
|
|
|
-- The accent pair a fresh session or `hyprctl reload` starts from.
|
|
-- services/ColorScheme.qml overwrites this live, from the same Theme.accents
|
|
-- data, once the shell settles (~1.2s after startup -- see its `settle`
|
|
-- Timer). This table exists only so the compositor is never observably blue
|
|
-- for a non-blue accent during the gap before that first live apply.
|
|
local accentScheme = prefs.get("colorScheme", "dark")
|
|
local accentPair = accents[prefs.get("accentName", "blue")] or accents.blue
|
|
local accentStart = accentScheme == "light" and accentPair.light or accentPair.dark
|
|
local accentEnd = accentScheme == "light" and accentPair.lightSecondary or accentPair.darkSecondary
|
|
|
|
hl.config({
|
|
general = {
|
|
gaps_in = prefs.get("gapsIn", 5),
|
|
gaps_out = prefs.get("gapsOut", 10),
|
|
|
|
border_size = prefs.get("borderSize", 2),
|
|
|
|
col = {
|
|
-- The focused accent role, on a diagonal so the pair is visible
|
|
-- on both a tall and a wide window. Driven by the chosen
|
|
-- accentName (see the `accents` table above); services/
|
|
-- ColorScheme.qml applies the same values live, and restates them
|
|
-- from Theme.accent/Theme.accentSecondary on every scheme change
|
|
-- too, since each accent carries a separate pair per scheme.
|
|
active_border = { colors = { "rgba(" .. accentStart .. "ee)", "rgba(" .. accentEnd .. "ee)" }, angle = 115 },
|
|
-- The neutral inactive role follows the color scheme because a
|
|
-- dark neutral disappears against a light desktop.
|
|
-- services/ColorScheme.qml applies the same values live; this is
|
|
-- the value a fresh session starts from.
|
|
inactive_border = prefs.get("colorScheme", "dark") == "light"
|
|
and "rgba(a8aecb99)" or "rgba(3b426199)",
|
|
},
|
|
|
|
resize_on_border = prefs.get("resizeOnBorder", true),
|
|
extend_border_grab_area = prefs.getInt("borderGrabArea", 15),
|
|
hover_icon_on_border = prefs.get("hoverIconOnBorder", true),
|
|
|
|
-- Enables the per-window "immediate" rule used for games in rules.lua.
|
|
-- Harmless on its own; tearing only happens where a rule opts in.
|
|
allow_tearing = true,
|
|
|
|
layout = prefs.get("windowLayout", "dwindle"),
|
|
|
|
snap = {
|
|
enabled = prefs.get("windowSnapping", true),
|
|
window_gap = prefs.getInt("snapWindowGap", 10),
|
|
monitor_gap = prefs.getInt("snapMonitorGap", 10),
|
|
respect_gaps = prefs.get("snapRespectGaps", false),
|
|
},
|
|
},
|
|
|
|
decoration = {
|
|
-- 18 to match the shell's popover radius, so a window and a panel sitting
|
|
-- next to each other read as the same object family.
|
|
rounding = prefs.get("windowRounding", 18),
|
|
rounding_power = prefs.get("roundingPower", 2),
|
|
|
|
active_opacity = prefs.get("activeOpacity", 1.0),
|
|
fullscreen_opacity = prefs.get("fullscreenOpacity", 1.0),
|
|
inactive_opacity = prefs.get("inactiveOpacity", 1.0),
|
|
|
|
-- Accessibility: dim every window but the focused one.
|
|
dim_inactive = prefs.get("dimInactive", false),
|
|
dim_strength = prefs.get("dimStrength", 0.5),
|
|
|
|
blur = {
|
|
enabled = prefs.get("blurEnabled", true),
|
|
size = prefs.get("blurSize", 8),
|
|
passes = prefs.get("blurPasses", 3),
|
|
|
|
-- Required for blur to be affordable. Never turn this off.
|
|
new_optimizations = true,
|
|
-- Floating windows skip blurring what's behind tiled ones. Big win.
|
|
xray = true,
|
|
|
|
ignore_opacity = true,
|
|
noise = 0.0117,
|
|
contrast = 0.9,
|
|
brightness = 0.85,
|
|
vibrancy = 0.17,
|
|
vibrancy_darkness = 0.05,
|
|
|
|
popups = true,
|
|
popups_ignorealpha = 0.2,
|
|
|
|
-- Blurring the special workspace is expensive and rarely seen.
|
|
special = false,
|
|
},
|
|
|
|
shadow = {
|
|
enabled = prefs.get("shadowEnabled", true),
|
|
range = prefs.get("shadowRange", 20),
|
|
render_power = prefs.getInt("shadowRenderPower", 3),
|
|
sharp = prefs.get("shadowSharp", false),
|
|
color = "rgba(15161eee)",
|
|
-- Deliberately not a setting: a two-axis offset needs a control we
|
|
-- do not have, and a slider bound to half a value is worse than
|
|
-- leaving it alone. SystemSettings understands the vec2 shape
|
|
-- already, so adding it later is only a matter of the widget.
|
|
offset = { 0, 4 },
|
|
scale = 1.0,
|
|
},
|
|
|
|
-- New in 0.56. Kept deliberately faint: in this direction the gradient
|
|
-- border is the signature, and a strong halo would compete with it.
|
|
-- This is just enough to lift the focused window off the wallpaper.
|
|
-- Derived from the same accent as active_border above, not hardcoded,
|
|
-- so the halo never disagrees with the border it surrounds.
|
|
glow = {
|
|
enabled = prefs.get("glowEnabled", true),
|
|
range = prefs.get("glowRange", 8),
|
|
render_power = 2,
|
|
color = "rgba(" .. accentStart .. "33)",
|
|
color_inactive = "rgba(00000000)",
|
|
},
|
|
|
|
-- Off: costs real frame time and reads as smeary on a 60Hz panel.
|
|
motion_blur = { enabled = false },
|
|
},
|
|
|
|
animations = { enabled = prefs.get("animationsEnabled", true) },
|
|
|
|
dwindle = {
|
|
-- Keep the split orientation a window was created with. Closest match
|
|
-- to how the Forge extension behaved on GNOME.
|
|
preserve_split = prefs.get("preserveSplit", true),
|
|
smart_resizing = true,
|
|
force_split = prefs.getInt("forceSplit", 0),
|
|
},
|
|
|
|
-- Only in effect when the tiling layout is "master". Panama ships dwindle,
|
|
-- but Settings offers master as a choice, and a layout you can select and
|
|
-- cannot configure is barely a choice at all.
|
|
master = {
|
|
mfact = prefs.get("masterFactor", 0.55),
|
|
orientation = prefs.get("masterOrientation", "left"),
|
|
new_status = prefs.get("masterNewStatus", "slave"),
|
|
new_on_top = prefs.get("masterNewOnTop", false),
|
|
},
|
|
|
|
misc = {
|
|
force_default_wallpaper = 0,
|
|
-- Stored as "show the logo / show the splash" and written as Hyprland's
|
|
-- `disable_*`, matching the `invert` flag on these entries in
|
|
-- PreferenceSchema so both sides agree about which way round they are.
|
|
disable_hyprland_logo = not prefs.get("hyprlandLogo", false),
|
|
disable_splash_rendering = not prefs.get("hyprlandSplash", false),
|
|
|
|
-- Keep native Wayland selection paste and GTK's matching preference
|
|
-- in lockstep. DesktopStyle applies the GTK half only after this value
|
|
-- has been read back and stored by SystemSettings.
|
|
middle_click_paste = prefs.get("middleClickPaste", true),
|
|
|
|
-- Same setting as Theme.fontFamily in the shell. If only the QML side
|
|
-- followed the preference, the compositor and the shell would disagree
|
|
-- about the interface font and nobody would notice until a tooltip
|
|
-- rendered in a different typeface.
|
|
font_family = prefs.get("interfaceFont", "Adwaita Sans"),
|
|
|
|
-- Variable refresh rate. 3 = enable only for fullscreen windows whose
|
|
-- content type is "video" or "game" -- the tag rules.lua applies to
|
|
-- games. Keeps VRR off the desktop, where it causes visible flicker.
|
|
vrr = prefs.getInt("vrrPolicy", 3),
|
|
|
|
-- Blur behind the lock screen.
|
|
session_lock_blur = true,
|
|
|
|
-- Let a crashed lock screen be recovered rather than stranding you.
|
|
allow_session_lock_restore = true,
|
|
|
|
-- Don't let apps steal focus by shouting; matches GNOME's behavior.
|
|
focus_on_activate = prefs.get("focusOnActivate", false),
|
|
|
|
-- Whether pointing at another monitor is enough to move focus there,
|
|
-- or it takes a click. GNOME moves on pointer; both are offered.
|
|
mouse_move_focuses_monitor = prefs.get("mouseMoveFocusesMonitor", true),
|
|
|
|
-- Window swallowing: a terminal hides itself while a graphical
|
|
-- application launched from it is open, and comes back when that
|
|
-- application exits. Off by default -- it is a real change in how the
|
|
-- desktop behaves, and one that is confusing rather than broken if you
|
|
-- did not ask for it: your terminal appears to vanish.
|
|
--
|
|
-- The regex is narrow on purpose. Anything matching it can swallow, so
|
|
-- a permissive pattern means windows disappearing in cases nobody
|
|
-- intended. Only the two terminals this desktop actually ships.
|
|
enable_swallow = prefs.get("windowSwallow", false),
|
|
swallow_regex = "^(kitty|com\\.mitchellh\\.ghostty)$",
|
|
},
|
|
|
|
render = {
|
|
-- Color management is stable and on by default in 0.56.
|
|
cm_enabled = true,
|
|
|
|
-- 1 = automatically flip the monitor into HDR for fullscreen content
|
|
-- that asks for it, and back out afterwards. This is how games get HDR
|
|
-- without the desktop paying the screencopy cost. See monitors.lua.
|
|
cm_auto_hdr = prefs.getInt("autoHdr", 1),
|
|
|
|
-- 2 = direct scanout only for windows tagged content = "game"
|
|
-- (set by the rules in rules.lua). Bypasses compositing for real
|
|
-- fullscreen games.
|
|
direct_scanout = prefs.getInt("directScanoutPolicy", 2),
|
|
},
|
|
|
|
cursor = {
|
|
-- 2 = auto. Disables the hardware cursor when tearing, which is what
|
|
-- keeps the cursor from ghosting in games.
|
|
no_hardware_cursors = 2,
|
|
|
|
-- Don't let the cursor break VRR on content tagged as a game.
|
|
no_break_fs_vrr = 2,
|
|
min_refresh_rate = 48,
|
|
|
|
-- Pull the cursor theme from gsettings so GTK apps and Hyprland agree.
|
|
sync_gsettings_theme = true,
|
|
|
|
-- Fade the cursor out after 4s of no movement, like GNOME does.
|
|
inactive_timeout = prefs.get("cursorInactiveTimeout", 4),
|
|
|
|
-- The accessibility magnifier. 1.0 = off.
|
|
zoom_factor = prefs.get("magnifierFactor", 1.0),
|
|
zoom_rigid = prefs.get("magnifierRigid", false),
|
|
},
|
|
|
|
ecosystem = {
|
|
no_update_news = not prefs.get("hyprlandUpdateNews", false),
|
|
no_donation_nag = not prefs.get("hyprlandDonationNag", false),
|
|
},
|
|
|
|
xwayland = {
|
|
-- Render X11 apps at 1:1 device pixels so they're sharp rather than
|
|
-- bilinearly upscaled. See the note in README.md about the unavoidable
|
|
-- sizing tradeoff at 1.5x.
|
|
force_zero_scaling = true,
|
|
},
|
|
|
|
quirks = {
|
|
-- 2 = report HDR as preferred to gamescope only. Stops gamescope from
|
|
-- white-screening when it expects HDR to already be live.
|
|
prefer_hdr = 2,
|
|
},
|
|
})
|
|
|
|
-- ── Motion ──────────────────────────────────────────────────────────────────
|
|
-- speed is in deciseconds: speed = 4 means 400ms. Higher = slower.
|
|
|
|
hl.curve("easeOutQuint", { type = "bezier", points = { { 0.23, 1 }, { 0.32, 1 } } })
|
|
hl.curve("almostLinear", { type = "bezier", points = { { 0.5, 0.5 }, { 0.75, 1 } } })
|
|
hl.curve("quick", { type = "bezier", points = { { 0.15, 0 }, { 0.1, 1 } } })
|
|
hl.curve("easy", { type = "spring", mass = 1, stiffness = 238.1191, dampening = 24.21279 })
|
|
-- A touch bouncier than "easy"; used for windows appearing.
|
|
hl.curve("snappy", { type = "spring", mass = 1, stiffness = 320, dampening = 22 })
|
|
|
|
hl.animation({ leaf = "global", enabled = true, speed = 10, bezier = "default" })
|
|
hl.animation({ leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" })
|
|
|
|
-- The focus sweep: when a window takes focus, its gradient border rotates into
|
|
-- place once. Style "once" is load-bearing -- the alternative, "loop", makes
|
|
-- Hyprland render at full refresh rate forever, even when no border is visible.
|
|
-- That is exactly the idle GPU cost this config exists to avoid, so: never loop.
|
|
hl.animation({ leaf = "borderangle", enabled = true, speed = 6, bezier = "easeOutQuint", style = "once" })
|
|
|
|
hl.animation({ leaf = "windows", enabled = true, speed = 4.79, spring = "easy" })
|
|
hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, spring = "snappy", style = "popin 90%" })
|
|
hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.49, bezier = "almostLinear", style = "popin 90%" })
|
|
|
|
hl.animation({ leaf = "fade", enabled = true, speed = 3.03, bezier = "quick" })
|
|
|
|
-- Layer surfaces = the Quickshell bar, dock, popovers and overlays.
|
|
hl.animation({ leaf = "layers", enabled = true, speed = 3.81, bezier = "easeOutQuint" })
|
|
hl.animation({ leaf = "layersIn", enabled = true, speed = 3, bezier = "easeOutQuint", style = "fade" })
|
|
hl.animation({ leaf = "layersOut", enabled = true, speed = 2, bezier = "almostLinear", style = "fade" })
|
|
|
|
-- Workspace switching: horizontal slide, matching Alt+H / Alt+L direction.
|
|
hl.animation({ leaf = "workspaces", enabled = true, speed = 2.5, bezier = "easeOutQuint", style = "slidefade 15%" })
|
|
|
|
hl.animation({ leaf = "zoomFactor", enabled = true, speed = 7, bezier = "quick" })
|
|
|
|
return true
|