davideisinger.com

My personal website
Log | Files | Refs | README

alexplescan-com-ffcy9q.txt (38564B)


      1 [1]Alex Plescan
      2 
      3 [2]Blog [3]Projects [4]Newsletter
      4 
      5 Okay, I really like WezTerm
      6 
      7 10 August 2024 | [5]Permalink
      8 
      9 A while back [6]my friend recommended that I try [7]WezTerm. I’d been an iTerm
     10 2 stalwart for the better part of a decade, but not to be too narrow-minded I
     11 conceded, started it up, and saw this:
     12 
     13 screenshot of WezTerm's default look
     14 
     15 Does the job, sure, but doesn’t feel quite right. Okay then, experiment over.
     16 Back to iTerm…
     17 
     18 Fast forward a couple of months and I got the itch to try a new terminal again.
     19 I wanted to use one whose config was entirely text based so I could pop it in
     20 to my dotfiles and share it across my work and personal machines. A few
     21 terminals already do this, but whispers of WezTerm’s powerful API and Lua
     22 config got me particularly interested.
     23 
     24 I tried it again with a bit more patience and I’m glad I did. My terminal is
     25 prettier than it’s ever been, more functional, and I can finally justify my
     26 mechanical keyboard purchase with all the keybindings I’ve configured.
     27 
     28 This post is an introduction to configuring WezTerm based on the setup that I
     29 eventually landed on. I’d consider it relatively low-frills. Most of what I
     30 talk about here can already be found in WezTerm’s [8]docs, but as they’ve got a
     31 large surface area, I’m hoping this post will be a useful jumping off point for
     32 WezTerm beginners.
     33 
     34 We won’t be looking at some of WezTerm’s key features, like custom hyperlinks
     35 highlighting rules, searchable scrollback, quick copy mode, and image support
     36 (you can find [9]more details here).
     37 
     38 The feature I find most exciting about WezTerm is the flexibility of its Lua
     39 config, so we’ll be focusing on that. This includes configuring appearance,
     40 keybindings, multiplexing, workspace navigation, status bar setup, and dynamic
     41 theming. By the end of it all, we’ll have a terminal that looks like this:
     42 
     43 screenshot of the WezTerm look we'll end up with at the end of this post
     44 
     45 Subtly prettier than the default, and with some great features to boot.
     46 
     47 I use macOS, so what follows is focused on ergonomics that make WezTerm great
     48 there. I haven’t tested my config on other systems, but I’m not doing anything
     49 too bespoke so things should be portable (WezTerm works pretty much
     50 everywhere).
     51 
     52 tl;dr? Here’s [10]a gist containing the config we’ll end up with.
     53 
     54 Pre-flight checks
     55 
     56 Start by installing WezTerm. Instructions for this are on [11]WezTerm’s site.
     57 If you’re on macOS and reading this you probably have Homebrew installed, so $
     58 brew install wezterm will do the trick.
     59 
     60 Now launch WezTerm, and you’re already winning.
     61 
     62 A note on Lua
     63 
     64 My favourite WezTerm feature is its use of Lua for defining config. Unlike
     65 terminals where your settings are adjusted via the UI (iTerm 2), your WezTerm
     66 config lives in your dotfiles and is portable across all your machines.
     67 
     68 And unlike other terminals where your configuration is written using a data
     69 serialization format like YAML or TOML (Alacritty, kitty), with Lua you can
     70 more easily achieve complex configs by leveraging dynamic scripts.
     71 
     72 Granted, Lua is a programming language so it is trickier to learn than YAML or
     73 TOML, but it’s still remarkably simple. If you’ve used another dynamic
     74 programming language (e.g. Ruby, Python, JavaScript) - you should be able to
     75 read the Lua code in this post easily. For achieving more complex configs, I’d
     76 recommend diving deeper into the language. Its [12]Getting Started guide is a
     77 good place to… get started.
     78 
     79 Config files, and the best feedback loop in town
     80 
     81 WezTerm supports loading in its config from all the usual places on your system
     82 ([13]docs). For this guide we’re going to be creating our config in
     83 $XDG_CONFIG_HOME/wezterm/wezterm.lua. On most systems (including macOS) this
     84 resolves to ~/.config/wezterm/wezterm.lua. Using a directory to store our
     85 config instead of dumping it in ~/.wezterm.lua will let us keep our config
     86 logically grouped as we split some of it out into different files.
     87 
     88 Create the wezterm.lua file on that path, and add this boilerplate to it:
     89 
     90 -- Import the wezterm module
     91 local wezterm = require 'wezterm'
     92 -- Creates a config object which we will be adding our config to
     93 local config = wezterm.config_builder()
     94 
     95 -- (This is where our config will go)
     96 
     97 -- Returns our config to be evaluated. We must always do this at the bottom of this file
     98 return config
     99 
    100 Save the file and all going well… nothing will happen. Well, at least nothing
    101 appeared to happen, but what WezTerm did behind the scenes is quite magical. It
    102 watched your config file, and when it changed it auto-reloaded instantly. This
    103 feature makes for a wonderfully tight feedback loop where you don’t need to
    104 restart your terminal to see the effects of your new config.
    105 
    106 We can quickly test this auto-reload by adding some invalid syntax and seeing
    107 what happens. Replace the call to wezterm.config_builder() with
    108 wezterm.config_builderZ(), save, and you should immediately see a window pop-up
    109 with:
    110 
    111 runtime error: [string "/Users/alex/.config/wezterm/wezterm.lua"]:2: attempt
    112 to call a nil value (field 'config_builderZ')
    113 stack traceback:
    114         [string "/Users/alex/.config/wezterm/wezterm.lua"]:2: in main chunk
    115 
    116 How’s that for a feedback loop? Fix the error and save the file again.
    117 
    118 This time, have your config log something:
    119 
    120 wezterm.log_info("hello world! my name is " .. wezterm.hostname())
    121 
    122 Save. Now… where did that log go? Press CTRL + SHIFT + L to bring up the debug
    123 overlay ([14]docs) and lo and behold, your beautiful log was waiting for you
    124 all along. Not only that but what you’re looking at is a full Lua REPL. Enter 1
    125 + 1 and you’ll see the result. Enter wezterm.home_dir and you’ll see the result
    126 of accessing the home_dir entry on the wezterm module ([15]docs).
    127 
    128 screenshot of the WezTerm's debug overlay
    129 
    130 The combination of hot reloading and the debug overlay makes experimenting with
    131 WezTerm configs extremely low friction and low consequence. The feedback loop
    132 is so tight now it’s more like a feedback lp.
    133 
    134 Configuring appearance
    135 
    136 Okay enough gushing - let’s cut to the chase and make this thing prettier. Add
    137 a few lines to the config to start customising the look of the terminal. We’ll
    138 start with a colour scheme ([16]docs):
    139 
    140 -- Pick a colour scheme. WezTerm ships with more than 1,000!
    141 -- Find them here: https://wezfurlong.org/wezterm/colorschemes/index.html
    142 config.color_scheme = 'Tokyo Night'
    143 
    144 Save, and you should immediately see it update. Thanks Wez!
    145 
    146 screenshot of applying a colour scheme to WezTerm
    147 
    148 (if the hot config reload doesn’t work for whatever reason, you can manually
    149 reload it by pressing CMD + R).
    150 
    151 Many colours, all at once
    152 
    153 With over 1,000 colour choices to choose from, it’s tough to decide on your
    154 favourite. Why not outsource that work to your computer? Let’s explore the
    155 power of WezTerm’s dynamic config by randomly assigning a colour scheme for
    156 each new window you open:
    157 
    158 -- Creates a lua table containing the name of every color scheme WezTerm
    159 -- ships with.
    160 local scheme_names = {}
    161 for name, scheme in pairs(wezterm.color.get_builtin_schemes()) do
    162   table.insert(scheme_names, name)
    163 end
    164 
    165 -- When the config for a window is reloaded (i.e. when you save this file
    166 -- or open a new window)...
    167 wezterm.on('window-config-reloaded', function(window, pane)
    168   -- Don't proceed if the config has already been overriden, otherwise
    169   -- we'll enter an infinite loop of neverending colour scheme changes.
    170   -- If that sounds like your kinda thing, then remove this line ;) - but
    171   -- don't say you haven't been warned.
    172   if window:get_config_overrides() then return end
    173   -- Pick a random colour scheme name.
    174   local scheme = scheme_names[math.random(#scheme_names)]
    175   -- Assign it as an override for this window.
    176   window:set_config_overrides { color_scheme = scheme }
    177   -- And log it for good measure
    178   wezterm.log_info("Your colour scheme is now: " .. scheme)
    179 end)
    180 
    181 Open up a few windows (CMD + N on macOS) and each one will have a different
    182 colour scheme. A cornucopia of terminals, each more surprising than the last.
    183 We, my friends, are truly innovating now.
    184 
    185 screenshot of many WezTerm terminal windows, each with a distinctive colour
    186 scheme
    187 
    188 But really, that was kind of a dumb idea meant to prove a point. Now that
    189 you’ve gotten a taste for dynamic config, you probably wanna remove those lines
    190 and stick to a colour scheme you do like.
    191 
    192 (You may find that after you remove that code and add your static color_scheme
    193 config back in, it doesn’t hot reload. That’s because our script set an
    194 override on the config specific to each window. To clear your overrides, you
    195 can go to your debug terminal and type window:set_config_overrides({}) - or you
    196 can just close and reopen your WezTerm window).
    197 
    198 Respecting the system’s appearance
    199 
    200 Light themes, dark themes… why not both? Let’s have the terminal’s colour
    201 scheme automatically change when the operating system’s appearance changes.
    202 While we’re at it, we’ll learn how to split up WezTerm config into different
    203 modules.
    204 
    205 Create a new file alongside wezterm.lua and call it appearance.lua. Add this to
    206 it:
    207 
    208 -- We almost always start by importing the wezterm module
    209 local wezterm = require 'wezterm'
    210 -- Define a lua table to hold _our_ module's functions
    211 local module = {}
    212 
    213 -- Returns a bool based on whether the host operating system's
    214 -- appearance is light or dark.
    215 function module.is_dark()
    216   -- wezterm.gui is not always available, depending on what
    217   -- environment wezterm is operating in. Just return true
    218   -- if it's not defined.
    219   if wezterm.gui then
    220     -- Some systems report appearance like "Dark High Contrast"
    221     -- so let's just look for the string "Dark" and if we find
    222     -- it assume appearance is dark.
    223     return wezterm.gui.get_appearance():find("Dark")
    224   end
    225   return true
    226 end
    227 
    228 return module
    229 
    230 Back in wezterm.lua:
    231 
    232 -- Import our new module (put this near the top of your wezterm.lua)
    233 local appearance = require 'appearance'
    234 
    235 -- Use it!
    236 if appearance.is_dark() then
    237   config.color_scheme = 'Tokyo Night'
    238 else
    239   config.color_scheme = 'Tokyo Night Day'
    240 end
    241 
    242 Toggle your system appearance between dark mode and light mode, and watch your
    243 theme change right before your eyes.
    244 
    245 screenshot of WezTerm in light and dark mode
    246 
    247 Fonts
    248 
    249 Next up let’s look at fonts. WezTerm ships with the lovely JetBrains Mono, and
    250 Nerd Font Symbols ([17]docs) so there’s nothing to complain about there. I do
    251 prefer Berkeley Mono at 13 points though, so:
    252 
    253 -- Choose your favourite font, make sure it's installed on your machine
    254 config.font = wezterm.font({ family = 'Berkeley Mono' })
    255 -- And a font size that won't have you squinting
    256 config.font_size = 13
    257 
    258 There’s good support for ligatures and other fancy font settings if you’re into
    259 that ([18]docs), but I’m not so let’s move on.
    260 
    261 Window styling
    262 
    263 Let’s style our terminal’s window. This controls the chrome that appears around
    264 it, and can vary between operating systems. On macOS, I like the below:
    265 
    266 -- Slightly transparent and blurred background
    267 config.window_background_opacity = 0.9
    268 config.macos_window_background_blur = 30
    269 -- Removes the title bar, leaving only the tab bar. Keeps
    270 -- the ability to resize by dragging the window's edges.
    271 -- On macOS, 'RESIZE|INTEGRATED_BUTTONS' also looks nice if
    272 -- you want to keep the window controls visible and integrate
    273 -- them into the tab bar.
    274 config.window_decorations = 'RESIZE'
    275 -- Sets the font for the window frame (tab bar)
    276 config.window_frame = {
    277   -- Berkeley Mono for me again, though an idea could be to try a
    278   -- serif font here instead of monospace for a nicer look?
    279   font = wezterm.font({ family = 'Berkeley Mono', weight = 'Bold' }),
    280   font_size = 11,
    281 }
    282 
    283 screenshot of WezTerm after we've styled its window
    284 
    285 Now, let’s do something a little kitsch. See that empty space to the right of
    286 our terminal’s tab bar? Let’s fill it with a powerline looking status bar.
    287 We’ll add an update-status callback:
    288 
    289 wezterm.on('update-status', function(window)
    290   -- Grab the utf8 character for the "powerline" left facing
    291   -- solid arrow.
    292   local SOLID_LEFT_ARROW = utf8.char(0xe0b2)
    293 
    294   -- Grab the current window's configuration, and from it the
    295   -- palette (this is the combination of your chosen colour scheme
    296   -- including any overrides).
    297   local color_scheme = window:effective_config().resolved_palette
    298   local bg = color_scheme.background
    299   local fg = color_scheme.foreground
    300 
    301   window:set_right_status(wezterm.format({
    302     -- First, we draw the arrow...
    303     { Background = { Color = 'none' } },
    304     { Foreground = { Color = bg } },
    305     { Text = SOLID_LEFT_ARROW },
    306     -- Then we draw our text
    307     { Background = { Color = bg } },
    308     { Foreground = { Color = fg } },
    309     { Text = ' ' .. wezterm.hostname() .. ' ' },
    310   }))
    311 end)
    312 
    313 screenshot of WezTerm with a right status bar showing the system's hostname
    314 
    315 A few interesting things happening here:
    316 
    317  1. We just used WezTerm’s events API with wezterm.on. Events are things that
    318     happen to the terminal (e.g. window resize) that we can define callbacks
    319     for. The update-status event is emitted periodically when the terminal is
    320     ready to have its status updated. WezTerm manages this cleverly to ensure
    321     that only one such update can run at any given time, and if your code takes
    322     too long to execute, a timeout will be hit and your handler will be
    323     abandoned… protecting your terminal from bogging down.
    324  2. We’re grabbing the effective_config() of the window to get the “effective”
    325     configuration, which is the config with any overrides applied. From this we
    326     can get the resolved_palette, which is the currently active colour scheme.
    327     To see what this data looks like you can enter the debug overlay (CTRL +
    328     SHIFT + L) and execute window:effective_config().resolved_palette.
    329  3. We’re using the wezterm.format function ([19]docs) to style our string with
    330     colours. Other ways you could format text include setting font weight,
    331     underlining text, and more.
    332  4. Finally, the wezterm.hostname() function ([20]docs) gives us the hostname
    333     of the machine we’re running on. WezTerm ships with a bunch of useful
    334     functions for getting the state of your system, and also… we’re doing stuff
    335     in Lua - so you have full access to your file system, are able to make
    336     network requests, etc.
    337 
    338 Altogether this gives us a powerline…ish. It’s a bit sad with only one segment
    339 isn’t it? Don’t you worry, we’ll be adding more soon…
    340 
    341 Keys
    342 
    343 Here’s the part where we justify our mechanical keyboard purchases. Let’s set
    344 up some key assignments. During this section we’ll look at WezTerm’s deep key
    345 handling capabilities and ability to take action based on your input.
    346 
    347 By default, WezTerm defines some standard key assignments ([21]docs). I leave
    348 them on because they’re very sensible, but if you wanna really wrest total
    349 control of your config, you can turn them off with
    350 config.disable_default_key_bindings = true.
    351 
    352 Our first key assignment will be a humble start for us macOS users… you might
    353 be used to Option + Left Arrow and Option + Right Arrow jumping between words
    354 on your terminal. That’s the default in iTerm 2 and Terminal.app, but not in
    355 WezTerm. However, we can map it!
    356 
    357 We do this by adding a keys table to our config:
    358 
    359 -- Table mapping keypresses to actions
    360 config.keys = {
    361   -- Sends ESC + b and ESC + f sequence, which is used
    362   -- for telling your shell to jump back/forward.
    363   {
    364     -- When the left arrow is pressed
    365     key = 'LeftArrow',
    366     -- With the "Option" key modifier held down
    367     mods = 'OPT',
    368     -- Perform this action, in this case - sending ESC + B
    369     -- to the terminal
    370     action = wezterm.action.SendString '\x1bb',
    371   },
    372   {
    373     key = 'RightArrow',
    374     mods = 'OPT',
    375     action = wezterm.action.SendString '\x1bf',
    376   },
    377 }
    378 
    379 By now you’ve probably figured out that you’re gonna be spending more time
    380 configuring WezTerm than doing actual work. There’s no shame in admitting this
    381 reality, so let’s encode it into our config. On macOS, the default shortcut for
    382 opening an application’s preferences is CMD + , - let’s make it so when we
    383 press this, our favourite editor opens up the WezTerm config. I’m using neovim,
    384 but feel free to substitute with your own:
    385 
    386 config.keys = {
    387   -- ... add these new entries to your config.keys table
    388   {
    389     key = ',',
    390     mods = 'SUPER',
    391     action = wezterm.action.SpawnCommandInNewTab {
    392       cwd = wezterm.home_dir,
    393       args = { 'nvim', wezterm.config_file },
    394     },
    395   },
    396 }
    397 
    398 Try that out, but you may see an error along the lines of:
    399 
    400 Unable to spawn nvim because:
    401 No viable candidates found in PATH "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
    402 
    403 If that error showed up, it’s typically because the process that launched
    404 WezTerm didn’t include a PATH environment variable that led to your editor’s
    405 binary (e.g. on macOS, Finder is usually WezTerm’s parent). We can work around
    406 this by specifying the full path to your editor in the SpawnCommandInNewTab
    407 properties ([22]docs), or by updating the default environment variables WezTerm
    408 spawns commands with. I prefer the latter, since it means that any other places
    409 in our config where we might spawn new commands will also inherit the same env
    410 vars:
    411 
    412 config.set_environment_variables = {
    413   PATH = '/opt/homebrew/bin:' .. os.getenv('PATH')
    414 }
    415 
    416 Try that again, and it should work.
    417 
    418 We really are just scratching the surface of all the commands available ([23]
    419 WezTerm supports a lot). In the next section, we’ll be growing our key bindings
    420 further.
    421 
    422 Multiplexing terminals, levelling up key assignments
    423 
    424 Let’s move on to WezTerm’s multiplexing capabilities. If you make use of a
    425 multiplexer (i.e. tmux) then you may consider using WezTerm’s builtin
    426 capabilities instead. They’ll generally give you a more integrated experience,
    427 with individual scrollback buffers per pane, better mouse control, easier
    428 selection functionality, and generally faster performance.
    429 
    430 Hit CTRL + SHIFT + P to bring up WezTerm’s command palette. (Yes, WezTerm has a
    431 command palette. Yes, it’s as customisable as everything else we’ve seen so
    432 far. No, we won’t dwell on it here). Type split horizontally until the “Shell:
    433 Split Horizontally” option is selected and hit ENTER. Ta-da! Your shell split
    434 horizontally! Do the same for split vertically and… you get the idea.
    435 
    436 screenshot of WezTerm's command palette
    437 
    438 You may have noticed that the command palette displays the keyboard shortcut
    439 assigned to each action. The ones for splitting are quite a fingerful, e.g.
    440 SHIFT + CTRL + OPTION + ". I get why they’re this complicated - because they’re
    441 trying not to clash with any other shortcuts you may have on your system, but
    442 we can do a lot better - and WezTerm gives us the tools do so easily!
    443 
    444 Splitting panes, leader key
    445 
    446 A leader key ([24]docs) is a special key combination that you press first,
    447 followed by another key combination, to perform a specific action. It can help
    448 you create complex shortcuts without needing to push a lot of keys all at once.
    449 
    450 Sounds like a perfect fit for splitting panes, right? We’ll bind our leader to
    451 CTRL + A, and in case you accidentally type the leader without following it up
    452 with another key, we’ll have it automatically deactivate after 1,000
    453 milliseconds.
    454 
    455 -- If you're using emacs you probably wanna choose a different leader here,
    456 -- since we're gonna be making it a bit harder to CTRL + A for jumping to 
    457 -- the start of a line
    458 config.leader = { key = 'a', mods = 'CTRL', timeout_milliseconds = 1000 }
    459 
    460 Next let’s define some key assignments for splitting panes:
    461 
    462 config.keys = {
    463   -- ... add these new entries to your config.keys table
    464   {
    465     -- I'm used to tmux bindings, so am using the quotes (") key to
    466     -- split horizontally, and the percent (%) key to split vertically.
    467     key = '"',
    468     -- Note that instead of a key modifier mapped to a key on your keyboard
    469     -- like CTRL or ALT, we can use the LEADER modifier instead.
    470     -- This means that this binding will be invoked when you press the leader
    471     -- (CTRL + A), quickly followed by quotes (").
    472     mods = 'LEADER',
    473     action = wezterm.action.SplitHorizontal { domain = 'CurrentPaneDomain' },
    474   },
    475   {
    476     key = '%',
    477     mods = 'LEADER',
    478     action = wezterm.action.SplitVertical { domain = 'CurrentPaneDomain' },
    479   },
    480 }
    481 
    482 Give it a go now. Press CTRL + A, quickly followed by ", and you’ll get a
    483 horizontal split. Use the other assignment and you’ll get a vertical split.
    484 
    485 screenshot of WezTerm's with split panes
    486 
    487 Before we move on - you might be wondering what happens if you actually want to
    488 send the CTRL + A keypress without invoking the leader? CTRL + A is useful in
    489 and of its own as pressing it jumps to the start of a line on your shell (and
    490 on operating systems like Emacs).
    491 
    492 Well there’s a solution for that. We can map CTRL + A quickly followed by CTRL
    493 + A to send a CTRL + A to our terminal. That’s a confusing sentence! It’ll be
    494 simpler to just look at the config:
    495 
    496 config.keys = {
    497   -- ... add these new entries to your config.keys table
    498   {
    499     key = 'a',
    500     -- When we're in leader mode _and_ CTRL + A is pressed...
    501     mods = 'LEADER|CTRL',
    502     -- Actually send CTRL + A key to the terminal
    503     action = wezterm.action.SendKey { key = 'a', mods = 'CTRL' },
    504   },
    505 },
    506 
    507 Moving around panes
    508 
    509 Okay with that done, let’s get back to multiplexing. Next up, navigating our
    510 splits. I like to use vim direction keybindings, but feel free to replace with
    511 arrow keys instead.
    512 
    513 config.keys = {
    514   -- ... add these new entries to your config.keys table
    515   {
    516     -- I like to use vim direction keybindings, but feel free to replace
    517     -- with directional arrows instead.
    518     key = 'j', -- or DownArrow
    519     mods = 'LEADER',
    520     action = wezterm.action.ActivatePaneDirection('Down'),
    521   },
    522   {
    523     key = 'k', -- or UpArrow
    524     mods = 'LEADER',
    525     action = wezterm.action.ActivatePaneDirection('Up'),
    526   },
    527   {
    528     key = 'h', -- or LeftArrow
    529     mods = 'LEADER',
    530     action = wezterm.action.ActivatePaneDirection('Left'),
    531   },
    532   {
    533     key = 'l', -- or RightArrow
    534     mods = 'LEADER',
    535     action = wezterm.action.ActivatePaneDirection('Right'),
    536   },
    537 }
    538 
    539 Look at all that duplication - We’re using a dynamic language for our config
    540 here, we don’t need to stand for that! Let’s go on a little side quest and see
    541 if we can extract it to a function.
    542 
    543 local function move_pane(key, direction)
    544   return {
    545     key = key,
    546     mods = 'LEADER',
    547     action = wezterm.action.ActivatePaneDirection(direction),
    548   }
    549 end
    550 
    551 config.keys = {
    552   -- ... remove the previous move bindings, and replace with
    553   move_pane('j', 'Down'),
    554   move_pane('k', 'Up'),
    555   move_pane('h', 'Left'),
    556   move_pane('l', 'Right'),
    557 }
    558 
    559 Ooh so much smaller, but it could be smaller still. I dare you to keep code
    560 golfing this down to 6 lines. Go on - I believe in you!
    561 
    562 Resizing panes, and introducing key tables
    563 
    564 You might’ve figured out that you can resize panes by dragging the edge of one
    565 with your mouse, but we’re developers here, not olympic athletes. What’re we
    566 expected to move our hands away from the safety of our keyboard and over to the
    567 mouse?! No! I won’t stand for it and neither should you!
    568 
    569 It’d be really nice to use the same keys that we use for moving between the
    570 panes for resizing (h, j, k, l)… but they’ve already been mapped… we could add
    571 another key modifier that needs to be held down when we want to resize vs. move
    572 between the panes:
    573 
    574 config.keys = {
    575   -- ... add this new entry to your config.keys table
    576   {
    577     key = 'h',
    578     mods = 'LEADER|CTRL',
    579     -- "3" here is the amount of cells we wish to resize
    580     -- the terminal by
    581     action = wezterm.action.AdjustPaneSize { 'Left', 3 },
    582   },
    583 }
    584 
    585 But that’s no good really. We have to first push our leader CTRL + A, then push
    586 CTRL + H, and keep repeating that each time we wanna resize the pane to the
    587 left. Fingers getting sore. Send help. Oh, here comes WezTerm with the
    588 antidote: [25]key tables.
    589 
    590 When you activate a key table you’re entering a different mode with its own set
    591 of assignments for whatever you’re doing. This allows you to have multiple
    592 layers of assignments that are context specific.
    593 
    594 It’s a similar kind of concept to the leader key, but unlike it, our key table
    595 will not automatically deactivate after an action is invoked, so it’ll be a
    596 good fit for resizing, where we want to keep pressing the same button over and
    597 over again until we’re happy with our pane’s new size.
    598 
    599 With all that… this is easier done that said, so let’s check out the code:
    600 
    601 local function resize_pane(key, direction)
    602   return {
    603     key = key,
    604     action = wezterm.action.AdjustPaneSize { direction, 3 }
    605   }
    606 end
    607 
    608 config.keys = {
    609   -- ... remove the yucky keybinding from above and replace it with this
    610   {
    611     -- When we push LEADER + R...
    612     key = 'r',
    613     mods = 'LEADER',
    614     -- Activate the `resize_panes` keytable
    615     action = wezterm.action.ActivateKeyTable {
    616       name = 'resize_panes',
    617       -- Ensures the keytable stays active after it handles its
    618       -- first keypress.
    619       one_shot = false,
    620       -- Deactivate the keytable after a timeout.
    621       timeout_milliseconds = 1000,
    622     }
    623   },
    624 }
    625 
    626 config.key_tables = {
    627   resize_panes = {
    628     resize_pane('j', 'Down'),
    629     resize_pane('k', 'Up'),
    630     resize_pane('h', 'Left'),
    631     resize_pane('l', 'Right'),
    632   },
    633 }
    634 
    635 Now you can push CTRL + A to activate leader, then R to activate the resizing
    636 layer… and movement keys to resize to your heart’s content. When 1,000
    637 milliseconds have elapsed, you’ll automatically exit the resizing layer and be
    638 back to the default keytable.
    639 
    640 WezTerm intensifies…
    641 
    642 (While we’re on multiplexing, if you’re using neovim, I’d recommend checking
    643 out [26]smart-splits.nvim - that’ll let you jump between your vim panes and
    644 your WezTerm ones).
    645 
    646 Project workspaces
    647 
    648 Okay let’s graduate from WezTerm university with one final assignment… project
    649 workspaces.
    650 
    651 I’m often working across a few different projects at a time, and need to be
    652 able to quickly switch between them. I want each project to maintain its own
    653 multiplexer instance with its own windows, panes, and tabs. In tmux you might
    654 achieve this with different sessions. In WezTerm we’ll do it with [27]
    655 workspaces.
    656 
    657 Creating and switching between workspaces
    658 
    659 Create a new file in your config directory and call it projects.lua. We’ll use
    660 this to provide some project switching functions to our main config file.
    661 
    662 local wezterm = require 'wezterm'
    663 local module = {}
    664 
    665 local function project_dirs()
    666   return {
    667     '~/Projects/mailgrip',
    668     '~/Projects/alexplescan.com',
    669     '~/Projects/wezterm_love_letters',
    670     -- ... keep going, list all your projects
    671     -- (or don't if you value your time. we'll improve on this soon)
    672   }
    673 end
    674 
    675 function module.choose_project()
    676   local choices = {}
    677   for _, value in ipairs(project_dirs()) do
    678     table.insert(choices, { label = value })
    679   end
    680 
    681   -- The InputSelector action presents a modal UI for choosing between a set of options
    682   -- within WezTerm.
    683   return wezterm.action.InputSelector {
    684     title = 'Projects',
    685     -- The options we wish to choose from
    686     choices = choices,
    687     -- Yes, we wanna fuzzy search (so typing "alex" will filter down to
    688     -- "~/Projects/alexplescan.com")
    689     fuzzy = true,
    690     -- The action we want to perform. Note that this doesn't have to be a
    691     -- static definition as we've done before, but can be a callback that
    692     -- evaluates any arbitrary code.
    693     action = wezterm.action_callback(function(child_window, child_pane, id, label)
    694       -- As a placeholder, we'll log the name of what you picked
    695       wezterm.log_info("you chose " .. label)
    696     end),
    697   }
    698 end
    699 
    700 return module
    701 
    702 … and in your wezterm.lua:
    703 
    704 local projects = require 'projects'
    705 
    706 config.keys = {
    707   -- ... add these new entries to your config.keys table
    708   {
    709     key = 'p',
    710     mods = 'LEADER',
    711     -- Present in to our project picker
    712     action = projects.choose_project(),
    713   },
    714   {
    715     key = 'f',
    716     mods = 'LEADER',
    717     -- Present a list of existing workspaces
    718     action = wezterm.action.ShowLauncherArgs { flags = 'FUZZY|WORKSPACES' },
    719   },
    720 }
    721 
    722 Lots going on here, take your time to read it and the comments. And give it a
    723 go! Push LEADER + P, and you’ll see the project input selector come up. Pick a
    724 project by highlighting one and pushing ENTER, or push CTRL + C to close the
    725 picker. Once you’ve picked a project you’ll see its directory logged to your
    726 debug overlay (CTRL + SHIFT + L).
    727 
    728 screenshot of WezTerm's with the workspace switcher we've configured
    729 
    730 Still a couple of issues though… it’s really annoying to type out all your
    731 projects by hand in that file, and, uh, what was the other issue? Oh yeah! When
    732 you pick a project nothing happens. Okay, let’s fix these. Back in
    733 projects.lua, we’ll start by having the list of projects automatically
    734 populate.
    735 
    736 -- The directory that contains all your projects.
    737 local project_dir = wezterm.home_dir .. "/Projects"
    738 
    739 local function project_dirs()
    740   -- Start with your home directory as a project, 'cause you might want
    741   -- to jump straight to it sometimes.
    742   local projects = { wezterm.home_dir }
    743 
    744   -- WezTerm comes with a glob function! Let's use it to get a lua table
    745   -- containing all subdirectories of your project folder.
    746   for _, dir in ipairs(wezterm.glob(project_dir .. '/*')) do
    747     -- ... and add them to the projects table.
    748     table.insert(projects, dir)
    749   end
    750 
    751   return projects
    752 end
    753 
    754 (This all assumes that you like to keep your projects grouped together in a
    755 folder, if not… well you’ve got Lua at your fingertips to implement whatever
    756 you want!)
    757 
    758 Now launch the project picker, and what do you see? All those projects staring
    759 back at thee.
    760 
    761 One thing left to do, let’s add the functionality that opens your project in a
    762 new WezTerm workspace. Still in projects.lua let’s change up choose_project:
    763 
    764 function module.choose_project()
    765   local choices = {}
    766   for _, value in ipairs(project_dirs()) do
    767     table.insert(choices, { label = value })
    768   end
    769 
    770   return wezterm.action.InputSelector {
    771     title = "Projects",
    772     choices = choices,
    773     fuzzy = true,
    774     action = wezterm.action_callback(function(child_window, child_pane, id, label)
    775       -- "label" may be empty if nothing was selected. Don't bother doing anything
    776       -- when that happens.
    777       if not label then return end
    778 
    779       -- The SwitchToWorkspace action will switch us to a workspace if it already exists,
    780       -- otherwise it will create it for us.
    781       child_window:perform_action(wezterm.action.SwitchToWorkspace {
    782         -- We'll give our new workspace a nice name, like the last path segment
    783         -- of the directory we're opening up.
    784         name = label:match("([^/]+)$"),
    785         -- Here's the meat. We'll spawn a new terminal with the current working
    786         -- directory set to the directory that was picked.
    787         spawn = { cwd = label },
    788       }, child_pane)
    789     end),
    790   }
    791 end
    792 
    793 Try that out, select a new project, and you’ll see a workspace get created for
    794 it. Switch back to your default workspace (we bound so LEADER, CTRL + F to show
    795 you a list of active workspaces) and you’ll see everything is right where you
    796 left it.
    797 
    798 Bonus: improving the powerline, and more colour stuff
    799 
    800 Let’s add a couple of polishing touches to this workflow and then I promise
    801 we’ll be done…
    802 
    803 Remember that sad powerline we set up earlier? Let’s make it happier by adding
    804 another segment to it which contains the name of the current workspace. In true
    805 powerline fashion, each subsequent segment on the powerline will display in a
    806 different colour. We’ll explore some of WezTerm’s colour maths support and do
    807 this all dynamically based on our theme. Back in wezterm.lua:
    808 
    809 -- Replace the old wezterm.on('update-status', ... function with this:
    810 
    811 local function segments_for_right_status(window)
    812   return {
    813     window:active_workspace(),
    814     wezterm.strftime('%a %b %-d %H:%M'),
    815     wezterm.hostname(),
    816   }
    817 end
    818 
    819 wezterm.on('update-status', function(window, _)
    820   local SOLID_LEFT_ARROW = utf8.char(0xe0b2)
    821   local segments = segments_for_right_status(window)
    822 
    823   local color_scheme = window:effective_config().resolved_palette
    824   -- Note the use of wezterm.color.parse here, this returns
    825   -- a Color object, which comes with functionality for lightening
    826   -- or darkening the colour (amongst other things).
    827   local bg = wezterm.color.parse(color_scheme.background)
    828   local fg = color_scheme.foreground
    829 
    830   -- Each powerline segment is going to be coloured progressively
    831   -- darker/lighter depending on whether we're on a dark/light colour
    832   -- scheme. Let's establish the "from" and "to" bounds of our gradient.
    833   local gradient_to, gradient_from = bg
    834   if appearance.is_dark() then
    835     gradient_from = gradient_to:lighten(0.2)
    836   else
    837     gradient_from = gradient_to:darken(0.2)
    838   end
    839 
    840   -- Yes, WezTerm supports creating gradients, because why not?! Although
    841   -- they'd usually be used for setting high fidelity gradients on your terminal's
    842   -- background, we'll use them here to give us a sample of the powerline segment
    843   -- colours we need.
    844   local gradient = wezterm.color.gradient(
    845     {
    846       orientation = 'Horizontal',
    847       colors = { gradient_from, gradient_to },
    848     },
    849     #segments -- only gives us as many colours as we have segments.
    850   )
    851 
    852   -- We'll build up the elements to send to wezterm.format in this table.
    853   local elements = {}
    854 
    855   for i, seg in ipairs(segments) do
    856     local is_first = i == 1
    857 
    858     if is_first then
    859       table.insert(elements, { Background = { Color = 'none' } })
    860     end
    861     table.insert(elements, { Foreground = { Color = gradient[i] } })
    862     table.insert(elements, { Text = SOLID_LEFT_ARROW })
    863 
    864     table.insert(elements, { Foreground = { Color = fg } })
    865     table.insert(elements, { Background = { Color = gradient[i] } })
    866     table.insert(elements, { Text = ' ' .. seg .. ' ' })
    867   end
    868 
    869   window:set_right_status(wezterm.format(elements))
    870 end)
    871 
    872 screenshot of WezTerm with an enhanced status line, showing multiple segments
    873 in different colours
    874 
    875 WezTerm delivers yet again. This updated callback supports arbitrary numbers of
    876 segments for its powerline. We’ve specified 3 but you could add way more. All
    877 this without needing to manually configure what colour we want on each segment,
    878 but rather have WezTerm do it for us by creating a gradient based on the
    879 currently active theme. Some highlights:
    880 
    881   • We use wezterm.color.parse to convert a string containing a hex colour code
    882     into a Color object ([28]docs) - this lets us perform more advanced
    883     operations on the color.
    884   • The colour scheme’s background colour is still what we want to use as the
    885     value that our gradient draws to, but to figure out where the gradient
    886     should start, we use either color:darken ([29]docs) or color:lighten to
    887     create a new colour.
    888   • The gradient itself is made with wezterm.color.gradient ([30]docs), which
    889     returns a table containing a evenly spaced colours between our gradient_to
    890     and gradient_from.
    891   • We then iterate over our powerline segments to create the items required
    892     for wezterm.format.
    893 
    894 Where to from here?
    895 
    896 There’s a [31]lot more that WezTerm does and that [32]you can do with WezTerm.
    897 By now you’ll have a good understanding of WezTerm config fundamentals, but I
    898 encourage you to keep exploring!
    899 
    900 If you’ve followed this guide step by step, I’d recommend pruning the config
    901 down to things that you’ll actually use, rewriting it in your own style, then
    902 start sprinkling in your own stuff. Take ownership of this thing! Make your own
    903 beautiful WezTerm snowflake!
    904 
    905 When you want some inspiration for what you could do next, browse through the 
    906 [33]WezTerm API docs to see what’s possible.
    907 
    908 And if you find that you too really like WezTerm, please consider [34]
    909 supporting Wez for his great open-source work.
    910 
    911 Receive an email when I post
    912 
    913 [35][                    ] [36][Subscribe]
    914 (You'll get no more than one email per post. I manage this list with [37]
    915 Buttondown).
    916 Want to get in touch? [38]Send me an email, [39]a tweet, or [40]check out my
    917 GitHub.
    918 How about an email when I next post something? [41]Subscribe to my newsletter.
    919 
    920 This website is [42]open source, and built using [43]Jekyll. Photos are © Alex
    921 Plescan (2024).
    922 
    923 
    924 References:
    925 
    926 [1] https://alexplescan.com/
    927 [2] https://alexplescan.com/posts/
    928 [3] https://alexplescan.com/projects/
    929 [4] https://buttondown.email/alexplescan
    930 [5] https://alexplescan.com/posts/2024/08/10/wezterm/
    931 [6] https://blog.lambo.land/
    932 [7] https://wezfurlong.org/wezterm/
    933 [8] https://wezfurlong.org/wezterm/config/lua/general.html
    934 [9] https://wezfurlong.org/wezterm/features.html
    935 [10] https://gist.github.com/alexpls/83d7af23426c8928402d6d79e72f9401
    936 [11] https://wezfurlong.org/wezterm/installation.html
    937 [12] https://www.lua.org/start.html
    938 [13] https://wezfurlong.org/wezterm/config/files.html#configuration-files
    939 [14] https://wezfurlong.org/wezterm/troubleshooting.html#debug-overlay
    940 [15] https://wezfurlong.org/wezterm/config/lua/wezterm/home_dir.html
    941 [16] https://wezfurlong.org/wezterm/config/appearance.html
    942 [17] https://wezfurlong.org/wezterm/config/fonts.html
    943 [18] https://wezfurlong.org/wezterm/config/font-shaping.html
    944 [19] https://wezfurlong.org/wezterm/config/lua/wezterm/format.html
    945 [20] https://wezfurlong.org/wezterm/config/lua/wezterm/hostname.html
    946 [21] https://wezfurlong.org/wezterm/config/default-keys.html
    947 [22] https://wezfurlong.org/wezterm/config/lua/SpawnCommand.html
    948 [23] https://wezfurlong.org/wezterm/config/lua/keyassignment/index.html
    949 [24] https://wezfurlong.org/wezterm/config/keys.html#leader-key
    950 [25] https://wezfurlong.org/wezterm/config/key-tables.html
    951 [26] https://github.com/mrjones2014/smart-splits.nvim
    952 [27] https://wezfurlong.org/wezterm/recipes/workspaces.html
    953 [28] https://wezfurlong.org/wezterm/config/lua/color/index.html
    954 [29] https://wezfurlong.org/wezterm/config/lua/color/darken.html
    955 [30] https://wezfurlong.org/wezterm/config/lua/wezterm.color/gradient.html
    956 [31] https://wezfurlong.org/wezterm/features.html
    957 [32] https://wezfurlong.org/wezterm/config/lua/general.html
    958 [33] https://wezfurlong.org/wezterm/config/lua/general.html
    959 [34] https://wezfurlong.org/sponsor/
    960 [37] https://buttondown.email/
    961 [38] https://alexplescan.com/cdn-cgi/l/email-protection#b0d1dcd5c8f0d1dcd5c8c0dcd5c3d3d1de9ed3dfdd
    962 [39] https://twitter.com/alexplescan
    963 [40] https://github.com/alexpls
    964 [41] https://buttondown.email/alexplescan
    965 [42] https://github.com/alexpls/alexplescan.com
    966 [43] https://jekyllrb.com/