Toggling on a Light Theme

13 September 2021

Linux - Vim

Abstract

While dark themes are great for working indoors and in low light conditions, dark themes in the direct sunlight can be unreadable. This used to be a setting I toggled in my IDE whenever I’d need to change things, but this adjustment isn’t as simple when moving into vim and other command line utilities.

Both light and dark themes
A side by side comparison of the light and dark theme.

Overview

In moving to a terminal heavy workflow, there’s a handful of applications that I use that I would need to reconfigure to handle a light theme. My goal is to have the light and dark theme both be easily accessible, and defined in a way that logging into a system would persist the currently selected theme.

Given my current workflow, this meant having to figure out how make a make a light or dark theme work situationally for the following apps:

  1. Terminator
  2. bash
  3. vim
  4. bat
  5. fzf
  6. lsd
  7. cointop
  8. venv

Terminator

For terminator, I installed the terminator-themes plugin which made managing two separate themes as easy a default layout for my dark theme, and a light layout for my light theme.

For the default theme I use a modification of “Cobalt Neon”, and for the light theme I use “OneHalfLight”. Each of these is setup as a profile in terminator, and for the light profile, under the command tab, I’ve checked the “Run a custom command instead of my shell”, with BASHTHEME=light bash -l being what is run, to set the BASHTHEME environment variable before starting our shell. Then the layouts are as simple as telling the default to launch with a single “Cobalt Neon” profile child, and the light layout to launch with a single “OneHalfLight” profile child.

Terminator profile config
Making sure the light theme profile passes the info along to bash when launching.

Then to make it so that I could launch directly into a light themed terminator layout directly from a keyboard shortcut, I copied the default terminator.desktop file and modified it so that it reflected we were launching a light session. This meant changing the Exec=terminator lines to Exec=terminator -l light, the name from Name=Terminator to Name=Terminator Light, and the Name=Open a New Window to Name=Open a New Light Window in the [NewWindow Shortcut Group].

The full light theme terminator-light.desktop file I use is as follows:

[Desktop Entry]
Categories=GNOME;GTK;Utility;TerminalEmulator;System;
Comment=Multiple terminals in one window
Exec=terminator -l light
Icon=utilities-terminal
Keywords=terminal;shell;prompt;command;commandline;
Name=Terminator Light
NoDisplay=false
StartupNotify=true
Terminal=0
TerminalOptions=
TryExec=terminator
Type=Application
X-Ayatana-Desktop-Shortcuts=NewWindow;
X-KDE-SubstituteUID=false
X-KDE-Username=
X-Ubuntu-Gettext-Domain=terminator

[NewWindow Shortcut Group]
Exec=terminator -l light
Name=Open a New Light Window
TargetEnvironment=Unity

Bash, bat, and fzf

Since we’ve setup Terminator to pass along a BASHTHEME=light whenever launching our light, let’s handle working with that in our ~/.bashrc. To keep my ~/.bashrc portable, and usable on systems even where I haven’t configured this, I’ll handle any of the actual configuration in a file called ~/.bash_theme. Only if it exists, and we’re on a color_256 system will we try to source it. These lines are directly out of my ~/.bashrc:

if [[ "$color_256" == "yes" && -f ~/.bash_theme ]]; then
  . ~/.bash_theme 
elif [ "$color_256" = yes ]; then
  PS1=...  

Now in the ~/.bash_theme file, we’re going to handle setting our PS1 to match our theme, as well as exporting any color configurations that we can set simply by environment variables. In this case, that means bat, and fzf can be nicely handled here.

We assume that if there is no $BASHTHEME environment variable is set, that we just want the dark theme.

With the dark theme, we want bat to use OneHalfDark as the theme for syntax highlighting, and OneHalfLight for the light theme.

With fzf, we only need to specify that we want the light theme in it’s default options by tacking on a --color=light option at the end of our default options.

In the case of our PS1/bind commands, these both configure my bash status line to look match the lightline plugin for vim, nicely displaying INSERT/NORMAL since I’m using vi mode.

The light-min and dark-min themes are specifically there for Termux , since rendering the full status line takes up too much space on a phone rotated in portrait.

~/.bash_theme
if [ -z "$BASHTHEME" ]; then
  export BASHTHEME="dark"
fi

