Skip to content

Contributing

This guide covers setting up a development environment, project architecture, and coding conventions.

Kyaraben uses a Nix flake for its development environment. If you have Nix installed:

Terminal window
nix develop

This provides Go, Node.js, golangci-lint, and other development tools.

Alternatively, install these manually:

  • Go 1.24+
  • Node.js (for the UI)
  • golangci-lint
  • just (task runner)

Install dependencies:

Terminal window
cd ui && npm ci
Terminal window
just dev # Run the Electron app in development mode
just test # Run Go tests
just lint # Run Go linter
just fmt # Format all code
just check # Run all checks (lint + test + typecheck)
just site-dev # Run the documentation site locally

See just --list for all available tasks.

The backend and CLI are Go. The UI is TypeScript (Electron + React). Both languages offer static typing, which the project relies on heavily.

Go types in internal/daemon/types.go are the source of truth for the daemon protocol. TypeScript types are generated from Go types using tygo to ensure the contract between components is enforced at compile time.

The project follows Martin Fowler’s distinction between fakes and mocks. Fakes are working implementations with shortcuts (an in-memory store instead of a real database). Mocks verify that specific methods were called with specific arguments, which couples tests to implementation details. Prefer fakes.

Test harnesses matter more than individual test cases. A well-designed harness that can spin up isolated environments, run commands, and assert on outcomes makes writing new tests trivial.

  • Unit tests cover pure logic
  • Integration tests use fakes for external dependencies
  • E2E tests invoke the real system

For UI E2E tests, follow the Playwright best practices. Prefer user-facing selectors like getByRole, getByLabel, and getByText.

The backend is a Go daemon that communicates with the TypeScript UI via JSON over stdin/stdout.

The core entities and their relationships:

  • System: a gaming platform (SNES, PSX, GameCube)
  • Emulator: an implementation that runs a system’s games (retroarch:bsnes, duckstation)
  • Provision: a user-provided file enabling emulation (BIOS, keys)
  • State: data an emulator produces (saves, savestates, screenshots)
  • EmulatorConfig: a configuration file for an emulator
  • KyarabenConfig: Kyaraben’s own configuration (config.toml)
  • Collection: where the user’s emulation data lives (~/Emulation)
  • Manifest: tracks what Kyaraben has installed and managed

The UI and CLI communicate through a daemon that handles long-running operations. The daemon uses JSON request-response over stdin/stdout.

Commands: apply, status, doctor, get_config, set_config, get_systems, preflight, sync_status, uninstall, and others.

Events: ready, result, progress, error, cancelled.

See .claude/skills/adding-emulator-support/SKILL.md for the complete process, including decision frameworks, code examples, provision strategies, and troubleshooting.

The goal is that users enable a system, run apply, and the emulator is immediately ready to use with paths pointing to the user store. If the user has data from another Kyaraben installation (ROMs, saves, BIOS files in the expected locations), Kyaraben should pick them up without reconfiguration.

Kyaraben assumes it is the only tool installing these emulators. If the user has existing installations from their package manager or other tools, Kyaraben may conflict with them. When Kyaraben uninstalls a system, it removes the emulator configuration it created.

The first-run experience should be “launch and play”, not “configure before you can play”. Suppress setup wizards and first-run prompts where possible.

When an emulator does not fully cooperate with Kyaraben’s configuration model, handle the workaround in the implementation:

  • Emulators that lack config options for certain paths need symlinks from their fixed data locations to the user store
  • Emulators that require user interaction (initial setup, BIOS installation through the emulator’s own UI) should surface this through the provisions system: the UI shows users where to place files, opens directories on click, and install-check provisions verify that required actions were completed
  • Game library configuration varies: some emulators can be pre-configured with ROM directories, others require the user to manually browse to them

The goal is that users can discover and resolve issues from within Kyaraben (UI or CLI) or the emulators themselves, without needing external documentation.

When adding support for an emulator, follow this priority order:

  1. Check for official versioned binary releases (AppImage preferred, then tarball, then plain binary). The URL must support version substitution for reproducible downloads.
  2. If no official binary exists, check for well-maintained unofficial builds (actively maintained, multiple contributors, builds from official source).
  3. For RetroArch cores, use the RetroArch buildbot archive.
  4. If no viable option exists, skip the system and document the blocker.

Rejected formats: Flatpak and Snap require system integration we do not want to force on users. Distribution packages (.deb, .rpm) could theoretically work by extracting their assets, but AppImages are preferred for simplicity.

When integrating an emulator, follow this priority order for configuring paths:

  1. Text-based configuration: if the emulator has a config file where paths can be set (INI, JSON, XML, TOML, etc.), use that. Point paths directly to the user store. This is the cleanest approach.
  2. Symlinks: if the emulator stores data in fixed locations that cannot be configured via text, learn the directory structure and create symlinks from those locations to the user store. Return symlink specs in GenerateResult.

Symlinks work by redirecting specific subdirectories from the emulator’s data location to Kyaraben’s standard locations:

