Unfortunately, I do not possess programming skills, so i asked claude for python extension. it created this:
“”"
Auto Theme Switcher for Anki
Automatically switches between dark and light mode based on time of day.
Configurable via Tools menu or config.json.
Styled to match Anki’s native Catppuccin-based color tokens.
“”"
import json
import os
from datetime import datetime
from aqt import mw, gui_hooks
from aqt.theme import theme_manager
from aqt.qt import (
QFontDatabase,
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QSpinBox,
QCheckBox, QPushButton, QFrame, QTimer, Qt
)
─── FONT LOADER ──────────────────────────────────────────────────────────────
FONT_PATH = os.path.join(os.path.dirname(file), ‘Lexend.ttf’)
FONT_URL = ‘https://github.com/googlefonts/lexend/raw/main/fonts/lexend/ttf/Lexend-Regular.ttf’
_lexend_loaded = False
def ensure_lexend():
global _lexend_loaded
if _lexend_loaded:
return
import urllib.request
if not os.path.exists(FONT_PATH):
try:
urllib.request.urlretrieve(FONT_URL, FONT_PATH)
except Exception:
return
QFontDatabase.addApplicationFont(FONT_PATH)
_lexend_loaded = True
─── CONFIG ───────────────────────────────────────────────────────────────────
ADDON_DIR = os.path.dirname(file)
CONFIG_PATH = os.path.join(ADDON_DIR, “config.json”)
DEFAULT_CONFIG = {
“enabled”: True,
“dark_start”: 19,
“light_start”: 7,
“check_interval_seconds”: 60
}
def load_config() → dict:
if os.path.exists(CONFIG_PATH):
try:
with open(CONFIG_PATH, “r”) as f:
data = json.load(f)
return {**DEFAULT_CONFIG, **data}
except Exception:
pass
return DEFAULT_CONFIG.copy()
def save_config(cfg: dict) → None:
with open(CONFIG_PATH, “w”) as f:
json.dump(cfg, f, indent=2)
─── THEME LOGIC ──────────────────────────────────────────────────────────────
_timer = None
def should_be_dark(cfg: dict) → bool:
“”"
Returns True if the current hour falls within the dark window.
Normal case (dark_start > light_start, e.g. light=7, dark=19):
dark when hour >= 19 OR hour < 7 (i.e. 19:00–06:59)
light when 7 <= hour < 19 (i.e. 07:00–18:59)
Inverted case (dark_start < light_start, e.g. dark=7, light=19):
dark when 7 <= hour < 19
light otherwise
"""
hour = datetime.now().hour
dark_start = cfg["dark_start"]
light_start = cfg["light_start"]
if dark_start > light_start:
# Standard: dark at night, light during the day
return hour >= dark_start or hour < light_start
else:
# Inverted: dark during the day (unusual but valid)
return dark_start <= hour < light_start
def _set_theme(night_mode: bool) → None:
“”"
Apply theme app-wide using the correct API for each Anki version.
Modern path (Anki 2.1.50+ / 25.x): mw.pm.set_theme(BuiltinTheme)
— writes to profile and triggers Anki's own full redraw chain.
Legacy path (pre-2.1.50): theme_manager.set_night_mode()
apply_style() is intentionally never called directly — its signature
and availability has changed repeatedly across Anki versions.
"""
if not mw:
return
# ── Modern path ────────────────────────────────────────────────────────
try:
from aqt.theme import BuiltinTheme
target = BuiltinTheme.DARK if night_mode else BuiltinTheme.LIGHT
if mw.pm.theme() == target:
return # already correct, nothing to do
mw.pm.set_theme(target)
mw.reset()
if hasattr(mw, "toolbar") and mw.toolbar:
mw.toolbar.draw()
if hasattr(mw, "web") and mw.web:
mw.web.reload()
return
except (ImportError, AttributeError):
pass
# ── Legacy path ────────────────────────────────────────────────────────
if theme_manager.night_mode != night_mode:
theme_manager.set_night_mode(night_mode)
mw.reset()
if hasattr(mw, "toolbar") and mw.toolbar:
mw.toolbar.draw()
if hasattr(mw, "web") and mw.web:
mw.web.reload()
def apply_theme() → None:
cfg = load_config()
if not cfg.get(“enabled”, True):
return
_set_theme(should_be_dark(cfg))
def restart_timer(cfg: dict) → None:
global _timer
if _timer is not None:
_timer.stop()
if mw and cfg.get(“enabled”, True):
interval_ms = cfg.get(“check_interval_seconds”, 60) * 1000
_timer = QTimer(mw)
_timer.timeout.connect(apply_theme)
_timer.start(interval_ms)
─── STYLE HELPERS ────────────────────────────────────────────────────────────
def get_stylesheet() → str:
night = theme_manager.night_mode
i = 2 if night else 1
t = {
"canvas": ("#eff1f5", "#24273a")[i - 1],
"canvas_elevated": ("#e6e9ef", "#1e2030")[i - 1],
"canvas_inset": ("#dce0e8", "#181926")[i - 1],
"fg": ("#4c4f69", "#cad3f5")[i - 1],
"fg_faint": ("#7c7f93", "#939ab7")[i - 1],
"fg_subtle": ("#5c5f77", "#b8c0e0")[i - 1],
"border": ("#bcc0cc", "#494d64")[i - 1],
"border_subtle": ("#ccd0da", "#363a4f")[i - 1],
"border_focus": ("#dc8a78", "#f4dbd6")[i - 1],
"btn_bg": ("#ccd0da", "#363a4f")[i - 1],
"btn_primary_bg": "#2f67e1",
"btn_primary_end": "#2544a8",
}
return f"""
QDialog {{ background-color: {t['canvas']}; color: {t['fg']}; }}
QLabel {{ color: {t['fg']}; font-family: 'Lexend', sans-serif; font-size: 13px; }}
QLabel#title {{ font-size: 16px; font-weight: 600; }}
QLabel#subtitle {{ font-size: 11px; color: {t['fg_faint']}; }}
QLabel#field {{ font-size: 13px; color: {t['fg_subtle']}; min-width: 170px; }}
QLabel#preview {{ font-size: 11px; color: {t['fg_faint']}; font-style: italic;
background-color: {t['canvas_inset']}; border: 1px solid {t['border_subtle']};
border-radius: 6px; padding: 6px 10px; }}
QSpinBox {{ background-color: {t['canvas_elevated']}; color: {t['fg']};
border: 1px solid {t['border']}; border-radius: 5px; padding: 5px 8px; }}
QSpinBox:focus {{ border: 1px solid {t['border_focus']}; }}
QCheckBox {{ color: {t['fg']}; font-size: 13px; spacing: 8px; }}
QPushButton {{ background-color: {t['btn_bg']}; color: {t['fg']};
border: 1px solid {t['border']}; border-radius: 5px; padding: 7px 18px; }}
QPushButton#save {{ background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 {t['btn_primary_bg']}, stop:1 {t['btn_primary_end']});
color: #ffffff; border: none; font-weight: 600; }}
QPushButton#cancel {{ background-color: transparent; color: {t['fg_faint']};
border: 1px solid {t['border_subtle']}; }}
QFrame#divider {{ background-color: {t['border_subtle']}; max-height: 1px; }}
"""
─── SETTINGS DIALOG ──────────────────────────────────────────────────────────
class ThemeSettingsDialog(QDialog):
def init(self, parent=None):
super().init(parent)
self.cfg = load_config()
self.setWindowTitle(“Auto Theme Switcher”)
self.setFixedWidth(420)
self.setModal(True)
self._build_ui()
# Re-skin dialog if Anki’s theme changes externally while it’s open
gui_hooks.theme_did_change.append(self._on_theme_changed)
def _on_theme_changed(self) -> None:
self.setStyleSheet(get_stylesheet())
def closeEvent(self, event):
try:
gui_hooks.theme_did_change.remove(self._on_theme_changed)
except ValueError:
pass
super().closeEvent(event)
def _build_ui(self):
ensure_lexend()
self.setStyleSheet(get_stylesheet())
root = QVBoxLayout(self)
root.setContentsMargins(24, 24, 24, 20)
title = QLabel("🌗 Auto Theme Switcher")
title.setObjectName("title")
root.addWidget(title)
subtitle = QLabel("Switches Anki between dark and light mode on a schedule.")
subtitle.setObjectName("subtitle")
root.addWidget(subtitle)
root.addSpacing(10)
self.enabled_cb = QCheckBox("Enable automatic theme switching")
self.enabled_cb.setChecked(self.cfg.get("enabled", True))
self.enabled_cb.stateChanged.connect(self._update_preview)
root.addWidget(self.enabled_cb)
root.addSpacing(10)
self.light_spin = QSpinBox()
self.dark_spin = QSpinBox()
self.interval_spin = QSpinBox()
root.addLayout(self._spin_row("☀️ Light mode starts at", self.light_spin, 0, 23, self.cfg.get("light_start", 7), suffix=":00"))
root.addLayout(self._spin_row("🌙 Dark mode starts at", self.dark_spin, 0, 23, self.cfg.get("dark_start", 19), suffix=":00"))
root.addLayout(self._spin_row("🔁 Check every (seconds)", self.interval_spin, 10, 3600, self.cfg.get("check_interval_seconds", 60), step=10))
self.preview_lbl = QLabel()
self.preview_lbl.setObjectName("preview")
self.preview_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
root.addWidget(self.preview_lbl)
self._update_preview()
btn_row = QHBoxLayout()
btn_row.addStretch()
cancel_btn = QPushButton("Cancel")
cancel_btn.setObjectName("cancel")
cancel_btn.clicked.connect(self.reject)
save_btn = QPushButton("Save && Apply")
save_btn.setObjectName("save")
save_btn.clicked.connect(self._save)
btn_row.addWidget(cancel_btn)
btn_row.addWidget(save_btn)
root.addLayout(btn_row)
def _spin_row(self, text, spin, min_v, max_v, val, step=1, suffix=""):
row = QHBoxLayout()
lbl = QLabel(text)
spin.setRange(min_v, max_v)
spin.setValue(val)
spin.setSingleStep(step)
spin.setSuffix(suffix)
spin.valueChanged.connect(self._update_preview)
row.addWidget(lbl)
row.addStretch()
row.addWidget(spin)
return row
def _update_preview(self):
if not self.enabled_cb.isChecked():
self.preview_lbl.setText("Auto switching is disabled.")
return
now = datetime.now().hour
is_dark = should_be_dark({
"dark_start": self.dark_spin.value(),
"light_start": self.light_spin.value()
})
mode = "🌙 Dark" if is_dark else "☀️ Light"
self.preview_lbl.setText(f"Right now ({now}:xx) → {mode} mode")
def _save(self):
light = self.light_spin.value()
dark = self.dark_spin.value()
if light == dark:
from aqt.utils import showWarning
showWarning("Light and dark start times cannot be the same hour.")
return
self.cfg.update({
"enabled": self.enabled_cb.isChecked(),
"dark_start": dark,
"light_start": light,
"check_interval_seconds": self.interval_spin.value()
})
save_config(self.cfg)
restart_timer(self.cfg)
apply_theme()
self.accept()
─── MENU & STARTUP ───────────────────────────────────────────────────────────
def open_settings():
ThemeSettingsDialog(mw).exec()
def toggle_theme():
“”“Immediately flip the theme, bypassing the schedule.”“”
_set_theme(not theme_manager.night_mode)
def on_main_window_init():
if mw:
a = mw.form.menuTools.addAction(“🌗 Auto Theme Switcher…”)
a.triggered.connect(open_settings)
t = mw.form.menuTools.addAction(“🔀 Toggle Theme Now”)
t.triggered.connect(toggle_theme)
cfg = load_config()
apply_theme()
restart_timer(cfg)
gui_hooks.main_window_did_init.append(on_main_window_init)
This one worked perfectly till after some time it automatically switched again to dark mode with some elements being/appearing as if they are in white mode.
even at morning when its supposed to be in light mode, it wasnt fully application wide. only the main UI was white. not the menu bars which were still dark.
My Operating system doesnt change light automatically so i cant choose the “System” theme in anki preferences. so i was seeking an extension.
Thank you


