-- Layout registry for the hyprlua session. -- -- Adding a layout is a one-file drop-in: create `layouts/.lua` returning a -- spec table (see scrolling.lua for the shape) and it is auto-discovered here — no -- other wiring. Exposes the global `layouts` table used by hyprland.lua (to pick the -- default) and by the astal-menu popup (over `hyprctl eval`) to switch a workspace's -- layout live. A JSON manifest is written for the menu to enumerate the choices. local M = { registry = {}, order = {}, current = {} } -- current: ws(str) -> name local HOME = os.getenv("HOME") local DIR = HOME .. "/.config/hypr/layouts" local MANIFEST = HOME .. "/.cache/astal-menu/layouts.json" local STATE = HOME .. "/.cache/astal-menu/layouts-state.json" local function scan() local p = io.popen("ls '" .. DIR .. "' 2>/dev/null") if not p then return {} end local names = {} for line in p:lines() do local n = line:match("^(.+)%.lua$") if n and n ~= "init" then names[#names + 1] = n end end p:close() table.sort(names) return names end local function jstr(s) return '"' .. tostring(s):gsub('\\', '\\\\'):gsub('"', '\\"') .. '"' end -- Write layouts.json so the menu can list {name,label,directional,dirs,default_dir}. function M.write_manifest() local items = {} for _, name in ipairs(M.order) do local s = M.registry[name] local dirs = {} for _, d in ipairs(s.dirs or {}) do dirs[#dirs + 1] = jstr(d) end local dlabels = {} for _, d in ipairs(s.dir_labels or {}) do dlabels[#dlabels + 1] = jstr(d) end items[#items + 1] = table.concat({ "{", '"name":', jstr(s.name), ',"label":', jstr(s.label or s.name), ',"directional":', tostring(s.directional or false), ',"dirs":[', table.concat(dirs, ","), "]", ',"dir_labels":[', table.concat(dlabels, ","), "]", ',"default_dir":', jstr(s.default_dir or ""), ',"fit_method":', tostring(s.fit_method or false), "}", }) end os.execute("mkdir -p '" .. MANIFEST:match("(.+)/[^/]+$") .. "' 2>/dev/null") local f = io.open(MANIFEST, "w") if f then f:write("[" .. table.concat(items, ",") .. "]\n"); f:close() end end function M.load() for _, name in ipairs(scan()) do local ok, spec = pcall(require, "layouts." .. name) if ok and type(spec) == "table" and spec.name then if not M.registry[spec.name] then M.order[#M.order + 1] = spec.name end M.registry[spec.name] = spec end end M.write_manifest() return M end -- Global default layout (called once from hyprland.lua). function M.set_default(name) local s = M.registry[name]; if not s then return end hl.config({ general = { layout = s.backend } }) if s.config then hl.config(s.config) end end -- Switch one workspace's layout live (called by the menu via hyprctl eval). -- `dir` is applied for directional layouts (scrolling.direction is a global option, -- so this affects every scrolling workspace — hyprland has no per-ws direction). local function write_state() local parts = {} for ws, name in pairs(M.current) do parts[#parts + 1] = jstr(ws) .. ":" .. jstr(name) end local f = io.open(STATE, "w") if f then f:write("{" .. table.concat(parts, ",") .. "}\n"); f:close() end end function M.set(ws, name, dir) local s = M.registry[name]; if not s then return end ws = tostring(ws) hl.workspace_rule({ workspace = ws, layout = s.backend }) if s.lua then -- custom Lua layout: orientation/options are per-layout state, set via a message -- to the active workspace's layout (not the global scrolling:direction option). if dir and dir ~= "" then hl.dispatch(hl.dsp.layout("orient " .. dir)) end elseif s.directional and dir and dir ~= "" then hl.config({ scrolling = { direction = dir } }) end M.current[ws] = name -- remember so the menu can show the real per-ws layout write_state() end -- Toggle the scrolling "centered follow" (focus_fit_method 0=fit / 1=centered). -- column_width stays < 1.0 (set in scrolling.lua) so neighbours remain clickable. function M.set_fit(method) hl.config({ scrolling = { focus_fit_method = tonumber(method) or 0 } }) end _G.layouts = M.load() return M