They were written down five times: ThemeProfileModel.js for QML, looks.lua for the compositor, and again in panama-theme-apps and panama-lock. The GNOME accent-name mapping was a sixth list. Adding a ninth accent meant editing all of them, and the file most likely to be missed was the lock screen, which fails silently -- the machine locks in last season's colour and nothing says why. panama-theme-apps admitted it in a comment: "there is no shared source between QML and a shell script". config/palette.json is that source now. looks.lua reads it through a new prefs.readJson, which uses the same never-raise parser the settings store uses, so an unreadable palette costs the accent colours and never the compositor config. The two shell generators read it through scripts/panama-palette, which also carries the hex-to-rgb conversion hyprlock needs and the GNOME member lookup. QML keeps its table, because a .js module imported into QML cannot read a file. That is still a copy, so the palette contract compares the two value by value -- every accent, every field -- and fails on any disagreement. Verified by planting a wrong hex and watching it name the exact field. The adwaita contract used to check the shell's own copy of the GNOME mapping. It now checks that the shell resolves through the palette, and fails if that copy ever grows back.
297 lines
14 KiB
Lua
297 lines
14 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 = "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 behavior.
|
|
focus_on_activate = false,
|
|
|
|
-- 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),
|
|
},
|
|
|
|
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
|