Skip to content

Commit da016b7

Browse files
committed
- unit tests
- split iterm/kitty writers into separate image.Image & io.Reader encoders - first stab at checking terminal for SIXEL capability - added RequestTermAttributes()
1 parent 0fe3747 commit da016b7

16 files changed

+425
-37
lines changed

README.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,17 @@ Encodes images to iTerm / Kitty / SIXEL (terminal) inline graphics protocols.
99
- *Sixel*: https://saitoha.github.io/libsixel/
1010

1111
## TODO
12+
- screenshots
13+
- check that mintty supports iterm/wezterm format, get mintty identifier
14+
- perhaps query tmux directly
15+
TMUX=/tmp/tmux-1000/default,3218,4
1216
- improve terminal identification
1317
4:sixel graphics
1418
ESC[0c = 63;1;2;4;6;9;15;22c
1519
19:VT340
1620
ESC[>0c = 19;344:0c
1721
https://invisible-island.net/xterm/ctlseqs/ctlseqs-contents.html
1822

19-
- check that mintty supports iterm/wezterm format, get mintty identifier
20-
- unit tests
21-
- conditionally enable/disable tmux passthrough
22-
23-
perhaps query tmux directly
24-
TMUX=/tmp/tmux-1000/default,3218,4
25-
2623
## TESTING
2724
- test sixel with
2825
- https://github.com/liamg/aminal

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
module github.com/BourgeoisBear/rasterm
22

33
go 1.16
4+
5+
require golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
2+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
3+
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
4+
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

iterm_wez.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,18 @@ func IsTermItermWez() bool {
3333
Encode image using the iTerm2/WezTerm terminal image protocol:
3434
https://iterm2.com/documentation-images.html
3535
*/
36-
func (S Settings) WriteItermImage(out io.Writer, iImg image.Image) (E error) {
36+
func (S Settings) ItermWriteImage(out io.Writer, iImg image.Image) error {
3737

3838
pBuf := new(bytes.Buffer)
39-
if E = png.Encode(pBuf, iImg); E != nil {
40-
return
39+
if E := png.Encode(pBuf, iImg); E != nil {
40+
return E
4141
}
4242

43+
return S.ItermCopyFileInline(out, pBuf, int64(pBuf.Len()))
44+
}
45+
46+
func (S Settings) ItermCopyFileInline(out io.Writer, in io.Reader, nLen int64) (E error) {
47+
4348
OSC_OPEN, OSC_CLOSE := ITERM_IMG_HDR, ITERM_IMG_FTR
4449
if S.EscapeTmux && IsTmuxScreen() {
4550
OSC_OPEN, OSC_CLOSE = TmuxOscOpenClose(OSC_OPEN, OSC_CLOSE)
@@ -49,13 +54,13 @@ func (S Settings) WriteItermImage(out io.Writer, iImg image.Image) (E error) {
4954
return
5055
}
5156

52-
hdrSize := fmt.Sprintf(";size=%d:", pBuf.Len())
57+
hdrSize := fmt.Sprintf(";size=%d:", nLen)
5358
if _, E = out.Write([]byte(hdrSize)); E != nil {
5459
return
5560
}
5661

5762
enc64 := base64.NewEncoder(base64.StdEncoding, out)
58-
if _, E = enc64.Write(pBuf.Bytes()); E != nil {
63+
if _, E = io.Copy(enc64, in); E != nil {
5964
return
6065
}
6166

kitty.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package rasterm
33
import (
44
"bytes"
55
"encoding/base64"
6+
"fmt"
67
"image"
78
"image/png"
89
"io"
@@ -12,7 +13,7 @@ import (
1213

1314
const (
1415
KITTY_IMG_HDR = "\x1b_G"
15-
KITTY_IMG_FTR = "\x1b\\\n"
16+
KITTY_IMG_FTR = "\x1b\\"
1617
)
1718

1819
// NOTE: uses $TERM, which is overwritten by tmux
@@ -23,10 +24,23 @@ func IsTermKitty() bool {
2324
}
2425

2526
/*
26-
Encode image using the Kitty terminal graphics protocol: https://sw.kovidgoyal.net/kitty/graphics-protocol.html
27+
Encode image using the Kitty terminal graphics protocol:
28+
https://sw.kovidgoyal.net/kitty/graphics-protocol.html
2729
*/
28-
func (S Settings) WriteKittyImage(out io.Writer, iImg image.Image) (E error) {
30+
func (S Settings) KittyWriteImage(out io.Writer, iImg image.Image) error {
2931

32+
pBuf := new(bytes.Buffer)
33+
if E := png.Encode(pBuf, iImg); E != nil {
34+
return E
35+
}
36+
37+
return S.KittyCopyPNGInline(out, pBuf, int64(pBuf.Len()))
38+
}
39+
40+
// Encode raw PNG data into Kitty terminal format
41+
func (S Settings) KittyCopyPNGInline(out io.Writer, in io.Reader, nLen int64) (E error) {
42+
43+
// OPTIONALLY TMUX-ESCAPE OPENING & CLOSING OSC CODES
3044
OSC_OPEN, OSC_CLOSE := KITTY_IMG_HDR, KITTY_IMG_FTR
3145
if S.EscapeTmux && IsTmuxScreen() {
3246
OSC_OPEN, OSC_CLOSE = TmuxOscOpenClose(OSC_OPEN, OSC_CLOSE)
@@ -51,7 +65,7 @@ func (S Settings) WriteKittyImage(out io.Writer, iImg image.Image) (E error) {
5165

5266
// os.Stderr.Write([]byte(fmt.Sprintf("%d", len(src))))
5367
if b_params == nil {
54-
b_params = []byte("a=T,f=100,z=-1,")
68+
b_params = []byte(fmt.Sprintf("a=T,f=100,z=-1,S=%d,", nLen))
5569
} else {
5670
b_params = nil
5771
}
@@ -64,6 +78,6 @@ func (S Settings) WriteKittyImage(out io.Writer, iImg image.Image) (E error) {
6478
enc64 := base64.NewEncoder(base64.StdEncoding, &oWC)
6579
defer enc64.Close()
6680

67-
E = png.Encode(enc64, iImg)
81+
_, E = io.Copy(enc64, in)
6882
return
6983
}

rasterm_test.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package rasterm
2+
3+
import (
4+
"fmt"
5+
"image"
6+
_ "image/gif"
7+
_ "image/jpeg"
8+
_ "image/png"
9+
"io"
10+
"os"
11+
"testing"
12+
)
13+
14+
// godoc -http=:8099 -goroot="$HOME/go"
15+
16+
var testFiles []string
17+
18+
func init() {
19+
20+
files, e := os.ReadDir("./test_images")
21+
if e != nil {
22+
panic(e)
23+
}
24+
25+
for ix := range files {
26+
testFiles = append(testFiles, files[ix].Name())
27+
}
28+
29+
os.Stdout.Write([]byte(ESC_ERASE_DISPLAY))
30+
}
31+
32+
func loadImage(path string) (iImg image.Image, imgFmt string, E error) {
33+
34+
pF, E := os.Open(path)
35+
if E != nil {
36+
return
37+
}
38+
defer pF.Close()
39+
40+
return image.Decode(pF)
41+
}
42+
43+
func getFile(fpath string) (*os.File, int64, error) {
44+
45+
pF, E := os.Open(fpath)
46+
if E != nil {
47+
return nil, 0, E
48+
}
49+
50+
fInf, E := pF.Stat()
51+
if E != nil {
52+
pF.Close()
53+
return nil, 0, E
54+
}
55+
56+
return pF, fInf.Size(), nil
57+
}
58+
59+
func getImgInfo(pF *os.File) (imgCfg image.Config, fmtName string, E error) {
60+
61+
if imgCfg, fmtName, E = image.DecodeConfig(pF); E != nil {
62+
return
63+
}
64+
65+
// REWIND FILE
66+
_, E = pF.Seek(0, 0)
67+
return
68+
}
69+
70+
func testEx(pT *testing.T, out io.Writer, mode string, testFiles []string) error {
71+
72+
S := Settings{
73+
EscapeTmux: false,
74+
}
75+
76+
for _, file := range testFiles {
77+
78+
fpath := "./test_images/" + file
79+
pT.Log(fpath)
80+
81+
fIn, nImgLen, e2 := getFile(fpath)
82+
if e2 != nil {
83+
pT.Log(e2)
84+
continue
85+
}
86+
defer fIn.Close()
87+
88+
pT.Logf("IMAGE SIZE %d", nImgLen)
89+
90+
imgCfg, fmtName, e2 := getImgInfo(fIn)
91+
if e2 != nil {
92+
pT.Log(e2)
93+
continue
94+
}
95+
96+
pT.Logf("FMT: %s, W: %d, H: %d", fmtName, imgCfg.Width, imgCfg.Height)
97+
98+
iImg, _, e2 := loadImage(fpath)
99+
if e2 != nil {
100+
pT.Log(e2)
101+
continue
102+
}
103+
104+
var e3 error = nil
105+
switch mode {
106+
case "iterm":
107+
108+
// WEZ/ITERM SUPPORT ALL FORMATS, SO NO NEED TO RE-ENCODE TO PNG
109+
e3 = S.ItermCopyFileInline(out, fIn, nImgLen)
110+
111+
case "sixel":
112+
113+
if iPaletted, bOK := iImg.(*image.Paletted); bOK {
114+
115+
e3 = S.SixelWriteImage(out, iPaletted)
116+
117+
} else {
118+
119+
pT.Logf("%s is type [%T], not *image.Paletted\n", file, iImg)
120+
continue
121+
}
122+
123+
case "kitty":
124+
125+
if fmtName == "png" {
126+
127+
e3 = S.KittyCopyPNGInline(out, fIn, nImgLen)
128+
129+
} else {
130+
131+
e3 = S.KittyWriteImage(out, iImg)
132+
}
133+
}
134+
135+
if e3 != nil {
136+
pT.Log(e3)
137+
}
138+
fmt.Println("")
139+
}
140+
141+
return nil
142+
}
143+
144+
// NOTE
145+
//
146+
// can't query terminal attributes here (i.e. sixel support) since golang
147+
// testbed intermediates itself between stdin/stdout with buffers
148+
func TestSixel(pT *testing.T) {
149+
150+
if IsTermItermWez() || IsTermKitty() {
151+
return
152+
}
153+
154+
if E := testEx(pT, os.Stdout, "sixel", testFiles); E != nil {
155+
pT.Fatal(E)
156+
}
157+
}
158+
159+
func TestItermWez(pT *testing.T) {
160+
161+
if IsTermItermWez() {
162+
pT.Log("TESTING ITERM2/WEZ:")
163+
if E := testEx(pT, os.Stdout, "iterm", testFiles); E != nil {
164+
pT.Fatal(E)
165+
}
166+
}
167+
}
168+
169+
func TestKitty(pT *testing.T) {
170+
171+
if IsTermKitty() {
172+
pT.Log("TESTING KITTY TERM")
173+
if E := testEx(pT, os.Stdout, "kitty", testFiles); E != nil {
174+
pT.Fatal(E)
175+
}
176+
}
177+
}

sixel.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@ const (
1313
SIXEL_MAX byte = 0x7e
1414
)
1515

16+
func IsSixelCapable() (bool, error) {
17+
18+
sATT, E := RequestTermAttributes()
19+
if E != nil {
20+
return false, E
21+
}
22+
23+
for ix := range sATT {
24+
25+
// IGNORE `4` @ 1ST INDEX -- THAT IS TERMINAL ID RATHER THAN SIXEL SUPPORT
26+
if (ix > 0) && (sATT[ix] == 4) {
27+
return true, nil
28+
}
29+
}
30+
31+
return false, nil
32+
}
33+
1634
/*
1735
Encodes a paletted image into DECSIXEL format.
1836
Forked & heavily modified from https://github.com/mattn/go-sixel/
@@ -28,7 +46,7 @@ For more information on DECSIXEL format:
2846
https://www.vt100.net/docs/vt3xx-gp/chapter14.html
2947
https://saitoha.github.io/libsixel/
3048
*/
31-
func (S Settings) WriteSixelImage(out io.Writer, pI *image.Paletted) (E error) {
49+
func (S Settings) SixelWriteImage(out io.Writer, pI *image.Paletted) (E error) {
3250

3351
width, height := pI.Bounds().Dx(), pI.Bounds().Dy()
3452
if (width <= 0) || (height <= 0) {

0 commit comments

Comments
 (0)