Keeping Terminal & Vim Colors in Sync

Arumoy Shome

2022-03-02 Wed 16:16

When you are staring at text all day long, little things such as the colors of your terminal screen & text editor start to matter. There is vast information & knowledge on color theory but very few colorscheme authors take this into account.

For the longest time, I was a devotee of the base16 colorschemes by Chris Kempson (oldest commit in my dotfiles repo that mentions base16 was almost 5 years ago, retrieved using the git log --grep 'base16' --reverse command).

Although the base16 themes work well, I did not find the text legible, especially in the light colorschemes. Fortunately, I stumbled upon Modus themes developed by Protesilaos Stavrou which conform to the highest standards of contrast, making the text in both the light & dark themes, extremely legible.

Although originally developed for Emacs, Protesilaos includes the modus-themes-exporter.el library as part of his dotfiles configuration (see his article for relevant links). Using this, I ported the colorschemes to vim and included them as part of my config.

I was a long term user of iTerm however, the wonderful GPU accelerated kitty terminal developed by Kovid Goyal soon stole my heart. Kitty ships with several themes, including the modus themes.

The final piece of the puzzle, is a few lines of bash along with some nifty configurations for kitty & vim to keep colorschemes across the shell & editors in sync.

I wrote modus (part of my dotfiles) which enables this. modus accepts a single parameter which can be light or dark and switches to the corresponding theme in all instances of kitty. Without any paramters, modus toggles the theme. Following are the contents of modus as of [2022-03-02 Wed 22:20]. Refer to my dotfiles for the latest version.

#!/usr/bin/env bash

# modus: switch between modus light (operandi) & dark (vivendi)
# themes.

# This script makes a few assumptions:
# 1. You are using kitty as your terminal <https://sw.kovidgoyal.net/kitty/>
# 2. You have a light & dark theme for kitty stored in kitty's config
#    dir (which is $XDG_CONFIG_HOME/kitty by default)
# 3. You exclusively use the wonderful modus themes by Protesilaos
#    Stravrou for your shell <https://protesilaos.com/emacs/modus-themes>
# 4. You have some plumbing on the vim side to automatically update
#    the `background' setting using autocommands. See
#    <https://github.com/arumoy-shome/dotfiles/blob/c3b172dc05446aff20a5660f09699ffcc89e0379/vim/autoload/aru.vim#L95>
#    and
#    <https://github.com/arumoy-shome/dotfiles/blob/c3b172dc05446aff20a5660f09699ffcc89e0379/vim/plugin/autocmds.vim#L27>
#    for examples.

# Usage: modus [COLOR]
# This script is meant to be called from the command line. Without any
# arguments, modus switches between the light & dark themes. It does
# so by looking at the first line of the
# $XDG_DATA_HOME/modus/background file which should contain the word
# `light' or `dark'.
# With the optional argument COLOR---which can be `light' or
# `dark'---it switches to the specified colorscheme.

[[ ! -d "$XDG_DATA_HOME/modus" ]] && mkdir -p "$XDG_DATA_HOME/modus"
MODUS_CURRENT_BG_FILE="$XDG_DATA_HOME/modus/background"
KITTY_LIGHT_THEME="$XDG_CONFIG_HOME/kitty/light-theme.conf"
KITTY_DARK_THEME="$XDG_CONFIG_HOME/kitty/dark-theme.conf"
KITTY_CURRENT_THEME="$XDG_CONFIG_HOME/kitty/current-theme.conf"

__update_emacs() {
  # update the colorscheme in Emacs. Assumes that the modus-themes
  # package <https://github.com/protesilaos/modus-themes> is installed
  # and in the load-path. The output is redirected to /dev/null to
  # avoid printing error messages if no emacs server is running.

  if [[ "$1" == "light" ]]; then
    emacsclient --eval '(modus-themes-load-operandi)' > /dev/null
  else
    emacsclient --eval '(modus-themes-load-vivendi)' > /dev/null
  fi
}

__update_kitty() {
  # Update the colorscheme in kitty. Assumes that you are using
  # a recent version of kitty with the `themes' kitten available & the
  # colorschemes are installed.
  # Note that we use a temp file to redirect the config output. This
  # is to avoid changing the kitty.conf file (causes false positive in
  # the vcs). Your kitty.conf should contain the following line:
  #
  #     include current-theme.conf

  TMPFILE="$(mktemp)"

  if [[ "$1" == "light" ]]; then
    kitty +kitten themes --config-file-name="$TMPFILE" Modus Operandi
  else
    kitty +kitten themes --config-file-name="$TMPFILE" Modus Vivendi
  fi

  rm -f "$TMPFILE" # cleanup
}

