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))
}