Skip to content

Instantly share code, notes, and snippets.

@johnlindquist
Created January 12, 2026 21:14
Show Gist options
  • Select an option

  • Save johnlindquist/346e18fd6875ae4207a9b69c62071e9a to your computer and use it in GitHub Desktop.

Select an option

Save johnlindquist/346e18fd6875ae4207a9b69c62071e9a to your computer and use it in GitHub Desktop.
WezTerm Power User Config - Zellij-style layouts, Smart Pickers, External Triggers, Session Persistence
-- ============================================================================
-- APPEARANCE MODULE
-- ============================================================================
-- Visual configuration: colors, fonts, window settings
local wezterm = require 'wezterm'
local theme = require 'theme'
local M = {}
function M.apply(config)
-- COLOR SCHEME
config.color_scheme = 'Hardcore'
-- FONT CONFIGURATION
config.font = wezterm.font 'JetBrains Mono'
config.font_size = 13.0
-- WINDOW DECORATIONS
config.window_decorations = "TITLE | RESIZE"
-- BACKGROUND OPACITY
config.window_background_opacity = 0.95
config.macos_window_background_blur = 90
-- WINDOW PADDING
config.window_padding = {
left = 10,
right = 10,
top = 10,
bottom = 10,
}
-- TAB BAR
config.hide_tab_bar_if_only_one_tab = false
config.use_fancy_tab_bar = false
config.tab_bar_at_bottom = true
config.tab_max_width = 32
-- INACTIVE PANE DIMMING
config.inactive_pane_hsb = {
brightness = 0.3,
}
-- Faster status updates for trigger system (default is 1000ms)
config.status_update_interval = 250
-- PANE SPLIT LINE AND TAB BAR STYLING
config.colors = {
split = theme.colors.pink,
tab_bar = {
background = theme.colors.bg,
active_tab = {
bg_color = theme.colors.bg_selection,
fg_color = theme.colors.fg_bright,
intensity = 'Bold',
},
inactive_tab = {
bg_color = theme.colors.bg,
fg_color = theme.colors.fg_dim,
},
inactive_tab_hover = {
bg_color = theme.colors.bg_light,
fg_color = theme.colors.fg,
italic = false,
},
new_tab = {
bg_color = theme.colors.bg,
fg_color = theme.colors.fg_dim,
},
new_tab_hover = {
bg_color = theme.colors.bg_selection,
fg_color = theme.colors.green,
},
},
}
-- CURSOR CONFIGURATION
config.default_cursor_style = 'SteadyBar'
-- OPTION KEY AS META
config.send_composed_key_when_left_alt_is_pressed = false
config.send_composed_key_when_right_alt_is_pressed = false
-- VISUAL BELL
config.audible_bell = "Disabled"
config.window_close_confirmation = "AlwaysPrompt"
config.visual_bell = {
fade_in_duration_ms = 75,
fade_out_duration_ms = 75,
target = 'CursorColor',
}
-- HYPERLINK RULES
config.hyperlink_rules = {
-- localhost URLs (with optional port)
{ regex = '\\bhttps?://localhost(:\\d+)?\\S*\\b', format = '$0' },
-- 127.0.0.1 URLs (with optional port)
{ regex = '\\bhttps?://127\\.0\\.0\\.1(:\\d+)?\\S*\\b', format = '$0' },
-- Standard URLs (http, https, ftp, file, mailto, ssh, git)
{ regex = '\\b\\w+://[\\w.-]+\\.[a-z]{2,15}\\S*\\b', format = '$0' },
{ regex = '\\b[\\w.+-]+@[\\w-]+(\\.[\\w-]+)+\\b', format = 'mailto:$0' },
{ regex = '\\b(/[\\w.-]+)+/?\\b', format = 'file://$0' },
}
end
return M

WezTerm Power User Guide

A guide to WezTerm workspaces, pane management, and productivity tricks for users coming from iTerm2.

Modular Config Structure

The config is split into logical modules for maintainability:

~/.config/wezterm/
├── wezterm.lua          # Main entry point (imports modules)
├── helpers.lua          # Utility functions (is_vim, is_micro, cwd helpers)
├── theme.lua            # Color definitions and theme list
├── layouts.lua          # Zellij-style layouts and smart pane management
├── appearance.lua       # Visual config (fonts, colors, window settings)
├── pickers.lua          # Fuzzy pickers (Quick Open, themes)
├── events.lua           # Event handlers (tab titles, status bar)
├── keys/
│   ├── init.lua         # Key aggregator
│   ├── micro.lua        # Micro editor Cmd→Ctrl mappings
│   ├── navigation.lua   # Pane/tab/workspace navigation
│   ├── layouts.lua      # Layout and splitting keybindings
│   └── power.lua        # Themes, zen mode, session management
└── wezterm.lua.backup   # Original monolithic config (for reference)

Key modules:

  • helpers.lua - is_vim(), is_micro(), short_cwd(), scheme_for_cwd()
  • layouts.lua - Layout modes (tiled/vertical/horizontal/main-*), smart_new_pane(), layout templates
  • theme.lua - Hardcore theme colors, high contrast theme list
  • pickers.lua - show_quick_open_picker() for Cmd+P/Cmd+N

Quick Reference

Key Action
Cmd+D Smart split (uses current layout mode)
Cmd+Shift+D Split pane down
Cmd+W Close current pane (closes tab if last pane)
Cmd+T New tab with zoxide picker
Cmd+N Quick Open picker (zoxide dirs, switch/create)
Cmd+P Quick Open picker (same as Cmd+N)
Cmd+Shift+S Fuzzy workspace picker
Cmd+O Smart workspace switcher (zoxide)
Cmd+E Pane selector (number overlay)
Cmd+Shift+E Swap panes
Cmd+K Command palette
Cmd+L Launcher
Cmd+Shift+T Theme picker
Cmd+Shift+L Layout template picker
Cmd+Shift+F Toggle fullscreen
Cmd+1-9 Switch to pane by number
Ctrl+B + z Toggle pane zoom / Zen mode
Ctrl+B + h/j/k/l Navigate panes (vim-style)
Ctrl+B + t Cycle themes
Ctrl+B + s Save workspace (resurrect)
Alt+n Smart new pane (Zellij-style)
Alt+[/] Cycle layout modes
Alt+Space Layout mode picker
Alt+h/j/k/l Navigate panes (Zellij-style)
Alt+Shift+h/j/k/l Resize panes
Alt+f Toggle pane zoom
Alt+x Close pane (no confirm)

Zellij-Style Layout System

The config includes a Zellij-inspired auto-layout system in layouts.lua:

Layout Modes

  • tiled - Grid layout, splits larger dimension (default)
  • vertical - All panes stacked top-to-bottom
  • horizontal - All panes side by side
  • main-vertical - Main pane left (60%), stack right (40%)
  • main-horizontal - Main pane top (60%), stack bottom (40%)

How It Works

  1. Each tab has a layout mode stored in wezterm.GLOBAL.layout_modes
  2. Alt+n or Cmd+D creates panes using the current mode's logic
  3. Alt+[ and Alt+] cycle through modes
  4. Status bar shows current mode (when in non-default)

Static Layout Templates

Access via Cmd+Shift+L or Leader+Shift+<key>:

  • dev - Editor + terminal stack (60/40)
  • editor - Full editor + bottom terminal
  • three_col - Three equal columns
  • quad - Four equal panes
  • stacked - Three horizontal rows
  • side_by_side - Two vertical columns
  • focus - Main pane + small sidebar
  • monitor - htop + logs (auto-starts htop)

Supported Terminal Editors

WezTerm is configured to work seamlessly with these terminal editors:

micro

  • Cmd+key mappings translate to Ctrl+key when micro is running (in keys/micro.lua)
  • Cmd+S → save, Cmd+Q → quit, Cmd+Z → undo, Cmd+C/V/X → copy/paste/cut
  • The leader key is Ctrl+B to avoid conflicts with micro's Ctrl+Q quit

Core Concepts

Tabs vs Panes vs Workspaces

  • Panes: Splits within a tab. Each pane is its own terminal session.
  • Tabs: Container for one or more panes. Shown in the tab bar.
  • Workspaces: Named collections of tabs/panes. Think of them as "projects" or "contexts".

iTerm-like Workflow

WezTerm's panes work similarly to iTerm's splits:

-- Cmd+W closes just the focused pane, not the whole tab
{ mods = "CMD", key = "w", action = act.CloseCurrentPane { confirm = false } }

Each split (Cmd+D) creates a self-contained pane. Cmd+W closes only that pane. When it's the last pane in a tab, the tab closes.

Fuzzy Finders & Switchers

Tab Picker (Cmd+P)

Fuzzy search through all open tabs by name/title:

{ mods = "CMD", key = "p", action = act.ShowLauncherArgs { flags = "FUZZY|TABS" } }

Workspace Picker (Cmd+Shift+S)

Switch between workspaces:

{ mods = "CMD|SHIFT", key = "s", action = act.ShowLauncherArgs { flags = "FUZZY|WORKSPACES" } }

Smart Workspace Switcher (Cmd+O)

Uses zoxide to fuzzy-find directories and create/switch workspaces:

local workspace_switcher = wezterm.plugin.require("https://github.com/MLFlexer/smart_workspace_switcher.wezterm")

{ mods = "CMD", key = "o", action = workspace_switcher.switch_workspace() }

Prerequisites:

brew install zoxide
echo 'eval "$(zoxide init zsh)"' >> ~/.zshrc

Pane Selector (Cmd+E)

Shows number overlay on each pane for quick switching:

{ mods = "CMD", key = "e", action = act.PaneSelect { alphabet = "1234567890", mode = "Activate" } }

Workspaces Deep Dive

What Are Workspaces?

Workspaces let you organize your terminal sessions by project or context. Each workspace maintains its own set of tabs and panes.

Creating Workspaces

  1. Via Smart Switcher (Cmd+O): Select a directory, and it creates a workspace named after that directory.

  2. Via Launcher (Cmd+L): Create a new workspace from the launcher menu.

  3. Programmatically:

wezterm.mux.spawn_window { workspace = "my-project" }

Switching Workspaces

  • Cmd+Shift+S - Fuzzy picker for existing workspaces
  • Cmd+O - Smart switcher (creates or switches)

Workspace Tips

  1. Name workspaces by project: When using the smart switcher, workspaces are auto-named by directory.

  2. Persist layouts: Workspaces remember their tab/pane layout during a session.

  3. Default workspace: Set a default workspace name:

config.default_workspace = "main"

Leader Key System

