Core
MainActivity.kt: hosts the top-screen UI, maps controller buttons (A select, B back; X, Y, Start, Select, L1, R1 go toButtons.events), shows the bottom screen and records play time on return.Platforms.kt+assets/platforms.json: systems and their emulators ("players"). The JSON is generated bytools/platforms.py(edit that, then run it); users can override it withROMs/tandem/platforms.json.Platform.playerslists installed emulators, default first;allPlayerslists all.CocoonPlatforms.kt: reads Cocoon/Daijisho platform files fromROMs/platforms/*.jsonand turns theiram startlines into players.LaunchIntents.kt: builds a player's intent (templates like{file.uri},{file.id},{tags.x}, typed extras),GameFiles(ids inside .dpt/.pcgame),LaunchDisplay,LaunchProblem.Launcher.kt:Launcher.launch(ctx, game)checks the game can start, plays the launch animation (ui/LaunchOverlay.kt) and starts it off the main thread: resume via Shizuku, document-URI grants, launch display, play sessions (Sessions).Emulators.kt: installed apps, kept current by package broadcasts.AndroidApps.kt: launchable apps and the Android Games system.Library.kt: scansROMs/<folder>(subfolders become groups);ArtIndexreadsROMs/downloaded_media/<system>/<kind>/.Collections.kt: favorites, most played, genres, smart folder contents and other lists from metadata, cached per library change.Selection.kt: what is highlighted (drives the bottom screen), button events, sharedLibrariesstate.GameDelete.kt: which files deleting a game removes and the root/symlink safety checks (popup inui/DeleteGame.kt).Running.kt,RunningTasks.kt: one cached Shizuku call (psplusdumpsys activity activities) says which emulators have a game open;RunningGamesmaps them to games. A leftover process alone never counts as running.Chords.kt: Select/Start holds and combos,Idle,Screensaver,Privacy(private mode).PrivacyPin.kt: the private mode PIN (PinCodehashing and waits,PinVaultin the no-backup folder) andPrivateMode(on, off and reveal behind it); the keypad isui/PinPad.kt.Offline.kt:NetGate(nothing goes online in private mode) and airplane mode while a hidden game plays (OfflinePlan, through Shizuku).HiddenFolders.kt: hidden ROM folders and systems (paths relative to ROMs) and hidden icon-grid folders. AskPrivacy.isHidden/canShow, neverGameMeta.hiddendirectly.DiscordPresence.kt: the optional Discord status (play session in, Rich Presence out), behindDiscordSdk; the backend issrc/discord(Kotlin plus a small JNI bridge to Discord's C++ Social SDK) when the SDK is inthird_party/discord, else the no-opsrc/nodiscord. Settings inui/DiscordSettings.kt. Its picture is the game's cover address (data/Covers.kt:GameMeta.coverUrlfrom the scrape, else a lazy lookup;CoverUrl.safelets only credential-free addresses out), or the Tandem icon.Achievements.kt: RetroAchievements client and ROM hashing (RaHash); network only when signed in, never for hidden games.
Data
data/Store.kt: persistent JSON infilesDir(library.jsonper-gameGameMeta,settings.json). Both are Compose state; update withStore.update(key) { it.copy(...) }/Store.updateSettings { ... }.data/Look.kt: every appearance, layout, prompt, screen and sound setting (Settings.look);nullfields follow the theme.data/SystemPrefs.kt(systems.json): per-system switches, default emulator, bottom screen, extra folders, single-screen mode.data/ScanCache.kt(scan-cache.json): folder listings, so startup shows the last scan at once and rescans only changed folders.data/Importer.kt: importsgamelist.xml(ES-DE, Cocoon backups).data/HomeStore.kt:home.json,sessions.json(play history),achievements.json(excluded from Android backups).data/ExtrasStore.kt:extras.json,journal.json.data/IconGridStore.kt:icon-grid.json.data/WidgetStore.kt:widgets.json.data/ControllerStore.kt:controllers.json. Each store hasreload().data/Backup.kt: versioned backups inROMs/tandem/backup/<yyyy-MM-dd_HH-mm-ss>/with amanifest.json, in four categories, restored category by category; automatic ones before big changes (last five kept). UI inui/BackupScreen.kt.
Scraping
data/Scraper.kt runs the job (singleton scope, pauses in the background) and the per-game worker, which asks
sources in the user's order, each only for what is still missing. ScrapeHits.kt (RunSources: lookup and pace
per source), ScrapeSources.kt (network, saving media, libretro, ScreenScraper, SteamGridDB, matching, hashing),
ScrapeApis.kt (IGDB, HowLongToBeat), LaunchBox.kt (pure Kotlin streaming parser and on-device index) and
LaunchBoxDb.kt (download, monthly refresh), DetailsFill.kt, ScrapePlatforms.kt, SourceTests.kt,
ScrapeConfig.kt (settings and keys, in their own private file). GameMeta.lockedFields marks user edits a scrape
keeps. UI: ui/ScrapeScreen.kt, ui/ArtSetup.kt, ui/GameEditor.kt. See SCRAPING.md and
PRIVACY.md.
UI foundations
ui/App.kt: routes (Route,Nav), library scans (LibraryScan, run byui/ScanRunner.kt), toasts, setup screen, top bar.ui/StartFlow.kt: the cold start.MainActivity.onCreatestarts the scan early (LibraryScan.startEarly). The first draw waits briefly for the cached library, and both screens hold their content until the home is composed and then play the swap's arrival (SwapFx.introHold/introPlay). The splash (StartSplash) shows over the wallpaper only while the library isn't ready.StartLook.ktkeeps the starting window (@style/Tandem.Start*) in the last theme's colour. Details in PERFORMANCE.md.ui/Design.kt:Palette, Nunito type,TandemTheme, thePipmascot,FocusCard(the focusable card everything is built from),ButtonHint,Pill.ui/Focus.kt:FocusMemory,rememberFocusTarget,Modifier.takesFocus,KeepInView, popupLayers(only the top layer takes focus),InterceptButtons.ui/Edges.kt:FocusRoom,FocusRoomSide,fadingEdges,PopupRoom,bleed,onArt. Follow UI-RULES.md for every new screen and popup.ui/Overlays.kt: Y game menu, view menu, X search,Confirm.ask,OverlayPanel.ui/ActionMenu.kt: every Y menu (the item lifts out over a blurred screen).ui/TextEntry.kt: the controller keyboard.ui/Theme.kt:ThemeSpec,BuiltInThemes,Themes(live theme with cross-fade),Motion.- Smoothness (PERFORMANCE.md):
ui/Warm.kt(Facts,AfterFrames),ui/Ambient.kt(the shared ambient clock,AmbientFrames),ui/QuietSemantics.kt(no accessibility tree for services that can't read it),ui/Perf.kt(frame timing overlay, logcat tagTandemJank).
Screens and features
ui/Screens.kt: home, system and folder grids, collections, genres.ui/Cards.kt:GameArt,GameCard,GroupCard,SystemCard,ShrinkText.ui/Layouts.kt: carousel, list and console homes, games views,DockBar, folder popups.ui/LayoutPrefs.kt: grid sizes and card shapes.- Icon grid:
ui/IconGrid.kt(GridCatalog,IconGridHome, move mode, tile menu, user folders,FolderSheet),ui/IconArt.kt(TileArt),ui/IconPacks.kt(pack art and smart-folder art),ui/PanelMorph.kt(folder popups grow from their tile),ui/Spotlight.kt(the other screen in this mode). Smart folders:data/SmartRule.kt(the rule and its matching, pure),ui/SmartFolders.kt(tile, Y menu rows, launch screen),ui/SmartFolderForm.kt(editor and the other screen's preview). - Themes and art:
ui/ThemePacks.kt(Tandem and Cocoon theme packs; THEMES.md),ui/PackImport.kt,ui/ThemePacksScreen.kt,ui/SystemIcons.kt,ui/Backdrops.ktandui/GameBackdrops.kt(wallpapers and drawn backdrops),ui/Video.kt(LoopingVideo),ui/HeroArt.kt,ui/ArtPick.kt(which picture fills a screen),ui/LogoFx.kt(logo pop, sway and shine),ui/Appearance.kt,ui/ThemeGallery.kt,ui/Glyphs.kt,ui/TandemIcons.kt. - Controls:
ui/Controls.kt(Prompts,Clock),Gamepads.kt(connected pads, per-controller Nintendo layout,HomeButton),ControllerHomeService.kt(optional accessibility service for a controller's Home button; key events only),ui/ControllerSettings.kt. Controller passthrough (an external pad plays as the built-in one):PadLink.ktruns the helpersrc/padlink/padlink.cthrough Shizuku or su,PadMap.kt(translation table),PadPlan.kt(which pad, state machine),ui/PadLinkUi.kt(the offer on the companion). - Sound:
audio/is pure Kotlin (runs on a desktop JVM too): DSP, synth and acoustic voices, drums, a tracker-style song DSL, sequencer and mixer, the Meadow Tales and Pastel Arcade soundtracks, UI sound packs and jingles.ui/Sound.kt(Sounds,Music),ui/SoundEngine.kt(audio thread while music plays),ui/SoundSettings.kt. - Motion:
ui/Flair.kt(floating,riseIn,drift,PipMind; graphics-layer only, still with Reduce motion),ui/SwapFx.kt(the X screen swap). - Settings (SETTINGS.md):
ui/SettingsCatalog.kt(pages and groups),ui/Settings.kt(row types, two-pane layout),ui/SettingsHome.kt,ui/SettingsSearch.kt,ui/Presets.kt(Quick looks). - Home extras:
ui/HomeHost.kt(popups and polling while resumed),ui/Switcher.kt,ui/HomeMode.kt(Home app, dock, apps drawer),ui/QuickPanel.kt,ui/Stats.kt,ui/AchievementsUi.kt,ui/ScreensaverUi.kt,ui/HomeExtras.kt(Resume page, Recently played strip),ui/StartPanel.kt,ui/Welcome.kt,ui/QuickFind.kt,ui/QuickJump.kt,ui/CocoonSetup.kt(Import my Cocoon setup, with undo). - Built-in apps:
ui/BuiltInApps.kt(registry,Route.Tool),ui/GalleryApp.kt,ui/JournalApp.kt,ui/FilesApp.kt,ui/ControllerTestApp.kt. - Widgets:
ui/Widgets.kt(framework, packing,WidgetHost),ui/WidgetKinds.kt,ui/WidgetEditor.kt,DeviceInfo.kt(storage, battery, memory, temperature). Widgets refresh withrememberNow/rememberPolled, which tick only while resumed.
The second screen
ui/BottomScreen.kt: the second display as aPresentation, showing details for the currentSelection. With the screens swapped it draws the library andMainActivityforwards the D-pad and A to it;ControlsGuard(ui/ScreenRoles.kt) swaps back if it cannot take the controls.GameCompanion.kt+ui/CompanionScreen.kt: the bottom screen during play.CompanionActivityopens on the second display before the game (own task, out of Recents,FLAG_NOT_FOCUSABLEso the game keeps the controller) and is dismissed once the presentation is back, or closes by itself once its game's task is gone (GameExit.kt). Modes:art,tandemandoff(the default for dual-screen emulators).CompanionRestore.ktreopens it if Tandem's process restarts mid-game; one left behind by a restart goes with the next dismissal.GameResume.ktbrings a running game back by its own task (never a new one).HomeBack.kt: Home with another app over a running game brings the game and its companion back first; a second Home goes to Tandem.SecondaryHome.kt+ui/SecondaryHomeScreen.kt: Tandem's secondary-display Home, shown when nothing else is on the second display and Tandem is the default Home. It issingleTop(Android refusessingleTaskfor a secondary home), not focusable and out of Recents. It shows the app in front or Pip over a still wallpaper, and dims after a minute. Android's own launcher opens only when the user picks it (openStockHome,openStockBottom).TandemDream.kt: Tandem as Android's screensaver. On the Thor, controller input goes to the presentation of the app behind a dream, so the dream shows its ownPresentationon the second display (DreamBottom) that wakes on any input, and every Tandem window checksDreaming.onKey/onMotionfirst.WakeGate(DreamWake.kt) swallows the rest of the waking press.
Conventions
- Controller first. Every screen is fully usable with the D-pad and A/B. Every focusable thing is a
FocusCard; screens pick the target withrememberFocusTarget, attachtakesFocusand keep it in view withKeepInView. Popups useOverlayPanel, which makes them a focus layer. - Sizes. The top screen is about 830×470 dp, the bottom about 470×540 dp (see UI-RULES.md). Keep text readable at arm's length.
- Style. Pastel
Palettecolours on the night background, rounded 14–26 dp corners, spring animations, Pip for empty states and messages. Friendly, plain text. - Privacy. No analytics or telemetry. Network only for features the user turns on. Never send hidden games anywhere.
- New features go in new files; keep edits to shared files small. A feature's settings are a
@Composable fun xxxSettings(): SettingSectionin its own file, added tosettingsCatalog(). - Build with
./gradlew assembleDebug(on NixOS,nix-shell --run './gradlew assembleDebug') before committing. Judge smoothness onassembleRelease; see PERFORMANCE.md.