From 46864a07b8d845ff108cfecd0c2181a34356151f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Apr 2026 13:25:21 +0000 Subject: [PATCH] feat(desktop): platform/appearance/accent overrides for local preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three override hooks so a single-OS developer can preview every platform's theming without VMs: AMETHYST_PLATFORM=GNOME ./gradlew :desktopApp:run AMETHYST_APPEARANCE=light ./gradlew :desktopApp:run AMETHYST_ACCENT=#3584E4 ./gradlew :desktopApp:run Each override accepts either an env var or a `-Damethyst.=` JVM property (forwarded from the gradle invocation via build.gradle.kts). - PlatformInfo: `amethyst.platform` accepts MACOS, WINDOWS, GNOME, KDE, LINUX_OTHER, UNKNOWN. Adds a `host` accessor for code that needs the real underlying OS (for a future fallback hook). - PlatformAppearance: `amethyst.appearance` accepts light/dark. - PlatformAccent: `amethyst.accent` accepts `#RRGGBB`, `RRGGBB`, or any libadwaita accent name (blue, teal, green, yellow, orange, red, pink, purple, slate). Window chrome stays native to the host OS — overriding the platform swaps in-app theming only, not the AWT title bar — so on a Mac you still see traffic lights even when previewing the GNOME theme. https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo --- desktopApp/build.gradle.kts | 7 +++++ .../desktop/platform/PlatformAccent.kt | 31 +++++++++++++++++-- .../desktop/platform/PlatformAppearance.kt | 28 +++++++++++++++-- .../amethyst/desktop/platform/PlatformInfo.kt | 23 +++++++++++++- 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index 0b1a166a0d..09a54abe6a 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -91,6 +91,13 @@ compose.desktop { jvmArgs += "-Xmx2g" + // Forward platform-preview overrides from the gradle invocation to the + // launched app's JVM so `./gradlew :desktopApp:run -Damethyst.platform=GNOME` + // works in addition to the env-var form (`AMETHYST_PLATFORM=GNOME`). + listOf("amethyst.platform", "amethyst.appearance", "amethyst.accent").forEach { key -> + System.getProperty(key)?.let { jvmArgs += "-D$key=$it" } + } + nativeDistributions { appResourcesRootDir.set(project.layout.projectDirectory.dir("src/jvmMain/appResources")) targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Rpm) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformAccent.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformAccent.kt index 48dc1950c5..fb76167822 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformAccent.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformAccent.kt @@ -38,14 +38,41 @@ import com.vitorpamplona.quartz.utils.Log * identity and looks intentional rather than broken. */ object PlatformAccent { - fun systemAccent(): Color = - when (PlatformInfo.current) { + /** + * Resolves the OS accent color, with an override hook for testing: + * `-Damethyst.accent=#3584E4` (system property) or `AMETHYST_ACCENT=blue` + * (env var). Accepted: `#RRGGBB`, `RRGGBB`, or any libadwaita accent name + * (blue, teal, green, yellow, orange, red, pink, purple, slate). + */ + fun systemAccent(): Color { + forcedAccent()?.let { return it } + return when (PlatformInfo.current) { Platform.MACOS -> macOSAccent() Platform.GNOME -> gnomeAccent() Platform.KDE -> kdeAccent() Platform.WINDOWS -> windowsAccent() else -> DefaultPrimary } ?: DefaultPrimary + } + + private fun forcedAccent(): Color? { + val raw = + System.getProperty("amethyst.accent") + ?: System.getenv("AMETHYST_ACCENT") + ?: return null + val trimmed = raw.trim() + // Hex (#RRGGBB or RRGGBB) — long form for clarity + val hex = trimmed.removePrefix("#") + if (hex.length == 6 && hex.all { it.isDigit() || it in 'a'..'f' || it in 'A'..'F' }) { + val v = hex.toLongOrNull(16) ?: return null + return Color( + red = ((v shr 16) and 0xFFL).toInt(), + green = ((v shr 8) and 0xFFL).toInt(), + blue = (v and 0xFFL).toInt(), + ) + } + return gnomeAccents[trimmed.lowercase()] + } // macOS named accent colors (NSColor controlAccentColor variants). // Index matches AppleAccentColor defaults value; -1 = Multicolor (use highlight). diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformAppearance.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformAppearance.kt index e2e6d10088..6754782d68 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformAppearance.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformAppearance.kt @@ -41,14 +41,38 @@ import java.awt.event.WindowFocusListener * system theme see Amethyst follow within a second of bringing the window forward. */ object PlatformAppearance { - fun isSystemDark(): Boolean = - when (PlatformInfo.current) { + /** + * Resolves the OS dark/light preference, with an override hook for testing + * on a single machine: `-Damethyst.appearance=light|dark` (system property) + * or `AMETHYST_APPEARANCE=light|dark` (env var). + * + * When the platform is overridden but appearance isn't, this calls the + * override platform's detection routine. On a Mac forced to GNOME the + * `gsettings` shell-out won't exist and it falls through to the GNOME + * default (dark) — pass `AMETHYST_APPEARANCE=light` to flip it. + */ + fun isSystemDark(): Boolean { + forcedAppearance()?.let { return it } + return when (PlatformInfo.current) { Platform.MACOS -> isMacOSDark() Platform.GNOME -> isGnomeDark() Platform.KDE -> isKdeDark() Platform.WINDOWS -> isWindowsDark() else -> true } + } + + private fun forcedAppearance(): Boolean? { + val raw = + System.getProperty("amethyst.appearance") + ?: System.getenv("AMETHYST_APPEARANCE") + ?: return null + return when (raw.lowercase()) { + "dark" -> true + "light" -> false + else -> null + } + } private fun isMacOSDark(): Boolean { val out = exec("defaults", "read", "-g", "AppleInterfaceStyle") ?: return false diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformInfo.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformInfo.kt index 58f41b32ca..6c9d7c59b0 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformInfo.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/platform/PlatformInfo.kt @@ -39,7 +39,20 @@ enum class Platform { } object PlatformInfo { - val current: Platform by lazy { detect() } + /** + * The detected (or overridden) UI platform — drives all per-OS theming. + * + * Override for testing on a single machine via `-Damethyst.platform=GNOME` + * (system property) or `AMETHYST_PLATFORM=GNOME` (env var). Accepted values + * are the [Platform] enum names, case-insensitive: MACOS, WINDOWS, GNOME, + * KDE, LINUX_OTHER, UNKNOWN. + */ + val current: Platform by lazy { override() ?: detect() } + + /** The actual host OS, ignoring any override. Useful for shell-out helpers + * that should always hit the real underlying system (e.g. accent detection + * falling back to the Mac's value when the override platform's CLI is absent). */ + val host: Platform by lazy { detect() } val isMacOS: Boolean get() = current == Platform.MACOS val isWindows: Boolean get() = current == Platform.WINDOWS @@ -47,6 +60,14 @@ object PlatformInfo { val isKde: Boolean get() = current == Platform.KDE val isLinux: Boolean get() = current.isLinux + private fun override(): Platform? { + val raw = + System.getProperty("amethyst.platform") + ?: System.getenv("AMETHYST_PLATFORM") + ?: return null + return runCatching { Platform.valueOf(raw.uppercase()) }.getOrNull() + } + private fun detect(): Platform { val osName = System.getProperty("os.name", "").lowercase() return when {