Files
Panama/config/dot/hypr/looks.lua
T
Gabriel Brown 9ba224d776 Finish threading the accent colour through the whole desktop
The recent accent-colour setting only reached part of the desktop.
looks.lua still hardcoded the focused-window border and glow to blue,
so any hyprctl reload -- or every fresh session, for about a second --
reverted a chosen accent; it now reads accentName the same way it
already read colorScheme. The lock screen and terminal stayed blue
regardless of the chosen accent despite the setting's own description
claiming otherwise; panama-lock and panama-theme-apps now resolve and
apply the real accent.

AccentPicker built its swatch model from Theme.accents directly
instead of the schema's own options list, so the two could drift
silently; switched it to read the schema. Its hit target only covered
the swatch, not the name label added specifically for colour-vision
accessibility -- extended to the whole row. Settings search had no
route for the "appearance" group, so searching for the accent or
colour scheme landed on Home.

ColorScheme's hex-to-Hyprland helper assumed 6-digit colours and would
silently corrupt a future translucent one; fixed it to read from the
end of the string instead of the start. An accent-only change no
longer reruns the full colour-scheme pipeline. The gradient it builds
for the focused border now goes through SystemSettings' existing
serialiser instead of a second, under-escaped copy of the same logic.

The settings-ownership contract test enforced the old rule that
ColorScheme must never touch the focused border; updated it to verify
the real, intended rule instead of contradicting the code.

Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
2026-08-18 21:23:16 -04:00

286 lines
14 KiB
Lua

-- ─────────────────────────────────────────────────────────────────────────────
-- Look and feel -- Tokyo Night Moon
--
-- Colours 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")
-- Mirrors config/Theme.qml's `accents` map. Lua has no import path into a QML
-- singleton, so the hex pairs are restated here -- just the four hex strings
-- each named accent needs, not the labels, which stay UI-only.
local accents = {
blue = { dark = "82aaff", darkSecondary = "b172b0", light = "2e7de9", lightSecondary = "9854f1" },
orchid = { dark = "c099ff", darkSecondary = "fca7ea", light = "7847bd", lightSecondary = "9854f1" },
teal = { dark = "86e1fc", darkSecondary = "82aaff", light = "007197", lightSecondary = "2e7de9" },
green = { dark = "c3e88d", darkSecondary = "86e1fc", light = "587539", lightSecondary = "007197" },
amber = { dark = "ffc777", darkSecondary = "ff966c", light = "8c6c3e", lightSecondary = "b15c00" },
orange = { dark = "ff966c", darkSecondary = "ff757f", light = "b15c00", lightSecondary = "c64343" },
rose = { dark = "ff757f", darkSecondary = "c099ff", light = "f52a65", lightSecondary = "9854f1" },
slate = { dark = "828bb8", darkSecondary = "82aaff", light = "6172b0", lightSecondary = "2e7de9" },
}
-- 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 colour 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 = "dwindle",
snap = {
enabled = 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),
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,
},
-- 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 behaviour.
focus_on_activate = false,
},
render = {
-- Colour 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),
},
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