惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
AI
AI
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
博客园 - 三生石上(FineUI控件)
Google Online Security Blog
Google Online Security Blog
N
News and Events Feed by Topic
Microsoft Security Blog
Microsoft Security Blog
AWS News Blog
AWS News Blog
S
Securelist
Schneier on Security
Schneier on Security
MyScale Blog
MyScale Blog
W
WeLiveSecurity
Hacker News: Ask HN
Hacker News: Ask HN
爱范儿
爱范儿
阮一峰的网络日志
阮一峰的网络日志
Project Zero
Project Zero
Recent Commits to openclaw:main
Recent Commits to openclaw:main
P
Proofpoint News Feed
I
Intezer
人人都是产品经理
人人都是产品经理
The Register - Security
The Register - Security
T
The Exploit Database - CXSecurity.com
宝玉的分享
宝玉的分享
L
LINUX DO - 热门话题
月光博客
月光博客
C
CERT Recently Published Vulnerability Notes
F
Fortinet All Blogs
GbyAI
GbyAI
博客园 - 聂微东
F
Full Disclosure
L
LINUX DO - 最新话题
T
The Blog of Author Tim Ferriss
Engineering at Meta
Engineering at Meta
IT之家
IT之家
Scott Helme
Scott Helme
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Security Affairs
I
InfoQ
The Hacker News
The Hacker News
雷峰网
雷峰网
Hacker News - Newest:
Hacker News - Newest: "LLM"
V2EX - 技术
V2EX - 技术
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
SecWiki News
SecWiki News
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
大猫的无限游戏
大猫的无限游戏
博客园_首页
B
Blog RSS Feed
S
Secure Thoughts
A
About on SuperTechFans

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
A Python Script for Automatically Switching Themes Based on the Time of Day
vast cow · 2026-05-13 · via DEV Community

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:

  1. It reads theme settings and schedule information from config.toml.
  2. It checks the current time.
  3. It determines whether the system should be in day mode or night mode.
  4. It applies the corresponding Windows, Windows Terminal, and VS Code themes.
  5. It waits until the next scheduled switch time.
  6. 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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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:

  • AppsUseLightTheme
  • SystemUsesLightTheme

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

It changes the value of:

workbench.colorTheme

Enter fullscreen mode Exit fullscreen mode

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

import aiofiles

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")
CONFIG: dict[str, object] = {}


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


async def load_config(path: Path = CONFIG_PATH) -> dict[str, object]:
    if not await asyncio.to_thread(path.is_file):
        raise FileNotFoundError(f"Config file was not found: {path}")

    async with aiofiles.open(path, "r", encoding="utf-8") as f:
        text = await f.read()

    config = tomllib.loads(text)

    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,
    }


def _get_windows_terminal_settings_path_sync() -> 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.")


async def get_windows_terminal_settings_path() -> Path:
    return await asyncio.to_thread(_get_windows_terminal_settings_path_sync)


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_sync() -> 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


async def get_current_theme() -> int:
    return await asyncio.to_thread(_get_current_theme_sync)


def _send_setting_change_sync(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)


async def send_setting_change(param: str) -> None:
    await asyncio.to_thread(_send_setting_change_sync, param)


async def notify_theme_changed() -> None:
    logger.debug("Notifying system that theme has changed")
    await send_setting_change("ImmersiveColorSet")


async def notify_environment_changed() -> None:
    logger.debug("Notifying system that environment/settings may have changed")
    await send_setting_change("Environment")


