feat(astal-menu): Columns count stepper + expanded taskbar is one module

- Columns layout publishes per-workspace column/row counts to columns-state.json
  and handles a direct "cols +1/-1/N" message. The menu's Columns page now shows a
  "Columns  (−)[N](+)" stepper that reads the live count and drives it.
- Expanding the taskbar now fully collapses the strip (header included) via its
  revealer, so the panel that covers the quads reads as a single module instead of a
  thin strip plus a detached card below it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUN7gg6GGfnToghMijLm5Y
main
Amir Alexander Abdelbaki 2026-07-07 13:31:45 +02:00
parent 9d1ec69a11
commit de666acac7
4 changed files with 81 additions and 14 deletions

View File

@ -25,6 +25,7 @@ from lib.proc import run_json, run_text
from paths import CACHE_DIR from paths import CACHE_DIR
_LAYOUTS_MANIFEST = CACHE_DIR / "layouts.json" _LAYOUTS_MANIFEST = CACHE_DIR / "layouts.json"
_COLUMNS_STATE = CACHE_DIR / "columns-state.json"
_LAYOUTS_STATE = CACHE_DIR / "layouts-state.json" _LAYOUTS_STATE = CACHE_DIR / "layouts-state.json"
@ -284,11 +285,46 @@ class Taskbar(Gtk.Box):
row.append(sw) row.append(sw)
page.append(row) page.append(row)
self._fit_sws[ly["name"]] = sw self._fit_sws[ly["name"]] = sw
if not ly.get("dirs") and not ly.get("fit_method"): if ly.get("stepper"): # Columns: (-)[N](+)
page.append(self._cols_stepper())
if not ly.get("dirs") and not ly.get("fit_method") and not ly.get("stepper"):
page.append(Gtk.Label(label="No adjustable options", xalign=0.0, page.append(Gtk.Label(label="No adjustable options", xalign=0.0,
css_classes=["net-ip"])) css_classes=["net-ip"]))
return page return page
# -- columns count stepper --------------------------------------------
def _cols_stepper(self) -> Gtk.Widget:
row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
row.append(Gtk.Label(label="Columns", xalign=0.0, hexpand=True))
minus = Gtk.Button(label="")
minus.add_css_class("quad-action")
self._cols_lbl = Gtk.Label(label=str(self._read_cols()))
self._cols_lbl.add_css_class("stepper-value")
plus = Gtk.Button(label="+")
plus.add_css_class("quad-action")
minus.connect("clicked", lambda *_a: self._step_cols(-1))
plus.connect("clicked", lambda *_a: self._step_cols(1))
row.append(minus)
row.append(self._cols_lbl)
row.append(plus)
return row
def _read_cols(self) -> int:
try:
data = json.loads(_COLUMNS_STATE.read_text())
return int(data.get(str(self._active_ws), {}).get("cols", 2))
except (FileNotFoundError, json.JSONDecodeError, ValueError, TypeError):
return 2
def _step_cols(self, delta: int) -> None:
try:
cur = int(self._cols_lbl.get_label())
except ValueError:
cur = self._read_cols()
new = max(1, cur + delta)
self._cols_lbl.set_label(str(new))
_eval(f'hl.dispatch(hl.dsp.layout("cols {"+1" if delta > 0 else "-1"}"))')
# -- sync + handlers --------------------------------------------------- # -- sync + handlers ---------------------------------------------------
def _sync_layout_controls(self) -> None: def _sync_layout_controls(self) -> None:
# reflect the current per-ws layout / direction / focus_fit in the tabs+options # reflect the current per-ws layout / direction / focus_fit in the tabs+options

View File

@ -159,10 +159,13 @@ class MenuWindow(Gtk.ApplicationWindow):
# -- taskbar panel expansion ------------------------------------------ # -- taskbar panel expansion ------------------------------------------
def _on_taskbar_panel(self, expanded: bool) -> None: def _on_taskbar_panel(self, expanded: bool) -> None:
# Cover the quads with the taskbar's workspace/window panel (the strip itself # Cover the quads with the taskbar's workspace/window panel. Fully collapse the
# collapses inside the Taskbar). Collapse any expanded quad first. # taskbar strip (header included) so the expanded panel reads as ONE module
# rather than a thin strip on top plus a detached card. Collapse any quad first.
if expanded and self.grid.is_expanded: if expanded and self.grid.is_expanded:
self.grid.request_collapse() self.grid.request_collapse()
self.taskbar_reveal.set_reveal_child(not expanded)
self.taskbar_reveal.set_visible(not expanded)
self.grid.show_takeover() if expanded else self.grid.hide_takeover() self.grid.show_takeover() if expanded else self.grid.hide_takeover()
# -- appdrawer expansion ---------------------------------------------- # -- appdrawer expansion ----------------------------------------------

View File

