Issues with macOS 'Follow System' dark mode detection and theme change crashes on destroyed temporary webviews (FindDupesWebView)

Hello Anki team and community,

I have encountered and successfully investigated two independent but related theme-rendering bugs in Anki Desktop on macOS. Below are the detailed reports, including symptoms, root causes, what I tried, and my proposed solutions (which I have verified locally and am ready to submit as a PR).

Issue 1: ‘Follow System’ night mode detection fails on certain macOS environments

1. What is the problem?

When the theme preference is set to Follow System, Anki fails to switch to night mode even when macOS system appearance is set to Dark. It gets stuck in light mode.

2. Steps to reproduce:

  1. Open Anki and set the theme preference to Follow System (System Default).
  2. Set macOS system appearance to Dark in System Settings.
  3. Restart Anki or trigger an appearance update. Anki remains rendering in Light mode.

3. Root Cause Analysis & What I tried:

I added print debug statements in ThemeManager._determine_night_mode() to check why macOS dark mode resolved to False.
The problem lies in get_macos_dark_mode() inside aqt/theme.py:

def get_macos_dark_mode() -> bool:
    from aqt._macos_helper import macos_helper
    if not macos_helper:
        return False
    return macos_helper.system_is_dark()

On some macOS systems, macos_helper.system_is_dark() returns False even when the system is actually in Dark mode. Because Anki immediately returns this value, the existing robust fallback detection commands (using shell defaults read -g AppleInterfaceStyle or AppleScript query via osascript) never run.

Additionally, even if Night Mode is forced, the native macOS widget styling sometimes remains visually light unless custom Fusion styling is forced to render the dark palette properly.

4. Proposed Solution:

Update get_macos_dark_mode() to fall back to the command-line/AppleScript methods if macos_helper.system_is_dark() returns False.
Also, force the Fusion style on macOS when night_mode is active, avoiding visual styling mismatch.


Issue 2: Theme updates cause RuntimeError on destroyed temporary webviews

1. What is the problem?

If a temporary webview (such as FindDupesWebView, StatsWebView, or EmptyCardsWebView) is opened, closed, and then a theme change occurs (e.g. system toggles appearance or preference updates), Anki crashes with a RuntimeError.

2. Steps to reproduce:

  1. Open a dialog that initializes a temporary webview (for example, go to Tools → Find Duplicates).
  2. Close the Find Duplicates dialog.
  3. Toggle the system appearance or update the theme preference in Anki.
  4. Anki raises a traceback or crashes.

3. Exact Error Message:

RuntimeError: wrapped C/C++ object of type FindDupesWebView has been deleted

4. Root Cause Analysis & What I tried:

When a dialog containing AnkiWebView is closed and destroyed, the underlying Qt C++ object is deleted. However:

  • The Python object may still receive theme-change callbacks from the gui_hooks.theme_did_change list if hook cleanup has not fully settled.
  • When on_theme_did_change() runs, it attempts to call self.page() and access page settings, which immediately raises a RuntimeError since the C++ object is dead.
  • Hook removal in cleanup() can also trigger a ValueError if the callback was already removed.

5. Proposed Solution:

  1. Add if sip.isdeleted(self): return guard at the beginning of on_theme_did_change().
  2. Wrap self.page() and page settings accesses in try/except RuntimeError blocks to safely ignore deleted Qt objects.
  3. Use tolerant try-except blocks during hook removal in cleanup() to prevent failures.

I have prepared these changes locally in the desktop repository, verified their behavior, and they resolve both issues cleanly without introducing regressions. I am happy to open a Pull Request for this!

Thank you for your time and all your volunteer work on Anki!

Best regards,
Allen

Can you be more specific here? In which systems does this fail? system_is_dark() already reads AppleInterfaceStyle.

I think the correct solution here is to add missing web.cleanup() calls to the dialogs. Your proposed solution might mask memory leaks: Fix memory leak in AnkiWebView by hikaru-y · Pull Request #1510 · ankitects/anki · GitHub