feat(desktop): platform/appearance/accent overrides for local preview

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.<key>=<value>`
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
This commit is contained in:
Claude
2026-04-24 13:25:21 +00:00
parent a15c5d2692
commit 46864a07b8
4 changed files with 84 additions and 5 deletions
@@ -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).
@@ -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
@@ -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 {