WezTerm projects selector

3 min read April 04, 2025 #productivity

Personally, I like WezTerm for using lua configs instead of boring json/yaml/toml files, and for the powerful and configurable multiplexer. In this post I'll show you how to create a fuzzy project selector that launches a named workspace, splits a window and executes predefined commands.

thumbnail

Multiplexer#

Let's start with the basic configuration to understand the power of multiplexing. Imagine that after starting a terminal, we want to have a preconfigured workspace with the narrow pane for spotify-player on the right, a very small pane at the bottom and the main working pane with weather.

-- ~/.config/wezterm.lua
local wezterm = require 'wezterm'

wezterm.on('gui-startup', function()
  local _, pane_main, window = wezterm.mux.spawn_window{
    workspace = 'main',
    cwd = '~/dev'
  }
  local pane_spoti = pane_main:split {
    direction = "Right",
    size = 0.12,
    cwd = '/tmp',
  }
  local pane_bottom = pane_main:split {
    direction = "Bottom",
    size = 0.05,
  }

  window:gui_window():maximize()
  pane_main:activate()
  pane_main:send_text "curl wttr.in?2\ncd "
  pane_spoti:send_text 'spotify_player\n'
  pane_bottom:send_text "date\n"
end)

We use the gui-startup event to configure WezTerm startup, in which we spawn a new window with a single pane pane_main, workspace name main and some working directory ~/dev. Next we split the pane_main to the right with a size of 0.12 and another working directory and call this pane pane_spoti. Same for pane_bottom.

The rest is simple: maximize WezTerm window, activate the main pane and execute some commands on each pane.

result

We've just configured only one workspace, but you can create as many as you like by spawning more windows with workspace = 'something'.

Projects#

This time we'll use a similar multiplexing approach, but instead of the gui-startup event, we'll trigger the project selector manually using a key binding:

-- ~/.config/wezterm.lua
local wezterm = require 'wezterm'
local projects = require 'projects'

-- .. config ..

-- leader = { mods="CTRL", key = '\\', timeout_milliseconds = 1200 },
keys = {
  -- projects
  {mods="LEADER", key="p", action=projects.choose_project()},
  {mods="CTRL|SHIFT", key="k", action=projects.choose_project()},
  -- rest key bindings... 
}

Here I've configured the same action for the two different key bindings: key combination and the leader key. Use the one you prefer or both like me.

In choose_project we'll construct the list of projects and will use the InputSelector action action with the fuzzy option enabled:

function list_projects()
  -- actual list of projects
  -- see the next code snippet
end

function module.choose_project()
  local choices = {}
  for key, _ in pairs(list_projects()) do
    table.insert(choices, { id = key, label = key })
  end
  table.sort(choices, function(a, b) return a.id < b.id end)
  return wezterm.action.InputSelector {
    title = 'Projects',
    choices = choices,
    fuzzy = true,
    fuzzy_description = 'Enter a project name: ',
    action = wezterm.action_callback(function(window, pane, id, label)
      if not id then return end
      local projects = list_projects()
      if not projects[id] then return end
      local project = projects[id]
      return project(window, pane)
    end)
  }
end

In list_projects we'll specify an object with project names and their initialization functions. For single pane it will be a single_project, and for other more complicated project configurations it will have a separate function:

-- ~/.config/projects.lua
local wezterm = require 'wezterm'
local utils = require 'utils'
local module = {}

-- Creates a project with single workspace
local function single_project(name, command)
  return function(window, pane)
    return utils.workspace_single_pane( window, pane, name, command)
  end
end

local function project_blog(window, pane)
  local cwd = '/m/blog'
  return utils.workspace_double_pane(
    window, pane, "Left", "blog",
    'cd ' .. cwd .. '/content\nyazi\n',
    'cd ' .. cwd .. '\nzola serve'
  )
end

local function list_projects()
  return {
    -- projects
    blog = project_blog,
    -- apps
    app_spoti = single_project('spotify', 'spoti\n'),
    app_notes = single_project('notes', 'cd ~/notes/main\nmoar "./$(fzf)"\n'),
    app_budget = single_project('budget', 'cd /opt/actual && docker-compose up'),
    -- ssh zellij
    ssh_anm = single_project('ssh anm', 'ssh anm -t "zellij a"\n'),
    ssh_gm = single_project('ssh gm', 'ssh gm -t "zellij a main"\n'),
    ssh_oracle = single_project('ssh oracle', 'ssh oracle -t "zellij a"\n'),
  }
end

-- function module.choose_project()
--   see the previous code snippet
-- end

return module

Here I only use single and dual pane project workspaces, but you can modify this example and setup really powerful projects with dynamic checks, setting environment variables, sourcing .env files, some preconditions, etc. Have fun!

If a project needs to be started frequently, consider assigning it to a different key binding directly:

-- ~/.config/projects.lua
function module.blog()
  return wezterm.action_callback(project_blog)
end
-- ~/.config/wezterm.lua
keys = {
  {mods="LEADER", key="b", action=projects.blog()},
  -- rest key bindings... 
}

See a full config: github.com/annimon-tutorials/wezterm-projects

Styling#

That gray project selector looks so boring, let's add some colors and icons to it. You can use wezterm.format to apply styles to fuzzy_description:

function module.choose_project()
  -- ...
    fuzzy = true,
    fuzzy_description = wezterm.format {
      { Attribute = { Intensity = 'Bold' }},
      { Foreground = { Color = '#aaffaa' }},
      { Text = 'Enter a project name: '}
    },
  -- ...
end

Instead of defining custom colors, you can use AnsiColor with 16 values: Black, Maroon, Green, Olive, Navy, Purple, Teal, Silver, Grey, Red, Lime, Yellow, Blue, Fuchsia, Aqua or White.

{ Foreground = { AnsiColor = 'Lime' }},

To use icons, see the wezterm.nerdfonts page.

The list of projects needs to be slightly modified:

local function list_projects()
  return {
    -- projects
    blog = {
      label = wezterm.format {
        { Foreground = { AnsiColor = 'Fuchsia' }},
        { Text = wezterm.nerdfonts.md_typewriter },
        { Foreground = { AnsiColor = 'White' }},
        { Text = ' Blog' },
      },
      action = project_blog
    },
    -- apps
    app_spoti = {
      label = wezterm.format {
        { Foreground = { AnsiColor = 'Green' }},
        { Text = wezterm.nerdfonts.md_spotify },
        { Foreground = { AnsiColor = 'White' }},
        { Text = ' Spoti' },
      },
      action = single_project('spotify', 'spoti\n')
    },
    -- other
  }
end

And the project selector function too:

function module.choose_project()
  local choices = {}
  for key, value in pairs(list_projects()) do
    table.insert(choices, { label = value.label, id = key })
  end
  table.sort(choices, function(a, b) return a.id < b.id end)
  -- ...
  action = wezterm.action_callback(function(window, pane, id, label)
    -- ...
    local project = projects[id].action
    return project(window, pane)
  end)
end)

Here's what I got:

input selector

Full config: github.com/annimon-tutorials/wezterm-projects