362 lines
17 KiB
Lua
362 lines
17 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
|
|
|
|
-- ── Color filters ───────────────────────────────────────────────────────────
|
|
--
|
|
-- Grayscale and three color-blindness corrections, as end-of-pipe screen
|
|
-- shaders. Hyprland composites the desktop and then runs one fragment shader
|
|
-- over the result, so a filter here covers every window, the shell, the cursor
|
|
-- and video alike -- which is the only way a filter is honest.
|
|
--
|
|
-- The PREFERENCE is the enum, not the path. That split is deliberate: storing
|
|
-- the path would put a filesystem location a person can edit into the value
|
|
-- that becomes `decoration:screen_shader`, and it would make the stored value
|
|
-- disagree with what hyprctl reports back (which is the path), failing the
|
|
-- schema shape and write-sweep contracts. So PreferenceSchema's colorFilter
|
|
-- entry carries no `hypr:` block, and the enum→path mapping is written twice
|
|
-- on purpose: here, for reloads and for the moment before the shell starts,
|
|
-- and in services/SystemSettings.qml's applyColorFilter for the live apply.
|
|
-- The two lists have to be edited together.
|
|
--
|
|
-- "none" and anything unrecognised both produce the empty string, which is
|
|
-- what Hyprland reads as "no shader" -- and is the value it needs to be given
|
|
-- to turn one OFF, since there is no way to unset the option.
|
|
local shaderDir = (function()
|
|
local configHome = os.getenv("XDG_CONFIG_HOME")
|
|
if configHome == nil or configHome == "" then
|
|
local home = os.getenv("HOME")
|
|
if home == nil or home == "" then
|
|
return nil
|
|
end
|
|
configHome = home .. "/.config"
|
|
end
|
|
return configHome .. "/hypr/shaders"
|
|
end)()
|
|
|
|
local colorFilters = {
|
|
grayscale = "grayscale.frag",
|
|
protanopia = "protanopia.frag",
|
|
deuteranopia = "deuteranopia.frag",
|
|
tritanopia = "tritanopia.frag",
|
|
}
|
|
|
|
local colorFilterShader = ""
|
|
do
|
|
local file = colorFilters[prefs.get("colorFilter", "none")]
|
|
if file ~= nil and shaderDir ~= nil then
|
|
colorFilterShader = shaderDir .. "/" .. file
|
|
end
|
|
end
|
|
|
|
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 },
|
|
|
|
-- The accessibility color filter, resolved above. Empty when off, and
|
|
-- empty costs nothing: Hyprland skips the pass entirely rather than
|
|
-- running an identity shader.
|
|
screen_shader = colorFilterShader,
|
|
},
|
|
|
|
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
|