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)