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)