dark() {
    PS1='\[\e[38;5;231m\]\[\e[48;5;244m\] \h | \u \[\e[48;5;240m\] \w \[\e[0m\] '
    bind 'set vi-cmd-mode-string "\1\e[01;38;5;232m\2\1\e[48;5;150m\2 NORMAL \1\e[0m\2"'
    bind 'set vi-ins-mode-string "\1\e[01;38;5;232m\2\1\e[48;5;111m\2 INSERT \1\e[0m\2"'
    export PS1
    export BAT_THEME="OneHalfDark"
}

light() {
    PS1='\[\e[38;5;231m\]\[\e[48;5;31m\] \h | \u \[\e[48;5;24m\] \w \[\e[0m\] '
    bind 'set vi-cmd-mode-string "\1\e[38;5;232m\2\1\e[48;5;231m\2 NORMAL \1\e[0m\2"'
    bind 'set vi-ins-mode-string "\1\e[38;5;63m\2\1\e[48;5;231m\2 INSERT \1\e[0m\2"'
    export PS1
    export BAT_THEME="OneHalfLight"
    export FZF_DEFAULT_OPTS="${FZF_DEFAULT_OPTS} --color=light"
}

dark-min() {
    PS1='\[\e[38;5;231m\]\[\e[48;5;240m\] \w \[\e[0m\] '
    bind 'set vi-cmd-mode-string "\1\e[01;38;5;232m\2\1\e[48;5;150m\2 N \1\e[0m\2"'
    bind 'set vi-ins-mode-string "\1\e[01;38;5;232m\2\1\e[48;5;111m\2 I \1\e[0m\2"'
    export PS1
    export BAT_THEME="OneHalfDark"
}

light-min() {
    PS1='\[\e[38;5;231m\]\[\e[48;5;31m\] \w \[\e[0m\] '
    bind 'set vi-cmd-mode-string "\1\e[38;5;232m\2\1\e[48;5;231m\2 N \1\e[0m\2"'
    bind 'set vi-ins-mode-string "\1\e[38;5;63m\2\1\e[48;5;231m\2 I \1\e[0m\2"'
    export PS1
    export BAT_THEME="OneHalfLight"
    export FZF_DEFAULT_OPTS="${FZF_DEFAULT_OPTS} --color=light"
}

case "$BASHTHEME" in
  dark)
    dark
    ;;
  light)
    light
    ;;
  dark-minimal)
    dark-min
    ;;
  light-minimal)
    light-min
    ;;
  *)
    echo "Warning: Unknown theme: ${BASHTHEME}. Using dark."
    dark
    ;;
esac

Vim

In vim, I have two different color scheme plugins installed, onedark and papercolor-theme. I also have two plugins that depending on the theme, I need to set configurations for, these are lightline and limelight.

I make use of my ~/.vim/after/plugin/ directory to handle the settings for each of them, there I check if the status of the $BASHTHEME variable and apply changes accordingly. For example, these are the contents of ~/.vim/after/plugin/papercolor-theme.vim:

if ($BASHTHEME ==? "light" || $BASHTHEME ==? "light-min")
 set background=light
 colorscheme PaperColor
endif

I do the same thing in ~/.vim/after/plugin/onedark.vim, except instead checking for a “dark” or “dark-min” theme. Then after making some modifications to the colorscheme to support a transparent background, I set the following:

set background=dark
colorscheme onedark

In ~/.vim/after/plugin/limelight.vim, I have to check if we’re on a dark theme, and adjust my conceal color to match the changes I made to the onedark colorsheme.

In ~/.vim/after/plugin/lightline.vim, for a dark theme, I use the one colorscheme and the dark background, and for the light theme, I use the PaperColor theme with the light background.

lsd

The latest release version of lsd didn’t support color themes, and the default values used have enough fields that refuse to show on a white background. Building the latest version from source with cargo allowed me access to the current theme configuration that they offer. Instead of changing this conditionally, instead I opted to change the values to something that would be visible in both light and dark theme.

To set it up so that it would see the themes, I have the following as my ~/.config/lsd/config.yaml

color:
  theme: ~/.config/lsd/themes/light.yaml

and the contents of ~/.config/lsd/themes/light.yaml are:

