From 11e4e94ee88c9399e5eee9e21d37479abb433d0b Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Mon, 1 Jun 2026 13:06:59 +0200 Subject: [PATCH] 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). --- dot_config/hypr/AGENTS.md | 2 +- .../hypr/hyprland.d.lua/keybinds.lua.tmpl | 51 +-- dot_config/hypr/hyprland.d.lua/layout.lua | 94 ++--- .../hypr/hyprland.d.lua/monitors.lua.tmpl | 2 +- dot_config/hypr/hyprland.d.lua/rules.lua.tmpl | 18 +- dot_config/hypr/hyprland.d.lua/state.lua | 9 + .../hypr/hyprland.lua.refactor/hyprland.lua | 366 ------------------ dot_config/hypr/hyprland_lua_api.md | 233 ----------- dot_config/quickshell/CLAUDE.md | 11 +- dot_config/quickshell/bar/GamemodePill.qml | 19 +- dot_config/quickshell/osd/Osd.qml | 38 +- 11 files changed, 119 insertions(+), 724 deletions(-) create mode 100644 dot_config/hypr/hyprland.d.lua/state.lua delete mode 100644 dot_config/hypr/hyprland.lua.refactor/hyprland.lua delete mode 100644 dot_config/hypr/hyprland_lua_api.md diff --git a/dot_config/hypr/AGENTS.md b/dot_config/hypr/AGENTS.md index 2c00497..9bb2439 100644 --- a/dot_config/hypr/AGENTS.md +++ b/dot_config/hypr/AGENTS.md @@ -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.` (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). diff --git a/dot_config/hypr/hyprland.d.lua/keybinds.lua.tmpl b/dot_config/hypr/hyprland.d.lua/keybinds.lua.tmpl index d4f0e4f..c52ebc7 100644 --- a/dot_config/hypr/hyprland.d.lua/keybinds.lua.tmpl +++ b/dot_config/hypr/hyprland.d.lua/keybinds.lua.tmpl @@ -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")) diff --git a/dot_config/hypr/hyprland.d.lua/layout.lua b/dot_config/hypr/hyprland.d.lua/layout.lua index 27b1dbc..23e8e46 100644 --- a/dot_config/hypr/hyprland.d.lua/layout.lua +++ b/dot_config/hypr/hyprland.d.lua/layout.lua @@ -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 diff --git a/dot_config/hypr/hyprland.d.lua/monitors.lua.tmpl b/dot_config/hypr/hyprland.d.lua/monitors.lua.tmpl index 3c49866..1b5531c 100644 --- a/dot_config/hypr/hyprland.d.lua/monitors.lua.tmpl +++ b/dot_config/hypr/hyprland.d.lua/monitors.lua.tmpl @@ -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 }} diff --git a/dot_config/hypr/hyprland.d.lua/rules.lua.tmpl b/dot_config/hypr/hyprland.d.lua/rules.lua.tmpl index 1c2d0fa..241f1ee 100644 --- a/dot_config/hypr/hyprland.d.lua/rules.lua.tmpl +++ b/dot_config/hypr/hyprland.d.lua/rules.lua.tmpl @@ -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) diff --git a/dot_config/hypr/hyprland.d.lua/state.lua b/dot_config/hypr/hyprland.d.lua/state.lua new file mode 100644 index 0000000..144a876 --- /dev/null +++ b/dot_config/hypr/hyprland.d.lua/state.lua @@ -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 diff --git a/dot_config/hypr/hyprland.lua.refactor/hyprland.lua b/dot_config/hypr/hyprland.lua.refactor/hyprland.lua deleted file mode 100644 index 985267a..0000000 --- a/dot_config/hypr/hyprland.lua.refactor/hyprland.lua +++ /dev/null @@ -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, -}) diff --git a/dot_config/hypr/hyprland_lua_api.md b/dot_config/hypr/hyprland_lua_api.md deleted file mode 100644 index 46d9631..0000000 --- a/dot_config/hypr/hyprland_lua_api.md +++ /dev/null @@ -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%" }) -``` diff --git a/dot_config/quickshell/CLAUDE.md b/dot_config/quickshell/CLAUDE.md index 12a2378..1ae46af 100644 --- a/dot_config/quickshell/CLAUDE.md +++ b/dot_config/quickshell/CLAUDE.md @@ -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 diff --git a/dot_config/quickshell/bar/GamemodePill.qml b/dot_config/quickshell/bar/GamemodePill.qml index df4f18b..c2dc052 100644 --- a/dot_config/quickshell/bar/GamemodePill.qml +++ b/dot_config/quickshell/bar/GamemodePill.qml @@ -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() diff --git a/dot_config/quickshell/osd/Osd.qml b/dot_config/quickshell/osd/Osd.qml index 298a020..ae3b9d6 100644 --- a/dot_config/quickshell/osd/Osd.qml +++ b/dot_config/quickshell/osd/Osd.qml @@ -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