1package soy.iko.opencode.data.repo
3import android.content.Context
4import kotlinx.coroutines.Dispatchers
5import kotlinx.coroutines.sync.Mutex
6import kotlinx.coroutines.sync.withLock
7import kotlinx.coroutines.withContext
8import kotlinx.serialization.Serializable
9import kotlinx.serialization.decodeFromString
10import kotlinx.serialization.encodeToString
11import kotlinx.serialization.json.Json
14/** A minimal session summary persisted for the home-screen widget and launcher shortcuts. */
16data class RecentSession(val id: String, val title: String)
19 * Persists a short list of recent sessions to a small JSON file under filesDir so app-external
20 * surfaces (the home-screen widget's [android.widget.RemoteViewsService] and the "Resume last"
21 * launcher shortcut) can read them without a live connection or in-memory state. Both the app
22 * and the widget's RemoteViews factory run in the same process, so a plain file is sufficient.
24object RecentSessionsStore {
26 private const val FILE = "recent_sessions.json"
29 private val json = Json { ignoreUnknownKeys = true }
31 /** Serializes concurrent [write]s. The synchronous [read] (called from the widget's
32 * RemoteViewsService) can't take a suspend lock, so [write] instead swaps the file in
33 * atomically via a temp-file rename — the reader always sees either the old or the new
34 * complete file, never a half-written one. */
35 private val writeMutex = Mutex()
37 private fun file(context: Context) = File(context.applicationContext.filesDir, FILE)
39 /** Synchronous read — safe to call from the widget factory (bounded, tiny file). */
40 fun read(context: Context): List<RecentSession> {
41 val f = file(context).takeIf { it.exists() } ?: return emptyList()
42 return runCatching { json.decodeFromString<List<RecentSession>>(f.readText()) }
43 .getOrDefault(emptyList())
46 /** Persist [sessions] (capped to [MAX]); a no-op empty list writes an empty file. */
47 suspend fun write(context: Context, sessions: List<RecentSession>) = withContext(Dispatchers.IO) {
50 val target = file(context)
51 val encoded = json.encodeToString(sessions.take(MAX))
52 val tmp = File(target.parentFile, "$FILE.tmp")
53 tmp.writeText(encoded)
54 // Atomic replace on POSIX filesystems (app's internal storage); the reader
55 // never observes a torn file. Fall back to a direct write if rename fails.
56 if (!tmp.renameTo(target)) {
57 target.writeText(encoded)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list