How I use Wezterm

I use the terminal a lot. Until the past few years, I basically used only a browser and a terminal. (The primary changes in the past couple years are that I'm using Logseq for tracking notes and todos, and now use native apps for Zoom and Slack.)

Today I'm going to detail my exploration of Wezterm, my current daily driver.

My terminal history

I used gnome-terminal for a long time. At a certain point, I learned about "Quake"-style, dropdown terminals, and realized that these would be ideal for running one-off tasks or doing quick lookups. I used Guake for a number of years, as it was very similar to gnome-terminal.

At a certain point, however, I found gnome-terminal to be a bit slow, and started looking for alternatives. I eventually landed on one I'd used in my early days using LInux: xfce4-terminal. This one was useful as it has a native dropdown mode, which meant I could use the same terminal for dropdowns as well as my main driver. It used very few resources, and "just worked."

Until it didn't. I adopted Wayland last year, as it was clear that Ubuntu 2024.04 would likely default to it, if not outright require it. And when I did, xfce4-terminal became an issue. I cannot recall if it became slow (as it had to run via the Xorg bindings), or if it outright didn't work, but I had to switch, regardless.

I tried a number of terminals, settling on Kitty. While it didn't have a dropdown mode, I was able to accomplish it via tdrop.

Now, the interesting thing is that I discovered tdrop when I was investigating new terminal alternatives, and tried out wezterm. At the time, I didn't go with wezterm, as I found the configuration confusing (I hadn't dove into Lua yet). I'll get back to wezterm later.

Tmux saves my bacon

Now, one tool I use all the time in a terminal is tmux. Tmux basically gives you windows and panes inside your terminal. On top of that, you can have different sessions, and attach to and detach from sessions independently; this allows you to attach to the same session from multiple windows, or detach from a session you're not actively working in so you can come back to it later. When you do return, you have all the windows and panes that were open previously.

Tmux is also pluggable, and I use a couple of plugins regularly:

  • vim-tmux-navigator is technically a vim/nvim plugin, but it works in tandem with configuration you set it tmux itself. What it does is make it so that you can use the same key combinations to navigate between vim/nvim and tmux panes. So, let's say you have vim in a tmux pane on the left, and it has two vertical panes. If you're in the right-most vim pane, and hit the keystroke to move right, with this plugin, that moves you into the tmux pane to the right.

    This is tremendously useful, as you don't need to think about what context you're in as you move around between vim/nvim and tmux panes.

  • tmux-resurrect allows you to persist sessions between tmux server invocations. In other words, if you restart your computer, the first time you start tmux, all your sessions re-appear, and each has any previous history already present. This plugin is has helped me feel confident that I won't lose track of what I was doing if I need to reboot.

What I have done for many years is setup my dropdown terminal to create or attach to a named tmux session. This means that I always have the ability to create new windows and panes as needed from my dropdown terminal, which is hugely useful.

Additionally, most times I open a terminal, I'm starting a tmux session named to reflect what I'm doing. This ensures if I need to reboot, I don't lose my place. In fact, I find myself getting upset quite often when I start working on something and only later realize I didn't start tmux, as I have to quit what I'm doing and start back up after starting tmux. And when I SSH to another server, I hate not having tmux available; I'll often use tmux locally, which, while it means I need multiple SSH sessions, it at least gives me what I need for functionality.

I depend on terminal multiplexing. It's a basic feature I absolutely need.

Learning wezterm

When I first came across wezterm, one thing I noted is that it had a built-in multiplexing. However, it was sufficiently different from tmux that I only noted it in passing.

Sometime this past winter, I became reacquainted with Wez Furlong, author of wezterm, via Mastodon, where he's fairly active. Wez used to contribute heavily to PHP, and we would cross paths at PHP conferences in the late oughts, and it was fun to see what he's been up to since — which includes creating wezterm. As such, I figured I'd try it out again.

It soon became my daily driver. While it took a bit to get it working well with tdrop, once I did, I found it was fast, unobtrusive, and "just worked".

But at the back of my head, I kept thinking, "I wonder if this could replace tmux for me?"

When I looked at what I actually use in tmux, I found that it was just these things:

_ Powerline. And, honestly, I discovered that I never actually LOOK at my powerline, other than to see what session and window I'm in. I decided that I'd be happy with minimally seeing active windows, better yet the session, and only vaguely interested in seeing anything else (CPU usage, memory usage, time, etc.).

  • Scrollback per pane
  • Select and copy
  • "Zoom"ing a pane (taking a single pane as "fullscreen" within a window, and then restoring to the previous layout)
  • vim-tmux-navigator (see the description in the previous section)
  • tmux-resurrect (natch)

