hypr: replace hypr-workspace-layout shell script with native Lua

Port all layout management to Hyprland v0.55 Lua API:
- Per-workspace layout toggle/cycle via hl.workspace_rule + hl.get_active_window()
- Layout-aware move/nav/resize as pure Lua functions
- Group smart-join via hl.get_active_window().grouped
- mfact exact handlers in all custom scroll/swap layouts
- No io.popen, no exec_cmd, no IPC deadlock risk
This commit is contained in:
2026-05-12 03:48:46 +02:00
parent 7b1c53cd64
commit 9c7bf54cf1
2 changed files with 216 additions and 36 deletions
+186 -33
View File
@@ -10,6 +10,9 @@ local filemanager = "uwsm app -- nautilus"
local launcher = "uwsm app -- owlry -p app,cmd,system,ssh"
local clipman = "uwsm app -- owlry -m clipboard"
local browser = "uwsm app -- firefox"
local browserprv = "uwsm app -- firefox --private-window"
local browsernewinst = "uwsm app -- firefox --new-instance"
local altbrowser = "uwsm app -- chromium"
local taskman = "uwsm app -- owlry -m uuctl"
local pwdmgr = "uwsm app -- bitwarden-desktop"
local soundctl = "uwsm app -- pwvucontrol"
@@ -24,7 +27,10 @@ hl.bind(mainMod .. " + SHIFT + Return", hl.dsp.exec_cmd(term_tmux))
hl.bind(mainMod .. " + CTRL + Return", hl.dsp.exec_cmd(term_tmux_append))
hl.bind(mainMod .. " + F1", hl.dsp.exec_cmd("hypr-show-binds"))
hl.bind(mainMod .. " + E", hl.dsp.exec_cmd(filemanager))
hl.bind(mainMod .. " + W", hl.dsp.exec_cmd(browser))
hl.bind(mainMod .. " + W", hl.dsp.exec_cmd(browser))
hl.bind(mainMod .. " + SHIFT + W", hl.dsp.exec_cmd(browserprv))
hl.bind(mainMod .. " + CTRL + W", hl.dsp.exec_cmd(altbrowser))
hl.bind(mainMod .. " + ALT + W", hl.dsp.exec_cmd(browsernewinst))
hl.bind(mainMod .. " + Space", hl.dsp.exec_cmd(launcher))
-- Secondary launchers
@@ -59,7 +65,7 @@ hl.bind(mainMod .. " + End", hl.dsp.exec_cmd("hyprlock"))
hl.bind(mainMod .. " + SHIFT + End", hl.dsp.exec_cmd("owlry-power-menu"))
-- Window management
hl.bind(mainMod .. " + Q", hl.dsp.window.kill())
hl.bind(mainMod .. " + Q", hl.dsp.window.close())
hl.bind(mainMod .. " + SHIFT + Q", hl.dsp.window.kill())
hl.bind(mainMod .. " + F", hl.dsp.window.float())
hl.bind(mainMod .. " + SHIFT + F", hl.dsp.window.fullscreen({ action = "toggle" }))
@@ -78,9 +84,140 @@ hl.bind(mainMod .. " + O", hl.dsp.focus({ monitor = "r" }))
hl.bind(mainMod .. " + SHIFT + I", hl.dsp.workspace.move({ monitor = "l" }))
hl.bind(mainMod .. " + SHIFT + O", hl.dsp.workspace.move({ monitor = "r" }))
-- ─── Workspace layout state ───────────────────────────────────────────────────
local ws_layouts = {}
local ws_cycle_order = { "dwindle", "master", "scrolling", "monocle" }
local function active_ws_id()
local w = hl.get_active_window()
return w and w.workspace and w.workspace.id
end
local function cur_ws_layout()
local id = active_ws_id()
return (id and ws_layouts[id]) or "master"
end
local function set_ws_layout(layout)
local id = active_ws_id()
hl.notification.create({ text = "Layout: " .. layout, timeout = 1800, icon = "info" })
if not id then return end
ws_layouts[id] = layout
hl.workspace_rule({ workspace = tostring(id), layout = layout })
end
local function ws_toggle_ms()
set_ws_layout(cur_ws_layout() == "master" and "scrolling" or "master")
end
local function ws_cycle()
local cur = cur_ws_layout()
local next_layout = ws_cycle_order[1]
for i, v in ipairs(ws_cycle_order) do
if v == cur then
next_layout = ws_cycle_order[(i % #ws_cycle_order) + 1]
break
end
end
set_ws_layout(next_layout)
end
local function move_window(dir)
return function()
local cur = cur_ws_layout()
if cur == "scrolling" and (dir == "l" or dir == "r") then
hl.dispatch(hl.dsp.layout("swapcol " .. dir))
elseif cur ~= "monocle" then
hl.dispatch(hl.dsp.window.move({ direction = dir }))
end
end
end
local function nav(dir)
return function()
local cur = cur_ws_layout()
if cur == "scrolling" then
hl.dispatch(hl.dsp.layout("focus " .. dir))
elseif cur == "master" and dir == "d" then
hl.dispatch(hl.dsp.layout("addmaster"))
elseif cur == "master" and dir == "u" then
hl.dispatch(hl.dsp.layout("removemaster"))
else
hl.dispatch(hl.dsp.focus({ direction = dir }))
end
end
end
local function resize_h(sign)
return function()
local cur = cur_ws_layout()
if cur == "scrolling" then
hl.dispatch(hl.dsp.layout("colresize " .. sign .. "0.1"))
elseif cur ~= "monocle" then
hl.dispatch(hl.dsp.window.resize({ x = sign == "+" and 25 or -25, y = 0, relative = true }))
end
end
end
-- Layout
hl.bind(mainMod .. " + comma", hl.dsp.exec_cmd("hypr-workspace-layout toggle-ms"))
hl.bind(mainMod .. " + period", hl.dsp.exec_cmd("hypr-workspace-layout cycle"))
hl.bind(mainMod .. " + comma", ws_toggle_ms)
hl.bind(mainMod .. " + period", ws_cycle)
-- Split ratio (ALT+1-9 = 10%-90%, ALT+0 = 95%)
-- mfact exact for master/custom layouts, colresize for built-in scrolling
local mfact_layouts = {
["master"] = true,
["lua:master-scroll"] = true,
["lua:slave-master-scroll"] = true,
["lua:center-master-scroll"] = true,
["lua:top-master-scroll"] = true,
["lua:center-master-scroll-v"] = true,
["lua:master-swap"] = true,
["lua:slave-master-swap"] = true,
["lua:top-master-swap"] = true,
}
local function layout_ratio(ratio)
return function()
local cur = hl.get_config("general.layout")
if mfact_layouts[cur] then
hl.dispatch(hl.dsp.layout("mfact exact " .. ratio))
elseif cur == "scrolling" then
hl.dispatch(hl.dsp.layout("colresize " .. ratio))
end
end
end
local function layout_ratio_delta(delta)
return function()
local cur = hl.get_config("general.layout")
local sign = delta > 0 and "+" or ""
if mfact_layouts[cur] then
hl.dispatch(hl.dsp.layout("mfact " .. sign .. delta))
elseif cur == "scrolling" then
hl.dispatch(hl.dsp.layout("colresize " .. sign .. delta))
end
end
end
for i = 1, 9 do
hl.bind(mainMod .. " + ALT + " .. i, layout_ratio(i / 10))
end
hl.bind(mainMod .. " + ALT + 0", layout_ratio(0.95))
hl.bind(mainMod .. " + ALT + comma", layout_ratio_delta(-0.05))
hl.bind(mainMod .. " + ALT + period", layout_ratio_delta(0.05))
-- Scrolling layout: resize ALL columns
for i = 1, 9 do
local ratio = i / 10
hl.bind(mainMod .. " + CTRL + ALT + " .. i, function()
if hl.get_config("general.layout") == "scrolling" then
hl.dispatch(hl.dsp.layout("colresize all " .. ratio))
end
end)
end
hl.bind(mainMod .. " + CTRL + ALT + 0", function()
if hl.get_config("general.layout") == "scrolling" then
hl.dispatch(hl.dsp.layout("colresize all 0.95"))
end
end)
-- Smart Gaps Toggle
hl.bind(mainMod .. " + SHIFT + G", function()
@@ -108,20 +245,35 @@ hl.bind(mainMod .. " + L", hl.dsp.focus({ direction = "r" }))
hl.bind(mainMod .. " + K", hl.dsp.focus({ direction = "u" }))
hl.bind(mainMod .. " + J", hl.dsp.focus({ direction = "d" }))
-- Move window
hl.bind(mainMod .. " + SHIFT + H", hl.dsp.exec_cmd("hypr-workspace-layout move-left"))
hl.bind(mainMod .. " + SHIFT + L", hl.dsp.exec_cmd("hypr-workspace-layout move-right"))
hl.bind(mainMod .. " + SHIFT + K", hl.dsp.exec_cmd("hypr-workspace-layout move-up"))
hl.bind(mainMod .. " + SHIFT + J", hl.dsp.exec_cmd("hypr-workspace-layout move-down"))
-- Move window (layout-aware)
hl.bind(mainMod .. " + SHIFT + H", move_window("l"))
hl.bind(mainMod .. " + SHIFT + L", move_window("r"))
hl.bind(mainMod .. " + SHIFT + K", move_window("u"))
hl.bind(mainMod .. " + SHIFT + J", move_window("d"))
-- Resize submap
hl.bind(mainMod .. " + R", hl.dsp.submap("resize"))
hl.define_submap("resize", function()
hl.bind("h", hl.dsp.window.resize({ x = -25, y = 0, relative = true }), { repeating = true })
hl.bind("l", hl.dsp.window.resize({ x = 25, y = 0, relative = true }), { repeating = true })
hl.bind("k", hl.dsp.window.resize({ x = 0, y = -25, relative = true }), { repeating = true })
hl.bind("j", hl.dsp.window.resize({ x = 0, y = 25, relative = true }), { repeating = true })
hl.bind("h", hl.dsp.window.resize({ x = -25, y = 0, relative = true }), { repeating = true })
hl.bind("l", hl.dsp.window.resize({ x = 25, y = 0, relative = true }), { repeating = true })
hl.bind("k", hl.dsp.window.resize({ x = 0, y = -25, relative = true }), { repeating = true })
hl.bind("j", hl.dsp.window.resize({ x = 0, y = 25, relative = true }), { repeating = true })
hl.bind("SHIFT + h", hl.dsp.window.resize({ x = -60, y = 0, relative = true }), { repeating = true })
hl.bind("SHIFT + l", hl.dsp.window.resize({ x = 60, y = 0, relative = true }), { repeating = true })
hl.bind("SHIFT + k", hl.dsp.window.resize({ x = 0, y = -60, relative = true }), { repeating = true })
hl.bind("SHIFT + j", hl.dsp.window.resize({ x = 0, y = 60, relative = true }), { repeating = true })
hl.bind("Escape", hl.dsp.submap("reset"))
hl.bind("Return", hl.dsp.submap("reset"))
end)
-- Zoom submap
hl.bind(mainMod .. " + M", hl.dsp.submap("zoom"))
hl.define_submap("zoom", function()
hl.bind("equal", hl.dsp.exec_cmd("hypr-zoom-step +0.2"))
hl.bind("minus", hl.dsp.exec_cmd("hypr-zoom-step -0.2"))
hl.bind("0", hl.dsp.exec_cmd("hypr-zoom-step reset"))
hl.bind("Escape", hl.dsp.submap("reset"))
hl.bind("Return", hl.dsp.submap("reset"))
end)
-- Workspace cycling
@@ -137,16 +289,10 @@ hl.bind(mainMod .. " + 0", hl.dsp.focus({ workspace = 30 }))
hl.bind(mainMod .. " + SHIFT + 0", hl.dsp.window.move({ workspace = 30 }))
-- Groups
local function is_grouped()
local h = io.popen("hyprctl activewindow -j 2>/dev/null")
if not h then return false end
local out = h:read("*a"); h:close()
local arr = out:match('"grouped":%[(.-)%]')
return arr ~= nil and arr ~= "" and arr ~= "null"
end
local function smart_group(dir)
if is_grouped() then
local w = hl.get_active_window()
local grouped = w and type(w.grouped) == "table" and #w.grouped > 0
if grouped then
hl.dispatch(hl.dsp.window.move({ out_of_group = true }))
else
hl.dispatch(hl.dsp.window.move({ into_or_create_group = dir }))
@@ -167,14 +313,14 @@ hl.bind(mainMod .. " + Z", hl.dsp.group.next())
hl.bind(mainMod .. " + SHIFT + Z", hl.dsp.group.prev())
-- Layout-aware navigation
hl.bind(mainMod .. " + ALT + H", hl.dsp.exec_cmd("hypr-workspace-layout nav-prev"))
hl.bind(mainMod .. " + ALT + L", hl.dsp.exec_cmd("hypr-workspace-layout nav-next"))
hl.bind(mainMod .. " + ALT + J", hl.dsp.exec_cmd("hypr-workspace-layout nav-down"))
hl.bind(mainMod .. " + ALT + K", hl.dsp.exec_cmd("hypr-workspace-layout nav-up"))
hl.bind(mainMod .. " + ALT + Tab", hl.dsp.exec_cmd("hypr-workspace-layout nav-next"))
hl.bind(mainMod .. " + ALT + SHIFT + Tab", hl.dsp.exec_cmd("hypr-workspace-layout nav-prev"))
hl.bind(mainMod .. " + ALT + SHIFT + J", hl.dsp.exec_cmd("hypr-workspace-layout resize-shrink-h"))
hl.bind(mainMod .. " + ALT + SHIFT + K", hl.dsp.exec_cmd("hypr-workspace-layout resize-grow-h"))
hl.bind(mainMod .. " + ALT + H", nav("l"))
hl.bind(mainMod .. " + ALT + L", nav("r"))
hl.bind(mainMod .. " + ALT + J", nav("d"))
hl.bind(mainMod .. " + ALT + K", nav("u"))
hl.bind(mainMod .. " + ALT + Tab", nav("r"))
hl.bind(mainMod .. " + ALT + SHIFT + Tab", nav("l"))
hl.bind(mainMod .. " + ALT + SHIFT + J", resize_h("-"))
hl.bind(mainMod .. " + ALT + SHIFT + K", resize_h("+"))
-- Mouse binds
hl.bind(mainMod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
@@ -207,6 +353,13 @@ hl.bind(mainMod .. " + Print", hl.dsp.exec_cmd("owlry-screenshot-menu"))
hl.bind("SHIFT + Print", hl.dsp.exec_cmd("uwsm app -- kitty --class=scrrec -e wf-recorder -f ~/Videos/scrrec.mkv -y -g \"$(slurp)\""))
-- Multimedia
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"), { repeating = true })
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { repeating = true })
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { locked = true })
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"), { repeating = true })
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { repeating = true })
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { locked = true })
hl.bind("SHIFT + XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SOURCE@ 5%+"), { repeating = true })
hl.bind("SHIFT + XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SOURCE@ 5%-"), { repeating = true })
hl.bind("SHIFT + XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"), { locked = true })
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
hl.bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true })
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true })
+30 -3
View File
@@ -115,6 +115,9 @@ hl.layout.register("master-scroll", {
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,
})
@@ -144,6 +147,9 @@ hl.layout.register("slave-master-scroll", {
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,
})
@@ -218,6 +224,9 @@ hl.layout.register("center-master-scroll", {
if addr and cm_right_addrs[addr] then return scroll_col(cm_right, -1) end
elseif msg == "reset" then
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
end
end,
})
@@ -312,6 +321,9 @@ hl.layout.register("top-master-scroll", {
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,
})
@@ -381,6 +393,9 @@ hl.layout.register("center-master-scroll-v", {
if addr and cm_vbot_addrs[addr] then return scroll_row(cm_vbot, cm_vbot_addrs, -1) end
elseif msg == "reset" then
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
end
end,
})
@@ -422,7 +437,11 @@ hl.layout.register("master-swap", {
targets[si]:place({ x=slave_area.x, y=slave_area.y+(j-1)*h, w=slave_area.w, h=h })
end
end,
layout_msg = function(_, msg) if msg == "recalc" then return true end 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,
})
-- ─── slave-master-swap: slaves left, master right ─────────────────────────────
@@ -448,7 +467,11 @@ hl.layout.register("slave-master-swap", {
targets[si]:place({ x=slave_area.x, y=slave_area.y+(j-1)*h, w=slave_area.w, h=h })
end
end,
layout_msg = function(_, msg) if msg == "recalc" then return true end 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,
})
-- ─── top-master-swap: master top, slaves bottom row ───────────────────────────
@@ -471,7 +494,11 @@ hl.layout.register("top-master-swap", {
targets[si]:place({ x=slave_area.x+(j-1)*w, y=slave_area.y, w=w, h=slave_area.h })
end
end,
layout_msg = function(_, msg) if msg == "recalc" then return true end 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,
})
-- ─── Auto-scroll on focus + swap-on-focus ────────────────────────────────────