tmux with a popup file picker to attach images and docs to your terminal sessions over ssh or mosh
Find a file
2026-08-18 19:58:52 -07:00
logo Add a logo to the top of the README 2026-08-18 18:52:41 -07:00
scripts Fix the popup picker never finding the pane it was opened from 2026-08-18 19:58:52 -07:00
systemd Add a daily retention timer for the drop directory 2026-08-18 16:42:30 -07:00
inbox.tmux Add tmux-inbox: popup file picker for remote tmux panes 2026-08-18 15:30:34 -07:00
README.md Mark drops you have already attached so they stand out when shown 2026-08-18 19:32:59 -07:00

tmux-inbox

tmux popup file picker for attaching images and docs to a terminal session over ssh or mosh.

The problem

Programs that take a file take it by path, so "pasting" an image into one running on a remote host is really two problems: getting the bytes onto that host, and getting the path onto the command line.

Neither is solvable from inside a remote terminal. The tmux server runs on the remote host, so it cannot reach your local clipboard. OSC 52 could bridge that gap, except it is text-only, size-capped, and mosh's terminal emulator drops it. Port forwarding could bridge it too, except mosh has no forwarding at all.

tmux-inbox sidesteps the clipboard entirely. Files arrive in a directory by whatever means you like; the plugin turns that directory into a picker and types the chosen path into the pane you invoked it from.

How it works

Two halves that know nothing about each other:

  1. A drop directory, ~/inbox by default. Anything that can write a file can fill it: Taildrop, scp, rsync, curl, or a program's own output.
  2. prefix + P opens a display-popup running fzf over that directory, newest first, with previews. Images render as symbol art via chafa, so they survive mosh and Blink. Selecting a file types its absolute path into the pane without pressing Enter, leaving you free to type a prompt around it.

Because the injected path arrives as ordinary pane output, it renders the same under ssh and mosh, from any client.

Install

With TPM:

set -g @plugin 'ssh://git@ssh.commons.fjord.host/stephen/tmux-inbox.git'

Manually:

run-shell '/path/to/tmux-inbox/inbox.tmux'

The repo is private, so TPM clones it over SSH and each host needs a key registered on your commons account. A plain run-shell against a local checkout works too.

Requires fzf. chafa enables image previews, bat improves text previews, and poppler-utils enables PDF previews. All three are optional.

Options

Option Default Meaning
@inbox-dir ~/inbox Directory the picker reads
@inbox-key P Prefix key that opens the picker
@inbox-newest-key unset Prefix key that attaches the newest file with no picker
@inbox-token-prefix empty String prepended to each path
@inbox-show-attached off Show drops you have already attached
@inbox-popup-width 80% Popup width
@inbox-popup-height 80% Popup height

Leave @inbox-token-prefix empty unless the program you are pasting into expects a sigil. A leading @ opens a completion menu in some readers, which text arriving via send-keys trips; a bare absolute path avoids that.

Inside the picker: enter attaches everything marked, ctrl-y puts the paths in a tmux buffer instead of typing them, tab marks one file, ctrl-a marks all, ctrl-x clears marks, ctrl-d deletes what is marked (falling back to the highlighted file when nothing is), ctrl-r rereads the directory so a drop that lands while the picker is open shows up, and ctrl-t toggles drops you have already attached back into view. Attaching several files types their paths space-separated, and any name needing quotes gets them.

Each row is prefixed with its age, newest first, which is usually enough to tell two screenshots apart without reading the preview.

Attached drops

Once a file has been attached it drops out of the listing, so the picker shows what you have not used yet rather than everything that ever arrived. ctrl-t brings them back for the rest of the session, and @inbox-show-attached 'on' turns the hiding off for good. Drops you have already attached are marked with a * when they are showing, so reattaching one is a deliberate act rather than an accident. The toggle sticks: ctrl-r and ctrl-d keep whichever view you are in.

Nothing moves on disk. The picker has just typed a path into your pane and you have not pressed Enter yet, so the file has to stay exactly where it is; hiding is a property of the listing, not of the filesystem. The record lives in $XDG_STATE_HOME/tmux-inbox/attached, keyed on modification time as well as name, so a redrop of the same name counts as a new file. Copying with ctrl-y does not mark anything, on the grounds that you are taking the path elsewhere rather than handing it to the pane.

Filling the inbox

Any transport works. If the host is on a Tailscale tailnet, Taildrop is the one that needs nothing installed per client, because every device you connect from is already a Tailscale client:

tailscale file cp screenshot.png myhost:

On iOS or macOS, use the system share sheet and pick Tailscale, then the host. This is what makes the setup work from Blink Shell, where no local helper could run.

To receive Taildrop files automatically, install the bundled user unit:

sudo tailscale set --operator="$USER"   # tailscaled denies the local API otherwise
install -Dm644 systemd/taildrop-inbox.service ~/.config/systemd/user/taildrop-inbox.service
systemctl --user enable --now taildrop-inbox.service
sudo loginctl enable-linger "$USER"     # keep receiving while logged out

The --operator step is not optional. Without it tailscale file get fails with Access denied: file access denied.

Retention

Drops accumulate, which makes the picker noisy. A bundled timer prunes anything older than 14 days; edit +14 in the service to change that.

install -Dm644 systemd/inbox-prune.service ~/.config/systemd/user/inbox-prune.service
install -Dm644 systemd/inbox-prune.timer ~/.config/systemd/user/inbox-prune.timer
systemctl --user enable --now inbox-prune.timer

Licence

MIT