- Shell 100%
| logo | ||
| scripts | ||
| systemd | ||
| inbox.tmux | ||
| README.md | ||
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:
- A drop directory,
~/inboxby default. Anything that can write a file can fill it: Taildrop,scp,rsync,curl, or a program's own output. prefix + Popens adisplay-popuprunning 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