user: 202
group: 129
permission:
  read: dark_green
  write: dark_yellow
  exec: dark_red
  exec-sticky: 5
  no-access: 250
date:
  hour-old: 40
  day-old: 42
  older: 36
size:
  none: 250
  small: 200
  medium: 172
  large: 196
inode:
  valid: 128
  invalid: 250
links:
  valid: 128
  invalid: 250
tree-edge: 250

The main difference here is I’ve changed the bright yellows to colors like orange and purple.

cointop

Cointop is the first instance that I used a hack to handle toggling themes. Cointop allows me to launch while specifying a colorscheme, so I cheated, and aliased cointop to a function called _cointop that checks to see if the current $BASHTHEME, and if so launch it with the xray colorscheme. The following lines are defined in my ~/.bash_aliases:

function _cointop(){
  case $BASHTHEME in
    light*)
      cointop --colorscheme xray $@
      ;;
    *)
      cointop $@
      ;;
    esac
}

# cointop colorscheme
alias cointop='_cointop'

venv

The only reason I have to care about venv, is because my PS1 doesn’t follow a convention that allows venv to nicely append a prefix to it. To handle this, I’ve modified the default activate script that venv normally installs to the following:

# This file must be used with "source bin/activate" *from bash*
# you cannot run it directly

deactivate () {
    # reset old environment variables
    if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
        PATH="${_OLD_VIRTUAL_PATH:-}"
        export PATH
        unset _OLD_VIRTUAL_PATH
    fi
    if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
        PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
        export PYTHONHOME
        unset _OLD_VIRTUAL_PYTHONHOME
    fi

    # This should detect bash and zsh, which have a hash command that must
    # be called to get it to forget past commands.  Without forgetting
    # past commands the $PATH changes we made may not be respected
    if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
        hash -r 2> /dev/null
    fi

    if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
        PS1="${_OLD_VIRTUAL_PS1:-}"
        export PS1
        unset _OLD_VIRTUAL_PS1
    fi

    unset VIRTUAL_ENV
    if [ ! "${1:-}" = "nondestructive" ] ; then
    # Self destruct!
        unset -f deactivate
    fi
}

# unset irrelevant variables
deactivate nondestructive

VIRTUAL_ENV="__VENV_DIR__"
export VIRTUAL_ENV

_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
export PATH

# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
    _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
    unset PYTHONHOME
fi

if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
    _OLD_VIRTUAL_PS1="${PS1:-}"
    case $BASHTHEME in
        light)
            PS1='\[\e[38;5;231m\]\[\e[48;5;31m\] \h | \u \[\e[48;5;24m\] __VENV_PROMPT__| \w \[\e[0m\] '
            ;;
        light-min)
           PS1='\[\e[38;5;231m\]\[\e[48;5;24m\] __VENV_PROMPT__| \w \[\e[0m\] '
           ;;
        dark-min)
            PS1='\[\e[38;5;231m\]\[\e[48;5;240m\] __VENV_PROMPT__| \w \[\e[0m\] '
            ;;
        *)
           PS1='\[\e[38;5;231m\]\[\e[48;5;244m\] \h | \u \[\e[48;5;240m\] __VENV_PROMPT__| \w \[\e[0m\] '
    esac
    export PS1
fi

# This should detect bash and zsh, which have a hash command that must
# be called to get it to forget past commands.  Without forgetting
# past commands the $PATH changes we made may not be respected
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
    hash -r 2> /dev/null
fi

This will manually set the PS1 based on the current $BASHTHEME, nicely including the prompt in with the color theme. I then have to run an installer script to make sure that I’ve properly overwritten the activate script venv normally installs to be the one presented above.

#!/bin/bash

dir="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"

if ! [ $(id -u) = 0 ]; then
   echo "I am not root!"
   exit 1
fi

_pyVersions="3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 3.12 3.13"

for _py in $_pyVersions; do
  if [ -d "/lib/python${_py}/venv/" ]; then
    if ! [ -f "/lib/python${_py}/venv/scripts/common/activate.bash" ]; then
      mv "/lib/python${_py}/venv/scripts/common/activate" "/lib/python${_py}/venv/scripts/common/activate.bash"
    fi
    cp "${dir}/activate" "/lib/python${_py}/venv/scripts/common/activate"
  fi
done

3