This is not a huge featureset, so I set out to see if I could do these things.

One note: I never did figure out how to show the current session name in the tab bar. Otherwise, I accomplished all other goals!

Lua

Wezterm, while written in Rust, manages configuration using Lua. As it turns out... Lua can be picked up very quickly if you've used basically any programming language at all. It looks a lot like JSON, but with a limited number of statements and expressions also available.

Wezterm ships with a small number of modules that allow you to configure the terminal, as well as perform a certain number of actions, from interacting with sessions, windows, and panes, to performing prompts.

Once I picked up Lua, configuring Wezterm became relatively straightforward. I'm now considering updating my nvim configuration to be entirely in Lua as well!

Take me to your Leader

Tmux and vim/nvim each have a concept of a leader character or key sequence. These allow you a bit more flexibility when creating keyboard shortcuts, as you don't need to worry about conflicts with other programs. The program "screen" (the OG terminal multiplexer) uses "Ctrl-A" as the leader, while tmux uses "Ctrl-B" by default (but most tmux users remap it to Ctrl-A). In vim, I map , as my leader character.

Wezterm allows defining a leader as well. I decided I'd use "Ctrl-A", as the plan is to replace tmux:

wezterm.config.leader = {
  key = 'a',
  mods = 'CTRL',
  timeout_milliseconds = 2000,
}
Scrollback, select, and copy

Wezterm allows scrollback in its copy mode. I setup a keybinding mimicing the backscroll/copy mode in tmux:

wezterm.config.keys = {
  {
    key = '[',
    mods = 'LEADER',
    action = wezterm.action.ActivateCopyMode,
  },
}

Interestingly, I found the copy mode in wezterm to be more predictable than in tmux!

Zooming

Sometimes I'm in a pane within a split window, and realize I need a bit more visual room. I had mapped Alt-F to this in tmux, so I did the same with wezterm.

wezterm.config.keys = {
  {
    key = 'f',
    mods = 'ALT',
    action = wezterm.action.TogglePaneZoomState,
  },
}
Windows versus Tabs

Tmux calls them windows, but wezterm calls them tabs. The idea is the same: a discrete, indexed and/or named terminal that can handle one or more panes.

