Color scheme switching for WezTerm

I really like WezTerm, a cross-platform terminal emulator and multiplexer I have been using for some time now. What particularly drew me to it was the combination of

WezTerm also supports color schemes, and I use one based on the beautiful Selenized color palette, in particular Selenized black & white. While WezTerm comes with a lot of color schemes, at the time of writing, support for Selenized Black and Selenized White is only present out of the box in the nightly builds (via Gogh). This is why I wrote my own color scheme definitions and put them in ~/config/wezterm-color-schemes so they can be picked up by WezTerm. Here they are.

Selenized black & white

Selenized Black.toml:

[colors]
ansi = [
   "#252525",
   "#ed4a46",
   "#70b433",
   "#dbb32d",
   "#368aeb",
   "#eb6eb7",
   "#3fc5b7",
   "#777777"
]
brights = [
   "#3b3b3b",
   "#ff5e56",
   "#83c746",
   "#efc541",
   "#4f9cfe",
   "#ff81ca",
   "#56d8c9",
   "#dedede"
]
background = "#181818"
foreground = "#b9b9b9"
cursor_bg = "#83c746"
cursor_border = "#83c746"
cursor_fg = "#181818"
selection_bg = "#b9b9b9"
selection_fg = "#181818"

[colors.indexed]

[metadata]
aliases = []
name = "Selenized Black"

Selenized White.toml:

[colors]
ansi = [
  "#ebebeb",
  "#d6000c",
  "#1d9700",
  "#c49700",
  "#0064e4",
  "#dd0f9d",
  "#00ad9c",
  "#878787"
]
brights = [
  "#cdcdcd",
  "#bf0000",
  "#008400",
  "#af8500",
  "#0054cf",
  "#c7008b",
  "#009a8a",
  "#282828"
]
background = "#ffffff"
foreground = "#474747"
cursor_bg = "#008400"
cursor_border = "#008400"
cursor_fg = "#ffffff"
selection_bg = "#474747"
selection_fg = "#ffffff"

[colors.indexed]

[metadata]
aliases = []
name = "Selenized White"

Switching between color schemes

This is all well and good. However, another thing WezTerm is missing out of the box is an easy way to switch color schemes. I mostly use dark colors, but sometimes I want to put a screenshot of my terminal in a document and want it to look bright, or I am sharing my screen with someone who can read better when the text is dark on light, so having a convenient way to switch betwwen dark and light modes is useful to me.

Fortunately, WezTerm is highly configurable, so I was able to implement the toggle myself. Here is a minimal, but heavily commented, WezTerm configuration that shows my solution. I have included links to the relevant WezTerm documentation so you can read up on what each of the parts does if you wish.

Standard preamble:

local wezterm = require 'wezterm'
local config = wezterm.config_builder()

Look up home directory to construct some paths (this is standard Lua):

local home = os.getenv('HOME')

WezTerm will look for color scheme files in the directories in color_scheme_dirs:

config.color_scheme_dirs = { home .. '/config/wezterm-color-schemes' }

We will persist the name of the current color scheme in ~/.color-scheme:

local color_scheme_file = home .. '/.color-scheme'

We will toggle between these two color schemes:

local default_color_scheme = 'Selenized Black'
local alternate_color_scheme = 'Selenized White'

In my case, these are the custom schemes described above, but they don't have to be. You can name any color scheme WezTerm is able to find, custom or built in.

Function to load the name of the current color scheme, falls back to default_color_scheme if the file is not present:

local function load_color_scheme()
  local color_scheme = default_color_scheme
  local file = io.open(color_scheme_file, 'r')
  if file then
    color_scheme = file:read('*a')
    file:close()
  end
  return color_scheme
end

Function to save the name of the current color scheme:

local function save_color_scheme(color_scheme)
  local file = io.open(color_scheme_file, 'w+')
  if file then
    file:write(color_scheme)
    file:flush()
    file:close()
  end
  return color_scheme
end

Make WezTerm use the saved color scheme by assigning it to color_scheme:

config.color_scheme = load_color_scheme()

This is an event handler for the custom event toggle-color-scheme. It determines which scheme to switch to and saves it.

wezterm.on('toggle-color-scheme', function(_window, _pane)
  local color_scheme = load_color_scheme()
  local new_color_scheme = default_color_scheme
  if color_scheme == default_color_scheme then
    new_color_scheme = alternate_color_scheme
  end
  save_color_scheme(new_color_scheme)
end)

I use Ctrl-b as the leader key, i. e., as a prefix for my custom keyboard shortcuts:

