The following Python script is a CLI tool that explicitly switches Windows between light and dark themes while also synchronizing Windows Terminal, VS Code, and desktop background colors. It is intended for Windows environments.
What This Script Does
Run the script with either --day or --night.
-
--day- Switch Windows to the light theme
- Set the desktop background color to white
- Change the Windows Terminal color scheme to the daytime scheme
- Change the VS Code theme to the daytime theme
-
--night- Switch Windows to the dark theme
- Set the desktop background color to black
- Change the Windows Terminal color scheme to the nighttime scheme
- Change the VS Code theme to the nighttime theme
There is no automatic time-based switching. It is designed to apply either the daytime or nighttime configuration explicitly and manually.
Usage
1. Required File
You must place the following configuration file in the same folder as the script:
config.toml
The script defines the path as:
CONFIG_PATH = Path(__file__).with_name("config.toml")
For example, if the script is located at:
C:\Users\you\theme_switcher.py
then the configuration file must be:
C:\Users\you\config.toml
2. Example config.toml
The script expects the following TOML structure:
[terminal]
day_color_scheme = "One Half Light"
night_color_scheme = "One Half Dark"
[vscode]
day_theme = "Default Light Modern"
night_theme = "Default Dark Modern"
The values must match actual theme names available in your Windows Terminal and VS Code installations.
3. Apply the Day Theme
python theme_switcher.py --day
This applies:
Windows: Light theme
Wallpaper: White
Windows Terminal: terminal.day_color_scheme from config.toml
VS Code: vscode.day_theme from config.toml
4. Apply the Night Theme
python theme_switcher.py --night
This applies:
Windows: Dark theme
Wallpaper: Black
Windows Terminal: terminal.night_color_scheme from config.toml
VS Code: vscode.night_theme from config.toml
Main Features
1. Windows Light/Dark Theme Switching
set_theme(light: bool) updates the Windows Registry.
Target registry key:
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize
The script modifies these values:
AppsUseLightTheme
SystemUsesLightTheme
Meaning:
| Value | Meaning |
|---|---|
1 |
Light theme |
0 |
Dark theme |
After updating the registry, it broadcasts WM_SETTINGCHANGE to notify Windows of the theme change.
2. Set the Wallpaper to a Solid Color
set_solid_wallpaper(r, g, b) uses the Windows COM API to change the desktop background to a solid color.
The script uses the following fixed values:
DAY_WALLPAPER_RGB = (255, 255, 255)
NIGHT_WALLPAPER_RGB = (0, 0, 0)
Therefore:
| Mode | Wallpaper Color |
|---|---|
--day |
White |
--night |
Black |
Instead of using an image file, the script calls IDesktopWallpaper::SetBackgroundColor and IDesktopWallpaper::Enable(False) to disable wallpaper images and apply a solid background color.
3. Change the Windows Terminal Color Scheme
set_windows_terminal_color_scheme(color_scheme) edits Windows Terminal's settings.json.
The script searches for the configuration file under:
%LOCALAPPDATA%\Packages\Microsoft.WindowsTerminal_*\LocalState\settings.json
It updates the following JSON field:
{
"profiles": {
"defaults": {
"colorScheme": "..."
}
}
}
For example, when using --night, the following value from config.toml is applied:
[terminal]
night_color_scheme = "One Half Dark"
4. Change the VS Code Theme
set_vscode_color_theme(theme_name) modifies VS Code's user settings.
Target file:
%USERPROFILE%\AppData\Roaming\Code\User\settings.json
It updates:
{
"workbench.colorTheme": "Default Dark Modern"
}
If settings.json does not exist, it will be created automatically.
5. Avoid Unnecessary File Writes
write_json_if_changed() compares the newly generated JSON with the existing file contents.
If there is no difference, the file is not rewritten:
if old_text == new_text:
return False
This avoids unnecessary updates when the desired configuration is already applied.
Execution Flow
When --day Is Specified
main()
└─ apply_day_theme()
└─ apply_explicit_theme(
light=True,
terminal_color_scheme=CONFIG["day_terminal_color_scheme"],
vscode_theme=CONFIG["day_vscode_theme"],
wallpaper_rgb=(255, 255, 255)
)
When --night Is Specified
main()
└─ apply_night_theme()
└─ apply_explicit_theme(
light=False,
terminal_color_scheme=CONFIG["night_terminal_color_scheme"],
vscode_theme=CONFIG["night_vscode_theme"],
wallpaper_rgb=(0, 0, 0)
)
Inside apply_explicit_theme(), the following operations occur in order:
- Check the current Windows theme
- Change the Windows theme if necessary
- Update the wallpaper color
- Change the Windows Terminal color scheme
- Change the VS Code theme
- Broadcast an environment-change notification if Terminal or VS Code settings were modified
Requirements and Dependencies
Required
- Windows
- Python 3.11 or newer (standard library only)
- Python 3.10 or older requires
tomli
The script handles this as follows:
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib
Python 3.11+ includes tomllib by default.
For Python 3.10 and earlier:
pip install tomli
Notes and Caveats
1. Windows Only
The script depends on Windows-specific APIs:
import winreg
ctypes.WinDLL("ole32")
ctypes.WinDLL("user32")
It will not run on macOS or Linux.
2. Windows Terminal Must Be Installed
The script searches for a package directory matching:
Microsoft.WindowsTerminal_*
If Windows Terminal is not installed, it raises:
FileNotFoundError: Windows Terminal settings.json was not found.
3. Missing config.toml Causes Startup Failure
The configuration file is loaded during module initialization:
CONFIG = load_config()
As a result, if config.toml is missing, the script may fail before any command-line processing occurs, including before most execution paths beyond basic interpreter startup.
4. JSON Comments May Be Lost
Both Windows Terminal and VS Code settings files are read and written using:
json.load()
json.dumps()
As a consequence:
- JSONC comments are not preserved
- The file may be reformatted
- Comments may be removed
- Key ordering may change
VS Code's settings.json commonly contains comments because it supports JSONC, so users relying on comments should be aware of this limitation.
Summary
This script provides a Windows theme-switching utility that synchronizes four settings at once:
| Target | --day |
--night |
|---|---|---|
| Windows Theme | Light | Dark |
| Wallpaper | Solid white | Solid black |
| Windows Terminal | Day color scheme | Night color scheme |
| VS Code | Day theme | Night theme |
Typical usage:
python theme_switcher.py --day
python theme_switcher.py --night
All theme-specific values are managed through config.toml located in the same directory as the script.
import argparse
import ctypes
import json
import logging
import os
import uuid
from ctypes import wintypes
from pathlib import Path
import winreg
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib
PERSONALIZE_KEY = "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"
APPS_KEY = "AppsUseLightTheme"
SYSTEM_KEY = "SystemUsesLightTheme"
HWND_BROADCAST = 0xFFFF
WM_SETTINGCHANGE = 0x001A
SMTO_ABORTIFHUNG = 0x0002
CONFIG_PATH = Path(__file__).with_name("config.toml")
CLSID_DESKTOP_WALLPAPER = "C2CF3110-460E-4FC1-B9D0-8A1C0C9CC4BD"
IID_IDESKTOP_WALLPAPER = "B92B56A9-8B55-4E14-9A89-0199BBB6F93B"
COINIT_APARTMENTTHREADED = 0x2
CLSCTX_ALL = 0x17
S_OK = 0
S_FALSE = 1
RPC_E_CHANGED_MODE = 0x80010106
DAY_WALLPAPER_RGB = (255, 255, 255)
NIGHT_WALLPAPER_RGB = (0, 0, 0)
if hasattr(wintypes, "ULONG_PTR"):
ULONG_PTR = wintypes.ULONG_PTR
else:
ULONG_PTR = ctypes.c_size_t
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger(__name__)
class GUID(ctypes.Structure):
_fields_ = [
("Data1", wintypes.DWORD),
("Data2", wintypes.WORD),
("Data3", wintypes.WORD),
("Data4", ctypes.c_ubyte * 8),
]
@classmethod
def from_string(cls, value: str) -> "GUID":
return cls.from_buffer_copy(uuid.UUID(value).bytes_le)
def check_hresult(hr: int) -> None:
if ctypes.c_long(hr).value < 0:
raise OSError(f"HRESULT 0x{hr & 0xFFFFFFFF:08X}")
def rgb_to_colorref(r: int, g: int, b: int) -> int:
"""Return Windows COLORREF value. COLORREF is 0x00BBGGRR."""
return (r & 0xFF) | ((g & 0xFF) << 8) | ((b & 0xFF) << 16)
def set_solid_wallpaper(r: int, g: int, b: int) -> None:
"""
Set the Windows desktop background to a solid color without using an image file.
Specify the background color via IDesktopWallpaper::SetBackgroundColor,
and disable wallpaper image display via IDesktopWallpaper::Enable(False).
"""
logger.debug("Setting solid desktop wallpaper color to RGB(%d, %d, %d)", r, g, b)
ole32 = ctypes.WinDLL("ole32")
ole32.CoInitializeEx.argtypes = [ctypes.c_void_p, wintypes.DWORD]
ole32.CoInitializeEx.restype = ctypes.c_long
ole32.CoUninitialize.argtypes = []
ole32.CoUninitialize.restype = None
ole32.CoCreateInstance.argtypes = [
ctypes.POINTER(GUID),
ctypes.c_void_p,
wintypes.DWORD,
ctypes.POINTER(GUID),
ctypes.POINTER(ctypes.c_void_p),
]
ole32.CoCreateInstance.restype = ctypes.c_long
co_initialized_here = False
hr = ole32.CoInitializeEx(None, COINIT_APARTMENTTHREADED)
hr_unsigned = hr & 0xFFFFFFFF
if hr in (S_OK, S_FALSE):
co_initialized_here = True
elif hr_unsigned == RPC_E_CHANGED_MODE:
logger.debug("COM was already initialized with a different threading model")
else:
check_hresult(hr)
p_wallpaper = ctypes.c_void_p()
try:
clsid = GUID.from_string(CLSID_DESKTOP_WALLPAPER)
iid = GUID.from_string(IID_IDESKTOP_WALLPAPER)
hr = ole32.CoCreateInstance(
ctypes.byref(clsid),
None,
CLSCTX_ALL,
ctypes.byref(iid),
ctypes.byref(p_wallpaper),
)
check_hresult(hr)
vtable = ctypes.cast(
p_wallpaper,
ctypes.POINTER(ctypes.POINTER(ctypes.c_void_p)),
).contents
release = ctypes.WINFUNCTYPE(
wintypes.ULONG,
ctypes.c_void_p,
)(vtable[2])
set_background_color = ctypes.WINFUNCTYPE(
ctypes.c_long,
ctypes.c_void_p,
wintypes.DWORD,
)(vtable[8])
enable = ctypes.WINFUNCTYPE(
ctypes.c_long,
ctypes.c_void_p,
wintypes.BOOL,
)(vtable[18])
hr = set_background_color(p_wallpaper, rgb_to_colorref(r, g, b))
check_hresult(hr)
hr = enable(p_wallpaper, False)
check_hresult(hr)
logger.debug("Solid desktop wallpaper color applied")
finally:
if p_wallpaper:
release(p_wallpaper)
if co_initialized_here:
ole32.CoUninitialize()
def set_day_wallpaper() -> None:
set_solid_wallpaper(*DAY_WALLPAPER_RGB)
def set_night_wallpaper() -> None:
set_solid_wallpaper(*NIGHT_WALLPAPER_RGB)
def load_config(path: Path = CONFIG_PATH) -> dict:
if not path.is_file():
raise FileNotFoundError(f"Config file was not found: {path}")
with path.open("rb") as f:
config = tomllib.load(f)
return {
"day_terminal_color_scheme": config["terminal"]["day_color_scheme"],
"night_terminal_color_scheme": config["terminal"]["night_color_scheme"],
"day_vscode_theme": config["vscode"]["day_theme"],
"night_vscode_theme": config["vscode"]["night_theme"],
}
CONFIG = load_config()
def get_windows_terminal_settings_path() -> Path:
packages_dir = Path(os.environ["LOCALAPPDATA"]) / "Packages"
logger.debug("Searching for Windows Terminal settings.json under: %s", packages_dir)
for package_dir in packages_dir.glob("Microsoft.WindowsTerminal_*"):
settings_path = package_dir / "LocalState" / "settings.json"
logger.debug("Checking candidate path: %s", settings_path)
if settings_path.is_file():
logger.debug("Found Windows Terminal settings.json: %s", settings_path)
return settings_path
raise FileNotFoundError("Windows Terminal settings.json was not found.")
def get_vscode_settings_path() -> Path:
path = Path.home() / "AppData" / "Roaming" / "Code" / "User" / "settings.json"
logger.debug("Using VS Code settings.json path: %s", path)
return path
def get_current_theme() -> int:
try:
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, PERSONALIZE_KEY) as key:
value, regtype = winreg.QueryValueEx(key, APPS_KEY)
if regtype == winreg.REG_DWORD:
logger.debug("Current Windows app theme registry value: %s", value)
return int(value)
except FileNotFoundError:
logger.debug("Theme registry key/value not found. Falling back to light theme.")
return 1
def send_setting_change(param: str) -> None:
logger.debug("Broadcasting WM_SETTINGCHANGE with param=%r", param)
user32 = ctypes.WinDLL("user32", use_last_error=True)
send_message_timeout = user32.SendMessageTimeoutW
send_message_timeout.argtypes = [
wintypes.HWND,
wintypes.UINT,
wintypes.WPARAM,
wintypes.LPCWSTR,
wintypes.UINT,
wintypes.UINT,
ctypes.POINTER(ULONG_PTR),
]
send_message_timeout.restype = wintypes.LPARAM
result = ULONG_PTR()
ret = send_message_timeout(
HWND_BROADCAST,
WM_SETTINGCHANGE,
0,
param,
SMTO_ABORTIFHUNG,
5000,
ctypes.byref(result),
)
if ret == 0:
raise ctypes.WinError(ctypes.get_last_error())
logger.debug("WM_SETTINGCHANGE broadcast completed successfully for param=%r", param)
def notify_theme_changed() -> None:
logger.debug("Notifying system that theme has changed")
send_setting_change("ImmersiveColorSet")
def notify_environment_changed() -> None:
logger.debug("Notifying system that environment/settings may have changed")
send_setting_change("Environment")
def set_theme(light: bool) -> None:
value = 1 if light else 0
logger.debug("Setting Windows theme to: %s", "light" if light else "dark")
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, PERSONALIZE_KEY) as key:
winreg.SetValueEx(key, APPS_KEY, 0, winreg.REG_DWORD, value)
winreg.SetValueEx(key, SYSTEM_KEY, 0, winreg.REG_DWORD, value)
notify_theme_changed()
logger.debug("Windows theme updated")
def write_json_if_changed(path: Path, data: dict, description: str) -> bool:
new_text = json.dumps(data, ensure_ascii=False, indent=4) + "\n"
old_text = None
if path.exists():
old_text = path.read_text(encoding="utf-8")
if old_text == new_text:
logger.debug("%s already matches desired content; no update needed", description)
return False
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(new_text, encoding="utf-8", newline="\n")
logger.debug("%s updated: %s", description, path)
return True
def set_windows_terminal_color_scheme(color_scheme: str) -> bool:
settings_path = get_windows_terminal_settings_path()
logger.debug("Loading Windows Terminal settings from: %s", settings_path)
with settings_path.open("r", encoding="utf-8") as f:
settings = json.load(f)
profiles = settings.setdefault("profiles", {})
defaults = profiles.setdefault("defaults", {})
current_scheme = defaults.get("colorScheme")
logger.debug("Current Terminal default colorScheme: %r", current_scheme)
logger.debug("Desired Terminal default colorScheme: %r", color_scheme)
if current_scheme == color_scheme:
return False
defaults["colorScheme"] = color_scheme
return write_json_if_changed(settings_path, settings, "Windows Terminal settings.json")
def set_vscode_color_theme(theme_name: str) -> bool:
settings_path = get_vscode_settings_path()
if settings_path.exists():
with settings_path.open("r", encoding="utf-8") as f:
settings = json.load(f)
else:
settings = {}
current_theme = settings.get("workbench.colorTheme")
if current_theme == theme_name:
return False
settings["workbench.colorTheme"] = theme_name
return write_json_if_changed(settings_path, settings, "VS Code settings.json")
def apply_explicit_theme(
*,
light: bool,
terminal_color_scheme: str,
vscode_theme: str,
wallpaper_rgb: tuple[int, int, int],
) -> bool:
"""
Apply the specified theme immediately without using time-based determination.
Used by the CLI options --day / --night.
"""
current_is_light = bool(get_current_theme())
if current_is_light != light:
set_theme(light)
set_solid_wallpaper(*wallpaper_rgb)
terminal_changed = set_windows_terminal_color_scheme(terminal_color_scheme)
vscode_changed = set_vscode_color_theme(vscode_theme)
if terminal_changed or vscode_changed:
try:
notify_environment_changed()
except OSError:
logger.exception("Failed to broadcast environment change notification")
logger.info(
"Applied explicit %s theme: terminal=%r, vscode=%r, wallpaper_rgb=%r",
"day/light" if light else "night/dark",
terminal_color_scheme,
vscode_theme,
wallpaper_rgb,
)
return light
def apply_day_theme() -> bool:
return apply_explicit_theme(
light=True,
terminal_color_scheme=CONFIG["day_terminal_color_scheme"],
vscode_theme=CONFIG["day_vscode_theme"],
wallpaper_rgb=DAY_WALLPAPER_RGB,
)
def apply_night_theme() -> bool:
return apply_explicit_theme(
light=False,
terminal_color_scheme=CONFIG["night_terminal_color_scheme"],
vscode_theme=CONFIG["night_vscode_theme"],
wallpaper_rgb=NIGHT_WALLPAPER_RGB,
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Switch Windows, Windows Terminal, and VS Code themes explicitly."
)
mode_group = parser.add_mutually_exclusive_group(required=True)
mode_group.add_argument(
"--day",
action="store_true",
help="Apply day/light settings immediately and exit. Ignores current time.",
)
mode_group.add_argument(
"--night",
action="store_true",
help="Apply night/dark settings immediately and exit. Ignores current time.",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
if args.day:
apply_day_theme()
return
if args.night:
apply_night_theme()
return
if __name__ == "__main__":
main()
Top comments (0)