Skip to content

LocalSend

P3DROVFX edited this page Oct 12, 2026 · 1 revision

LocalSend

LocalSend is an open, cross-platform "AirDrop for everyone": files and text go straight between devices on the same Wi-Fi, encrypted, with no account and no cloud. The shell makes your desktop both a receiver (an incoming-transfer popup with Accept / Decline) and a sender (drop a file on the island, pick a device, done), using the official localsend-cli under the hood.

If your phone runs Android, LocalSend is the quickest no-setup option; KDE Connect is the richer, paired alternative. Both live side by side in the shell.

Quick start

  1. Install the CLI. Open Settings → Devices & Phone → LocalSend. If localsend-cli is not found, press Install — it downloads the prebuilt binary for your OS and architecture from LocalSend's GitHub releases into ~/.local/bin/localsend-cli (no Rust toolchain needed on x86_64 / aarch64 Linux or macOS). The page shows the installed version, the install log and the equivalent manual command. On other architectures the installer prints cargo build --release instructions instead.
  2. Receiving starts by itself (localsend.autoStart, on by default) when the shell starts. Your desktop appears in the LocalSend app on your phone under its alias.
  3. Receive: send from the phone. A popup (or a notification, see preferPopupOverNotification) shows who is sending, the files and their sizes, with Accept / Decline. Accepted files are saved to localsend.downloadPath (default: ~/Downloads/localsend) and a "saved" summary appears. Text sent to you arrives as text.
  4. Send: use any of —
    • Drop files on the Dynamic Island (it opens a drop area while you drag),
    • drag files onto the Clock widget in the bar,
    • the LocalSend quick toggle in the Dashboard,
    • More actions in the launcher (Ctrl + K on a file result — Launcher & Search), — then choose the destination device from the list of devices found on the network.
qs -c ii ipc call localsend start     # start the receiver
qs -c ii ipc call localsend stop
qs -c ii ipc call localsend status    # JSON: available, running, cliVersion, pyteAvailable…

What the shell adds

  • Popup anchoring. The transfer popup docks to the bar's edge and stays visible when a sidebar opens on the same side, shifting instead of closing (with Bluetooth connection and Color Picker popups).
  • Island integration. An active transfer shows in the island as progress ("Live progress" in Bar & Dynamic Island), and the island is a drop target for files.
  • Quick toggle to switch the receiver on and off, and the "Send file" actions from a Dashboard dialog (a queued file opens the device list on the next sidebar open).
  • Notifications (showNotifications) for incoming and finished transfers; preferPopupOverNotification picks the popup when both would fire.

How it works (the interesting part)

The official CLI is a fully interactive terminal program (crossterm + ratatui): receiving needs you to press Y / N / P, and sending opens a full-screen device list navigated with the arrow keys. There is no --json or headless mode. So the shell does not reimplement the protocol; it drives the real program through a pseudo-terminal:

LocalSend.qml ──JSON events──  services/localsend_bridge.py  ──pty──  localsend-cli (official, Rust)
   (UI, popup)                    reads the screen, writes keys           (protocol v2.2, TLS, checksums)
  • Receive mode — the CLI's log view is plain appended text, never an alternate screen. The bridge runs an ANSI tokenizer, drops lines that get erased (the status line redraws itself), and feeds complete lines to a small state machine that reconstructs the multi-line blocks (R <alias> → Files (N, size): → file lines → Accept? Y/N/P) and re-emits JSON events: ready, device, incoming, text, saved, cancelled. Accepting or declining just forwards y / n / p to the pty as a keystroke.
  • Send mode — --file opens the device list immediately. The bridge uses pyte, a headless terminal emulator, only during that alternate-screen window, reads the rendered rows (stripping the box borders), finds the target device by IP, and presses Down the right number of times and Enter. This is the only part that needs the pyte Python module (pip install --user pyte); receiving works without it and a missing pyte is shown as a separate warning, not as "LocalSend unavailable".
  • Sizes are approximate in the UI — the CLI prints only formatted sizes like "1.2 MB"; the bridge reverses that for display. Integrity is never affected: the bridge never reads or writes a file's bytes, so the checksum verification is the official implementation's, end to end. (An earlier version used a third-party Python re-implementation that started corrupting files when the LocalSend app moved to protocol v2.2; replacing it with the official CLI removed that class of bug by construction.)
  • One port, one process. The CLI is a combined sender + receiver + discoverer bound to a single port (53317), with no client-only mode. To send, the service remembers whether the receiver was running, stops it, waits 400 ms for the port, starts the one-shot send, and restores the receiver when it exits. There is therefore a brief window (seconds) during a send when you cannot receive — inherent to the CLI, not a bug.
  • Discovery is the shell's own (services/localsend_scan.py, multicast + subnet probing), independent of the CLI, so the device list is available before you pick a file.

The installer also removes the old, unrelated localsend-cli pip package and its orphaned certificate files before installing, because that package installed into the same ~/.local/bin/localsend-cli path.

Configuration (Config.options.localsend)

Key Default Meaning
autoStart on Run the receiver at login
downloadPath ~/Downloads/localsend Where accepted files go
showNotifications on Notify on transfers
preferPopupOverNotification on Popup instead of a notification for an incoming transfer

Troubleshooting

Symptom Fix
Page says the CLI was not found Install, or run scripts/localsend/install_localsend_cli.sh
Phone doesn't see the desktop Same network? A firewall must allow TCP/UDP 53317 (multicast discovery uses 224.0.0.167)
Sending fails with Address already in use Another instance holds the port; qs -c ii ipc call localsend stop, then retry
"pyte missing" warning Only the send flow needs it: pip install --user pyte
A release has no CLI binary LocalSend publishes the CLI only with some releases; the installer pages through releases to find the newest one that has it

Under the hood

Piece Role
services/LocalSend.qml The singleton: availability / version / pyte checks, receiver lifecycle, send orchestration, events → UI
services/localsend_bridge.py The pty bridge (tokenizer, receive parser, pyte device-list scraper)
services/localsend_scan.py Discovery
scripts/localsend/install_localsend_cli.sh Prebuilt-binary installer
modules/ii/localSendPopup/ The transfer popup
modules/common/models/quickToggles/LocalSendToggle.qml Quick toggle
modules/settings/configs/DevicesPhoneConfig.qml Settings

Related pages

← Back to Home

Clone this wiki locally