Skip to content

Instantly share code, notes, and snippets.

@al3rez
Created April 1, 2026 17:02
Show Gist options
  • Select an option

  • Save al3rez/9f6e225df6fa06cac3a457da7ffaf4c3 to your computer and use it in GitHub Desktop.

Select an option

Save al3rez/9f6e225df6fa06cac3a457da7ffaf4c3 to your computer and use it in GitHub Desktop.

Revisions

  1. al3rez created this gist Apr 1, 2026.
    287 changes: 287 additions & 0 deletions .wezterm-equalize.lua
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,287 @@
    local wezterm = require("wezterm")
    local action = wezterm.action

    -- Equalize all panes: each column gets equal width, each row within a column gets
    -- equal height. Weights are column/row group counts so mixed split orientations
    -- produce the expected N-ary equal-split layout.
    --
    -- WezTerm's AdjustPaneSize targets the nearest ancestor split in the internal tree,
    -- which may differ from our reconstructed tree (multiple binary trees produce the same
    -- pixel layout). We probe with +1 adjustments to discover which boundary each pane
    -- actually targets, verifying by checking that panes on BOTH sides of the intended
    -- boundary are affected. This ensures adjustments hit the correct split bar.
    --
    -- BUG FIXES:
    -- - hsplit total calculation: was tc.height + tc.height, now tc.height + bc.height
    -- - verification panes: must use near_pane (adjacent to boundary) not far_pane
    -- For vsplit: left_near (rightmost of left) and right_near (leftmost of right)
    -- For hsplit: top_near (bottommost of top) and bot_near (topmost of bottom)
    local function equalize_tab(window)
    local tab = window:active_tab()
    local initial_panes = tab:panes_with_info()
    if #initial_panes <= 1 then
    return
    end

    local active_idx = 0
    for _, pi in ipairs(initial_panes) do
    if pi.is_active then
    active_idx = pi.index
    end
    end

    -- Reconstruct the binary split tree from pane positions.
    local function build_tree(ps)
    if #ps == 1 then
    return { type = "pane", pane = ps[1], width = ps[1].width, height = ps[1].height }
    end

    local xs = {}
    for _, p in ipairs(ps) do xs[p.left + p.width] = true end
    local xs_sorted = {}
    for x in pairs(xs) do table.insert(xs_sorted, x) end
    table.sort(xs_sorted)
    for _, x in ipairs(xs_sorted) do
    local left_ps, right_ps = {}, {}
    for _, p in ipairs(ps) do
    if p.left + p.width <= x then table.insert(left_ps, p)
    elseif p.left >= x then table.insert(right_ps, p) end
    end
    if #left_ps + #right_ps == #ps and #left_ps > 0 and #right_ps > 0 then
    local lc = build_tree(left_ps)
    local rc = build_tree(right_ps)
    return { type = "vsplit", left_child = lc, right_child = rc,
    width = lc.width + rc.width, height = lc.height }
    end
    end

    local ys = {}
    for _, p in ipairs(ps) do ys[p.top + p.height] = true end
    local ys_sorted = {}
    for y in pairs(ys) do table.insert(ys_sorted, y) end
    table.sort(ys_sorted)
    for _, y in ipairs(ys_sorted) do
    local top_ps, bot_ps = {}, {}
    for _, p in ipairs(ps) do
    if p.top + p.height <= y then table.insert(top_ps, p)
    elseif p.top >= y then table.insert(bot_ps, p) end
    end
    if #top_ps + #bot_ps == #ps and #top_ps > 0 and #bot_ps > 0 then
    local tc = build_tree(top_ps)
    local bc = build_tree(bot_ps)
    return { type = "hsplit", top_child = tc, bot_child = bc,
    width = tc.width, height = tc.height + bc.height }
    end
    end

    return { type = "pane", pane = ps[1], width = ps[1].width, height = ps[1].height }
    end

    -- Leftmost/topmost pane in subtree (first-child path — cascade preserver).
    local function near_pane(node)
    if node.type == "pane" then return node.pane end
    if node.type == "vsplit" then return near_pane(node.left_child) end
    return near_pane(node.top_child)
    end

    -- Rightmost/bottommost pane in subtree (second-child path — cascade absorber).
    local function far_pane(node)
    if node.type == "pane" then return node.pane end
    if node.type == "vsplit" then return far_pane(node.right_child) end
    return far_pane(node.bot_child)
    end

    -- Collect all leaf panes in a subtree.
    local function collect_panes(node, out)
    out = out or {}
    if node.type == "pane" then
    table.insert(out, node.pane)
    elseif node.type == "vsplit" then
    collect_panes(node.left_child, out)
    collect_panes(node.right_child, out)
    elseif node.type == "hsplit" then
    collect_panes(node.top_child, out)
    collect_panes(node.bot_child, out)
    end
    return out
    end

    local function count_columns(node)
    if node.type == "vsplit" then
    return count_columns(node.left_child) + count_columns(node.right_child)
    end
    return 1
    end

    local function count_rows(node)
    if node.type == "hsplit" then
    return count_rows(node.top_child) + count_rows(node.bot_child)
    end
    return 1
    end

    -- Snapshot pane sizes keyed by index.
    local function snapshot()
    local s = {}
    for _, pi in ipairs(tab:panes_with_info()) do
    s[pi.index] = { width = pi.width, height = pi.height }
    end
    return s
    end

    -- Probe from a candidate pane to see if it targets the intended boundary.
    -- Returns "grow"/"shrink"/nil indicating the effect of pos_dir on the
    -- first-child side of the intended boundary.
    -- pos_dir/neg_dir: e.g. "Right"/"Left" for vsplits, "Down"/"Up" for hsplits.
    -- prop: "width" or "height".
    -- verify_pane: a pane on the OTHER side whose size must also change.
    local function probe(candidate_idx, pos_dir, neg_dir, prop, verify_idx)
    local before = snapshot()
    window:perform_action(action.ActivatePaneByIndex(candidate_idx), tab:active_pane())
    window:perform_action(action.AdjustPaneSize({ pos_dir, 1 }), tab:active_pane())
    local after = snapshot()

    local cand_delta = after[candidate_idx][prop] - before[candidate_idx][prop]
    local verify_delta = after[verify_idx][prop] - before[verify_idx][prop]

    -- Undo the probe
    window:perform_action(action.AdjustPaneSize({ neg_dir, 1 }), tab:active_pane())

    if cand_delta ~= 0 and verify_delta ~= 0 and cand_delta ~= verify_delta then
    return cand_delta > 0 and "grow" or "shrink"
    end
    return nil
    end

    -- Try to adjust a boundary using probe-and-discover.
    -- candidate_panes: list of {index, side} to try ("left" or "right" of boundary).
    -- delta: desired change to first-child size (positive = grow first child).
    -- pos_dir/neg_dir: direction pair (e.g. "Right"/"Left").
    -- prop: "width" or "height".
    -- verify_idx: pane on opposite side to verify correct boundary.
    local function try_adjust(candidates, delta, pos_dir, neg_dir, prop)
    for _, c in ipairs(candidates) do
    local result = probe(c.index, pos_dir, neg_dir, prop, c.verify)
    if result then
    window:perform_action(action.ActivatePaneByIndex(c.index), tab:active_pane())
    -- result tells us what pos_dir does to this pane relative to the boundary.
    -- We need to figure out what direction achieves our desired delta.
    if c.side == "left" then
    -- Candidate is on left/top side. We want delta applied to left side.
    if result == "grow" then
    if delta > 0 then
    window:perform_action(action.AdjustPaneSize({ pos_dir, delta }), tab:active_pane())
    else
    window:perform_action(action.AdjustPaneSize({ neg_dir, -delta }), tab:active_pane())
    end
    else
    if delta > 0 then
    window:perform_action(action.AdjustPaneSize({ neg_dir, delta }), tab:active_pane())
    else
    window:perform_action(action.AdjustPaneSize({ pos_dir, -delta }), tab:active_pane())
    end
    end
    else
    -- Candidate is on right/bot side. pos_dir effect on candidate is
    -- opposite to its effect on the first-child side.
    if result == "grow" then
    if delta > 0 then
    window:perform_action(action.AdjustPaneSize({ neg_dir, delta }), tab:active_pane())
    else
    window:perform_action(action.AdjustPaneSize({ pos_dir, -delta }), tab:active_pane())
    end
    else
    if delta > 0 then
    window:perform_action(action.AdjustPaneSize({ pos_dir, delta }), tab:active_pane())
    else
    window:perform_action(action.AdjustPaneSize({ neg_dir, -delta }), tab:active_pane())
    end
    end
    end
    return true
    end
    end
    return false
    end

    -- BFS: process one boundary at a time, re-reading between each.
    for _ = 1, #initial_panes - 1 do
    local ps = tab:panes_with_info()
    local tree = build_tree(ps)

    -- Find shallowest unbalanced split (BFS).
    local queue = { tree }
    local adjusted = false
    while #queue > 0 and not adjusted do
    local node = table.remove(queue, 1)
    if node.type == "vsplit" then
    local lc, rc = node.left_child, node.right_child
    local l_cols = count_columns(lc)
    local r_cols = count_columns(rc)
    local total = lc.width + rc.width
    local target_l = math.floor(total * l_cols / (l_cols + r_cols))
    local delta = target_l - lc.width
    if delta ~= 0 then
    local left_panes = collect_panes(lc)
    local right_panes = collect_panes(rc)
    local right_near = near_pane(rc)
    local left_near = near_pane(lc)
    local candidates = {}
    for _, p in ipairs(left_panes) do
    table.insert(candidates, { index = p.index, side = "left", verify = right_near.index })
    end
    for _, p in ipairs(right_panes) do
    table.insert(candidates, { index = p.index, side = "right", verify = left_near.index })
    end
    adjusted = try_adjust(candidates, delta, "Right", "Left", "width")
    end
    if not adjusted then
    table.insert(queue, lc)
    table.insert(queue, rc)
    end
    elseif node.type == "hsplit" then
    local tc, bc = node.top_child, node.bot_child
    local t_rows = count_rows(tc)
    local b_rows = count_rows(bc)
    local total = tc.height + bc.height
    local target_t = math.floor(total * t_rows / (t_rows + b_rows))
    local delta = target_t - tc.height
    if delta ~= 0 then
    local top_panes = collect_panes(tc)
    local bot_panes = collect_panes(bc)
    local bot_near = near_pane(bc)
    local top_near = near_pane(tc)
    local candidates = {}
    for _, p in ipairs(top_panes) do
    table.insert(candidates, { index = p.index, side = "left", verify = bot_near.index })
    end
    for _, p in ipairs(bot_panes) do
    table.insert(candidates, { index = p.index, side = "right", verify = top_near.index })
    end
    adjusted = try_adjust(candidates, delta, "Down", "Up", "height")
    end
    if not adjusted then
    table.insert(queue, tc)
    table.insert(queue, bc)
    end
    end
    end
    if not adjusted then break end
    end

    window:perform_action(action.ActivatePaneByIndex(active_idx), tab:active_pane())
    end

    -- Split and equalize all panes in the current tab
    local function split_and_equalize(direction)
    return wezterm.action_callback(function(window, pane)
    if direction == "vertical" then
    window:perform_action(action.SplitPane({ direction = "Down" }), pane)
    else
    window:perform_action(action.SplitPane({ direction = "Right" }), pane)
    end
    equalize_tab(window)
    end)
    end

    return { equalize_tab = equalize_tab, split_and_equalize = split_and_equalize }