mirror of
https://github.com/kovidgoyal/kitty.git
synced 2024-09-11 14:45:31 +03:00
Start work on implementing kitty @ as a static binary using Go
This commit is contained in:
parent
126468a5dd
commit
bbf7504303
19
go.mod
Normal file
19
go.mod
Normal file
@ -0,0 +1,19 @@
|
||||
module kitty
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/mattn/go-runewidth v0.0.13
|
||||
github.com/spf13/cobra v1.5.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.9 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect
|
||||
)
|
28
go.sum
Normal file
28
go.sum
Normal file
@ -0,0 +1,28 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
|
||||
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
3
tools/README.rst
Normal file
3
tools/README.rst
Normal file
@ -0,0 +1,3 @@
|
||||
This folder contains various small command line utilities compiled statically
|
||||
for doing things like kitty remote control or icat on machines where the full
|
||||
kitty is not available.
|
328
tools/base85/base85.go
Normal file
328
tools/base85/base85.go
Normal file
@ -0,0 +1,328 @@
|
||||
// package base85
|
||||
// This package provides a RFC1924 implementation of base85 encoding.
|
||||
//
|
||||
// See http://www.ietf.org/rfc/rfc1924.txt
|
||||
package base85
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~
|
||||
var b2c_table = [85]byte{
|
||||
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
|
||||
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, //0
|
||||
0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, //1
|
||||
0x57, 0x58, 0x59, 0x5A, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, //2
|
||||
0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x21, 0x23, //3
|
||||
0x24, 0x25, 0x26, 0x28, 0x29, 0x2A, 0x2B, 0x2D, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x5E, 0x5F, //4
|
||||
0x60, 0x7B, 0x7C, 0x7D, 0x7E,
|
||||
}
|
||||
|
||||
var encode [85]byte
|
||||
var decode [256]byte
|
||||
|
||||
func init() {
|
||||
copy(encode[:], b2c_table[:])
|
||||
for i := 0; i < len(decode); i++ {
|
||||
decode[i] = 0xFF
|
||||
}
|
||||
for i := 0; i < len(encode); i++ {
|
||||
decode[encode[i]] = byte(i)
|
||||
}
|
||||
}
|
||||
|
||||
// encodeChunk encodes 4 byte-chunk to 5 byte
|
||||
// if chunk size is less then 4, then it is padded before convertion.
|
||||
// return written bytes or error
|
||||
func encodeChunk(dst, src []byte) int {
|
||||
if len(src) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
//read 4 byte as big-endian uint32 into small endian uint32
|
||||
var val uint32
|
||||
switch len(src) {
|
||||
default:
|
||||
val |= uint32(src[3])
|
||||
fallthrough
|
||||
case 3:
|
||||
val |= uint32(src[2]) << 8
|
||||
fallthrough
|
||||
case 2:
|
||||
val |= uint32(src[1]) << 16
|
||||
fallthrough
|
||||
case 1:
|
||||
val |= uint32(src[0]) << 24
|
||||
}
|
||||
|
||||
buf := [5]byte{0, 0, 0, 0, 0}
|
||||
|
||||
for i := 4; i >= 0; i-- {
|
||||
r := val % 85
|
||||
val /= 85
|
||||
buf[i] = encode[r]
|
||||
}
|
||||
|
||||
m := EncodedLen(len(src))
|
||||
copy(dst[:], buf[:m])
|
||||
return m
|
||||
}
|
||||
|
||||
var decode_base = [5]uint32{85 * 85 * 85 * 85, 85 * 85 * 85, 85 * 85, 85, 1}
|
||||
|
||||
// decodeChunk decodes 5 byte-chunk to 4 byte
|
||||
// if chunk size is less then 5, then it is padded before convertion.
|
||||
// return written bytes and error input index
|
||||
func decodeChunk(dst, src []byte) (int, int) {
|
||||
if len(src) == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
var val uint32
|
||||
m := DecodedLen(len(src))
|
||||
buf := [5]byte{84, 84, 84, 84, 84}
|
||||
for i := 0; i < len(src); i++ {
|
||||
e := decode[src[i]]
|
||||
if e == 0xFF {
|
||||
return 0, i + 1
|
||||
}
|
||||
buf[i] = e
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
r := buf[i]
|
||||
val += uint32(r) * decode_base[i]
|
||||
}
|
||||
//small endian uint32 to big endian uint32 in bytes
|
||||
switch m {
|
||||
default:
|
||||
dst[3] = byte(val & 0xff)
|
||||
fallthrough
|
||||
case 3:
|
||||
dst[2] = byte((val >> 8) & 0xff)
|
||||
fallthrough
|
||||
case 2:
|
||||
dst[1] = byte((val >> 16) & 0xff)
|
||||
fallthrough
|
||||
case 1:
|
||||
dst[0] = byte((val >> 24) & 0xff)
|
||||
}
|
||||
return m, 0
|
||||
}
|
||||
|
||||
// Encode encodes src into dst, return the bytes written
|
||||
// The dst must have size of EncodedLen(len(src))
|
||||
func Encode(dst, src []byte) int {
|
||||
n := 0
|
||||
for len(src) > 0 {
|
||||
if len(src) < 4 {
|
||||
n += encodeChunk(dst, src)
|
||||
return n
|
||||
}
|
||||
n += encodeChunk(dst[:5], src[:4])
|
||||
src = src[4:]
|
||||
dst = dst[5:]
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// EncodeToString returns the base85 encoding of src.
|
||||
func EncodeToString(src []byte) string {
|
||||
buf := make([]byte, EncodedLen(len(src)))
|
||||
Encode(buf, src)
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
// DecodeString returns the bytes represented by the base85 string s.
|
||||
func DecodeString(src string) ([]byte, error) {
|
||||
buf := make([]byte, DecodedLen(len(src)))
|
||||
_, err := Decode(buf, []byte(src))
|
||||
return buf, err
|
||||
}
|
||||
|
||||
// Decode decodes src into dst, return the bytes written
|
||||
// The dst must have size of DecodedLen(len(src))
|
||||
// An CorruptInputError is returned when invalid character is found in src.
|
||||
func Decode(dst, src []byte) (int, error) {
|
||||
f := 0
|
||||
t := 0
|
||||
for len(src) > 0 {
|
||||
if len(src) < 5 {
|
||||
w, err := decodeChunk(dst, src)
|
||||
if err > 0 {
|
||||
return t, CorruptInputError(f + err)
|
||||
}
|
||||
return t + w, nil
|
||||
}
|
||||
|
||||
_, err := decodeChunk(dst[:4], src[:5])
|
||||
if err > 0 {
|
||||
return t, CorruptInputError(f + err)
|
||||
} else {
|
||||
t += 4
|
||||
f += 5
|
||||
src = src[5:]
|
||||
dst = dst[4:]
|
||||
}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// EncodedLen returns the length in bytes of the base64 encoding of an input
|
||||
// buffer of length n.
|
||||
func EncodedLen(n int) int {
|
||||
s := n / 4
|
||||
r := n % 4
|
||||
if r > 0 {
|
||||
return s*5 + 5 - (4 - r)
|
||||
} else {
|
||||
return s * 5
|
||||
}
|
||||
}
|
||||
|
||||
// DecodedLen returns the maximum length in bytes of the decoded data
|
||||
// corresponding to n bytes of base85-encoded data.
|
||||
func DecodedLen(n int) int {
|
||||
s := n / 5
|
||||
r := n % 5
|
||||
if r > 0 {
|
||||
return s*4 + 4 - (5 - r)
|
||||
} else {
|
||||
return s * 4
|
||||
}
|
||||
}
|
||||
|
||||
type encoder struct {
|
||||
w io.Writer
|
||||
bufin [4]byte
|
||||
encoded [5]byte
|
||||
fill int
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *encoder) Write(p []byte) (n int, err error) {
|
||||
if e.err != nil {
|
||||
return 0, e.err
|
||||
}
|
||||
|
||||
for len(p) >= len(e.bufin)-e.fill {
|
||||
//copy len(e.buf) - fill bytes into e.buf to make it full
|
||||
to_copy := len(e.bufin) - e.fill
|
||||
copy(e.bufin[e.fill:], p[:to_copy])
|
||||
p = p[to_copy:]
|
||||
|
||||
//write the encoded whole buffer
|
||||
encodeChunk(e.encoded[:], e.bufin[:])
|
||||
_, e.err = e.w.Write(e.encoded[:])
|
||||
if e.err != nil {
|
||||
return n, e.err
|
||||
}
|
||||
n += 4
|
||||
e.fill = 0
|
||||
}
|
||||
for i := 0; i < len(p); i++ {
|
||||
e.bufin[e.fill] = p[i]
|
||||
e.fill += 1
|
||||
}
|
||||
return n, e.w.(*bufio.Writer).Flush()
|
||||
}
|
||||
|
||||
func (e *encoder) Close() error {
|
||||
if e.err == nil && e.fill > 0 {
|
||||
m := EncodedLen(e.fill)
|
||||
encodeChunk(e.encoded[:m], e.bufin[:e.fill])
|
||||
_, e.err = e.w.Write(e.encoded[:m])
|
||||
if e.err != nil {
|
||||
return e.err
|
||||
}
|
||||
e.err = e.w.(*bufio.Writer).Flush()
|
||||
}
|
||||
err := e.err
|
||||
e.err = errors.New("encoder closed")
|
||||
return err
|
||||
}
|
||||
|
||||
// NewEncoder returns a stream encoder of w.
|
||||
// All write to the encoder is encoded into base85 and write to w.
|
||||
// The writer should call Close() to indicate the end of stream
|
||||
func NewEncoder(w io.Writer) io.WriteCloser {
|
||||
encoder := new(encoder)
|
||||
encoder.w = bufio.NewWriterSize(w, 1000)
|
||||
return encoder
|
||||
}
|
||||
|
||||
type decoder struct {
|
||||
r io.Reader
|
||||
bufin [1000]byte
|
||||
decoded []byte
|
||||
fill int
|
||||
err error
|
||||
}
|
||||
|
||||
// NewDecoder returns a stream decoder of r.
|
||||
// All read from the reader will read the base85 encoded string from r and decode it.
|
||||
func NewDecoder(r io.Reader) io.Reader {
|
||||
decoder := new(decoder)
|
||||
decoder.r = bufio.NewReaderSize(r, 1000)
|
||||
return decoder
|
||||
}
|
||||
|
||||
func (d *decoder) Read(p []byte) (n int, err error) {
|
||||
if d.err != nil {
|
||||
return 0, d.err
|
||||
}
|
||||
|
||||
for len(p) > 0 {
|
||||
// try filling the buffer
|
||||
m, err := d.r.Read(d.bufin[d.fill:])
|
||||
d.fill += m
|
||||
if err != nil {
|
||||
// no further input, decode and copy into p
|
||||
d.decoded = make([]byte, DecodedLen(d.fill))
|
||||
if d.err == io.EOF {
|
||||
k, err := Decode(d.decoded, d.bufin[:d.fill])
|
||||
copy(p, d.decoded[:k])
|
||||
n += k
|
||||
d.fill -= EncodedLen(k)
|
||||
if err != nil {
|
||||
d.err = err
|
||||
return n, err
|
||||
}
|
||||
} else {
|
||||
k, err := Decode(d.decoded, d.bufin[:d.fill-d.fill%5])
|
||||
copy(p, d.decoded[:k])
|
||||
n += k
|
||||
d.fill -= EncodedLen(k)
|
||||
if err != nil {
|
||||
d.err = err
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
d.err = err
|
||||
return n, d.err
|
||||
}
|
||||
//decode d.fill - d.fill % 5 byte of d.bufin
|
||||
chunked_max := d.fill
|
||||
d.fill = d.fill % 5
|
||||
chunked_max -= d.fill
|
||||
d.decoded = make([]byte, DecodedLen(chunked_max))
|
||||
k, err := Decode(d.decoded, d.bufin[:chunked_max])
|
||||
copy(p, d.decoded[:k])
|
||||
p = p[k:]
|
||||
n += k
|
||||
if err != nil {
|
||||
d.err = err
|
||||
return n, d.err
|
||||
}
|
||||
}
|
||||
return n, d.err
|
||||
}
|
||||
|
||||
type CorruptInputError int64
|
||||
|
||||
func (e CorruptInputError) Error() string {
|
||||
return "illegal base85 data at input byte " + strconv.FormatInt(int64(e), 10)
|
||||
}
|
225
tools/cli/infrastructure.go
Normal file
225
tools/cli/infrastructure.go
Normal file
@ -0,0 +1,225 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unicode"
|
||||
"unsafe"
|
||||
|
||||
"github.com/fatih/color"
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"kitty"
|
||||
)
|
||||
|
||||
type Winsize struct {
|
||||
Rows uint16
|
||||
Cols uint16
|
||||
Xpixels uint16
|
||||
Ypixels uint16
|
||||
}
|
||||
|
||||
func GetTTYSize() (Winsize, error) {
|
||||
var ws Winsize
|
||||
f, err := os.OpenFile("/dev/tty", os.O_RDONLY, 0755)
|
||||
if err != nil {
|
||||
return ws, err
|
||||
}
|
||||
fd := f.Fd()
|
||||
retCode, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&ws)))
|
||||
f.Close()
|
||||
|
||||
if int(retCode) == -1 {
|
||||
return ws, errno
|
||||
}
|
||||
return ws, nil
|
||||
}
|
||||
|
||||
func add_choices(cmd *cobra.Command, flags *pflag.FlagSet, choices []string, name string, usage string) {
|
||||
flags.String(name, choices[0], usage)
|
||||
cmd.Annotations["choices-"+name] = strings.Join(choices, "\000")
|
||||
}
|
||||
|
||||
func Choices(cmd *cobra.Command, name string, usage string, choices ...string) {
|
||||
add_choices(cmd, cmd.Flags(), choices, name, usage)
|
||||
}
|
||||
|
||||
func PersistentChoices(cmd *cobra.Command, name string, usage string, choices ...string) {
|
||||
add_choices(cmd, cmd.PersistentFlags(), choices, name, usage)
|
||||
}
|
||||
|
||||
func key_in_slice(vals []string, key string) bool {
|
||||
for _, q := range vals {
|
||||
if q == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ValidateChoices(cmd *cobra.Command, args []string) error {
|
||||
for key, val := range cmd.Annotations {
|
||||
if strings.HasPrefix(key, "choices-") {
|
||||
allowed := strings.Split(val, "\000")
|
||||
name := key[len("choices-"):]
|
||||
if cval, err := cmd.Flags().GetString(name); err == nil && !key_in_slice(allowed, cval) {
|
||||
return fmt.Errorf("%s: Invalid value: %s. Allowed values are: %s", color.YellowString("--"+name), color.RedString(cval), strings.Join(allowed, ", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var title_fmt = color.New(color.FgBlue, color.Bold).SprintFunc()
|
||||
var exe_fmt = color.New(color.FgYellow, color.Bold).SprintFunc()
|
||||
var opt_fmt = color.New(color.FgGreen).SprintFunc()
|
||||
var italic_fmt = color.New(color.Italic).SprintFunc()
|
||||
|
||||
func cmd_name(cmd *cobra.Command) string {
|
||||
if cmd.Annotations != nil {
|
||||
parts := strings.Split(cmd.Annotations["exe"], " ")
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
return cmd.Name()
|
||||
}
|
||||
|
||||
func print_created_by(root *cobra.Command) {
|
||||
fmt.Println(italic_fmt(root.Annotations["exe"]), opt_fmt(root.Version), "created by", title_fmt("Kovid Goyal"))
|
||||
}
|
||||
|
||||
func print_with_indent(text string, indent string, screen_width int) {
|
||||
x := len(indent)
|
||||
fmt.Print(indent)
|
||||
in_sgr := false
|
||||
current_word := ""
|
||||
|
||||
print_word := func(r rune) {
|
||||
w := runewidth.StringWidth(current_word)
|
||||
if x+w > screen_width {
|
||||
fmt.Println()
|
||||
fmt.Print(indent)
|
||||
x = len(indent)
|
||||
current_word = strings.TrimSpace(current_word)
|
||||
}
|
||||
fmt.Print(current_word)
|
||||
current_word = string(r)
|
||||
x += w
|
||||
}
|
||||
|
||||
for _, r := range text {
|
||||
if in_sgr {
|
||||
if r == 'm' {
|
||||
in_sgr = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if r == 0x1b {
|
||||
in_sgr = true
|
||||
continue
|
||||
}
|
||||
if current_word != "" && unicode.IsSpace(r) && r != 0xa0 {
|
||||
print_word(r)
|
||||
} else {
|
||||
current_word += string(r)
|
||||
}
|
||||
}
|
||||
if current_word != "" {
|
||||
print_word(' ')
|
||||
fmt.Print(current_word)
|
||||
}
|
||||
if len(text) > 0 {
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func show_usage(cmd *cobra.Command) error {
|
||||
ws, tty_size_err := GetTTYSize()
|
||||
screen_width := 80
|
||||
if tty_size_err == nil && ws.Cols < 80 {
|
||||
screen_width = int(ws.Cols)
|
||||
}
|
||||
fmt.Println(title_fmt("Usage")+":", exe_fmt(cmd.Annotations["exe"]), cmd.Use)
|
||||
fmt.Println()
|
||||
if len(cmd.Long) > 0 {
|
||||
print_with_indent(cmd.Long, "", screen_width)
|
||||
} else if len(cmd.Short) > 0 {
|
||||
print_with_indent(cmd.Short, "", screen_width)
|
||||
}
|
||||
if cmd.HasAvailableSubCommands() {
|
||||
fmt.Println()
|
||||
fmt.Println(title_fmt("Commands") + ":")
|
||||
for _, child := range cmd.Commands() {
|
||||
fmt.Println(" ", opt_fmt(cmd_name(child)))
|
||||
print_with_indent(child.Short, " ", screen_width)
|
||||
}
|
||||
fmt.Println()
|
||||
print_with_indent("Get help for an individual command by running:", "", screen_width)
|
||||
fmt.Println(" ", cmd.Annotations["exe"], italic_fmt("command"), "-h")
|
||||
}
|
||||
if cmd.HasAvailableFlags() {
|
||||
options_title := cmd.Annotations["options_title"]
|
||||
if len(options_title) == 0 {
|
||||
options_title = "Options"
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println(title_fmt(options_title) + ":")
|
||||
flag_set := cmd.LocalFlags()
|
||||
flag_set.VisitAll(func(flag *pflag.Flag) {
|
||||
fmt.Print(opt_fmt(" --" + flag.Name))
|
||||
if flag.Shorthand != "" {
|
||||
fmt.Print(", ", opt_fmt("-"+flag.Shorthand))
|
||||
}
|
||||
defval := ""
|
||||
switch flag.Value.Type() {
|
||||
default:
|
||||
defval = fmt.Sprintf("[=%s]", italic_fmt(flag.DefValue))
|
||||
case "bool":
|
||||
case "count":
|
||||
}
|
||||
if defval != "" {
|
||||
fmt.Print(" ", defval)
|
||||
}
|
||||
fmt.Println()
|
||||
if flag.Name == "help" {
|
||||
fmt.Println(" ", "Print this help message")
|
||||
fmt.Println()
|
||||
return
|
||||
}
|
||||
for _, line := range strings.Split(flag.Usage, "\n") {
|
||||
print_with_indent(line, " ", screen_width)
|
||||
}
|
||||
if cmd.Annotations["choices-"+flag.Name] != "" {
|
||||
fmt.Println(" Choices:", strings.Join(strings.Split(cmd.Annotations["choices-"+flag.Name], "\000"), ", "))
|
||||
}
|
||||
fmt.Println()
|
||||
})
|
||||
}
|
||||
if cmd.HasParent() {
|
||||
cmd.VisitParents(func(cmd *cobra.Command) {
|
||||
if !cmd.HasParent() {
|
||||
print_created_by(cmd)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
print_created_by(cmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateCommand(cmd *cobra.Command, exe string) *cobra.Command {
|
||||
cmd.Annotations = make(map[string]string)
|
||||
cmd.Annotations["exe"] = exe
|
||||
return cmd
|
||||
}
|
||||
|
||||
func Init(root *cobra.Command) {
|
||||
root.Version = kitty.VersionString
|
||||
root.PersistentPreRunE = ValidateChoices
|
||||
root.SetUsageFunc(show_usage)
|
||||
root.SetHelpTemplate("{{.UsageString}}")
|
||||
}
|
32
tools/cmd/at/main.go
Normal file
32
tools/cmd/at/main.go
Normal file
@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"kitty/tools/cli"
|
||||
"kitty/tools/crypto"
|
||||
)
|
||||
|
||||
var encrypt_cmd = crypto.Encrypt_cmd
|
||||
|
||||
func main() {
|
||||
var root = cli.CreateCommand(&cobra.Command{
|
||||
Use: "[global options] command [command options] [command args]",
|
||||
Short: "Control kitty remotely",
|
||||
Long: "Control kitty by sending it commands. Set the allow_remote_control option in kitty.conf or use a password, for this to work.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
use_password, _ := cmd.Flags().GetString("use-password")
|
||||
fmt.Println("In global run, use-password:", use_password)
|
||||
},
|
||||
}, "kitty-at")
|
||||
cli.PersistentChoices(root, "use-password", "If no password is available, kitty will usually just send the remote control command without a password. This option can be used to force it to always or never use the supplied password.", "if-available", "always", "never")
|
||||
root.Annotations["options_title"] = "Global options"
|
||||
cli.Init(root)
|
||||
if err := root.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
117
tools/crypto/crypto.go
Normal file
117
tools/crypto/crypto.go
Normal file
@ -0,0 +1,117 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"kitty/tools/base85"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Cmd struct {
|
||||
Cmd string `json:"cmd"`
|
||||
Version [3]int `json:"version"`
|
||||
NoResponse bool `json:"no_response,omitifempty"`
|
||||
Payload map[string]interface{} `json:"payload,omitifempty"`
|
||||
Timestamp int64 `json:"timestamp,omitifempty"`
|
||||
Password string `json:"password,omitifempty"`
|
||||
}
|
||||
|
||||
func curve25519_key_pair() (private_key []byte, public_key []byte, err error) {
|
||||
private_key = make([]byte, 32)
|
||||
_, err = rand.Read(private_key)
|
||||
if err == nil {
|
||||
public_key, err = curve25519.X25519(private_key[:], curve25519.Basepoint)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func curve25519_derive_shared_secret(private_key []byte, public_key []byte) (secret []byte, err error) {
|
||||
secret, err = curve25519.X25519(private_key[:], public_key[:])
|
||||
return
|
||||
}
|
||||
|
||||
func b85_encode(data []byte) (encoded string) {
|
||||
encoded = base85.EncodeToString(data)
|
||||
return
|
||||
}
|
||||
|
||||
func b85_decode(data string) (decoded []byte, err error) {
|
||||
decoded, err = base85.DecodeString(data)
|
||||
return
|
||||
}
|
||||
|
||||
func encrypt(plaintext []byte, alice_public_key []byte) (iv []byte, tag []byte, ciphertext []byte, bob_public_key []byte, err error) {
|
||||
bob_private_key, bob_public_key, err := curve25519_key_pair()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
shared_secret_raw, err := curve25519_derive_shared_secret(bob_private_key, alice_public_key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
shared_secret_hashed := sha256.Sum256(shared_secret_raw)
|
||||
shared_secret := shared_secret_hashed[:]
|
||||
block, err := aes.NewCipher(shared_secret)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
aesgcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
iv = make([]byte, aesgcm.NonceSize())
|
||||
_, err = rand.Read(iv)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output := aesgcm.Seal(nil, iv, plaintext, nil)
|
||||
ciphertext = output[0 : len(output)-16]
|
||||
tag = output[len(output)-16:]
|
||||
return
|
||||
}
|
||||
|
||||
type EncryptedCmd struct {
|
||||
Version [3]int `json:"version"`
|
||||
IV string `json:"iv"`
|
||||
Tag string `json:"tag"`
|
||||
Pubkey string `json:"pubkey"`
|
||||
Encrypted string `json:"encrypted"`
|
||||
}
|
||||
|
||||
func Encrypt_cmd(cmd *Cmd, password string, other_pubkey []byte) (encrypted_cmd EncryptedCmd, err error) {
|
||||
if len(other_pubkey) == 0 {
|
||||
raw := os.Getenv("KITTY_PUBLIC_KEY")
|
||||
if len(raw) == 0 {
|
||||
err = errors.New("No KITTY_PUBLIC_KEY environment variable set cannot use passwords")
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(raw, "1:") {
|
||||
err = fmt.Errorf("KITTY_PUBLIC_KEY has unknown protocol: %s", raw[:2])
|
||||
return
|
||||
}
|
||||
other_pubkey, err = b85_decode(raw[2:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
cmd.Password = password
|
||||
cmd.Timestamp = time.Now().UnixNano()
|
||||
plaintext, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
iv, tag, ciphertext, pubkey, err := encrypt(plaintext, other_pubkey)
|
||||
encrypted_cmd = EncryptedCmd{
|
||||
Version: cmd.Version, IV: b85_encode(iv), Tag: b85_encode(tag), Pubkey: b85_encode(pubkey), Encrypted: b85_encode(ciphertext)}
|
||||
return
|
||||
}
|
||||
|
||||
// }}}
|
44
version.go
Normal file
44
version.go
Normal file
@ -0,0 +1,44 @@
|
||||
package kitty
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
//go:embed kitty/constants.py
|
||||
var raw string
|
||||
|
||||
type VersionType struct {
|
||||
major, minor, patch int
|
||||
}
|
||||
|
||||
var VersionString string
|
||||
var Version VersionType
|
||||
var VCSRevision string
|
||||
|
||||
func init() {
|
||||
var verpat = regexp.MustCompile(`Version\((\d+),\s*(\d+),\s*(\d+)\)`)
|
||||
matches := verpat.FindStringSubmatch(raw)
|
||||
major, err := strconv.Atoi(matches[1])
|
||||
minor, err := strconv.Atoi(matches[2])
|
||||
patch, err := strconv.Atoi(matches[3])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Version.major = major
|
||||
Version.minor = minor
|
||||
Version.patch = patch
|
||||
VersionString = fmt.Sprint(major, ".", minor, ".", patch)
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if ok {
|
||||
for _, bs := range bi.Settings {
|
||||
if bs.Key == "vcs.revision" {
|
||||
VCSRevision = bs.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user