Vectrum
Sign In
76 lines · 2.6 KB · Go
package tui

import (
	"os"
	"strconv"
	"strings"

	"github.com/atotto/clipboard"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/x/ansi"
)

// Copying out of a full-screen terminal program is done twice, because
// neither way works everywhere.
//
// OSC 52 asks the terminal itself to take the text, which is the only way
// that works over ssh, in a container, or anywhere else the process is not
// sitting next to the desktop it is displayed on. Several terminals ship with
// it turned off, though, so the desktop's own clipboard is written too, with
// the same bytes — whichever of the two lands, the text is the text.
//
// The OSC 52 sequence is written straight to the terminal rather than through
// a frame. It carries no cursor movement and ends itself, so landing between
// two frames costs nothing on screen.

// copyMax caps what is sent. Terminals have their own limit on an OSC 52
// payload — commonly around 100k after base64 — and a selection large enough
// to hit it is a mis-drag rather than something a person means to paste.
const copyMax = 64 * 1024

// copiedMsg reports a finished copy. The text comes back with it so the
// chrome can say what was taken without reaching for the selection again,
// which by then may already be gone.
type copiedMsg struct {
	text string
	err  error
}

func copyToClipboard(text string) error {
	if text == "" {
		return nil
	}
	if len(text) > copyMax {
		text = text[:copyMax]
	}
	// The desktop clipboard is best effort: no X display, no xclip, no
	// pbcopy — all of them are ordinary on a remote machine, and none of them
	// means the copy failed, because OSC 52 is still on its way.
	_ = clipboard.WriteAll(text)

	_, err := os.Stdout.WriteString(ansi.SetSystemClipboard(text))
	return err
}

// finishCopy is what the chrome says about it. A failure is worth a line in
// the transcript — the text is not on the clipboard and the person is about
// to try to paste it — but a success is not: it belongs on the status line,
// where it costs no scrollback.
func (m *model) finishCopy(msg copiedMsg) tea.Cmd {
	if msg.err != nil {
		m.tr.addError("could not copy: " + msg.err.Error())
		return m.sync()
	}
	m.copied = "✂ " + copyNote(msg.text)
	return nil
}

// copyNote says what was taken. One line is quoted back, because seeing the
// words is the fastest way to know the right thing was caught; more than one
// is counted, because a quote would not fit.
func copyNote(text string) string {
	if n := strings.Count(text, "\n"); n > 0 {
		return "copied " + plural(n+1, "line")
	}
	return "copied " + strconv.Quote(truncate(text, 28))
}