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 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, }) -- ─── 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 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, }) -- ─── 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 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, }) -- ─── Scrolling slave row helper (horizontal) ───────────────────────────────── -- Mirror of place_scroll_col but scrolls left↔right instead of up↔down. -- state fields: visible, peek, offset, peek_top_addr (left peek), peek_bottom_addr (right peek) local function place_scroll_row(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 w = slave_area.w / n for j = 1, n do targets[slave_indices[j]]:place({ x = slave_area.x + (j - 1) * w, y = slave_area.y, w = w, h = slave_area.h, }) end return end local has_left = state.offset > 0 local has_right = state.offset < max_off local left_f = has_left and state.peek or 0 local right_f = has_right and state.peek or 0 -- w chosen so left_peek + visible*w + right_peek == slave_area.w exactly local w = slave_area.w / (state.visible + left_f + right_f) if has_left then local wi = targets[slave_indices[state.offset]].window if wi then state.peek_top_addr = wi.address end end if has_right then local ti = slave_indices[state.offset + state.visible + 1] if ti then local wi = targets[ti].window if wi then state.peek_bottom_addr = wi.address end end end for j = 1, n do local t = targets[slave_indices[j]] if has_left and j == state.offset then t:place({ x=slave_area.x - w*(1-state.peek), y=slave_area.y, w=w, h=slave_area.h }) elseif has_right and j == state.offset + state.visible + 1 then t:place({ x=slave_area.x + (left_f + state.visible)*w, y=slave_area.y, w=w, h=slave_area.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 + (left_f + k)*w, y=slave_area.y, w=w, h=slave_area.h }) else t:set_box({ x=slave_area.x + slave_area.w + w, y=slave_area.y, w=w, h=slave_area.h }) end end 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 } local cm_vtop = { visible=cm_v.visible, peek=cm_v.peek, offset=0, peek_top_addr=nil, peek_bottom_addr=nil } local cm_vbot = { visible=cm_v.visible, peek=cm_v.peek, offset=0, peek_top_addr=nil, peek_bottom_addr=nil } local cm_vtop_addrs = {} local cm_vbot_addrs = {} -- ─── top-master-scroll: master top, slaves bottom row ──────────────────────── hl.layout.register("top-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, "bottom", 1.0 - mfact_v) local master_area = { x=ctx.area.x, y=ctx.area.y, w=ctx.area.w, h=slave_area.y-ctx.area.y } targets[1]:place(master_area) local idx = {} for i = 2, n do idx[#idx+1] = i end 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, }) -- ─── center-master-scroll-v: center master, top and bottom slave rows ──────── -- Slaves alternate: odd (1,3,5…) → top row, even (2,4,6…) → bottom row. -- scrolldown/scrollup scrolls whichever row the active window is in. hl.layout.register("center-master-scroll-v", { recalculate = function(ctx) local targets = ctx.targets local n = #targets cm_vtop_addrs = {} cm_vbot_addrs = {} if n == 0 then return end if n == 1 then targets[1]:place(ctx.area); return end local top_area = ctx:split(ctx.area, "top", cm_v.side_h) local bot_area = ctx:split(ctx.area, "bottom", cm_v.side_h) local master_area = { x = ctx.area.x, y = top_area.y + top_area.h, w = ctx.area.w, h = bot_area.y - (top_area.y + top_area.h), } targets[1]:place(master_area) local top_idx, bot_idx = {}, {} for i = 2, n do local slave_pos = i - 1 if slave_pos % 2 == 1 then top_idx[#top_idx+1] = i else bot_idx[#bot_idx+1] = i end local w = targets[i].window if w then if slave_pos % 2 == 1 then cm_vtop_addrs[w.address] = true else cm_vbot_addrs[w.address] = true end end end place_scroll_row(cm_vtop, top_area, targets, top_idx) place_scroll_row(cm_vbot, bot_area, targets, bot_idx) end, layout_msg = function(ctx, msg) local aw = hl.get_active_window() local addr = aw and aw.address local function scroll_row(state, col_addrs, delta) local col_n = 0 for i = 2, #ctx.targets do local w = ctx.targets[i].window if w and col_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_vtop_addrs[addr] then return scroll_row(cm_vtop, cm_vtop_addrs, 1) end if addr and cm_vbot_addrs[addr] then return scroll_row(cm_vbot, cm_vbot_addrs, 1) end elseif msg == "scrollup" then if addr and cm_vtop_addrs[addr] then return scroll_row(cm_vtop, cm_vtop_addrs, -1) end 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, }) -- ─── Swap-on-focus layouts ──────────────────────────────────────────────────── -- Focusing any window promotes it to master. recalculate reads the active -- window directly via hl.get_active_window() — no external address tracking. local function swap_master_idx(targets) local aw = hl.get_active_window() if aw and aw.address then for i = 1, #targets do local w = targets[i].window if w and w.address == aw.address then return i end end end return 1 end local function swap_place_slaves(targets, midx, slave_area, vertical) local sidx = {} for i = 1, #targets do if i ~= midx then sidx[#sidx+1] = i end end if #sidx == 0 then return end if vertical then local h = slave_area.h / #sidx for j, si in ipairs(sidx) do targets[si]:place({ x=slave_area.x, y=slave_area.y+(j-1)*h, w=slave_area.w, h=h }) end else local w = slave_area.w / #sidx for j, si in ipairs(sidx) do targets[si]:place({ x=slave_area.x+(j-1)*w, y=slave_area.y, w=w, h=slave_area.h }) end end end -- ─── master-swap: master left, slaves right ─────────────────────────────────── hl.layout.register("master-swap", { recalculate = function(ctx) local targets, n = ctx.targets, #ctx.targets if n == 0 then return end if n == 1 then targets[1]:place(ctx.area); return end local midx = swap_master_idx(targets) 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[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, }) -- ─── slave-master-swap: slaves left, master right ───────────────────────────── hl.layout.register("slave-master-swap", { recalculate = function(ctx) local targets, n = ctx.targets, #ctx.targets if n == 0 then return end if n == 1 then targets[1]:place(ctx.area); return end local midx = swap_master_idx(targets) 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[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, }) -- ─── top-master-swap: master top, slaves bottom row ─────────────────────────── hl.layout.register("top-master-swap", { recalculate = function(ctx) local targets, n = ctx.targets, #ctx.targets if n == 0 then return end if n == 1 then targets[1]:place(ctx.area); return end local midx = swap_master_idx(targets) local slave_area = ctx:split(ctx.area, "bottom", 1.0 - mfact_v) local master_area = { x=ctx.area.x, y=ctx.area.y, w=ctx.area.w, h=slave_area.y-ctx.area.y } 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, }) -- ─── Auto-scroll on focus + swap-on-focus ──────────────────────────────────── local all_col_states = { ms, sm, cm_left, cm_right, tm, cm_vtop, cm_vbot } hl.on("window.active", function(w) if w == nil or w.address == nil then return end -- Auto-scroll: if the focused window is a peek slot, slide it into view 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 -- 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. 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] 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 end)