Files
dotfiles/dot_config/hypr/hyprland.d.lua/layout.lua
T
mpuchstein d53bc5dadb hypr: migrate to Lua config (v0.55) with custom scroll layouts
Replace hyprlang hyprland.d/ with Lua-based hyprland.d.lua/ modules:
- theme.lua.tmpl: apex-neon + apex-aeon color tables, col.* bracket keys
- general.lua: config, bezier curves, animations (bezier=/spring= fixed)
- monitors.lua.tmpl, workspaces.lua.tmpl, input.lua.tmpl, rules.lua.tmpl
- keybinds.lua.tmpl: +/SHIFT/CTRL format, monitor focus (Super+I/O),
  scroll binds for custom layouts
- layout.lua: master-scroll, slave-master-scroll, center-master-scroll
  (peek-hint scrolling slave columns, focus-triggered auto-scroll)

Entry point uses package.path + require() for per-file error isolation.
Old hyprlang configs preserved under hyprland.conf.bak/.
Add .luarc.json for hyprland stub autocompletion in editors.
2026-05-12 01:00:42 +02:00

240 lines
9.1 KiB
Lua

hl.config({
master = {
orientation = "left",
mfact = 0.60,
new_status = "slave",
new_on_top = true,
new_on_active = "after",
allow_small_split = true,
special_scale_factor = 0.8,
drop_at_cursor = true
},
scrolling = {
fullscreen_on_one_column = true,
focus_fit_method = 0,
follow_focus = true,
follow_min_visible = 0.1,
column_width = 0.7
}
})
-- ─── Scrolling slave column helper ───────────────────────────────────────────
-- Shared by all master-scroll variants.
-- state = { visible, peek, offset, peek_top_addr, peek_bottom_addr }
-- slave_area = HL.Box for this column
-- targets = ctx.targets
-- slave_indices = ordered list of indices into `targets` for this column
local function place_scroll_col(state, slave_area, targets, slave_indices)
local n = #slave_indices
state.peek_top_addr = nil
state.peek_bottom_addr = nil
if n == 0 then return end
local max_off = math.max(0, n - state.visible)
state.offset = math.max(0, math.min(state.offset, max_off))
if n <= state.visible then
local h = slave_area.h / n
for j = 1, n do
targets[slave_indices[j]]:place({
x = slave_area.x, y = slave_area.y + (j - 1) * h,
w = slave_area.w, h = h,
})
end
return
end
local has_top = state.offset > 0
local has_bot = state.offset < max_off
local top_f = has_top and state.peek or 0
local bot_f = has_bot and state.peek or 0
-- h chosen so top_peek + visible*h + bot_peek == slave_area.h exactly
local h = slave_area.h / (state.visible + top_f + bot_f)
if has_top then
local w = targets[slave_indices[state.offset]].window
if w then state.peek_top_addr = w.address end
end
if has_bot then
local ti = slave_indices[state.offset + state.visible + 1]
if ti then
local w = targets[ti].window
if w then state.peek_bottom_addr = w.address end
end
end
for j = 1, n do
local t = targets[slave_indices[j]]
if has_top and j == state.offset then
-- Peek at top: extends above slave_area; safe because master is in a different x-range
t:place({ x=slave_area.x, y=slave_area.y - h*(1-state.peek), w=slave_area.w, h=h })
elseif has_bot and j == state.offset + state.visible + 1 then
-- Peek at bottom: extends below the last visible slot
t:place({ x=slave_area.x, y=slave_area.y + (top_f + state.visible)*h, w=slave_area.w, h=h })
elseif j >= state.offset + 1 and j <= state.offset + state.visible then
local k = j - state.offset - 1
t:place({ x=slave_area.x, y=slave_area.y + (top_f + k)*h, w=slave_area.w, h=h })
else
-- Fully off-screen: park below work area
t:set_box({ x=slave_area.x, y=slave_area.y + slave_area.h + h, w=slave_area.w, h=h })
end
end
end
-- ─── Layout states ────────────────────────────────────────────────────────────
local mfact = 0.60 -- master width fraction (shared across single-master variants)
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 }
local cm = { side_w=0.20, visible=2, peek=0.10 }
local cm_left = { visible=cm.visible, peek=cm.peek, offset=0, peek_top_addr=nil, peek_bottom_addr=nil }
local cm_right = { visible=cm.visible, peek=cm.peek, offset=0, peek_top_addr=nil, peek_bottom_addr=nil }
local cm_left_addrs = {}
local cm_right_addrs = {}
-- ─── master-scroll: master left, slaves right ─────────────────────────────────
hl.layout.register("master-scroll", {
recalculate = function(ctx)
local targets = ctx.targets
local n = #targets
if n == 0 then return end
if n == 1 then targets[1]:place(ctx.area); return end
local slave_area = ctx:split(ctx.area, "right", 1.0 - mfact)
local master_area = { x=ctx.area.x, y=ctx.area.y, w=slave_area.x-ctx.area.x, h=ctx.area.h }
targets[1]:place(master_area)
local idx = {}
for i = 2, n do idx[#idx+1] = i end
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
end
end,
})
-- ─── slave-master-scroll: slaves left, master right ──────────────────────────
hl.layout.register("slave-master-scroll", {
recalculate = function(ctx)
local targets = ctx.targets
local n = #targets
if n == 0 then return end
if n == 1 then targets[1]:place(ctx.area); return end
local slave_area = ctx:split(ctx.area, "left", 1.0 - mfact)
local master_area = {
x = slave_area.x + slave_area.w, y = ctx.area.y,
w = ctx.area.w - slave_area.w, h = ctx.area.h,
}
targets[1]:place(master_area)
local idx = {}
for i = 2, n do idx[#idx+1] = i end
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
end
end,
})
-- ─── center-master-scroll: center master, both columns scroll ─────────────────
-- Slaves alternate: odd (1,3,5…) → left column, even (2,4,6…) → right column.
-- scrolldown/scrollup applies to whichever column the active window is in.
hl.layout.register("center-master-scroll", {
recalculate = function(ctx)
local targets = ctx.targets
local n = #targets
cm_left_addrs = {}
cm_right_addrs = {}
if n == 0 then return end
if n == 1 then targets[1]:place(ctx.area); return end
local left_area = ctx:split(ctx.area, "left", cm.side_w)
local right_area = ctx:split(ctx.area, "right", cm.side_w)
local master_area = {
x = left_area.x + left_area.w, y = ctx.area.y,
w = right_area.x - (left_area.x + left_area.w), h = ctx.area.h,
}
targets[1]:place(master_area)
local left_idx, right_idx = {}, {}
for i = 2, n do
local slave_pos = i - 1 -- 1-indexed slave number
if slave_pos % 2 == 1 then
left_idx[#left_idx+1] = i
else
right_idx[#right_idx+1] = i
end
local w = targets[i].window
if w then
if slave_pos % 2 == 1 then cm_left_addrs[w.address] = true
else cm_right_addrs[w.address] = true
end
end
end
place_scroll_col(cm_left, left_area, targets, left_idx)
place_scroll_col(cm_right, right_area, targets, right_idx)
end,
layout_msg = function(ctx, msg)
local aw = hl.get_active_window()
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
local w = ctx.targets[i].window
if w and ((state == cm_left and cm_left_addrs[w.address])
or (state == cm_right and cm_right_addrs[w.address])) then
col_n = col_n + 1
end
end
local col_max = math.max(0, col_n - state.visible)
if delta > 0 then state.offset = math.min(state.offset + 1, col_max)
else state.offset = math.max(state.offset - 1, 0)
end
return true
end
if msg == "scrolldown" then
if addr and cm_left_addrs[addr] then return scroll_col(cm_left, 1) end
if addr and cm_right_addrs[addr] then return scroll_col(cm_right, 1) end
elseif msg == "scrollup" then
if addr and cm_left_addrs[addr] then return scroll_col(cm_left, -1) end
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
end
end,
})
-- ─── Auto-scroll on focus ─────────────────────────────────────────────────────
-- When a peek-slot window gets focus (Super+J/K or mouse click on the strip),
-- dispatch the appropriate scroll so it slides into full view.
local all_col_states = { ms, sm, cm_left, cm_right }
hl.on("window.active", function(w)
if w == nil or w.address == nil then return end
for _, state in ipairs(all_col_states) do
if w.address == state.peek_bottom_addr then
hl.dispatch(hl.dsp.layout("scrolldown")); return
elseif w.address == state.peek_top_addr then
hl.dispatch(hl.dsp.layout("scrollup")); return
end
end
end)