A WezTerm terminal config

Published on by

Terminal emulators often become personal tools, reflecting years of accumulated preferences, workflow optimizations and moments of madness. After years of cobbling together my Tmux and Kitty setup (which worked kind of worked fine), I threw it all away about a year ago for WezTerm giving me a simpler config and setup.

I had considered Alacritty (since it has solid performance), but WezTerm's Lua-based configuration offered the programmability I wanted without sacrificing speed - and has great community support. Its robust feature set captured everything I needed in a single tool, eliminating the need to manage separate terminal and multiplexer configurations.

An important aspect of my setup bridges the gap between terminal panes and Neovim splits. It was the first thing I needed to get right before I could move off Kitty and Tmux. Instead of remembering different key bindings for each context, the same shortcuts work everywhere:

local function isVi(pane)
  return pane:get_foreground_process_name():find("n?vim") ~= nil
end

local act = wezterm.action

local function activatePane(window, pane, pane_direction, vim_direction)
  if isVi(pane) then
    window:perform_action(act.SendKey({ key = vim_direction, mods = "CTRL" }), pane)
  else
    window:perform_action(act.ActivatePaneDirection(pane_direction), pane)
  end
end

Thank you Navigator.nvim WezTerm Integration which guided me in the creation of wezterm.nvim. Similar approaches are explored in WezTerm and Neovim keybindings in macOS.

This integration means Ctrl+h/j/k/l moves between panes regardless of whether you're in the terminal or inside a Neovim session. The terminal detects when Neovim is running and sends the appropriate navigation command. It's the kind of seamless experience that reduces cognitive overhead when switching between different contexts.

For Neovim configuration management, I currently use LazyVim which provides a well-structured foundation with sensible defaults and easy customization.

Tabs bootstrap

Rather than manually opening tabs and navigating to project directories each morning, the configuration handles workspace initialization:

wezterm.on("gui-startup", function(_)
  local _, _, window = wezterm.mux.spawn_window({})
  window:gui_window():maximize()
  local _, second_pane, _ = window:spawn_tab({ cwd = wezterm.home_dir .. "/.dotfiles" })
  local _, third_pane, _ = window:spawn_tab({ cwd = wezterm.home_dir .. "/projects/things" })
  window:spawn_tab({ cwd = wezterm.home_dir .. "/projects" })

  second_pane:send_text("vi\n")
  third_pane:send_text("vi\n")
end)

Each startup creates a predefined set of tabs in specific directories with Neovim already launched. The window maximizes automatically, and the first tab becomes active. This setup eliminates the repetitive navigation that starts most coding sessions—a small automation that compounds over time.

Tab titles

Tab titles adapt based on the current working directory, with some custom logic for frequently used directories:

local function tab_title(tab)
  -- Personally for each tab, I have nvim in the directory of interest in the
  -- first pane. Other panes I might navigate around. I want the tab label to
  -- based on the directory of this first pane, since this is my context.
  local pane = tab.panes[1]
  local current_working_dir = pane.current_working_dir
  if current_working_dir == nil then
    return "special"
  end
  if current_working_dir.file_path == wezterm.home_dir then
    return "~"
  end
  local relative_working_dir = get_relative_working_dir(pane)
  if relative_working_dir == ".dotfiles" then
    return "."
  elseif relative_working_dir == "things" then
    return "󰧮"
  end
  return relative_working_dir
end

This keeps tab labels concise while remaining informative. The dotfiles directory shows as a simple dot, my main project gets a file character, and everything else displays the directory name. It's a detail that makes better of screen real estate for something that is always there.

The configuration includes custom hyperlink patterns for development workflows:

config.hyperlink_rules = {
  -- Matches: a URL in parens: (URL)
  {
    regex = "\\((https?://\\S+)\\)",
    format = "$1",
    highlight = 1,
  },
  -- Matches: a URL in brackets: [URL]
  {
    regex = "\\[(https?://\\S+)\\]",
    format = "$1",
    highlight = 1,
  },
  -- Matches: a URL in curly braces: {URL}
  {
    regex = "\\{(https?://\\S+)\\}",
    format = "$1",
    highlight = 1,
  },
  -- Matches: a URL in angle brackets: <URL>
  {
    regex = "<(https?://\\S+)>",
    format = "$1",
    highlight = 1,
  },
  -- Then handle URLs not wrapped in brackets
  {
    regex = "\\bhttps?://\\S+[)/a-zA-Z0-9-]+",
    format = "$0",
  },
}

These patterns recognize URLs in various bracket formats commonly found in markdown and documentation, making them clickable without manual selection. A practical improvement for anyone working regularly with documentation or issue trackers.

Key binding

The key bindings prioritize consistency with system conventions and muscle memory:

config.keys = {
  {
    key = "-",
    mods = "CMD",
    action = act.SplitVertical,
  },
  {
    key = "\\",
    mods = "CMD",
    action = act.SplitHorizontal,
  },
  {
    key = "LeftArrow",
    mods = "OPT",
    action = act.ActivateTabRelative(-1),
  },
  {
    key = "RightArrow",
    mods = "OPT",
    action = act.ActivateTabRelative(1),
  },
  {
    key = "RightArrow",
    mods = "CTRL",
    action = act.EmitEvent("ActivatePaneDirection-right"),
  },
  {
    key = "LeftArrow",
    mods = "CTRL",
    action = act.EmitEvent("ActivatePaneDirection-left"),
  },
  {
    key = "UpArrow",
    mods = "CTRL",
    action = act.EmitEvent("ActivatePaneDirection-up"),
  },
  {
    key = "DownArrow",
    mods = "CTRL",
    action = act.EmitEvent("ActivatePaneDirection-down"),
  },
}

CMD key feel natural on macOS, while the directional navigation maintains consistency with Vim-style movement patterns. The bindings avoid conflicts with shortcuts from some other application I use.

In practice

This configuration removes daily friction. Starting work means opening WezTerm and finding the tabs already set up. Navigation between panes happens without thinking about which shortcuts to use.

The setup represents several years of gradual refinement—adding features that proved genuinely useful while removing clever additions that ultimately created unnecessary complexity than value. I've ended up with less than I had a few years ago.

Terminal configuration can help reduce repetitive tasks while keeping things simple. This was as much about learning WezTerm's capabilities as solving specific productivity challenges, but the result has been useful.

See dotfiles for the full configuration in context.