~/.local/share/dolphin-emu/GC/ → ~/Emulation/saves/gamecube/
~/.local/share/dolphin-emu/Wii/ → ~/Emulation/saves/wii/
~/.local/share/dolphin-emu/StateSaves/ → ~/Emulation/states/dolphin/
~/.local/share/dolphin-emu/ScreenShots/ → ~/Emulation/screenshots/dolphin/

When symlinks work, the emulator runs identically to a manual installation. Prefer this over CLI flags like -u <dir> which create Kyaraben-specific launch commands that diverge from default behavior and complicate debugging.

Emulator integration requires manual testing on real hardware. Automated tests verify config generation, but only hardware testing catches issues with paths, controllers, hotkeys, and first-run behavior.

  • Adding a new emulator
  • Upgrading an emulator version
  • Changing config generation logic
  • Modifying path or symlink handling
  • Steam Deck (primary target)
  • Desktop Linux (secondary)
  • Controllers: Xbox-style (full features) and SNES USB (limited buttons)

Before manual testing, run automated checks to catch obvious issues:

Terminal window
just check # Lint, test, typecheck

Not all emulators support all features. See the emulator support table for what each emulator supports. Use it to know what to test and what to skip.

Source files are in internal/emulators/<name>/. Each emulator’s definition file contains:

  • StateKinds: which data types are synced (saves, states, screenshots)
  • ProvisionGroups: BIOS/firmware requirements
  • ConfigGenerator.Generate(): config patches, symlinks, and controller bindings

Verify paths by checking where files actually land after gameplay:

  1. Saves: play a game, save in-game, check ~/Emulation/saves/<system>/
  2. Savestates: create a savestate via hotkey (if supported), check ~/Emulation/states/<emulator>/
  3. Screenshots: take a screenshot via hotkey (if supported), check ~/Emulation/screenshots/<emulator>/
  4. BIOS: verify the emulator finds BIOS from ~/Emulation/bios/<system>/

For emulators using symlinks, verify the symlink exists and points to the correct user store location. Symlink specs are returned from each emulator’s ConfigGenerator.Generate() method.

For emulators with Controller: configured in the support table, test:

  1. Face buttons: verify A/B/X/Y map correctly (some emulators swap layouts)
  2. D-pad: all four directions register
  3. Analog sticks: left and right sticks work (if system has them)
  4. Shoulders: L/R buttons register
  5. Triggers: LT/RT register (analog for systems that support it)
  6. Start/Select: both buttons work

For multiplayer (Players > 1), connect additional controllers and verify each slot binds correctly.

Skip controller testing for emulators with Controller: auto (RPCS3, Vita3K); these emulators handle controller detection internally.

Verify kyaraben suppresses setup wizards and prompts:

  1. Delete the emulator’s config directory to simulate first run
  2. Run kyaraben apply to regenerate config
  3. Launch the emulator and verify no wizard or prompt appears

Check for these common issues:

  • Update check prompts (should be disabled)
  • Welcome dialogs (should be suppressed)
  • Controller setup wizards (should be skipped)
  • BIOS selection prompts (should use configured path)

Verify ES-DE launches games correctly:

  1. Add a ROM to ~/Emulation/roms/<system>/
  2. Open ES-DE and navigate to the system
  3. Launch the game and verify the correct emulator opens
  4. Verify fullscreen mode works (if configured)

Pass dependencies explicitly. No hidden instantiation deep in the call stack. Expensive instantiations happen at the composition root (main.go) and dependencies are threaded through constructors.

Define dependencies as interfaces where substitution is needed. Swap real implementations for fakes at construction time.

The Go daemon writes to ~/.local/state/kyaraben/kyaraben.log. Use the logging package for all Go log output:

var log = logging.New("apply")
func DoSomething() {
log.Info("starting operation")
log.Debug("details: %v", details)
log.Error("operation failed: %v", err)
}

Log levels: Debug (detailed debugging info), Info (significant events), Error (error conditions).

Make code self-evident. Do not write comments explaining what code does; if the code needs explanation, rewrite it to be clearer.

Use sentence-case in headings. Do not use bold text in documentation. Avoid em-dashes. No emoji.

Kyaraben should feel calm. Do not use language that implies urgency or alarm. Features should have few moving parts. The user should never feel nervous because Kyaraben is explicit about what is happening and what has happened.

Avoid phrases like “needs attention”, “action required”, or “warning” when a neutral description suffices. Instead of “3 files need attention”, say “3 local files” or “3 files only on this device”. State facts; let the user decide if action is needed.

The format is a brief actionable title in imperative mood, followed by a body explaining what changed and why, followed by a test plan:

Brief, actionable description
What changed and why. Use paragraphs, lists, or both.
## Test plan
Reproducible verification steps.

Use imperative mood in titles: “Add”, “Fix”, “Remove”. No trailing periods on list items. Use backticks for code references. Do not use bold text.