Wireless file transfer for retro handhelds.
retsend (retro + sending) is a Rust LocalSend
client for retro handhelds: send and receive files with your phone
or PC over Wi-Fi, no cable or SSH. Compatible with the official LocalSend apps.
It targets PortMaster-compatible Linux handhelds (Knulli, muOS, ROCKNIX — Anbernic/TrimUI class devices, gamepad-only, no compositor) and runs on regular desktop Linux too.
Getting files onto a handheld usually means pulling the SD card or setting up SSH/SMB. Phones and desktops already have LocalSend — this is the missing end: a client built for a gamepad and screen, moving files both ways.
- Discovery — a live radar of nearby devices (LocalSend protocol v2.1).
- Receive — accept/decline dialog with countdown, speed/ETA, and cancel from either side; quick-save mode auto-accepts.
- Send — gamepad file browser with multi-select and per-file progress; pin the folders and files you send often and they lead every listing.
- Save routes — received ROMs land in the console folder they belong to, detected from the card; per-extension overrides on top.
- Encryption — the protocol's HTTPS mode, on by default; works with the official app's default settings both ways.
- Settings on device — alias, save folder, and port, applied live.
- Headless —
retsend --receiveruns with no screen, for SSH and scripting. - Small and simple — no async runtime, a minimal HTTP server, system SDL2.
Grab retsend-portmaster.zip from
Releases and unpack it
into your ports folder (e.g. /roms/ports/).
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 · leave the keyboard |
| X | X / Bksp | Erase a character · take every file in the folder |
| Y | Y | Pin / unpin the row under the cursor |
| Start | F1 | 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[transfer] history_limit— max transfers kept in the History tab (default 200)
Received files land in save_dir by default, but ROMs are sorted on their own:
retsend looks for the console folders already present in save_dir and routes by
extension — .gba into gba, .sfc into snes, .ws into wswan. The folder
names come from KNULLI, ROCKNIX, and muOS themselves, which disagree (Mega Drive
is megadrive, genesis, and md respectively — all three are recognized), and
SD-card templates that rename folders to Nintendo Game Boy Advance (GBA) are
matched through the code in parentheses. Nothing is created for you: a console
folder that isn't there gets no route, so on desktop this does nothing. Only
extensions that name exactly one console take part — .zip, .iso, .bin, and
.md never do. Turn it off with Settings → Auto save routes or
[transfer] auto_routes = false.
[transfer.routes] overrides all of that per file extension, and wins over the
detected routes. 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.
X in the send browser takes every file of the folder you are looking at into
the selection, and gives them back on a second press. Subfolders are left alone:
the protocol has no notion of a directory, so a send is always a flat list of
files — see sanitize_filename for what that means on the receiving end.
Y pins whatever the cursor is on — a folder you send from often, or a single
file. Pinned rows lead every listing, so a pinned folder is one press away from
anywhere in the tree, and a pinned file can be selected and sent without
navigating at all. Pins live in [transfer] pinned_paths; the folder pickers list
only the pinned folders, and a pin on a card that isn't in the slot is skipped
rather than shown dead. The send browser also reopens in the folder your last send
came from ([transfer] last_send_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.
