Getting Started

Using Porter

Choosing an encoding, driving the sender, and the three ways to get a file back.
porter-sender myfile.pdf --fountain --slideshow

Open the receiver, point it at the terminal. That is the common case — the rest of this page is what the flags do and when they matter.

Sequential or fountain

This is the one decision worth making deliberately.

Sequential (default)Fountain (--fountain)
Frameindex|total|mode|id|payloadF|seq|K|fileSize|id|payload
A missed frameWait for the sender to loop back to that indexCosts nothing — catch a different one
Frames neededExactly the ones you're missing~1.33×–1.89× K, any of them
Good forSmall files, good light, a steady handLong transfers, poor light, handheld scanning

Sequential is fine for a few hundred chunks. Past that, one frame your camera consistently misses — a glare spot, a blur at the same point in every loop — stalls the whole transfer, and fountain is worth the overhead.

--base64 has no effect in fountain mode; fountain payloads are always base64.

Driving the slideshow

The sender is a full TUI: a QR grid, a sidebar showing chunk, progress, QR version, ETA, elapsed time and loop count, and a status line.

KeyAction
l k spaceNext frame
h jPrevious frame
Shift+/Scrub ~100 chunks
JJump to a specific chunk
GGap-fill mode — loop only the indices you name
+ / -Faster / slower
SToggle slideshow
IToggle the sidebar
QQuit

G is the sequential-mode rescue: when the receiver reports missing chunks 4,102 and 9,887, loop just those instead of the whole file again.

Speed

--speed is the delay between frames in seconds. The default works essentially everywhere; going faster trades reliability for time.

ValueRateConditions
0.52/secDefault — works everywhere
0.33.3/secGood lighting
0.25/secBright light, steady hand
0.110/secOptimal only

Dropped frames cost more than a slow rate does in sequential mode. In fountain mode they cost almost nothing, so push harder there.

--multi=N renders up to four QR codes per frame in a grid, multiplying throughput if the camera can resolve them all. A larger terminal also means a higher QR version and more bytes per code, so a maximised window is free throughput.

Resizing the terminal mid-transfer changes the QR version, which changes K and blockSize — forking the stream into one the receiver can't merge with what it already has. Size the window before you start.

Binary files and verification

Binary input is detected automatically and switches on base64; --base64 forces it. --verify=<file> checks the input against an existing SHA-256 file before sending.

Fountain transfers always end with a CHECKSUM frame, so the receiver verifies by default. Sequential transfers only send one when you ask.

By default the sender writes nothing to disk. --resume opts into persisting slideshow position in .porter_history; --reset ignores a saved position without discarding the file.

Receiving

Scanning starts automatically — there is no button and no session to establish, because the sender does not know you exist. Each decoded payload is parsed, handed to a worker isolate, and written to disk as it resolves. On completion the app verifies SHA-256 and saves to Downloads.

Fountain progress is not linear against K. Peeling needs materially more than K distinct symbols — 1.33× to 1.89× across real transfers — so a bar scaled to K sits at 99% with a third of the scanning left. The app scales to ~2×K.

Chunks are written to disk as they decode, one file per recovered block. If the app is killed, it rehydrates from those files on the next launch — no re-scanning what you already caught. Hydration trusts the chunk filenames, not the metadata file, which is debounced and can lag what is actually on disk.

Without a camera

Two subcommands finish a transfer when the bytes are already somewhere useful.

serve — over a network

porter-sender serve --port=8080 --output-dir=received

An HTTP receiver on axum. POST /upload accepts raw bodies, multipart/form-data, and QR-scan JSON — sequential, fountain and checksum frames alike — and auto-joins completed transfers.

join — from parts on disk

porter-sender join <transfer-dir|file|id> [--output <path>] [--force] [--no-verify]

Reassembles the .partaa, .partab, … files a receiver wrote, verifying against <base>.sha256 when one is present. The target can be a transfer directory, any file inside one, or a bare transfer id. Output goes to <base>.joined unless --output says otherwise; an existing destination is never overwritten without --force.

Stray files that merely share a .part prefix are skipped rather than concatenated in — a deliberate difference from the TypeScript implementation this was ported from.
Copyright © 2026