Skip to content
desktop app

Complete Guide to Enabling Auto-Start for KMP Desktop Apps

When developing cross-platform desktop applications, enabling auto-start on system boot is a common and important feature. This article provides a detailed guide on implementing auto-start functionality for Windows, macOS, and Linux using Kotlin Multiplatform (KMP), including interface design, platform-specific strategies, and full implementation.

All source code examples are based on my open-source project: crosspaste-desktop

1.Design

1.1Unified Interface

To support cross-platform auto-start, we first define a common interface:

AppStartUpService.kt

kotlin
interface AppStartUpService {
    fun followConfig()               // Set auto-start based on configuration
    fun isAutoStartUp(): Boolean     // Check if auto-start is enabled
    fun makeAutoStartUp()            // Enable auto-start
    fun removeAutoStartUp()          // Disable auto-start
}

1.2 Platform Dispatch

Create a factory class to dynamically choose the appropriate platform-specific implementation:

DesktopAppStartUpService.kt

kotlin
class DesktopAppStartUpService(
    appLaunchState: DesktopAppLaunchState,
    configManager: ConfigManager,
) : AppStartUpService {
    private val currentPlatform = getPlatform()
    private val isProduction = getAppEnvUtils().isProduction()
    
    private val appStartUpService: AppStartUpService =
        when {
            currentPlatform.isMacos() -> MacAppStartUpService(configManager)
            currentPlatform.isWindows() -> WindowsAppStartUpService(appLaunchState, configManager)
            currentPlatform.isLinux() -> LinuxAppStartUpService(configManager)
            else -> throw IllegalStateException("Unsupported platform")
        }
        
    override fun followConfig() {
        if (isProduction) {
            appStartUpService.followConfig()
        }
    }
    
    // Other method implementations...
}

2. macOS Implementation

2.1 Mechanism

macOS uses LaunchAgents for user-level auto-start. This is Apple's officially recommended method:

  • Config file location: ~/Library/LaunchAgents/
  • Scope: current user only
  • Trigger: automatically loaded after user login

2.2 Implementation

MacAppStartUpService

kotlin
class MacAppStartUpService(private val configManager: ConfigManager) : AppStartUpService {
    private val logger: KLogger = KotlinLogging.logger {}
    private val crosspasteBundleID = getSystemProperty().get("mac.bundleID")
    private val plist = "$crosspasteBundleID.plist"

    override fun makeAutoStartUp() {
        try {
            if (!isAutoStartUp()) {
                logger.info { "Make auto startup" }

                val plistPath = pathProvider.userHome.resolve("Library/LaunchAgents/$plist")

                filePersist.createOneFilePersist(plistPath)
                    .saveBytes("""
                        <?xml version="1.0" encoding="UTF-8"?>
                        <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN">
                        <plist version="1.0">
                        <dict>
                            <key>Label</key>
                            <string>$crosspasteBundleID</string>
                            <key>ProgramArguments</key>
                            <array>
                                <string>${pathProvider.pasteAppPath.resolve("Contents/MacOS/CrossPaste")}</string>
                                <string>--minimize</string>
                            </array>
                            <key>RunAtLoad</key>
                            <true/>
                        </dict>
                        </plist>
                    """.trimIndent().toByteArray())
            }
        } catch (e: Exception) {
            logger.error(e) { "Failed to make auto startup" }
        }
    }

    override fun isAutoStartUp(): Boolean {
        return pathProvider.userHome.resolve("Library/LaunchAgents/$plist").toFile().exists()
    }

    override fun removeAutoStartUp() {
        try {
            if (isAutoStartUp()) {
                logger.info { "Remove auto startup" }
                pathProvider.userHome.resolve("Library/LaunchAgents/$plist").toFile().delete()
            }
        } catch (e: Exception) {
            logger.error(e) { "Failed to remove auto startup" }
        }
    }
}

2.3 Key Configuration Fields

  • Label: Unique identifier, typically the app’s bundle ID
  • ProgramArguments: Program path and startup arguments
  • RunAtLoad: Set to true to start at login

3. Windows Implementation

3.1 Mechanism

Windows supports two installation types:

1. Regular install: use the registry key `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`
2. Microsoft Store: due to sandbox restrictions, use `shell:appsFolder` to launch the app

3.2 Implementation

WindowsAppStartUpService

kotlin
class WindowsAppStartUpService(
    appLaunchState: DesktopAppLaunchState,
    private val configManager: ConfigManager,
) : AppStartUpService {
    companion object {
        const val PFN = "ShenzhenCompileFutureTech.CrossPaste_gphsk9mrjnczc"
    }

    private val isMicrosoftStore = appLaunchState.installFrom == MICROSOFT_STORE
    private val appExePath = DesktopAppPathProvider.pasteAppPath.resolve("bin").resolve("CrossPaste.exe")
    private val microsoftStartup = "explorer.exe shell:appsFolder\\$PFN!$AppName"
    private fun getRegValue(): String = if (isMicrosoftStore) microsoftStartup else appExePath.toString()

    override fun makeAutoStartUp() {
        try {
            if (!isAutoStartUp()) {
                val command = "reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\" " +
                    "/v \"$AppName\" /d \"${getRegValue()}\" /f"
                Runtime.getRuntime().exec(command)
            }
        } catch (e: Exception) {
            logger.error(e) { "Failed to make auto startup" }
        }
    }

    override fun isAutoStartUp(): Boolean {
        val command = "reg query \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\" /v \"$AppName\""
        try {
            val process = Runtime.getRuntime().exec(command)
            val reader = BufferedReader(InputStreamReader(process.inputStream))
            var line: String?
            while (reader.readLine().also { line = it } != null) {
                if (line!!.contains("REG_SZ")) {
                    val registryValue = line.substringAfter("REG_SZ").trim()
                    return registryValue.equals(getRegValue(), ignoreCase = true)
                }
            }
        } catch (e: Exception) {
            logger.error(e) { "Failed to check auto startup status" }
        }
        return false
    }
}

4. Linux Implementation

4.1 Mechanism

Linux desktop environments follow the XDG Autostart Specification:

  • Config location: ~/.config/autostart/
  • File format: .desktop
  • Compatibility: supported by GNOME, KDE, XFCE, etc.

4.2 Implementation

LinuxAppStartUpService

kotlin
class LinuxAppStartUpService(private val configManager: ConfigManager) : AppStartUpService {
    private val logger: KLogger = KotlinLogging.logger {}
    private val desktopFile = "crosspaste.desktop"
    private val appExePath = pathProvider.pasteAppPath.resolve("bin").resolve("crosspaste")

    override fun makeAutoStartUp() {
        try {
            if (!isAutoStartUp()) {
                logger.info { "Make auto startup" }
                val desktopFilePath = pathProvider.userHome.resolve(".config/autostart/$desktopFile")
                filePersist.createOneFilePersist(desktopFilePath)
                    .saveBytes("""
                        [Desktop Entry]
                        Type=Application
                        Name=CrossPaste
                        Exec=$appExePath --minimize
                        Categories=Utility
                        Terminal=false
                        X-GNOME-Autostart-enabled=true
                        X-GNOME-Autostart-Delay=10
                        X-KDE-autostart-after=panel
                    """.trimIndent().toByteArray())
            }
        } catch (e: Exception) {
            logger.error(e) { "Failed to make auto startup" }
        }
    }

    override fun isAutoStartUp(): Boolean {
        return pathProvider.userHome.resolve(".config/autostart/$desktopFile").toFile().exists()
    }
}

4.3 Key Configuration Fields

desktop 文件的关键配置项:

  • Type: Must be "Application"
  • Exec: Startup command and arguments
  • X-GNOME-Autostart-enabled: Enables autostart in GNOME
  • X-GNOME-Autostart-Delay: Delay startup to avoid conflicts
  • X-KDE-autostart-after: Ensures KDE system tray is ready

5. Conclusion

Implementing cross-platform auto-start in KMP involves:

  • Designing a unified interface abstraction
  • Understanding and using each platform’s official recommendation
  • Handling platform-specific differences
  • Providing robust error handling and logging

This design ensures maintainable code and fully leverages native capabilities of each system, delivering a consistent and reliable experience for users.