From 5e84e6aba870bb7ee8895c7f158279eef77ad610 Mon Sep 17 00:00:00 2001 From: muon Date: Sun, 8 Mar 2026 15:19:42 +0000 Subject: [PATCH] Add stacked panes --- modules/home/terminal/wezterm.nix | 123 +++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 4 deletions(-) diff --git a/modules/home/terminal/wezterm.nix b/modules/home/terminal/wezterm.nix index 86aee8d..655b5d4 100644 --- a/modules/home/terminal/wezterm.nix +++ b/modules/home/terminal/wezterm.nix @@ -152,7 +152,7 @@ modes = { locked = { fg = color.base03; - hint = "⌥󱁐:󰌌 ⌥hl:󰿵 ⌥1-9:󰓩 ⌥n:󰤻 ⌥w:󰅙 ^ud:󱕷"; + hint = "⌥󱁐:󰌌 ⌥hl:󰹳 ^ud:󰹹 ⌥1-9:󰓩 ⌥n:󰤻 ⌥s:󱇳 ⌥w:󰅙 ⌥t:󰓩 ⌥p:󱃔"; }; normal = { fg = color.base0D; @@ -207,6 +207,39 @@ in { end) end + -- tab_is_zoomed: true if any pane in the current tab is zoomed. + -- Used by move_vertical to decide whether to carry zoom forward. + local function tab_is_zoomed(pane) + for _, info in ipairs(pane:tab():panes_with_info()) do + if info.is_zoomed then return true end + end + return false + end + + -- move_vertical: move focus Up/Down. + -- If any pane in the tab is currently zoomed (zoom-mode is "on" for this + -- tab), zoom the destination pane too — stacked-panel behaviour. + -- Otherwise just move focus plainly. + local function move_vertical(direction) + return wezterm.action_callback(function(window, pane) + local neighbour = pane:tab():get_pane_direction(direction) + if neighbour == nil then return end + if tab_is_zoomed(pane) then + -- unzoom_on_switch_pane unzooms the current pane when we move; + -- then SetPaneZoomState(true) zooms the newly active pane. + window:perform_action( + act.Multiple({ + act.ActivatePaneDirection(direction), + act.SetPaneZoomState(true), + }), + pane + ) + else + window:perform_action(act.ActivatePaneDirection(direction), pane) + end + end) + end + -- ─── Status bar ──────────────────────────────────────────────────────────── -- Per-mode accent colour and hint text, generated from Nix/stylix palette @@ -251,6 +284,51 @@ in { return tab.active_pane.title end) + -- format-tab-title: show "rank/total title" when the active tab is in + -- stacked (zoom) mode; otherwise show plain " title ". + -- Returning a cell table gives us explicit padding and colours; + -- returning a plain string loses the retro tab bar's built-in spacing. + local tab_bg_active = "${color.base0D}" + local tab_fg_active = "${color.base00}" + local tab_bg_inactive = "${color.base01}" + local tab_fg_inactive = "${color.base03}" + + wezterm.on("format-tab-title", function(tab, _tabs, panes, _config, _hover, _max_width) + local title = (tab.tab_title and tab.tab_title ~= "") and tab.tab_title or tab.active_pane.title + local bg = tab.is_active and tab_bg_active or tab_bg_inactive + local fg = tab.is_active and tab_fg_active or tab_fg_inactive + + -- Only compute stack position for the active tab. + -- The `panes` event parameter only contains the active pane snapshot + -- and lacks `top`, so we use wezterm.mux.get_tab():panes_with_info() + -- which returns full PaneInformation including top/is_zoomed/is_active. + if tab.is_active then + local mux_tab = wezterm.mux.get_tab(tab.tab_id) + if mux_tab then + local infos = mux_tab:panes_with_info() + table.sort(infos, function(a, b) return a.top < b.top end) + + local any_zoomed = false + local active_rank, total = 0, 0 + for _, p in ipairs(infos) do + total = total + 1 + if p.is_active then active_rank = total end + if p.is_zoomed then any_zoomed = true end + end + + if any_zoomed and total > 1 and active_rank > 0 then + title = active_rank .. "/" .. total .. " " .. title + end + end + end + + return { + { Background = { Color = bg } }, + { Foreground = { Color = fg } }, + { Text = " " .. title .. " " }, + } + end) + wezterm.on("update-status", function(window, _pane) local kt = window:active_key_table() local mode = kt or "locked" @@ -290,6 +368,7 @@ in { config.default_cursor_style = "BlinkingBar" config.hide_tab_bar_if_only_one_tab = false config.use_fancy_tab_bar = false + config.tab_max_width = 24 config.colors = { tab_bar = { @@ -313,6 +392,11 @@ in { config.scrollback_lines = 10000 + -- ─── Stacked panes ───────────────────────────────────────────────────────── + -- When switching away from a zoomed pane, unzoom it automatically. + -- Paired with stack_focus (Alt+j/k) this gives Zellij-style stacked panels. + config.unzoom_on_switch_pane = true + -- ─── Key bindings ────────────────────────────────────────────────────────── -- -- Default mode = "locked" (all keys pass through to the shell). @@ -324,7 +408,7 @@ in { config.keys = { -- Alt+h/l: move focus or switch tab at edge (Zellij MoveFocusOrTab) - -- Alt+j/k: move focus between panes + -- Alt+j/k: initial bindings (overridden below by stacked-focus variants) ${viDirKeys { mods = "ALT"; indent = " "; @@ -340,6 +424,21 @@ in { }} -- Alt+n: new pane (split right) { key = "n", mods = "ALT", action = act.SplitHorizontal({ domain = "CurrentPaneDomain" }) }, + -- Alt+s: split below, then zoom the new pane so stacked mode starts. + -- Two separate perform_action calls: the split runs first and focuses + -- the new pane, then the second call zooms window:active_pane() which + -- is now the new pane (not the original `pane` argument). + { key = "s", mods = "ALT", action = wezterm.action_callback(function(window, pane) + window:perform_action(act.SplitVertical({ domain = "CurrentPaneDomain" }), pane) + window:perform_action(act.SetPaneZoomState(true), window:active_pane()) + end) + }, + -- Alt+j/k: move_vertical — plain focus move normally, stacked zoom + -- when any pane in the tab is already zoomed (mirrors Alt+f state). + { key = "j", mods = "ALT", action = move_vertical("Down") }, + { key = "k", mods = "ALT", action = move_vertical("Up") }, + { key = "DownArrow", mods = "ALT", action = move_vertical("Down") }, + { key = "UpArrow", mods = "ALT", action = move_vertical("Up") }, -- Alt+f: toggle zoom { key = "f", mods = "ALT", action = act.TogglePaneZoomState }, -- Alt+[/]: rotate panes @@ -352,8 +451,24 @@ in { -- Alt+i/o: reorder tabs { key = "i", mods = "ALT", action = act.MoveTabRelative(-1) }, { key = "o", mods = "ALT", action = act.MoveTabRelative(1) }, - -- Alt+w: close current pane - { key = "w", mods = "ALT", action = act.CloseCurrentPane({ confirm = true }) }, + -- Alt+w: close current pane; if tab was in stacked (zoom) mode, re-zoom + -- the new active pane so zoom mode persists across close. + -- confirm=false because the callback must run synchronously after close; + -- a confirm dialog would make the zoom fire before the user answers. + -- Guard: only re-zoom when there will still be panes left after close. + { key = "w", mods = "ALT", action = wezterm.action_callback(function(window, pane) + local panes = pane:tab():panes() + local was_zoomed = tab_is_zoomed(pane) + window:perform_action(act.CloseCurrentPane({ confirm = false }), pane) + if was_zoomed and #panes > 1 then + window:perform_action(act.SetPaneZoomState(true), window:active_pane()) + end + end) + }, + -- Alt+p: command palette + { key = "p", mods = "ALT", action = act.ActivateCommandPalette }, + -- Alt+t: tab navigator (searchable tab list) + { key = "t", mods = "ALT", action = act.ShowTabNavigator }, -- Alt+q: quit { key = "q", mods = "ALT", action = act.QuitApplication }, -- Ctrl+U/D: scroll half page (matches Alacritty)