From 46cbe2263b62a81e13ceef9ab1f0ad045fb7af42 Mon Sep 17 00:00:00 2001 From: AmirHossein Abdolmotallebi Date: Fri, 8 May 2026 11:07:21 +0330 Subject: [PATCH 1/3] use an interface for dark mode detector --- .../darkmodedetector/DarkModeDetector.kt | 28 +++++++++++++++ .../darkmodedetector/IsSystemInDarkMode.kt | 35 +++++++++++++------ .../linux/LinuxPortalThemeDetector.kt | 32 +++-------------- .../mac/MacOSThemeDetector.kt | 32 +++-------------- .../windows/WindowsThemeDetector.kt | 35 +++---------------- 5 files changed, 68 insertions(+), 94 deletions(-) create mode 100644 darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/DarkModeDetector.kt diff --git a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/DarkModeDetector.kt b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/DarkModeDetector.kt new file mode 100644 index 000000000..c699f07ba --- /dev/null +++ b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/DarkModeDetector.kt @@ -0,0 +1,28 @@ +package io.github.kdroidfilter.nucleus.darkmodedetector + +import io.github.kdroidfilter.nucleus.core.runtime.Platform +import io.github.kdroidfilter.nucleus.darkmodedetector.linux.LinuxPortalThemeDetector +import io.github.kdroidfilter.nucleus.darkmodedetector.mac.MacOSThemeDetector +import io.github.kdroidfilter.nucleus.darkmodedetector.windows.WindowsThemeDetector +import java.util.function.Consumer + +interface IDarkModeDetector { + fun isDark(): Boolean + fun registerListener(listener: Consumer) + fun removeListener(listener: Consumer) +} + +object NoopDarkModeDetector : IDarkModeDetector { + override fun isDark(): Boolean = false + override fun registerListener(listener: Consumer) = Unit + override fun removeListener(listener: Consumer) = Unit +} + +public fun getPlatformDarkModeDetector(): IDarkModeDetector { + return when (Platform.Current) { + Platform.MacOS -> MacOSThemeDetector + Platform.Windows -> WindowsThemeDetector + Platform.Linux -> LinuxPortalThemeDetector + else -> NoopDarkModeDetector + } +} diff --git a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/IsSystemInDarkMode.kt b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/IsSystemInDarkMode.kt index a1f669a1c..5e339e51d 100644 --- a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/IsSystemInDarkMode.kt +++ b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/IsSystemInDarkMode.kt @@ -1,12 +1,12 @@ package io.github.kdroidfilter.nucleus.darkmodedetector import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalInspectionMode -import io.github.kdroidfilter.nucleus.core.runtime.Platform -import io.github.kdroidfilter.nucleus.darkmodedetector.linux.isLinuxInDarkMode -import io.github.kdroidfilter.nucleus.darkmodedetector.mac.isMacOsInDarkMode -import io.github.kdroidfilter.nucleus.darkmodedetector.windows.isWindowsInDarkMode +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import java.util.function.Consumer /** * Composable function that returns whether the system is in dark mode. @@ -18,11 +18,26 @@ fun isSystemInDarkMode(): Boolean { if (isInPreview) { return isSystemInDarkTheme() } + val darkModeDetector = remember { getPlatformDarkModeDetector() } + val darkModeState = remember { mutableStateOf(darkModeDetector.isDark()) } + + DisposableEffect(Unit) { + debugln(TAG) { "Registering OS dark mode listener in Compose" } + val listener = + Consumer { newValue -> + debugln(TAG) { "OS dark mode updated: $newValue" } + darkModeState.value = newValue + } - return when (Platform.Current) { - Platform.MacOS -> isMacOsInDarkMode() - Platform.Windows -> isWindowsInDarkMode() - Platform.Linux -> isLinuxInDarkMode() - else -> false + darkModeDetector.registerListener(listener) + + onDispose { + debugln(TAG) { "Removing OS dark mode listener in Compose" } + darkModeDetector.removeListener(listener) + } } + + return darkModeState.value } + +private const val TAG = "PlatformThemeDetectorCompose" diff --git a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/linux/LinuxPortalThemeDetector.kt b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/linux/LinuxPortalThemeDetector.kt index 789c3b15c..d23b7da03 100644 --- a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/linux/LinuxPortalThemeDetector.kt +++ b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/linux/LinuxPortalThemeDetector.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import io.github.kdroidfilter.nucleus.darkmodedetector.debugln +import io.github.kdroidfilter.nucleus.darkmodedetector.IDarkModeDetector import java.util.function.Consumer private const val TAG = "LinuxPortalThemeDetector" @@ -18,42 +19,19 @@ private const val TAG = "LinuxPortalThemeDetector" * The detector also monitors for SettingChanged signals in real-time via a background * D-Bus dispatch thread. */ -internal object LinuxPortalThemeDetector { +internal object LinuxPortalThemeDetector: IDarkModeDetector { init { debugln(TAG) { "Initializing Linux portal theme observer via JNI" } NativeLinuxBridge.nativeStartObserving() } - fun isDark(): Boolean = NativeLinuxBridge.nativeIsDark() + override fun isDark(): Boolean = NativeLinuxBridge.nativeIsDark() - fun registerListener(listener: Consumer) { + override fun registerListener(listener: Consumer) { NativeLinuxBridge.registerListener(listener) } - fun removeListener(listener: Consumer) { + override fun removeListener(listener: Consumer) { NativeLinuxBridge.removeListener(listener) } } - -/** - * A helper composable function that returns the current Linux dark mode state - * via the XDG Desktop Portal, updating automatically when the system theme changes. - */ -@Composable -fun isLinuxInDarkMode(): Boolean { - val darkModeState = remember { mutableStateOf(LinuxPortalThemeDetector.isDark()) } - DisposableEffect(Unit) { - debugln(TAG) { "Registering Linux portal dark mode listener in Compose" } - val listener = - Consumer { newValue -> - debugln(TAG) { "Compose Linux portal dark mode updated: $newValue" } - darkModeState.value = newValue - } - LinuxPortalThemeDetector.registerListener(listener) - onDispose { - debugln(TAG) { "Removing Linux portal dark mode listener in Compose" } - LinuxPortalThemeDetector.removeListener(listener) - } - } - return darkModeState.value -} diff --git a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/mac/MacOSThemeDetector.kt b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/mac/MacOSThemeDetector.kt index 491afef23..06e04ca30 100644 --- a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/mac/MacOSThemeDetector.kt +++ b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/mac/MacOSThemeDetector.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import io.github.kdroidfilter.nucleus.darkmodedetector.debugln +import io.github.kdroidfilter.nucleus.darkmodedetector.IDarkModeDetector import java.util.function.Consumer private const val TAG = "MacOSThemeDetector" @@ -14,42 +15,19 @@ private const val TAG = "MacOSThemeDetector" * to detect theme changes in macOS. It reads the system preference "AppleInterfaceStyle" * (which is "Dark" when in dark mode) from NSUserDefaults. */ -internal object MacOSThemeDetector { +internal object MacOSThemeDetector: IDarkModeDetector { init { debugln(TAG) { "Initializing macOS theme observer via JNI" } NativeDarkModeBridge.nativeStartObserving() } - fun isDark(): Boolean = NativeDarkModeBridge.nativeIsDark() + override fun isDark(): Boolean = NativeDarkModeBridge.nativeIsDark() - fun registerListener(listener: Consumer) { + override fun registerListener(listener: Consumer) { NativeDarkModeBridge.registerListener(listener) } - fun removeListener(listener: Consumer) { + override fun removeListener(listener: Consumer) { NativeDarkModeBridge.removeListener(listener) } } - -/** - * A helper composable function that returns the current macOS dark mode state, - * updating automatically when the system theme changes. - */ -@Composable -internal fun isMacOsInDarkMode(): Boolean { - val darkModeState = remember { mutableStateOf(MacOSThemeDetector.isDark()) } - DisposableEffect(Unit) { - debugln(TAG) { "Registering macOS dark mode listener in Compose" } - val listener = - Consumer { newValue -> - debugln(TAG) { "Compose macOS dark mode updated: $newValue" } - darkModeState.value = newValue - } - MacOSThemeDetector.registerListener(listener) - onDispose { - debugln(TAG) { "Removing macOS dark mode listener in Compose" } - MacOSThemeDetector.removeListener(listener) - } - } - return darkModeState.value -} diff --git a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/windows/WindowsThemeDetector.kt b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/windows/WindowsThemeDetector.kt index 9dbe098ed..a0243c822 100644 --- a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/windows/WindowsThemeDetector.kt +++ b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/windows/WindowsThemeDetector.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import io.github.kdroidfilter.nucleus.darkmodedetector.debugln +import io.github.kdroidfilter.nucleus.darkmodedetector.IDarkModeDetector import java.util.function.Consumer private const val TAG = "WindowsThemeDetector" @@ -18,45 +19,19 @@ private const val TAG = "WindowsThemeDetector" * The detector monitors the registry for changes in real-time via a native * background thread using RegNotifyChangeKeyValue in async mode. */ -internal object WindowsThemeDetector { +internal object WindowsThemeDetector: IDarkModeDetector { init { debugln(TAG) { "Initializing Windows theme observer via JNI" } NativeWindowsBridge.nativeStartObserving() } - fun isDark(): Boolean = NativeWindowsBridge.nativeIsDark() + override fun isDark(): Boolean = NativeWindowsBridge.nativeIsDark() - fun registerListener(listener: Consumer) { + override fun registerListener(listener: Consumer) { NativeWindowsBridge.registerListener(listener) } - fun removeListener(listener: Consumer) { + override fun removeListener(listener: Consumer) { NativeWindowsBridge.removeListener(listener) } } - -/** - * Composable function that returns whether Windows is currently in dark mode. - */ -@Composable -internal fun isWindowsInDarkMode(): Boolean { - val darkModeState = remember { mutableStateOf(WindowsThemeDetector.isDark()) } - - DisposableEffect(Unit) { - debugln(TAG) { "Registering Windows dark mode listener in Compose" } - val listener = - Consumer { newValue -> - debugln(TAG) { "Windows dark mode updated: $newValue" } - darkModeState.value = newValue - } - - WindowsThemeDetector.registerListener(listener) - - onDispose { - debugln(TAG) { "Removing Windows dark mode listener in Compose" } - WindowsThemeDetector.removeListener(listener) - } - } - - return darkModeState.value -} From ea8995f83c2295ca108d57f9ac5f2054719e6c54 Mon Sep 17 00:00:00 2001 From: AmirHossein Abdolmotallebi Date: Fri, 8 May 2026 14:15:15 +0330 Subject: [PATCH 2/3] update dark mode mode detector docs --- docs/runtime/darkmode-detector.md | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/runtime/darkmode-detector.md b/docs/runtime/darkmode-detector.md index 9fbe5901f..8f1542438 100644 --- a/docs/runtime/darkmode-detector.md +++ b/docs/runtime/darkmode-detector.md @@ -14,6 +14,40 @@ dependencies { ## Usage +### Manual usage + +This is the core functionality - it’s totally up to you how you want to use it. Just don’t forget to dispose its callback (`removeListener`) when you don’t need it anymore. + +You can use Compose states, RxJava/Kotlin, or anything else you prefer. + +Here we’re using a coroutines Flow wrapper just for demonstration. + +```kt +val isSystemDarkFlow: Flow = callbackFlow { + val listener: Consumer = { isDark: Boolean -> + trySend(isDark) + } + + val darkModeDetector = getPlatformDarkModeDetector() + + // emit initial value + trySend(darkModeDetector.isDark()) + + // listen to dark mode change events + darkModeDetector.registerListener(listener) + awaitClose { + darkModeDetector.removeListener(listener) + } +} +isSystemDarkFlow.collect { + println("dark mode changes, isDark: $it") +} +``` + +### In compose + +There is already a composable function available out of the box, which you can use it in your compose applications. + ```kotlin @Composable fun App() { From c443012c4f1925dc406d6aa3d2850556e20b9c05 Mon Sep 17 00:00:00 2001 From: Elie Gambache Date: Sun, 10 May 2026 21:59:13 +0300 Subject: [PATCH 3/3] fix: lint errors in darkmode-detector refactor --- .../nucleus/darkmodedetector/DarkModeDetector.kt | 9 ++++++--- .../nucleus/darkmodedetector/IsSystemInDarkMode.kt | 2 +- .../darkmodedetector/linux/LinuxPortalThemeDetector.kt | 8 ++------ .../nucleus/darkmodedetector/mac/MacOSThemeDetector.kt | 8 ++------ .../darkmodedetector/windows/WindowsThemeDetector.kt | 8 ++------ 5 files changed, 13 insertions(+), 22 deletions(-) diff --git a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/DarkModeDetector.kt b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/DarkModeDetector.kt index c699f07ba..259768254 100644 --- a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/DarkModeDetector.kt +++ b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/DarkModeDetector.kt @@ -8,21 +8,24 @@ import java.util.function.Consumer interface IDarkModeDetector { fun isDark(): Boolean + fun registerListener(listener: Consumer) + fun removeListener(listener: Consumer) } object NoopDarkModeDetector : IDarkModeDetector { override fun isDark(): Boolean = false + override fun registerListener(listener: Consumer) = Unit + override fun removeListener(listener: Consumer) = Unit } -public fun getPlatformDarkModeDetector(): IDarkModeDetector { - return when (Platform.Current) { +public fun getPlatformDarkModeDetector(): IDarkModeDetector = + when (Platform.Current) { Platform.MacOS -> MacOSThemeDetector Platform.Windows -> WindowsThemeDetector Platform.Linux -> LinuxPortalThemeDetector else -> NoopDarkModeDetector } -} diff --git a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/IsSystemInDarkMode.kt b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/IsSystemInDarkMode.kt index 5e339e51d..65aa4b33b 100644 --- a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/IsSystemInDarkMode.kt +++ b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/IsSystemInDarkMode.kt @@ -1,11 +1,11 @@ package io.github.kdroidfilter.nucleus.darkmodedetector import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalInspectionMode import java.util.function.Consumer /** diff --git a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/linux/LinuxPortalThemeDetector.kt b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/linux/LinuxPortalThemeDetector.kt index d23b7da03..a97d485b4 100644 --- a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/linux/LinuxPortalThemeDetector.kt +++ b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/linux/LinuxPortalThemeDetector.kt @@ -1,11 +1,7 @@ package io.github.kdroidfilter.nucleus.darkmodedetector.linux -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import io.github.kdroidfilter.nucleus.darkmodedetector.debugln import io.github.kdroidfilter.nucleus.darkmodedetector.IDarkModeDetector +import io.github.kdroidfilter.nucleus.darkmodedetector.debugln import java.util.function.Consumer private const val TAG = "LinuxPortalThemeDetector" @@ -19,7 +15,7 @@ private const val TAG = "LinuxPortalThemeDetector" * The detector also monitors for SettingChanged signals in real-time via a background * D-Bus dispatch thread. */ -internal object LinuxPortalThemeDetector: IDarkModeDetector { +internal object LinuxPortalThemeDetector : IDarkModeDetector { init { debugln(TAG) { "Initializing Linux portal theme observer via JNI" } NativeLinuxBridge.nativeStartObserving() diff --git a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/mac/MacOSThemeDetector.kt b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/mac/MacOSThemeDetector.kt index 06e04ca30..6cd6e9580 100644 --- a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/mac/MacOSThemeDetector.kt +++ b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/mac/MacOSThemeDetector.kt @@ -1,11 +1,7 @@ package io.github.kdroidfilter.nucleus.darkmodedetector.mac -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import io.github.kdroidfilter.nucleus.darkmodedetector.debugln import io.github.kdroidfilter.nucleus.darkmodedetector.IDarkModeDetector +import io.github.kdroidfilter.nucleus.darkmodedetector.debugln import java.util.function.Consumer private const val TAG = "MacOSThemeDetector" @@ -15,7 +11,7 @@ private const val TAG = "MacOSThemeDetector" * to detect theme changes in macOS. It reads the system preference "AppleInterfaceStyle" * (which is "Dark" when in dark mode) from NSUserDefaults. */ -internal object MacOSThemeDetector: IDarkModeDetector { +internal object MacOSThemeDetector : IDarkModeDetector { init { debugln(TAG) { "Initializing macOS theme observer via JNI" } NativeDarkModeBridge.nativeStartObserving() diff --git a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/windows/WindowsThemeDetector.kt b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/windows/WindowsThemeDetector.kt index a0243c822..33689c14e 100644 --- a/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/windows/WindowsThemeDetector.kt +++ b/darkmode-detector/src/main/kotlin/io/github/kdroidfilter/nucleus/darkmodedetector/windows/WindowsThemeDetector.kt @@ -1,11 +1,7 @@ package io.github.kdroidfilter.nucleus.darkmodedetector.windows -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import io.github.kdroidfilter.nucleus.darkmodedetector.debugln import io.github.kdroidfilter.nucleus.darkmodedetector.IDarkModeDetector +import io.github.kdroidfilter.nucleus.darkmodedetector.debugln import java.util.function.Consumer private const val TAG = "WindowsThemeDetector" @@ -19,7 +15,7 @@ private const val TAG = "WindowsThemeDetector" * The detector monitors the registry for changes in real-time via a native * background thread using RegNotifyChangeKeyValue in async mode. */ -internal object WindowsThemeDetector: IDarkModeDetector { +internal object WindowsThemeDetector : IDarkModeDetector { init { debugln(TAG) { "Initializing Windows theme observer via JNI" } NativeWindowsBridge.nativeStartObserving()