// License: GPLv3 Copyright: 2022, Kovid Goyal, package clipboard import ( "bytes" "encoding/base64" "errors" "fmt" "io" "os" "strings" "kitty/tools/tty" "kitty/tools/tui/loop" "kitty/tools/utils" ) var _ = fmt.Print var _ = fmt.Print func encode_read_from_clipboard(use_primary bool) string { dest := "c" if use_primary { dest = "p" } return fmt.Sprintf("\x1b]52;%s;?\x1b\\", dest) } type base64_streaming_enc struct { output func(string) loop.IdType last_written_id loop.IdType } func (self *base64_streaming_enc) Write(p []byte) (int, error) { if len(p) > 0 { self.last_written_id = self.output(string(p)) } return len(p), nil } var ErrTooMuchPipedData = errors.New("Too much piped data") func read_all_with_max_size(r io.Reader, max_size int) ([]byte, error) { b := make([]byte, 0, utils.Min(8192, max_size)) for { if len(b) == cap(b) { new_size := utils.Min(2*cap(b), max_size) if new_size <= cap(b) { return b, ErrTooMuchPipedData } b = append(make([]byte, 0, new_size), b...) } n, err := r.Read(b[len(b):cap(b)]) b = b[:len(b)+n] if err != nil { if err == io.EOF { err = nil } return b, err } } } func preread_stdin() (data_src io.Reader, tempfile *os.File, err error) { // we pre-read STDIN because otherwise if the output of a command is being piped in // and that command itself transmits on the tty we will break. For example // kitten @ ls | kitten clipboard var stdin_data []byte stdin_data, err = read_all_with_max_size(os.Stdin, 2*1024*1024) if err == nil { os.Stdin.Close() } else if err != ErrTooMuchPipedData { os.Stdin.Close() err = fmt.Errorf("Failed to read from STDIN pipe with error: %w", err) return } if err == ErrTooMuchPipedData { tempfile, err = utils.CreateAnonymousTemp("") if err != nil { return nil, nil, fmt.Errorf("Failed to create a temporary from STDIN pipe with error: %w", err) } tempfile.Write(stdin_data) _, err = io.Copy(tempfile, os.Stdin) os.Stdin.Close() if err != nil { return nil, nil, fmt.Errorf("Failed to copy data from STDIN pipe to temp file with error: %w", err) } tempfile.Seek(0, os.SEEK_SET) data_src = tempfile } else if stdin_data != nil { data_src = bytes.NewBuffer(stdin_data) } return } func run_plain_text_loop(opts *Options) (err error) { stdin_is_tty := tty.IsTerminal(os.Stdin.Fd()) var data_src io.Reader var tempfile *os.File if !stdin_is_tty { data_src, tempfile, err = preread_stdin() if err != nil { return err } if tempfile != nil { defer tempfile.Close() } } lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors, loop.NoMouseTracking) if err != nil { return } dest := "c" if opts.UsePrimary { dest = "p" } send_to_loop := func(data string) loop.IdType { return lp.QueueWriteString(data) } enc_writer := base64_streaming_enc{output: send_to_loop} enc := base64.NewEncoder(base64.StdEncoding, &enc_writer) transmitting := true after_read_from_stdin := func() { transmitting = false if opts.GetClipboard { lp.QueueWriteString(encode_read_from_clipboard(opts.UsePrimary)) } else if opts.WaitForCompletion { lp.QueueWriteString("\x1bP+q544e\x1b\\") } else { lp.Quit(0) } } buf := make([]byte, 8192) write_one_chunk := func() error { n, err := data_src.Read(buf[:cap(buf)]) if err != nil && !errors.Is(err, io.EOF) { send_to_loop("\x1b\\") return err } if n > 0 { enc.Write(buf[:n]) } if errors.Is(err, io.EOF) { enc.Close() send_to_loop("\x1b\\") after_read_from_stdin() } return nil } lp.OnInitialize = func() (string, error) { if data_src != nil { send_to_loop(fmt.Sprintf("\x1b]52;%s;", dest)) return "", write_one_chunk() } after_read_from_stdin() return "", nil } lp.OnWriteComplete = func(id loop.IdType) error { if id == enc_writer.last_written_id { return write_one_chunk() } return nil } var clipboard_contents []byte lp.OnEscapeCode = func(etype loop.EscapeCodeType, data []byte) (err error) { switch etype { case loop.DCS: if strings.HasPrefix(utils.UnsafeBytesToString(data), "1+r") { lp.Quit(0) } case loop.OSC: q := utils.UnsafeBytesToString(data) if strings.HasPrefix(q, "52;") { parts := strings.SplitN(q, ";", 3) if len(parts) < 3 { lp.Quit(0) return } data, err := base64.StdEncoding.DecodeString(parts[2]) if err != nil { return fmt.Errorf("Invalid base64 encoded data from terminal with error: %w", err) } clipboard_contents = data lp.Quit(0) } } return } esc_count := 0 lp.OnKeyEvent = func(event *loop.KeyEvent) error { if event.MatchesPressOrRepeat("ctrl+c") || event.MatchesPressOrRepeat("esc") { if transmitting { return nil } event.Handled = true esc_count++ if esc_count < 2 { key := "Esc" if event.MatchesPressOrRepeat("ctrl+c") { key = "Ctrl+C" } lp.QueueWriteString(fmt.Sprintf("Waiting for response from terminal, press %s again to abort. This could cause garbage to be spewed to the screen.\r\n", key)) } else { return fmt.Errorf("Aborted by user!") } } return nil } err = lp.Run() if err != nil { return } ds := lp.DeathSignalName() if ds != "" { fmt.Println("Killed by signal: ", ds) lp.KillIfSignalled() return } if len(clipboard_contents) > 0 { _, err = os.Stdout.Write(clipboard_contents) if err != nil { err = fmt.Errorf("Failed to write to STDOUT with error: %w", err) } } return }