config.leader = { key = 'b', mods = 'CTRL', timeout_milliseconds = 1000 }

Now we configure the key binding to toggle the color scheme. Specifying both LEADER and CTRL makes it so I have to press the leader (Ctrl-b), followed by Ctrl-c.

config.keys = {
  {
    key = 'c',
    mods = 'LEADER|CTRL',

Use Multiple to trigger two actions when the key combination is pressed:

    action = wezterm.action.Multiple {

First action: fire the event that will save the new color scheme:

      wezterm.action.EmitEvent 'toggle-color-scheme',

Second action: make WezTerm reload its configuration, thus activating the new color scheme:

      wezterm.action.ReloadConfiguration,
    },
  },
}

Finally, return the configuration to WezTerm:

return config

And here is the entire thing:

local wezterm = require 'wezterm'
local config = wezterm.config_builder()
local home = os.getenv('HOME')

config.color_scheme_dirs = { home .. '/config/wezterm-color-schemes' }
local color_scheme_file = home .. '/.color-scheme'
local default_color_scheme = 'Selenized Black'
local alternate_color_scheme = 'Selenized White'

local function load_color_scheme()
  local color_scheme = default_color_scheme
  local file = io.open(color_scheme_file, 'r')
  if file then
    color_scheme = file:read('*a')
    file:close()
  end
  return color_scheme
end

local function save_color_scheme(color_scheme)
  local file = io.open(color_scheme_file, 'w+')
  if file then
    file:write(color_scheme)
    file:flush()
    file:close()
  end
  return color_scheme
end

config.color_scheme = load_color_scheme()

wezterm.on('toggle-color-scheme', function(_window, _pane)
  local color_scheme = load_color_scheme()
  local new_color_scheme = default_color_scheme
  if color_scheme == default_color_scheme then
    new_color_scheme = alternate_color_scheme
  end
  save_color_scheme(new_color_scheme)
end)

config.leader = { key = 'b', mods = 'CTRL', timeout_milliseconds = 1000 }
config.keys = {
  {
    key = 'c',
    mods = 'LEADER|CTRL',
    action = wezterm.action.Multiple {
      wezterm.action.EmitEvent 'toggle-color-scheme',
      wezterm.action.ReloadConfiguration,
    },
  },
}

return config

Saving this to your home directory as .wezterm.lua sets you up to toggle between light and dark mode by pressing Ctrl-b Ctrl-c.

Using this combination of persisting the color scheme and config reload means that the change applies to all open WezTerm panes and windows (because they all belong to the same WezTerm instance).

Switching the current pane only

I also wrote a script to change the color scheme for the current pane, but this one only works for my custom color schemes because it reads the TOML files and outputs the escape sequences to change the terminal color palette. The script expects to live in the same directory as the TOML files and requires toml-cli to work. It does not toggle between two color schemes, but expects a color scheme name as an argument. Here it is:

#!/usr/bin/zsh
if [ -z "$1" -o -n "$2" ]; then
  echo "Usage: $0 COLOR_SCHEME_NAME" >&2
  exit 1
fi
SCHEME="$1"
HERE="$(dirname "$0")"
TOML="$HERE/$SCHEME.toml" 
if ! [ -f "$TOML" ]; then
  echo "Color scheme not found at $TOML" >&2
  exit 1
fi

function color() {
  case "$1" in
    0|1|2|3|4|5|6|7) toml get -r "$TOML" "colors.ansi[$1]" ;;
    8|9|10|11|12|13|14|15) toml get -r "$TOML" "colors.brights[$(($1-8))]" ;;
    *) toml get -r "$TOML" "colors.$1" ;;
  esac
}

echo -en "\\e]4;0;`color 0`;1;`color 1`;2;`color 2`;3;`color 3`;4;`color 4`;5;`color 5`;6;`color 6`;7;`color 7`;8;`color 8`;9;`color 9`;10;`color 10`;11;`color 11`;12;`color 12`;13;`color 13`;14;`color 14`;15;`color 15`\\a"
echo -en "\\e]10;`color foreground`;`color background`;`color cursor_bg`\\a"
echo -en "\\e]17;`color selection_bg`\\a"
echo -en "\\e]19;`color selection_fg`\\a"

It have not yet created a key binding to invoke this script, but it should be possible with a bit of Lua code (emit a custom event and handle it by executing the script in a subprocess, making sure the output goes to the correct pane).

Where to now?

For now, I am happy with my setup. I can toggle between dark and light modes with a keybinding. Possible extensions I have thought about, but not yet implemented are: