While configuring Wezterm, I stumbled across Matthew O’Phinney’s blog post about Wezterm.
This post is based on his, so if you haven’t read it yet go read his first.
How I Chose Wezterm
I used to use exclusively tiling window managers (i3-> sway -> DWM*). For practical reasons I have used custom keybindings for tiling windows and swapping workspaces on Gnome. What started as constant tmux use turned occasional and was eventually superseded by vim’s (later neovim) split windows.
Gnome-terminal was less than satisfactory but hadn’t bothered with custom terminals** until recently.
I use Wayland and enjoy writing core code in modern type-safe languages. My neovim configuration enjoys the flexibility of Lua. Wezterm stood out when compared to the other modern terminal emulator options.
How Matthew uses Wezterm
His post was a fantastic introduction. It adds nice functional additions on top of the default configuration. Sessions were the original reason that I took interest in his post. The ability to switch between and resize neovim windows and Wezterm panes using the same keybindings was interesting. I had never used the tmux plugin that he mentioned, but I saw the benefit in that and was intrigued.
Another look at the integration plugin
After experimenting with the configuration that he shared, I took another look at the neovim integration plugin. Depending on it made me uncomfortable for a number of reasons:
- last update was 3 years ago
- low user activity (3 forks / 41 stars including mine)
- depends on deprecated behavior
None of the above is a dealbreaker for short-term use, but my experience with neovim is that the community is large and moves fast; lots of little projects get left behind as APIs change and larger projects gain community traction. The extra source dependency wasn’t appealing either.
Upon inspecting the code, I realized that the entire utility is 32 lines of go: a single RPC call.
A small change in the tooling
I set out to remove the dependency on the deprecated environment variable and Go.
Deprecated $NVIM_LISTEN_ADDRESS
The plugin uses the pane ID to tell neovim where to open the RPC socket for the
server to listen on. This is what it used $NVIM_LISTEN_ADDRESS
is used for.
This environment variable is
deprecated
because it overloaded for too many things which causes problems in various
ways. The location of the socket file can be set using --listen
, so a simple
shell alias allows us to tell neovim where to set the unix socket. Wezterm
communicates the pane ID to the current shell via the $WEZTERM_PANE
environment variable. If this environment variable exists, we can tell neovim
to use it with the following in .bashrc
.
## wezterm <-> neovim integration
#
# this sets the pane to a known filepath when neovim is invoked in a wezterm pane
# see ~/.weztermrc for where this gets used
#
# note: NVIM_RUNTIME_DIR is deprecated, and NVIM does not set socket location, per the docs
# --listen is the preferred way to set the primary socket
if [ -n "$WEZTERM_PANE" ]; then
alias nvim='nvim --listen $XDG_RUNTIME_DIR/wezterm/pane-${WEZTERM_PANE}.sock'
fi wezterm <-> neovim integration
#
# this sets the pane to a known filepath when neovim is invoked in a wezterm pane
# see ~/.weztermrc for where this gets used
#
# note: NVIM_RUNTIME_DIR is deprecated, and NVIM does not set socket location, per the docs
# --listen is the preferred way to set the primary socket
if [ -n "$WEZTERM_PANE" ]; then
alias nvim='nvim --listen $XDG_RUNTIME_DIR/wezterm/pane-${WEZTERM_PANE}.sock'
fi
Golang dependency
Wezterm is configured in lua. Neovim is configured in lua. Go as a language doesn’t add a benefit - and thankfully isn’t required.
Neovim supports using the nvim
command itself
for RPC calls with running neovim
sessions.
The following command in a key press event handler is sufficient to indicate whether the key press should be redirected to neovim or kept within Wezterm:
nvim --headless --server <socket file> --remote-expr 'winnr() == winnr("<direction>")'
The winnr() == winnr("<>")
piece above just checks to see if there is another
neovim window in the direction of the key press. If they are the same number,
then the key press is directed to Wezterm, otherwise sit is sent to neovim.
-- Wezterm <-> nvim pane integration
--
-- Before switching or resizing a wezterm pane, check first whether
-- switching or resizing a neovim window would be preferred.
--
-- This requires the neovim socket to be discoverable by wezterm at a known
-- location. NVIM_RUNTIME_DIR is the deprecated way to do that, '--listen' is
-- preferred. Therefore, set the following in your shell configuration
--
-- if [ -n "$WEZTERM_PANE" ]; then
-- alias nvim="nvim --listen $XDG_RUNTIME_DIR/wezterm/pane-${WEZTERM_PANE}.sock"
-- fi
--
-- This uses neovim as an RPC client to discover the position of the current window.
--
local commands_for_nvim = function(pane, direction)
-- if nvim is installed locally rather than system-wide, set this variable to
-- the correct path, otherwise Wezterm won't be able to find it and the nvim
-- command will exit with error code 127.
local nvim_path = "nvim"
local runtime_dir = os.getenv("XDG_RUNTIME_DIR")
if not runtime_dir then
-- no XDG_RUNTIME_DIR, no neovim pane integration
-- it is probably possible to support no XDG_RUNTIME_DIR -> why bother?
wezterm.log_warn("Environment variable XDG_RUNTIME_DIR not set, neovim integration not supported.")
return false
end
-- no socket exists, no neovim running in this pane
-- this assumes neovim was started with
local socket_dir = tostring(runtime_dir) .. "/wezterm/"
local socket = socket_dir .. "pane-" .. tostring(pane:pane_id()) .. ".sock"
-- standard library doesn't give you tools to check if a path exists
for _, file_name in ipairs(wezterm.read_dir(socket_dir)) do
if file_name == socket then
local cmd = nvim_path
.. " --headless --server "
.. socket
.. " --remote-expr 'winnr() == winnr(\""
.. direction
.. '")'
.. "'"
local handle = io.popen(cmd)
if not handle then
wezterm.log_error("Failed to send RPC to neovim server: " .. cmd)
return false
end
local result = handle:read("*a")
-- this is weird but not well documented and it works?
local rc = { handle:close() }
local exit_code = rc[3]
if exit_code ~= 0 then
wezterm.log_error("Failed to run RPC command: " .. cmd .. " - ")
wezterm.log_error(rc)
end
-- a debug log level would be nice, but since info events are
-- logged, leave this commented out unless debugging
-- wezterm.log_info(cmd .. " result: " .. tostring(result))
if "0" == result then
return true
else
return false
end
end
end
return false
end
That did most of the heavy lifting - now we just need to update the
move_around()
and vim_resize()
function from Matthew’s post:
-- move: integrate Wezterm and neovim keybindings
local move_around = function(window, pane, direction_wez, direction_nvim)
if commands_for_nvim(pane, direction_nvim) then
window:perform_action(act({ SendString = "\x17" .. direction_nvim }), pane)
else
window:perform_action(act({ ActivatePaneDirection = direction_wez }), pane)
end
end
-- resize: integrate Wezterm and neovim keybindings
local vim_resize = function(window, pane, direction_wez, direction_nvim)
if commands_for_nvim(pane, direction_nvim) then
window:perform_action(act({ SendString = "\x1b" .. direction_nvim }), pane)
else
window:perform_action(act({ AdjustPaneSize = { direction_wez, 2 } }), pane)
end
end
There you have it. No extra dependencies or deprecated environment variables!
Thanks, Matthew!
Notes
- a workflow based around the concept that single window could live on multiple workspace was powerful for me
** okay fine, I used to use terminator and urxvt
, probably some others too