{ pkgs, lib, config, ... }: let cfg = config.mods.terminal; color = config.lib.stylix.colors.withHashtag; # Shell to use inside wezterm shell = if cfg.nushell.enable then lib.getExe pkgs.nushell else if cfg.zsh.enable then lib.getExe pkgs.zsh else null; # ── Lua codegen helpers ────────────────────────────────────────────────────── luaKey = { key, mods ? null, action, indent ? " ", }: let modsStr = lib.optionalString (mods != null) ", mods = \"${mods}\""; in "${indent}{ key = \"${key}\"${modsStr}, action = ${action} },\n"; # vi directions: key, arrow alias, wezterm direction name viDirs = [ {key = "h"; arrow = "LeftArrow"; dir = "Left";} {key = "j"; arrow = "DownArrow"; dir = "Down";} {key = "k"; arrow = "UpArrow"; dir = "Up";} {key = "l"; arrow = "RightArrow"; dir = "Right";} ]; # hjkl + arrow equivalents; mkAction :: dir -> lua-action-string viDirKeys = {mods ? null, indent ? " ", mkAction}: lib.concatMapStrings (d: luaKey {inherit mods indent; key = d.key; action = mkAction d.dir;} + luaKey {inherit mods indent; key = d.arrow; action = mkAction d.dir;}) viDirs; # 1-9 tab-jump bindings tabJumpKeys = {mods ? null, indent ? " "}: lib.concatStrings (builtins.genList (i: luaKey { inherit mods indent; key = toString (i + 1); action = "act.ActivateTab(${toString i})"; }) 9); # hjkl + arrows for AdjustPaneSize at a given step resizeDirKeys = {step, indent ? " "}: lib.concatMapStrings (d: luaKey {inherit indent; key = d.key; action = "act.AdjustPaneSize({ \"${d.dir}\", ${toString step} })";} + luaKey {inherit indent; key = d.arrow; action = "act.AdjustPaneSize({ \"${d.dir}\", ${toString step} })";}) viDirs; # Uppercase HJKL fine-tune resize resizeShiftKeys = {step, indent ? " "}: lib.concatMapStrings (d: luaKey {inherit indent; key = lib.toUpper d.key; action = "act.AdjustPaneSize({ \"${d.dir}\", ${toString step} })";}) viDirs; # Wrap a lua action string so it also pops the key table (return to locked) andPop = action: "act.Multiple({ ${action}, act.PopKeyTable })"; # Standard exit bindings, given the mode's own pop key exitKeys = {selfKey, indent ? " "}: lib.concatStrings [ (luaKey {inherit indent; key = selfKey; action = "act.PopKeyTable";}) (luaKey {inherit indent; key = "Escape"; action = "act.PopKeyTable";}) (luaKey {inherit indent; key = "Enter"; action = "act.PopKeyTable";}) (luaKey {inherit indent; key = "Space"; mods = "ALT"; action = "act.PopKeyTable";}) ]; # ── Status bar ─────────────────────────────────────────────────────────────── # Per-mode accent colour (base16) and hint string shown on the left status bar modes = { locked = {fg = color.base03; hint = "Alt+Space:enter Alt+hl:focus/tab Alt+1-9:tab Alt+n:split C-ud:scroll";}; normal = {fg = color.base0D; hint = "p:pane t:tab r:resize s:scroll n:split Esc:exit";}; pane = {fg = color.base0B; hint = "hjkl:focus n/r:split→ d:split↓ f:zoom x:close Esc:exit";}; tab = {fg = color.base0C; hint = "hjkl:prev/next 1-9:jump n:new x:close r:rename Esc:exit";}; resize = {fg = color.base0A; hint = "hjkl:+5 HJKL:+1 Esc:exit";}; scroll = {fg = color.base0E; hint = "jk:line ud:half hl:page f:search Esc:exit";}; }; # Lua table literal mapping mode name -> { fg, hint } modeTableEntries = lib.concatStringsSep "\n " (lib.mapAttrsToList (name: m: '' ["${name}"] = { fg = "${m.fg}", hint = "${m.hint}" }, '') modes); in { options.mods.terminal.wezterm.enable = lib.mkEnableOption "enables wezterm"; config = lib.mkIf cfg.wezterm.enable { programs.wezterm = { enable = true; extraConfig = '' local wezterm = require("wezterm") local act = wezterm.action -- ─── Helpers ─────────────────────────────────────────────────────────────── -- move_focus_or_tab: mirrors Zellij's MoveFocusOrTab. -- Move focus in direction; if at the edge, switch to the adjacent tab. local function move_focus_or_tab(direction) return wezterm.action_callback(function(window, pane) local neighbour = pane:tab():get_pane_direction(direction) if neighbour ~= nil then window:perform_action(act.ActivatePaneDirection(direction), pane) else if direction == "Left" then window:perform_action(act.ActivateTabRelative(-1), pane) elseif direction == "Right" then window:perform_action(act.ActivateTabRelative(1), pane) end end end) end -- ─── Status bar ──────────────────────────────────────────────────────────── -- Per-mode accent colour and hint text, generated from Nix/stylix palette local mode_info = { ${modeTableEntries} } local bg_bar = "${color.base01}" local fg_text = "${color.base05}" local bg_main = "${color.base00}" wezterm.on("update-status", function(window, _pane) local kt = window:active_key_table() local mode = kt or "locked" local info = mode_info[mode] or mode_info["locked"] -- Left: coloured mode badge + hint line window:set_left_status(wezterm.format({ { Background = { Color = info.fg } }, { Foreground = { Color = bg_main } }, { Attribute = { Intensity = "Bold" } }, { Text = " " .. mode:upper() .. " " }, "ResetAttributes", { Background = { Color = bg_bar } }, { Foreground = { Color = fg_text } }, { Text = " " .. info.hint .. " " }, "ResetAttributes", })) window:set_right_status("") end) -- ─── Appearance ──────────────────────────────────────────────────────────── local config = wezterm.config_builder() -- Stylix sets the color scheme via its wezterm integration; -- this is a fallback in case it is not active. config.color_scheme = "Synthwave (Gogh)" config.font = wezterm.font("CommitMono Nerd Font") config.font_size = 12.0 config.window_padding = { left = 6, right = 6, top = 6, bottom = 6 } config.default_cursor_style = "BlinkingBar" config.hide_tab_bar_if_only_one_tab = false config.use_fancy_tab_bar = false config.tab_bar_at_bottom = true -- ─── Shell ───────────────────────────────────────────────────────────────── ${lib.optionalString (shell != null) '' config.default_prog = { "${shell}" } ''} -- ─── Scrollback ──────────────────────────────────────────────────────────── config.scrollback_lines = 10000 -- ─── Key bindings ────────────────────────────────────────────────────────── -- -- Default mode = "locked" (all keys pass through to the shell). -- Alt+Space enters "normal" mode (like Zellij). -- From normal: p→pane t→tab r→resize s→scroll -- Esc / Enter / Alt+Space → back to locked from any mode. config.disable_default_key_bindings = true config.keys = { -- Alt+h/l: move focus or switch tab at edge (Zellij MoveFocusOrTab) -- Alt+j/k: move focus between panes ${viDirKeys { mods = "ALT"; indent = " "; mkAction = d: if d == "Left" || d == "Right" then "move_focus_or_tab(\"${d}\")" else "act.ActivatePaneDirection(\"${d}\")"; }} -- Alt+1-9: jump to tab by number ${tabJumpKeys {mods = "ALT"; indent = " ";}} -- Alt+n: new pane (split right) { key = "n", mods = "ALT", action = act.SplitHorizontal({ domain = "CurrentPaneDomain" }) }, -- Alt+f: toggle zoom { key = "f", mods = "ALT", action = act.TogglePaneZoomState }, -- Alt+[/]: rotate panes { key = "[", mods = "ALT", action = act.RotatePanes("CounterClockwise") }, { key = "]", mods = "ALT", action = act.RotatePanes("Clockwise") }, -- Alt++/-/=: font size { key = "+", mods = "ALT", action = act.IncreaseFontSize }, { key = "-", mods = "ALT", action = act.DecreaseFontSize }, { key = "=", mods = "ALT", action = act.ResetFontSize }, -- Alt+i/o: reorder tabs { key = "i", mods = "ALT", action = act.MoveTabRelative(-1) }, { key = "o", mods = "ALT", action = act.MoveTabRelative(1) }, -- Alt+q: quit { key = "q", mods = "ALT", action = act.QuitApplication }, -- Ctrl+U/D: scroll half page (matches Alacritty) { key = "u", mods = "CTRL", action = act.ScrollByPage(-0.5) }, { key = "d", mods = "CTRL", action = act.ScrollByPage(0.5) }, -- Copy / paste { key = "c", mods = "CTRL|SHIFT", action = act.CopyTo("Clipboard") }, { key = "v", mods = "CTRL|SHIFT", action = act.PasteFrom("Clipboard") }, -- Enter normal mode { key = "Space", mods = "ALT", action = act.ActivateKeyTable({ name = "normal", one_shot = false }) }, } config.key_tables = { -- ── NORMAL ───────────────────────────────────────────────────────────── normal = { { key = "p", action = act.ActivateKeyTable({ name = "pane", one_shot = false }) }, { key = "t", action = act.ActivateKeyTable({ name = "tab", one_shot = false }) }, { key = "r", action = act.ActivateKeyTable({ name = "resize", one_shot = false }) }, { key = "s", action = act.ActivateKeyTable({ name = "scroll", one_shot = false }) }, { key = "n", action = ${andPop "act.SplitHorizontal({ domain = \"CurrentPaneDomain\" })"} }, ${exitKeys {selfKey = "Escape";}} }, -- ── PANE ─────────────────────────────────────────────────────────────── pane = { ${viDirKeys { mkAction = d: if d == "Left" || d == "Right" then "move_focus_or_tab(\"${d}\")" else "act.ActivatePaneDirection(\"${d}\")"; }} { key = "n", action = ${andPop "act.SplitHorizontal({ domain = \"CurrentPaneDomain\" })"} }, { key = "r", action = ${andPop "act.SplitHorizontal({ domain = \"CurrentPaneDomain\" })"} }, { key = "d", action = ${andPop "act.SplitVertical({ domain = \"CurrentPaneDomain\" })"} }, { key = "f", action = ${andPop "act.TogglePaneZoomState"} }, { key = "z", action = ${andPop "act.TogglePaneZoomState"} }, { key = "x", action = ${andPop "act.CloseCurrentPane({ confirm = true })"} }, { key = "Tab", action = act.ActivatePaneDirection("Next") }, ${exitKeys {selfKey = "p";}} }, -- ── TAB ──────────────────────────────────────────────────────────────── tab = { { key = "h", action = act.ActivateTabRelative(-1) }, { key = "k", action = act.ActivateTabRelative(-1) }, { key = "j", action = act.ActivateTabRelative(1) }, { key = "l", action = act.ActivateTabRelative(1) }, { key = "LeftArrow", action = act.ActivateTabRelative(-1) }, { key = "UpArrow", action = act.ActivateTabRelative(-1) }, { key = "RightArrow", action = act.ActivateTabRelative(1) }, { key = "DownArrow", action = act.ActivateTabRelative(1) }, { key = "Tab", action = act.ActivateTabRelative(1) }, ${tabJumpKeys {}} { key = "n", action = ${andPop "act.SpawnTab(\"CurrentPaneDomain\")"} }, { key = "x", action = ${andPop "act.CloseCurrentTab({ confirm = true })"} }, { key = "r", action = act.PromptInputLine({ description = "Rename tab", action = wezterm.action_callback(function(window, _, line) if line then window:active_tab():set_title(line) end window:perform_action(act.PopKeyTable, window:active_pane()) end), }) }, ${exitKeys {selfKey = "t";}} }, -- ── RESIZE ───────────────────────────────────────────────────────────── resize = { ${resizeDirKeys {step = 5;}}${resizeShiftKeys {step = 1;}} { key = "+", action = act.AdjustPaneSize({ "Right", 5 }) }, { key = "-", action = act.AdjustPaneSize({ "Left", 5 }) }, { key = "=", action = act.AdjustPaneSize({ "Right", 5 }) }, ${exitKeys {selfKey = "r";}} }, -- ── SCROLL ───────────────────────────────────────────────────────────── scroll = { { key = "j", action = act.ScrollByLine(1) }, { key = "k", action = act.ScrollByLine(-1) }, { key = "d", action = act.ScrollByPage(0.5) }, { key = "u", action = act.ScrollByPage(-0.5) }, { key = "h", action = act.ScrollByPage(-1) }, { key = "l", action = act.ScrollByPage(1) }, { key = "DownArrow", action = act.ScrollByLine(1) }, { key = "UpArrow", action = act.ScrollByLine(-1) }, { key = "PageDown", action = act.ScrollByPage(1) }, { key = "PageUp", action = act.ScrollByPage(-1) }, { key = "f", action = act.Search({ CaseSensitiveString = "" }) }, { key = "c", mods = "CTRL", action = act.ScrollToBottom }, ${exitKeys {selfKey = "s";}} }, } return config ''; }; }; }