sept 2024 progress
This commit is contained in:
966
static/archive/alexplescan-com-ffcy9q.txt
Normal file
966
static/archive/alexplescan-com-ffcy9q.txt
Normal file
@@ -0,0 +1,966 @@
|
||||
[1]Alex Plescan
|
||||
|
||||
[2]Blog [3]Projects [4]Newsletter
|
||||
|
||||
Okay, I really like WezTerm
|
||||
|
||||
10 August 2024 | [5]Permalink
|
||||
|
||||
A while back [6]my friend recommended that I try [7]WezTerm. I’d been an iTerm
|
||||
2 stalwart for the better part of a decade, but not to be too narrow-minded I
|
||||
conceded, started it up, and saw this:
|
||||
|
||||
screenshot of WezTerm's default look
|
||||
|
||||
Does the job, sure, but doesn’t feel quite right. Okay then, experiment over.
|
||||
Back to iTerm…
|
||||
|
||||
Fast forward a couple of months and I got the itch to try a new terminal again.
|
||||
I wanted to use one whose config was entirely text based so I could pop it in
|
||||
to my dotfiles and share it across my work and personal machines. A few
|
||||
terminals already do this, but whispers of WezTerm’s powerful API and Lua
|
||||
config got me particularly interested.
|
||||
|
||||
I tried it again with a bit more patience and I’m glad I did. My terminal is
|
||||
prettier than it’s ever been, more functional, and I can finally justify my
|
||||
mechanical keyboard purchase with all the keybindings I’ve configured.
|
||||
|
||||
This post is an introduction to configuring WezTerm based on the setup that I
|
||||
eventually landed on. I’d consider it relatively low-frills. Most of what I
|
||||
talk about here can already be found in WezTerm’s [8]docs, but as they’ve got a
|
||||
large surface area, I’m hoping this post will be a useful jumping off point for
|
||||
WezTerm beginners.
|
||||
|
||||
We won’t be looking at some of WezTerm’s key features, like custom hyperlinks
|
||||
highlighting rules, searchable scrollback, quick copy mode, and image support
|
||||
(you can find [9]more details here).
|
||||
|
||||
The feature I find most exciting about WezTerm is the flexibility of its Lua
|
||||
config, so we’ll be focusing on that. This includes configuring appearance,
|
||||
keybindings, multiplexing, workspace navigation, status bar setup, and dynamic
|
||||
theming. By the end of it all, we’ll have a terminal that looks like this:
|
||||
|
||||
screenshot of the WezTerm look we'll end up with at the end of this post
|
||||
|
||||
Subtly prettier than the default, and with some great features to boot.
|
||||
|
||||
I use macOS, so what follows is focused on ergonomics that make WezTerm great
|
||||
there. I haven’t tested my config on other systems, but I’m not doing anything
|
||||
too bespoke so things should be portable (WezTerm works pretty much
|
||||
everywhere).
|
||||
|
||||
tl;dr? Here’s [10]a gist containing the config we’ll end up with.
|
||||
|
||||
Pre-flight checks
|
||||
|
||||
Start by installing WezTerm. Instructions for this are on [11]WezTerm’s site.
|
||||
If you’re on macOS and reading this you probably have Homebrew installed, so $
|
||||
brew install wezterm will do the trick.
|
||||
|
||||
Now launch WezTerm, and you’re already winning.
|
||||
|
||||
A note on Lua
|
||||
|
||||
My favourite WezTerm feature is its use of Lua for defining config. Unlike
|
||||
terminals where your settings are adjusted via the UI (iTerm 2), your WezTerm
|
||||
config lives in your dotfiles and is portable across all your machines.
|
||||
|
||||
And unlike other terminals where your configuration is written using a data
|
||||
serialization format like YAML or TOML (Alacritty, kitty), with Lua you can
|
||||
more easily achieve complex configs by leveraging dynamic scripts.
|
||||
|
||||
Granted, Lua is a programming language so it is trickier to learn than YAML or
|
||||
TOML, but it’s still remarkably simple. If you’ve used another dynamic
|
||||
programming language (e.g. Ruby, Python, JavaScript) - you should be able to
|
||||
read the Lua code in this post easily. For achieving more complex configs, I’d
|
||||
recommend diving deeper into the language. Its [12]Getting Started guide is a
|
||||
good place to… get started.
|
||||
|
||||
Config files, and the best feedback loop in town
|
||||
|
||||
WezTerm supports loading in its config from all the usual places on your system
|
||||
([13]docs). For this guide we’re going to be creating our config in
|
||||
$XDG_CONFIG_HOME/wezterm/wezterm.lua. On most systems (including macOS) this
|
||||
resolves to ~/.config/wezterm/wezterm.lua. Using a directory to store our
|
||||
config instead of dumping it in ~/.wezterm.lua will let us keep our config
|
||||
logically grouped as we split some of it out into different files.
|
||||
|
||||
Create the wezterm.lua file on that path, and add this boilerplate to it:
|
||||
|
||||
-- Import the wezterm module
|
||||
local wezterm = require 'wezterm'
|
||||
-- Creates a config object which we will be adding our config to
|
||||
local config = wezterm.config_builder()
|
||||
|
||||
-- (This is where our config will go)
|
||||
|
||||
-- Returns our config to be evaluated. We must always do this at the bottom of this file
|
||||
return config
|
||||
|
||||
Save the file and all going well… nothing will happen. Well, at least nothing
|
||||
appeared to happen, but what WezTerm did behind the scenes is quite magical. It
|
||||
watched your config file, and when it changed it auto-reloaded instantly. This
|
||||
feature makes for a wonderfully tight feedback loop where you don’t need to
|
||||
restart your terminal to see the effects of your new config.
|
||||
|
||||
We can quickly test this auto-reload by adding some invalid syntax and seeing
|
||||
what happens. Replace the call to wezterm.config_builder() with
|
||||
wezterm.config_builderZ(), save, and you should immediately see a window pop-up
|
||||
with:
|
||||
|
||||
runtime error: [string "/Users/alex/.config/wezterm/wezterm.lua"]:2: attempt
|
||||
to call a nil value (field 'config_builderZ')
|
||||
stack traceback:
|
||||
[string "/Users/alex/.config/wezterm/wezterm.lua"]:2: in main chunk
|
||||
|
||||
How’s that for a feedback loop? Fix the error and save the file again.
|
||||
|
||||
This time, have your config log something:
|
||||
|
||||
wezterm.log_info("hello world! my name is " .. wezterm.hostname())
|
||||
|
||||
Save. Now… where did that log go? Press CTRL + SHIFT + L to bring up the debug
|
||||
overlay ([14]docs) and lo and behold, your beautiful log was waiting for you
|
||||
all along. Not only that but what you’re looking at is a full Lua REPL. Enter 1
|
||||
+ 1 and you’ll see the result. Enter wezterm.home_dir and you’ll see the result
|
||||
of accessing the home_dir entry on the wezterm module ([15]docs).
|
||||
|
||||
screenshot of the WezTerm's debug overlay
|
||||
|
||||
The combination of hot reloading and the debug overlay makes experimenting with
|
||||
WezTerm configs extremely low friction and low consequence. The feedback loop
|
||||
is so tight now it’s more like a feedback lp.
|
||||
|
||||
Configuring appearance
|
||||
|
||||
Okay enough gushing - let’s cut to the chase and make this thing prettier. Add
|
||||
a few lines to the config to start customising the look of the terminal. We’ll
|
||||
start with a colour scheme ([16]docs):
|
||||
|
||||
-- Pick a colour scheme. WezTerm ships with more than 1,000!
|
||||
-- Find them here: https://wezfurlong.org/wezterm/colorschemes/index.html
|
||||
config.color_scheme = 'Tokyo Night'
|
||||
|
||||
Save, and you should immediately see it update. Thanks Wez!
|
||||
|
||||
screenshot of applying a colour scheme to WezTerm
|
||||
|
||||
(if the hot config reload doesn’t work for whatever reason, you can manually
|
||||
reload it by pressing CMD + R).
|
||||
|
||||
Many colours, all at once
|
||||
|
||||
With over 1,000 colour choices to choose from, it’s tough to decide on your
|
||||
favourite. Why not outsource that work to your computer? Let’s explore the
|
||||
power of WezTerm’s dynamic config by randomly assigning a colour scheme for
|
||||
each new window you open:
|
||||
|
||||
-- Creates a lua table containing the name of every color scheme WezTerm
|
||||
-- ships with.
|
||||
local scheme_names = {}
|
||||
for name, scheme in pairs(wezterm.color.get_builtin_schemes()) do
|
||||
table.insert(scheme_names, name)
|
||||
end
|
||||
|
||||
-- When the config for a window is reloaded (i.e. when you save this file
|
||||
-- or open a new window)...
|
||||
wezterm.on('window-config-reloaded', function(window, pane)
|
||||
-- Don't proceed if the config has already been overriden, otherwise
|
||||
-- we'll enter an infinite loop of neverending colour scheme changes.
|
||||
-- If that sounds like your kinda thing, then remove this line ;) - but
|
||||
-- don't say you haven't been warned.
|
||||
if window:get_config_overrides() then return end
|
||||
-- Pick a random colour scheme name.
|
||||
local scheme = scheme_names[math.random(#scheme_names)]
|
||||
-- Assign it as an override for this window.
|
||||
window:set_config_overrides { color_scheme = scheme }
|
||||
-- And log it for good measure
|
||||
wezterm.log_info("Your colour scheme is now: " .. scheme)
|
||||
end)
|
||||
|
||||
Open up a few windows (CMD + N on macOS) and each one will have a different
|
||||
colour scheme. A cornucopia of terminals, each more surprising than the last.
|
||||
We, my friends, are truly innovating now.
|
||||
|
||||
screenshot of many WezTerm terminal windows, each with a distinctive colour
|
||||
scheme
|
||||
|
||||
But really, that was kind of a dumb idea meant to prove a point. Now that
|
||||
you’ve gotten a taste for dynamic config, you probably wanna remove those lines
|
||||
and stick to a colour scheme you do like.
|
||||
|
||||
(You may find that after you remove that code and add your static color_scheme
|
||||
config back in, it doesn’t hot reload. That’s because our script set an
|
||||
override on the config specific to each window. To clear your overrides, you
|
||||
can go to your debug terminal and type window:set_config_overrides({}) - or you
|
||||
can just close and reopen your WezTerm window).
|
||||
|
||||
Respecting the system’s appearance
|
||||
|
||||
Light themes, dark themes… why not both? Let’s have the terminal’s colour
|
||||
scheme automatically change when the operating system’s appearance changes.
|
||||
While we’re at it, we’ll learn how to split up WezTerm config into different
|
||||
modules.
|
||||
|
||||
Create a new file alongside wezterm.lua and call it appearance.lua. Add this to
|
||||
it:
|
||||
|
||||
-- We almost always start by importing the wezterm module
|
||||
local wezterm = require 'wezterm'
|
||||
-- Define a lua table to hold _our_ module's functions
|
||||
local module = {}
|
||||
|
||||
-- Returns a bool based on whether the host operating system's
|
||||
-- appearance is light or dark.
|
||||
function module.is_dark()
|
||||
-- wezterm.gui is not always available, depending on what
|
||||
-- environment wezterm is operating in. Just return true
|
||||
-- if it's not defined.
|
||||
if wezterm.gui then
|
||||
-- Some systems report appearance like "Dark High Contrast"
|
||||
-- so let's just look for the string "Dark" and if we find
|
||||
-- it assume appearance is dark.
|
||||
return wezterm.gui.get_appearance():find("Dark")
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return module
|
||||
|
||||
Back in wezterm.lua:
|
||||
|
||||
-- Import our new module (put this near the top of your wezterm.lua)
|
||||
local appearance = require 'appearance'
|
||||
|
||||
-- Use it!
|
||||
if appearance.is_dark() then
|
||||
config.color_scheme = 'Tokyo Night'
|
||||
else
|
||||
config.color_scheme = 'Tokyo Night Day'
|
||||
end
|
||||
|
||||
Toggle your system appearance between dark mode and light mode, and watch your
|
||||
theme change right before your eyes.
|
||||
|
||||
screenshot of WezTerm in light and dark mode
|
||||
|
||||
Fonts
|
||||
|
||||
Next up let’s look at fonts. WezTerm ships with the lovely JetBrains Mono, and
|
||||
Nerd Font Symbols ([17]docs) so there’s nothing to complain about there. I do
|
||||
prefer Berkeley Mono at 13 points though, so:
|
||||
|
||||
-- Choose your favourite font, make sure it's installed on your machine
|
||||
config.font = wezterm.font({ family = 'Berkeley Mono' })
|
||||
-- And a font size that won't have you squinting
|
||||
config.font_size = 13
|
||||
|
||||
There’s good support for ligatures and other fancy font settings if you’re into
|
||||
that ([18]docs), but I’m not so let’s move on.
|
||||
|
||||
Window styling
|
||||
|
||||
Let’s style our terminal’s window. This controls the chrome that appears around
|
||||
it, and can vary between operating systems. On macOS, I like the below:
|
||||
|
||||
-- Slightly transparent and blurred background
|
||||
config.window_background_opacity = 0.9
|
||||
config.macos_window_background_blur = 30
|
||||
-- Removes the title bar, leaving only the tab bar. Keeps
|
||||
-- the ability to resize by dragging the window's edges.
|
||||
-- On macOS, 'RESIZE|INTEGRATED_BUTTONS' also looks nice if
|
||||
-- you want to keep the window controls visible and integrate
|
||||
-- them into the tab bar.
|
||||
config.window_decorations = 'RESIZE'
|
||||
-- Sets the font for the window frame (tab bar)
|
||||
config.window_frame = {
|
||||
-- Berkeley Mono for me again, though an idea could be to try a
|
||||
-- serif font here instead of monospace for a nicer look?
|
||||
font = wezterm.font({ family = 'Berkeley Mono', weight = 'Bold' }),
|
||||
font_size = 11,
|
||||
}
|
||||
|
||||
screenshot of WezTerm after we've styled its window
|
||||
|
||||
Now, let’s do something a little kitsch. See that empty space to the right of
|
||||
our terminal’s tab bar? Let’s fill it with a powerline looking status bar.
|
||||
We’ll add an update-status callback:
|
||||
|
||||
wezterm.on('update-status', function(window)
|
||||
-- Grab the utf8 character for the "powerline" left facing
|
||||
-- solid arrow.
|
||||
local SOLID_LEFT_ARROW = utf8.char(0xe0b2)
|
||||
|
||||
-- Grab the current window's configuration, and from it the
|
||||
-- palette (this is the combination of your chosen colour scheme
|
||||
-- including any overrides).
|
||||
local color_scheme = window:effective_config().resolved_palette
|
||||
local bg = color_scheme.background
|
||||
local fg = color_scheme.foreground
|
||||
|
||||
window:set_right_status(wezterm.format({
|
||||
-- First, we draw the arrow...
|
||||
{ Background = { Color = 'none' } },
|
||||
{ Foreground = { Color = bg } },
|
||||
{ Text = SOLID_LEFT_ARROW },
|
||||
-- Then we draw our text
|
||||
{ Background = { Color = bg } },
|
||||
{ Foreground = { Color = fg } },
|
||||
{ Text = ' ' .. wezterm.hostname() .. ' ' },
|
||||
}))
|
||||
end)
|
||||
|
||||
screenshot of WezTerm with a right status bar showing the system's hostname
|
||||
|
||||
A few interesting things happening here:
|
||||
|
||||
1. We just used WezTerm’s events API with wezterm.on. Events are things that
|
||||
happen to the terminal (e.g. window resize) that we can define callbacks
|
||||
for. The update-status event is emitted periodically when the terminal is
|
||||
ready to have its status updated. WezTerm manages this cleverly to ensure
|
||||
that only one such update can run at any given time, and if your code takes
|
||||
too long to execute, a timeout will be hit and your handler will be
|
||||
abandoned… protecting your terminal from bogging down.
|
||||
2. We’re grabbing the effective_config() of the window to get the “effective”
|
||||
configuration, which is the config with any overrides applied. From this we
|
||||
can get the resolved_palette, which is the currently active colour scheme.
|
||||
To see what this data looks like you can enter the debug overlay (CTRL +
|
||||
SHIFT + L) and execute window:effective_config().resolved_palette.
|
||||
3. We’re using the wezterm.format function ([19]docs) to style our string with
|
||||
colours. Other ways you could format text include setting font weight,
|
||||
underlining text, and more.
|
||||
4. Finally, the wezterm.hostname() function ([20]docs) gives us the hostname
|
||||
of the machine we’re running on. WezTerm ships with a bunch of useful
|
||||
functions for getting the state of your system, and also… we’re doing stuff
|
||||
in Lua - so you have full access to your file system, are able to make
|
||||
network requests, etc.
|
||||
|
||||
Altogether this gives us a powerline…ish. It’s a bit sad with only one segment
|
||||
isn’t it? Don’t you worry, we’ll be adding more soon…
|
||||
|
||||
Keys
|
||||
|
||||
Here’s the part where we justify our mechanical keyboard purchases. Let’s set
|
||||
up some key assignments. During this section we’ll look at WezTerm’s deep key
|
||||
handling capabilities and ability to take action based on your input.
|
||||
|
||||
By default, WezTerm defines some standard key assignments ([21]docs). I leave
|
||||
them on because they’re very sensible, but if you wanna really wrest total
|
||||
control of your config, you can turn them off with
|
||||
config.disable_default_key_bindings = true.
|
||||
|
||||
Our first key assignment will be a humble start for us macOS users… you might
|
||||
be used to Option + Left Arrow and Option + Right Arrow jumping between words
|
||||
on your terminal. That’s the default in iTerm 2 and Terminal.app, but not in
|
||||
WezTerm. However, we can map it!
|
||||
|
||||
We do this by adding a keys table to our config:
|
||||
|
||||
-- Table mapping keypresses to actions
|
||||
config.keys = {
|
||||
-- Sends ESC + b and ESC + f sequence, which is used
|
||||
-- for telling your shell to jump back/forward.
|
||||
{
|
||||
-- When the left arrow is pressed
|
||||
key = 'LeftArrow',
|
||||
-- With the "Option" key modifier held down
|
||||
mods = 'OPT',
|
||||
-- Perform this action, in this case - sending ESC + B
|
||||
-- to the terminal
|
||||
action = wezterm.action.SendString '\x1bb',
|
||||
},
|
||||
{
|
||||
key = 'RightArrow',
|
||||
mods = 'OPT',
|
||||
action = wezterm.action.SendString '\x1bf',
|
||||
},
|
||||
}
|
||||
|
||||
By now you’ve probably figured out that you’re gonna be spending more time
|
||||
configuring WezTerm than doing actual work. There’s no shame in admitting this
|
||||
reality, so let’s encode it into our config. On macOS, the default shortcut for
|
||||
opening an application’s preferences is CMD + , - let’s make it so when we
|
||||
press this, our favourite editor opens up the WezTerm config. I’m using neovim,
|
||||
but feel free to substitute with your own:
|
||||
|
||||
config.keys = {
|
||||
-- ... add these new entries to your config.keys table
|
||||
{
|
||||
key = ',',
|
||||
mods = 'SUPER',
|
||||
action = wezterm.action.SpawnCommandInNewTab {
|
||||
cwd = wezterm.home_dir,
|
||||
args = { 'nvim', wezterm.config_file },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Try that out, but you may see an error along the lines of:
|
||||
|
||||
Unable to spawn nvim because:
|
||||
No viable candidates found in PATH "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
|
||||
If that error showed up, it’s typically because the process that launched
|
||||
WezTerm didn’t include a PATH environment variable that led to your editor’s
|
||||
binary (e.g. on macOS, Finder is usually WezTerm’s parent). We can work around
|
||||
this by specifying the full path to your editor in the SpawnCommandInNewTab
|
||||
properties ([22]docs), or by updating the default environment variables WezTerm
|
||||
spawns commands with. I prefer the latter, since it means that any other places
|
||||
in our config where we might spawn new commands will also inherit the same env
|
||||
vars:
|
||||
|
||||
config.set_environment_variables = {
|
||||
PATH = '/opt/homebrew/bin:' .. os.getenv('PATH')
|
||||
}
|
||||
|
||||
Try that again, and it should work.
|
||||
|
||||
We really are just scratching the surface of all the commands available ([23]
|
||||
WezTerm supports a lot). In the next section, we’ll be growing our key bindings
|
||||
further.
|
||||
|
||||
Multiplexing terminals, levelling up key assignments
|
||||
|
||||
Let’s move on to WezTerm’s multiplexing capabilities. If you make use of a
|
||||
multiplexer (i.e. tmux) then you may consider using WezTerm’s builtin
|
||||
capabilities instead. They’ll generally give you a more integrated experience,
|
||||
with individual scrollback buffers per pane, better mouse control, easier
|
||||
selection functionality, and generally faster performance.
|
||||
|
||||
Hit CTRL + SHIFT + P to bring up WezTerm’s command palette. (Yes, WezTerm has a
|
||||
command palette. Yes, it’s as customisable as everything else we’ve seen so
|
||||
far. No, we won’t dwell on it here). Type split horizontally until the “Shell:
|
||||
Split Horizontally” option is selected and hit ENTER. Ta-da! Your shell split
|
||||
horizontally! Do the same for split vertically and… you get the idea.
|
||||
|
||||
screenshot of WezTerm's command palette
|
||||
|
||||
You may have noticed that the command palette displays the keyboard shortcut
|
||||
assigned to each action. The ones for splitting are quite a fingerful, e.g.
|
||||
SHIFT + CTRL + OPTION + ". I get why they’re this complicated - because they’re
|
||||
trying not to clash with any other shortcuts you may have on your system, but
|
||||
we can do a lot better - and WezTerm gives us the tools do so easily!
|
||||
|
||||
Splitting panes, leader key
|
||||
|
||||
A leader key ([24]docs) is a special key combination that you press first,
|
||||
followed by another key combination, to perform a specific action. It can help
|
||||
you create complex shortcuts without needing to push a lot of keys all at once.
|
||||
|
||||
Sounds like a perfect fit for splitting panes, right? We’ll bind our leader to
|
||||
CTRL + A, and in case you accidentally type the leader without following it up
|
||||
with another key, we’ll have it automatically deactivate after 1,000
|
||||
milliseconds.
|
||||
|
||||
-- If you're using emacs you probably wanna choose a different leader here,
|
||||
-- since we're gonna be making it a bit harder to CTRL + A for jumping to
|
||||
-- the start of a line
|
||||
config.leader = { key = 'a', mods = 'CTRL', timeout_milliseconds = 1000 }
|
||||
|
||||
Next let’s define some key assignments for splitting panes:
|
||||
|
||||
config.keys = {
|
||||
-- ... add these new entries to your config.keys table
|
||||
{
|
||||
-- I'm used to tmux bindings, so am using the quotes (") key to
|
||||
-- split horizontally, and the percent (%) key to split vertically.
|
||||
key = '"',
|
||||
-- Note that instead of a key modifier mapped to a key on your keyboard
|
||||
-- like CTRL or ALT, we can use the LEADER modifier instead.
|
||||
-- This means that this binding will be invoked when you press the leader
|
||||
-- (CTRL + A), quickly followed by quotes (").
|
||||
mods = 'LEADER',
|
||||
action = wezterm.action.SplitHorizontal { domain = 'CurrentPaneDomain' },
|
||||
},
|
||||
{
|
||||
key = '%',
|
||||
mods = 'LEADER',
|
||||
action = wezterm.action.SplitVertical { domain = 'CurrentPaneDomain' },
|
||||
},
|
||||
}
|
||||
|
||||
Give it a go now. Press CTRL + A, quickly followed by ", and you’ll get a
|
||||
horizontal split. Use the other assignment and you’ll get a vertical split.
|
||||
|
||||
screenshot of WezTerm's with split panes
|
||||
|
||||
Before we move on - you might be wondering what happens if you actually want to
|
||||
send the CTRL + A keypress without invoking the leader? CTRL + A is useful in
|
||||
and of its own as pressing it jumps to the start of a line on your shell (and
|
||||
on operating systems like Emacs).
|
||||
|
||||
Well there’s a solution for that. We can map CTRL + A quickly followed by CTRL
|
||||
+ A to send a CTRL + A to our terminal. That’s a confusing sentence! It’ll be
|
||||
simpler to just look at the config:
|
||||
|
||||
config.keys = {
|
||||
-- ... add these new entries to your config.keys table
|
||||
{
|
||||
key = 'a',
|
||||
-- When we're in leader mode _and_ CTRL + A is pressed...
|
||||
mods = 'LEADER|CTRL',
|
||||
-- Actually send CTRL + A key to the terminal
|
||||
action = wezterm.action.SendKey { key = 'a', mods = 'CTRL' },
|
||||
},
|
||||
},
|
||||
|
||||
Moving around panes
|
||||
|
||||
Okay with that done, let’s get back to multiplexing. Next up, navigating our
|
||||
splits. I like to use vim direction keybindings, but feel free to replace with
|
||||
arrow keys instead.
|
||||
|
||||
config.keys = {
|
||||
-- ... add these new entries to your config.keys table
|
||||
{
|
||||
-- I like to use vim direction keybindings, but feel free to replace
|
||||
-- with directional arrows instead.
|
||||
key = 'j', -- or DownArrow
|
||||
mods = 'LEADER',
|
||||
action = wezterm.action.ActivatePaneDirection('Down'),
|
||||
},
|
||||
{
|
||||
key = 'k', -- or UpArrow
|
||||
mods = 'LEADER',
|
||||
action = wezterm.action.ActivatePaneDirection('Up'),
|
||||
},
|
||||
{
|
||||
key = 'h', -- or LeftArrow
|
||||
mods = 'LEADER',
|
||||
action = wezterm.action.ActivatePaneDirection('Left'),
|
||||
},
|
||||
{
|
||||
key = 'l', -- or RightArrow
|
||||
mods = 'LEADER',
|
||||
action = wezterm.action.ActivatePaneDirection('Right'),
|
||||
},
|
||||
}
|
||||
|
||||
Look at all that duplication - We’re using a dynamic language for our config
|
||||
here, we don’t need to stand for that! Let’s go on a little side quest and see
|
||||
if we can extract it to a function.
|
||||
|
||||
local function move_pane(key, direction)
|
||||
return {
|
||||
key = key,
|
||||
mods = 'LEADER',
|
||||
action = wezterm.action.ActivatePaneDirection(direction),
|
||||
}
|
||||
end
|
||||
|
||||
config.keys = {
|
||||
-- ... remove the previous move bindings, and replace with
|
||||
move_pane('j', 'Down'),
|
||||
move_pane('k', 'Up'),
|
||||
move_pane('h', 'Left'),
|
||||
move_pane('l', 'Right'),
|
||||
}
|
||||
|
||||
Ooh so much smaller, but it could be smaller still. I dare you to keep code
|
||||
golfing this down to 6 lines. Go on - I believe in you!
|
||||
|
||||
Resizing panes, and introducing key tables
|
||||
|
||||
You might’ve figured out that you can resize panes by dragging the edge of one
|
||||
with your mouse, but we’re developers here, not olympic athletes. What’re we
|
||||
expected to move our hands away from the safety of our keyboard and over to the
|
||||
mouse?! No! I won’t stand for it and neither should you!
|
||||
|
||||
It’d be really nice to use the same keys that we use for moving between the
|
||||
panes for resizing (h, j, k, l)… but they’ve already been mapped… we could add
|
||||
another key modifier that needs to be held down when we want to resize vs. move
|
||||
between the panes:
|
||||
|
||||
config.keys = {
|
||||
-- ... add this new entry to your config.keys table
|
||||
{
|
||||
key = 'h',
|
||||
mods = 'LEADER|CTRL',
|
||||
-- "3" here is the amount of cells we wish to resize
|
||||
-- the terminal by
|
||||
action = wezterm.action.AdjustPaneSize { 'Left', 3 },
|
||||
},
|
||||
}
|
||||
|
||||
But that’s no good really. We have to first push our leader CTRL + A, then push
|
||||
CTRL + H, and keep repeating that each time we wanna resize the pane to the
|
||||
left. Fingers getting sore. Send help. Oh, here comes WezTerm with the
|
||||
antidote: [25]key tables.
|
||||
|
||||
When you activate a key table you’re entering a different mode with its own set
|
||||
of assignments for whatever you’re doing. This allows you to have multiple
|
||||
layers of assignments that are context specific.
|
||||
|
||||
It’s a similar kind of concept to the leader key, but unlike it, our key table
|
||||
will not automatically deactivate after an action is invoked, so it’ll be a
|
||||
good fit for resizing, where we want to keep pressing the same button over and
|
||||
over again until we’re happy with our pane’s new size.
|
||||
|
||||
With all that… this is easier done that said, so let’s check out the code:
|
||||
|
||||
local function resize_pane(key, direction)
|
||||
return {
|
||||
key = key,
|
||||
action = wezterm.action.AdjustPaneSize { direction, 3 }
|
||||
}
|
||||
end
|
||||
|
||||
config.keys = {
|
||||
-- ... remove the yucky keybinding from above and replace it with this
|
||||
{
|
||||
-- When we push LEADER + R...
|
||||
key = 'r',
|
||||
mods = 'LEADER',
|
||||
-- Activate the `resize_panes` keytable
|
||||
action = wezterm.action.ActivateKeyTable {
|
||||
name = 'resize_panes',
|
||||
-- Ensures the keytable stays active after it handles its
|
||||
-- first keypress.
|
||||
one_shot = false,
|
||||
-- Deactivate the keytable after a timeout.
|
||||
timeout_milliseconds = 1000,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
config.key_tables = {
|
||||
resize_panes = {
|
||||
resize_pane('j', 'Down'),
|
||||
resize_pane('k', 'Up'),
|
||||
resize_pane('h', 'Left'),
|
||||
resize_pane('l', 'Right'),
|
||||
},
|
||||
}
|
||||
|
||||
Now you can push CTRL + A to activate leader, then R to activate the resizing
|
||||
layer… and movement keys to resize to your heart’s content. When 1,000
|
||||
milliseconds have elapsed, you’ll automatically exit the resizing layer and be
|
||||
back to the default keytable.
|
||||
|
||||
WezTerm intensifies…
|
||||
|
||||
(While we’re on multiplexing, if you’re using neovim, I’d recommend checking
|
||||
out [26]smart-splits.nvim - that’ll let you jump between your vim panes and
|
||||
your WezTerm ones).
|
||||
|
||||
Project workspaces
|
||||
|
||||
Okay let’s graduate from WezTerm university with one final assignment… project
|
||||
workspaces.
|
||||
|
||||
I’m often working across a few different projects at a time, and need to be
|
||||
able to quickly switch between them. I want each project to maintain its own
|
||||
multiplexer instance with its own windows, panes, and tabs. In tmux you might
|
||||
achieve this with different sessions. In WezTerm we’ll do it with [27]
|
||||
workspaces.
|
||||
|
||||
Creating and switching between workspaces
|
||||
|
||||
Create a new file in your config directory and call it projects.lua. We’ll use
|
||||
this to provide some project switching functions to our main config file.
|
||||
|
||||
local wezterm = require 'wezterm'
|
||||
local module = {}
|
||||
|
||||
local function project_dirs()
|
||||
return {
|
||||
'~/Projects/mailgrip',
|
||||
'~/Projects/alexplescan.com',
|
||||
'~/Projects/wezterm_love_letters',
|
||||
-- ... keep going, list all your projects
|
||||
-- (or don't if you value your time. we'll improve on this soon)
|
||||
}
|
||||
end
|
||||
|
||||
function module.choose_project()
|
||||
local choices = {}
|
||||
for _, value in ipairs(project_dirs()) do
|
||||
table.insert(choices, { label = value })
|
||||
end
|
||||
|
||||
-- The InputSelector action presents a modal UI for choosing between a set of options
|
||||
-- within WezTerm.
|
||||
return wezterm.action.InputSelector {
|
||||
title = 'Projects',
|
||||
-- The options we wish to choose from
|
||||
choices = choices,
|
||||
-- Yes, we wanna fuzzy search (so typing "alex" will filter down to
|
||||
-- "~/Projects/alexplescan.com")
|
||||
fuzzy = true,
|
||||
-- The action we want to perform. Note that this doesn't have to be a
|
||||
-- static definition as we've done before, but can be a callback that
|
||||
-- evaluates any arbitrary code.
|
||||
action = wezterm.action_callback(function(child_window, child_pane, id, label)
|
||||
-- As a placeholder, we'll log the name of what you picked
|
||||
wezterm.log_info("you chose " .. label)
|
||||
end),
|
||||
}
|
||||
end
|
||||
|
||||
return module
|
||||
|
||||
… and in your wezterm.lua:
|
||||
|
||||
local projects = require 'projects'
|
||||
|
||||
config.keys = {
|
||||
-- ... add these new entries to your config.keys table
|
||||
{
|
||||
key = 'p',
|
||||
mods = 'LEADER',
|
||||
-- Present in to our project picker
|
||||
action = projects.choose_project(),
|
||||
},
|
||||
{
|
||||
key = 'f',
|
||||
mods = 'LEADER',
|
||||
-- Present a list of existing workspaces
|
||||
action = wezterm.action.ShowLauncherArgs { flags = 'FUZZY|WORKSPACES' },
|
||||
},
|
||||
}
|
||||
|
||||
Lots going on here, take your time to read it and the comments. And give it a
|
||||
go! Push LEADER + P, and you’ll see the project input selector come up. Pick a
|
||||
project by highlighting one and pushing ENTER, or push CTRL + C to close the
|
||||
picker. Once you’ve picked a project you’ll see its directory logged to your
|
||||
debug overlay (CTRL + SHIFT + L).
|
||||
|
||||
screenshot of WezTerm's with the workspace switcher we've configured
|
||||
|
||||
Still a couple of issues though… it’s really annoying to type out all your
|
||||
projects by hand in that file, and, uh, what was the other issue? Oh yeah! When
|
||||
you pick a project nothing happens. Okay, let’s fix these. Back in
|
||||
projects.lua, we’ll start by having the list of projects automatically
|
||||
populate.
|
||||
|
||||
-- The directory that contains all your projects.
|
||||
local project_dir = wezterm.home_dir .. "/Projects"
|
||||
|
||||
local function project_dirs()
|
||||
-- Start with your home directory as a project, 'cause you might want
|
||||
-- to jump straight to it sometimes.
|
||||
local projects = { wezterm.home_dir }
|
||||
|
||||
-- WezTerm comes with a glob function! Let's use it to get a lua table
|
||||
-- containing all subdirectories of your project folder.
|
||||
for _, dir in ipairs(wezterm.glob(project_dir .. '/*')) do
|
||||
-- ... and add them to the projects table.
|
||||
table.insert(projects, dir)
|
||||
end
|
||||
|
||||
return projects
|
||||
end
|
||||
|
||||
(This all assumes that you like to keep your projects grouped together in a
|
||||
folder, if not… well you’ve got Lua at your fingertips to implement whatever
|
||||
you want!)
|
||||
|
||||
Now launch the project picker, and what do you see? All those projects staring
|
||||
back at thee.
|
||||
|
||||
One thing left to do, let’s add the functionality that opens your project in a
|
||||
new WezTerm workspace. Still in projects.lua let’s change up choose_project:
|
||||
|
||||
function module.choose_project()
|
||||
local choices = {}
|
||||
for _, value in ipairs(project_dirs()) do
|
||||
table.insert(choices, { label = value })
|
||||
end
|
||||
|
||||
return wezterm.action.InputSelector {
|
||||
title = "Projects",
|
||||
choices = choices,
|
||||
fuzzy = true,
|
||||
action = wezterm.action_callback(function(child_window, child_pane, id, label)
|
||||
-- "label" may be empty if nothing was selected. Don't bother doing anything
|
||||
-- when that happens.
|
||||
if not label then return end
|
||||
|
||||
-- The SwitchToWorkspace action will switch us to a workspace if it already exists,
|
||||
-- otherwise it will create it for us.
|
||||
child_window:perform_action(wezterm.action.SwitchToWorkspace {
|
||||
-- We'll give our new workspace a nice name, like the last path segment
|
||||
-- of the directory we're opening up.
|
||||
name = label:match("([^/]+)$"),
|
||||
-- Here's the meat. We'll spawn a new terminal with the current working
|
||||
-- directory set to the directory that was picked.
|
||||
spawn = { cwd = label },
|
||||
}, child_pane)
|
||||
end),
|
||||
}
|
||||
end
|
||||
|
||||
Try that out, select a new project, and you’ll see a workspace get created for
|
||||
it. Switch back to your default workspace (we bound so LEADER, CTRL + F to show
|
||||
you a list of active workspaces) and you’ll see everything is right where you
|
||||
left it.
|
||||
|
||||
Bonus: improving the powerline, and more colour stuff
|
||||
|
||||
Let’s add a couple of polishing touches to this workflow and then I promise
|
||||
we’ll be done…
|
||||
|
||||
Remember that sad powerline we set up earlier? Let’s make it happier by adding
|
||||
another segment to it which contains the name of the current workspace. In true
|
||||
powerline fashion, each subsequent segment on the powerline will display in a
|
||||
different colour. We’ll explore some of WezTerm’s colour maths support and do
|
||||
this all dynamically based on our theme. Back in wezterm.lua:
|
||||
|
||||
-- Replace the old wezterm.on('update-status', ... function with this:
|
||||
|
||||
local function segments_for_right_status(window)
|
||||
return {
|
||||
window:active_workspace(),
|
||||
wezterm.strftime('%a %b %-d %H:%M'),
|
||||
wezterm.hostname(),
|
||||
}
|
||||
end
|
||||
|
||||
wezterm.on('update-status', function(window, _)
|
||||
local SOLID_LEFT_ARROW = utf8.char(0xe0b2)
|
||||
local segments = segments_for_right_status(window)
|
||||
|
||||
local color_scheme = window:effective_config().resolved_palette
|
||||
-- Note the use of wezterm.color.parse here, this returns
|
||||
-- a Color object, which comes with functionality for lightening
|
||||
-- or darkening the colour (amongst other things).
|
||||
local bg = wezterm.color.parse(color_scheme.background)
|
||||
local fg = color_scheme.foreground
|
||||
|
||||
-- Each powerline segment is going to be coloured progressively
|
||||
-- darker/lighter depending on whether we're on a dark/light colour
|
||||
-- scheme. Let's establish the "from" and "to" bounds of our gradient.
|
||||
local gradient_to, gradient_from = bg
|
||||
if appearance.is_dark() then
|
||||
gradient_from = gradient_to:lighten(0.2)
|
||||
else
|
||||
gradient_from = gradient_to:darken(0.2)
|
||||
end
|
||||
|
||||
-- Yes, WezTerm supports creating gradients, because why not?! Although
|
||||
-- they'd usually be used for setting high fidelity gradients on your terminal's
|
||||
-- background, we'll use them here to give us a sample of the powerline segment
|
||||
-- colours we need.
|
||||
local gradient = wezterm.color.gradient(
|
||||
{
|
||||
orientation = 'Horizontal',
|
||||
colors = { gradient_from, gradient_to },
|
||||
},
|
||||
#segments -- only gives us as many colours as we have segments.
|
||||
)
|
||||
|
||||
-- We'll build up the elements to send to wezterm.format in this table.
|
||||
local elements = {}
|
||||
|
||||
for i, seg in ipairs(segments) do
|
||||
local is_first = i == 1
|
||||
|
||||
if is_first then
|
||||
table.insert(elements, { Background = { Color = 'none' } })
|
||||
end
|
||||
table.insert(elements, { Foreground = { Color = gradient[i] } })
|
||||
table.insert(elements, { Text = SOLID_LEFT_ARROW })
|
||||
|
||||
table.insert(elements, { Foreground = { Color = fg } })
|
||||
table.insert(elements, { Background = { Color = gradient[i] } })
|
||||
table.insert(elements, { Text = ' ' .. seg .. ' ' })
|
||||
end
|
||||
|
||||
window:set_right_status(wezterm.format(elements))
|
||||
end)
|
||||
|
||||
screenshot of WezTerm with an enhanced status line, showing multiple segments
|
||||
in different colours
|
||||
|
||||
WezTerm delivers yet again. This updated callback supports arbitrary numbers of
|
||||
segments for its powerline. We’ve specified 3 but you could add way more. All
|
||||
this without needing to manually configure what colour we want on each segment,
|
||||
but rather have WezTerm do it for us by creating a gradient based on the
|
||||
currently active theme. Some highlights:
|
||||
|
||||
• We use wezterm.color.parse to convert a string containing a hex colour code
|
||||
into a Color object ([28]docs) - this lets us perform more advanced
|
||||
operations on the color.
|
||||
• The colour scheme’s background colour is still what we want to use as the
|
||||
value that our gradient draws to, but to figure out where the gradient
|
||||
should start, we use either color:darken ([29]docs) or color:lighten to
|
||||
create a new colour.
|
||||
• The gradient itself is made with wezterm.color.gradient ([30]docs), which
|
||||
returns a table containing a evenly spaced colours between our gradient_to
|
||||
and gradient_from.
|
||||
• We then iterate over our powerline segments to create the items required
|
||||
for wezterm.format.
|
||||
|
||||
Where to from here?
|
||||
|
||||
There’s a [31]lot more that WezTerm does and that [32]you can do with WezTerm.
|
||||
By now you’ll have a good understanding of WezTerm config fundamentals, but I
|
||||
encourage you to keep exploring!
|
||||
|
||||
If you’ve followed this guide step by step, I’d recommend pruning the config
|
||||
down to things that you’ll actually use, rewriting it in your own style, then
|
||||
start sprinkling in your own stuff. Take ownership of this thing! Make your own
|
||||
beautiful WezTerm snowflake!
|
||||
|
||||
When you want some inspiration for what you could do next, browse through the
|
||||
[33]WezTerm API docs to see what’s possible.
|
||||
|
||||
And if you find that you too really like WezTerm, please consider [34]
|
||||
supporting Wez for his great open-source work.
|
||||
|
||||
Receive an email when I post
|
||||
|
||||
[35][ ] [36][Subscribe]
|
||||
(You'll get no more than one email per post. I manage this list with [37]
|
||||
Buttondown).
|
||||
Want to get in touch? [38]Send me an email, [39]a tweet, or [40]check out my
|
||||
GitHub.
|
||||
How about an email when I next post something? [41]Subscribe to my newsletter.
|
||||
|
||||
This website is [42]open source, and built using [43]Jekyll. Photos are © Alex
|
||||
Plescan (2024).
|
||||
|
||||
|
||||
References:
|
||||
|
||||
[1] https://alexplescan.com/
|
||||
[2] https://alexplescan.com/posts/
|
||||
[3] https://alexplescan.com/projects/
|
||||
[4] https://buttondown.email/alexplescan
|
||||
[5] https://alexplescan.com/posts/2024/08/10/wezterm/
|
||||
[6] https://blog.lambo.land/
|
||||
[7] https://wezfurlong.org/wezterm/
|
||||
[8] https://wezfurlong.org/wezterm/config/lua/general.html
|
||||
[9] https://wezfurlong.org/wezterm/features.html
|
||||
[10] https://gist.github.com/alexpls/83d7af23426c8928402d6d79e72f9401
|
||||
[11] https://wezfurlong.org/wezterm/installation.html
|
||||
[12] https://www.lua.org/start.html
|
||||
[13] https://wezfurlong.org/wezterm/config/files.html#configuration-files
|
||||
[14] https://wezfurlong.org/wezterm/troubleshooting.html#debug-overlay
|
||||
[15] https://wezfurlong.org/wezterm/config/lua/wezterm/home_dir.html
|
||||
[16] https://wezfurlong.org/wezterm/config/appearance.html
|
||||
[17] https://wezfurlong.org/wezterm/config/fonts.html
|
||||
[18] https://wezfurlong.org/wezterm/config/font-shaping.html
|
||||
[19] https://wezfurlong.org/wezterm/config/lua/wezterm/format.html
|
||||
[20] https://wezfurlong.org/wezterm/config/lua/wezterm/hostname.html
|
||||
[21] https://wezfurlong.org/wezterm/config/default-keys.html
|
||||
[22] https://wezfurlong.org/wezterm/config/lua/SpawnCommand.html
|
||||
[23] https://wezfurlong.org/wezterm/config/lua/keyassignment/index.html
|
||||
[24] https://wezfurlong.org/wezterm/config/keys.html#leader-key
|
||||
[25] https://wezfurlong.org/wezterm/config/key-tables.html
|
||||
[26] https://github.com/mrjones2014/smart-splits.nvim
|
||||
[27] https://wezfurlong.org/wezterm/recipes/workspaces.html
|
||||
[28] https://wezfurlong.org/wezterm/config/lua/color/index.html
|
||||
[29] https://wezfurlong.org/wezterm/config/lua/color/darken.html
|
||||
[30] https://wezfurlong.org/wezterm/config/lua/wezterm.color/gradient.html
|
||||
[31] https://wezfurlong.org/wezterm/features.html
|
||||
[32] https://wezfurlong.org/wezterm/config/lua/general.html
|
||||
[33] https://wezfurlong.org/wezterm/config/lua/general.html
|
||||
[34] https://wezfurlong.org/sponsor/
|
||||
[37] https://buttondown.email/
|
||||
[38] https://alexplescan.com/cdn-cgi/l/email-protection#b0d1dcd5c8f0d1dcd5c8c0dcd5c3d3d1de9ed3dfdd
|
||||
[39] https://twitter.com/alexplescan
|
||||
[40] https://github.com/alexpls
|
||||
[41] https://buttondown.email/alexplescan
|
||||
[42] https://github.com/alexpls/alexplescan.com
|
||||
[43] https://jekyllrb.com/
|
||||
Reference in New Issue
Block a user