def _set_theme_registry_sync(light: bool) -> None:
    value = 1 if light else 0
    logger.debug("Setting Windows theme registry values 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)


async def set_theme(light: bool) -> None:
    await asyncio.to_thread(_set_theme_registry_sync, light)
    await 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 = (
        int(CONFIG["day_start_minutes"])
        <= minutes
        < int(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 = (
        str(CONFIG["day_terminal_color_scheme"])
        if desired_theme_for_now(now)
        else str(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 = (
        str(CONFIG["day_vscode_theme"])
        if desired_theme_for_now(now)
        else str(CONFIG["night_vscode_theme"])
    )

    logger.debug(
        "Evaluated desired VS Code theme at %s -> %s",
        now.isoformat(),
        theme,
    )
    return theme


async def read_json_file(path: Path) -> dict:
    try:
        async with aiofiles.open(path, "r", encoding="utf-8") as f:
            text = await f.read()
    except FileNotFoundError:
        return {}

    if not text.strip():
        return {}

    return json.loads(text)


async 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
    try:
        async with aiofiles.open(path, "r", encoding="utf-8") as f:
            old_text = await f.read()
    except FileNotFoundError:
        pass

    if old_text == new_text:
        logger.debug("%s already matches desired content; no update needed", description)
        return False

    await asyncio.to_thread(path.parent.mkdir, parents=True, exist_ok=True)
    async with aiofiles.open(path, "w", encoding="utf-8", newline="\n") as f:
        await f.write(new_text)

    logger.debug("%s updated: %s", description, path)
    return True


async def set_windows_terminal_color_scheme(color_scheme: str) -> bool:
    settings_path = await get_windows_terminal_settings_path()
    logger.debug("Loading Windows Terminal settings from: %s", settings_path)

    settings = await read_json_file(settings_path)

    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 await write_json_if_changed(
        settings_path,
        settings,
        "Windows Terminal settings.json",
    )


async def set_vscode_color_theme(theme_name: str) -> bool:
    settings_path = get_vscode_settings_path()
    settings = await read_json_file(settings_path)

    current_theme = settings.get("workbench.colorTheme")

    if current_theme == theme_name:
        return False

    settings["workbench.colorTheme"] = theme_name
    return await write_json_if_changed(settings_path, settings, "VS Code settings.json")


async def apply_explicit_theme(
    *,
    light: bool,
    terminal_color_scheme: str,
    vscode_theme: str,
) -> bool:
    current_theme_task = asyncio.create_task(get_current_theme())
    terminal_task = asyncio.create_task(
        set_windows_terminal_color_scheme(terminal_color_scheme)
    )
    vscode_task = asyncio.create_task(set_vscode_color_theme(vscode_theme))

    current_is_light = bool(await current_theme_task)

    theme_task: asyncio.Task[None] | None = None
    if current_is_light != light:
        theme_task = asyncio.create_task(set_theme(light))

    terminal_changed = await terminal_task
    vscode_changed = await vscode_task

    if theme_task is not None:
        await theme_task

    if terminal_changed or vscode_changed:
        try:
            await 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


async def apply_day_theme() -> bool:
    return await apply_explicit_theme(
        light=True,
        terminal_color_scheme=str(CONFIG["day_terminal_color_scheme"]),
        vscode_theme=str(CONFIG["day_vscode_theme"]),
    )


async def apply_night_theme() -> bool:
    return await apply_explicit_theme(
        light=False,
        terminal_color_scheme=str(CONFIG["night_terminal_color_scheme"]),
        vscode_theme=str(CONFIG["night_vscode_theme"]),
    )


async def apply_theme_for_now() -> bool:
    now = datetime.now()

    should_use_light_theme = desired_theme_for_now(now)
    desired_terminal_scheme = desired_terminal_color_scheme_for_now(now)
    desired_vscode_theme = desired_vscode_theme_for_now(now)

    current_theme_task = asyncio.create_task(get_current_theme())
    terminal_task = asyncio.create_task(
        set_windows_terminal_color_scheme(desired_terminal_scheme)
    )
    vscode_task = asyncio.create_task(set_vscode_color_theme(desired_vscode_theme))

    current_is_light = bool(await current_theme_task)

    theme_task: asyncio.Task[None] | None = None
    if current_is_light != should_use_light_theme:
        theme_task = asyncio.create_task(set_theme(should_use_light_theme))

    terminal_changed = await terminal_task
    vscode_changed = await vscode_task

    if theme_task is not None:
        await theme_task

    if terminal_changed or vscode_changed:
        try:
            await 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=int(CONFIG["day_start_hour"]),
        minute=int(CONFIG["day_start_minute"]),
        second=0,
        microsecond=0,
    )

    today_night_start = now.replace(
        hour=int(CONFIG["night_start_hour"]),
        minute=int(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:
    await apply_theme_for_now()

    while True:
        next_dt = next_switch_datetime()
        await wait_until(next_dt)
        await 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()


async def async_main() -> None:
    global CONFIG
    CONFIG = await load_config()

    args = parse_args()

    if args.day:
        await apply_day_theme()
        return

    if args.night:
        await apply_night_theme()
        return

    await scheduler()


def main() -> None:
    asyncio.run(async_main())


if __name__ == "__main__":
    main()

Enter fullscreen mode Exit fullscreen mode