From a721ffeb7dd5bafcf32b25884dcdbfabed30ec2a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 26 Nov 2022 15:15:30 +0530 Subject: [PATCH] Finish porting clipboard kitten to Go --- gen-go-code.py | 14 ++- tools/cmd/clipboard/legacy.go | 194 ++++++++++++++++++++++++++++++++++ tools/cmd/clipboard/main.go | 9 +- tools/tui/loop/api.go | 15 +++ tools/tui/loop/run.go | 18 ++++ 5 files changed, 245 insertions(+), 5 deletions(-) create mode 100644 tools/cmd/clipboard/legacy.go diff --git a/gen-go-code.py b/gen-go-code.py index 96cdcb7f3..a62eb8ae6 100755 --- a/gen-go-code.py +++ b/gen-go-code.py @@ -292,23 +292,29 @@ def wrapped_kittens() -> Sequence[str]: def kitten_clis() -> None: for kitten in wrapped_kittens(): with replace_if_needed(f'tools/cmd/{kitten}/cli_generated.go'): + od = [] kcd = kitten_cli_docs(kitten) has_underscore = '_' in kitten print(f'package {kitten}') print('import "kitty/tools/cli"') - print('func create_cmd(root *cli.Command, run_func cli.RunFunc) {') + print('func create_cmd(root *cli.Command, run_func func(*cli.Command, *Options, []string)(int, error)) {') print('ans := root.AddSubCommand(&cli.Command{') print(f'Name: "{kitten}",') print(f'ShortDescription: "{serialize_as_go_string(kcd["short_desc"])}",') print(f'Usage: "{serialize_as_go_string(kcd["usage"])}",') print(f'HelpText: "{serialize_as_go_string(kcd["help_text"])}",') - print('Run: run_func,') + print('Run: func(cmd *cli.Command, args []string) (int, error) {') + print('opts := Options{}') + print('err := cmd.GetOptionValues(&opts)') + print('if err != nil { return 1, err }') + print('return run_func(cmd, &opts, args)},') if has_underscore: print('Hidden: true,') print('})') gopts, ac = go_options_for_kitten(kitten) for opt in gopts: print(opt.as_option('ans')) + od.append(opt.struct_declaration()) if ac is not None: print(''.join(ac.as_go_code('ans.ArgCompleter', ' = '))) if has_underscore: @@ -316,6 +322,10 @@ def kitten_clis() -> None: print('clone.Hidden = false') print(f'clone.Name = "{serialize_as_go_string(kitten.replace("_", "-"))}"') print('}') + print('type Options struct {') + print('\n'.join(od)) + print('}') + # }}} diff --git a/tools/cmd/clipboard/legacy.go b/tools/cmd/clipboard/legacy.go new file mode 100644 index 000000000..b871358e4 --- /dev/null +++ b/tools/cmd/clipboard/legacy.go @@ -0,0 +1,194 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package clipboard + +import ( + "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) +} + +func (self *base64_streaming_enc) Write(p []byte) (int, error) { + if len(p) > 0 { + self.output(string(p)) + } + return len(p), nil +} + +func run_plain_text_loop(opts *Options) (err error) { + lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors, loop.NoMouseTracking) + if err != nil { + return + } + stdin_chan := make(chan string, 1) + err_chan := make(chan error, 1) + dest := "c" + if opts.UsePrimary { + dest = "p" + } + + send_to_loop := func(data string) { + select { + case stdin_chan <- data: + lp.WakeupMainThread() + return + default: + lp.WakeupMainThread() + } + stdin_chan <- data + } + + read_from_stdin := func() { + if !tty.IsTerminal(os.Stdin.Fd()) { + var buf [8192]byte + enc := base64.NewEncoder(base64.StdEncoding, &base64_streaming_enc{send_to_loop}) + header_written := false + for { + n, err := os.Stdin.Read(buf[:]) + if err != nil { + if errors.Is(err, io.EOF) { + enc.Close() + send_to_loop("\a") + close(stdin_chan) + os.Stdin.Close() + break + } + err_chan <- fmt.Errorf("Failed to read from STDIN with error: %w", err) + lp.WakeupMainThread() + break + } + if n > 0 { + if !header_written { + header_written = true + send_to_loop(fmt.Sprintf("\x1b]52;%s;", dest)) + } + enc.Write(buf[:n]) + } + } + } else { + close(stdin_chan) + lp.WakeupMainThread() + } + } + + transmitting := true + + lp.OnWakeup = func() error { + for transmitting { + select { + case p, more := <-stdin_chan: + if more { + lp.QueueWriteString(p) + } else { + 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) + } + } + case err := <-err_chan: + return err + default: + return nil + } + } + return nil + } + + lp.OnInitialize = func() (string, error) { + go read_from_stdin() + 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 +} diff --git a/tools/cmd/clipboard/main.go b/tools/cmd/clipboard/main.go index 7fe3eea00..d548b9a7e 100644 --- a/tools/cmd/clipboard/main.go +++ b/tools/cmd/clipboard/main.go @@ -4,13 +4,16 @@ package clipboard import ( "fmt" + "kitty/tools/cli" ) -var _ = fmt.Print +func clipboard_main(cmd *cli.Command, opts *Options, args []string) (rc int, err error) { + if len(args) > 0 { + return 1, fmt.Errorf("Unrecognized extra command line arguments") + } -func clipboard_main(cmd *cli.Command, args []string) (int, error) { - return 0, nil + return 0, run_plain_text_loop(opts) } func EntryPoint(parent *cli.Command) { diff --git a/tools/tui/loop/api.go b/tools/tui/loop/api.go index 7a333d0a4..b2d811fa2 100644 --- a/tools/tui/loop/api.go +++ b/tools/tui/loop/api.go @@ -20,6 +20,16 @@ type ScreenSize struct { type IdType uint64 type TimerCallback func(timer_id IdType) error +type EscapeCodeType int + +const ( + CSI EscapeCodeType = iota + DCS + OSC + APC + SOS + PM +) type timer struct { interval time.Duration @@ -47,6 +57,8 @@ type Loop struct { pending_writes []*write_msg on_SIGTSTP func() error + // Send strings to this channel to queue writes in a thread safe way + // Callbacks // Called when the terminal has been fully setup. Any string returned is sent to @@ -76,6 +88,9 @@ type Loop struct { // Called when any input from tty is received OnReceivedData func(data []byte) error + // Called when an escape code is received that is not handled by any other handler + OnEscapeCode func(EscapeCodeType, []byte) error + // Called when resuming from a SIGTSTP or Ctrl-z OnResumeFromStop func() error diff --git a/tools/tui/loop/run.go b/tools/tui/loop/run.go index 67d3828c5..adc6a6035 100644 --- a/tools/tui/loop/run.go +++ b/tools/tui/loop/run.go @@ -72,6 +72,9 @@ func (self *Loop) handle_csi(raw []byte) error { if ke != nil { return self.handle_key_event(ke) } + if self.OnEscapeCode != nil { + return self.OnEscapeCode(CSI, raw) + } return nil } @@ -100,6 +103,9 @@ func (self *Loop) handle_key_event(ev *KeyEvent) error { } func (self *Loop) handle_osc(raw []byte) error { + if self.OnEscapeCode != nil { + return self.OnEscapeCode(OSC, raw) + } return nil } @@ -107,18 +113,30 @@ func (self *Loop) handle_dcs(raw []byte) error { if self.OnRCResponse != nil && bytes.HasPrefix(raw, []byte("@kitty-cmd")) { return self.OnRCResponse(raw[len("@kitty-cmd"):]) } + if self.OnEscapeCode != nil { + return self.OnEscapeCode(DCS, raw) + } return nil } func (self *Loop) handle_apc(raw []byte) error { + if self.OnEscapeCode != nil { + return self.OnEscapeCode(APC, raw) + } return nil } func (self *Loop) handle_sos(raw []byte) error { + if self.OnEscapeCode != nil { + return self.OnEscapeCode(SOS, raw) + } return nil } func (self *Loop) handle_pm(raw []byte) error { + if self.OnEscapeCode != nil { + return self.OnEscapeCode(PM, raw) + } return nil }