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:
@@ -91,6 +91,13 @@ compose.desktop {
|
|||||||
|
|
||||||
jvmArgs += "-Xmx2g"
|
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 {
|
nativeDistributions {
|
||||||
appResourcesRootDir.set(project.layout.projectDirectory.dir("src/jvmMain/appResources"))
|
appResourcesRootDir.set(project.layout.projectDirectory.dir("src/jvmMain/appResources"))
|
||||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Rpm)
|
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Rpm)
|
||||||
|
|||||||
+29
-2
@@ -38,14 +38,41 @@ import com.vitorpamplona.quartz.utils.Log
|
|||||||
* identity and looks intentional rather than broken.
|
* identity and looks intentional rather than broken.
|
||||||
*/
|
*/
|
||||||
object PlatformAccent {
|
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.MACOS -> macOSAccent()
|
||||||
Platform.GNOME -> gnomeAccent()
|
Platform.GNOME -> gnomeAccent()
|
||||||
Platform.KDE -> kdeAccent()
|
Platform.KDE -> kdeAccent()
|
||||||
Platform.WINDOWS -> windowsAccent()
|
Platform.WINDOWS -> windowsAccent()
|
||||||
else -> DefaultPrimary
|
else -> DefaultPrimary
|
||||||
} ?: 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).
|
// macOS named accent colors (NSColor controlAccentColor variants).
|
||||||
// Index matches AppleAccentColor defaults value; -1 = Multicolor (use highlight).
|
// Index matches AppleAccentColor defaults value; -1 = Multicolor (use highlight).
|
||||||
|
|||||||
+26
-2
@@ -41,14 +41,38 @@ import java.awt.event.WindowFocusListener
|
|||||||
* system theme see Amethyst follow within a second of bringing the window forward.
|
* system theme see Amethyst follow within a second of bringing the window forward.
|
||||||
*/
|
*/
|
||||||
object PlatformAppearance {
|
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.MACOS -> isMacOSDark()
|
||||||
Platform.GNOME -> isGnomeDark()
|
Platform.GNOME -> isGnomeDark()
|
||||||
Platform.KDE -> isKdeDark()
|
Platform.KDE -> isKdeDark()
|
||||||
Platform.WINDOWS -> isWindowsDark()
|
Platform.WINDOWS -> isWindowsDark()
|
||||||
else -> true
|
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 {
|
private fun isMacOSDark(): Boolean {
|
||||||
val out = exec("defaults", "read", "-g", "AppleInterfaceStyle") ?: return false
|
val out = exec("defaults", "read", "-g", "AppleInterfaceStyle") ?: return false
|
||||||
|
|||||||
+22
-1
@@ -39,7 +39,20 @@ enum class Platform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object PlatformInfo {
|
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 isMacOS: Boolean get() = current == Platform.MACOS
|
||||||
val isWindows: Boolean get() = current == Platform.WINDOWS
|
val isWindows: Boolean get() = current == Platform.WINDOWS
|
||||||
@@ -47,6 +60,14 @@ object PlatformInfo {
|
|||||||
val isKde: Boolean get() = current == Platform.KDE
|
val isKde: Boolean get() = current == Platform.KDE
|
||||||
val isLinux: Boolean get() = current.isLinux
|
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 {
|
private fun detect(): Platform {
|
||||||
val osName = System.getProperty("os.name", "").lowercase()
|
val osName = System.getProperty("os.name", "").lowercase()
|
||||||
return when {
|
return when {
|
||||||
|
|||||||
Reference in New Issue
Block a user