- Messages to a PIN-protected peer now prompt for the PIN and retry, like file sends (was a silent failure). SendMessage takes a pin argument and the TUI tracks whether a pending send is a message or files. - Copy a received message to the clipboard with `y` (in the Messages tab or while reading one). - Send the clipboard as a message with `v` on a device: it opens the compose box pre-filled with the clipboard text to review before sending. - New internal/clipboard package shells out to wl-clipboard / xclip / xsel, and falls back to tmux's paste buffer when running inside tmux on a headless box (tmux load-buffer -w also reaches the outer clipboard via set-clipboard, which omaterm enables by default). No system tool -> "clipboard unavailable". - Route the standard logger to the debug log (or discard) at startup so stray stdlib logging — e.g. net/http's "unsolicited response on idle channel" notice after a peer sends a late response — can't paint over the TUI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 lines
2.7 KiB
Go
88 lines
2.7 KiB
Go
// Package clipboard reads and writes the system clipboard by shelling out to
|
|
// whichever helper is available: wl-clipboard (Wayland), xclip, xsel, or — when
|
|
// running inside tmux on a headless box — tmux's own paste buffer (which can
|
|
// reach the real clipboard via tmux's set-clipboard/OSC52). The binary stays
|
|
// dependency-free and static; these tools are optional, so a box with none
|
|
// simply reports the clipboard unavailable.
|
|
package clipboard
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
// ErrUnavailable means no supported clipboard helper is reachable.
|
|
var ErrUnavailable = errors.New("no clipboard available (need wl-clipboard, xclip, xsel, or tmux)")
|
|
|
|
// inTmux reports whether we're running inside a tmux session.
|
|
func inTmux() bool { return os.Getenv("TMUX") != "" }
|
|
|
|
// candidate is one clipboard helper invocation. eligible gates it beyond the
|
|
// binary simply existing on PATH (used to require an active tmux session).
|
|
type candidate struct {
|
|
bin string
|
|
args []string
|
|
eligible bool
|
|
}
|
|
|
|
// Read returns the current clipboard text, or ErrUnavailable if no helper is
|
|
// reachable. A single trailing newline is trimmed. System tools take priority;
|
|
// tmux's buffer is the fallback for headless sessions.
|
|
func Read() (string, error) {
|
|
for _, c := range []candidate{
|
|
{"wl-paste", []string{"--no-newline"}, true},
|
|
{"xclip", []string{"-selection", "clipboard", "-o"}, true},
|
|
{"xsel", []string{"--clipboard", "--output"}, true},
|
|
{"tmux", []string{"save-buffer", "-"}, inTmux()},
|
|
} {
|
|
if !c.eligible {
|
|
continue
|
|
}
|
|
if _, err := exec.LookPath(c.bin); err != nil {
|
|
continue
|
|
}
|
|
out, err := exec.Command(c.bin, c.args...).Output()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.TrimRight(string(out), "\n"), nil
|
|
}
|
|
return "", ErrUnavailable
|
|
}
|
|
|
|
// Write sets the clipboard to text, or returns ErrUnavailable if no helper is
|
|
// reachable.
|
|
func Write(text string) error {
|
|
for _, c := range []candidate{
|
|
{"wl-copy", nil, true},
|
|
{"xclip", []string{"-selection", "clipboard"}, true},
|
|
{"xsel", []string{"--clipboard", "--input"}, true},
|
|
// -w also pushes the buffer to the outer terminal's clipboard when
|
|
// tmux's set-clipboard is on; we fall back to a plain load on failure.
|
|
{"tmux", []string{"load-buffer", "-w", "-"}, inTmux()},
|
|
} {
|
|
if !c.eligible {
|
|
continue
|
|
}
|
|
if _, err := exec.LookPath(c.bin); err != nil {
|
|
continue
|
|
}
|
|
if err := run(c.bin, c.args, text); err != nil {
|
|
if c.bin == "tmux" {
|
|
return run("tmux", []string{"load-buffer", "-"}, text) // older tmux: no -w
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
return ErrUnavailable
|
|
}
|
|
|
|
func run(bin string, args []string, stdin string) error {
|
|
cmd := exec.Command(bin, args...)
|
|
cmd.Stdin = bytes.NewBufferString(stdin)
|
|
return cmd.Run()
|
|
}
|