Tmux puts the list of windows at the bottom by default (I think this can be configured, but I've literally only ever had them at the bottom of the screen). Wezterm puts them at the top. I wanted to switch the behavior, but could not find the setting... so I popped into the wezterm matrix room to ask. Wez responded within minutes, and pointed me to the tab_bar_at_bottom setting. (The matrix room is incredibly friendly and positive!)

wezterm.config.tab_bar_at_bottom = true

How do I create a tab? Wezterm, like most terminals, uses Ctrl-Shift-Tab, but with tmux, I would use <Leader>c, so I added that binding:

wezterm.config.keys = {
  {
    key = 'c',
    mods = 'LEADER',
    action = act.SpawnTab 'CurrentPaneDomain',
  },
}

Next, I wanted a way to move between tabs without a mouse. I mapped these the same as I did in tmux: <Leader>n for the next tab, <Leader>p for the previous:

wezterm.config.keys = {
  {
    key = 'n',
    mods = 'LEADER',
    action = wezterm.action.ActivateTabRelative(1),
  },
  {
    key = 'p',
    mods = 'LEADER',
    action = wezterm.action.ActivateTabRelative(-1),
  },
}

I like to name my windows/tabs. This helps me understand what work I'm doing in each: tests, running docker images, etc. In tmux, I use <Leader>, to do this, and this was where my first real Lua came in, as I had to define a callback with a conditional to make it happen:

wezterm.config.keys = {
  {
    key = ',',
    mods = 'LEADER',
    action = act.PromptInputLine {
      description = 'Enter new name for tab',
      action = wezterm.action_callback(
        function(window, pane, line)
          if line then
            window:active_tab():set_title(line)
          end
        end
      ),
    },
  },
}

How about switching to a specific tab? With tmux, you would do <Leader><index>, where <index> is the index of the window you want (e.g. "1" or "3"). For wezterm, I decided to use the tab navigator, which gives a dialog displaying each tab with their index; you then specify the one you want, either by navigating to it, or entering the index. Even better, you can use / to filter through them, which is handy if you have more than ten windows, as otherwise indexes only get you from 0-9!

wezterm.config.keys = {
  {
    key = 'w',
    mods = 'LEADER',
    action = act.ShowTabNavigator,
  },
}

I also want to be able to kill/close a tab without needing to type "exit"; this is particularly important if a process is stuck. Again, I went with what I knew from tmux, and used <Leader>&... and learned a valuable lesson in wezterm configuration in the process!

wezterm.config.keys = {
  -- Close tab
  {
    key = '&',
    mods = 'LEADER|SHIFT',
    action = act.CloseCurrentTab{ confirm = true },
  },
}

As it turns out, if a character requires hitting the shift key normally, such as any of the symbols above the numeric keys, or even keys like ?, <, >, the | and {/} symbols, the SHIFT mod MUST be included!

Finally, there were a smattering of other configuration settings I changed to make the behavior match my expectations, as well as to make it visually easier to parse:

-- Make it look like tabs, with better GUI controls
wezterm.config.use_fancy_tab_bar = true
-- Don't let any individual tab name take too much room
wezterm.config.tab_max_width = 32
wezterm.config.colors = {
  tab_bar = {
    active_tab = {
      -- I use a solarized dark theme; this gives a teal background to the active tab
      fg_color = '#073642'
      bg_color = '#2aa198'
    }
  }
}
-- Switch to the last active tab when I close a tab
wezterm.config.switch_to_last_active_tab_when_closing_tab = true

I really appreciate the attention to detail when naming configuration options. Once you figure out what option you need, you don't need to look up what it does!

Panes

The huge benefit that screen, and later, tmux, introduced is the ability to have multiple panes within a window. This allows you to essentially treat your terminal like you would a tiled window manager. During my years as an engineer, this allowed me to treat my CLI as an IDE: I'd have one pane open with vim/nvim, split between a test case and the source code I was modifying; another would be used to run unit tests, which also gave me the ability to scroll back and view specific error messages and traces.

So if I was going to use wezterm, I'd need to be able to split my window/tab into panes. Additionally, I would need the ability to keep a fair amount of scrollback history, be able to use my mouse when desired, etc. On top of all that, I'd want to be able to navigate seamlessly between vim/nvim panes and wezterm panes.

Let's tackle the (n)vim <-> wezterm pane issues first, as this also dictates keybindings eventually. I found wezterm.nvim, which, despite its name, interacts primarily with wezterm.

The first step of setup is to modify your shell configuration to add the following line to your shell configuation (assuming you use bash or zsh):

[ -n "$WEZTERM_PANE" ] && export NVIM_LISTEN_ADDRESS="/tmp/nvim$WEZTERM_PANE"

Once that's in place, you clone the project, and use the go language compiler to build and install it; this will install the utilitywezterm.nvim.navigator. Run which wezterm.nvim.navigator when done to find the path on your sytem where it's installed, as it will be needed to setup your wezterm configuration.

From there, you define a function in the configuration that uses that utility to determine if you're in a vim/nvim pane or in a wezterm pane, and, from there, determines what needs to be done to move in the direction requested. You then also define a number of event handlers that invoke this function, and finally some keybindings to trigger those events.

The beauty of this is that you don't need anything in your vim/nvim configuration; it just uses the default keybindings to move between panes, as wezterm now invokes them!

Now, on top of this, you can also specify some keybindings for resizing panes, which is really useful. This requires setting up some very similar functionality (a function to perform the resizing, event handlers to invoke it, and keybindings).

I bound <Ctrl>-(hjkl) for moving between windows (these are the standard vim movement mappings), and <Alt>-(hjkl) for resizing.

The full configuration for these motions is here:

-- Pull in the wezterm API
local os              = require 'os'
local wezterm         = require 'wezterm'
local act             = wezterm.action

-- This table will hold the configuration.
local config = {}

-- In newer versions of wezterm, use the config_builder which will
-- help provide clearer error messages
if wezterm.config_builder then
  config = wezterm.config_builder()
end


local move_around = function(window, pane, direction_wez, direction_nvim)
  local result = os.execute("env NVIM_LISTEN_ADDRESS=/tmp/nvim" .. pane:pane_id() .. " " .. wezterm.home_dir .. "/.local/bin/wezterm.nvim.navigator" .. " " .. direction_nvim)
  if result then
  window:perform_action(
      act({ SendString = "\x17" .. direction_nvim }),
      pane
    )
  else
    window:perform_action(
      act({ ActivatePaneDirection = direction_wez }),
      pane
    )
  end
end

wezterm.on("move-left", function(window, pane)
	move_around(window, pane, "Left", "h")
end)

wezterm.on("move-right", function(window, pane)
	move_around(window, pane, "Right", "l")
end)

wezterm.on("move-up", function(window, pane)
	move_around(window, pane, "Up", "k")
end)

wezterm.on("move-down", function(window, pane)
	move_around(window, pane, "Down", "j")
end)

local vim_resize = function(window, pane, direction_wez, direction_nvim)
	local result = os.execute(
		"env NVIM_LISTEN_ADDRESS=/tmp/nvim"
			.. pane:pane_id()
			.. " "
            .. wezterm.home_dir
			.. "/.local/bin/wezterm.nvim.navigator"
			.. " "
			.. direction_nvim
	)
	if result then
		window:perform_action(act({ SendString = "\x1b" .. direction_nvim }), pane)
	else
		window:perform_action(act({ ActivatePaneDirection = direction_wez }), pane)
	end
end

wezterm.on("resize-left", function(window, pane)
	vim_resize(window, pane, "Left", "h")
end)

wezterm.on("resize-right", function(window, pane)
	vim_resize(window, pane, "Right", "l")
end)

wezterm.on("resize-up", function(window, pane)
	vim_resize(window, pane, "Up", "k")
end)

wezterm.on("resize-down", function(window, pane)
	vim_resize(window, pane, "Down", "j")
end)

config.keys = {
    -- CTRL + (h,j,k,l) to move between panes
    {
        key = 'h',
        mods = 'CTRL',
        action = act({ EmitEvent = "move-left" }),
    },
    {
        key = 'j',
        mods = 'CTRL',
        action = act({ EmitEvent = "move-down" }),
    },
    {
        key = 'k',
        mods = 'CTRL',
        action = act({ EmitEvent = "move-up" }),
    },
    {
        key = 'l',
        mods = 'CTRL',
        action = act({ EmitEvent = "move-right" }),
    },
    -- ALT + (h,j,k,l) to resize panes
    {
        key = 'h',
        mods = 'ALT',
        action = act({ EmitEvent = "resize-left" }),
    },
    {
        key = 'j',
        mods = 'ALT',
        action = act({ EmitEvent = "resize-down" }),
    },
    {
        key = 'k',
        mods = 'ALT',
        action = act({ EmitEvent = "resize-up" }),
    },
    {
        key = 'l',
        mods = 'ALT',
        action = act({ EmitEvent = "resize-right" }),
    },
}

So this gives me movement, but how do I actually split the window into panes? For this, I've always favored <Leader>| to split into vertical panes, and <Leader>- to split into horizontal panes:

config.keys = {
  -- Vertical split
  {
    -- |
    key = '|',
    mods = 'LEADER|SHIFT',
    action = act.SplitPane {
      direction = 'Right',
      size = { Percent = 50 },
    },
  },
  -- Horizontal split
  {
    -- -
    key = '-',
    mods = 'LEADER',
    action = act.SplitPane {
      direction = 'Down',
      size = { Percent = 50 },
    },
  },
}

Sometimes I want to swap a pane with another one, because they're not positioned the way I'd like. I bound <Leader>{ to the wezterm PaneSelect action, which allows me to choose the pane I wish to swap with using an alphanumerical index:

config.keys = {
  {
    -- |
    key = '{',
    mods = 'LEADER|SHIFT',
    action = act.PaneSelect { mode = 'SwapWithActiveKeepFocus' }
  },
}

And sometimes I like to move back and forth between panes; I use <Leader>; and <Leader>; for that:

config.keys = {
  {
    key = ';',
    mods = 'LEADER',
    action = act.ActivatePaneDirection('Prev'),
  },
  {
    key = 'o',
    mods = 'LEADER',
    action = act.ActivatePaneDirection('Next'),
  },
}

With all of these in place, I'm very nearly there. Just a couple of minor settings:

config.pane_focus_follows_mouse = true
config.scrollback_lines = 5000
-- I don't really have need for padding between panes
config.window_padding = {
  left = 0,
  right = 0,
  top = 0,
  bottom = 0,
}
Sessions

Next up on my list of functionality: sessions.

As noted before, tmux sessions, paired with tmux-resurrect, have given me confidence that I won't lose work if I need to restart my computer, or if the battery dies. How can I reproduce this with wezterm?

By default, each wezterm instance defines its own workspaces, which are analogous to tmux sessions... kind of. You can create additional workspaces and switch between workspaces using a variety of tools.

However, if you want to share workspaces between wezterm instances, you need to have a mux domain running, and connect to it. A mux domain is a set of workspaces, windows, and tabs. By default, a "local" domain is created everytime you start a wezterm terminal, but you can define additional domains as well, including remote mux servers running on an SSH-accessible server or TLS-guarded remote port, or a simple unix domain.

In my case, I'm configuring a unix domain to run out of the box:

config.unix_domains = {
  {
    name = 'unix',
  },
}

This tells wezterm to setup a mux server on a unix socket. It does not connect to it out of the box, but it does ensure it's running.

From there, I've created two keybindings: one for connecting to the mux server, and another for detaching from it. This approach somewhat mirrors how you use tmux: you have to start tmux to create sessions, and detaching from it will drop you back into your original shell.

config.keys = {
  -- Attach to muxer
  {
    key = 'a',
    mods = 'LEADER',
    action = act.AttachDomain 'unix',
  },

  -- Detach from muxer
  {
    key = 'd',
    mods = 'LEADER',
    action = act.DetachDomain { DomainName = 'unix' },
  },
}

With the muxer, I can create workspaces. These are analogous to tmux sessions, and are a group of windows and panes. When you start a wezterm terminal, you're in a "default" workspace. So the first thing to do is to allow renaming the workspace to reflect what I'm working on:

config.keys = {
  -- Rename current session; analagous to command in tmux
  {
    key = '$',
    mods = 'LEADER|SHIFT',
    action = act.PromptInputLine {
      description = 'Enter new name for session',
      action = wezterm.action_callback(
        function(window, pane, line)
          if line then
            mux.rename_workspace(
              window:mux_window():get_workspace(),
              line
            )
          end
        end
      ),
    },
  },
}

The above maps <Leader>$ to prompt you to rename the session.

From here, how do I switch to another workspace, or create a new one? For that, I mapped <Leader>s to show the workspace launcher; this lists available workspaces, allows you to switch between them, as well as create new workspaces:

config.keys = {
  -- Show list of workspaces
  {
    key = 's',
    mods = 'LEADER',
    action = act.ShowLauncherArgs { flags = 'WORKSPACES' },
  },
}

The workflow then looks like this:

  • Start wezterm.

  • If I want to be able to switch between mux workspaces on other windows, I then hit <Leader>a. This will connect to the mux server, and throw me into the default workspace.

  • If I want to leave the mux session, I can hit <Leader>d, which will detach me from it, taking me back to the "Local" domain for the GUI window I'm in.

    • At this point, typing exit will close the window, as I'll have left the last pane of the last window in the workspace.

One thing that is odd about the situation: if you are in a mux session with multiple workspaces, and you exit out of the last pane of the last window of a workspace... it doesn't close the window or drop you out of the mux domain. Instead, it takes you to the last active workspace. This can be confusing at first. However, all you need to do at that point is hit <Leader>d to detach, and that will take you back to the local domain, from which you can exit normally.

Interestingly, you can use the system controls to close the window at any point (e.g., hitting the close button, or using a keybinding like <Ctrl>w or <Alt>F4). The mux server will keep the state for you. Of course, if you do this and you don't want the workspace to persist, you'll need to return to that workspace at some point and exit out of it.

Keeping things safe

Now that I have a concept of sessions, what about recovery? One of my most used features in tmux was the tmux-resurrect plugin, which allowed me to save and restore sessions (and did some background periodic saving for me). Can I do that with wezterm?

Yes, to an extent, via the wezterm-session-manager project. This is a Lua module that you add to your wezterm configuration; you clone it in the $HOME/.config/wezterm/ directory, and then import it into your configuration:

local session_manager = require 'wezterm-session-manager/session-manager'

From there, you need to register some event listeners:

wezterm.on("save_session", function(window) session_manager.save_state(window) end)
wezterm.on("load_session", function(window) session_manager.load_state(window) end)
wezterm.on("restore_session", function(window) session_manager.restore_state(window) end)

Finally, add some keybindings; I registered <Leader>S to save the session, <Leader>l to load a session (this doesn't currently work), and <Leader>R to restore from the most recent previous save point:

config.keys = {
  -- Session manager bindings
  {
    key = 's',
    mods = 'LEADER|SHIFT',
    action = act({ EmitEvent = "save_session" }),
  },
  {
    key = 'L',
    mods = 'LEADER|SHIFT',
    action = act({ EmitEvent = "load_session" }),
  },
  {
    key = 'R',
    mods = 'LEADER|SHIFT',
    action = act({ EmitEvent = "restore_session" }),
  },
}
Ready to Launch

Wezterm ships with an XDG desktop file, which works, but it will always open the same window by default. Also, I prefer to have a simpler shortcut than <Super> + type something to search for the app. As such, I usually bind <Super>t to open my terminal.

For the actual command, I use wezterm-gui start --always-new-process. This ensures that I get a new window each time, and that it's not bound to another domain or workspace at start; I can use my other keybindings to attach to a domain, rename or create new workspaces, etc.

Watch for the drop!

I mentioned earlier that I find a dropdown terminal handy, and that I started using tdrop to provide one when I adopted kitty as my terminal. I was able to do the same with wezterm.

A few things to note:

  • tdrop does NOT play well with Wayland, but it provides a sneaky way to work within Wayland. You can prefix your command with WAYLAND_DISPLAY=no to force usage of XWayland when invoking your terminal. I had to do this with Wezterm, as otherwise it simply didn't work. It would spawn the terminal, but without resizing it.

  • For my dropdown terminal, I like to have it slightly transparent, and without window decorations.

  • I ALWAYS want this one using the mux domain. Since it's always running, I need to be able to look back at history and keep the panes in the same state between reboots or logging out.

To make things simpler for myself, I created a simple shell script:

###!/bin/bash
### File: $HOME/.local/bin/dropdown-terminal 
WAYLAND_DISPLAY=no \
  $HOME/.local/bin/tdrop --class=dropdown -mta \
  wezterm-gui \
    --config "window_background_opacity=0.85" \
    --config "window_decorations='NONE'" \
    start \
      --domain unix \
      --attach \
      --workspace dropdown

What does this do?

  • I have tdrop in my $HOME/.local/bin/ directory. I have found gnome-shell will not look in that path, despite it being in my shell profile, so I reference it fully.

  • I set the window class for the generated window.

  • And I tell tdrop to be aware of which monitor I'm on, and autosize the terminal based on the monitor dimensions.

  • I tell wezterm to override two configuration values:

  • I set the background opacity to 0.85, making it slightly transparent

  • I disable window decorations

  • and I tell it to connect to the "unix" domain on startup, attach to the existing window, and use the "dropdown" workspace (creating it if it does not exist).

In my gnome-shell keybindings, I bind <F12> to run $HOME/.local/bin/dropdown-terminal.

tdrop will then toggle the dropdown state everytime I hit that key. By default, it takes up 50% of the screen.

The full configuration

Here it is. I'm sure there's stuff I could do better, but it works.

wezterm.lua (click to expand/hide)
-- Pull in the wezterm API
local os              = require 'os'
local wezterm         = require 'wezterm'
local session_manager = require 'wezterm-session-manager/session-manager'
local act             = wezterm.action
local mux             = wezterm.mux

-- --------------------------------------------------------------------
-- FUNCTIONS AND EVENT BINDINGS
-- --------------------------------------------------------------------

-- Session Manager event bindings
-- See https://github.com/danielcopper/wezterm-session-manager
wezterm.on("save_session", function(window) session_manager.save_state(window) end)
wezterm.on("load_session", function(window) session_manager.load_state(window) end)
wezterm.on("restore_session", function(window) session_manager.restore_state(window) end)

-- Wezterm <-> nvim pane navigation
-- You will need to install https://github.com/aca/wezterm.nvim
-- and ensure you export NVIM_LISTEN_ADDRESS per the README in that repo

local move_around = function(window, pane, direction_wez, direction_nvim)
    local result = os.execute("env NVIM_LISTEN_ADDRESS=/tmp/nvim" .. pane:pane_id() .. " " .. wezterm.home_dir .. "/.local/bin/wezterm.nvim.navigator" .. " " .. direction_nvim)
    if result then
		window:perform_action(
            act({ SendString = "\x17" .. direction_nvim }),
            pane
        )
    else
        window:perform_action(
            act({ ActivatePaneDirection = direction_wez }),
            pane
        )
    end
end

wezterm.on("move-left", function(window, pane)
	move_around(window, pane, "Left", "h")
end)

wezterm.on("move-right", function(window, pane)
	move_around(window, pane, "Right", "l")
end)

wezterm.on("move-up", function(window, pane)
	move_around(window, pane, "Up", "k")
end)

wezterm.on("move-down", function(window, pane)
	move_around(window, pane, "Down", "j")
end)

local vim_resize = function(window, pane, direction_wez, direction_nvim)
	local result = os.execute(
		"env NVIM_LISTEN_ADDRESS=/tmp/nvim"
			.. pane:pane_id()
			.. " "
            .. wezterm.home_dir
			.. "/.local/bin/wezterm.nvim.navigator"
			.. " "
			.. direction_nvim
	)
	if result then
		window:perform_action(act({ SendString = "\x1b" .. direction_nvim }), pane)
	else
		window:perform_action(act({ ActivatePaneDirection = direction_wez }), pane)
	end
end

wezterm.on("resize-left", function(window, pane)
	vim_resize(window, pane, "Left", "h")
end)

wezterm.on("resize-right", function(window, pane)
	vim_resize(window, pane, "Right", "l")
end)

wezterm.on("resize-up", function(window, pane)
	vim_resize(window, pane, "Up", "k")
end)

wezterm.on("resize-down", function(window, pane)
	vim_resize(window, pane, "Down", "j")
end)

-- --------------------------------------------------------------------
-- CONFIGURATION
-- --------------------------------------------------------------------

-- This table will hold the configuration.
local config = {}

-- In newer versions of wezterm, use the config_builder which will
-- help provide clearer error messages
if wezterm.config_builder then
  config = wezterm.config_builder()
end

config.adjust_window_size_when_changing_font_size = false
config.automatically_reload_config = true
config.color_scheme = 'Solarized (dark) (terminal.sexy)'
config.enable_scroll_bar = true
config.enable_wayland = true
-- config.font = wezterm.font('Hack')
config.font = wezterm.font('Monaspace Neon')
config.font_size = 12.0
config.hide_tab_bar_if_only_one_tab = true
-- The leader is similar to how tmux defines a set of keys to hit in order to
-- invoke tmux bindings. Binding to ctrl-a here to mimic tmux
config.leader = { key = 'a', mods = 'CTRL', timeout_milliseconds = 2000 }
config.mouse_bindings = {
    -- Open URLs with Ctrl+Click
    {
        event = { Up = { streak = 1, button = 'Left' } },
        mods = 'CTRL',
        action = act.OpenLinkAtMouseCursor,
    }
}
config.pane_focus_follows_mouse = true
config.scrollback_lines = 5000
config.use_dead_keys = false
config.warn_about_missing_glyphs = false
config.window_decorations = 'TITLE | RESIZE'
config.window_padding = {
    left = 0,
    right = 0,
    top = 0,
    bottom = 0,
}

-- Tab bar
config.use_fancy_tab_bar = true
config.tab_bar_at_bottom = true
config.switch_to_last_active_tab_when_closing_tab = true
config.tab_max_width = 32
config.colors = {
    tab_bar = {
        active_tab = {
            fg_color = '#073642',
            bg_color = '#2aa198',
        }
    }
}

-- Setup muxing by default
config.unix_domains = {
  {
    name = 'unix',
  },
}

-- Custom key bindings
config.keys = {
    -- -- Disable Alt-Enter combination (already used in tmux to split pane)
    -- {
    --     key = 'Enter',
    --     mods = 'ALT',
    --     action = act.DisableDefaultAssignment,
    -- },

    -- Copy mode
    {
        key = '[',
        mods = 'LEADER',
        action = act.ActivateCopyMode,
    },

    -- ----------------------------------------------------------------
    -- TABS
    --
    -- Where possible, I'm using the same combinations as I would in tmux
    -- ----------------------------------------------------------------

    -- Show tab navigator; similar to listing panes in tmux
    {
        key = 'w',
        mods = 'LEADER',
        action = act.ShowTabNavigator,
    },
    -- Create a tab (alternative to Ctrl-Shift-Tab)
    {
        key = 'c',
        mods = 'LEADER',
        action = act.SpawnTab 'CurrentPaneDomain',
    },
    -- Rename current tab; analagous to command in tmux
    {
        key = ',',
        mods = 'LEADER',
        action = act.PromptInputLine {
            description = 'Enter new name for tab',
            action = wezterm.action_callback(
                function(window, pane, line)
                    if line then
                        window:active_tab():set_title(line)
                    end
                end
            ),
        },
    },
    -- Move to next/previous TAB
    {
        key = 'n',
        mods = 'LEADER',
        action = act.ActivateTabRelative(1),
    },
    {
        key = 'p',
        mods = 'LEADER',
        action = act.ActivateTabRelative(-1),
    },
    -- Close tab
    {
        key = '&',
        mods = 'LEADER|SHIFT',
        action = act.CloseCurrentTab{ confirm = true },
    },

    -- ----------------------------------------------------------------
    -- PANES
    --
    -- These are great and get me most of the way to replacing tmux
    -- entirely, particularly as you can use "wezterm ssh" to ssh to another
    -- server, and still retain Wezterm as your terminal there.
    -- ----------------------------------------------------------------

    -- -- Vertical split
    {
        -- |
        key = '|',
        mods = 'LEADER|SHIFT',
        action = act.SplitPane {
            direction = 'Right',
            size = { Percent = 50 },
        },
    },
    -- Horizontal split
    {
        -- -
        key = '-',
        mods = 'LEADER',
        action = act.SplitPane {
            direction = 'Down',
            size = { Percent = 50 },
        },
    },
    -- CTRL + (h,j,k,l) to move between panes
    {
        key = 'h',
        mods = 'CTRL',
        action = act({ EmitEvent = "move-left" }),
    },
    {
        key = 'j',
        mods = 'CTRL',
        action = act({ EmitEvent = "move-down" }),
    },
    {
        key = 'k',
        mods = 'CTRL',
        action = act({ EmitEvent = "move-up" }),
    },
    {
        key = 'l',
        mods = 'CTRL',
        action = act({ EmitEvent = "move-right" }),
    },
    -- ALT + (h,j,k,l) to resize panes
    {
        key = 'h',
        mods = 'ALT',
        action = act({ EmitEvent = "resize-left" }),
    },
    {
        key = 'j',
        mods = 'ALT',
        action = act({ EmitEvent = "resize-down" }),
    },
    {
        key = 'k',
        mods = 'ALT',
        action = act({ EmitEvent = "resize-up" }),
    },
    {
        key = 'l',
        mods = 'ALT',
        action = act({ EmitEvent = "resize-right" }),
    },
    -- Close/kill active pane
    {
        key = 'x',
        mods = 'LEADER',
        action = act.CloseCurrentPane { confirm = true },
    },
    -- Swap active pane with another one
    {
        key = '{',
        mods = 'LEADER|SHIFT',
        action = act.PaneSelect { mode = "SwapWithActiveKeepFocus" },
    },
    -- Zoom current pane (toggle)
    {
        key = 'z',
        mods = 'LEADER',
        action = act.TogglePaneZoomState,
    },
    {
        key = 'f',
        mods = 'ALT',
        action = act.TogglePaneZoomState,
    },
    -- Move to next/previous pane
    {
        key = ';',
        mods = 'LEADER',
        action = act.ActivatePaneDirection('Prev'),
    },
    {
        key = 'o',
        mods = 'LEADER',
        action = act.ActivatePaneDirection('Next'),
    },

    -- ----------------------------------------------------------------
    -- Workspaces
    --
    -- These are roughly equivalent to tmux sessions.
    -- ----------------------------------------------------------------

    -- Attach to muxer
    {
        key = 'a',
        mods = 'LEADER',
        action = act.AttachDomain 'unix',
    },

    -- Detach from muxer
    {
        key = 'd',
        mods = 'LEADER',
        action = act.DetachDomain { DomainName = 'unix' },
    },

    -- Show list of workspaces
    {
        key = 's',
        mods = 'LEADER',
        action = act.ShowLauncherArgs { flags = 'WORKSPACES' },
    },
    -- Rename current session; analagous to command in tmux
    {
        key = '$',
        mods = 'LEADER|SHIFT',
        action = act.PromptInputLine {
            description = 'Enter new name for session',
            action = wezterm.action_callback(
                function(window, pane, line)
                    if line then
                        mux.rename_workspace(
                            window:mux_window():get_workspace(),
                            line
                        )
                    end
                end
            ),
        },
    },

    -- Session manager bindings
    {
        key = 's',
        mods = 'LEADER|SHIFT',
        action = act({ EmitEvent = "save_session" }),
    },
    {
        key = 'L',
        mods = 'LEADER|SHIFT',
        action = act({ EmitEvent = "load_session" }),
    },
    {
        key = 'R',
        mods = 'LEADER|SHIFT',
        action = act({ EmitEvent = "restore_session" }),
    },
}

-- and finally, return the configuration to wezterm
return config

Closing

I've been really impressed by Wezterm! One thing that's absolutely magical is that I don't ever have to think about whether or not I've started tmux; I can just start splitting the window into panes on the fly as needed. On top of that, having the configuration be a limited programming language, and one that is NOT specific to Wezterm, means that I can (a) use a skill I already have, and (b) do some limited programming of terminal behavior, which allows me to customize it for my own use cases.

Would I recommend Wezterm to others? Absolutely! One reason I was excited to try it is so I could use a terminal I could potentially port elsewhere; Wezterm works on Linux, obviously, but also Windows and Mac, making it a great cross-platform replacement for whatever native terminals you were using previously. This can be hugely useful if you switch between systems regularly, or even if you are contemplating a switch in the future and want to make your landing as soft as possible.

Thanks, Wez, for this great software, and a huge thank you to your community and contributors as well!