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/