Run on startup

This commit is contained in:
Rukira 2026-03-04 15:13:18 +00:00
parent 44656c81d5
commit 4036febb70
3 changed files with 133 additions and 4 deletions

View file

@ -19,6 +19,7 @@ import com.rukira.wowbackup.config.ConfigManager
import com.rukira.wowbackup.logging.LoggingSetup
import com.rukira.wowbackup.platform.OS
import com.rukira.wowbackup.platform.Platform
import com.rukira.wowbackup.platform.StartupManager
import com.rukira.wowbackup.platform.WoWLocations
import com.rukira.wowbackup.ui.Screen
import com.rukira.wowbackup.ui.trayIconPainter
@ -34,6 +35,9 @@ fun main() {
ConfigManager.load()
logger.info { "Config loaded. Configured: ${ConfigManager.isConfigured}" }
// Sync OS startup registration with persisted config
StartupManager.setEnabled(ConfigManager.config.value.runAtStartup)
val detectedWoW = WoWLocations.findWoWInstall()
if (detectedWoW != null) {
logger.info { "Auto-detected WoW at: $detectedWoW" }
@ -67,10 +71,6 @@ fun main() {
isWindowVisible = true
})
Separator()
Item("Backup Now", onClick = {
BackupScheduler.triggerBackupNow()
})
Separator()
Item("Quit", onClick = {
BackupScheduler.stop()
exitApplication()

View file

@ -0,0 +1,127 @@
package com.rukira.wowbackup.platform
import io.github.oshai.kotlinlogging.KotlinLogging
import java.io.File
private val logger = KotlinLogging.logger {}
object StartupManager {
private const val APP_ID = "com.rukira.wowbackup"
fun setEnabled(enabled: Boolean) {
val appPath = getAppPath()
if (appPath == null) {
logger.warn { "Cannot register startup: not running from a packaged app (dev mode?)" }
return
}
logger.info { "Setting launch-at-startup to $enabled (appPath=$appPath)" }
when (Platform.current) {
OS.Mac -> if (enabled) enableMac(appPath) else disableMac()
OS.Windows -> if (enabled) enableWindows(appPath) else disableWindows()
else -> logger.warn { "Launch at startup is not supported on ${Platform.current}" }
}
}
private fun getAppPath(): String? = when (Platform.current) {
OS.Mac -> {
// java.home is e.g. /Applications/com.rukira.wowbackup.app/Contents/runtime/Contents/Home
val javaHome = System.getProperty("java.home") ?: return null
val marker = ".app/Contents"
val idx = javaHome.indexOf(marker)
if (idx < 0) null else javaHome.substring(0, idx + ".app".length)
}
OS.Windows -> {
ProcessHandle.current().info().command().orElse(null)?.takeIf { it.endsWith(".exe", ignoreCase = true) }
}
else -> null
}
// --- macOS: LaunchAgent plist ---
private val plistFile: File
get() = File(System.getProperty("user.home"), "Library/LaunchAgents/$APP_ID.plist")
private fun enableMac(appPath: String) {
val plist = """
|<?xml version="1.0" encoding="UTF-8"?>
|<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
| "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|<plist version="1.0">
|<dict>
| <key>Label</key>
| <string>$APP_ID</string>
| <key>ProgramArguments</key>
| <array>
| <string>open</string>
| <string>-a</string>
| <string>$appPath</string>
| </array>
| <key>RunAtLoad</key>
| <true/>
|</dict>
|</plist>
""".trimMargin()
try {
plistFile.parentFile.mkdirs()
plistFile.writeText(plist)
logger.info { "LaunchAgent plist written to ${plistFile.absolutePath}" }
} catch (e: Exception) {
logger.error(e) { "Failed to write LaunchAgent plist" }
}
}
private fun disableMac() {
try {
if (plistFile.exists()) {
plistFile.delete()
logger.info { "LaunchAgent plist removed" }
}
} catch (e: Exception) {
logger.error(e) { "Failed to remove LaunchAgent plist" }
}
}
// --- Windows: Registry ---
private const val REG_KEY = """HKCU\Software\Microsoft\Windows\CurrentVersion\Run"""
private const val REG_VALUE = "WoWBackup"
private fun enableWindows(appPath: String) {
try {
val process = ProcessBuilder(
"reg", "add", REG_KEY, "/v", REG_VALUE, "/d", appPath, "/f"
).redirectErrorStream(true).start()
val exitCode = process.waitFor()
if (exitCode == 0) {
logger.info { "Registry startup entry added" }
} else {
val output = process.inputStream.bufferedReader().readText()
logger.error { "reg add failed (exit $exitCode): $output" }
}
} catch (e: Exception) {
logger.error(e) { "Failed to add registry startup entry" }
}
}
private fun disableWindows() {
try {
val process = ProcessBuilder(
"reg", "delete", REG_KEY, "/v", REG_VALUE, "/f"
).redirectErrorStream(true).start()
val exitCode = process.waitFor()
if (exitCode == 0) {
logger.info { "Registry startup entry removed" }
} else {
val output = process.inputStream.bufferedReader().readText()
// Exit code 1 with "not found" is expected when disabling and key doesn't exist
logger.warn { "reg delete exited $exitCode: $output" }
}
} catch (e: Exception) {
logger.error(e) { "Failed to remove registry startup entry" }
}
}
}

View file

@ -5,6 +5,7 @@ import com.rukira.wowbackup.config.AccentColor
import com.rukira.wowbackup.config.AppConfig
import com.rukira.wowbackup.config.ConfigManager
import com.rukira.wowbackup.config.ThemeMode
import com.rukira.wowbackup.platform.StartupManager
import com.rukira.wowbackup.platform.WoWLocations
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.flow.MutableStateFlow
@ -170,6 +171,7 @@ class ConfigViewModel : ViewModel() {
}
ConfigManager.save(_state.value.config)
StartupManager.setEnabled(_state.value.config.runAtStartup)
_state.update { it.copy(errors = emptyMap(), saveSuccess = true) }
logger.info { "Configuration saved" }
return true