package dev.stave.ui import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch /** * Coroutine exception handler shared by every ViewModel.launch site. * * Without this, uncaught exceptions from viewModelScope.launch bubble up to the JVM * default handler, which Compose Desktop / Swing surfaces as an opaque "Error" * dialog showing only the exception message — e.g. `index: 57, size: 57` after a * right-click → Play Next, with no stack trace and no way to recover gracefully. * * Logging here means the same exception becomes a stderr stack trace * (recoverable from `./gradlew :desktop:run` output or a shell-launched packaged * install) and the user keeps playing music. The app remains responsive. * * Note: This swallows the exception by *handling* it — viewModelScope's * SupervisorJob doesn't cancel siblings, so other launches continue normally. */ private val viewModelExceptionHandler = CoroutineExceptionHandler { _, throwable -> System.err.println( "[Earhorn] ViewModel coroutine threw (${throwable.javaClass.simpleName}):" ) throwable.printStackTrace(System.err) } /** * Like viewModelScope.launch, but installs [viewModelExceptionHandler] so an * unexpected throw becomes a logged stack trace rather than an error dialog. * * Use for any coroutine triggered by a user action (right-click handlers, * button clicks, menu items, drag callbacks, etc). Long-running collection * jobs (state flows, lifecycle observers) can also use this; the handler * only fires on actually-uncaught exceptions. */ fun ViewModel.safeLaunch(block: suspend CoroutineScope.() -> Unit): Job = viewModelScope.launch(viewModelExceptionHandler, block = block)