hypr: fix per-workspace layout detection, refactor lua, event-driven quickshell

Lua config (hyprland.d.lua):
- keybinds: layout-aware binds now read the per-workspace layout via
  cur_ws_layout() instead of the global hl.get_config("general.layout"),
  fixing mouse-wheel/bracket scrolling and ratio keys on the lua:*-scroll
  layouts.
- add state.lua shared module (ws_layouts) replacing the _G globals.
- layout: factor the 9 duplicated layout_msg bodies into scroll_msg/swap_msg
  builders; drop a dead #state expression.
- rules: NO_BLUELIGHT window.open handler no longer leaks a rule per open
  (one per class) and regex-escapes/nil-guards the class.
- monitors: quote non-numeric scale so scale="auto" renders.
- drop debug print() focus handler, local-next shadowing, stray {mouse=true}
  on wheel binds (kept on drag/resize, which require it).

Quickshell:
- brightness OSD is now event-driven: Osd.qml gains an IpcHandler(target:osd)
  and the keybind pushes the new level via `qs ipc call osd brightness`,
  removing the always-on 500ms brightnessctl poll.
- GamemodePill watches GameMode's D-Bus signals via gdbus monitor instead of
  polling gamemoded --status every 5s.

Cleanup:
- remove stock hyprland.lua.refactor/ boilerplate and the redundant,
  partly-wrong hyprland_lua_api.md (both were deployed into ~/.config/hypr;
  .luarc.json already points the LSP at /usr/share/hypr/stubs).
