Enabling Theme Mode Automation for Windows through WSL

As I’ve posted before, I created a script to swap my tools between light and dark modes, which I call SolarSwap. This tool lets me change themes for Neovim, Tmux, and more. However, until now, it didn’t support Windows or Windows Terminal. I set out to automate those as well.

I needed to figure out how to programmatically change the Windows theme and the Windows Terminal theme. Fortunately, it was relatively straightforward.

Note: This utility is called from a main script named SolarSwap.sh, which sets environment variables such as $SOLARSWAP_DIR, $DEFAULT_MODE, and $THEME. Please review the tool as a whole and understand the code here before trying to use it.

Why Automate Theme Switching?

I find that I get eye strain when using bright themes in low-light conditions, and vice versa. So, I like to switch between light and dark themes based on the time of day or my environment.

It’s quite tedious to manually change themes across multiple applications and the operating system itself. By automating this process, I can ensure a consistent look and feel across my development environment with just a single command.

Windows Terminal

The Windows Terminal settings are stored in a JSON file located at:

C:\Users\<username>\AppData\Local\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json

Once I found this out, I realized that I could write a Python script to handle the json manipulation.

import json
import sys

FILE_PATH = sys.argv[1]
COLORSCHEME = sys.argv[2]

with open(FILE_PATH, "r") as f:
    settings = json.load(f)

for profile in settings["profiles"]["list"]:
    if profile.get("name") == "Ubuntu":
    profile["colorScheme"] = COLORSCHEME

with open(FILE_PATH, "w") as f:
    json.dump(settings, f, indent=4)

Here, I pass in the settings file path and the color scheme I want to set. The script reads the json, updates the color scheme for my Ubuntu profile, and writes it back out. Windows Terminal automatically picks up the change. In my case, I am passing in one of Catppuccin Latte or Catppuccin Mocha. The SolarSwap script handles the logic of which to use. Here’s the Bash script that calls it:

source "$SOLARSWAP_DIR/lib/solarswap.sh"
source "$SOLARSWAP_DIR/lib/wsl.sh"

function main() {
  local background=${1:-$DEFAULT_MODE}
  local theme=${2:-$THEME}
  local config_file="$WIN_LOCALAPPDATA/Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState/settings.json"
  variant="$(get_variant ${background})"
  local full_theme_name="${theme^} ${variant^}"
  python "$SOLARSWAP_DIR/tools/wt.exe/change_settings.py" "${config_file}" "${full_theme_name}"
}

main $@ || exit 1

This script sources the SolarSwap and WSL helper libraries, determines the theme to use, constructs the full theme name, and calls the Python script to update the Windows Terminal settings. The variant variable is set to either Latte or Mocha based on whether the background is light or dark.

Windows Theme

Now I needed to figure out how to change the Windows theme. After some searching, I found that custom themes are stored in this directory:

C:\Users\<username>\AppData\Local\Microsoft\Windows\Themes

I created two themes, one for light mode and one for dark mode using the Windows settings under Preferences > Themes. The files are named MyLight.theme and MyDark.theme. Then it’s just a matter of running the following command to apply the theme:

start-process -filepath C:\Users\<username>\AppData\Local\Microsoft\Windows\Themes\My<mode>.theme

From WSL, I can call this command using powershell.exe -Command "<command>". Bringing this all together, here’s the final Bash script that swaps both Windows Terminal and Windows themes:

source "$HOME/.bash/lib/error_handling.sh"
source "$HOME/.bash/lib/logging.sh"
source "$SOLARSWAP_DIR/lib/solarswap.sh"
source "$SOLARSWAP_DIR/lib/wsl.sh"

function main() {
  local background=${1:-$DEFAULT_MODE}
  local theme=${2:-$THEME}
  local config_file="$WIN_LOCALAPPDATA/Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState/settings.json"
  local theme_file="$WIN_LOCALAPPDATA/Microsoft/Windows/Themes/My${background^}.theme"
  bx.log info "[Windows Terminal] Setting Windows Terminal theme."
  bx.log debug "[Windows Terminal] Config file: $config_file"
  variant="$(get_variant ${background})"
  local full_theme_name="${theme^} ${variant^}"
  python "$SOLARSWAP_DIR/tools/wt.exe/change_settings.py" "${config_file}" "${full_theme_name}"
  bx.log info "[Windows Terminal] Finished swap."
  # Set Windows theme
  bx.log info "[Windows] Setting Windows theme."
  local win_path=$(convert_linux_path_to_windows "$theme_file")
  bx.log debug "[Windows] Theme file: $win_path"
  powershell.exe -Command start-process -filepath "'${win_path}'"
  bx.log info "[Windows] Finished swap."
}

main $@ || exit 1

WSL Helper Functions

I’ve abstracted some of the WSL-specific logic into a helper library located at $SOLARSWAP_DIR/lib/wsl.sh. Here are the functions:

function get_windows_username() {
  cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r'
}

WIN_USER=$(get_windows_username)
WIN_HOME="/mnt/c/Users/$WIN_USER"
WIN_LOCALAPPDATA="$WIN_HOME/AppData/Local"

export WIN_USER
export WIN_HOME
export WIN_LOCALAPPDATA

function convert_linux_path_to_windows() {
  local path="$1"
  local win_path=$(
    echo "$path" | sed -E 's|^/mnt/([a-z])|\\\1:|; s|/|\\|g; s|\\$||; s|^\\||'
  )
  printf "%s" "$win_path"
}

The function get_windows_username retrieves the Windows username by calling a Windows command from WSL. The other function convert_linux_path_to_windows converts a Linux-style path to a Windows-style path, which is necessary for passing file paths to PowerShell commands.

Personal Bash Extension Libraries

I have included two libraries for logging and error handling, which I use in many of my Bash scripts. You can find them at: https://gitlab.com/jasoncarpenter/configuration_bash These provide functions like bx.log info "message" and bx.log debug "message" for logging, as well as error handling functions and backtrace functionality.

Final Thoughts

Now, I can call SolarSwap.sh inside my WSL environment, and it will swap my themes across Neovim, Tmux, Windows Terminal, and Windows itself. This makes it easy to switch between light and dark modes based on the time of day or my preference. The approach can be extended to other applications that support theme changes via configuration files or command-line options. Happy theming!