Skip to content

Repository files navigation

Memory Pak

GitHub Release GitHub Release Downloads License

A cross-platform game collection tracker written in Rust. Memory Pak tracks consoles, games and toy-to-life collectibles (LEGO Dimensions, Skylanders and friends) as owned, favorite, wishlist and notes — across a desktop app, an offline web app, and a terminal.

Architecture

Everything that is actually Memory Pak lives in one crate. A frontend is a thin adapter over it, and adding another one does not touch the others.

                     ┌──────────────────────┐
                     │   memory_pak_core    │  catalog · query engine
                     │  (no UI, no I/O)     │  state · import/export
                     └──────────┬───────────┘
              ┌─────────────────┼─────────────────┐
     memory_pak_tauri    memory_pak_wasm     memory_pak_tui
     desktop + Android    browser / PWA        terminal
         (Svelte)           (Svelte)          (ratatui)

memory_pak_core has no dependency on a UI toolkit, a window system or a browser, and touches the filesystem only behind its optional fs feature. A frontend does three things:

  1. load a PersistedState (via store::FileStore, or however its host prefers) and build a MemoryPakApp from it;
  2. drive query_consoles / query_games / query_collectibles and render the views they return;
  3. save persisted_state() after a mutation.

All three shipped frontends read and write the same state.json shape and the same 2.0 export files, so a collection moves between them unchanged.

The compiled catalog

