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