~ghost08/kgp

3832ac2a2e55d61b197eaec0742598f91c600972 — VladimĂ­r Magyar 2 years ago master
init
5 files changed, 210 insertions(+), 0 deletions(-)

A go.mod
A gopher.png
A kgp_test.go
A main.go
A write_chunker.go
A  => go.mod +3 -0
@@ 1,3 @@
module git.sr.ht/~ghost08/kgp

go 1.17

A  => gopher.png +0 -0
A  => kgp_test.go +20 -0
@@ 1,20 @@
package kgp

import (
	"image"
	_ "image/png"
	"os"
	"testing"
)

func TestWriteImage(t *testing.T) {
	f, err := os.Open("gopher.png")
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	img, _, err := image.Decode(f)
	if err := WriteImage(os.Stdout, img); err != nil {
		t.Fatal(err)
	}
}

A  => main.go +120 -0
@@ 1,120 @@
package kgp

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"image"
	"image/png"
	"io"
	"os"
	"strings"
	"syscall"
	"unsafe"
)

const tiocgwinsz = 0x5413

func ioctl(fd, op, arg uintptr) error {
	_, _, ep := syscall.Syscall(syscall.SYS_IOCTL, fd, op, arg)
	if ep != 0 {
		return syscall.Errno(ep)
	}
	return nil
}

type WinSize struct {
	Rows   int16 /* rows, in characters */
	Cols   int16 /* columns, in characters */
	Xpixel int16 /* horizontal size, pixels */
	Ypixel int16 /* vertical size, pixels */
}

func GetWinSize() (WinSize, error) {
	var sz WinSize
	err := ioctl(0, tiocgwinsz, uintptr(unsafe.Pointer(&sz)))
	return sz, err
}

const (
	KITTY_IMG_HDR = "\x1b_G"
	KITTY_IMG_FTR = "\x1b\\"
)

// NOTE: uses $TERM, which is overwritten by tmux
func IsTermKitty() bool {

	V := getEnvIdentifiers()
	return V["TERM"] == "xterm-kitty"
}

func getEnvIdentifiers() map[string]string {

	KEYS := []string{"TERM", "TERM_PROGRAM", "LC_TERMINAL"}
	V := make(map[string]string)
	for _, K := range KEYS {
		V[K] = lcaseEnv(K)
	}

	return V
}

func lcaseEnv(k string) string {
	return strings.ToLower(strings.TrimSpace(os.Getenv(k)))
}

/*
Encode image using the Kitty terminal graphics protocol:
https://sw.kovidgoyal.net/kitty/graphics-protocol.html
*/
func WriteImage(out io.Writer, iImg image.Image) error {

	pBuf := new(bytes.Buffer)
	if E := png.Encode(pBuf, iImg); E != nil {
		return E
	}

	return CopyPNGInline(out, pBuf, int64(pBuf.Len()))
}

// Encode raw PNG data into Kitty terminal format
func CopyPNGInline(out io.Writer, in io.Reader, nLen int64) (err error) {

	OSC_OPEN, OSC_CLOSE := KITTY_IMG_HDR, KITTY_IMG_FTR

	// LAST CHUNK SIGNAL `m=0` TO KITTY
	defer func() {

		if err == nil {
			out.Write([]byte(OSC_OPEN))
			out.Write([]byte("m=0;"))
			_, err = out.Write([]byte(OSC_CLOSE))
		}
	}()

	// PIPELINE: PNG -> B64 -> CHUNKER -> out io.Writer
	// SEND IN 4K CHUNKS
	oWC := NewWriteChunker(out, 4096)
	defer oWC.Flush()
	bsHdr := []byte(fmt.Sprintf("a=T,f=100,z=-1,S=%d,", nLen))
	oWC.CustomWriFunc = func(iWri io.Writer, bsDat []byte) (int, error) {

		parts := [][]byte{
			[]byte(OSC_OPEN),
			bsHdr,
			[]byte("m=1;"),
			bsDat,
			[]byte(OSC_CLOSE),
		}

		bsHdr = nil

		return iWri.Write(bytes.Join(parts, nil))
	}

	enc64 := base64.NewEncoder(base64.StdEncoding, &oWC)
	defer enc64.Close()

	_, err = io.Copy(enc64, in)
	return
}

A  => write_chunker.go +67 -0
@@ 1,67 @@
package kgp

import "io"

/*
Used by WriteChunker to optionally transform chunks before
sending them on to the underlying io.Writer.
*/
type CustomWriFunc func(io.Writer, []byte) (int, error)

/*
Wraps an io.Writer interface to buffer/flush in chunks that are
`chunkSize` bytes long.  Optional `CustomWriFunc` in struct
allows for additional []byte processing before sending each
chunk to the underlying writer. Currently used for encoding to
Kitty terminal's image format.
*/
type WriteChunker struct {
	chunk  []byte
	writer io.Writer
	ix     int
	CustomWriFunc
}

func NewWriteChunker(iWri io.Writer, chunkSize int) WriteChunker {

	if chunkSize < 1 {
		panic("invalid chunk size")
	}

	return WriteChunker{
		chunk:  make([]byte, chunkSize),
		writer: iWri,
	}
}

func (pC *WriteChunker) Flush() (E error) {

	tmp := pC.chunk[:pC.ix]
	if pC.CustomWriFunc != nil {
		_, E = pC.CustomWriFunc(pC.writer, tmp)
	} else {
		_, E = pC.writer.Write(tmp)
	}

	pC.ix = 0
	return
}

func (pC *WriteChunker) Write(src []byte) (int, error) {

	chunkSize := len(pC.chunk)

	for _, bt := range src {

		pC.chunk[pC.ix] = bt
		pC.ix++

		if pC.ix >= chunkSize {
			if e := pC.Flush(); e != nil {
				return 0, e
			}
		}
	}

	return len(src), nil
}