Four things a Hyprland desktop can do that this one was not. Searching from the launcher needed no launcher work at all: Vicinae already models it, so this is a script command with one percent-encoded argument. Make it the fallback command and anything typed that matches nothing else offers to search it. Bangs come free -- they are a property of where the query is sent, not of the launcher -- so !yt reaches YouTube without a line of bang parsing. Suggestions could not be a script command. They need a view that reacts as you type, which is an extension: TypeScript, compiled, querying the same endpoint Firefox's address bar uses. It debounces, and aborts the request in flight on every keystroke -- typing is faster than the network, and an older answer landing after a newer one leaves the list describing a query that is no longer on screen. A bang skips suggestions entirely, because Google has no useful guesses about "!yt". The engine is now written down twice, once in each. The contract pins that they agree, since searching from the fallback and searching from the suggestions reaching different places is the kind of wrong that looks fine. Gestures mirror GNOME: three fingers sideways for workspaces, up for the overview, down to dismiss it. Open and close rather than toggle both ways -- toggling means swiping up from an open overview closes it, which is not what the fingers meant. Hyprland reads gesture registrations at startup so they cannot be a setting, but distance and direction can be, and are. Window swallowing is off by default and a preference like every other misc setting here. A terminal that vanishes when you did not ask for it is confusing rather than broken, which is worse. Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1
298 lines
15 KiB
Lua
298 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")
|
|
|
|
-- 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 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
|