Skip to content

Commit c782e5f

Browse files
Add IDE theme detection and live synchronization
Detects theme from VS Code, Cursor, JetBrains IDEs, and Zed by reading their config files. Adds file watchers to sync theme changes in real-time. Falls back to system theme if IDE theme unavailable. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent cca0dbb commit c782e5f

File tree

3 files changed

+539
-3
lines changed

3 files changed

+539
-3
lines changed

cli/src/hooks/use-system-theme-detector.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect, useRef, useState } from 'react'
22
import { type ThemeName, detectSystemTheme } from '../utils/theme-system'
3+
import { logger } from '../utils/logger'
34
import {
45
spawnMacOSThemeListener,
56
type ThemeListenerProcess,
@@ -24,9 +25,17 @@ export const useSystemThemeDetector = (): ThemeName => {
2425
const listenerRef = useRef<ThemeListenerProcess | null>(null)
2526

2627
useEffect(() => {
28+
logger.info(`[theme] initial theme ${themeName}`)
29+
2730
const handleThemeChange = () => {
2831
const currentTheme = detectSystemTheme()
2932

33+
if (currentTheme !== lastThemeRef.current) {
34+
logger.info(`[theme] theme changed ${lastThemeRef.current} -> ${currentTheme}`)
35+
} else {
36+
logger.info('[theme] theme change event with no delta')
37+
}
38+
3039
// Only update state if theme actually changed
3140
if (currentTheme !== lastThemeRef.current) {
3241
lastThemeRef.current = currentTheme

cli/src/utils/theme-listener-macos.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { existsSync, watch, type FSWatcher } from 'fs'
2+
3+
import { getIDEThemeConfigPaths } from './theme-system'
4+
15
/**
26
* macOS theme change listener using polling
37
* Checks the system theme preference every 0.5 seconds
@@ -24,6 +28,65 @@ while true; do
2428
done
2529
`
2630

31+
const IDE_THEME_DEBOUNCE_MS = 200
32+
33+
interface IDEWatcherHandle {
34+
watchers: FSWatcher[]
35+
dispose: () => void
36+
}
37+
38+
const createIDEThemeWatchers = (
39+
onThemeChange: () => void,
40+
): IDEWatcherHandle => {
41+
const watchers: FSWatcher[] = []
42+
const targets = new Set(getIDEThemeConfigPaths())
43+
44+
if (targets.size === 0) {
45+
return {
46+
watchers,
47+
dispose: () => {},
48+
}
49+
}
50+
51+
let debounceTimer: ReturnType<typeof setTimeout> | null = null
52+
const scheduleNotify = () => {
53+
if (debounceTimer) {
54+
clearTimeout(debounceTimer)
55+
}
56+
57+
debounceTimer = setTimeout(() => {
58+
debounceTimer = null
59+
onThemeChange()
60+
}, IDE_THEME_DEBOUNCE_MS)
61+
}
62+
63+
for (const path of targets) {
64+
try {
65+
if (!existsSync(path)) {
66+
continue
67+
}
68+
69+
const watcher = watch(path, { persistent: false }, () => {
70+
scheduleNotify()
71+
})
72+
73+
watchers.push(watcher)
74+
} catch {
75+
// Ignore watcher failures (e.g., permissions)
76+
}
77+
}
78+
79+
return {
80+
watchers,
81+
dispose: () => {
82+
if (debounceTimer) {
83+
clearTimeout(debounceTimer)
84+
debounceTimer = null
85+
}
86+
},
87+
}
88+
}
89+
2790
export interface ThemeListenerProcess {
2891
kill: () => void
2992
}
@@ -56,6 +119,8 @@ export const spawnMacOSThemeListener = (
56119
stderr: 'pipe',
57120
})
58121

122+
const watcherHandle = createIDEThemeWatchers(onThemeChange)
123+
59124
// Read stderr to prevent blocking
60125
const readStderr = async () => {
61126
const reader = proc.stderr.getReader()
@@ -106,6 +171,16 @@ export const spawnMacOSThemeListener = (
106171
} catch {
107172
// Ignore errors when killing
108173
}
174+
175+
for (const watcher of watcherHandle.watchers) {
176+
try {
177+
watcher.close()
178+
} catch {
179+
// Ignore watcher closure errors
180+
}
181+
}
182+
183+
watcherHandle.dispose()
109184
},
110185
}
111186
} catch {

0 commit comments

Comments
 (0)