Skip to content

Commit 5d4d962

Browse files
committed
Fix dark mode toolbar icon not displaying correctly on Safari launch
Problem: The toolbar icon defaulted to light mode on fresh Safari launch because background scripts don't have access to window.matchMedia for detecting the system color scheme. The cachedIsDark variable was initialized to false and only updated when the popup was opened. Solution: Use native messaging to query the macOS container app for the system appearance on extension startup. The Swift SafariWebExtensionHandler uses NSApp.effectiveAppearance to reliably detect dark mode and returns it to the background script. Additionally, the content script now detects and reports the color scheme on every page load and listens for appearance changes, providing live updates when the user toggles dark/light mode. Detection now happens through multiple mechanisms: 1. On startup: Native messaging queries Swift for NSApp.effectiveAppearance 2. On page load: Content script sends prefers-color-scheme state 3. On appearance change: Content script listener fires 4. On popup open: Popup reports color scheme (fallback) Changes: - Add nativeMessaging permission to manifest.json - Add getAppearance handler to SafariWebExtensionHandler.swift - Add initializeAppearance() in background.js to query native app - Add color scheme detection to content.js - Add debug logging throughout for troubleshooting - Remove dark mode issue from KNOWN_ISSUES.md (now resolved)
1 parent f6976be commit 5d4d962

6 files changed

Lines changed: 111 additions & 20 deletions

File tree

CF Cache Status/CF Cache Status Extension/Resources/background.js

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,41 @@ const ICONS = {
3636
}
3737
};
3838

39-
/** Cached color scheme (updated by popup which has DOM access) */
39+
/** Cached color scheme (updated by native app or popup) */
4040
let cachedIsDark = false;
4141

4242
function isDarkMode() {
4343
return cachedIsDark;
4444
}
4545

