feat(columns): open a workspace's first window in the middle column

least_col() now breaks count ties by nearness to the middle column, so the
first window of a workspace (every column still empty) lands centred instead
of pinned to the left edge, and a restored batch fans out from the middle.
reflow() keeps a lone window in the middle too, so the menu's column stepper
doesn't shove it back to the left.

Applied identically to hyprlua and hyprdrive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main
Amir Alexander Abdelbaki 2026-08-04 18:09:36 +02:00
parent d5da519c9b
commit d6cb0909ae
2 changed files with 56 additions and 6 deletions

View File

@ -17,6 +17,11 @@
-- SCROLLS. Resizing up/down retunes the focused window's height weight.
-- Weights and per-column pan persist across relayouts.
--
-- A workspace's FIRST window opens in the middle column (an even column count has no
-- true middle — it takes the left of the two), so a lone window is centred rather than
-- stuck against the left edge; further windows stack into the column you're working in
-- and otherwise fill outward from the middle.
--
-- This is a real custom layout (hl.layout.register), not the built-in scrolling engine —
-- that one has a single shared tape; this pans each column apart.
--
@ -123,6 +128,11 @@ local function ws_state(wsid, area)
return s
end
-- The MIDDLE column — where a workspace's first window lands, and the tie-break for
-- every "emptiest column" pick, so a lone window sits centred on screen instead of pinned
-- to the left edge. An even column count has no true middle; take the left of the two.
local function mid_col(s) return math.floor((s.ncols + 1) / 2) end
-- Reflow every window evenly across the current column count, by insertion order.
local function reflow(s, targets)
local addrs = {}
@ -131,7 +141,13 @@ local function reflow(s, targets)
if ok and addr then addrs[#addrs + 1] = addr end
end
table.sort(addrs, function(a, b) return (s.seq[a] or 0) < (s.seq[b] or 0) end)
if #addrs == 1 then
-- a lone window keeps the middle (changing the column count from the menu
-- shouldn't shove it to the left edge).
s.assign[addrs[1]] = mid_col(s)
else
for i, addr in ipairs(addrs) do s.assign[addr] = ((i - 1) % s.ncols) + 1 end
end
for k in pairs(s.pair) do s.pair[k] = nil end -- a redistribute breaks parked pairs
end
@ -177,9 +193,18 @@ local function columnize(s, targets)
end
local fresh = {}
for addr in pairs(present) do if not s.assign[addr] then fresh[#fresh + 1] = addr end end
-- emptiest column, ties broken by nearness to the middle — so the first window of a
-- workspace (every column still empty) opens in the middle column, and a fresh batch
-- fills outward from there rather than left-to-right.
local function least_col()
local best, bestn = 1, math.huge
for c = 1, s.ncols do if counts[c] < bestn then best, bestn = c, counts[c] end end
local mid = mid_col(s)
local best, bestn, bestd = mid, math.huge, math.huge
for c = 1, s.ncols do
local d = math.abs(c - mid)
if counts[c] < bestn or (counts[c] == bestn and d < bestd) then
best, bestn, bestd = c, counts[c], d
end
end
return best
end
if #fresh == 1 and next(s.assign) ~= nil then

View File

@ -17,6 +17,11 @@
-- SCROLLS. Resizing up/down retunes the focused window's height weight.
-- Weights and per-column pan persist across relayouts.
--
-- A workspace's FIRST window opens in the middle column (an even column count has no
-- true middle — it takes the left of the two), so a lone window is centred rather than
-- stuck against the left edge; further windows stack into the column you're working in
-- and otherwise fill outward from the middle.
--
-- This is a real custom layout (hl.layout.register), not the built-in scrolling engine —
-- that one has a single shared tape; this pans each column apart.
--
@ -123,6 +128,11 @@ local function ws_state(wsid, area)
return s
end
-- The MIDDLE column — where a workspace's first window lands, and the tie-break for
-- every "emptiest column" pick, so a lone window sits centred on screen instead of pinned
-- to the left edge. An even column count has no true middle; take the left of the two.
local function mid_col(s) return math.floor((s.ncols + 1) / 2) end
-- Reflow every window evenly across the current column count, by insertion order.
local function reflow(s, targets)
local addrs = {}
@ -131,7 +141,13 @@ local function reflow(s, targets)
if ok and addr then addrs[#addrs + 1] = addr end
end
table.sort(addrs, function(a, b) return (s.seq[a] or 0) < (s.seq[b] or 0) end)
if #addrs == 1 then
-- a lone window keeps the middle (changing the column count from the menu
-- shouldn't shove it to the left edge).
s.assign[addrs[1]] = mid_col(s)
else
for i, addr in ipairs(addrs) do s.assign[addr] = ((i - 1) % s.ncols) + 1 end
end
for k in pairs(s.pair) do s.pair[k] = nil end -- a redistribute breaks parked pairs
end
@ -177,9 +193,18 @@ local function columnize(s, targets)
end
local fresh = {}
for addr in pairs(present) do if not s.assign[addr] then fresh[#fresh + 1] = addr end end
-- emptiest column, ties broken by nearness to the middle — so the first window of a
-- workspace (every column still empty) opens in the middle column, and a fresh batch
-- fills outward from there rather than left-to-right.
local function least_col()
local best, bestn = 1, math.huge
for c = 1, s.ncols do if counts[c] < bestn then best, bestn = c, counts[c] end end
local mid = mid_col(s)
local best, bestn, bestd = mid, math.huge, math.huge
for c = 1, s.ncols do
local d = math.abs(c - mid)
if counts[c] < bestn or (counts[c] == bestn and d < bestd) then
best, bestn, bestd = c, counts[c], d
end
end
return best
end
if #fresh == 1 and next(s.assign) ~= nil then