__light() {
  # Switch to the light (operandi) theme. Some preliminary error
  # handling is done when $KITTY_LIGHT_THEME does not exist.

  if [[ ! -f $KITTY_LIGHT_THEME ]]; then
    echo "Error: $KITTY_LIGHT_THEME does not exist."
    echo "Info: Use kitty +kitten themes to save the theme first."
    return 1
  fi

  ln -sf "$KITTY_LIGHT_THEME" "$KITTY_CURRENT_THEME"
  __update_kitty light
  __update_emacs light
  echo light > "$MODUS_CURRENT_BG_FILE"
}

__dark() {
  # Switch to the dark (vivendi) theme. Some preliminary error
  # handling is done when $KITTY_DARK_THEME does not exist.

  if [[ ! -f $KITTY_DARK_THEME ]]; then
    echo "Error: $KITTY_DARK_THEME does not exist."
    echo "Info: Use kitty +kitten themes to save the theme first."
    return 1
  fi

  ln -sf "$KITTY_DARK_THEME" "$KITTY_CURRENT_THEME"
  __update_kitty dark
  __update_emacs dark
  echo dark > "$MODUS_CURRENT_BG_FILE"
}

__toggle() {
  # Toggle between light and dark themes. Some preliminary error
  # handling is done when $MODUS_CURRENT_BG_FILE does not exist or is
  # not in the expected format.

  if [[ ! -f $MODUS_CURRENT_BG_FILE ]]; then
    echo "Error: $MODUS_CURRENT_BG_FILE does not exist."
    echo "Info: set a colorscheme using modus {light,dark} first."
    return 1
  fi

  if [[ ! -r $MODUS_CURRENT_BG_FILE || ! -s $MODUS_CURRENT_BG_FILE ]]; then
    echo "Error: Something is not right with $MODUS_CURRENT_BG_FILE."
    echo "Info: Make sure that it is readable & not empty."
    return 1
  fi

  local CURRENT_BG="$(head -n 1 "$MODUS_CURRENT_BG_FILE")"
  case "$CURRENT_BG" in
    light)
      __dark
      ;;
    dark)
      __light
      ;;
    *)
      echo "Error: Something is not right with $MODUS_CURRENT_BG_FILE."
      echo "Info: Make sure it is valid & try again."
      ;;
  esac
}

main() {
  if [[ "$#" -eq 0 ]]; then
    __toggle
  else
    case "$1" in
      light)
        __light
        ;;
      dark)
        __dark
        ;;
      *)
        # Do nothing
        ;;
    esac
  fi
}

main "$@"

To preserve the colorscheme across kitty sessions, modus creates a current-theme.conf in kitty’s config dir. This file should be sourced in kitty.conf using the following snippet.

include current-theme.conf

The configuration on vim side is a bit more involved. modus writes the current theme type (light or dark) in $XDG_DATA_HOME/modus/background. The following autoloaded function reads this file and sets the appropriate colorscheme in vim. To learn more about autoloaded functions in vim, consult the manual using :help autoload-functions.

function! aru#sync_background() abort
  let s:bg_file = expand('~/.local/share/modus/background')

  if filereadable(s:bg_file)
    let s:bg = readfile(s:bg_file, '', 1)
    execute 'colorscheme ' . s:bg[0]
  else
    colorscheme default
  endif
endfunction

The above function is called once on startup. I like to keep all code related to colors in the after/ directory to avoid overrides from external packages. The following line of vimscript placed in after/plugin/color.vim does the trick.

call aru#sync_background()

If we run modus from another shell, the colors in existing vim sessions does not update. To account for this, we can fire an autocmd every time vim gains focus. To learn more about vim autocmds, consult the manual using :help autocmd.

function! AruHighlightAutocmds() abort
  augroup AruHighlight
    autocmd!
    autocmd FocusGained * call aru#sync_background()
  augroup END
endfunction
call AruHighlightAutocmds()

Here are some screenshots of the end result.

Modus dark
Modus light