Hello, How do i create an extension that changes the light and dark themes automatically based on time? all previous attempts have failed

Try this code…

“”"
Auto Theme Switcher for Anki
Monitors the system clock in a background thread and switches themes automatically.
“”"

import json
import os
import time
import threading

from aqt import mw, gui_hooks
from aqt.qt import (
QShortcut, QKeySequence, QDialog, QVBoxLayout, QHBoxLayout,
QLabel, QTimeEdit, QCheckBox, QPushButton, QGroupBox,
QFormLayout, QTimer, QTime, QAction, Qt, QObject, pyqtSignal,
QWidget
)
from aqt.theme import theme_manager, Theme
from aqt.utils import tooltip

── Config ────────────────────────────────────────────────────────────────────

CONFIG_FILE = os.path.join(os.path.dirname(file), “config.json”)

DEFAULT_CONFIG = {
“auto_switch_enabled”: False,
“dark_mode_start”: “20:00:00”,
“dark_mode_end”:   “07:00:00”,
}

FMT = “HH:mm:ss”

def load_config():
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, “r”, encoding=“utf-8”) as f:
cfg = json.load(f)
for k, v in DEFAULT_CONFIG.items():
cfg.setdefault(k, v)
for key in (“dark_mode_start”, “dark_mode_end”):
if cfg[key].count(“:”) == 1:
cfg[key] += “:00”
return cfg
except Exception:
pass
return DEFAULT_CONFIG.copy()

def save_config(cfg):
with open(CONFIG_FILE, “w”, encoding=“utf-8”) as f:
json.dump(cfg, f, indent=2, ensure_ascii=False)

config = load_config()

── Time logic ────────────────────────────────────────────────────────────────

def _to_secs(t: QTime) → int:
return t.hour() * 3600 + t.minute() * 60 + t.second()

def is_dark_now(cfg) → bool:
now   = _to_secs(QTime.currentTime())
start = _to_secs(QTime.fromString(cfg[“dark_mode_start”], FMT))
end   = _to_secs(QTime.fromString(cfg[“dark_mode_end”],   FMT))
if start == end:
return False
if start < end:
return start <= now < end
return now >= start or now < end

── Signal bridge (background thread → main thread) ───────────────────────────

class _Bridge(QObject):
switch_theme = pyqtSignal(bool)

def __init__(self):
    super().__init__()
    self.switch_theme.connect(self._apply)

def _apply(self, dark: bool):
    if theme_manager.night_mode == dark:
        return
    mw.set_theme(Theme.DARK if dark else Theme.LIGHT)
    mw.reset()

_bridge = _Bridge()

── Background watcher thread ─────────────────────────────────────────────────

_stop_event = threading.Event()

def _watcher():
last_state = None
while not _stop_event.is_set():
if config[“auto_switch_enabled”]:
want_dark = is_dark_now(config)
if want_dark != last_state:
last_state = want_dark
_bridge.switch_theme.emit(want_dark)
else:
last_state = None
time.sleep(1)

_watcher_thread = threading.Thread(target=_watcher, daemon=True)

── Manual toggle ─────────────────────────────────────────────────────────────

def toggle_theme_manual():
mw.set_theme(Theme.LIGHT if theme_manager.night_mode else Theme.DARK)
mw.reset()

── Settings window (non-modal) ───────────────────────────────────────────────

class ThemeSchedulerDialog(QDialog):
def init(self, parent=None):
super().init(parent)
self.setWindowTitle(“Auto Theme Switcher – Settings”)
self.setMinimumWidth(420)

    # Non-modal + normal window with minimize/maximize buttons
    self.setModal(False)
    self.setWindowFlags(
        Qt.WindowType.Window |
        Qt.WindowType.WindowMinimizeButtonHint |
        Qt.WindowType.WindowMaximizeButtonHint |
        Qt.WindowType.WindowCloseButtonHint
    )

    self._build_ui()
    self._load()

    self._dlg_timer = QTimer(self)
    self._dlg_timer.setInterval(1000)
    self._dlg_timer.timeout.connect(self._refresh_status)
    self._dlg_timer.start()

