# OS Detection & Platform-Specific Code Patterns for detecting operating system and implementing platform-specific behavior in Compose Desktop. ## OS Detection ### Basic Detection ```kotlin val osName = System.getProperty("os.name").lowercase() val isMacOS = osName.contains("mac") val isWindows = osName.contains("win") val isLinux = osName.contains("nux") || osName.contains("nix") ``` ### System Properties ```kotlin // OS name System.getProperty("os.name") // Examples: "Mac OS X", "Windows 10", "Linux" // OS version System.getProperty("os.version") // Examples: "14.2.1", "10.0", "6.5.0-14-generic" // OS architecture System.getProperty("os.arch") // Examples: "aarch64", "x86_64", "amd64" // User home directory System.getProperty("user.home") // Examples: "/Users/username", "C:\Users\username", "/home/username" // File separator System.getProperty("file.separator") // Examples: "/" (Unix), "\" (Windows) // Path separator System.getProperty("path.separator") // Examples: ":" (Unix), ";" (Windows) ``` --- ## PlatformDetector Utility Create a centralized utility for platform detection. **File:** `commons/src/jvmMain/kotlin/utils/PlatformDetector.kt` ```kotlin package com.vitorpamplona.amethyst.commons.utils object PlatformDetector { private val osName = System.getProperty("os.name").lowercase() val isMacOS: Boolean = osName.contains("mac") val isWindows: Boolean = osName.contains("win") val isLinux: Boolean = osName.contains("nux") || osName.contains("nix") val platform: Platform = when { isMacOS -> Platform.MacOS isWindows -> Platform.Windows isLinux -> Platform.Linux else -> Platform.Unknown } enum class Platform { MacOS, Windows, Linux, Unknown } // File paths val fileSeparator: String = System.getProperty("file.separator") val pathSeparator: String = System.getProperty("path.separator") // User directories val userHome: String = System.getProperty("user.home") val appDataDir: String = when (platform) { Platform.MacOS -> "$userHome/Library/Application Support" Platform.Windows -> System.getenv("APPDATA") ?: "$userHome\\AppData\\Roaming" Platform.Linux -> System.getenv("XDG_CONFIG_HOME") ?: "$userHome/.config" Platform.Unknown -> userHome } // Modifier key names val primaryModifierName: String = if (isMacOS) "Cmd" else "Ctrl" val secondaryModifierName: String = if (isMacOS) "Option" else "Alt" fun platformSpecific( macOS: () -> Unit = {}, windows: () -> Unit = {}, linux: () -> Unit = {}, fallback: () -> Unit = {} ) { when (platform) { Platform.MacOS -> macOS() Platform.Windows -> windows() Platform.Linux -> linux() Platform.Unknown -> fallback() } } } ``` **Usage:** ```kotlin // Simple check if (PlatformDetector.isMacOS) { // macOS-specific code } // Pattern matching when (PlatformDetector.platform) { Platform.MacOS -> setupMacDock() Platform.Windows -> setupWindowsTray() Platform.Linux -> setupLinuxTray() Platform.Unknown -> showWarning() } // Platform-specific execution PlatformDetector.platformSpecific( macOS = { setupMacMenuBar() }, windows = { setupWindowsMenu() }, linux = { setupLinuxMenu() } ) // File paths val configPath = "${PlatformDetector.appDataDir}${PlatformDetector.fileSeparator}amethyst" ``` --- ## Platform-Specific UI ### Keyboard Shortcuts Helper ```kotlin package com.vitorpamplona.amethyst.commons.utils import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyShortcut object DesktopShortcuts { private val isMacOS = PlatformDetector.isMacOS fun primary(key: Key) = if (isMacOS) { KeyShortcut(key, meta = true) } else { KeyShortcut(key, ctrl = true) } fun primaryShift(key: Key) = if (isMacOS) { KeyShortcut(key, meta = true, shift = true) } else { KeyShortcut(key, ctrl = true, shift = true) } fun primaryAlt(key: Key) = if (isMacOS) { KeyShortcut(key, meta = true, alt = true) } else { KeyShortcut(key, ctrl = true, alt = true) } val modifierName = PlatformDetector.primaryModifierName val secondaryName = PlatformDetector.secondaryModifierName fun formatShortcut(key: String, withPrimary: Boolean = true): String { return if (withPrimary) "$modifierName+$key" else key } } ``` ### File Paths Helper ```kotlin package com.vitorpamplona.amethyst.commons.utils import java.io.File object FilePaths { private val separator = PlatformDetector.fileSeparator fun join(vararg parts: String): String { return parts.joinToString(separator) } fun appConfig(appName: String): String { return join(PlatformDetector.appDataDir, appName) } fun appCache(appName: String): String { return when (PlatformDetector.platform) { PlatformDetector.Platform.MacOS -> join(PlatformDetector.userHome, "Library", "Caches", appName) PlatformDetector.Platform.Windows -> join(System.getenv("LOCALAPPDATA") ?: "${PlatformDetector.userHome}\\AppData\\Local", appName) PlatformDetector.Platform.Linux -> join(System.getenv("XDG_CACHE_HOME") ?: "${PlatformDetector.userHome}/.cache", appName) else -> join(PlatformDetector.userHome, ".cache", appName) } } fun ensureDirectory(path: String): File { return File(path).apply { if (!exists()) { mkdirs() } } } } // Usage val configDir = FilePaths.ensureDirectory(FilePaths.appConfig("amethyst")) val cacheDir = FilePaths.ensureDirectory(FilePaths.appCache("amethyst")) ``` --- ## Platform-Specific Features ### Open External URL ```kotlin // commons/src/commonMain/kotlin/utils/ExternalUrl.kt expect fun openExternalUrl(url: String) // commons/src/jvmMain/kotlin/utils/ExternalUrl.jvm.kt import java.awt.Desktop import java.net.URI actual fun openExternalUrl(url: String) { if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { Desktop.getDesktop().browse(URI(url)) } } ``` ### File Picker ```kotlin // Platform-specific file picker fun showFilePicker( title: String = "Select file", mode: FilePickerMode = FilePickerMode.Load ): String? { val fileDialog = java.awt.FileDialog( java.awt.Frame(), title, when (mode) { FilePickerMode.Load -> java.awt.FileDialog.LOAD FilePickerMode.Save -> java.awt.FileDialog.SAVE } ) // macOS-specific: Enable file selection features if (PlatformDetector.isMacOS) { System.setProperty("apple.awt.fileDialogForDirectories", "false") } fileDialog.isVisible = true return fileDialog.file?.let { "${fileDialog.directory}$it" } } enum class FilePickerMode { Load, Save } ``` ### Directory Picker (macOS) ```kotlin fun showDirectoryPicker(title: String = "Select directory"): String? { if (PlatformDetector.isMacOS) { // macOS-specific directory picker System.setProperty("apple.awt.fileDialogForDirectories", "true") } val fileDialog = java.awt.FileDialog(java.awt.Frame(), title, java.awt.FileDialog.LOAD) fileDialog.isVisible = true if (PlatformDetector.isMacOS) { System.setProperty("apple.awt.fileDialogForDirectories", "false") } return fileDialog.directory } ``` --- ## Window Decorations ### macOS-Specific ```kotlin // Unified title bar (macOS Big Sur+) if (PlatformDetector.isMacOS) { Window( undecorated = false, transparent = true, // ... ) { // Custom title bar } } ``` ### Windows-Specific ```kotlin // Custom window chrome (Windows) if (PlatformDetector.isWindows) { Window( undecorated = true, // Custom decorations ) { Column { // Custom title bar with min/max/close buttons WindowTitleBar() // Content } } } ``` --- ## System Tray Icons Different icon formats per OS: ```kotlin fun getTrayIcon(): Painter { return when (PlatformDetector.platform) { Platform.MacOS -> painterResource("tray-icon-mac.png") // Template icon Platform.Windows -> painterResource("tray-icon-win.ico") Platform.Linux -> painterResource("tray-icon-linux.png") else -> painterResource("tray-icon.png") } } // macOS: Template icons (black/transparent) // Windows: ICO format, 16x16 // Linux: PNG, typically 24x24 ``` --- ## Native Notifications ```kotlin // Platform-specific notification implementation fun sendNotification(title: String, message: String) { PlatformDetector.platformSpecific( macOS = { // macOS: Use NSUserNotification (via tray) trayState.sendNotification( Notification(title, message, Notification.Type.Info) ) }, windows = { // Windows: Use Windows toast notifications trayState.sendNotification( Notification(title, message, Notification.Type.Info) ) }, linux = { // Linux: Use libnotify (via tray) trayState.sendNotification( Notification(title, message, Notification.Type.Info) ) } ) } ``` --- ## Architecture Detection ```kotlin object ArchDetector { private val arch = System.getProperty("os.arch").lowercase() val isArm: Boolean = arch.contains("aarch") || arch.contains("arm") val isX64: Boolean = arch.contains("x86_64") || arch.contains("amd64") val isX86: Boolean = arch.contains("x86") && !isX64 val architecture: Architecture = when { isArm -> Architecture.ARM isX64 -> Architecture.X64 isX86 -> Architecture.X86 else -> Architecture.Unknown } enum class Architecture { ARM, X64, X86, Unknown } } // Usage: Load correct native library fun loadNativeLib() { val libName = when { PlatformDetector.isMacOS && ArchDetector.isArm -> "libsecp256k1-macos-arm64" PlatformDetector.isMacOS && ArchDetector.isX64 -> "libsecp256k1-macos-x64" PlatformDetector.isWindows && ArchDetector.isX64 -> "libsecp256k1-win-x64" PlatformDetector.isLinux && ArchDetector.isX64 -> "libsecp256k1-linux-x64" else -> throw UnsupportedOperationException("Unsupported platform") } System.loadLibrary(libName) } ``` --- ## Testing Platform Detection ```kotlin @Test fun testPlatformDetection() { println("OS: ${System.getProperty("os.name")}") println("Version: ${System.getProperty("os.version")}") println("Arch: ${System.getProperty("os.arch")}") println() println("Is macOS: ${PlatformDetector.isMacOS}") println("Is Windows: ${PlatformDetector.isWindows}") println("Is Linux: ${PlatformDetector.isLinux}") println("Platform: ${PlatformDetector.platform}") println() println("User home: ${PlatformDetector.userHome}") println("App data: ${PlatformDetector.appDataDir}") println("File separator: ${PlatformDetector.fileSeparator}") } // Example output (macOS): // OS: Mac OS X // Version: 14.2.1 // Arch: aarch64 // // Is macOS: true // Is Windows: false // Is Linux: false // Platform: MacOS // // User home: /Users/username // App data: /Users/username/Library/Application Support // File separator: / ``` --- ## Best Practices ### 1. Centralize Detection ✅ **DO:** Use PlatformDetector singleton ```kotlin if (PlatformDetector.isMacOS) { /* ... */ } ``` ❌ **DON'T:** Repeat detection everywhere ```kotlin if (System.getProperty("os.name").lowercase().contains("mac")) { /* ... */ } ``` ### 2. Use expect/actual for Platform APIs ```kotlin // commonMain expect fun openFile(path: String) // jvmMain (Desktop) actual fun openFile(path: String) { Desktop.getDesktop().open(File(path)) } // androidMain actual fun openFile(path: String) { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(path))) } ``` ### 3. Graceful Degradation ```kotlin fun openBrowser(url: String) { try { if (Desktop.isDesktopSupported()) { Desktop.getDesktop().browse(URI(url)) } else { // Fallback: Copy to clipboard Toolkit.getDefaultToolkit().systemClipboard.setContents( StringSelection(url), null ) showMessage("URL copied to clipboard: $url") } } catch (e: Exception) { showError("Failed to open browser: ${e.message}") } } ``` ### 4. Test on All Platforms Always test platform-specific code on: - macOS (Intel + Apple Silicon if possible) - Windows (10/11) - Linux (Ubuntu/Fedora) --- ## Common Patterns ### Pattern: Config File Location ```kotlin fun getConfigFile(filename: String): File { val configDir = when (PlatformDetector.platform) { Platform.MacOS -> File("${PlatformDetector.userHome}/Library/Application Support/Amethyst") Platform.Windows -> File("${System.getenv("APPDATA")}\\Amethyst") Platform.Linux -> File("${PlatformDetector.userHome}/.config/amethyst") else -> File("${PlatformDetector.userHome}/.amethyst") } if (!configDir.exists()) { configDir.mkdirs() } return File(configDir, filename) } // Usage val settingsFile = getConfigFile("settings.json") ``` ### Pattern: Platform-Specific Resources ```kotlin fun getPlatformIcon(name: String): Painter { val extension = when (PlatformDetector.platform) { Platform.MacOS -> "icns" Platform.Windows -> "ico" else -> "png" } return painterResource("$name.$extension") } // Resources: // src/jvmMain/resources/app-icon.icns (macOS) // src/jvmMain/resources/app-icon.ico (Windows) // src/jvmMain/resources/app-icon.png (Linux) ``` --- ## References - [System Properties (Java)](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/System.html#getProperties()) - [Desktop API (Java)](https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/Desktop.html) - [File System Standards (XDG)](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)