jesseduffield.lazygit/pkg/commands/oscommands/osc52_clipboard_test.go
patwie 4f9dc0449c Add tmux and OSC 52 clipboard fallbacks for headless hosts
On a headless SSH host there is no X11 or Wayland display, so none of
xclip, xsel, or wl-copy can reach a clipboard. atotto/clipboard then
reports itself unsupported and every copy action fails with "No
clipboard utilities available", even though installing those tools
cannot help without a display server. This breaks copying branch
names, commit hashes, file paths, and diffs in the common setup of
developing on a remote box over SSH inside tmux.

Extend CopyToClipboard and PasteFromClipboard with a built-in fallback
chain that only engages when no os.copyToClipboardCmd is configured
and clipboard.Unsupported is true, so any setup with a working native
utility or a configured command is completely unaffected.

When running inside tmux, copy via `tmux load-buffer -w -` and paste
via `tmux save-buffer -`. The -w flag relays the buffer to the outer
terminal's system clipboard through tmux's own OSC 52 emission (with
`set-clipboard on`), and because the tmux buffer is readable this
gives real bidirectional clipboard support within the session,
including paste.

Without tmux, emit an OSC 52 escape sequence to the controlling
terminal, so terminals that support it still receive the copy. OSC 52
read-back is refused by most terminals for security reasons, so paste
returns the last value copied in-session, matching how Neovim's and
Helix's clipboard providers behave. The sequence framing is factored
into a pure buildOSC52Sequence so it can be verified without touching
the terminal.

Failing both, fall through to the native atotto path unchanged.

Fixes #5724
2026-07-14 20:40:04 +00:00

56 lines
2 KiB
Go

package oscommands
import (
"encoding/base64"
"testing"
"github.com/stretchr/testify/assert"
)
// TestBuildOSC52Sequence verifies the escape sequence is framed correctly:
// the `\x1b]52;c;` prefix, the base64 of the payload, and the ST terminator.
func TestBuildOSC52Sequence(t *testing.T) {
const payload = "commit-hash-abc123"
sequence, err := buildOSC52Sequence(payload)
assert.NoError(t, err)
expected := osc52SequencePrefix + base64.StdEncoding.EncodeToString([]byte(payload)) + osc52SequenceSuffix
assert.Equal(t, expected, sequence)
// Guard against accidental terminator/prefix changes that would silently
// break terminals which only accept the exact framing.
assert.Equal(t, "\x1b]52;c;", osc52SequencePrefix)
assert.Equal(t, "\x1b\\", osc52SequenceSuffix)
}
// TestBuildOSC52SequenceRejectsEmpty verifies we never assemble an empty
// payload, which OSC 52 interprets as a request to clear the clipboard.
func TestBuildOSC52SequenceRejectsEmpty(t *testing.T) {
sequence, err := buildOSC52Sequence("")
assert.Error(t, err)
assert.Equal(t, "", sequence)
}
// TestOSC52ClipboardCacheStartsEmpty verifies paste returns the empty string
// before anything has been copied, rather than panicking or erroring.
func TestOSC52ClipboardCacheStartsEmpty(t *testing.T) {
osCommand := NewDummyOSCommand()
assert.Equal(t, "", osCommand.pasteFromClipboardOSC52())
}
// TestOSC52ClipboardCacheRoundTrip verifies the central fallback contract:
// because OSC 52 read-back is unreliable, paste must return exactly the value
// most recently copied.
func TestOSC52ClipboardCacheRoundTrip(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.rememberOSC52Clipboard("first-copied-value")
assert.Equal(t, "first-copied-value", osCommand.pasteFromClipboardOSC52())
// A subsequent copy must overwrite, not append or stick to the old value.
osCommand.rememberOSC52Clipboard("second-copied-value")
assert.Equal(t, "second-copied-value", osCommand.pasteFromClipboardOSC52())
}