-
Notifications
You must be signed in to change notification settings - Fork 0
/
kitty.go
154 lines (127 loc) · 3.35 KB
/
kitty.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package igo
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"image"
"image/png"
"io"
"strings"
)
// See https://sw.kovidgoyal.net/kitty/graphics-protocol.html for more details.
const (
KITTY_IMG_HDR = "\x1b_G"
KITTY_IMG_FTR = "\x1b\\"
)
type KittyImgOpts struct {
SrcX uint32 // x=
SrcY uint32 // y=
SrcWidth uint32 // w=
SrcHeight uint32 // h=
CellOffsetX uint32 // X= (pixel x-offset inside terminal cell)
CellOffsetY uint32 // Y= (pixel y-offset inside terminal cell)
DstCols uint32 // c= (display width in terminal columns)
DstRows uint32 // r= (display height in terminal rows)
ZIndex int32 // z=
ImageId uint32 // i=
ImageNo uint32 // I=
PlacementId uint32 // p=
}
func (o KittyImgOpts) ToHeader(opts ...string) string {
type fldmap struct {
pv *uint32
code rune
}
sFld := []fldmap{
{&o.SrcX, 'x'},
{&o.SrcY, 'y'},
{&o.SrcWidth, 'w'},
{&o.SrcHeight, 'h'},
{&o.CellOffsetX, 'X'},
{&o.CellOffsetY, 'Y'},
{&o.DstCols, 'c'},
{&o.DstRows, 'r'},
{&o.ImageId, 'i'},
{&o.ImageNo, 'I'},
{&o.PlacementId, 'p'},
}
for _, f := range sFld {
if *f.pv != 0 {
opts = append(opts, fmt.Sprintf("%c=%d", f.code, *f.pv))
}
}
if o.ZIndex != 0 {
opts = append(opts, fmt.Sprintf("z=%d", o.ZIndex))
}
return KITTY_IMG_HDR + strings.Join(opts, ",") + ";"
}
// checks if terminal supports kitty image protocols
func IsKittyCapable() bool {
// TODO: more rigorous check
V := GetEnvIdentifiers()
return (len(V["KITTY_WINDOW_ID"]) > 0) || (V["TERM_PROGRAM"] == "wezterm")
}
// Display local PNG file
// - pngFileName must be directly accesssible from Kitty instance
// - pngFileName must be an absolute path
func KittyWritePNGLocal(out io.Writer, pngFileName string, opts KittyImgOpts) error {
_, e := fmt.Fprint(out, opts.ToHeader("a=T", "f=100", "t=f"))
if e != nil {
return e
}
enc64 := base64.NewEncoder(base64.StdEncoding, out)
_, e = fmt.Fprint(enc64, pngFileName)
if e != nil {
return e
}
e = enc64.Close()
if e != nil {
return e
}
_, e = fmt.Fprint(out, KITTY_IMG_FTR)
return e
}
// Serialize image.Image into Kitty terminal in-band format.
func KittyWriteImage(out io.Writer, iImg image.Image, opts KittyImgOpts) error {
pBuf := new(bytes.Buffer)
if E := png.Encode(pBuf, iImg); E != nil {
return E
}
return KittyWritePngReader(out, pBuf, opts)
}
// Serialize PNG image from io.Reader into Kitty terminal in-band format.
func KittyWritePngReader(out io.Writer, in io.Reader, opts KittyImgOpts) error {
_, err := fmt.Fprint(out, opts.ToHeader("a=T", "f=100", "t=d", "m=1"), KITTY_IMG_FTR)
if err != nil {
return err
}
// PIPELINE: PNG (io.Reader) -> B64 -> CHUNKER -> (io.Writer)
// SEND IN 4K CHUNKS
chunk := kittyChunk{
chunkSize: 4096,
writer: out,
}
enc64 := base64.NewEncoder(base64.StdEncoding, &chunk)
_, err = io.Copy(enc64, in)
return errors.Join(
err,
enc64.Close(),
chunk.Close(),
)
}
func KittyClean(out io.Writer, opts KittyImgOpts) error {
// Remove the image
_, err := fmt.Fprint(out, opts.ToHeader("a=d", "d=a"), KITTY_IMG_FTR)
if err != nil {
return err
}
// Clear the affected terminal lines
_, err = fmt.Fprint(out, "\033[2J") // Clear the entire screen
if err != nil {
return err
}
// Optionally, reset the cursor position if needed
_, err = fmt.Fprint(out, "\033[H") // Move cursor to home position (top-left)
return err
}