legacy.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. // License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package clipboard
  3. import (
  4. "bytes"
  5. "encoding/base64"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "os"
  10. "strings"
  11. "kitty/tools/tty"
  12. "kitty/tools/tui/loop"
  13. "kitty/tools/utils"
  14. )
  15. var _ = fmt.Print
  16. var _ = fmt.Print
  17. func encode_read_from_clipboard(use_primary bool) string {
  18. dest := "c"
  19. if use_primary {
  20. dest = "p"
  21. }
  22. return fmt.Sprintf("\x1b]52;%s;?\x1b\\", dest)
  23. }
  24. type base64_streaming_enc struct {
  25. output func(string) loop.IdType
  26. last_written_id loop.IdType
  27. }
  28. func (self *base64_streaming_enc) Write(p []byte) (int, error) {
  29. if len(p) > 0 {
  30. self.last_written_id = self.output(string(p))
  31. }
  32. return len(p), nil
  33. }
  34. var ErrTooMuchPipedData = errors.New("Too much piped data")
  35. func read_all_with_max_size(r io.Reader, max_size int) ([]byte, error) {
  36. b := make([]byte, 0, utils.Min(8192, max_size))
  37. for {
  38. if len(b) == cap(b) {
  39. new_size := utils.Min(2*cap(b), max_size)
  40. if new_size <= cap(b) {
  41. return b, ErrTooMuchPipedData
  42. }
  43. b = append(make([]byte, 0, new_size), b...)
  44. }
  45. n, err := r.Read(b[len(b):cap(b)])
  46. b = b[:len(b)+n]
  47. if err != nil {
  48. if err == io.EOF {
  49. err = nil
  50. }
  51. return b, err
  52. }
  53. }
  54. }
  55. func preread_stdin() (data_src io.Reader, tempfile *os.File, err error) {
  56. // we pre-read STDIN because otherwise if the output of a command is being piped in
  57. // and that command itself transmits on the tty we will break. For example
  58. // kitten @ ls | kitten clipboard
  59. var stdin_data []byte
  60. stdin_data, err = read_all_with_max_size(os.Stdin, 2*1024*1024)
  61. if err == nil {
  62. os.Stdin.Close()
  63. } else if err != ErrTooMuchPipedData {
  64. os.Stdin.Close()
  65. err = fmt.Errorf("Failed to read from STDIN pipe with error: %w", err)
  66. return
  67. }
  68. if err == ErrTooMuchPipedData {
  69. tempfile, err = utils.CreateAnonymousTemp("")
  70. if err != nil {
  71. return nil, nil, fmt.Errorf("Failed to create a temporary from STDIN pipe with error: %w", err)
  72. }
  73. tempfile.Write(stdin_data)
  74. _, err = io.Copy(tempfile, os.Stdin)
  75. os.Stdin.Close()
  76. if err != nil {
  77. return nil, nil, fmt.Errorf("Failed to copy data from STDIN pipe to temp file with error: %w", err)
  78. }
  79. tempfile.Seek(0, io.SeekStart)
  80. data_src = tempfile
  81. } else if stdin_data != nil {
  82. data_src = bytes.NewBuffer(stdin_data)
  83. }
  84. return
  85. }
  86. func run_plain_text_loop(opts *Options) (err error) {
  87. stdin_is_tty := tty.IsTerminal(os.Stdin.Fd())
  88. var data_src io.Reader
  89. var tempfile *os.File
  90. if !stdin_is_tty && !opts.GetClipboard {
  91. // we dont read STDIN when getting clipboard as it makes it hard to use the kitten in contexts where
  92. // the user does not control STDIN such as being execed from other programs.
  93. data_src, tempfile, err = preread_stdin()
  94. if err != nil {
  95. return err
  96. }
  97. if tempfile != nil {
  98. defer tempfile.Close()
  99. }
  100. }
  101. lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors, loop.NoMouseTracking)
  102. if err != nil {
  103. return
  104. }
  105. dest := "c"
  106. if opts.UsePrimary {
  107. dest = "p"
  108. }
  109. send_to_loop := func(data string) loop.IdType {
  110. return lp.QueueWriteString(data)
  111. }
  112. enc_writer := base64_streaming_enc{output: send_to_loop}
  113. enc := base64.NewEncoder(base64.StdEncoding, &enc_writer)
  114. transmitting := true
  115. after_read_from_stdin := func() {
  116. transmitting = false
  117. if opts.GetClipboard {
  118. lp.QueueWriteString(encode_read_from_clipboard(opts.UsePrimary))
  119. } else if opts.WaitForCompletion {
  120. lp.QueueWriteString("\x1bP+q544e\x1b\\")
  121. } else {
  122. lp.Quit(0)
  123. }
  124. }
  125. buf := make([]byte, 8192)
  126. write_one_chunk := func() error {
  127. n, err := data_src.Read(buf[:cap(buf)])
  128. if err != nil && !errors.Is(err, io.EOF) {
  129. send_to_loop("\x1b\\")
  130. return err
  131. }
  132. if n > 0 {
  133. enc.Write(buf[:n])
  134. }
  135. if errors.Is(err, io.EOF) {
  136. enc.Close()
  137. send_to_loop("\x1b\\")
  138. after_read_from_stdin()
  139. }
  140. return nil
  141. }
  142. lp.OnInitialize = func() (string, error) {
  143. if data_src != nil {
  144. send_to_loop(fmt.Sprintf("\x1b]52;%s;", dest))
  145. return "", write_one_chunk()
  146. }
  147. after_read_from_stdin()
  148. return "", nil
  149. }
  150. lp.OnWriteComplete = func(id loop.IdType, has_pending_writes bool) error {
  151. if id == enc_writer.last_written_id {
  152. return write_one_chunk()
  153. }
  154. return nil
  155. }
  156. var clipboard_contents []byte
  157. lp.OnEscapeCode = func(etype loop.EscapeCodeType, data []byte) (err error) {
  158. switch etype {
  159. case loop.DCS:
  160. if strings.HasPrefix(utils.UnsafeBytesToString(data), "1+r") {
  161. lp.Quit(0)
  162. }
  163. case loop.OSC:
  164. q := utils.UnsafeBytesToString(data)
  165. if strings.HasPrefix(q, "52;") {
  166. parts := strings.SplitN(q, ";", 3)
  167. if len(parts) < 3 {
  168. lp.Quit(0)
  169. return
  170. }
  171. data, err := base64.StdEncoding.DecodeString(parts[2])
  172. if err != nil {
  173. return fmt.Errorf("Invalid base64 encoded data from terminal with error: %w", err)
  174. }
  175. clipboard_contents = data
  176. lp.Quit(0)
  177. }
  178. }
  179. return
  180. }
  181. esc_count := 0
  182. lp.OnKeyEvent = func(event *loop.KeyEvent) error {
  183. if event.MatchesPressOrRepeat("ctrl+c") || event.MatchesPressOrRepeat("esc") {
  184. if transmitting {
  185. return nil
  186. }
  187. event.Handled = true
  188. esc_count++
  189. if esc_count < 2 {
  190. key := "Esc"
  191. if event.MatchesPressOrRepeat("ctrl+c") {
  192. key = "Ctrl+C"
  193. }
  194. 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))
  195. } else {
  196. return fmt.Errorf("Aborted by user!")
  197. }
  198. }
  199. return nil
  200. }
  201. err = lp.Run()
  202. if err != nil {
  203. return
  204. }
  205. ds := lp.DeathSignalName()
  206. if ds != "" {
  207. fmt.Println("Killed by signal: ", ds)
  208. lp.KillIfSignalled()
  209. return
  210. }
  211. if len(clipboard_contents) > 0 {
  212. _, err = os.Stdout.Write(clipboard_contents)
  213. if err != nil {
  214. err = fmt.Errorf("Failed to write to STDOUT with error: %w", err)
  215. }
  216. }
  217. return
  218. }