Finish porting clipboard kitten to Go

This commit is contained in:
Kovid Goyal 2022-11-26 15:15:30 +05:30
parent 63a08dc6cc
commit a721ffeb7d
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 245 additions and 5 deletions

View File

@ -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('}')
# }}}

View File

@ -0,0 +1,194 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
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
}

View File

@ -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) {

View File

@ -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

View File

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