Shortcuts you invent, rules you write, gestures you own - all still just data

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-25 01:51:59 -04:00
parent f9e5d3f470
commit 06c53d6c21
48 changed files with 4749 additions and 140 deletions
+93
View File
@@ -9,6 +9,8 @@
-- to invert it.
-- ─────────────────────────────────────────────────────────────────────────────
local prefs = require("prefs")
-- ── Upstream sanity rules ───────────────────────────────────────────────────
hl.window_rule({
name = "suppress-maximize-events",
@@ -136,6 +138,97 @@ hl.window_rule({
no_dim = true,
})
-- ── Per-application rules the user wrote ────────────────────────────────────
--
-- `windowRules` in settings.json, edited from Settings' Windows page. Each
-- entry is data and nothing else:
--
-- { class, label, float, center, size = {w, h}, workspace, noAnim, game,
-- noDim, pin }
--
-- `class` is matched LITERALLY. Hyprland matches with RE2, so a class typed
-- into a text field is a regular expression unless something escapes it -- and
-- "org.gnome.Files" as a pattern also matches "orgxgnomexFiles", while a
-- half-typed "(" is a pattern error rather than a rule that matches nothing.
-- Escaped and anchored here, so what the user typed is what gets matched.
--
-- Emitted AFTER the shipped rules and deliberately WITHOUT a name. Hyprland
-- evaluates every named rule before every anonymous one, so a named user rule
-- would silently outrank the anonymous shipped rules above it -- the opposite
-- of the intended precedence. Anonymous, last, is what "the user's rule wins"
-- actually means here.
--
-- An entry that fails any check is skipped whole rather than emitted with the
-- bad field dropped: a rule that half-applies is harder to understand than one
-- that is not there, and the settings page can see the same thing is wrong.
local function escape_regex(value)
return (value:gsub("[%^%$%(%)%%%.%[%]%*%+%-%?%{%}%|\\]", "\\%0"))
end
-- The shell's own surfaces are layers, not windows -- but Quickshell's helper
-- windows are not, and a rule that floats or moves one of them would be a user
-- breaking their own desktop from the Windows page. Refused at both ends; this
-- is the end that matters, because the file is hand-editable.
local function reserved_class(class)
local lowered = class:lower()
return lowered:match("^quickshell") ~= nil or lowered:match("^qs%-") ~= nil
end
local function positive_integer(value, low, high)
if type(value) ~= "number" or value ~= math.floor(value) then
return nil
end
if value < low or value > high then
return nil
end
return value
end
for _, entry in ipairs(prefs.get("windowRules", {})) do
if type(entry) == "table" and type(entry.class) == "string" then
local class = entry.class
local rule = nil
if #class >= 1 and #class <= 128 and not reserved_class(class) then
rule = { match = { class = "^" .. escape_regex(class) .. "$" } }
if entry.float == true then rule.float = true end
if entry.center == true then rule.center = true end
if entry.noAnim == true then rule.no_anim = true end
if entry.noDim == true then rule.no_dim = true end
if entry.pin == true then rule.pin = true end
-- The keystone the gaming rules above use: misc.vrr,
-- render.direct_scanout and cursor.no_break_fs_vrr all key off it.
if entry.game == true then rule.content = "game" end
if entry.size ~= nil then
local size = entry.size
local width = type(size) == "table" and positive_integer(size[1], 50, 10000) or nil
local height = type(size) == "table" and positive_integer(size[2], 50, 10000) or nil
if width == nil or height == nil then
rule = nil
else
rule.size = { width, height }
end
end
if rule ~= nil and entry.workspace ~= nil then
local workspace = positive_integer(entry.workspace, 1, 10)
if workspace == nil then
rule = nil
else
rule.workspace = workspace
end
end
end
if rule ~= nil then
hl.window_rule(rule)
end
end
end
-- ── Workspace rules ─────────────────────────────────────────────────────────
-- Deliberately NO "smart gaps".
--