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…