database/*.json (38,540 games, 92 consoles, 733 collectibles) is compiled by crates/memory_pak_core/build.rs into a single columnar binary blob that the crate embeds. The blob is read in place: every string the catalog hands out is a &'static str pointing into it, so opening the catalog costs one UTF-8 validation and no allocations.

Work that can happen at build time does:

  • Strings are deduplicated into one arena, and records reference them by index — publishers and developers repeat thousands of times.
  • Games are grouped by console (collectibles by collection) and sorted by slug, so filtering to one console is an index range and looking an entry up by id is a binary search.
  • Sort orders are precomputed as permutations, so listing by title or year sorts nothing at runtime. Sorting by status, which depends on the user's own collection, is derived by bucketing one of those orders.
  • Accent folding is generated for exactly the ~70 non-ASCII characters the catalog contains, so no Unicode normalization tables ship in the binary.

At runtime, collection flags are held in bit sets indexed by catalog position rather than a map keyed by id, which makes a filter check a bit test and "how many of this console's games are owned?" a popcount over a range. Queries walk a permutation, test the cheap filters first, and build a view struct only for the rows inside the requested window.

Measured on the full catalog (bun run bench), against the previous postcard + HashMap + sort-then-paginate implementation:

before after
Open the catalog 20.2 ms 0.46 ms
First page of all games 28.9 ms 0.08 ms
Search mario 60.3 ms 1.6 ms
Build the initial UI state 4.0 ms 0.01 ms
Embedded catalog size 4.60 MB 3.02 MB

Project structure

Memory-Pak/
├── crates/
│   ├── memory_pak_core/    # catalog, query engine, state, import/export
│   ├── memory_pak_tauri/   # Tauri 2 desktop + Android frontend
│   ├── memory_pak_tui/     # ratatui terminal frontend
│   └── memory_pak_wasm/    # wasm-bindgen frontend for the browser/PWA
├── frontend/               # Svelte 5 + TypeScript + Vite (Tauri and web)
├── database/               # consoles.json, games/*.json, collectibles/*.json
├── icons/                  # platform icons, shared by Tauri and the PWA
└── site/                   # Svelte landing page; built PWA is copied to dist/app/

Requirements

  • Rust 1.94.0 (pinned via rust-toolchain.toml); the workspace is Rust 2024 edition and needs 1.88 or newer
  • Bun 1.4.2
  • wasm-pack 0.14.0 and Tauri CLI 2.11.1 — needed only for the web and Tauri frontends; the terminal frontend builds with cargo alone
  • Tauri platform prerequisites for desktop/mobile builds (Xcode CLT on macOS, WebView2 on Windows, webkit2gtk and friends on Linux)
bun run setup   # wasm target, wasm-pack, tauri-cli, frontend and site deps

Development

bun run dev:web            # Svelte/Vite PWA dev server
bun run dev:site           # landing page dev server
bun run dev:desktop        # Tauri desktop app
bun run dev:android        # Tauri Android app
bun run dev:tui            # terminal app

bun run build:web          # production PWA build
bun run build:site         # production landing page build
bun run build:pages        # landing page + PWA ready to publish from site/dist
bun run build:desktop      # desktop build, no installers
bun run build:android      # Android package
bun run build:tui          # release terminal binary

bun run package:desktop    # installers for the current platform
bun run package:win        # Windows NSIS + MSI
bun run package:mac        # macOS DMG
bun run package:linux      # Linux .deb + portable .tar.gz

bun run fmt                # rustfmt
bun run lint               # clippy, warnings denied
bun run test               # Rust tests across the workspace
bun run bench              # query engine latency
bun run verify             # fmt + lint + test + wasm + frontend/site checks + web/site builds
bun run verify:full        # verify + Playwright + desktop build

Frontend-only scripts live in frontend/package.json; landing-page scripts live in site/package.json. Run them with bun run --cwd frontend <script> or bun run --cwd site <script>.

memory_pak_tauri enables Tauri's custom-protocol feature by default, which is what makes the window load the frontend bundled into the binary. That is a compile-time switch and is not implied by --release, so cargo build --release -p memory-pak-tauri gives a working app while tauri dev --no-default-features (what dev:desktop runs) turns it off to pick up the Vite dev server instead. Run bun run build:web first: the bundled assets come from frontend/dist as it stood when the Rust crate was compiled.

Icons under icons/web/ are the canonical PWA icon source. Vite serves them at /icons/... in dev and emits them to dist/icons/..., so there is no copy to keep in sync. Generated WASM bindings go to frontend/generated/wasm/ and are gitignored; the frontend scripts regenerate them before TypeScript or Vite runs.

Tauri mobile entry points are scaffolded through the standard CLI:

bun run android:init
bun run ios:init

The terminal frontend

cargo run -p memory_pak_tui
key
↑ ↓ / j k move · PgUp PgDn Home End jump
Tab / 1 2 3 switch between Consoles, Games, Collectibles
/ search, Enter to keep it, Esc to clear
o f w toggle owned / favorite / wishlist
n edit notes
s F g cycle sort, filter, console or collection
e export to memory_pak_export.json
q quit

It also runs without opening the UI:

memory-pak-tui --path                    # the file this and the desktop app share
memory-pak-tui --import collection.json  # merge in any Memory Pak export
memory-pak-tui --export collection.json  # write one out

Where your data lives

One file, shared by every native frontend. The desktop app and the terminal app open the same state.json, so you can mark something owned in one and carry on in the other:

platform
Windows %APPDATA%\Memory-Pak\state.json
macOS ~/Library/Application Support/Memory-Pak/state.json
Linux ~/.local/share/Memory-Pak/state.json (or $XDG_DATA_HOME)

A plainly named folder, not the reverse-DNS bundle identifier — this is something you open and back up, so it should read like a folder. The identifier still belongs to the installers and is unchanged.

Collections from earlier releases are migrated forward the first time they are loaded, newest location first: <data dir>/com.Aspenini.MemoryPak, then the older ProjectDirs path. The old file is left where it is, so an older build still finds its data.

Set MEMORY_PAK_DATA_DIR to put the file somewhere else, such as a synced folder; every native frontend honours it.

MEMORY_PAK_DATA_DIR=~/Dropbox/memory-pak memory-pak-tui

Writes are atomic — a temporary file and a rename — so an interrupted save cannot truncate a good collection. The frontends do not watch the file, so close one before making changes in the other, or the second one to save wins.

Android sandboxes every app, so it gets its own private copy in the app data directory. There is no other frontend on the device to share with.

The browser app cannot use that file at all. A web page has no filesystem access, so the PWA keeps its collection in one IndexedDB record in the memory-pak database, written debounced so rapid toggles coalesce.

Ordinary exports cross that gap, because there is only one export format and every frontend both writes and reads it — there is nothing web-specific about the file. Export from the browser app and --import it here or open it with the desktop app's Import; export from either of those and load it with the browser app's Import button.

Ids not present in the running build's catalog — from a newer release, or a hand-edited export — are kept verbatim and written back out, so moving a collection backwards between versions does not lose anything.

Releases and updates

Desktop and Android installers are built locally; there are no packaging workflows in this repository. The landing page and web app deploy to GitHub Pages from master.

bun run package:win      # Windows NSIS + MSI
bun run package:mac      # macOS DMG
bun run package:linux    # Linux .deb + portable .tar.gz

Bundles land in target/release/bundle/. Attach them to a GitHub release yourself, together with checksums if you want them.

scripts/stage-release-artifacts.mts collects bundles plus checksums.sha256 into one directory.

The apps do not download or install updates themselves. They ask GitHub for the latest release and, if that tag is newer than the running version, prompt to open the release page. Linux packages still update through the distro or the portable tarball; the web app is a rolling deploy of site/dist/app. AppImage is not a supported release format.

Publishing the web app

Pushing to master (or running the Pages workflow by hand) builds the Svelte landing page and the PWA and deploys site/dist/ to GitHub Pages. The PWA is served from /app/. The first time, set the Pages source to GitHub Actions under Settings → Pages.

To build the same tree locally:

bun run build:pages

Export format

{
  "version": "2.0",
  "exportedAt": "2024-01-01T00:00:00Z",
  "entries": [
    {
      "id": "console:nes",
      "owned": true,
      "favorite": false,
      "wishlist": false,
      "notes": "My original NES"
    },
    {
      "id": "game:nes/super-mario-bros",
      "owned": true,
      "favorite": true,
      "wishlist": false,
      "notes": ""
    }
  ]
}

Every frontend writes and reads exactly this, through the same two functions in memory_pak_core; there are no per-frontend variants, so any export imports anywhere.

Entry ids are stable and derived from slugs — console:nes, game:nes/super-mario-bros, collectible:legodimensions/batman — so exports stay readable and diffable, and are sorted for byte-identical repeat exports.

About

A cross-platform retro game tracker powered by Rust and Tauri

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages