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:
@@ -33,9 +33,11 @@ Don't "fix" them.
|
||||
| `looks.lua` | Colors, blur, glow, shadows, animations, VRR, scanout |
|
||||
| `input.lua` | Keyboard/mouse. Click-to-focus, like GNOME |
|
||||
| `rules.lua` | Window rules, gaming rules, layer rules for the shell |
|
||||
| `keybinds.lua` | The full keymap |
|
||||
| `keybinds.lua` | The full keymap, including the custom shortcuts the Settings app stores |
|
||||
| `actions.lua` | Named-action resolver: the whitelist tables that turn stored `{kind, target}` data into binds, gestures — never into free-form commands |
|
||||
| `autostart.lua` | Session startup |
|
||||
| `overrides.lua` | Per-machine escape hatch, loaded last |
|
||||
| `shaders/` | Whole-screen color-filter shaders (grayscale and the three color-blindness corrections) `looks.lua` maps the `colorFilter` setting onto |
|
||||
| `hyprlock.conf` / `hypridle.conf` / `hyprpaper.conf` / `hyprtoolkit.conf` | Ecosystem tools (hyprlang) |
|
||||
|
||||
Validate any change without leaving your session:
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Named actions
|
||||
--
|
||||
-- The one thing that makes it safe for settings.json to describe a shortcut.
|
||||
--
|
||||
-- Panama Settings lets a person invent a keyboard shortcut and assign a
|
||||
-- four-finger gesture. Both are stored in the same user-editable JSON file the
|
||||
-- rest of the desktop reads, and both have to end up as something the
|
||||
-- compositor executes -- which is exactly the shape of every configuration
|
||||
-- format that turned out to be a shell injection.
|
||||
--
|
||||
-- It is not one here, and this file is why. A stored action is DATA:
|
||||
--
|
||||
-- { kind = "app" | "shell" | "window", target = "<id>", label = "<text>" }
|
||||
--
|
||||
-- `kind` is an enum with three members. `target` is either a key of one of the
|
||||
-- whitelist tables below -- whose values are literals written here, in Lua, by
|
||||
-- a human -- or, for `app`, an identifier that has to match a character class
|
||||
-- containing no shell metacharacter at all, and which is then quoted as a
|
||||
-- single argv element for panama-launch rather than pasted into a command.
|
||||
--
|
||||
-- So the worst a hand-edited (or maliciously written) settings file can do is
|
||||
-- pick a different entry from a list that is fixed at ship time, or launch an
|
||||
-- application by id. It cannot introduce a command. There is no path from a
|
||||
-- stored string to a new exec string; the table lookups are the only source of
|
||||
-- one.
|
||||
--
|
||||
-- Everything invalid returns nil and the caller skips the bind or gesture --
|
||||
-- the prefs.lua philosophy: never raise, never guess. A malformed entry costs
|
||||
-- one shortcut, never the keymap and never the compositor.
|
||||
--
|
||||
-- Required by keybinds.lua (custom shortcuts) and input.lua (four-finger
|
||||
-- gestures). services/Keybinds.qml's describeAction() is the QML mirror of the
|
||||
-- vocabulary below; the two lists have to be edited together.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local actions = {}
|
||||
|
||||
-- ── Shell verbs ─────────────────────────────────────────────────────────────
|
||||
--
|
||||
-- Every entry is a command string written HERE. Nothing stored anywhere else
|
||||
-- contributes a character to one; `target` only chooses which of these to use.
|
||||
--
|
||||
-- The `qs ipc call` targets and functions are the ones quickshell/shell.qml
|
||||
-- actually registers -- an IpcHandler silently declines to register a function
|
||||
-- it cannot type-check, so a verb invented here would be a shortcut that does
|
||||
-- nothing. Checked against shell.qml, not remembered.
|
||||
--
|
||||
-- The three that are not IPC (`launcher`, `color-picker`, `lock`) are the same
|
||||
-- literal commands the shipped binds in keybinds.lua use, for the same reason
|
||||
-- they use them: they are the tools, not the shell.
|
||||
local SHELL = {
|
||||
["dnd-toggle"] = { label = "Do Not Disturb", command = "qs ipc call notifications dnd" },
|
||||
["notifications"] = { label = "Notifications", command = "qs ipc call notifications toggle" },
|
||||
["overview"] = { label = "Overview", command = "qs ipc call overview toggle" },
|
||||
["launcher"] = { label = "Launcher", command = "vicinae toggle" },
|
||||
["clipboard"] = { label = "Clipboard history", command = "qs ipc call clipboard toggle" },
|
||||
["screenshot"] = { label = "Screenshot / record", command = "qs ipc call capture open" },
|
||||
["screenshot-screen"] = { label = "Screenshot: whole screen", command = "qs ipc call capture screenNow" },
|
||||
["screenshot-window"] = { label = "Screenshot: window", command = "qs ipc call capture windowNow" },
|
||||
["screen-intelligence"]= { label = "Screen Intelligence", command = "qs ipc call screen-intelligence open" },
|
||||
["color-picker"] = { label = "Color picker", command = "hyprpicker -a -f hex" },
|
||||
["quick-settings"] = { label = "Quick settings", command = "qs ipc call quicksettings toggle" },
|
||||
["settings"] = { label = "Settings", command = "qs ipc call settings toggle" },
|
||||
["cheatsheet"] = { label = "Keyboard shortcuts", command = "qs ipc call cheatsheet toggle" },
|
||||
["focus-session"] = { label = "Focus session", command = "qs ipc call focus reveal" },
|
||||
["caffeine"] = { label = "Keep awake", command = "qs ipc call caffeine toggle" },
|
||||
["night-light"] = { label = "Night Light", command = "qs ipc call night-light toggle" },
|
||||
["activity"] = { label = "Activity", command = "qs ipc call activity toggle" },
|
||||
["power-menu"] = { label = "Power menu", command = "qs ipc call powermenu toggle" },
|
||||
["lock"] = { label = "Lock", command = "loginctl lock-session" },
|
||||
}
|
||||
|
||||
-- ── Window verbs ────────────────────────────────────────────────────────────
|
||||
--
|
||||
-- Builders rather than dispatchers, so nothing is constructed for a verb that
|
||||
-- is never chosen, and so a gesture builds its dispatcher when the fingers
|
||||
-- move rather than holding one from config time.
|
||||
--
|
||||
-- `workspace:N` is not in the table: it is ten entries that differ by a number,
|
||||
-- and the number is validated as 1..10 in `window_action` below.
|
||||
local WINDOW = {
|
||||
["float-toggle"] = { label = "Toggle float", build = function() return hl.dsp.window.float({ action = "toggle" }) end },
|
||||
["fullscreen"] = { label = "Fullscreen", build = function() return hl.dsp.window.fullscreen({ mode = "fullscreen" }) end },
|
||||
["pin"] = { label = "Pin window", build = function() return hl.dsp.window.pin({ action = "toggle" }) end },
|
||||
}
|
||||
|
||||
-- Published so a contract can read the vocabulary without parsing this file,
|
||||
-- and so the ten workspace verbs have one definition rather than two.
|
||||
actions.shell_verbs = SHELL
|
||||
actions.window_verbs = WINDOW
|
||||
actions.workspace_min = 1
|
||||
actions.workspace_max = 10
|
||||
|
||||
-- ── Targets ─────────────────────────────────────────────────────────────────
|
||||
|
||||
-- Letters, digits, and the four punctuation marks a desktop id actually uses.
|
||||
-- Deliberately excludes every shell metacharacter, quote, slash and space, so
|
||||
-- an id that passes cannot change the meaning of a command line even before it
|
||||
-- is quoted -- the quoting below is the second lock on the same door.
|
||||
local APP_TARGET = "^[A-Za-z0-9@._%-]+$"
|
||||
|
||||
local function valid_app_target(target)
|
||||
return type(target) == "string"
|
||||
and #target >= 1 and #target <= 128
|
||||
and target:match(APP_TARGET) ~= nil
|
||||
end
|
||||
|
||||
-- Single-quoted for the shell, with the one escape single quotes need. Same
|
||||
-- function keybinds.lua uses for the go-to patterns, and used here for the
|
||||
-- same reason: the value reaches the command as one argument.
|
||||
local function shell_quote(value)
|
||||
return "'" .. value:gsub("'", "'\\''") .. "'"
|
||||
end
|
||||
|
||||
-- Google RE2 metacharacters, escaped so the id matches itself literally.
|
||||
-- panama-launch takes a regular expression, and an unescaped "org.gnome.Files"
|
||||
-- would also match "orgxgnomexFiles".
|
||||
local function escape_regex(value)
|
||||
return (value:gsub("[%^%$%(%)%%%.%[%]%*%+%-%?%{%}%|\\]", "\\%0"))
|
||||
end
|
||||
|
||||
-- ── Resolution ──────────────────────────────────────────────────────────────
|
||||
|
||||
local launcher_bin = "$HOME/.local/share/Panama/bin/panama-launch"
|
||||
|
||||
-- The launch-or-focus path the shipped application keys use: raise the window
|
||||
-- if it is already open, start it if it is not. The id is the class pattern
|
||||
-- (anchored, escaped) and the thing to start; on Wayland an application's
|
||||
-- desktop id and its window class are the same string often enough that this
|
||||
-- is the right first guess, and the wrong guess costs a second window rather
|
||||
-- than an error.
|
||||
--
|
||||
-- gtk-launch activates a desktop entry by id, which is what the applications
|
||||
-- catalog in Settings offers -- a desktop id is not a binary and cannot be
|
||||
-- exec'd directly.
|
||||
local function app_action(target)
|
||||
if not valid_app_target(target) then
|
||||
return nil
|
||||
end
|
||||
local launch_command = table.concat({
|
||||
launcher_bin,
|
||||
"--class", shell_quote("^" .. escape_regex(target) .. "$"),
|
||||
"--", "gtk-launch", shell_quote(target),
|
||||
}, " ")
|
||||
return function() return hl.dsp.exec_cmd(launch_command) end
|
||||
end
|
||||
|
||||
local function shell_action(target)
|
||||
if type(target) ~= "string" then
|
||||
return nil
|
||||
end
|
||||
local verb = SHELL[target]
|
||||
if verb == nil then
|
||||
return nil
|
||||
end
|
||||
return function() return hl.dsp.exec_cmd(verb.command) end
|
||||
end
|
||||
|
||||
local function window_action(target)
|
||||
if type(target) ~= "string" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local verb = WINDOW[target]
|
||||
if verb ~= nil then
|
||||
return verb.build
|
||||
end
|
||||
|
||||
local index = target:match("^workspace:(%d+)$")
|
||||
if index == nil then
|
||||
return nil
|
||||
end
|
||||
local number = tonumber(index)
|
||||
if number == nil or number < actions.workspace_min or number > actions.workspace_max then
|
||||
return nil
|
||||
end
|
||||
return function() return hl.dsp.focus({ workspace = number }) end
|
||||
end
|
||||
|
||||
local KINDS = {
|
||||
app = app_action,
|
||||
shell = shell_action,
|
||||
window = window_action,
|
||||
}
|
||||
|
||||
-- The builder for one stored entry, or nil when the entry is anything this
|
||||
-- file does not recognise. Everything above funnels through here, so there is
|
||||
-- exactly one place where a stored value becomes an action.
|
||||
local function builder(entry)
|
||||
if type(entry) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
local resolve = KINDS[entry.kind]
|
||||
if resolve == nil then
|
||||
return nil
|
||||
end
|
||||
return resolve(entry.target)
|
||||
end
|
||||
|
||||
-- For hl.bind: the dispatcher itself, or nil.
|
||||
function actions.dispatcher(entry)
|
||||
local build = builder(entry)
|
||||
if build == nil then
|
||||
return nil
|
||||
end
|
||||
return build()
|
||||
end
|
||||
|
||||
-- For hl.gesture: a function, which is what a gesture action has to be when it
|
||||
-- is not one of Hyprland's own built-in names ("workspace" and friends).
|
||||
function actions.gesture(entry)
|
||||
local build = builder(entry)
|
||||
if build == nil then
|
||||
return nil
|
||||
end
|
||||
return function() hl.dispatch(build()) end
|
||||
end
|
||||
|
||||
-- True when an entry resolves to something. Cheap enough to call twice; used
|
||||
-- where the caller wants to check before it commits to emitting anything.
|
||||
function actions.valid(entry)
|
||||
return builder(entry) ~= nil
|
||||
end
|
||||
|
||||
return actions
|
||||
@@ -6,6 +6,7 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local prefs = require("prefs")
|
||||
local actions = require("actions")
|
||||
|
||||
hl.config({
|
||||
input = {
|
||||
@@ -141,4 +142,34 @@ hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
|
||||
hl.gesture({ fingers = 3, direction = "up", action = overview("open") })
|
||||
hl.gesture({ fingers = 3, direction = "down", action = overview("close") })
|
||||
|
||||
-- ── Four-finger gestures ────────────────────────────────────────────────────
|
||||
--
|
||||
-- The three above are the desktop's, fixed. These four are the user's: each
|
||||
-- holds a named action from settings.json, or {} for unassigned, and the
|
||||
-- vocabulary is exactly the one custom shortcuts use -- actions.lua resolves
|
||||
-- both, through the same whitelist tables, so a gesture can no more introduce
|
||||
-- a command than a keybind can.
|
||||
--
|
||||
-- Nothing is emitted for an unassigned direction. That matters more here than
|
||||
-- it looks: a registration is read at config time and there is no way to
|
||||
-- remove one afterwards, so emitting a no-op gesture for every direction would
|
||||
-- consume the four-finger swipes permanently, including for whatever the
|
||||
-- compositor might do with them later.
|
||||
--
|
||||
-- Four rather than three because three is spoken for, and because four fingers
|
||||
-- is the largest number of them a touchpad this size can tell apart.
|
||||
local custom_gestures = {
|
||||
{ pref = "gestureFourUp", direction = "up" },
|
||||
{ pref = "gestureFourDown", direction = "down" },
|
||||
{ pref = "gestureFourLeft", direction = "left" },
|
||||
{ pref = "gestureFourRight", direction = "right" },
|
||||
}
|
||||
|
||||
for _, gesture in ipairs(custom_gestures) do
|
||||
local action = actions.gesture(prefs.get(gesture.pref, {}))
|
||||
if action ~= nil then
|
||||
hl.gesture({ fingers = 4, direction = gesture.direction, action = action })
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
local prefs = require("prefs")
|
||||
local actions = require("actions")
|
||||
|
||||
local mod = "SUPER"
|
||||
|
||||
@@ -574,6 +575,52 @@ bind("XF86WWW", hl.dsp.exec_cmd(browser), { description = "Browser" })
|
||||
bind("XF86Mail", hl.dsp.exec_cmd(mail), { description = "Mail" })
|
||||
bind("XF86Search", hl.dsp.exec_cmd(launcher), { description = "Launcher" })
|
||||
|
||||
-- ── Custom shortcuts ────────────────────────────────────────────────────────
|
||||
--
|
||||
-- Shortcuts the user invented, from `customBinds` in settings.json. Each entry
|
||||
-- is { chord, kind, target, label } -- data, never a command. actions.lua turns
|
||||
-- the kind/target pair into a dispatcher through whitelist tables; an entry it
|
||||
-- does not recognise resolves to nil and is silently not emitted.
|
||||
--
|
||||
-- Emitted LAST, and through hl.bind rather than the `bind` wrapper above. Two
|
||||
-- separate reasons, both about keeping the two rebinding mechanisms apart:
|
||||
--
|
||||
-- * `keybindOverrides` is keyed by a SHIPPED chord. A custom bind has no
|
||||
-- shipped chord -- it is rebound by rewriting its own entry -- so putting
|
||||
-- one through `bind` would let an override for some shipped key silently
|
||||
-- move a custom one that happened to share a chord.
|
||||
-- * last means a custom chord that collides with a shipped one loses, which
|
||||
-- is checked explicitly below rather than left to Hyprland's ordering.
|
||||
--
|
||||
-- Settings prevents a collision upstream; this is the second lock, because the
|
||||
-- file is hand-editable and losing a shipped key to a typo is not acceptable.
|
||||
category("Custom")
|
||||
|
||||
local function custom_bind(chord, dispatcher, label)
|
||||
categories[chord] = current_category
|
||||
return hl.bind(chord, dispatcher, { description = label })
|
||||
end
|
||||
|
||||
for _, entry in ipairs(prefs.get("customBinds", {})) do
|
||||
if type(entry) == "table" then
|
||||
local chord = entry.chord
|
||||
local label = entry.label
|
||||
|
||||
-- A description is not decoration: keybinds-contract fails a build
|
||||
-- with a description-less bind, and the cheatsheet and Shortcuts page
|
||||
-- both list what they find. A nameless shortcut is unfindable.
|
||||
if valid_chord(chord)
|
||||
and type(label) == "string" and label ~= ""
|
||||
and categories[chord] == nil
|
||||
then
|
||||
local dispatcher = actions.dispatcher(entry)
|
||||
if dispatcher ~= nil then
|
||||
custom_bind(chord, dispatcher, label)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
write_categories()
|
||||
|
||||
return true
|
||||
|
||||
@@ -37,6 +37,53 @@ 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),
|
||||
@@ -145,6 +192,11 @@ hl.config({
|
||||
|
||||
-- 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) },
|
||||
|
||||
@@ -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".
|
||||
--
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Deuteranopia -- green-blind.
|
||||
//
|
||||
// Panama's accessibility color filters. Selected by the `colorFilter`
|
||||
// preference; hypr/looks.lua maps the enum to this path at config time and
|
||||
// services/SystemSettings.qml does the same live.
|
||||
//
|
||||
// Same family of matrices as protanopia.frag, weighted for the missing green
|
||||
// cone instead of the red one. See that file for why this is a correction
|
||||
// rather than a simulation, and for the column-major note.
|
||||
|
||||
#version 300 es
|
||||
|
||||
precision mediump float;
|
||||
in vec2 v_texcoord;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
uniform sampler2D tex;
|
||||
|
||||
const mat3 deuteranopia = mat3(
|
||||
0.625, 0.700, 0.000,
|
||||
0.375, 0.300, 0.300,
|
||||
0.000, 0.000, 0.700
|
||||
);
|
||||
|
||||
void main() {
|
||||
vec4 pixColor = texture(tex, v_texcoord);
|
||||
|
||||
fragColor = vec4(clamp(deuteranopia * pixColor.rgb, 0.0, 1.0), pixColor.a);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Grayscale.
|
||||
//
|
||||
// Panama's accessibility color filters. Selected by the `colorFilter`
|
||||
// preference; hypr/looks.lua maps the enum to this path at config time and
|
||||
// services/SystemSettings.qml does the same live.
|
||||
//
|
||||
// Hyprland runs one fragment shader over the finished frame, so this covers
|
||||
// every window, the shell, video and the cursor alike.
|
||||
//
|
||||
// Rec. 709 luminance weights -- the same ones an SVG <feColorMatrix
|
||||
// type="saturate" values="0"> uses. A flat average would make reds and blues
|
||||
// far too bright and greens far too dark, because the eye does not weigh the
|
||||
// channels equally.
|
||||
|
||||
#version 300 es
|
||||
|
||||
precision mediump float;
|
||||
in vec2 v_texcoord;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
uniform sampler2D tex;
|
||||
|
||||
void main() {
|
||||
vec4 pixColor = texture(tex, v_texcoord);
|
||||
|
||||
float luminance = dot(pixColor.rgb, vec3(0.2126, 0.7152, 0.0722));
|
||||
|
||||
fragColor = vec4(vec3(luminance), pixColor.a);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Protanopia -- red-blind.
|
||||
//
|
||||
// Panama's accessibility color filters. Selected by the `colorFilter`
|
||||
// preference; hypr/looks.lua maps the enum to this path at config time and
|
||||
// services/SystemSettings.qml does the same live.
|
||||
//
|
||||
// The matrix is the feColorMatrix set the mock uses, which is the widely
|
||||
// carried HCIRN-derived one: it redistributes the red channel into the two the
|
||||
// eye can still separate, so a red/green pair that was one colour becomes two
|
||||
// distinguishable ones. It is a CORRECTION, not a simulation -- the point is to
|
||||
// make the screen readable, not to show what protanopia looks like.
|
||||
//
|
||||
// Row-major here, column-major to GLSL: mat3 takes its arguments column by
|
||||
// column, so the transpose below is the matrix as written in the SVG.
|
||||
|
||||
#version 300 es
|
||||
|
||||
precision mediump float;
|
||||
in vec2 v_texcoord;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
uniform sampler2D tex;
|
||||
|
||||
const mat3 protanopia = mat3(
|
||||
0.567, 0.558, 0.000,
|
||||
0.433, 0.442, 0.242,
|
||||
0.000, 0.000, 0.758
|
||||
);
|
||||
|
||||
void main() {
|
||||
vec4 pixColor = texture(tex, v_texcoord);
|
||||
|
||||
fragColor = vec4(clamp(protanopia * pixColor.rgb, 0.0, 1.0), pixColor.a);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Tritanopia -- blue-blind.
|
||||
//
|
||||
// Panama's accessibility color filters. Selected by the `colorFilter`
|
||||
// preference; hypr/looks.lua maps the enum to this path at config time and
|
||||
// services/SystemSettings.qml does the same live.
|
||||
//
|
||||
// Same family of matrices as protanopia.frag, weighted for the missing blue
|
||||
// cone. See that file for why this is a correction rather than a simulation,
|
||||
// and for the column-major note.
|
||||
|
||||
#version 300 es
|
||||
|
||||
precision mediump float;
|
||||
in vec2 v_texcoord;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
uniform sampler2D tex;
|
||||
|
||||
const mat3 tritanopia = mat3(
|
||||
0.950, 0.000, 0.000,
|
||||
0.050, 0.433, 0.475,
|
||||
0.000, 0.567, 0.525
|
||||
);
|
||||
|
||||
void main() {
|
||||
vec4 pixColor = texture(tex, v_texcoord);
|
||||
|
||||
fragColor = vec4(clamp(tritanopia * pixColor.rgb, 0.0, 1.0), pixColor.a);
|
||||
}
|
||||
Reference in New Issue
Block a user