def _build_ui(self):
    root = QVBoxLayout(self)
    root.setSpacing(16)
    root.setContentsMargins(24, 24, 24, 24)

    title = QLabel("🌓  Automatic Theme Switching")
    title.setStyleSheet("font-size: 15px; font-weight: bold;")
    root.addWidget(title)

    subtitle = QLabel(
        "Set the time window during which Anki should use dark mode.\n"
        "Outside that window, light mode will be applied automatically."
    )
    subtitle.setWordWrap(True)
    subtitle.setStyleSheet("color: #666; font-size: 12px;")
    root.addWidget(subtitle)

    self.chk_auto = QCheckBox("Enable automatic theme switching by schedule")
    self.chk_auto.setStyleSheet("font-size: 13px;")
    root.addWidget(self.chk_auto)

    self.grp = QGroupBox("Dark Mode Schedule")
    self.grp.setStyleSheet(
        "QGroupBox { font-weight: bold; font-size: 13px; margin-top: 8px; }"
        "QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 4px; }"
    )
    form = QFormLayout(self.grp)
    form.setSpacing(12)

    self.time_start = QTimeEdit()
    self.time_start.setDisplayFormat("HH:mm:ss")
    self.time_start.setToolTip("Time when dark mode begins")
    self.time_start.timeChanged.connect(self._refresh_status)

    self.time_end = QTimeEdit()
    self.time_end.setDisplayFormat("HH:mm:ss")
    self.time_end.setToolTip("Time when dark mode ends (light mode resumes)")
    self.time_end.timeChanged.connect(self._refresh_status)

    form.addRow("Dark mode start:", self.time_start)
    form.addRow("Dark mode end:",   self.time_end)

    hint = QLabel(
        "💡 Tip: for an overnight window (e.g. 20:00:00 → 07:00:00), the end time\n"
        "   can be earlier than the start — midnight crossover is handled correctly."
    )
    hint.setStyleSheet("color: #888; font-size: 11px;")
    form.addRow(hint)

    root.addWidget(self.grp)

    self.lbl_status = QLabel()
    self.lbl_status.setMinimumHeight(36)
    self.lbl_status.setStyleSheet(
        "font-size: 12px; padding: 8px; border-radius: 6px; background: #f0f0f0;"
    )
    root.addWidget(self.lbl_status)

    btn_row = QHBoxLayout()
    btn_row.addStretch()

    btn_apply = QPushButton("Apply")
    btn_apply.setDefault(True)
    btn_apply.setMinimumWidth(90)
    btn_apply.clicked.connect(self._apply_and_save)

    btn_row.addWidget(btn_apply)
    root.addLayout(btn_row)

    self.chk_auto.toggled.connect(self._on_toggle)

def _load(self):
    self.chk_auto.setChecked(config["auto_switch_enabled"])
    self.time_start.setTime(QTime.fromString(config["dark_mode_start"], FMT))
    self.time_end.setTime(  QTime.fromString(config["dark_mode_end"],   FMT))
    self._on_toggle(config["auto_switch_enabled"])

def _on_toggle(self, checked):
    self.grp.setEnabled(checked)
    self._refresh_status()

def _refresh_status(self):
    now_str = QTime.currentTime().toString("HH:mm:ss")
    if not self.chk_auto.isChecked():
        self.lbl_status.setText(f"⏸  Automatic switching disabled.  [{now_str}]")
        self.lbl_status.setStyleSheet(
            "font-size: 12px; padding: 8px; border-radius: 6px; background: #f0f0f0;"
        )
        return
    tmp = {
        "dark_mode_start": self.time_start.time().toString(FMT),
        "dark_mode_end":   self.time_end.time().toString(FMT),
    }
    dark = is_dark_now(tmp)
    if dark:
        self.lbl_status.setText(f"🌙  Now ({now_str}) → DARK mode is active.")
        self.lbl_status.setStyleSheet(
            "font-size: 12px; padding: 8px; border-radius: 6px;"
            "background: #1e1e2e; color: #cdd6f4;"
        )
    else:
        self.lbl_status.setText(f"☀️   Now ({now_str}) → LIGHT mode is active.")
        self.lbl_status.setStyleSheet(
            "font-size: 12px; padding: 8px; border-radius: 6px;"
            "background: #fffbe6; color: #5c4a00;"
        )

def _apply_and_save(self):
    config["auto_switch_enabled"] = self.chk_auto.isChecked()
    config["dark_mode_start"] = self.time_start.time().toString(FMT)
    config["dark_mode_end"]   = self.time_end.time().toString(FMT)
    save_config(config)
    _bridge.switch_theme.emit(is_dark_now(config))
    self._refresh_status()
    tooltip("Settings saved!", parent=self)

── Keep a single instance ────────────────────────────────────────────────────

_settings_window = None

def open_settings():
global _settings_window
if _settings_window is None or not _settings_window.isVisible():
_settings_window = ThemeSchedulerDialog(mw)
_settings_window.show()
else:
settings_window.raise()
_settings_window.activateWindow()

── Menu ──────────────────────────────────────────────────────────────────────

def setup_menu():
action = QAction(“Auto Theme Switcher…”, mw)
action.triggered.connect(open_settings)
mw.form.menuTools.addAction(action)

── Init ──────────────────────────────────────────────────────────────────────

def on_profile_loaded():
if not _watcher_thread.is_alive():
_watcher_thread.start()
if config[“auto_switch_enabled”]:
_bridge.switch_theme.emit(is_dark_now(config))

gui_hooks.profile_did_open.append(on_profile_loaded)
QShortcut(QKeySequence(“Ctrl+Shift+T”), mw).activated.connect(toggle_theme_manual)
setup_menu()

You can use the shortcut Ctrl+Shift+T to change the theme too.
Note: this was based on the addon I made below…

https://ankiweb.net/shared/info/2002470555