@ -19,15 +19,41 @@
local state = {} -- [wsid] = { ncols, rows, orient, assign={addr=col}, seq={addr=n}, nextseq, pan={col=px} } local state = {} -- [wsid] = { ncols, rows, orient, assign={addr=col}, seq={addr=n}, nextseq, pan={col=px} }
-- Publish per-workspace column/row counts so the astal-menu can show a live stepper.
local STATE_FILE = (os.getenv("HOME") or "") .. "/.cache/astal-menu/columns-state.json"
local function publish()
local parts = {}
for wsid, s in pairs(state) do
parts[#parts + 1] = string.format('"%s":{"cols":%d,"rows":%d,"orient":"%s"}',
tostring(wsid), s.ncols, s.rows, s.orient)
end
pcall(function()
local f = io.open(STATE_FILE, "w")
if f then f:write("{" .. table.concat(parts, ",") .. "}\n"); f:close() end
end)
end
local function ws_state(wsid) local function ws_state(wsid)
local s = state[wsid] local s = state[wsid]
if not s then if not s then
s = { ncols = 2, rows = 2, orient = "h", assign = {}, seq = {}, nextseq = 1, pan = {} } s = { ncols = 2, rows = 2, orient = "h", assign = {}, seq = {}, nextseq = 1, pan = {} }
state[wsid] = s state[wsid] = s
publish()
end end
return s return s
end end
-- Reflow every window evenly across the current column count, by insertion order.
local function reflow(s, targets)
local addrs = {}
for _, tg in ipairs(targets) do
local ok, addr = pcall(function() return tg.window.address end)
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)
for i, addr in ipairs(addrs) do s.assign[addr] = ((i - 1) % s.ncols) + 1 end
end
local function win_ws_id(win) local function win_ws_id(win)
local ok, ws = pcall(function() return win.workspace end) local ok, ws = pcall(function() return win.workspace end)
if ok and ws ~= nil then if ok and ws ~= nil then
@ -185,18 +211,18 @@ local function layout_msg(ctx, cmd)
elseif arg == less_cols then s.ncols = math.max(1, s.ncols - 1); changed_cols = true elseif arg == less_cols then s.ncols = math.max(1, s.ncols - 1); changed_cols = true
elseif arg == more_rows then s.rows = s.rows + 1 elseif arg == more_rows then s.rows = s.rows + 1
elseif arg == less_rows then s.rows = math.max(1, s.rows - 1) end elseif arg == less_rows then s.rows = math.max(1, s.rows - 1) end
if changed_cols then if changed_cols then reflow(s, t) end
-- reflow all windows evenly across the new column count (by insertion order) publish()
local addrs = {} elseif verb == "cols" then
for _, tg in ipairs(t) do -- direct column-count change (the menu's (-)[N](+) stepper).
local ok, addr = pcall(function() return tg.window.address end) if arg == "+" or arg == "+1" then s.ncols = s.ncols + 1
if ok and addr then addrs[#addrs + 1] = addr end elseif arg == "-" or arg == "-1" then s.ncols = math.max(1, s.ncols - 1)
end else local n = tonumber(arg); if n then s.ncols = math.max(1, math.floor(n)) end end
table.sort(addrs, function(a, b) return (s.seq[a] or 0) < (s.seq[b] or 0) end) reflow(s, t)
for i, addr in ipairs(addrs) do s.assign[addr] = ((i - 1) % s.ncols) + 1 end publish()
end
elseif verb == "orient" then elseif verb == "orient" then
s.orient = (arg == "v") and "v" or "h" s.orient = (arg == "v") and "v" or "h"
publish()
end end
end end
@ -216,4 +242,5 @@ return {
dir_labels = { "Horizontal", "Vertical" }, dir_labels = { "Horizontal", "Vertical" },
default_dir = "h", default_dir = "h",
fit_method = false, fit_method = false,
stepper = true, -- menu shows a "Columns: (-)[N](+)" count stepper
} }

View File

@ -44,7 +44,8 @@ function M.write_manifest()
',"dirs":[', table.concat(dirs, ","), "]", ',"dirs":[', table.concat(dirs, ","), "]",
',"dir_labels":[', table.concat(dlabels, ","), "]", ',"dir_labels":[', table.concat(dlabels, ","), "]",
',"default_dir":', jstr(s.default_dir or ""), ',"default_dir":', jstr(s.default_dir or ""),
',"fit_method":', tostring(s.fit_method or false), "}", ',"fit_method":', tostring(s.fit_method or false),
',"stepper":', tostring(s.stepper or false), "}",
}) })
end end
os.execute("mkdir -p '" .. MANIFEST:match("(.+)/[^/]+$") .. "' 2>/dev/null") os.execute("mkdir -p '" .. MANIFEST:match("(.+)/[^/]+$") .. "' 2>/dev/null")