This Python script automatically switches your Windows appearance theme, Windows Terminal color scheme, and Visual Studio Code theme according to the time of day.
It is designed for users who want a brighter development environment during the day and a darker, more comfortable setup at night—without having to change each setting manually.
Why This Script Exists
Many developers prefer different themes depending on the time of day.
During the day, a light theme can make the screen easier to read in bright environments. At night, a dark theme can reduce eye strain and make long work sessions more comfortable.
For example:
- Daytime: light Windows theme, bright Terminal scheme, light VS Code theme
- Nighttime: dark Windows theme, dark Terminal scheme, dark VS Code theme
Instead of switching these settings by hand, this script automates the process based on a configurable schedule.
How It Works
The script follows a simple workflow:
- It reads theme settings and schedule information from
config.toml. - It checks the current time.
- It determines whether the system should be in day mode or night mode.
- It applies the corresponding Windows, Windows Terminal, and VS Code themes.
- It waits until the next scheduled switch time.
- It repeats the process automatically.
The result is a lightweight theme scheduler for a Windows-based development environment.
Configuration
The script expects a config.toml file in the same directory as the Python script.
The configuration file defines values such as:
- the time when the day theme starts, for example
08:00 - the time when the night theme starts, for example
20:00 - the Windows Terminal color scheme for daytime
- the Windows Terminal color scheme for nighttime
- the VS Code theme for daytime
- the VS Code theme for nighttime
This makes it easy to adapt the script to your own working style and preferred themes.
Running the Script
Automatic Mode
To run the script in scheduled mode, use:
python script.py
In this mode, the script stays running in the background. It applies the correct theme immediately, then waits until the next scheduled switch time.
Manual Day Mode
To apply the day theme immediately, use:
python script.py --day
This ignores the current time and applies the configured daytime settings.
Manual Night Mode
To apply the night theme immediately, use:
python script.py --night
This also ignores the current time and applies the configured nighttime settings.
Main Features
The script provides several useful features:
- switches the Windows system and app theme between light and dark
- updates the Windows Terminal default color scheme
- updates the Visual Studio Code color theme
- notifies Windows after theme-related settings change
- safely updates JSON configuration files only when changes are necessary
- supports both scheduled automation and manual one-off switching
- uses asynchronous file and system operations where appropriate
Windows Theme Switching
The Windows theme is changed by updating registry values under the current user’s personalization settings.
The script updates both:
AppsUseLightThemeSystemUsesLightTheme
After changing these values, it broadcasts a WM_SETTINGCHANGE message so Windows and related applications can respond to the update.
Windows Terminal Integration
For Windows Terminal, the script locates the Terminal settings.json file under the user’s local application data directory.
It then updates the default profile color scheme by modifying:
profiles.defaults.colorScheme
The script only writes the file if the desired color scheme is different from the current one. This avoids unnecessary file updates.
Visual Studio Code Integration
For Visual Studio Code, the script updates the user settings file located at:
AppData/Roaming/Code/User/settings.json
It changes the value of:
workbench.colorTheme
As with Windows Terminal, the file is only rewritten when the configured theme differs from the current setting.
Scheduled Switching
When run without command-line options, the script works as a scheduler.
It first applies the correct theme for the current time. Then it calculates the next switch time based on the configured day and night start times.
For example, if the configuration says:
- day starts at
08:00 - night starts at
20:00
then the script will use the day theme from 08:00 to 19:59, and the night theme from 20:00 until the next morning.
The script uses wait_until() from sleep_absolute to wait until the exact next switching point.
Manual Override
The script also supports manual switching through command-line arguments.
The --day option immediately applies the day theme and exits. The --night option immediately applies the night theme and exits.
These options are useful when you want to temporarily override the schedule or test your configuration.
Practical Use Cases
This script is useful for:
- reducing eye strain during long development sessions
- keeping Windows, Terminal, and VS Code visually consistent
- improving readability based on ambient lighting
- removing the need to manually switch themes every day
- maintaining a more comfortable development environment
Summary
This script provides a simple but practical way to automate theme switching on Windows.
By combining Windows theme settings, Windows Terminal configuration, and Visual Studio Code settings, it keeps the entire development environment aligned with the time of day.
Once configured, it can run continuously and quietly maintain the preferred visual setup, switching between day and night themes automatically.
import argparse
import asyncio
import ctypes
import json
import logging
import os
from ctypes import wintypes
from datetime import datetime, timedelta
from pathlib import Path
import winreg
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib
from sleep_absolute import wait_until
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")
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__)
def parse_hhmm(value: str) -> tuple[int, int]:
try:
hour_text, minute_text = value.split(":", 1)
hour = int(hour_text)
minute = int(minute_text)
except ValueError as exc:
raise ValueError(f"Invalid time format: {value!r}. Expected HH:MM.") from exc
if not (0 <= hour <= 23):
raise ValueError(f"Invalid hour in time: {value!r}")
if not (0 <= minute <= 59):
raise ValueError(f"Invalid minute in time: {value!r}")
return hour, minute
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)
day_start_hour, day_start_minute = parse_hhmm(
config["schedule"]["day_start"]
)
night_start_hour, night_start_minute = parse_hhmm(
config["schedule"]["night_start"]
)
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"],
"day_start_hour": day_start_hour,
"day_start_minute": day_start_minute,
"night_start_hour": night_start_hour,
"night_start_minute": night_start_minute,
"day_start_minutes": day_start_hour * 60 + day_start_minute,
"night_start_minutes": night_start_hour * 60 + night_start_minute,
}
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 desired_theme_for_now(now: datetime | None = None) -> bool:
now = now or datetime.now()
minutes = now.hour * 60 + now.minute
is_light = (
CONFIG["day_start_minutes"]
<= minutes
< CONFIG["night_start_minutes"]
)
logger.debug(
"Evaluated desired Windows theme at %s -> %s",
now.isoformat(),
"light" if is_light else "dark",
)
return is_light
def desired_terminal_color_scheme_for_now(now: datetime | None = None) -> str:
now = now or datetime.now()
scheme = (
CONFIG["day_terminal_color_scheme"]
if desired_theme_for_now(now)
else CONFIG["night_terminal_color_scheme"]
)
logger.debug(
"Evaluated desired Terminal color scheme at %s -> %s",
now.isoformat(),
scheme,
)
return scheme
def desired_vscode_theme_for_now(now: datetime | None = None) -> str:
now = now or datetime.now()
theme = (
CONFIG["day_vscode_theme"]
if desired_theme_for_now(now)
else CONFIG["night_vscode_theme"]
)
logger.debug(
"Evaluated desired VS Code theme at %s -> %s",
now.isoformat(),
theme,
)
return theme
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,
) -> bool:
"""
時刻判定を使わず、指定されたテーマを即時適用する。
CLI の --day / --night から使う。
"""
current_is_light = bool(get_current_theme())
if current_is_light != light:
set_theme(light)
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",
"day/light" if light else "night/dark",
terminal_color_scheme,
vscode_theme,
)
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"],
)
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"],
)
def apply_theme_for_now() -> bool:
now = datetime.now()
should_use_light_theme = desired_theme_for_now(now)
current_is_light = bool(get_current_theme())
if current_is_light != should_use_light_theme:
set_theme(should_use_light_theme)
desired_terminal_scheme = desired_terminal_color_scheme_for_now(now)
terminal_changed = set_windows_terminal_color_scheme(desired_terminal_scheme)
desired_vscode_theme = desired_vscode_theme_for_now(now)
vscode_changed = set_vscode_color_theme(desired_vscode_theme)
if terminal_changed or vscode_changed:
try:
notify_environment_changed()
except OSError:
logger.exception("Failed to broadcast environment change notification")
return should_use_light_theme
def next_switch_datetime(now: datetime | None = None) -> datetime:
now = now or datetime.now()
today_day_start = now.replace(
hour=CONFIG["day_start_hour"],
minute=CONFIG["day_start_minute"],
second=0,
microsecond=0,
)
today_night_start = now.replace(
hour=CONFIG["night_start_hour"],
minute=CONFIG["night_start_minute"],
second=0,
microsecond=0,
)
if now < today_day_start:
next_dt = today_day_start
elif now < today_night_start:
next_dt = today_night_start
else:
next_dt = today_day_start + timedelta(days=1)
logger.debug("Next scheduled switch time: %s", next_dt.isoformat())
return next_dt
async def scheduler() -> None:
apply_theme_for_now()
while True:
next_dt = next_switch_datetime()
await wait_until(next_dt)
apply_theme_for_now()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Switch Windows, Windows Terminal, and VS Code themes by schedule."
)
mode_group = parser.add_mutually_exclusive_group()
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
asyncio.run(scheduler())
if __name__ == "__main__":
main()
Top comments (0)