All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.8.0 - 2026-08-26
- The UI fits itself to the screen it is on. Every screen is laid out in points against a 640×480 design, and one zoom factor carries the lot — fonts, rows, gaps — so a desktop window and a 752×560 Flip panel show the same layout at the size each has room for, rather than the design at 1:1 pixels and text that shrinks with the panel. A panel only a little past the design keeps a whole zoom: fractional zoom lands glyphs between pixels, which reads as uneven type where there is no subpixel positioning, so the spare pixels become margin instead. A phone held upright is the same screen turned, so the fit measures long edge to long edge and the lists get a narrower measure, not smaller rows.
RETSEND_SCALEnow pins the base the setting is read over, instead of being the whole answer. Android no longer passes the display density — the fit lands on the same physical size at any density.
-
A muOS package,
retsend-muos.muxapp, installed from Archive Manager and listed under Applications rather than under Ports as the PortMaster build is. The launcher hands muOS's ownSETUP_APPthe pad and the governor, holds the device awake for the length of a transfer, and points saves atROMS/on the card that booted. B on the home screen quits, which muOS needs: its only exit is akill -9that would cut a transfer where it stands, so the app answers the key itself and closes the sockets on the way out. Other ports leave B inert — there the launcher owns quitting — and the launcher opts in withRETSEND_BACK_QUIT. The home tabs name the key in their hints only where it is on, so nowhere else grows a hint for a button that does nothing. -
A "UI scale" setting, 60% to 160% of what the screen itself asks for, stepped with ◂ ▸ on its row (L1/R1 still switch tabs, and the hints name both arrows so a screen with no pad can reach them). Persisted as
[display] scale, applied live. -
A send can be repeated from the History tab, with A on its row. The log now keeps the source paths of a send and the address it went to; the resend dials the peer under that alias as the radar has it now, so a device that changed address in between is still reached, and falls back to the recorded address when it is off the radar. Files deleted since are left out — the rest still go, and a toast says how many were missing. Sends of more than 64 files are logged without their paths rather than growing
history.jsonwithout bound, and their rows offer no resend. -
A history row can be dropped from the log, with X.
0.7.1 - 2026-08-24
- Pinned rows sit in a band of their own in the file browser, closed by a rule where the folder's own entries start. They lead every listing but belong to other folders, and a star alone left them reading as rows of the folder being looked at.
- The file browser opened inside the app's own folder on the Miyoo CFWs and
would not leave it.
/mnt/SDCARDwas no known mount point, so the only root left was$HOME— which those launchers point at the app folder for a writable path — and a root is the one directory B cannot leave. The card is a root now, and$HOMEonly becomes one when it sits outside every other.
0.7.0 - 2026-08-24
- An Allium package,
retsend-allium.zip, for the same Miyoo Mini Plus and Flip the OnionOS one serves. Allium is a launcher on the stock firmware, so the binary and the bundled SDL2 are the OnionOS package's; what differs is the layout (Apps/Retsend.pak/— only a.pakfolder counts as an app there) and the launcher. Allium keeps no kill helper and does nothing with MENU for an app, so the app answers the key itself, behindRETSEND_MENU_QUIT— set by that launcher alone, so OnionOS keeps its helper and its behaviour. - A spruceOS package,
retsend-spruceos.zip, for those same two devices. spruce readsApp/Retsend/as OnionOS does, so what the package adds is aconfig.jsonnaming the devices it belongs on and a launcher; the bundled SDL2 counts for more here, since spruce carries a Miyoo build of its own whose drivers answer to other names. MENU is the way out and genuinely the app's: spruce's own handler returns early for anything launched out ofApp/rather thanEmu/. The launcher writes a line into spruce's log when wifi is off in its settings, since an empty radar otherwise reads as a bug. - Retsend in Allium's Games tab, from a
ports/Retsend.port/folder the Allium package carries. Copied toRoms/PORTS/, the Ports Collection console runs itslaunch.sh, which hands over to the install underApps/— one copy of the app, and one identity on the network. SIGTERMcloses the app instead of killing it, so powering the device off or closing the lid stops the announcer and shuts the sockets down first, and a peer sees a transfer end rather than a connection that stopped answering. A second signal still kills outright, and the headless receiver is left alone.
- Starts on an Allium card, which carries neither
libGLESv2.sonorlibshmvar.so— an OnionOS card supplies both, and the bundled SDL2 names them inDT_NEEDEDwhile calling into neither.lib/fallback/holds empty stubs with the right SONAMEs, last onLD_LIBRARY_PATHso a real library still wins. - Fills the Miyoo Mini Flip's screen. Its panel is 752x560 and the bundled
drivers assumed the Mini's 640x480: the renderer scales by an integer factor and
centres what it draws, so the app ran as a 640x480 island in a black margin. The
bundled SDL2 is now our own fork of steward-fu's, which reads the framebuffer's
real size, reports it through
SDL_GetDesktopDisplayModeand takes its texture limit from it; the app opens its window at that size. A Mini and a Mini+ resolve to the same 640x480 they always did.
0.6.0 - 2026-08-20
- An Android build,
retsend-android-arm64.apk. SDL2's Android port loads the Rust code as a cdylib and enters it throughSDL_main, so windowing, the GLES path, gamepad input and the whole net stack are the ones the handhelds run — what is Android's alone is packaging, storage and the Back button, all of it behindcfg(target_os = "android")or additive Cargo entries. The system Back button is B and quits at the top level, and the activity holds aMulticastLockso the Wi-Fi driver stops filtering out the announces discovery is built on. It runs landscape, which is both the UI it already had and one less relayout to get wrong. The app asks for all-files access before starting: granted, received files land inDownload/where an emulator can find them; denied, it keeps to its own external folder, which file managers can't open on Android 11+. Built withandroid/scripts/build.sh; the port is written up in android/README.md. - Touch and mouse input, which the UI had never read: it is painted from a
cursor that only a pad or the keyboard moved, so on a phone there was nothing
to press. A tap now becomes the same
AppCommanda button emits — on a row it places the cursor there and confirms, so tapping a device sends to it and tapping a file picks it, and each slot of the footer hint bar is the button it names, which is what makes Start/Select/X/Y reachable without a pad. The same applies to a desktop mouse, where clicking used to do nothing. RETSEND_BROWSER_ROOTS(:-separated) adds file-browser roots a launcher knows and no built-in candidate could name, andRETSEND_ALIASseeds the device name where there is no hostname to read. The Android activity passes its storage volumes andBuild.MODELthrough them.
0.5.5 - 2026-08-18
- The OnionOS package carries an SDL2 built from
steward-fu/sdl2 instead of the copy taken
from the Ports-Collection. The old build declared two texture formats but
filled slots 0 and 2 of the table, so SDL read RGB565 and a zero and could
offer no 32-bit format at all: every frame was converted down to RGB565 on the
CPU before the panel saw it, and flat fills and thin dividers banded. This one
declares RGB565 and ARGB8888, so the frame reaches the SoC's blitter as it was
drawn. It also drops that build's
SELECT + R1scaling hotkey, which ate the R1 press that pages a list. Its video driver is namedMinirather thanmmiyoo, so the launcher names the render driver and asks for the pad layout outright, andlibSDL2_imageandlibSDL2_ttfare gone from the bundle with the fonts and image codecs they pulled in.
- The rendering artifacts on the devices that fall back to SDL's renderer, the Miyoo among them, are gone — fixed in the egui-sdl2 releases the app has moved onto. egui's colours are premultiplied and SDL's blend multiplied by alpha a second time, so a panel's fade-in flashed close to black for a few frames; a feathered edge ran to black rather than to invisible, drawing a dark line along the bottom and right of the screen; and the frame the blit path presents was blended with the window instead of replacing it, dimming everything short of opaque. The three of them reached a release in 0.5.3, where they went unrecorded here; the banding above is this release's.
0.5.4 - 2026-08-13
- Receiving from LocalSend 1.18+. It streams uploads through reqwest, which
frames the body
Transfer-Encoding: chunkedinstead of sending a Content-Length, and the server answered every chunked request with 411 — the sender sawstatusCode(status: 411). Chunked bodies are now decoded; extensions and trailers are dropped, and a Content-Length sent beside chunked framing is ignored rather than trusted. Transfer codings we can't decode moved from 411 to 501.
0.5.3 - 2026-08-13
- Sending to a receiver that requires client authentication. LocalSend's newer
core makes a client certificate mandatory whenever it isn't also serving its
web UI, and we presented none, so the handshake died before the first request
and the transfer read Send failed: certificate required. Outbound requests
now carry our persisted identity — the same certificate the announced
fingerprint hashes. It is loaded even with
https = false, since a peer announces its own scheme and we can be the client of an HTTPS receiver while serving plain http.
0.5.2 - 2026-08-08
- The settings list scrolls. Its nine rows are taller than a 640x480 screen has
room for, and the list had no scroll area, so the last row (About) was cut off
at the panel edge — the cursor still moved onto it, off-screen. A smaller panel
or a larger
RETSEND_SCALEcut it sooner.
0.5.1 - 2026-08-08
- The tab bar leads with Receive, the tab the app opens on. Landing mid-bar meant left and right stepped off the landing screen in two unrelated directions.
- Button hints name what the button does. A on the radar reads Choose files, not "Send" — it opens the file browser, and Start is what sends. Select on Receive reads Announce rather than the radar's "Refresh": there is nothing to refresh there, the button re-multicasts our announce. X on an incoming request reads Save to… rather than "Folder".
- Settings rows are grouped: the settings that decide what happens to an incoming
file now run unbroken, and Port — set once, if ever — moved down next to About.
Aliasis Device name,Save tois Save folder, andExisting filesasks If a file exists. - A settings row's action moved out of the row and into the footer, where every other screen keeps its buttons.
- The radar badges only peers announcing plain HTTP, in red. Encryption is the norm now, so an HTTPS badge on every row was noise and the exception was not visible as an exception.
- History rows use the tab bar's arrows for direction — ↓ came in, ↑ went out — instead of a second, unrelated arrow language.
- A transfer's ETA reads in minutes and hours once it passes a minute ("5m 05s"), rather than counting seconds to "~305s".
- The Receive screen shows the folder incoming files land in, under the address.
- The destination picker's countdown turns red for its last ten seconds.
0.5.0 - 2026-08-08
- Folders are received as folders. Protocol v2 carries a folder transfer as
files whose
fileNameholds the relative path, which the receiver flattened to a basename; it now rebuilds the sender's tree under the save folder, up to eight levels deep, with every component sanitized on its own so a path still cannot leave the save folder. Files that arrive inside a folder skip the save routes — a folder lands whole instead of split across console folders — and loose files route as before. Settings → Received folders switches it off ([transfer] keep_folders = false) for the old flattening.
0.4.1 - 2026-08-05
- A held direction wakes the event loop once per auto-repeat instead of spinning it. The loop stopped blocking on the event pump at all while a direction was down, so it ran a core flat out to fire repeats at the configured interval; the block is now bounded by the earlier of egui's repaint request and the moment the next repeat comes due.
- The browser and history lists lay out and paint only the rows in view. A ROM folder can hold thousands of entries, and every one of them was a laid-out, painted row on every frame. History rows differ in height depending on whether they carry a path line, so their tops are prefix sums rather than a fixed step.
- A frame builds the data for the one screen it draws. Peers, history rows and save routes were all assembled every frame no matter which screen was on top, so covered screens still paid for their allocations; the base-screen precedence the render pass already followed now picks what to build. The peer and history counts the command router clamps its cursors against are from the last frame that showed those tabs.
- Directory listings sort on a cached lowercase key — one allocation per entry rather than two per comparison.
- The About screen names the author, next to the version, build date, commit and project URL.
0.4.0 - 2026-08-04
- Left and right switch tabs, on every tab — the tab bar no longer needs the shoulder buttons, which some of these pads place awkwardly. L1/R1 still do it. The port is typed on the on-screen keyboard now (A on the row) rather than stepped one number at a time with left/right, which was the only thing left holding those two directions; every other settings row was already on A.
- The ARM binaries are cross-built with
cargo-zigbuild, which puts the glibc floor in the target triple instead of pinning the build to whichever distro ships that glibc — no archived apt suites in the pipeline. One build per arch feeds every package, so the armhf binary in the PortMaster zip is the same file the OnionOS zip carries.tools/arm/build.shruns the same thing locally.
- An OnionOS package for the Miyoo Mini Plus and Flip (the plain Mini has no
wifi):
retsend-onionos.zipunzips at the card root intoApp/Retsend/, built and released by a workflow of its own. It ships the SDL2 the platform's own ports use and preloads it the same way, keeps the config and received files on the card, skips the GL probes the SSD202 cannot answer, and wires MENU to quit throughpressMenu2Kill— nothing on these devices raises an SDL quit event.
- The armhf binary starts on a glibc 2.28 userland, and the aarch64 one now
loads on older ArkOS. Both were built against whatever glibc the CI runner
had — 2.31 for armhf, 2.35 for aarch64 — so armhf asked for
log,log2,pow@GLIBC_2.29andgettid@GLIBC_2.30on a 2.28 userland and the loader refused it beforemain(). Both are built withcargo-zigbuildagainst a pinned glibc 2.28 now, and CI fails the job if a reference above the floor comes back.port.jsonstates that floor for PortMaster. - Text is no longer clipped along the bottom of every glyph on the Miyoo Mini Plus and Flip, and the UI no longer stutters. Both came from the same place: egui's meshes were rasterized as triangles, and the SDL 2.26 those devices' SDL2 forks are built on drops the last row of a textured triangle. Glyphs and plain rectangles are blitted now instead, which is also cheaper than per-pixel triangle work on a CPU with no GPU behind it.
- Buttons work on the Miyoo Mini Plus and Flip. Their SDL2 offers the pad as a
joystick with no gamepad mapping and sends key presses instead (A is Space, B
is Left Ctrl, Start is Return), none of which matched the desktop bindings.
The layout is picked from the
mmiyoovideo driver, or byRETSEND_KEYMAP.
0.3.0 - 2026-08-02
- Devices can be added to the radar by IP: X on the Send tab types
ip[:port](port defaults to 53317) and registers with whatever answers there — the way in on networks where multicast discovery never arrives, like guest Wi-Fi and AP-isolated access points. HTTPS is tried before plain HTTP,/infobefore giving up, and a hand-added device stays on the radar instead of expiring after two minutes of silence. It still has to be routable: this replaces discovery, not the network path. - The incoming-request modal takes a folder: X opens the browser, Start drops that transfer into the folder on screen. The pick is the whole answer to "where" — save routes are skipped, so nothing gets moved out of the folder just chosen. The request keeps counting down while you browse, so the header shows the seconds left before it auto-declines.
- armhf builds for 32-bit handhelds: a
retsend-linux-armhf.ziprelease asset, and both binaries in the PortMaster zip, picked byDEVICE_ARCHat launch. - Missing GL is no longer fatal: GLES 3.0, then desktop GL, then SDL's renderer
(software if that is all there is).
RETSEND_SOFTWARE=1skips GL outright. Every build needs SDL 2.0.18 or newer on the device now — that painter callsSDL_RenderGeometry.
0.2.1 - 2026-07-30
- PortMaster: received files default to the ROMs folder on muOS too. The launcher
took PortMaster's
$directoryfor the ROMs root, which on muOS is the card root (/mnt/mmc) one level aboveROMS. Installs that already have a config keep their folder — change it in Settings.
0.2.0 - 2026-07-30
- Every footer now ends with A, so the confirm hint sits under the same thumb on all screens: the Send tab and the on-screen keyboard used to lead with it.
- The on-screen keyboard erases with X; B now just leaves it, instead of erasing and then closing once the buffer ran empty.
- The routes editor scrolls through the auto save routes: the cursor carries on past the add row into them, so a list longer than the screen no longer hides its last rows. They stay read-only — A does nothing there.
- History rows show the folder the files landed in (
→) or were sent from (←), so a save route that moved a ROM somewhere unexpected is visible without hunting for it. A session split across routes shows the first folder and how many more it used. Entries logged before this stay two-line. - The incoming-request modal names the routed destinations instead of the plain save directory, so the folders the files will actually land in are visible before pressing A — one line per folder when the save routes spread a request over several, up to four before the rest collapse into a count.
- X in the send browser selects every file of the current folder (and clears them again), so a folder full of saves goes out without walking the list. Subfolders stay out: the protocol only carries a flat list of files.
- Pinned paths: Y in the file browser pins the folder or file under the cursor,
and pinned rows lead every listing — a pinned folder is one press away from
anywhere in the tree, a pinned file can be sent without navigating
(
[transfer] pinned_paths). The send browser also reopens where the last send came from ([transfer] last_send_dir). - Automatic save routes: received ROMs are sorted into the console folders that
already exist in the save directory, using the folder names KNULLI, ROCKNIX,
and muOS each actually use. Nothing is created, and only extensions that name
exactly one console take part. Toggled from Settings → Auto save routes
(
[transfer] auto_routes, on by default); explicit[transfer.routes]entries still win.
0.1.0 - 2026-07-28
Initial release. A Rust LocalSend client built for handheld and gamepad-first use (Knulli, muOS, ROCKNIX), running on desktop Linux too.
- LocalSend protocol v2.1: multicast announce and
/registerdiscovery, shown as a live radar of nearby devices. - Receive flow: accept/decline dialog with countdown, progress with speed and ETA, and cancel from either side; quick-save mode auto-accepts.
- Send flow: gamepad file browser with multi-select and switchable roots, staged files, a waiting phase, and per-file progress.
- Encryption: the protocol's HTTPS mode, on by default — a self-signed identity generated once and served via rustls, announced as the certificate's SHA-256 fingerprint. Works with the official app's defaults both ways.
- Save routes: received files sorted into folders by extension, editable on the device from Settings → Save routes.
- Settings on device: alias, save folder, and port, applied live — the network layer restarts on a port change.
- History tab: persisted transfer log with a configurable limit.
- On-screen keyboard (3-layer grid) for text entry without a keyboard.
- Tabbed navigation (Send, Receive, History, Settings) and an about overlay.
- Headless
--receivemode for SSH and scripting. - Streaming writes with sanitized names, one transfer at a time, and a sweep of
stale
.partfiles at startup. - Brand wordmark and window icon.
- Builds for Linux x86_64 and aarch64, with PortMaster packaging.