Wireless file transfer for retro handhelds.
retsend (retro + sending) is a LocalSend-protocol file-transfer
client written in Rust, using SDL2 for windowing and input and
egui for the UI. Receive ROMs from your phone
or PC to the device and send saves or screenshots back over wifi — no cable, no
SSH. Fully compatible with the official LocalSend apps.
It targets PortMaster-compatible Linux handhelds (Knulli, muOS, ROCKNIX — Anbernic/TrimUI/Miyoo class devices, gamepad-only, no compositor) and runs on regular desktop Linux too.
Getting files onto a handheld usually means pulling the SD card, plugging in a cable, or setting up SSH/SMB. Meanwhile every phone and desktop can run LocalSend. What's missing is the other end: a client built for a gamepad and a screen with no desktop behind it. This is that client — files move in both directions.
- Discovery (LocalSend protocol v2.1) — UDP multicast and the TCP
/registerexchange; a live radar of nearby devices. - Receive — accept/decline dialog with a countdown, streaming to
.part, - sanitized file names, speed/ETA, cancel from either side; a quick-save mode auto-accepts.
- Send — gamepad file browser with multi-select that survives folder navigation; per-file progress, cancel, partial accepts handled.
- Encryption — the protocol's https mode, on by default: a persisted self-signed identity, announce fingerprint = its SHA-256. Works with the official app's default settings both ways.
- Settings on device — alias, save folder picker, port; applied live.
- Headless —
retsend --receive: no screen, auto-accept, progress on stdout. For ssh sessions and scripting. - Small and simple — threads instead of an async runtime, a minimal hand-rolled HTTP server, system SDL2; the protocol stack is SDL-free and tested headless over real TCP.
Grab retsend-portmaster.zip from
Releases and unpack it
into your ports folder (e.g. /roms/ports/). Received files land in the ROMs root by default — change the folder in Settings.
System SDL2 is the only native dependency. On Debian/Ubuntu:
sudo apt-get install -y build-essential pkg-config libsdl2-dev
cargo runFiles passed as arguments (cargo run -- file1 file2) come pre-selected in the
send browser.
Two instances on one machine discover each other (multicast loopback) — handy for trying both sides of a transfer without a second device:
RETSEND_DATA_DIR=/tmp/ls-a cargo run &
RETSEND_DATA_DIR=/tmp/ls-b cargo runTests are headless (no SDL, no network setup needed):
cargo test| Pad | Keyboard | Action |
|---|---|---|
| D-pad / stick | Arrows | Navigate |
| A | Enter | Send to device · select file · accept · type |
| B | Esc | Back · decline · cancel · erase (keyboard) |
| Start | F1 | Jump to Settings tab · confirm send · OK (keyboard) |
| Select | Tab / F5 | Refresh radar · switch roots · layer (keyboard) |
| L1 / R1 | PgUp / PgDn | Switch tabs · page the file browser |
config.toml lives in the data dir (created with defaults on first run) and
everything in it is also editable from the Settings screen, except:
[network] https = false— fall back to the protocol's plain-http mode[network] announce_interval_secs— multicast announce cadence[transfer] browser_roots— extra mount points for the file browser
Received files land in save_dir by default. [transfer.routes] overrides
that per file extension — handy for dropping ROMs straight into each console's
folder. Edit it on the device from Settings → Save routes (type the extension,
pick the folder), or in the config:
[transfer.routes]
gbc = "gb" # relative → <save_dir>/gb
gba = "/roms/gba" # absolute → used as-is
png = "/roms/screenshots"Extensions match case-insensitively, folders are created on demand, and
anything without a matching route still lands in save_dir.
Environment variables override paths and control logging at launch:
RETSEND_DATA_DIR, RETSEND_CONFIG, RETSEND_SAVE_DIR, RETSEND_SCALE,
RETSEND_GLES=0|1, RETSEND_LOG_LEVEL, RETSEND_LOG_FILE,
RETSEND_PANIC_FILE.
