
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:
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:
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
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 IDProgramArguments
: Program path and startup argumentsRunAtLoad
: 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
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
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 argumentsX-GNOME-Autostart-enabled
: Enables autostart in GNOMEX-GNOME-Autostart-Delay
: Delay startup to avoid conflictsX-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.