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