- refresh hypr/AGENTS.md (lua layout) and quickshell/CLAUDE.md (v0.3.0).
This commit is contained in:
2026-06-01 13:06:59 +02:00
parent 6962fa393d
commit 11e4e94ee8
11 changed files with 119 additions and 724 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ This directory is the Hyprland portion of a chezmoi dotfiles source tree. Key pa
- `.chezmoiscripts/` holds chezmoi hooks such as `run_onchange_*`.
- Host-specific variants use `##hostname.<name>` (e.g., `hyprpaper.conf##hostname.owlenlap01`).
Hyprland configuration lives here in `hyprland.conf`, with modular includes under `hyprland.d/` and related configs like `hypridle.conf`, `hyprlock.conf.tmpl`, and `hyprpaper.conf.tmpl`.
Hyprland configuration uses the v0.55+ **Lua** config: entry point `hyprland.lua.tmpl` (rendered to `~/.config/hypr/hyprland.lua`), with modular includes under `hyprland.d.lua/` (`general.lua`, `layout.lua`, `keybinds.lua.tmpl`, etc.) plus the shared `state.lua` module. Related configs: `hypridle.conf.tmpl`, `hyprlock.conf.tmpl`, `hyprsunset.conf.tmpl`. The `.luarc.json` points the Lua LSP at the API stubs in `/usr/share/hypr/stubs`.
## Chezmoi Config (chezmoi.toml)
- Source of truth: `~/.config/chezmoi/chezmoi.toml` (not tracked here).
@@ -86,8 +86,8 @@ hl.bind(mainMod .. " + SHIFT + I", hl.dsp.workspace.move({ monitor = "l" }),
hl.bind(mainMod .. " + SHIFT + O", hl.dsp.workspace.move({ monitor = "r" }), { description = "Move workspace to right monitor" })
-- ─── Workspace layout state ───────────────────────────────────────────────────
local ws_layouts = {}
_G._hl_ws_layouts = ws_layouts
-- Shared with layout.lua (the window.active handler reads it for swap layouts).
local ws_layouts = require("state").ws_layouts
local ws_cycle_order = {
"master",
"lua:master-scroll",
@@ -123,8 +123,8 @@ end
local function ws_toggle_ms()
local cur = cur_ws_layout()
-- Any master variant → scrolling; scrolling/monocle → master
local next = (cur == "scrolling" or cur == "monocle") and "master" or "scrolling"
set_ws_layout(next)
local next_layout = (cur == "scrolling" or cur == "monocle") and "master" or "scrolling"
set_ws_layout(next_layout)
end
local function ws_cycle()
@@ -195,7 +195,7 @@ local mfact_layouts = {
}
local function layout_ratio(ratio)
return function()
local cur = hl.get_config("general.layout")
local cur = cur_ws_layout()
if mfact_layouts[cur] then
hl.dispatch(hl.dsp.layout("mfact exact " .. ratio))
elseif cur == "scrolling" then
@@ -205,7 +205,7 @@ local function layout_ratio(ratio)
end
local function layout_ratio_delta(delta)
return function()
local cur = hl.get_config("general.layout")
local cur = cur_ws_layout()
local sign = delta > 0 and "+" or ""
if mfact_layouts[cur] then
hl.dispatch(hl.dsp.layout("mfact " .. sign .. delta))
@@ -225,13 +225,13 @@ hl.bind(mainMod .. " + ALT + period", layout_ratio_delta(0.05), { description =
for i = 1, 9 do
local ratio = i / 10
hl.bind(mainMod .. " + CTRL + ALT + " .. i, function()
if hl.get_config("general.layout") == "scrolling" then
if cur_ws_layout() == "scrolling" then
hl.dispatch(hl.dsp.layout("colresize all " .. ratio))
end
end, { description = "All columns " .. (i * 10) .. "% (scrolling)" })
end
hl.bind(mainMod .. " + CTRL + ALT + 0", function()
if hl.get_config("general.layout") == "scrolling" then
if cur_ws_layout() == "scrolling" then
hl.dispatch(hl.dsp.layout("colresize all 0.95"))
end
end, { description = "All columns 95% (scrolling)" })
@@ -249,13 +249,6 @@ hl.bind(mainMod .. " + SHIFT + G", function()
end
end, { description = "Toggle gaps" })
-- Global Focus Notification (Lua Event)
hl.on("window.active", function(w)
if w ~= nil and w.title ~= nil then
print("Focused: " .. w.title)
end
end)
-- Vim-like navigation
hl.bind(mainMod .. " + H", hl.dsp.focus({ direction = "l" }), { description = "Focus left" })
hl.bind(mainMod .. " + L", hl.dsp.focus({ direction = "r" }), { description = "Focus right" })
@@ -354,13 +347,13 @@ local scroll_layouts = {
}
local function layout_scroll(dir)
return function()
if scroll_layouts[hl.get_config("general.layout")] then
if scroll_layouts[cur_ws_layout()] then
hl.dispatch(hl.dsp.layout(dir))
end
end
end
hl.bind(mainMod .. " + mouse_down", layout_scroll("scrollup"), { mouse = true })
hl.bind(mainMod .. " + mouse_up", layout_scroll("scrolldown"), { mouse = true })
hl.bind(mainMod .. " + mouse_down", layout_scroll("scrollup"))
hl.bind(mainMod .. " + mouse_up", layout_scroll("scrolldown"))
hl.bind(mainMod .. " + bracketright", layout_scroll("scrolldown"), { description = "Scroll slave pane down" })
hl.bind(mainMod .. " + bracketleft", layout_scroll("scrollup"), { description = "Scroll slave pane up" })
@@ -381,17 +374,13 @@ hl.bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), { locked = tr
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true, description = "Next track" })
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true, description = "Previous track" })
-- Brightness — let brightnessctl do the math; parse its -m output for the OSD.
-- Brightness — let brightnessctl do the math, then push the new percentage to
-- the Quickshell OSD (field 4 of `-m` output is the percentage, e.g. "70%").
local function brightness_step(arg)
return function()
local f = io.popen("brightnessctl -e4 -n2 -m s " .. arg .. " 2>/dev/null")
if not f then return end
local line = f:read("*l"); f:close()
local pct = line and line:match("[^,]*,[^,]*,[^,]*,(%d+)%%")
if pct then
hl.notification.create({ text = "Brightness: " .. pct .. "%", timeout = 1200, icon = "info" })
end
end
return hl.dsp.exec_cmd(
"brightnessctl -e4 -n2 s " .. arg ..
" >/dev/null && qs ipc call osd brightness \"$(brightnessctl -m | cut -d, -f4 | tr -d %)\""
)
end
hl.bind("XF86MonBrightnessUp", brightness_step("1%+"), { locked = true, repeating = true, description = "Brightness up (1%)" })
hl.bind("XF86MonBrightnessDown", brightness_step("1%-"), { locked = true, repeating = true, description = "Brightness down (1%)" })
@@ -419,10 +408,10 @@ local function get_scheduled_temperature()
end
end
_G._bluelight_enabled = true
local bluelight_enabled = true
local function toggle_bluelight()
_G._bluelight_enabled = not _G._bluelight_enabled
if _G._bluelight_enabled then
bluelight_enabled = not bluelight_enabled
if bluelight_enabled then
local target = get_scheduled_temperature()
if target == "identity" then
hl.dispatch(hl.dsp.exec_cmd("hyprctl hyprsunset identity"))
+41 -53
View File
@@ -82,7 +82,37 @@ local function place_scroll_col(state, slave_area, targets, slave_indices)
end
-- ─── Layout states ────────────────────────────────────────────────────────────
local mfact = 0.60 -- master width fraction (shared across single-master variants)
local shared = require("state") -- shared ws_layouts table (written by keybinds.lua)
local mfact = 0.60 -- master width fraction (horizontal single-master variants)
local mfact_v = 0.55 -- master height fraction (vertical variants)
-- mfact setters: clamp and write back to the shared upvalue.
local function set_mfact(v) if v then mfact = math.max(0.1, math.min(0.95, v)) end end
local function set_mfact_v(v) if v then mfact_v = math.max(0.1, math.min(0.95, v)) end end
-- layout_msg builder for single-column/row scroll layouts (master + N slaves).
local function scroll_msg(s, set_mf)
return function(ctx, msg)
local max_off = math.max(0, #ctx.targets - 1 - s.visible)
if msg == "scrolldown" then s.offset = math.min(s.offset + 1, max_off); return true
elseif msg == "scrollup" then s.offset = math.max(s.offset - 1, 0); return true
elseif msg == "reset" then s.offset = 0; return true
else
local v = msg:match("^mfact exact (.+)$")
if v then set_mf(tonumber(v)); return true end
end
end
end
-- layout_msg builder for swap-on-focus layouts (recalc + mfact only).
local function swap_msg(set_mf)
return function(_, msg)
if msg == "recalc" then return true end
local v = msg:match("^mfact exact (.+)$")
if v then set_mf(tonumber(v)); return true end
end
end
local ms = { visible=2, peek=0.10, offset=0, peek_top_addr=nil, peek_bottom_addr=nil }
local sm = { visible=2, peek=0.10, offset=0, peek_top_addr=nil, peek_bottom_addr=nil }
@@ -110,16 +140,7 @@ hl.layout.register("master-scroll", {
place_scroll_col(ms, slave_area, targets, idx)
end,
layout_msg = function(ctx, msg)
local max_off = math.max(0, #ctx.targets - 1 - ms.visible)
if msg == "scrolldown" then ms.offset = math.min(ms.offset + 1, max_off); return true
elseif msg == "scrollup" then ms.offset = math.max(ms.offset - 1, 0); return true
elseif msg == "reset" then ms.offset = 0; return true
else
local v = msg:match("^mfact exact (.+)$")
if v then mfact = math.max(0.1, math.min(0.95, tonumber(v) or mfact)); return true end
end
end,
layout_msg = scroll_msg(ms, set_mfact),
})
-- ─── slave-master-scroll: slaves left, master right ──────────────────────────
@@ -142,16 +163,7 @@ hl.layout.register("slave-master-scroll", {
place_scroll_col(sm, slave_area, targets, idx)
end,
layout_msg = function(ctx, msg)
local max_off = math.max(0, #ctx.targets - 1 - sm.visible)
if msg == "scrolldown" then sm.offset = math.min(sm.offset + 1, max_off); return true
elseif msg == "scrollup" then sm.offset = math.max(sm.offset - 1, 0); return true
elseif msg == "reset" then sm.offset = 0; return true
else
local v = msg:match("^mfact exact (.+)$")
if v then mfact = math.max(0.1, math.min(0.95, tonumber(v) or mfact)); return true end
end
end,
layout_msg = scroll_msg(sm, set_mfact),
})
-- ─── center-master-scroll: center master, both columns scroll ─────────────────
@@ -199,7 +211,6 @@ hl.layout.register("center-master-scroll", {
local addr = aw and aw.address
local function scroll_col(state, delta)
local max_off = math.max(0, #state - state.visible) -- recalculated below
-- Count windows in this column from targets
local col_n = 0
for i = 2, #ctx.targets do
@@ -226,7 +237,7 @@ hl.layout.register("center-master-scroll", {
cm_left.offset = 0; cm_right.offset = 0; return true
else
local v = msg:match("^mfact exact (.+)$")
if v then mfact = math.max(0.1, math.min(0.95, tonumber(v) or mfact)); return true end
if v then set_mfact(tonumber(v)); return true end
end
end,
})
@@ -289,8 +300,6 @@ local function place_scroll_row(state, slave_area, targets, slave_indices)
end
-- ─── Vertical layout states ───────────────────────────────────────────────────
local mfact_v = 0.55 -- master height fraction for vertical layouts
local tm = { visible=2, peek=0.10, offset=0, peek_top_addr=nil, peek_bottom_addr=nil }
local cm_v = { side_h=0.20, visible=2, peek=0.10 }
@@ -316,16 +325,7 @@ hl.layout.register("top-master-scroll", {
place_scroll_row(tm, slave_area, targets, idx)
end,
layout_msg = function(ctx, msg)
local max_off = math.max(0, #ctx.targets - 1 - tm.visible)
if msg == "scrolldown" then tm.offset = math.min(tm.offset + 1, max_off); return true
elseif msg == "scrollup" then tm.offset = math.max(tm.offset - 1, 0); return true
elseif msg == "reset" then tm.offset = 0; return true
else
local v = msg:match("^mfact exact (.+)$")
if v then mfact_v = math.max(0.1, math.min(0.95, tonumber(v) or mfact_v)); return true end
end
end,
layout_msg = scroll_msg(tm, set_mfact_v),
})
-- ─── center-master-scroll-v: center master, top and bottom slave rows ────────
@@ -395,7 +395,7 @@ hl.layout.register("center-master-scroll-v", {
cm_vtop.offset = 0; cm_vbot.offset = 0; return true
else
local v = msg:match("^mfact exact (.+)$")
if v then mfact_v = math.max(0.1, math.min(0.95, tonumber(v) or mfact_v)); return true end
if v then set_mfact_v(tonumber(v)); return true end
end
end,
})
@@ -444,11 +444,7 @@ hl.layout.register("master-swap", {
targets[midx]:place(master_area)
swap_place_slaves(targets, midx, slave_area, true)
end,
layout_msg = function(_, msg)
if msg == "recalc" then return true end
local v = msg:match("^mfact exact (.+)$")
if v then mfact = math.max(0.1, math.min(0.95, tonumber(v) or mfact)); return true end
end,
layout_msg = swap_msg(set_mfact),
})
-- ─── slave-master-swap: slaves left, master right ─────────────────────────────
@@ -463,11 +459,7 @@ hl.layout.register("slave-master-swap", {
targets[midx]:place(master_area)
swap_place_slaves(targets, midx, slave_area, true)
end,
layout_msg = function(_, msg)
if msg == "recalc" then return true end
local v = msg:match("^mfact exact (.+)$")
if v then mfact = math.max(0.1, math.min(0.95, tonumber(v) or mfact)); return true end
end,
layout_msg = swap_msg(set_mfact),
})
-- ─── top-master-swap: master top, slaves bottom row ───────────────────────────
@@ -482,11 +474,7 @@ hl.layout.register("top-master-swap", {
targets[midx]:place(master_area)
swap_place_slaves(targets, midx, slave_area, false)
end,
layout_msg = function(_, msg)
if msg == "recalc" then return true end
local v = msg:match("^mfact exact (.+)$")
if v then mfact_v = math.max(0.1, math.min(0.95, tonumber(v) or mfact_v)); return true end
end,
layout_msg = swap_msg(set_mfact_v),
})
-- ─── Auto-scroll on focus + swap-on-focus ────────────────────────────────────
@@ -505,9 +493,9 @@ hl.on("window.active", function(w)
end
-- Trigger recalc for swap layouts so the focused window becomes master.
-- _G._hl_ws_layouts is set by keybinds.lua and tracks per-workspace layout.
-- shared.ws_layouts is written by keybinds.lua and tracks per-workspace layout.
local ws_id = w.workspace and w.workspace.id
local cur = ws_id and _G._hl_ws_layouts and _G._hl_ws_layouts[ws_id]
local cur = ws_id and shared.ws_layouts[ws_id]
if cur == "lua:master-swap" or cur == "lua:slave-master-swap" or cur == "lua:top-master-swap" then
hl.dispatch(hl.dsp.layout("recalc"))
end
@@ -3,7 +3,7 @@ hl.monitor({
output = "{{ .name }}",
mode = "{{ .width }}x{{ .height }}@{{ .refresh_rate }}",
position = "{{ .position }}",
scale = {{ .scale }}
scale = {{ if kindIs "string" .scale }}"{{ .scale }}"{{ else }}{{ .scale }}{{ end }}
{{- if hasKey . "vrr" }},
vrr = {{ .vrr }}
{{- end }}
+13 -5
View File
@@ -91,12 +91,20 @@ local function get_process_env(pid, name)
return nil
end
-- Escape regex metacharacters so classes like "org.mozilla.firefox" match literally.
local function regex_escape(s)
return (s:gsub("([%^%$%.%|%?%*%+%(%)%[%]%{%}\\])", "\\%1"))
end
-- window_rule registrations persist, so only add one per class (otherwise rules
-- accumulate on every matching window.open).
local bluelight_bypassed = {}
hl.on("window.open", function(w)
if w.pid and w.pid > 0 then
local no_bl = get_process_env(w.pid, "NO_BLUELIGHT")
if no_bl == "1" then
hl.window_rule({ match = { class = "^" .. w.class .. "$" }, content = "game" })
end
if not (w.pid and w.pid > 0 and w.class) then return end
if bluelight_bypassed[w.class] then return end
if get_process_env(w.pid, "NO_BLUELIGHT") == "1" then
bluelight_bypassed[w.class] = true
hl.window_rule({ match = { class = "^" .. regex_escape(w.class) .. "$" }, content = "game" })
end
end)
+9
View File
@@ -0,0 +1,9 @@
-- Shared mutable state across hyprland.d.lua modules.
-- `require` caches the module, so every requirer sees the same tables.
local M = {}
-- Per-workspace layout name, keyed by workspace id.
-- Written by keybinds.lua (set_ws_layout); read by layout.lua (window.active).
M.ws_layouts = {}
return M
@@ -1,366 +0,0 @@
-- This is an example Hyprland Lua config file.
-- Refer to the wiki for more information.
-- https://wiki.hypr.land/Configuring/Start/
-- Please note not all available settings / options are set here.
-- For a full list, see the wiki
-- You can (and should!!) split this configuration into multiple files
-- Create your files separately and then require them like this:
-- require("myColors")
------------------
---- MONITORS ----
------------------
-- See https://wiki.hypr.land/Configuring/Basics/Monitors/
hl.monitor({
output = "",
mode = "preferred",
position = "auto",
scale = "auto",
})
---------------------
---- MY PROGRAMS ----
---------------------
-- Set programs that you use
local terminal = "kitty"
local fileManager = "dolphin"
local menu = "hyprlauncher"
-------------------
---- AUTOSTART ----
-------------------
-- See https://wiki.hypr.land/Configuring/Basics/Autostart/
-- Autostart necessary processes (like notifications daemons, status bars, etc.)
-- Or execute your favorite apps at launch like this:
--
-- hl.on("hyprland.start", function ()
-- hl.exec_cmd(terminal)
-- hl.exec_cmd("nm-applet")
-- hl.exec_cmd("waybar & hyprpaper & firefox")
-- end)
-------------------------------
---- ENVIRONMENT VARIABLES ----
-------------------------------
-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Environment-variables/
hl.env("XCURSOR_SIZE", "24")
hl.env("HYPRCURSOR_SIZE", "24")
-----------------------
----- PERMISSIONS -----
-----------------------
-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Permissions/
-- Please note permission changes here require a Hyprland restart and are not applied on-the-fly
-- for security reasons
-- hl.config({
-- ecosystem = {
-- enforce_permissions = true,
-- },
-- })
-- hl.permission("/usr/(bin|local/bin)/grim", "screencopy", "allow")
-- hl.permission("/usr/(lib|libexec|lib64)/xdg-desktop-portal-hyprland", "screencopy", "allow")
-- hl.permission("/usr/(bin|local/bin)/hyprpm", "plugin", "allow")
-----------------------
---- LOOK AND FEEL ----
-----------------------
-- Refer to https://wiki.hypr.land/Configuring/Basics/Variables/
hl.config({
general = {
gaps_in = 5,
gaps_out = 20,
border_size = 2,
col = {
active_border = { colors = { "rgba(33ccffee)", "rgba(00ff99ee)" }, angle = 45 },
inactive_border = "rgba(595959aa)",
},
-- Set to true to enable resizing windows by clicking and dragging on borders and gaps
resize_on_border = false,
-- Please see https://wiki.hypr.land/Configuring/Advanced-and-Cool/Tearing/ before you turn this on
allow_tearing = false,
layout = "dwindle",
},
decoration = {
rounding = 10,
rounding_power = 2,
-- Change transparency of focused and unfocused windows
active_opacity = 1.0,
inactive_opacity = 1.0,
shadow = {
enabled = true,
range = 4,
render_power = 3,
color = 0xee1a1a1a,
},
blur = {
enabled = true,
size = 3,
passes = 1,
vibrancy = 0.1696,
},
},
animations = {
enabled = true,
},
})
-- Default curves and animations, see https://wiki.hypr.land/Configuring/Advanced-and-Cool/Animations/
hl.curve("easeOutQuint", { type = "bezier", points = { { 0.23, 1 }, { 0.32, 1 } } })
hl.curve("easeInOutCubic", { type = "bezier", points = { { 0.65, 0.05 }, { 0.36, 1 } } })
hl.curve("linear", { type = "bezier", points = { { 0, 0 }, { 1, 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 } } })
-- Default springs
hl.curve("easy", { type = "spring", mass = 1, stiffness = 71.2633, dampening = 15.8273644 })
hl.animation({ leaf = "global", enabled = true, speed = 10, bezier = "default" })
hl.animation({ leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" })
hl.animation({ leaf = "windows", enabled = true, speed = 4.79, spring = "easy" })
hl.animation({ leaf = "windowsIn", enabled = true, speed = 4.1, spring = "easy", style = "popin 87%" })
hl.animation({ leaf = "windowsOut", enabled = true, speed = 1.49, bezier = "linear", style = "popin 87%" })
hl.animation({ leaf = "fadeIn", enabled = true, speed = 1.73, bezier = "almostLinear" })
hl.animation({ leaf = "fadeOut", enabled = true, speed = 1.46, bezier = "almostLinear" })
hl.animation({ leaf = "fade", enabled = true, speed = 3.03, bezier = "quick" })
hl.animation({ leaf = "layers", enabled = true, speed = 3.81, bezier = "easeOutQuint" })
hl.animation({ leaf = "layersIn", enabled = true, speed = 4, bezier = "easeOutQuint", style = "fade" })
hl.animation({ leaf = "layersOut", enabled = true, speed = 1.5, bezier = "linear", style = "fade" })
hl.animation({ leaf = "fadeLayersIn", enabled = true, speed = 1.79, bezier = "almostLinear" })
hl.animation({ leaf = "fadeLayersOut", enabled = true, speed = 1.39, bezier = "almostLinear" })
hl.animation({ leaf = "workspaces", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" })
hl.animation({ leaf = "workspacesIn", enabled = true, speed = 1.21, bezier = "almostLinear", style = "fade" })
hl.animation({ leaf = "workspacesOut", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" })
hl.animation({ leaf = "zoomFactor", enabled = true, speed = 7, bezier = "quick" })
-- Ref https://wiki.hypr.land/Configuring/Basics/Workspace-Rules/
-- "Smart gaps" / "No gaps when only"
-- uncomment all if you wish to use that.
-- hl.workspace_rule({ workspace = "w[tv1]", gaps_out = 0, gaps_in = 0 })
-- hl.workspace_rule({ workspace = "f[1]", gaps_out = 0, gaps_in = 0 })
-- hl.window_rule({
-- name = "no-gaps-wtv1",
-- match = { float = false, workspace = "w[tv1]" },
-- border_size = 0,
-- rounding = 0,
-- })
-- hl.window_rule({
-- name = "no-gaps-f1",
-- match = { float = false, workspace = "f[1]" },
-- border_size = 0,
-- rounding = 0,
-- })
-- See https://wiki.hypr.land/Configuring/Layouts/Dwindle-Layout/ for more
hl.config({
dwindle = {
preserve_split = true, -- You probably want this
},
})
-- See https://wiki.hypr.land/Configuring/Layouts/Master-Layout/ for more
hl.config({
master = {
new_status = "master",
},
})
-- See https://wiki.hypr.land/Configuring/Layouts/Scrolling-Layout/ for more
hl.config({
scrolling = {
fullscreen_on_one_column = true,
},
})
----------------
---- MISC ----
----------------
hl.config({
misc = {
force_default_wallpaper = -1, -- Set to 0 or 1 to disable the anime mascot wallpapers
disable_hyprland_logo = false, -- If true disables the random hyprland logo / anime girl background. :(
},
})
---------------
---- INPUT ----
---------------
hl.config({
input = {
kb_layout = "us",
kb_variant = "",
kb_model = "",
kb_options = "",
kb_rules = "",
follow_mouse = 1,
sensitivity = 0, -- -1.0 - 1.0, 0 means no modification.
touchpad = {
natural_scroll = false,
},
},
})
hl.gesture({
fingers = 3,
direction = "horizontal",
action = "workspace",
})
-- Example per-device config
-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Devices/ for more
hl.device({
name = "epic-mouse-v1",
sensitivity = -0.5,
})
---------------------
---- KEYBINDINGS ----
---------------------
local mainMod = "SUPER" -- Sets "Windows" key as main modifier
-- Example binds, see https://wiki.hypr.land/Configuring/Basics/Binds/ for more
hl.bind(mainMod .. " + Q", hl.dsp.exec_cmd(terminal))
local closeWindowBind = hl.bind(mainMod .. " + C", hl.dsp.window.close())
-- closeWindowBind:set_enabled(false)
hl.bind(
mainMod .. " + M",
hl.dsp.exec_cmd("command -v hyprshutdown >/dev/null 2>&1 && hyprshutdown || hyprctl dispatch 'hl.dsp.exit()'")
)
hl.bind(mainMod .. " + E", hl.dsp.exec_cmd(fileManager))
hl.bind(mainMod .. " + V", hl.dsp.window.float({ action = "toggle" }))
hl.bind(mainMod .. " + R", hl.dsp.exec_cmd(menu))
hl.bind(mainMod .. " + P", hl.dsp.window.pseudo())
hl.bind(mainMod .. " + J", hl.dsp.layout("togglesplit")) -- dwindle only
-- Move focus with mainMod + arrow keys
hl.bind(mainMod .. " + left", hl.dsp.focus({ direction = "left" }))
hl.bind(mainMod .. " + right", hl.dsp.focus({ direction = "right" }))
hl.bind(mainMod .. " + up", hl.dsp.focus({ direction = "up" }))
hl.bind(mainMod .. " + down", hl.dsp.focus({ direction = "down" }))
-- Switch workspaces with mainMod + [0-9]
-- Move active window to a workspace with mainMod + SHIFT + [0-9]
for i = 1, 10 do
local key = i % 10 -- 10 maps to key 0
hl.bind(mainMod .. " + " .. key, hl.dsp.focus({ workspace = i }))
hl.bind(mainMod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i }))
end
-- Example special workspace (scratchpad)
hl.bind(mainMod .. " + S", hl.dsp.workspace.toggle_special("magic"))
hl.bind(mainMod .. " + SHIFT + S", hl.dsp.window.move({ workspace = "special:magic" }))
-- Scroll through existing workspaces with mainMod + scroll
hl.bind(mainMod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" }))
hl.bind(mainMod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" }))
-- Move/resize windows with mainMod + LMB/RMB and dragging
hl.bind(mainMod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
hl.bind(mainMod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true })
-- Laptop multimedia keys for volume and LCD brightness
hl.bind(
"XF86AudioRaiseVolume",
hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+"),
{ locked = true, repeating = true }
)
hl.bind(
"XF86AudioLowerVolume",
hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"),
{ locked = true, repeating = true }
)
hl.bind(
"XF86AudioMute",
hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"),
{ locked = true, repeating = true }
)
hl.bind(
"XF86AudioMicMute",
hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"),
{ locked = true, repeating = true }
)
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%+"), { locked = true, repeating = true })
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%-"), { locked = true, repeating = true })
-- Requires playerctl
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true })
hl.bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true })
--------------------------------
---- WINDOWS AND WORKSPACES ----
--------------------------------
-- See https://wiki.hypr.land/Configuring/Basics/Window-Rules/
-- and https://wiki.hypr.land/Configuring/Basics/Workspace-Rules/
-- Example window rules that are useful
local suppressMaximizeRule = hl.window_rule({
-- Ignore maximize requests from all apps. You'll probably like this.
name = "suppress-maximize-events",
match = { class = ".*" },
suppress_event = "maximize",
})
-- suppressMaximizeRule:set_enabled(false)
hl.window_rule({
-- Fix some dragging issues with XWayland
name = "fix-xwayland-drags",
match = {
class = "^$",
title = "^$",
xwayland = true,
float = true,
fullscreen = false,
pin = false,
},
no_focus = true,
})
-- Layer rules also return a handle.
-- local overlayLayerRule = hl.layer_rule({
-- name = "no-anim-overlay",
-- match = { namespace = "^my-overlay$" },
-- no_anim = true,
-- })
-- overlayLayerRule:set_enabled(false)
-- Hyprland-run windowrule
hl.window_rule({
name = "move-hyprland-run",
match = { class = "hyprland-run" },
move = "20 monitor_h-120",
float = true,
})
-233
View File
@@ -1,233 +0,0 @@
# Hyprland Lua API Documentation
## 1. Introduction
Since Hyprland v0.55, the traditional `hyprlang` syntax has been deprecated in favor of a powerful **Lua configuration system**. The configuration file is located at `~/.config/hypr/hyprland.lua`. This system allows for dynamic scripting, logic-based configurations, and better integration with external tools through Lua's standard libraries and Hyprland's internal API (`hl`).
## 2. Global Configuration (`hl.config`)
The `hl.config` function is used to set global variables. It accepts a table where keys correspond to configuration sections.
```lua
hl.config({
general = {
border_size = 2,
gaps_in = 5,
gaps_out = 20,
layout = "dwindle",
["col.active_border"] = "rgba(33ccffee) rgba(00ff99ee) 45deg",
["col.inactive_border"] = "rgba(595959aa)"
},
decoration = {
rounding = 10,
blur = {
enabled = true,
size = 3,
passes = 1
},
shadow = {
enabled = true,
range = 4,
render_power = 3,
color = "rgba(1a1a1aee)"
}
},
input = {
kb_layout = "us",
follow_mouse = 1,
touchpad = {
natural_scroll = false
}
},
gestures = {
workspace_swipe = true
},
misc = {
force_default_wallpaper = 0,
disable_hyprland_logo = true
}
})
```
### Main Categories
* **General**: `border_size`, `gaps_in`, `gaps_out`, `layout`, `col.active_border`, `col.inactive_border`, `allow_tearing`, `no_focus_fallback`.
* **Decoration**: `rounding`, `rounding_power`, `active_opacity`, `inactive_opacity`, `blur` (subcategory), `shadow` (subcategory), `glow` (subcategory).
* **Animations**: `enabled`, `workspace_wraparound`.
* **Input**: `kb_layout`, `kb_model`, `sensitivity`, `repeat_rate`, `follow_mouse`, `touchpad` (subcategory), `tablet` (subcategory).
* **Gestures**: `workspace_swipe_distance`, `workspace_swipe_touch`, `workspace_swipe_create_new`.
* **Group**: `auto_group`, `insert_after_current`, `col.border_active`, `groupbar` (subcategory).
* **Misc**: `disable_hyprland_logo`, `vrr`, `animate_manual_resizes`, `enable_swallow`, `focus_on_activate`.
* **Binds**: `pass_mouse_when_bound`, `scroll_event_delay`, `workspace_back_and_forth`.
* **XWayland**: `enabled`, `use_nearest_neighbor`, `force_zero_scaling`.
* **Render**: `direct_scanout`, `ctm_animation`, `cm_enabled`.
* **Cursor**: `no_hardware_cursors`, `zoom_factor`, `inactive_timeout`, `hide_on_key_press`.
* **Ecosystem**: `no_update_news`, `no_donation_nag`, `enforce_permissions`.
## 3. Monitors (`hl.monitor`)
Monitors are configured using the `hl.monitor` function.
**Syntax:**
```lua
hl.monitor({
output = "DP-1", -- Name or "desc:..."
mode = "1920x1080@144", -- Resolution@Hz or "preferred", "highres", "highrr"
position = "0x0", -- Layout position or "auto"
scale = 1, -- Scale factor or "auto"
transform = 0, -- Rotation (0-7)
disabled = false, -- Whether to disable the output
mirror = "DP-2", -- Mirror another output
bitdepth = 10, -- 8 or 10
vrr = 1 -- VRR mode
})
```
## 4. Window Rules (`hl.window_rule`)
Window rules allow targeting windows by properties to apply static or dynamic effects.
**Syntax:**
```lua
hl.window_rule({
name = "my-rule", -- Optional for handle management
match = {
class = "kitty",
title = ".*term.*",
floating = true
},
opacity = "0.8",
rounding = 10
})
```
### Props (Match fields)
`class`, `title`, `initial_class`, `initial_title`, `tag`, `xwayland`, `floating`, `fullscreen`, `focus`, `group`, `modal`, `workspace`.
### Effects
* **Static**: `float`, `tile`, `fullscreen`, `move`, `size`, `center`, `pseudo`, `workspace`, `monitor`, `pin`.
* **Dynamic**: `opacity`, `border_color`, `rounding`, `animation`, `no_blur`, `idle_inhibit`, `dim_around`, `no_anim`, `stay_focused`.
## 5. Layer Rules (`hl.layer_rule`)
Layer rules target Wayland layers like waybar, rofi, and wallpapers.
```lua
hl.layer_rule({
match = { namespace = "waybar" },
blur = true,
ignore_alpha = 0.5
})
```
## 6. Workspace Rules (`hl.workspace_rule`)
Workspace rules define behavior for specific workspaces.
```lua
hl.workspace_rule({
workspace = "3",
monitor = "DP-1",
default = true,
persistent = true,
gaps_in = 0,
gaps_out = 0,
no_border = true
})
```
## 7. Keybinds (`hl.bind`, `hl.unbind`, `hl.define_submap`)
Keybinds are created using `hl.bind`.
**Syntax:**
```lua
hl.bind(keys, dispatcher, flags)
```
**Examples:**
```lua
-- Simple command
hl.bind("SUPER + Q", hl.dsp.exec_cmd("kitty"))
-- Lua function with logic
hl.bind("SUPER + T", function()
local win = hl.get_active_window()
if win then print(win.title) end
end)
-- Flags
hl.bind("SUPER + L", hl.dsp.exec_cmd("swaylock"), { locked = true })
```
### Submaps
```lua
hl.define_submap("resize", function()
hl.bind("right", hl.dsp.window.resize({ x = 10, y = 0, relative = true }), { repeating = true })
hl.bind("escape", hl.dsp.submap("reset"))
end)
hl.bind("ALT + R", hl.dsp.submap("resize"))
```
## 8. Dispatchers (`hl.dsp.*`)
Dispatchers perform compositor actions.
| Category | Dispatchers |
|---|---|
| **General** | `exec_cmd(cmd, rules?)`, `focus({ direction/window/monitor/workspace })`, `exit()`, `dpms({ action })`, `global(string)` |
| **Window** | `close()`, `kill()`, `float({ action })`, `fullscreen({ mode, action })`, `move({ direction/workspace/monitor/coords })`, `resize({ x, y, relative })`, `pin()`, `tag({ tag })` |
| **Workspace** | `rename({ workspace, name })`, `move({ monitor })`, `toggle_special(name)` |
| **Group** | `toggle()`, `next()`, `prev()`, `active(index)`, `lock({ action })` |
## 9. Events (`hl.on`)
Define callbacks for compositor events.
```lua
hl.on("window.open", function(window)
print("New window opened: " .. window.class)
end)
```
**Events**: `hyprland.start`, `window.active`, `workspace.active`, `monitor.added`, `config.reloaded`.
## 10. Timers (`hl.timer`)
Timers for delayed or repeating actions.
```lua
local myTimer = hl.timer(function()
hl.notification.create({ text = "Timer fired!" })
end, { timeout = 1000, type = "oneshot" })
```
## 11. Convenience Functions
* `hl.get_active_window()`
* `hl.get_active_workspace()`
* `hl.get_monitors()`
* `hl.get_config(key)`
* `hl.get_cursor_pos()`
## 12. Environment Variables (`hl.env`)
Set environment variables.
```lua
hl.env("QT_QPA_PLATFORM", "wayland;xcb")
hl.env("XCURSOR_SIZE", "24")
```
## 13. Custom Layouts (`hl.layout.register`)
Register custom Lua-based layouts.
## 14. Permissions (`hl.permission`)
Control sensitive application access.
```lua
hl.permission({ binary = "/usr/bin/grim", type = "screencopy", mode = "allow" })
```
## 15. Plugins
Configure and interact with plugins.
```lua
if hl.plugin.csgo_vulkan_fix ~= nil then
hl.config({ plugin = { csgo_vulkan_fix = { fix_mouse = false } } })
end
```
## 16. Animations & Curves (`hl.animation`, `hl.curve`)
Define easing curves and apply them to the animation tree.
```lua
hl.curve("myBezier", { type = "bezier", points = { {0.1, 1}, {1, 0.1} } })
hl.animation({ leaf = "windows", enabled = true, speed = 8, curve = "myBezier", style = "popin 80%" })
```
+10 -1
View File
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## What This Is
A Quickshell (v0.2.x) desktop shell configuration for Hyprland on Arch Linux. It renders a vertical bar on the right edge of the screen with popout panels. Written entirely in QML using Quickshell's extended Qt Quick modules.
A Quickshell (v0.3.0) desktop shell configuration for Hyprland on Arch Linux. It renders a vertical bar on the right edge of the screen with popout panels. Written entirely in QML using Quickshell's extended Qt Quick modules.
## Running / Reloading
@@ -53,9 +53,18 @@ Each popout follows the same structure:
`SystemPopout` polls system stats via `Process` + `StdioCollector`:
- CPU, memory, temperature, GPU (via `scripts/gpu.sh`), disk, network, updates
- Different refresh intervals: 5s (CPU/mem/temp/GPU), 30s (disk/net), 5min (updates)
- These timers only run while the popout is open — the `PopoutSlot` Loader
(`active: isOpen || animating`) destroys the component when closed.
Weather uses `curl wttr.in` with a 30-minute refresh timer.
**Prefer event-driven over polling** where a source exists:
- **OSD** (`osd/Osd.qml`) — volume/mic via PipeWire `Connections`; brightness via
`IpcHandler { target: "osd" }` (`brightness(pct)`), pushed by the Hyprland
brightness keybinds with `qs ipc call osd brightness <0-100>` (no polling).
- **Gamemode** (`bar/GamemodePill.qml`) — a long-lived `gdbus monitor` Process on
`com.feralinteractive.GameMode` re-checks status on register/unregister signals.
### Quickshell-Specific Patterns
- `Variants { model: Quickshell.screens }` — per-screen window instantiation
+13 -6
View File
@@ -4,8 +4,9 @@ import QtQuick
import QtQuick.Layouts
import "../shared" as Shared
// Gamemode indicator — visible only when gamemode is active
// Polls `gamemoded --status` every 5 seconds
// Gamemode indicator — visible only when gamemode is active.
// Event-driven: re-checks status when GameMode emits a D-Bus register/unregister
// signal (via `gdbus monitor`), instead of polling on a timer.
BarPill {
id: root
@@ -37,11 +38,17 @@ BarPill {
function poll() { gameProc.running = false; gameProc.running = true; }
Timer {
interval: 5000
// Long-lived monitor on GameMode's session-bus object. Each GameRegistered /
// GameUnregistered signal prints a line; re-check status only then.
Process {
id: monitor
running: true
repeat: true
onTriggered: root.poll()
command: ["gdbus", "monitor", "-e",
"-d", "com.feralinteractive.GameMode",
"-o", "/com/feralinteractive/GameMode"]
stdout: SplitParser {
onRead: line => { if (line.indexOf("Registered") >= 0) root.poll() }
}
}
Component.onCompleted: root.poll()
+11 -27
View File
@@ -32,6 +32,17 @@ Scope {
onTriggered: root.osdVisible = false
}
// Brightness is event-driven: the Hyprland brightness keybinds set the level
// and then push the new percentage here via `qs ipc call osd brightness <0-100>`.
// (Volume/mic remain event-driven via the PipeWire Connections below.)
IpcHandler {
target: "osd"
function brightness(pct: string): void {
let frac = Math.max(0, Math.min(1, (parseFloat(pct) || 0) / 100));
root.showOsd("\u{f00df}", frac, false, "Brightness");
}
}
// Event-driven audio change detection
property var sinkAudio: Pipewire.defaultAudioSink?.audio ?? null
property var sourceAudio: Pipewire.defaultAudioSource?.audio ?? null
@@ -60,33 +71,6 @@ Scope {
}
}
// Brightness monitoring via brightnessctl (auto-disables if no backlight device)
property real lastBrightness: -1
property bool hasBrightness: false
Process {
id: brightProc
command: ["brightnessctl", "-m"]
stdout: StdioCollector {
onStreamFinished: {
let parts = this.text.trim().split(",");
if (parts.length >= 5) {
root.hasBrightness = true;
let pct = parseInt(parts[4]) / 100;
if (root.lastBrightness >= 0 && Math.abs(pct - root.lastBrightness) > 0.005)
root.showOsd("\u{f00df}", pct, false, "Brightness");
root.lastBrightness = pct;
}
}
}
}
Timer {
interval: 500
running: root.hasBrightness
repeat: true
onTriggered: { brightProc.running = false; brightProc.running = true; }
}
Component.onCompleted: { brightProc.running = true; }
Variants {
model: Quickshell.screens