46+
// Request appearance from native app on startup
47+
async function initializeAppearance() {
48+
console.log('[CF Cache Status] initializeAppearance() called');
49+
try {
50+
console.log('[CF Cache Status] Sending native message to com.cfcachestatus.CF-Cache-Status');
51+
const response = await browser.runtime.sendNativeMessage(
52+
'com.cfcachestatus.CF-Cache-Status',
53+
{ type: 'getAppearance' }
54+
);
55+
console.log('[CF Cache Status] Native message response:', JSON.stringify(response));
56+
if (response && typeof response.isDark === 'boolean') {
57+
cachedIsDark = response.isDark;
58+
console.log('[CF Cache Status] Set cachedIsDark to:', cachedIsDark);
59+
const icon = getDefaultIcon();
60+
console.log('[CF Cache Status] Setting icon to:', JSON.stringify(icon));
61+
browser.action.setIcon({ path: icon });
62+
} else {
63+
console.log('[CF Cache Status] Invalid response format, isDark not found');
64+
}
65+
} catch (e) {
66+
console.error('[CF Cache Status] Native messaging error:', e.message || e);
67+
}
68+
}
69+
70+
// Initialize appearance on extension load
71+
console.log('[CF Cache Status] Background script loaded at', new Date().toISOString());
72+
initializeAppearance();
73+
4674
function getDefaultIcon() {
4775
return isDarkMode() ? ICONS.dark : ICONS.light;
4876
}
@@ -85,6 +113,7 @@ function updateBadge(tabId, status) {
85113

86114
// Reset to default icon (respects dark/light mode)
87115
const icon = getDefaultIcon();
116+
console.log('[CF Cache Status] updateBadge called, tabId:', tabId, 'status:', status, 'cachedIsDark:', cachedIsDark, 'icon:', icon['16']);
88117
browser.action.setIcon({ path: icon });
89118
browser.action.setBadgeText({ text: badgeText });
90119
browser.action.setBadgeBackgroundColor({ color });
@@ -276,10 +305,15 @@ browser.tabs.onActivated.addListener((activeInfo) => {
276305
// --- Window Events ---
277306

278307
browser.windows.onFocusChanged.addListener(async (windowId) => {
279-
if (windowId === browser.windows.WINDOW_ID_NONE) return;
308+
console.log('[CF Cache Status] onFocusChanged, windowId:', windowId, 'cachedIsDark:', cachedIsDark);
309+
if (windowId === browser.windows.WINDOW_ID_NONE) {
310+
console.log('[CF Cache Status] Window lost focus (WINDOW_ID_NONE), ignoring');
311+
return;
312+
}
280313

281314
try {
282315
const tabs = await browser.tabs.query({ active: true, windowId });
316+
console.log('[CF Cache Status] Active tab:', tabs?.[0]?.id, 'hasData:', tabData.has(tabs?.[0]?.id));
283317
if (tabs?.[0]) {
284318
const data = tabData.get(tabs[0].id);
285319
if (data) {
@@ -293,7 +327,7 @@ browser.windows.onFocusChanged.addListener(async (windowId) => {
293327
}
294328
}
295329
} catch (e) {
296-
// Ignore errors during window switching
330+
console.error('[CF Cache Status] onFocusChanged error:', e);
297331
}
298332
});
299333

@@ -305,9 +339,12 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
305339
}
306340

307341
if (message.type === 'colorScheme') {
342+
console.log('[CF Cache Status] Received colorScheme message, isDark:', message.isDark, 'from:', sender.tab?.url || sender.url || 'popup');
308343
const changed = cachedIsDark !== message.isDark;
309344
cachedIsDark = message.isDark;
345+
console.log('[CF Cache Status] cachedIsDark updated to:', cachedIsDark, 'changed:', changed);
310346
if (changed) {
347+
console.log('[CF Cache Status] Updating all tab icons');
311348
updateAllTabIcons();
312349
}
313350
}

CF Cache Status/CF Cache Status Extension/Resources/manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"webRequest",
3333
"webNavigation",
3434
"activeTab",
35-
"storage"
35+
"storage",
36+
"nativeMessaging"
3637
],
3738
"host_permissions": [
3839
"<all_urls>"

CF Cache Status/CF Cache Status Extension/SafariWebExtensionHandler.swift

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77

88
import SafariServices
99
import os.log
10+
#if os(macOS)
11+
import AppKit
12+
#else
13+
import UIKit
14+
#endif
1015

1116
class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
1217

@@ -29,14 +34,39 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
2934

3035
os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@ (profile: %@)", String(describing: message), profile?.uuidString ?? "none")
3136

37+
// Handle appearance request
38+
var responseData: [String: Any] = [:]
39+
40+
if let msg = message as? [String: Any], let type = msg["type"] as? String {
41+
if type == "getAppearance" {
42+
let isDark = Self.isDarkMode()
43+
responseData = ["isDark": isDark]
44+
os_log(.default, "Returning appearance: isDark = %{public}@", isDark ? "true" : "false")
45+
} else {
46+
responseData = ["echo": message as Any]
47+
}
48+
} else {
49+
responseData = ["echo": message as Any]
50+
}
51+
3252
let response = NSExtensionItem()
3353
if #available(iOS 15.0, macOS 11.0, *) {
34-
response.userInfo = [ SFExtensionMessageKey: [ "echo": message ] ]
54+
response.userInfo = [SFExtensionMessageKey: responseData]
3555
} else {
36-
response.userInfo = [ "message": [ "echo": message ] ]
56+
response.userInfo = ["message": responseData]
3757
}
3858

39-
context.completeRequest(returningItems: [ response ], completionHandler: nil)
59+
context.completeRequest(returningItems: [response], completionHandler: nil)
60+
}
61+
62+
/// Detect current system appearance (dark or light mode)
63+
private static func isDarkMode() -> Bool {
64+
#if os(macOS)
65+
let appearance = NSApp?.effectiveAppearance ?? NSAppearance.currentDrawing()
66+
return appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua
67+
#else
68+
return UITraitCollection.current.userInterfaceStyle == .dark
69+
#endif
4070
}
4171

4272
}

CF Cache Status/CF Cache Status Extension/content.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,32 @@ if (document.readyState === 'complete') {
7474
setTimeout(sendMetrics, 100);
7575
});
7676
}
77+
78+
// =============================================================================
79+
// Color Scheme Detection
80+
// =============================================================================
81+
82+
/**
83+
* Sends the current color scheme to the background script.
84+
* Content scripts have DOM access, so they can detect system appearance.
85+
*/
86+
function sendColorScheme() {
87+
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
88+
console.log('[CF Cache Status] Content script sending colorScheme, isDark:', isDark);
89+
browser.runtime.sendMessage({
90+
type: 'colorScheme',
91+
isDark: isDark
92+
}).catch((e) => {
93+
console.log('[CF Cache Status] Content script colorScheme error:', e.message || e);
94+
});
95+
}
96+
97+
// Send color scheme on page load
98+
console.log('[CF Cache Status] Content script loaded');
99+
sendColorScheme();
100+
101+
// Listen for color scheme changes (user toggles dark/light mode)
102+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
103+
console.log('[CF Cache Status] Color scheme changed event, isDark:', e.matches);
104+
sendColorScheme();
105+
});

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Fixed
8+
- Dark mode toolbar icon now displays correctly on Safari launch (previously defaulted to light icon until popup was opened)
9+
10+
### Added
11+
- Native messaging to detect system appearance on extension startup
12+
- Content script color scheme detection for live dark/light mode switching
13+
714
## [v1.0.0] - 2026-01-08
815

916
First stable release.

KNOWN_ISSUES.md

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,3 @@ Safari's webRequest API has several known misalignments with Chrome and Firefox:
3737
### Reporting to Apple
3838

3939
If you encounter this issue, consider filing a bug report via [Apple Feedback Assistant](https://feedbackassistant.apple.com) to help prioritize a fix.
40-
41-
## Dark Mode Icon Switching
42-
43-
**Issue:** The toolbar icon does not automatically switch between light and dark variants when the system appearance changes.
44-
45-
**Cause:** Safari extension background scripts do not have access to `window.matchMedia`, so the extension cannot detect color scheme changes directly. The color scheme is detected via the popup, which has proper DOM access.
46-
47-
**Workaround:** Click the extension icon once after switching between light and dark mode. The icon will update to the correct variant.
48-
49-
**Technical Details:**
50-
- Background scripts in Safari run in a limited environment without full DOM access
51-
- The popup detects the color scheme and sends it to the background script
52-
- Icons are updated when the popup is opened or when tab data changes

0 commit comments

Comments
 (0)