The leader key (Ctrl+B) enables tmux-style keybindings:

config.leader = { key = 'b', mods = 'CTRL', timeout_milliseconds = 1000 }

After pressing Ctrl+B, you have 1 second to press the next key:

Sequence Action
Ctrl+B- Split vertical
Ctrl+B| Split horizontal
Ctrl+Bh/j/k/l Navigate panes
Ctrl+Bz Toggle pane zoom
Ctrl+Bx Close pane (with confirm)
Ctrl+Bc New tab
Ctrl+Bn/p Next/previous tab
Ctrl+B1-5 Jump to tab by number
Ctrl+B[ Enter copy mode

Note: Using Ctrl+B as leader means you lose the "back one character" readline shortcut. Use Ctrl+A (beginning of line) remains available.

Plugins

Installing Plugins

WezTerm plugins are loaded via URL:

local plugin = wezterm.plugin.require("https://github.com/user/plugin-name.wezterm")
plugin.apply_to_config(config)

Installed Plugins

  1. smart_workspace_switcher - Zoxide-powered workspace management (Cmd+O)

  2. resurrect - Session persistence (Leader+s to save)

Tips & Tricks

1. Quick Directory Navigation

With the smart workspace switcher + zoxide, you can:

  • Cmd+O → type partial directory name → Enter
  • Instantly opens a new workspace in that directory

2. Pane Zoom for Focus

Ctrl+Bz temporarily maximizes the current pane. Press again to restore layout.

3. Copy Mode (Vim-style)

Ctrl+B[ enters copy mode:

  • v to start selection
  • y to yank
  • q to exit

4. Command Palette

Cmd+K opens the command palette - search for any WezTerm action.

5. Hot Reloading

WezTerm supports config hot-reloading. Edit any .lua file in ~/.config/wezterm/ and changes apply immediately.

6. Zen Mode

Leader+z toggles Zen mode - hides tab bar and removes padding for distraction-free work.

External Trigger System

The config includes a file-based trigger system that allows external tools (Karabiner, yabai, etc.) to invoke WezTerm actions without keystroke emulation.

How It Works

  1. External script writes action name to /tmp/wezterm.trigger
  2. Script focuses WezTerm via open -a WezTerm
  3. WezTerm's window-focus-changed event reads and consumes the trigger
  4. Action is executed (picker opens, command runs, etc.)

Available Triggers

Trigger Action
quick_open Open Quick Open picker (Cmd+P equivalent)
quick_open_cursor Open Quick Open in Cursor mode
workspaces Open workspace picker
command_palette Open command palette
launcher Open launcher
shortcuts Show keyboard shortcuts
app_launcher Show all installed macOS apps
copy_path Browse directories with fzf, copy path to clipboard
notepad Open ~/dev/notes/notes.md in micro, auto-commit on exit
themes Open theme picker
layouts Open layout picker
zen Toggle zen mode

Usage

# Trigger script location
~/.config/scripts/wezterm_trigger.sh <action>

# Examples
~/.config/scripts/wezterm_trigger.sh notepad
~/.config/scripts/wezterm_trigger.sh copy_path
~/.config/scripts/wezterm_trigger.sh app_launcher

Karabiner Integration

Add to karabiner.edn:

:templates {
  :wez-trigger "/Users/YOUR_USERNAME/.config/scripts/wezterm_trigger.sh \"%s\""
}

;; Then use in rules:
[:!Oa [:wez-trigger "app_launcher"]]  ;; Opt+A → App launcher
[:!On [:wez-trigger "notepad"]]        ;; Opt+N → Notepad

Helper Scripts

  • wezterm_trigger.sh - Main trigger dispatcher
  • wezterm_copy_path.sh - fzf-based directory browser (←/→ for navigation)
  • wezterm_notepad.sh - Opens micro editor, auto-commits on exit

Troubleshooting

Spawned Commands Can't Find Homebrew Tools (fzf, micro, etc.)

Symptom: Trigger opens a tab that immediately flashes and closes.

Cause: When WezTerm is launched via GUI automation (Karabiner, open -a), spawned processes don't inherit your shell's PATH. Tools in /opt/homebrew/bin aren't found.

Solution: The config sets set_environment_variables in wezterm.lua to ensure Homebrew paths are available:

config.set_environment_variables = {
  PATH = table.concat({
    "/opt/homebrew/bin",
    "/opt/homebrew/sbin",
    "/usr/local/bin",
    os.getenv("PATH") or "",
  }, ":"),
}

Debugging: Run the script directly in WezTerm to see errors:

~/.config/scripts/wezterm_copy_path.sh
# If it prints "command not found", PATH is the issue

Config Not Loading / Wrong Config

WezTerm searches for config files in this order:

  1. --config-file CLI argument
  2. $WEZTERM_CONFIG_FILE environment variable
  3. $XDG_CONFIG_HOME/wezterm/wezterm.lua (recommended: ~/.config/wezterm/wezterm.lua)
  4. ~/.wezterm.lua (legacy location)

IMPORTANT: If you have BOTH ~/.wezterm.lua AND ~/.config/wezterm/wezterm.lua, WezTerm may load the wrong one depending on environment variables. The WEZTERM_CONFIG_FILE env var takes precedence.

To debug which config is loading:

# Check for competing config files
ls -la ~/.wezterm.lua ~/.config/wezterm/wezterm.lua

# Check env var
echo $WEZTERM_CONFIG_FILE

# Test config and see keybindings
wezterm show-keys 2>&1 | head -30

# Test config with debug logging
WEZTERM_LOG=config=debug wezterm show-keys 2>&1 | grep -i error

Fix: Remove or rename ~/.wezterm.lua if you want to use ~/.config/wezterm/wezterm.lua:

mv ~/.wezterm.lua ~/.wezterm.lua.bak

Pane vs PaneInformation Objects

In event handlers like format-tab-title and format-window-title, the pane parameter is a PaneInformation table, not a Pane object:

  • PaneInformation: Use pane.current_working_dir (property access)
  • Pane object: Use pane:get_current_working_dir() (method call)

To handle both:

local cwd
if pane.get_current_working_dir then
  cwd = pane:get_current_working_dir()  -- Pane object
else
  cwd = pane.current_working_dir        -- PaneInformation table
end

Color Scheme Not Found

Color scheme names changed in upstream iTerm2-Color-Schemes:

  • 'Gruvbox Dark' (old name with space)
  • 'GruvboxDark' (new name, no space)

Debug Overlay

Press Ctrl+Shift+L to open the debug overlay - shows Lua errors and provides a REPL.

Ctrl+A Not Working?

If you set Ctrl+A as your leader key, it intercepts the readline "beginning of line" shortcut. Either:

  • Change leader to Ctrl+B or Ctrl+Space
  • Add a double-tap binding to send Ctrl+A through

Space Key Jumping to End of Line?

If you have custom ZLE bindings in zsh, check for bindkey " " that might be moving the cursor. Use zle self-insert for the fallback case.

Plugins Not Loading?

WezTerm downloads plugins on first use. Check your network connection and the plugin URL.

Theme System

Available Themes (High Contrast)

Defined in theme.lua, accessible via Cmd+Shift+T:

  • Hardcore (default) - Maximum contrast
  • Solarized Dark Higher Contrast
  • Dracula
  • Catppuccin Mocha
  • GruvboxDark
  • Tokyo Night
  • Selenized Dark (Gogh)
  • Snazzy

Dynamic Theming

  • scheme_for_cwd() in helpers.lua can map directories to color schemes
  • User-selected themes persist in wezterm.GLOBAL.user_selected_theme
  • The update-status event respects user selection over auto-theming

Quick Open Picker (Cmd+P / Cmd+N)

The custom picker in pickers.lua:

  • Shows zoxide frecent directories
  • green = tab already open (switches to it)
  • yellow = no tab (opens new)
  • Type a non-matching path to create new directory
  • Sorts open tabs by most recently focused

Common Development Tasks

Adding a New Keybinding

  1. Identify the category: micro, navigation, layouts, or power
  2. Edit the appropriate file in keys/
  3. Add to the get_keys() return table
  4. Test with wezterm show-keys | grep YOUR_KEY

Adding a New Layout Template

  1. Edit layouts.lua
  2. Add function to M.templates table
  3. Add metadata to M.layout_list
  4. Access via Cmd+Shift+L picker or add hotkey in keys/layouts.lua

Debugging

# Validate config loads
wezterm show-keys 2>&1 | head -5

# Check for Lua errors
WEZTERM_LOG=config=debug wezterm show-keys 2>&1 | grep -i error

# Open debug overlay in WezTerm
Ctrl+Shift+L

WezTerm Lua Config Reference

Comprehensive reference for wezterm.lua configuration options.

Window & Display Options

Option Type Description
initial_cols / initial_rows int Initial window dimensions
window_decorations string "TITLE", "RESIZE", "NONE", `"TITLE
window_padding table {left, right, top, bottom} in pixels
window_background_opacity float 0.0-1.0, requires compositor
window_background_gradient table {orientation, colors, ...}
window_close_confirmation string "AlwaysPrompt", "NeverPrompt"
adjust_window_size_when_changing_font_size bool Resize window with font
window_frame table Frame colors, fonts for fancy tab bar
window_content_alignment string Where to place excess pixels
integrated_title_buttons table {"Hide", "Maximize", "Close"}
macos_window_background_blur int macOS blur radius
native_macos_fullscreen_mode bool Use native fullscreen

Tab Bar Options

Option Type Description
enable_tab_bar bool Show/hide tab bar
hide_tab_bar_if_only_one_tab bool Auto-hide with single tab
tab_bar_at_bottom bool Position at bottom
use_fancy_tab_bar bool Native-style vs retro
tab_max_width int Max tab width in cells
show_tab_index_in_tab_bar bool Show tab numbers
show_new_tab_button_in_tab_bar bool Show + button
show_close_tab_button_in_tabs bool Show X on tabs
tab_bar_style table Custom tab edge styling

Font Options

Option Type Description
font Font wezterm.font("Name") or wezterm.font_with_fallback({...})
font_size float Point size
line_height float Multiplier (1.0 = normal)
cell_width float Horizontal scaling
font_rules table Conditional font selection
font_dirs table Additional font directories
font_locator string "ConfigDirsOnly", "FontConfig", "CoreText"
font_shaper string "Harfbuzz", "Allsorts"
harfbuzz_features table {"calt=0", "liga=0", ...}
freetype_load_target string "Normal", "Light", "Mono", "HorizontalLcd"
freetype_load_flags string "DEFAULT", "NO_HINTING", "FORCE_AUTOHINT"
bold_brightens_ansi_colors bool/string true, false, "BrightAndBold", "BrightOnly"
allow_square_glyphs_to_overflow_width string "Never", "Always", "WhenFollowedBySpace"

Color Options

Option Type Description
color_scheme string Scheme name from built-in or custom
color_scheme_dirs table Additional scheme directories
colors table Individual color overrides
colors.foreground / background string Hex colors "#rrggbb"
colors.cursor_bg / cursor_fg / cursor_border string Cursor colors
colors.selection_fg / selection_bg string Selection colors
colors.ansi / brights table 8-color ANSI palette
colors.tab_bar table Tab bar color overrides
colors.split string Pane split color
force_reverse_video_cursor bool Invert cursor colors
inactive_pane_hsb table {hue, saturation, brightness} for dimming

Cursor Options

Option Type Description
default_cursor_style string "SteadyBlock", "BlinkingBlock", "SteadyUnderline", "BlinkingUnderline", "SteadyBar", "BlinkingBar"
cursor_blink_rate int Milliseconds (0 = no blink)
cursor_blink_ease_in / ease_out string Easing function
cursor_thickness float Thickness multiplier
animation_fps int Cursor animation framerate

Input & Keyboard Options

Option Type Description
keys table Key bindings {key, mods, action}
key_tables table Named key tables for modes
leader table {key, mods, timeout_milliseconds}
mouse_bindings table Mouse button bindings
disable_default_key_bindings bool Start with empty keymap
disable_default_mouse_bindings bool Start with empty mouse map
enable_csi_u_key_encoding bool Modern key encoding
enable_kitty_keyboard bool Kitty keyboard protocol
use_ime bool Enable input method editor
debug_key_events bool Log key events
swap_backspace_and_delete bool Swap BS/DEL
send_composed_key_when_left_alt_is_pressed bool macOS alt behavior
send_composed_key_when_right_alt_is_pressed bool macOS alt behavior

Scrolling & Selection

Option Type Description
scrollback_lines int Lines to keep (default 3500)
enable_scroll_bar bool Show scrollbar
min_scroll_bar_height string e.g. "2cell"
scroll_to_bottom_on_input bool Auto-scroll on input
selection_word_boundary string Characters that break word selection
quick_select_alphabet string Characters for quick select hints
quick_select_patterns table Regex patterns for quick select

Program & Shell Options

Option Type Description
default_prog table {"/bin/zsh", "-l"}
default_cwd string Starting directory
default_domain string Domain name
default_workspace string Initial workspace name
launch_menu table Launcher menu entries
set_environment_variables table {VAR = "value", ...}
term string $TERM value (default "xterm-256color")

Domain Options

Option Type Description
unix_domains table Unix socket mux domains
ssh_domains table SSH connection domains
wsl_domains table WSL distribution domains
tls_clients / tls_servers table TLS mux connections
ssh_backend string "Libssh", "Ssh2"

Behavior Options

Option Type Description
automatically_reload_config bool Hot reload on file change
check_for_updates bool Update notifications
audible_bell string "Disabled", "SystemBeep"
visual_bell table Flash settings
exit_behavior string "Close", "Hold", "CloseOnCleanExit"
exit_behavior_messaging string Message on exit
clean_exit_codes table Exit codes considered clean
status_update_interval int Milliseconds for status updates
hyperlink_rules table URL detection patterns
detect_password_input bool Password input detection
canonicalize_pasted_newlines string "None", "LineFeed", "CarriageReturn", "CarriageReturnAndLineFeed"
unicode_version int Unicode version for width
unzoom_on_switch_pane bool Unzoom when switching panes

Rendering & Performance

Option Type Description
front_end string "WebGpu", "OpenGL", "Software"
webgpu_power_preference string "LowPower", "HighPerformance"
max_fps int Maximum framerate
prefer_egl bool Prefer EGL over GLX
enable_wayland bool Use Wayland if available

KeyAssignment Actions (for action = ...)

Window/App: ToggleFullScreen, Hide, Show, QuitApplication, ActivateWindow, ActivateWindowRelative

Tabs: SpawnTab, ActivateTab(n), ActivateTabRelative(n), ActivateLastTab, CloseCurrentTab, MoveTab(n), MoveTabRelative(n), ShowTabNavigator

Panes: SplitHorizontal, SplitVertical, SplitPane{direction, size, ...}, CloseCurrentPane, ActivatePaneDirection("Left"|"Right"|"Up"|"Down"), ActivatePaneByIndex(n), TogglePaneZoomState, RotatePanes("Clockwise"|"CounterClockwise"), AdjustPaneSize("Left"|"Right"|"Up"|"Down", n), PaneSelect{alphabet, mode}

Clipboard: Copy, Paste, PasteFrom("Clipboard"|"PrimarySelection"), CopyTo("Clipboard"|"PrimarySelection"|"ClipboardAndPrimarySelection"), ClearSelection

Scrolling: ScrollByLine(n), ScrollByPage(n), ScrollToTop, ScrollToBottom, ScrollToPrompt(n)

Font: IncreaseFontSize, DecreaseFontSize, ResetFontSize, ResetFontAndWindowSize

Search/Select: Search, QuickSelect, QuickSelectArgs{...}, CharSelect, ActivateCopyMode

Misc: ShowLauncher, ShowLauncherArgs{flags}, ActivateCommandPalette, ShowDebugOverlay, ReloadConfiguration, ClearScrollback, ResetTerminal, SendString("text"), SendKey{key, mods}, EmitEvent("event-name"), Nop, DisableDefaultAssignment, Multiple{action1, action2, ...}, SwitchToWorkspace{name, spawn}, SwitchWorkspaceRelative(n)

wezterm Module Functions

Config: config_builder(), config_dir, config_file

Fonts: font("Name", {weight="Bold", ...}), font_with_fallback({"Font1", "Font2"}), get_builtin_color_schemes()

Events: on("event-name", function(window, pane) ... end), emit("event-name", ...)

Actions: action.ActionName, action_callback(function(window, pane) ... end)

Formatting: format({...}), strftime(fmt), strftime_utc(fmt), pad_left(str, width), pad_right(str, width), truncate_left(str, width), truncate_right(str, width)

System: hostname(), home_dir, executable_dir, target_triple, version, running_under_wsl(), battery_info(), run_child_process(args), background_child_process(args)

Data: json_encode(value), json_parse(str), split_by_newlines(str), column_width(str), utf16_to_utf8(str)

Shell: shell_split(str), shell_join_args(table), shell_quote_arg(str)

Files: read_dir(path), glob(pattern), enumerate_ssh_hosts()

Logging: log_info(msg), log_warn(msg), log_error(msg)

Utilities: sleep_ms(ms), nerdfonts, permute_any_mods(table), permute_any_or_no_mods(table)

Event Hooks

Event Parameters Purpose
gui-startup cmd Initial window setup
gui-attached domain Domain attached
mux-startup Mux server startup
update-status window, pane Status bar updates
update-right-status window, pane Right status updates
format-tab-title tab, tabs, panes, config, hover, max_width Tab title formatting
format-window-title tab, pane, tabs, panes, config Window title
window-config-reloaded window Config reload notification
window-focus-changed window, pane Focus changed
window-resized window, pane Window resized
bell window, pane Bell triggered
user-var-changed window, pane, name, value User var changed
open-uri window, pane, uri URI opened
new-tab-button-click window, pane, button, held_keys New tab button clicked

Common Config Patterns

Custom key binding:

config.keys = {
  {key = "d", mods = "CMD", action = wezterm.action.SplitHorizontal{domain="CurrentPaneDomain"}},
  {key = "w", mods = "CMD", action = wezterm.action.CloseCurrentPane{confirm=false}},
}

Leader key + table:

config.leader = {key = "b", mods = "CTRL", timeout_milliseconds = 1000}
config.keys = {
  {key = "-", mods = "LEADER", action = wezterm.action.SplitVertical{domain="CurrentPaneDomain"}},
}

Key tables (modal keys):

config.key_tables = {
  resize_pane = {
    {key = "h", action = wezterm.action.AdjustPaneSize{"Left", 1}},
    {key = "Escape", action = "PopKeyTable"},
  },
}
config.keys = {
  {key = "r", mods = "LEADER", action = wezterm.action.ActivateKeyTable{name="resize_pane", one_shot=false}},
}

Dynamic status bar:

wezterm.on("update-status", function(window, pane)
  window:set_right_status(wezterm.format({
    {Foreground = {Color = "#808080"}},
    {Text = wezterm.strftime("%H:%M")},
  }))
end)

Custom tab title:

wezterm.on("format-tab-title", function(tab, tabs, panes, config, hover, max_width)
  local title = tab.active_pane.title
  if tab.is_active then
    return {{Background = {Color = "#1a1b26"}}, {Text = " " .. title .. " "}}
  end
  return " " .. title .. " "
end)

Font with fallback:

config.font = wezterm.font_with_fallback({
  "JetBrains Mono",
  "Symbols Nerd Font Mono",
  "Noto Color Emoji",
})

Conditional config (per-OS):

if wezterm.target_triple:find("darwin") then
  config.font_size = 14
elseif wezterm.target_triple:find("windows") then
  config.default_prog = {"pwsh.exe"}
end

Background image:

config.background = {
  {source = {File = "/path/to/image.png"}, hsb = {brightness = 0.02}},
  {source = {Color = "#1a1b26"}, width = "100%", height = "100%", opacity = 0.9},
}

SSH domain:

config.ssh_domains = {
  {name = "server", remote_address = "user@host", username = "user"},
}

FormatItem Types (for wezterm.format)

  • {Text = "string"} - Plain text
  • {Foreground = {Color = "#hex"}} - Text color
  • {Background = {Color = "#hex"}} - Background color
  • {Foreground = {AnsiColor = "Red"}} - ANSI color
  • {Attribute = {Intensity = "Bold"}} - Bold
  • {Attribute = {Italic = true}} - Italic
  • {Attribute = {Underline = "Single"}} - Underline
  • "ResetAttributes" - Reset formatting

Resources

-- ============================================================================
-- EVENTS MODULE
-- ============================================================================
-- Event handlers for tab titles, window titles, status bar, etc.
local wezterm = require 'wezterm'
local helpers = require 'helpers'
local theme = require 'theme'
local layouts = require 'layouts'
local pickers = require 'pickers'
local act = wezterm.action
local M = {}
-- ============================================================================
-- EXTERNAL TRIGGER SYSTEM (Approach A)
-- ============================================================================
-- Allows external scripts (Karabiner, yabai, etc.) to trigger WezTerm actions
-- by writing to /tmp/wezterm.trigger
local TRIGGER_FILE = "/tmp/wezterm.trigger"
local function read_trigger()
local f = io.open(TRIGGER_FILE, "r")
if not f then return nil end
local content = f:read("*a")
f:close()
if not content then return nil end
-- Trim whitespace
content = content:gsub("^%s+", ""):gsub("%s+$", "")
if content == "" then return nil end
return content
end
local function consume_trigger()
local action = read_trigger()
if action then
os.remove(TRIGGER_FILE)
end
return action
end
-- Process external triggers (called on focus change for instant response)
local function process_trigger(window, pane)
-- Check if trigger file exists first (avoid unnecessary IO)
local f = io.open(TRIGGER_FILE, "r")
if not f then return end
f:close()
local trigger = consume_trigger()
if not trigger then return end
wezterm.log_info("TRIGGER: Processing '" .. trigger .. "'")
-- Debounce: prevent re-triggering within 1 second
wezterm.GLOBAL._last_trigger = wezterm.GLOBAL._last_trigger or { v = nil, t = 0 }
local now = os.time()
if wezterm.GLOBAL._last_trigger.v == trigger and (now - wezterm.GLOBAL._last_trigger.t) < 1 then
return
end
wezterm.GLOBAL._last_trigger = { v = trigger, t = now }
wezterm.log_info("Processing trigger: " .. trigger)
-- Parse trigger (format: "action" or "action:arg")
local action, arg = trigger:match("^([^:]+):?(.*)$")
if action == "quick_open" then
pickers.show_quick_open_picker(window, pane)
elseif action == "quick_open_cursor" then
pickers.show_quick_open_picker(window, pane, 'cursor')
elseif action == "workspaces" then
window:perform_action(act.ShowLauncherArgs { flags = "FUZZY|WORKSPACES" }, pane)
elseif action == "command_palette" then
window:perform_action(act.ActivateCommandPalette, pane)
elseif action == "launcher" then
window:perform_action(act.ShowLauncher, pane)
elseif action == "shortcuts" then
pickers.show_shortcuts_picker(window, pane)
elseif action == "app_launcher" then
pickers.show_app_launcher(window, pane)
elseif action == "copy_path" then
-- Use fzf-based script for natural arrow key navigation
local home = os.getenv('HOME')
window:perform_action(
act.SpawnCommandInNewTab {
args = { '/bin/bash', '-c', home .. '/.config/scripts/wezterm_copy_path.sh' },
cwd = home,
},
pane
)
elseif action == "notepad" then
-- Open notes in micro, auto-commit on exit (needs interactive shell)
local home = os.getenv('HOME')
window:perform_action(
act.SpawnCommandInNewTab {
args = { '/bin/bash', '-c', home .. '/.config/scripts/wezterm_notepad.sh' },
cwd = home .. '/dev/notes',
},
pane
)
elseif action == "themes" then
-- Trigger theme picker (same as CMD+SHIFT+T)
local choices = {}
for _, t in ipairs(theme.high_contrast_themes) do
table.insert(choices, { id = t.id, label = t.name .. ' - ' .. t.desc })
end
window:perform_action(
act.InputSelector({
title = "Select Theme",
choices = choices,
fuzzy = true,
action = wezterm.action_callback(function(inner_window, inner_pane, id, label)
if id then
wezterm.GLOBAL.user_selected_theme = id
local overrides = inner_window:get_config_overrides() or {}
overrides.color_scheme = id
inner_window:set_config_overrides(overrides)
end
end),
}),
pane
)
elseif action == "layouts" then
-- Trigger layout picker (same as CMD+SHIFT+L)
local choices = {}
for _, layout in ipairs(layouts.layout_list) do
table.insert(choices, { id = layout.id, label = layout.name .. ' - ' .. layout.desc })
end
window:perform_action(
act.InputSelector({
title = "Select Layout",
choices = choices,
action = wezterm.action_callback(function(inner_window, inner_pane, id, label)
if id then
layouts.apply_layout(inner_window, id)
end
end),
}),
pane
)
elseif action == "zen" then
-- Toggle zen mode
local overrides = window:get_config_overrides() or {}
if overrides.enable_tab_bar == false then
overrides.enable_tab_bar = true
overrides.window_padding = { left = 10, right = 10, top = 10, bottom = 10 }
else
overrides.enable_tab_bar = false
overrides.window_padding = { left = 0, right = 0, top = 0, bottom = 0 }
end
window:set_config_overrides(overrides)
else
wezterm.log_warn("Unknown trigger action: " .. action)
end
end
function M.setup()
-- Initialize global state
wezterm.GLOBAL.user_selected_theme = wezterm.GLOBAL.user_selected_theme or nil
wezterm.GLOBAL.claude_alerts = wezterm.GLOBAL.claude_alerts or {}
wezterm.GLOBAL.recording_mode = wezterm.GLOBAL.recording_mode or false
-- INSTANT TRIGGER PROCESSING on focus (much faster than update-status polling)
wezterm.on('window-focus-changed', function(window, pane, focused)
if focused then
process_trigger(window, pane)
end
end)
-- CUSTOM TAB TITLES
wezterm.on('format-tab-title', function(tab, tabs, panes, config, hover, max_width)
local pane = tab.active_pane
local cwd = pane.current_working_dir
local dir_name = "~"
if cwd and cwd.file_path then
dir_name = cwd.file_path:match("([^/]+)$") or "~"
end
-- Check for Claude alert on this tab (use string key for GLOBAL table)
local tab_id_str = tostring(tab.tab_id)
local alert = wezterm.GLOBAL.claude_alerts[tab_id_str]
local indicator = ""
local alert_bg = nil
if alert and not tab.is_active then
local elapsed = os.time() - alert.time
if elapsed < 60 then -- Show alert for 60 seconds
-- Pulse effect: alternate icons based on time
local pulse = math.floor(elapsed * 2) % 2 == 0
if alert.type == 'stop' then
indicator = pulse and "🔴 " or "⭕ "
alert_bg = pulse and "#662222" or "#442222"
else -- notification
indicator = pulse and "🔔 " or "🔕 "
alert_bg = pulse and "#664422" or "#443311"
end
else
-- Alert expired, clear it
wezterm.GLOBAL.claude_alerts[tab_id_str] = nil
end
end
-- Fallback to unseen output indicator if no Claude alert
if indicator == "" then
for _, p in ipairs(tab.panes) do
if p.has_unseen_output then
indicator = "● "
break
end
end
end
local zoom = ""
if tab.active_pane.is_zoomed then
zoom = "🔍 "
end
local title = string.format(" %s%s%s ", indicator, zoom, dir_name)
if tab.is_active then
-- Clear alert when tab becomes active
wezterm.GLOBAL.claude_alerts[tab_id_str] = nil
return {
{ Background = { Color = theme.colors.bg_selection } },
{ Foreground = { Color = theme.colors.fg_bright } },
{ Text = title },
}
end
-- Use alert background if present, otherwise default
local bg_color = alert_bg or theme.colors.bg
return {
{ Background = { Color = bg_color } },
{ Foreground = { Color = theme.colors.fg_dim } },
{ Text = title },
}
end)
-- WINDOW TITLE
wezterm.on('format-window-title', function(tab, pane, tabs, panes, cfg)
return helpers.short_cwd(pane)
end)
-- STATUS BAR
wezterm.on("update-status", function(window, pane)
-- Fallback trigger check (for when WezTerm is already focused)
if window:is_focused() then
process_trigger(window, pane)
end
-- Track directory focus time
local cwd = pane:get_current_working_dir()
if cwd and cwd.file_path then
layouts.record_dir_focus(cwd.file_path)
end
-- Dynamic color scheme based on cwd (MERGE into existing overrides, don't replace!)
if not wezterm.GLOBAL.user_selected_theme then
local scheme = helpers.scheme_for_cwd(pane)
local overrides = window:get_config_overrides() or {}
if scheme and scheme ~= overrides.color_scheme then
overrides.color_scheme = scheme
window:set_config_overrides(overrides)
elseif not scheme and overrides.color_scheme then
overrides.color_scheme = nil
window:set_config_overrides(overrides)
end
end
-- Recording mode: hide all status bar content
if wezterm.GLOBAL.recording_mode then
window:set_left_status("")
window:set_right_status("")
return
end
-- Disable left status bar
window:set_left_status("")
-- Build right status bar
local cells = {}
-- Mode indicator
local mode = window:active_key_table()
if mode then
table.insert(cells, { Foreground = { Color = theme.colors.pink } })
table.insert(cells, { Attribute = { Intensity = "Bold" } })
table.insert(cells, { Text = " " .. mode:upper() .. " │" })
table.insert(cells, { Attribute = { Intensity = "Normal" } })
end
-- Leader key indicator
if window:leader_is_active() then
table.insert(cells, { Foreground = { Color = theme.colors.orange } })
table.insert(cells, { Attribute = { Intensity = "Bold" } })
table.insert(cells, { Text = " LEADER │" })
table.insert(cells, { Attribute = { Intensity = "Normal" } })
end
-- Current working directory
local cwd = pane:get_current_working_dir()
if cwd then
local home = os.getenv("HOME") or ""
local path = cwd.file_path:gsub(home, "~")
if #path > 40 then
path = "…" .. path:sub(-39)
end
table.insert(cells, { Foreground = { Color = theme.colors.fg_dim } })
table.insert(cells, { Text = " " .. path })
end
window:set_right_status(wezterm.format(cells))
end)
-- SLID PRESENTATION MODE
wezterm.on('user-var-changed', function(window, pane, name, value)
if name == 'slid_presentation' then
local overrides = window:get_config_overrides() or {}
if value == '1' then
overrides.enable_tab_bar = false
overrides.window_padding = { left = 0, right = 0, top = 0, bottom = 0 }
else
overrides.enable_tab_bar = nil
overrides.window_padding = nil
end
window:set_config_overrides(overrides)
end
-- CLAUDE ALERT HANDLER
if name == 'claude_alert' then
local tab = pane:tab()
if tab then
local tab_id = tostring(tab:tab_id())
wezterm.GLOBAL.claude_alerts[tab_id] = {
time = os.time(),
type = value, -- 'stop' or 'notification'
}
wezterm.log_info('Claude alert received: ' .. value .. ' for tab ' .. tab_id)
end
end
-- CLAUDE PANE STATE HANDLER - Changes background when Claude is waiting
if name == 'claude_pane_state' then
wezterm.log_info('Claude pane state received: ' .. tostring(value))
local overrides = window:get_config_overrides() or {}
if value == 'waiting' then
-- Apply a tinted color scheme or custom colors for waiting state
overrides.colors = overrides.colors or {}
overrides.colors.background = '#1a0808' -- Dark red tint
wezterm.log_info('Setting waiting background: #1a0808')
else
-- Reset to default (remove custom background)
if overrides.colors then
overrides.colors.background = nil
if next(overrides.colors) == nil then
overrides.colors = nil
end
end
wezterm.log_info('Resetting to default background')
end
window:set_config_overrides(overrides)
end
end)
-- BELL HANDLER - Also triggers tab alert
wezterm.on('bell', function(window, pane)
local tab = pane:tab()
if tab then
local tab_id = tostring(tab:tab_id())
local active_tab = window:active_tab()
local is_active = active_tab and tostring(active_tab:tab_id()) == tab_id
if not is_active then
-- Only set alert if not already set (don't override claude_alert with bell)
if not wezterm.GLOBAL.claude_alerts[tab_id] then
wezterm.GLOBAL.claude_alerts[tab_id] = {
time = os.time(),
type = 'bell',
}
end
end
end
end)
end
return M
-- ============================================================================
-- HELPER FUNCTIONS MODULE
-- ============================================================================
-- Utility functions used across the WezTerm configuration
local wezterm = require 'wezterm'
local M = {}
-- Centralized zoxide path (avoids hardcoding in multiple places)
M.zoxide_path = os.getenv("ZOXIDE_BIN") or "/opt/homebrew/bin/zoxide"
-- Detect if the current pane is running Neovim/Vim
function M.is_vim(pane)
local process_info = pane:get_foreground_process_info()
local process_name = process_info and process_info.executable or ""
return process_name:find("n?vim") ~= nil
end
-- Detect if the current pane is running micro editor
function M.is_micro(pane)
local process_info = pane:get_foreground_process_info()
local process_name = process_info and process_info.executable or ""
return process_name:find("micro") ~= nil
end
-- Detect if running any terminal editor (vim, neovim, micro)
function M.is_editor(pane)
return M.is_vim(pane) or M.is_micro(pane)
end
-- Percent helper for pane:split size
function M.pct(n)
return n / 100.0
end
-- Safe cwd extraction (handles both string and Url object formats)
function M.cwd_path(cwd)
if not cwd then return nil end
if type(cwd) == "string" then return cwd end
return cwd.file_path
end
-- Shorten cwd for display (shows last 2 path components)
-- Handles both Pane objects (method call) and PaneInformation tables (property access)
function M.short_cwd(pane)
local cwd
local ok, result = pcall(function() return pane:get_current_working_dir() end)
if ok and result then
cwd = result
else
cwd = pane.current_working_dir
end
if not cwd then return "~" end
local home = os.getenv("HOME") or ""
local path = cwd.file_path:gsub(home, "~")
local last_two = path:match("([^/]+/[^/]+)$")
return last_two or path:match("([^/]+)$") or path
end
-- Map cwd patterns to color schemes
function M.scheme_for_cwd(pane)
local cwd = pane:get_current_working_dir()
if not cwd or not cwd.file_path then return nil end
local home = os.getenv("HOME") or ""
local path = cwd.file_path:gsub("^file://", ""):gsub(home, "~")
-- Define your project-to-scheme mappings here
local mappings = {
-- { pattern = "~/dev/special-project", scheme = "Catppuccin Mocha" },
}
for _, m in ipairs(mappings) do
if path:find(m.pattern, 1, true) then
return m.scheme
end
end
return nil
end
return M
-- ============================================================================
-- KEYBINDINGS MODULE
-- ============================================================================
-- Aggregates all keybindings from sub-modules
local wezterm = require 'wezterm'
local act = wezterm.action
local micro_keys = require 'keys.micro'
local navigation_keys = require 'keys.navigation'
local layout_keys = require 'keys.layouts'
local power_keys = require 'keys.power'
local M = {}
function M.apply(config, workspace_switcher, resurrect)
-- Leader key configuration
config.leader = { key = 'b', mods = 'CTRL', timeout_milliseconds = 2000 }
-- Aggregate all keys
config.keys = {}
-- Add micro editor keys
for _, key in ipairs(micro_keys.get_keys()) do
table.insert(config.keys, key)
end
-- Add navigation keys
for _, key in ipairs(navigation_keys.get_keys(workspace_switcher)) do
table.insert(config.keys, key)
end
-- Add layout keys
for _, key in ipairs(layout_keys.get_keys()) do
table.insert(config.keys, key)
end
-- Add power user keys
for _, key in ipairs(power_keys.get_keys(resurrect, config.color_scheme)) do
table.insert(config.keys, key)
end
-- Key tables for modal keybindings
config.key_tables = {
resize_pane = {
{ key = 'h', action = act.AdjustPaneSize { 'Left', 1 } },
{ key = 'j', action = act.AdjustPaneSize { 'Down', 1 } },
{ key = 'k', action = act.AdjustPaneSize { 'Up', 1 } },
{ key = 'l', action = act.AdjustPaneSize { 'Right', 1 } },
{ key = 'Escape', action = 'PopKeyTable' },
{ key = 'Enter', action = 'PopKeyTable' },
},
}
-- Add resize mode activation
table.insert(config.keys, {
mods = "LEADER",
key = "r",
action = act.ActivateKeyTable { name = 'resize_pane', one_shot = false },
})
end
return M
-- ============================================================================
-- LAYOUT KEYBINDINGS
-- ============================================================================
-- Pane splitting and layout management
local wezterm = require 'wezterm'
local act = wezterm.action
local layouts = require 'layouts'
local M = {}
function M.get_keys()
return {
-- PANE SPLITTING
{ mods = "LEADER", key = "-", action = act.SplitVertical { domain = "CurrentPaneDomain" } },
{ mods = "LEADER", key = "|", action = act.SplitHorizontal { domain = "CurrentPaneDomain" } },
-- Next pane (cycle through panes)
{ mods = "CMD", key = "d", action = act.ActivatePaneDirection "Next" },
-- Previous pane (cycle back through panes)
{ mods = "CMD|SHIFT", key = "d", action = act.ActivatePaneDirection "Prev" },
-- ZELLIJ-STYLE AUTO-LAYOUT
{
mods = "ALT",
key = "n",
action = wezterm.action_callback(function(window, pane)
layouts.smart_new_pane(window, pane)
end),
},
{
mods = "ALT",
key = "]",
action = wezterm.action_callback(function(window, pane)
local tab = window:active_tab()
local current = layouts.get_layout_mode(tab)
local new_mode = layouts.cycle_layout_mode(current, 1)
layouts.set_layout_mode(tab, new_mode)
local mode_name = new_mode
for _, m in ipairs(layouts.LAYOUT_MODES) do
if m.id == new_mode then mode_name = m.name .. ' - ' .. m.desc break end
end
window:toast_notification('Layout', mode_name, nil, 1500)
end),
},
{
mods = "ALT",
key = "[",
action = wezterm.action_callback(function(window, pane)
local tab = window:active_tab()
local current = layouts.get_layout_mode(tab)
local new_mode = layouts.cycle_layout_mode(current, -1)
layouts.set_layout_mode(tab, new_mode)
local mode_name = new_mode
for _, m in ipairs(layouts.LAYOUT_MODES) do
if m.id == new_mode then mode_name = m.name .. ' - ' .. m.desc break end
end
window:toast_notification('Layout', mode_name, nil, 1500)
end),
},
{
mods = "ALT",
key = "Space",
action = wezterm.action.InputSelector({
title = "Select Layout Mode (Zellij-style)",
choices = (function()
local choices = {}
for _, mode in ipairs(layouts.LAYOUT_MODES) do
table.insert(choices, { id = mode.id, label = mode.name .. ' - ' .. mode.desc })
end
return choices
end)(),
action = wezterm.action_callback(function(window, pane, id, label)
if id then
local tab = window:active_tab()
layouts.set_layout_mode(tab, id)
end
end),
}),
},
{
mods = "ALT",
key = "r",
action = wezterm.action_callback(function(window, pane)
window:perform_action(act.RotatePanes "Clockwise", pane)
end),
},
{ mods = "ALT", key = "=", action = act.SetPaneZoomState(false) },
{
mods = "ALT",
key = "Enter",
action = act.Multiple({
act.SetPaneZoomState(false),
act.AdjustPaneSize { "Left", 15 },
act.AdjustPaneSize { "Right", 15 },
act.AdjustPaneSize { "Up", 8 },
act.AdjustPaneSize { "Down", 8 },
}),
},
{
mods = "ALT",
key = "x",
action = wezterm.action_callback(function(window, pane)
window:perform_action(act.CloseCurrentPane { confirm = false }, pane)
end),
},
-- STATIC LAYOUT TEMPLATES
{
mods = "CMD|SHIFT",
key = "l",
action = wezterm.action.InputSelector({
title = "Select Layout",
choices = (function()
local choices = {}
for _, layout in ipairs(layouts.layout_list) do
table.insert(choices, { id = layout.id, label = layout.name .. ' - ' .. layout.desc })
end
return choices
end)(),
action = wezterm.action_callback(function(window, pane, id, label)
if id then
layouts.apply_layout(window, id)
end
end),
}),
},
{ mods = "LEADER|SHIFT", key = "d", action = wezterm.action_callback(function(w, p) layouts.apply_layout(w, 'dev') end) },
{ mods = "LEADER|SHIFT", key = "e", action = wezterm.action_callback(function(w, p) layouts.apply_layout(w, 'editor') end) },
{ mods = "LEADER|SHIFT", key = "3", action = wezterm.action_callback(function(w, p) layouts.apply_layout(w, 'three_col') end) },
{ mods = "LEADER|SHIFT", key = "4", action = wezterm.action_callback(function(w, p) layouts.apply_layout(w, 'quad') end) },
{ mods = "LEADER|SHIFT", key = "s", action = wezterm.action_callback(function(w, p) layouts.apply_layout(w, 'stacked') end) },
{ mods = "LEADER|SHIFT", key = "v", action = wezterm.action_callback(function(w, p) layouts.apply_layout(w, 'side_by_side') end) },
{ mods = "LEADER|SHIFT", key = "f", action = wezterm.action_callback(function(w, p) layouts.apply_layout(w, 'focus') end) },
{ mods = "LEADER|SHIFT", key = "m", action = wezterm.action_callback(function(w, p) layouts.apply_layout(w, 'monitor') end) },
{ mods = "LEADER", key = "Enter", action = act.PaneSelect { mode = "SwapWithActive" } },
}
end
return M
-- ============================================================================
-- MICRO EDITOR KEYBINDINGS
-- ============================================================================
-- Mac-style Cmd key mappings for micro editor
local wezterm = require 'wezterm'
local act = wezterm.action
local helpers = require 'helpers'
local M = {}
function M.get_keys()
return {
{
mods = "CMD",
key = "s",
action = wezterm.action_callback(function(window, pane)
if helpers.is_micro(pane) then
window:perform_action(act.SendKey({ key = 's', mods = 'CTRL' }), pane)
else
-- Cycle to next pane
local tab = window:active_tab()
local panes = tab:panes()
if #panes > 1 then
local current_idx = 0
for i, p in ipairs(panes) do
if p:pane_id() == pane:pane_id() then
current_idx = i
break
end
end
local next_idx = (current_idx % #panes) -- 0-based for ActivatePaneByIndex
window:perform_action(act.ActivatePaneByIndex(next_idx), pane)
end
end
end),
},
{
mods = "CMD",
key = "q",
action = wezterm.action_callback(function(window, pane)
if helpers.is_micro(pane) then
window:perform_action(act.SendKey({ key = 'q', mods = 'CTRL' }), pane)
else
window:perform_action(act.QuitApplication, pane)
end
end),
},
{
mods = "CMD",
key = "z",
action = wezterm.action_callback(function(window, pane)
if helpers.is_micro(pane) then
window:perform_action(act.SendKey({ key = 'z', mods = 'CTRL' }), pane)
else
window:perform_action(act.SendKey({ key = 'z', mods = 'CMD' }), pane)
end
end),
},
{
mods = "CMD|SHIFT",
key = "z",
action = wezterm.action_callback(function(window, pane)
if helpers.is_micro(pane) then
window:perform_action(act.SendKey({ key = 'y', mods = 'CTRL' }), pane)
else
window:perform_action(act.SendKey({ key = 'z', mods = 'CMD|SHIFT' }), pane)
end
end),
},
{
mods = "CMD",
key = "c",
action = wezterm.action_callback(function(window, pane)
if helpers.is_micro(pane) then
window:perform_action(act.SendKey({ key = 'c', mods = 'CTRL' }), pane)
else
window:perform_action(act.CopyTo('Clipboard'), pane)
end
end),
},
{
mods = "CMD",
key = "v",
action = wezterm.action_callback(function(window, pane)
if helpers.is_micro(pane) then
window:perform_action(act.SendKey({ key = 'v', mods = 'CTRL' }), pane)
else
window:perform_action(act.PasteFrom('Clipboard'), pane)
end
end),
},
{
mods = "CMD",
key = "x",
action = wezterm.action_callback(function(window, pane)
if helpers.is_micro(pane) then
window:perform_action(act.SendKey({ key = 'x', mods = 'CTRL' }), pane)
else
window:perform_action(act.SendKey({ key = 'x', mods = 'CMD' }), pane)
end
end),
},
{
mods = "CMD",
key = "a",
action = wezterm.action_callback(function(window, pane)
if helpers.is_micro(pane) then
window:perform_action(act.SendKey({ key = 'a', mods = 'CTRL' }), pane)
else
window:perform_action(act.SendKey({ key = 'a', mods = 'CMD' }), pane)
end
end),
},
{
mods = "CMD",
key = "g",
action = wezterm.action_callback(function(window, pane)
if helpers.is_micro(pane) then
window:perform_action(act.SendKey({ key = 'g', mods = 'CTRL' }), pane)
else
window:perform_action(act.SendKey({ key = 'g', mods = 'CMD' }), pane)
end
end),
},
}
end
return M
-- ============================================================================
-- NAVIGATION KEYBINDINGS
-- ============================================================================
-- Pane, tab, and workspace navigation
local wezterm = require 'wezterm'
local act = wezterm.action
local pickers = require 'pickers'
local M = {}
function M.get_keys(workspace_switcher)
local helpers = require 'helpers'
return {
-- SMART CLOSE: confirm if editor running, close tab if last pane
{
mods = "CMD",
key = "w",
action = wezterm.action_callback(function(window, pane)
local tab = window:active_tab()
local pane_count = #tab:panes()
-- If running an editor, require confirmation
if helpers.is_editor(pane) then
window:perform_action(act.CloseCurrentPane { confirm = true }, pane)
elseif pane_count == 1 then
-- Last pane in tab: close the tab
window:perform_action(act.CloseCurrentTab { confirm = false }, pane)
else
-- Multiple panes: close just this pane
window:perform_action(act.CloseCurrentPane { confirm = false }, pane)
end
end),
},
-- SMART SPLIT (uses current layout mode)
{
mods = "CMD",
key = "t",
action = wezterm.action_callback(function(window, pane)
local layouts = require 'layouts'
layouts.smart_new_pane(window, pane)
end),
},
-- QUICK OPEN PICKER
{
mods = "CMD",
key = "n",
action = wezterm.action_callback(function(window, pane)
pickers.show_quick_open_picker(window, pane)
end),
},
{
mods = "CMD",
key = "p",
action = wezterm.action_callback(function(window, pane)
pickers.show_quick_open_picker(window, pane)
end),
},
-- OPEN IN CURSOR (same picker, but opens in Cursor editor)
{
mods = "CMD|SHIFT",
key = "p",
action = wezterm.action_callback(function(window, pane)
pickers.show_quick_open_picker(window, pane, 'cursor')
end),
},
{ mods = "CMD|SHIFT", key = "n", action = act.SpawnWindow },
{ mods = "CMD|SHIFT", key = "f", action = act.ToggleFullScreen },
-- WORKSPACE AND PANE NAVIGATION
{ mods = "CMD|SHIFT", key = "s", action = act.ShowLauncherArgs { flags = "FUZZY|WORKSPACES" } },
{ mods = "CMD", key = "o", action = workspace_switcher.switch_workspace() },
{ mods = "CMD", key = "e", action = act.PaneSelect { alphabet = "1234567890", mode = "Activate" } },
{ mods = "CMD|SHIFT", key = "e", action = act.PaneSelect { alphabet = "1234567890", mode = "SwapWithActive" } },
-- COMMAND PALETTE AND LAUNCHER
{ mods = "CMD", key = "l", action = act.ShowLauncher },
-- PANE NAVIGATION (LEADER KEY)
{ mods = "LEADER", key = "h", action = act.ActivatePaneDirection "Left" },
{ mods = "LEADER", key = "j", action = act.ActivatePaneDirection "Down" },
{ mods = "LEADER", key = "k", action = act.ActivatePaneDirection "Up" },
{ mods = "LEADER", key = "l", action = act.ActivatePaneDirection "Right" },
{ mods = "LEADER", key = "x", action = act.CloseCurrentPane { confirm = true } },
-- TAB MANAGEMENT (LEADER KEY)
{ mods = "LEADER", key = "c", action = act.SpawnTab "CurrentPaneDomain" },
{ mods = "LEADER", key = "n", action = act.ActivateTabRelative(1) },
{ mods = "LEADER", key = "p", action = act.ActivateTabRelative(-1) },
-- RENAME TAB
{
mods = "LEADER",
key = ",",
action = act.PromptInputLine {
description = "Rename tab:",
action = wezterm.action_callback(function(window, pane, line)
if line and line ~= "" then
window:active_tab():set_title(line)
end
end),
},
},
{ mods = "LEADER", key = "1", action = act.ActivateTab(0) },
{ mods = "LEADER", key = "2", action = act.ActivateTab(1) },
{ mods = "LEADER", key = "3", action = act.ActivateTab(2) },
{ mods = "LEADER", key = "4", action = act.ActivateTab(3) },
{ mods = "LEADER", key = "5", action = act.ActivateTab(4) },
{ mods = "LEADER", key = "[", action = act.ActivateCopyMode },
-- COPY CURRENT COMMAND LINE (uses semantic zones from shell integration)
{
mods = "CMD",
key = "y",
action = act.Multiple {
act.ActivateCopyMode,
act.CopyMode { SetSelectionMode = 'SemanticZone' },
act.CopyTo 'Clipboard',
act.CopyMode 'Close',
},
},
-- ZELLIJ-STYLE PANE NAVIGATION
{ mods = "ALT", key = "h", action = act.ActivatePaneDirection "Left" },
{ mods = "ALT", key = "j", action = act.ActivatePaneDirection "Down" },
{ mods = "ALT", key = "k", action = act.ActivatePaneDirection "Up" },
{ mods = "ALT", key = "l", action = act.ActivatePaneDirection "Right" },
-- PANE RESIZING
{ mods = "ALT|SHIFT", key = "h", action = act.AdjustPaneSize { "Left", 5 } },
{ mods = "ALT|SHIFT", key = "j", action = act.AdjustPaneSize { "Down", 5 } },
{ mods = "ALT|SHIFT", key = "k", action = act.AdjustPaneSize { "Up", 5 } },
{ mods = "ALT|SHIFT", key = "l", action = act.AdjustPaneSize { "Right", 5 } },
-- DIRECT PANE SWITCHING
{ mods = "CMD", key = "1", action = act.ActivatePaneByIndex(0) },
{ mods = "CMD", key = "2", action = act.ActivatePaneByIndex(1) },
{ mods = "CMD", key = "3", action = act.ActivatePaneByIndex(2) },
{ mods = "CMD", key = "4", action = act.ActivatePaneByIndex(3) },
{ mods = "CMD", key = "5", action = act.ActivatePaneByIndex(4) },
{ mods = "CMD", key = "6", action = act.ActivatePaneByIndex(5) },
{ mods = "CMD", key = "7", action = act.ActivatePaneByIndex(6) },
{ mods = "CMD", key = "8", action = act.ActivatePaneByIndex(7) },
{ mods = "CMD", key = "9", action = act.ActivatePaneByIndex(8) },
-- SEARCH
{ mods = "CMD", key = "f", action = act.Search { CaseInSensitiveString = '' } },
-- PASSTHROUGH READLINE KEYS (disable WezTerm defaults)
-- Ctrl+K: kill-line in shell (was: ClearScrollback)
{ mods = "CTRL", key = "k", action = act.DisableDefaultAssignment },
-- Ctrl+X: readline prefix (was: ActivateCopyMode)
{ mods = "CTRL", key = "x", action = act.DisableDefaultAssignment },
}
end
return M
-- ============================================================================
-- PICKERS MODULE
-- ============================================================================
-- Fuzzy pickers for tabs, directories, themes, etc.
local wezterm = require 'wezterm'
local act = wezterm.action
local theme = require 'theme'
local layouts = require 'layouts'
local helpers = require 'helpers'
local M = {}
-- Keyboard shortcuts data (updated to reflect actual bindings)
local shortcuts = {
-- Navigation
{ key = "⌘P/N", desc = "Quick Open picker", cat = "nav" },
{ key = "⌘⇧P", desc = "Open in Cursor editor", cat = "nav" },
{ key = "⌘O", desc = "Workspace switcher (zoxide)", cat = "nav" },
{ key = "⌘T", desc = "New tab (pick dir first)", cat = "nav" },
{ key = "⌘W", desc = "Smart close (context-aware)", cat = "nav" },
{ key = "⌘1-9", desc = "Jump to pane by number", cat = "nav" },
{ key = "⌘F", desc = "Search in scrollback", cat = "nav" },
-- Panes
{ key = "⌘D", desc = "Smart split (layout-aware)", cat = "pane" },
{ key = "⌘⇧D", desc = "Split pane down", cat = "pane" },
{ key = "⌘E", desc = "Pane selector (number overlay)", cat = "pane" },
{ key = "⌘⇧E", desc = "Swap panes", cat = "pane" },
{ key = "⌥hjkl", desc = "Navigate panes (vim-style)", cat = "pane" },
{ key = "⌥⇧hjkl", desc = "Resize panes", cat = "pane" },
{ key = "⌥f", desc = "Toggle pane zoom", cat = "pane" },
{ key = "⌥n", desc = "New pane (auto-layout)", cat = "pane" },
{ key = "⌥x", desc = "Close pane (no confirm)", cat = "pane" },
-- Layouts
{ key = "⌘⇧L", desc = "Layout template picker", cat = "layout" },
{ key = "⌥Space", desc = "Layout mode picker", cat = "layout" },
{ key = "⌥[]", desc = "Cycle layout modes (toast)", cat = "layout" },
{ key = "⌥r", desc = "Rotate panes", cat = "layout" },
{ key = "⌥=", desc = "Reset zoom", cat = "layout" },
-- Power
{ key = "⌘/", desc = "Show this shortcuts help", cat = "power" },
{ key = "⌘⇧T", desc = "Theme picker", cat = "power" },
{ key = "⌘K", desc = "Command palette", cat = "power" },
{ key = "⌘⇧K", desc = "Clear scrollback", cat = "power" },
{ key = "⌘⇧F", desc = "Toggle fullscreen", cat = "power" },
{ key = "⌘⇧N", desc = "New window", cat = "power" },
{ key = "trigger", desc = "App launcher (via trigger)", cat = "power" },
-- Leader (Ctrl+B)
{ key = "^B z", desc = "Zen mode (hide tabs)", cat = "leader" },
{ key = "^B s", desc = "Save session (resurrect)", cat = "leader" },
{ key = "^B t", desc = "Cycle themes", cat = "leader" },
{ key = "^B ⇧T", desc = "Restore auto-theme", cat = "leader" },
{ key = "^B ,", desc = "Rename tab", cat = "leader" },
{ key = "^B r", desc = "Enter resize mode", cat = "leader" },
{ key = "^B [", desc = "Enter copy mode (vim)", cat = "leader" },
{ key = "^B -", desc = "Split vertical", cat = "leader" },
{ key = "^B |", desc = "Split horizontal", cat = "leader" },
{ key = "^B c", desc = "New tab", cat = "leader" },
{ key = "^B n/p", desc = "Next/prev tab", cat = "leader" },
}
-- Show keyboard shortcuts help picker (standalone)
function M.show_shortcuts_picker(window, pane)
local c = theme.colors
local choices = {}
local cat_colors = {
nav = c.cyan,
pane = c.green,
layout = c.orange,
power = c.pink,
leader = c.purple,
}
local cat_names = {
nav = "NAV",
pane = "PANE",
layout = "LAYOUT",
power = "POWER",
leader = "LEADER",
}
for _, s in ipairs(shortcuts) do
local label = wezterm.format({
{ Foreground = { Color = c.yellow } },
{ Attribute = { Intensity = "Bold" } },
{ Text = string.format("%-10s", s.key) },
{ Attribute = { Intensity = "Normal" } },
{ Foreground = { Color = c.fg } },
{ Text = " " .. s.desc .. " " },
{ Foreground = { Color = cat_colors[s.cat] or c.fg_dim } },
{ Text = "[" .. (cat_names[s.cat] or s.cat) .. "]" },
})
table.insert(choices, { id = "shortcut:" .. s.key, label = label })
end
window:perform_action(
act.InputSelector({
title = wezterm.format({
{ Foreground = { Color = c.cyan } },
{ Attribute = { Intensity = "Bold" } },
{ Text = " Keyboard Shortcuts" },
}),
choices = choices,
fuzzy = true,
fuzzy_description = wezterm.format({
{ Foreground = { Color = c.pink } },
{ Text = "󰈞 Filter: " },
}),
action = wezterm.action_callback(function(_, _, _, _)
-- Selecting a shortcut just dismisses the picker
end),
}),
pane
)
end
-- Show the zoxide-powered Quick Open picker
-- action_mode: 'tab' (default) opens in new tab, 'cursor' opens in Cursor editor
function M.show_quick_open_picker(window, pane, action_mode)
local home = os.getenv("HOME") or ""
local tabs = window:mux_window():tabs()
local green = theme.colors.green
local yellow = theme.colors.yellow
local aqua = theme.colors.cyan
-- Build a map of directories that have open tabs
local open_dirs = {}
for _, t in ipairs(tabs) do
for _, p in ipairs(t:panes()) do
local cwd = p:get_current_working_dir()
if cwd and cwd.file_path then
local normalized = cwd.file_path:lower()
open_dirs[normalized] = { tab = t, tab_id = t:tab_id(), original_path = cwd.file_path }
end
end
end
-- Get zoxide directories
local success, stdout, stderr = wezterm.run_child_process({ helpers.zoxide_path, 'query', '-l' })
if not success then
wezterm.log_error("zoxide failed: " .. tostring(stderr))
return
end
local open_choices = {}
local unopened_choices = {}
local seen_normalized = {}
for line in stdout:gmatch('[^\n]+') do
local normalized = line:lower()
if seen_normalized[normalized] then
goto continue
end
seen_normalized[normalized] = true
local open_entry = open_dirs[normalized]
local is_open = open_entry ~= nil
local actual_path = is_open and open_entry.original_path or line
local display = actual_path:gsub("^" .. home, "~")
local label = wezterm.format({
{ Foreground = { Color = is_open and green or yellow } },
{ Text = is_open and "● " or "○ " },
{ Foreground = { Color = aqua } },
{ Text = display },
})
if is_open then
table.insert(open_choices, { id = actual_path, label = label, focus_time = layouts.get_dir_focus_time(normalized) })
else
table.insert(unopened_choices, { id = line, label = label })
end
::continue::
end
-- Sort open tabs by most recently focused
table.sort(open_choices, function(a, b)
return a.focus_time > b.focus_time
end)
-- Combine: open tabs first, then unopened directories
local choices = {}
for _, choice in ipairs(open_choices) do
table.insert(choices, { id = choice.id, label = choice.label })
end
for _, choice in ipairs(unopened_choices) do
table.insert(choices, choice)
end
-- Ensure ~/dev is always first in the list
local dev_path = home .. "/dev"
local dev_normalized = dev_path:lower()
local dev_index = nil
for i, choice in ipairs(choices) do
if choice.id:lower() == dev_normalized then
dev_index = i
break
end
end
if dev_index then
-- Move ~/dev to front
local dev_choice = table.remove(choices, dev_index)
table.insert(choices, 1, dev_choice)
else
-- ~/dev not in list, add it at the front
local is_open = open_dirs[dev_normalized] ~= nil
local label = wezterm.format({
{ Foreground = { Color = is_open and green or yellow } },
{ Text = is_open and "● " or "○ " },
{ Foreground = { Color = aqua } },
{ Text = "~/dev" },
})
table.insert(choices, 1, { id = dev_path, label = label })
end
-- Determine title and hint based on mode
action_mode = action_mode or 'tab'
local is_cursor_mode = action_mode == 'cursor'
local title = wezterm.format({
{ Foreground = { Color = is_cursor_mode and theme.colors.purple or theme.colors.cyan } },
{ Attribute = { Intensity = "Bold" } },
{ Text = is_cursor_mode and " Open in Cursor" or "󰍉 Quick Open" },
})
local hint = is_cursor_mode and "(opens in Cursor)" or "(⌘⇧P for Cursor)"
window:perform_action(
act.InputSelector({
title = title,
fuzzy_description = wezterm.format({
{ Foreground = { Color = theme.colors.pink } },
{ Attribute = { Intensity = "Bold" } },
{ Text = "󰈞 Search: " },
{ Attribute = { Intensity = "Normal" } },
{ Foreground = { Color = theme.colors.fg_dim } },
{ Text = hint },
}),
choices = choices,
fuzzy = true,
action = wezterm.action_callback(function(inner_window, inner_pane, id, label)
if id == nil and label == nil then return end
-- Resolve target path from id or user input
local target_path
if id then
target_path = id
else
local user_input = label
if not user_input or user_input == "" then return end
if user_input:match("^/") then
target_path = user_input
elseif user_input:match("^~") then
target_path = user_input:gsub("^~", home)
else
target_path = home .. "/dev/" .. user_input
end
-- Create directory if it doesn't exist
local check_success, _, _ = wezterm.run_child_process({ 'test', '-d', target_path })
if not check_success then
local mkdir_success, _, mkdir_err = wezterm.run_child_process({ 'mkdir', '-p', target_path })
if not mkdir_success then
wezterm.log_error("Failed to create directory: " .. tostring(mkdir_err))
return
end
end
end
-- Execute action based on mode
if is_cursor_mode then
-- Open in Cursor editor
wezterm.background_child_process({ '/usr/local/bin/cursor', target_path })
else
-- Default: open/switch tab
local existing = open_dirs[target_path:lower()]
if existing then
existing.tab:activate()
else
inner_window:mux_window():spawn_tab({ cwd = target_path })
end
end
end),
}),
pane
)
end
-- Show app launcher picker (lists all installed macOS apps)
function M.show_app_launcher(window, pane)
-- Get list of apps from /Applications and ~/Applications
local apps = {}
local seen = {}
local function scan_dir(dir)
local success, stdout = wezterm.run_child_process({
'find', dir, '-maxdepth', '2', '-name', '*.app', '-type', 'd'
})
if success and stdout then
for app_path in stdout:gmatch('[^\n]+') do
local app_name = app_path:match('([^/]+)%.app$')
if app_name and not seen[app_name:lower()] then
seen[app_name:lower()] = true
table.insert(apps, { name = app_name, path = app_path })
end
end
end
end
scan_dir('/Applications')
scan_dir('/System/Applications')
scan_dir(os.getenv('HOME') .. '/Applications')
-- Sort alphabetically
table.sort(apps, function(a, b) return a.name:lower() < b.name:lower() end)
-- Build choices
local choices = {}
for _, app in ipairs(apps) do
local label = wezterm.format({
{ Foreground = { Color = theme.colors.cyan } },
{ Text = " " },
{ Foreground = { Color = theme.colors.fg } },
{ Text = app.name },
})
table.insert(choices, { id = app.path, label = label })
end
window:perform_action(
act.InputSelector({
title = wezterm.format({
{ Foreground = { Color = theme.colors.green } },
{ Attribute = { Intensity = "Bold" } },
{ Text = " App Launcher" },
}),
fuzzy_description = wezterm.format({
{ Foreground = { Color = theme.colors.pink } },
{ Attribute = { Intensity = "Bold" } },
{ Text = "󰈞 Search: " },
}),
choices = choices,
fuzzy = true,
action = wezterm.action_callback(function(inner_window, inner_pane, id, label)
if id then
wezterm.background_child_process({ 'open', '-a', id })
end
end),
}),
pane
)
end
return M
-- ============================================================================
-- POWER USER KEYBINDINGS
-- ============================================================================
-- Session management, themes, zen mode, etc.
local wezterm = require 'wezterm'
local act = wezterm.action
local helpers = require 'helpers'
local theme = require 'theme'
local pickers = require 'pickers'
local M = {}
function M.get_keys(resurrect, default_color_scheme)
return {
-- KEYBOARD SHORTCUTS HELP
{
mods = "CMD",
key = "/",
action = wezterm.action_callback(function(window, pane)
pickers.show_shortcuts_picker(window, pane)
end),
},
-- SESSION PERSISTENCE
{
mods = "LEADER",
key = "s",
action = wezterm.action_callback(function(win, pane)
resurrect.save_state(resurrect.workspace_state.get_workspace_state())
end),
},
-- QUICK SELECT
{ mods = "LEADER", key = "Space", action = act.QuickSelect },
-- THEME SWITCHER
{
mods = "CMD|SHIFT",
key = "t",
action = wezterm.action_callback(function(window, pane)
local choices = {}
for _, t in ipairs(theme.high_contrast_themes) do
table.insert(choices, { id = t.id, label = t.name .. ' - ' .. t.desc })
end
window:perform_action(
act.InputSelector({
title = wezterm.format({
{ Foreground = { Color = theme.colors.cyan } },
{ Attribute = { Intensity = "Bold" } },
{ Text = " Select Theme" },
}),
description = wezterm.format({
{ Foreground = { Color = theme.colors.fg_dim } },
{ Text = "High contrast themes for better readability" },
}),
fuzzy_description = wezterm.format({
{ Foreground = { Color = theme.colors.pink } },
{ Attribute = { Intensity = "Bold" } },
{ Text = " Search: " },
}),
choices = choices,
fuzzy = true,
action = wezterm.action_callback(function(inner_window, inner_pane, id, label)
if id then
wezterm.GLOBAL.user_selected_theme = id
local overrides = inner_window:get_config_overrides() or {}
overrides.color_scheme = id
inner_window:set_config_overrides(overrides)
end
end),
}),
pane
)
end),
},
-- QUICK THEME CYCLE
{
mods = "LEADER",
key = "t",
action = wezterm.action_callback(function(window, pane)
local overrides = window:get_config_overrides() or {}
local current = overrides.color_scheme or default_color_scheme
local current_idx = 1
for i, t in ipairs(theme.high_contrast_themes) do
if t.id == current then
current_idx = i
break
end
end
local next_idx = (current_idx % #theme.high_contrast_themes) + 1
local next_theme = theme.high_contrast_themes[next_idx]
wezterm.GLOBAL.user_selected_theme = next_theme.id
overrides.color_scheme = next_theme.id
window:set_config_overrides(overrides)
end),
},
-- RESTORE AUTO-THEME (clear user selection, return to CWD-based theming)
{
mods = "LEADER|SHIFT",
key = "t",
action = wezterm.action_callback(function(window, pane)
wezterm.GLOBAL.user_selected_theme = nil
local overrides = window:get_config_overrides() or {}
overrides.color_scheme = nil
window:set_config_overrides(overrides)
end),
},
-- ZEN MODE
{
mods = "LEADER",
key = "z",
action = wezterm.action_callback(function(window, pane)
local overrides = window:get_config_overrides() or {}
if overrides.enable_tab_bar == false then
overrides.enable_tab_bar = true
overrides.window_padding = { left = 10, right = 10, top = 10, bottom = 10 }
else
overrides.enable_tab_bar = false
overrides.window_padding = { left = 0, right = 0, top = 0, bottom = 0 }
end
window:set_config_overrides(overrides)
end),
},
-- RECORDING MODE (hide tab bar + status bar for demos/lessons)
{
mods = "CMD|SHIFT",
key = "r",
action = wezterm.action_callback(function(window, pane)
wezterm.GLOBAL.recording_mode = not wezterm.GLOBAL.recording_mode
local overrides = window:get_config_overrides() or {}
if wezterm.GLOBAL.recording_mode then
overrides.enable_tab_bar = false
else
overrides.enable_tab_bar = true
end
window:set_config_overrides(overrides)
end),
},
-- SMART SCROLLBACK CLEAR (CMD+SHIFT+K to avoid conflict with Command Palette)
{
mods = "CMD|SHIFT",
key = "k",
action = wezterm.action_callback(function(window, pane)
if helpers.is_vim(pane) then
window:perform_action(act.SendKey({ key = 'k', mods = 'CMD' }), pane)
else
window:perform_action(act.ClearScrollback 'ScrollbackOnly', pane)
window:perform_action(act.SendKey({ key = 'L', mods = 'CTRL' }), pane)
end
end),
},
-- OPEN ZED AT CURRENT DIRECTORY
{
mods = "CMD",
key = "z",
action = wezterm.action_callback(function(window, pane)
local cwd = pane:get_current_working_dir()
local path = cwd and cwd.file_path or os.getenv("HOME")
wezterm.background_child_process({ "/usr/local/bin/zed", path })
end),
},
}
end
return M
-- ============================================================================
-- THEME MODULE
-- ============================================================================
-- Color scheme definitions and theme-related utilities
local M = {}
-- Hardcore theme colors for UI elements
M.colors = {
bg = '#121212', -- Background (very dark)
bg_light = '#1b1d1e', -- Slightly lighter background
bg_selection = '#453b39', -- Selection/highlight background
fg = '#a0a0a0', -- Main foreground (gray)
fg_bright = '#f8f8f2', -- Bright foreground (white)
fg_dim = '#505354', -- Dimmed foreground
pink = '#f92672', -- Accent: pink/magenta
green = '#a6e22e', -- Accent: green
orange = '#fd971f', -- Accent: orange
yellow = '#e6db74', -- Accent: yellow
cyan = '#66d9ef', -- Accent: cyan
purple = '#9e6ffe', -- Accent: purple
}
-- High contrast theme options
M.high_contrast_themes = {
{ id = 'Solarized Dark Higher Contrast', name = 'Solarized High Contrast', desc = 'Enhanced Solarized readability' },
{ id = 'Hardcore', name = 'Hardcore', desc = 'Maximum contrast, bold colors' },
{ id = 'Dracula', name = 'Dracula', desc = 'Dark purple with vivid accents' },
{ id = 'Catppuccin Mocha', name = 'Catppuccin Mocha', desc = 'Warm pastels, good contrast' },
{ id = 'GruvboxDark', name = 'Gruvbox Dark', desc = 'Warm retro classic' },
{ id = 'Tokyo Night', name = 'Tokyo Night', desc = 'Cool blue/purple palette' },
{ id = 'Selenized Dark (Gogh)', name = 'Selenized Dark', desc = 'Scientifically designed readability' },
{ id = 'Snazzy', name = 'Snazzy', desc = 'Vibrant on dark' },
}
-- Dynamic theme based on macOS appearance
function M.scheme_for_appearance(appearance)
if appearance:find("Dark") then
return "Catppuccin Mocha"
else
return "Catppuccin Latte"
end
end
return M
-- ============================================================================
-- WEZTERM CONFIGURATION
-- ============================================================================
-- WezTerm is a GPU-accelerated terminal emulator written in Rust.
-- This config transforms WezTerm into a powerful terminal multiplexer,
-- combining features from tmux, Zellij, and iTerm2 into one cohesive setup.
--
-- MODULAR STRUCTURE:
-- helpers.lua - Utility functions (is_vim, is_micro, cwd helpers)
-- theme.lua - Color schemes and theme definitions
-- layouts.lua - Zellij-style layouts and smart pane management
-- appearance.lua - Visual config (fonts, colors, window settings)
-- pickers.lua - Fuzzy pickers for tabs, directories, themes
-- keys/ - Keybindings split by category
-- events.lua - Event handlers (tab titles, status bar)
-- ============================================================================
local wezterm = require 'wezterm'
local config = wezterm.config_builder()
-- ============================================================================
-- ENVIRONMENT (ensure Homebrew tools available in spawned processes)
-- ============================================================================
local path = os.getenv("PATH") or ""
config.set_environment_variables = {
PATH = table.concat({
"/opt/homebrew/bin",
"/opt/homebrew/sbin",
"/usr/local/bin",
path,
}, ":"),
}
-- Load modules
local appearance = require 'appearance'
local events = require 'events'
local keys = require 'keys'
-- ============================================================================
-- PLUGINS
-- ============================================================================
local workspace_switcher = wezterm.plugin.require("https://github.com/MLFlexer/smart_workspace_switcher.wezterm")
workspace_switcher.zoxide_path = "/opt/homebrew/bin/zoxide"
local resurrect = wezterm.plugin.require("https://github.com/MLFlexer/resurrect.wezterm")
-- ============================================================================
-- APPLY CONFIGURATION
-- ============================================================================
-- Apply appearance settings
appearance.apply(config)
-- Apply keybindings (needs plugins for some bindings)
keys.apply(config, workspace_switcher, resurrect)
-- Setup event handlers
events.setup()
-- Apply plugin configurations
workspace_switcher.apply_to_config(config)
-- ============================================================================
-- RETURN CONFIGURATION
-- ============================================================================
return config
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment