diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt index 062f26cfd7..24958500a3 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt @@ -26,9 +26,13 @@ actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle) actual fun StringResource.localized(): String = desc().toString() -private val detector: OsThemeDetector = OsThemeDetector.getDetector() +internal val systemThemeDetector: OsThemeDetector? = try { + OsThemeDetector.getDetector() +} catch (_: LinkageError) { + null +} actual fun isInNightMode() = try { - detector.isDark + systemThemeDetector?.isDark ?: false } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt index 8006b4febb..cb72aa660e 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt @@ -4,10 +4,11 @@ import androidx.compose.runtime.Composable import chat.simplex.common.platform.Log import chat.simplex.common.platform.TAG import chat.simplex.common.platform.desktopPlatform +import chat.simplex.common.platform.systemThemeDetector import com.jthemedetecor.OsThemeDetector -private val detector: OsThemeDetector = OsThemeDetector.getDetector() - .apply { +private val detector: OsThemeDetector? = systemThemeDetector + ?.apply { registerListener(::reactOnDarkThemeChanges) if (desktopPlatform.isLinux()) Runtime.getRuntime().addShutdownHook(Thread(::stopGsettingsMonitor)) } @@ -23,7 +24,7 @@ private fun stopGsettingsMonitor() = @Composable actual fun isSystemInDarkTheme(): Boolean = try { - detector.isDark + detector?.isDark ?: false } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) diff --git a/apps/multiplatform/common/src/desktopMain/resources/simplelogger.properties b/apps/multiplatform/common/src/desktopMain/resources/simplelogger.properties new file mode 100644 index 0000000000..ea9eb5a00f --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/resources/simplelogger.properties @@ -0,0 +1 @@ +org.slf4j.simpleLogger.log.com.jthemedetecor.MacOSThemeDetector=off diff --git a/apps/multiplatform/spec/services/theme.md b/apps/multiplatform/spec/services/theme.md index e278dbe2a9..c223032285 100644 --- a/apps/multiplatform/spec/services/theme.md +++ b/apps/multiplatform/spec/services/theme.md @@ -408,12 +408,12 @@ Delegates to the standard Compose function which reads `Configuration.uiMode`. **Desktop** ([`Theme.desktop.kt`](../../common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt)): ```kotlin -private val detector: OsThemeDetector = OsThemeDetector.getDetector() - .apply { registerListener(::reactOnDarkThemeChanges) } +private val detector: OsThemeDetector? = systemThemeDetector + ?.apply { registerListener(::reactOnDarkThemeChanges) } @Composable actual fun isSystemInDarkTheme(): Boolean = try { - detector.isDark + detector?.isDark ?: false } catch (e: Exception) { false // Fallback for macOS exceptions } @@ -421,6 +421,8 @@ actual fun isSystemInDarkTheme(): Boolean = try { Uses the [jSystemThemeDetector](https://github.com/Dansoftowner/jSystemThemeDetector) library (`OsThemeDetector`). The detector also registers a listener that calls `reactOnDarkThemeChanges()` proactively when the OS theme changes, ensuring the app responds even outside of composition. +`systemThemeDetector` is declared in `Resources.desktop.kt` as `OsThemeDetector.getDetector()` guarded by `catch (_: LinkageError)`. It is `null` when JNA's native library cannot be loaded; the app then starts with the light theme, `isInNightMode()` and `isSystemInDarkTheme()` return `false`, and no listener is registered. + ### reactOnDarkThemeChanges [`Theme.kt` line 763](../../common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt#L763): diff --git a/plans/2026-10-28-desktop-macos-theme-detector-log.md b/plans/2026-10-28-desktop-macos-theme-detector-log.md new file mode 100644 index 0000000000..bcd613d77e --- /dev/null +++ b/plans/2026-10-28-desktop-macos-theme-detector-log.md @@ -0,0 +1,23 @@ +# Desktop: macOS theme detector logs an error in light mode + +## Problem + +On macOS the app prints `ERROR com.jthemedetecor.MacOSThemeDetector - Couldn't execute theme name query with the Os` with a stack trace. + +## Cause + +`MacOSThemeDetector.isDark()` reads `AppleInterfaceStyle`, which macOS leaves unset in light mode. `toStringViaUTF8` then returns `null`, `isDarkTheme(null)` throws `NullPointerException`, and the library logs it through slf4j-simple and returns `false`. The library catches the exception itself, so the app's `catch (e: Exception)` is never reached. Read from the jSystemThemeDetector 3.8 and jfa 1.2.0 bytecode; the unset key in light mode is not verified on a Mac. + +## Fix + +`simplelogger.properties` turns off the `com.jthemedetecor.MacOSThemeDetector` logger. This also hides that logger's other error ("Observer method cannot be added") and any other `RuntimeException` that `isDark()` catches; `isDark()` returns `false` as before. The oshi and Gnome loggers still print. + +## Approaches considered + +1. Turn off the logger (chosen): slf4j-simple can only set a level per logger, so this is the smallest change. +2. Read `AppleInterfaceStyle` in the app instead of the library: removes the cause, but is larger and cannot be tested here. +3. Bump jSystemThemeDetector to 3.9.1, which adds the null check: a different change. The same release rewrites the Gnome detector (it also watches `color-scheme`, and its loop does not block when `readLine` returns `null`, so it busy-loops once the `gsettings` process exits), which would need its own Gnome and shutdown-hook testing. + +## Scope + +Desktop only (`desktopMain`); in effect on macOS only. diff --git a/plans/2026-10-28-desktop-startup-without-jna.md b/plans/2026-10-28-desktop-startup-without-jna.md new file mode 100644 index 0000000000..675224b7d7 --- /dev/null +++ b/plans/2026-10-28-desktop-startup-without-jna.md @@ -0,0 +1,30 @@ +# Desktop starts when JNA cannot load + +## Problem + +On desktop the app exits before any window opens when the JNA native library cannot be loaded, for example when the JNA temp directory (`~/.cache/JNA/temp`) is on a `noexec` mount. The log shows `NoClassDefFoundError: Could not initialize class com.sun.jna.Native` from `Resources_desktopKt.`, caused by `UnsatisfiedLinkError: ... failed to map segment from shared object`. System theme detection is optional, yet it ends the startup. + +## Cause + +- `Resources.desktop.kt` (bf91666f0, #4109) and `Theme.desktop.kt` (26a233ab1, #2755) each create the detector in a top-level `val` (`OsThemeDetector.getDetector()`). +- `getDetector()` loads oshi, which loads JNA. The failure is a `LinkageError` thrown from a static initializer, outside the `try` around `isDark`, so nothing catches it. +- `Resources.desktop.kt` also declares `settings`, so the first preference read (`AppPreferences`) triggers the failing initializer. + +## Fix + +- `systemThemeDetector` in `Resources.desktop.kt` replaces the private `detector`: `getDetector()` inside `try`, `catch (_: LinkageError)` returns `null`. `WindowActivation.kt` catches `LinkageError` for the same condition (#7605). +- It stays an eager top-level `val`, so the detector is created at the same point as before. It does not log: `Log.w` reads `appPrefs`, and `AppPreferences` is under construction when `Resources_desktopKt` initialises (the first `settings` read), so logging there re-enters `appPrefs` and fails with a `NullPointerException`. On Linux oshi has already logged the failure; on Windows and macOS nothing is logged (not verified). +- `isInNightMode()` and `isSystemInDarkTheme()` return `false` (light theme) when there is no detector, the same value the existing `catch (e: Exception)` returns. +- In `Theme.desktop.kt`, `detector` is initialised from `systemThemeDetector`; `getDetector()` returns one cached instance, so both files already shared it. The change listener and the gsettings shutdown hook (#7747) are registered only when a detector exists. + +## Approaches considered + +1. Shared eager accessor with a `LinkageError` guard (chosen). +2. Lazy accessor that logs a warning: no re-entrancy, but moves the detector's start-up cost (about 190 ms measured on the baseline) from `runMigrations` to the first composition on the UI thread. +3. A guard in each file: the same fact (how to continue when JNA cannot be loaded) stated twice. +4. Guard only `Resources.desktop.kt`, the file in the report's trace: `Theme.desktop.kt` would throw the same error when the window starts. +5. Point JNA at an executable temp directory at launch: a different change (running native code extracted into a user-writable directory) and it does not help when mapping fails for another reason. + +## Scope + +Desktop only (`desktopMain`); `spec/services/theme.md` is updated to the new code. Not changed: JNA temp directory, VLC/video behaviour without JNA. A JNA native library of another version selected through an explicit `jna.boot.library.path` or `jna.nosys=false` throws `java.lang.Error`, not `LinkageError`, and is not caught; the app sets neither property.