Skip to content

Commit 49c2256

Browse files
committed
Rewrite
1 parent 0ce274f commit 49c2256

22 files changed

+587
-1023
lines changed

README.md

+21-121
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,33 @@
11
# rasterm
22

3-
Encodes images to iTerm / Kitty / SIXEL (terminal) inline graphics protocols.
3+
Package `rasterm` provides a simple way to encode images as terminal graphics,
4+
supporting Kitty, iTerm, and Sixel.
45

5-
[![GoDoc](https://godoc.org/github.com/BourgeoisBear/rasterm?status.png)](http://godoc.org/github.com/BourgeoisBear/rasterm)
6+
[![Unit Tests][rasterm-ci-status]][rasterm-ci]
7+
[![Go Reference][goref-rasterm-status]][goref-rasterm]
8+
[![Releases][release-status]][Releases]
9+
[![Discord Discussion][discord-status]][discord]
610

7-
![rasterm sample output](screenshot.png)
8-
9-
## Supported Image Encodings
10-
11-
- **Kitty**
12-
- **iTerm2 / WezTerm**
13-
- **Sixel**
14-
15-
## TODO
16-
17-
- mintty:
18-
- detection for iTerm format: https://github.com/mintty/mintty/issues/881
19-
- iTerm2:
20-
- support: name, width, height, preserveAspectRatio options
21-
- Kitty:
22-
- support animation
23-
- support image placement /dims
24-
- perhaps query tmux directly: TMUX=/tmp/tmux-1000/default,3218,4
25-
- improve terminal identification
26-
19:VT340
27-
ESC[>0c = 19;344:0c
28-
https://invisible-island.net/xterm/ctlseqs/ctlseqs-contents.html
29-
30-
## TESTING
31-
32-
- test sixel with
33-
- https://domterm.org/
34-
- https://www.macterm.net/
35-
- test wez/iterm img with
36-
- iterm2
37-
- https://www.macterm.net/
38-
39-
## Notes
40-
41-
### terminal features matrix
11+
Fork/rewrite of [`rasterm`][original-rasterm].
4212

43-
| terminal | sixel | iTerm2 format | kitty format |
44-
| :--- | :--: | :--: | :--: |
45-
| iterm2 | Y | Y | |
46-
| kitty | | | Y |
47-
| mintty | Y | Y | |
48-
| mlterm | Y | Y | |
49-
| putty | | | |
50-
| rlogin | Y | Y | |
51-
| wezterm | Y | Y | |
52-
| xterm | Y | | |
53-
54-
### known responses
55-
56-
#### CSI 0 c
57-
58-
| terminal | response |
59-
| :---- | :---- |
60-
| apple terminal | `\x1b[?1;2c` |
61-
| guake | `\x1b[?65;1;9c` |
62-
| iterm2 | `\x1b[?62;4c` |
63-
| kitty | `\x1b[?62;c` |
64-
| mintty | `\x1b[?64;1;2;4;6;9;15;21;22;28;29c` |
65-
| mlterm | `\x1b[?63;1;2;3;4;7;29c` |
66-
| putty | `\x1b[?6c` |
67-
| rlogin | `\x1b[?65;1;2;3;4;6;7;8;9;15;18;21;22;29;39;42;44c` |
68-
| st | `\x1b[?6c` |
69-
| terminology | `\x1b[?64;1;9;15;18;21;22c` |
70-
| vimterm | `\x1b[?1;2c` |
71-
| wez | `\x1b[?65;4;6;18;22c` |
72-
| xfce | `\x1b[?65;1;9c` |
73-
| xterm | `\x1b[?63;1;2;4;6;9;15;22c` |
74-
75-
#### CSI > 0 c
76-
77-
| terminal | response |
78-
| :---- | :---- |
79-
| apple terminal | `\x1b[>1;95;0c` |
80-
| guake | `\x1b[>65;5402;1c` |
81-
| iterm2 | `\x1b[>0;95;0c` |
82-
| kitty | `\x1b[>1;4000;19c` |
83-
| mintty | `\x1b[>77;30104;0c` |
84-
| mlterm | `\x1b[>24;279;0c` |
85-
| putty | `\x1b[>0;136;0c` |
86-
| rlogin | `\x1b[>65;331;0c` |
87-
| st | NO RESPONSE |
88-
| vimterm | `\x1b[>0;100;0c` |
89-
| wez | `\x1b[>0;0;0c` |
90-
| xfce | `\x1b[>65;5402;1c` |
91-
| xterm | `\x1b[>19;344;0c` |
92-
93-
#### identifications
13+
![rasterm sample output](screenshot.png)
9414

95-
| terminal | values |
96-
| :---- | :---- |
97-
| apple terminal | `TERM_PROGRAM="Apple_Terminal" ` |
98-
| apple terminal | `__CFBundleIdentifier="com.apple.Terminal"` |
99-
| guake | ` ` |
100-
| iterm2 | `LC_TERMINAL="iTerm2" ` |
101-
| kitty | `TERM="xterm-kitty" ` |
102-
| mintty | `TERM="mintty" ` |
103-
| mlterm | ` ` |
104-
| putty | ` ` |
105-
| rlogin | ` ` |
106-
| st | ` ` |
107-
| terminology | `TERM_PROGRAM=terminology` |
108-
| vimterm | `VIM_TERMINAL is set ` |
109-
| wez | `TERM_PROGRAM="wezterm" ` |
110-
| xfce | ` ` |
111-
| xterm | ` ` |
15+
[rasterm-ci]: https://github.com/kenshaw/rasterm/actions/workflows/test.yml "Test CI"
16+
[rasterm-ci-status]: https://github.com/kenshaw/rasterm/actions/workflows/test.yml/badge.svg "Test CI"
17+
[goref-rasterm]: https://pkg.go.dev/github.com/kenshaw/rasterm "Go Reference"
18+
[goref-rasterm-status]: https://pkg.go.dev/badge/github.com/kenshaw/rasterm.svg "Go Reference"
19+
[release-status]: https://img.shields.io/github/v/release/kenshaw/rasterm?display_name=tag&sort=semver "Latest Release"
20+
[discord]: https://discord.gg/WDWAgXwJqN "Discord Discussion"
21+
[discord-status]: https://img.shields.io/discord/829150509658013727.svg?label=Discord&logo=Discord&colorB=7289da&style=flat-square "Discord Discussion"
22+
[releases]: https://github.com/kenshaw/rasterm/releases "Releases"
23+
[original-rasterm]: https://github.com/BourgeoisBear/rasterm
11224

113-
### opinions
25+
[![GoDoc](https://pkg.go.dev/github.com/kenshaw/rasterm?status.png)](http://pkg.go.dev/github.com/kenshaw/rasterm)
11426

115-
- Sixel is a primitive and wasteful format. Most sixel terminals also support the iTerm2 format--fewer bytes, full color instead of paletted, and no pixel re-processing required. Much better!
27+
## Using
11628

117-
### go stuff
29+
Install in the usual Go fashion:
11830

11931
```sh
120-
go tool pprof -http=:8080 ./name.prof
121-
godoc -http=:8099 -goroot="$HOME/go"
122-
go test -v
123-
go mod tidy
124-
https://blog.golang.org/pprof
32+
$ go get -u github.com/kenshaw/rasterm@latest
12533
```
126-
127-
### more reading
128-
129-
- kitty inline images: https://sw.kovidgoyal.net/kitty/graphics-protocol.html
130-
- iterm2 inline images: https://iterm2.com/documentation-images.html
131-
- xterm ctl seqs: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
132-
- sixel ctl seqs: https://vt100.net/docs/vt3xx-gp/chapter14.html
133-
- libsixel: https://saitoha.github.io/libsixel/

encode.go

+231
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package rasterm
2+
3+
import (
4+
"bytes"
5+
"encoding/base64"
6+
"fmt"
7+
"image"
8+
"image/jpeg"
9+
"image/png"
10+
"io"
11+
"os"
12+
"strings"
13+
"sync"
14+
15+
"github.com/mattn/go-sixel"
16+
)
17+
18+
// DefaultJPEGQuality is the default JPEG encode quality.
19+
var DefaultJPEGQuality = 93
20+
21+
// Encoder provides a common interface for terminal graphic encoders.
22+
type Encoder interface {
23+
String() string
24+
Available() bool
25+
Encode(io.Writer, image.Image) error
26+
}
27+
28+
// KittyEncoder is a Kitty terminal graphics encoder.
29+
type KittyEncoder struct {
30+
NoNewline bool
31+
}
32+
33+
// NewKittyEncoder creates a Kitty terminal graphics encoder.
34+
//
35+
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol.html
36+
func NewKittyEncoder() Encoder {
37+
return KittyEncoder{}
38+
}
39+
40+
// String satisfies the [Encoder] interface.
41+
func (KittyEncoder) String() string {
42+
return "kitty"
43+
}
44+
45+
// Available satisfies the [Encoder] interface.
46+
func (KittyEncoder) Available() bool {
47+
return strings.ToLower(os.Getenv("TERM_GRAPHICS")) == "kitty" ||
48+
strings.ToLower(os.Getenv("TERM")) == "xterm-kitty"
49+
}
50+
51+
// Encode satisfies the [Encoder] interface.
52+
func (r KittyEncoder) Encode(w io.Writer, img image.Image) error {
53+
buf := new(bytes.Buffer)
54+
enc := base64.NewEncoder(base64.StdEncoding, buf)
55+
if err := png.Encode(enc, img); err != nil {
56+
return err
57+
}
58+
if err := enc.Close(); err != nil {
59+
return err
60+
}
61+
if err := chunkEncode(w, buf.Bytes(), 4096); err != nil {
62+
return err
63+
}
64+
if r.NoNewline {
65+
return nil
66+
}
67+
_, err := fmt.Fprintln(w)
68+
return err
69+
}
70+
71+
// ITermEncoder is a iTerm terminal graphics encoder.
72+
//
73+
// See: https://iterm2.com/documentation-images.html
74+
type ITermEncoder struct {
75+
NoNewline bool
76+
}
77+
78+
// NewITermEncoder creates a iTerm terminal graphics encoder.
79+
func NewITermEncoder() Encoder {
80+
return ITermEncoder{}
81+
}
82+
83+
// String satisfies the [Encoder] interface.
84+
func (ITermEncoder) String() string {
85+
return "iterm"
86+
}
87+
88+
// Available satisfies the [Encoder] interface.
89+
func (ITermEncoder) Available() bool {
90+
return strings.ToLower(os.Getenv("TERM_GRAPHICS")) == "iterm" ||
91+
strings.ToLower(os.Getenv("TERM")) == "mintty" ||
92+
strings.ToLower(os.Getenv("LC_TERMINAL")) == "iterm2" ||
93+
strings.ToLower(os.Getenv("TERM_PROGRAM")) == "wezterm"
94+
}
95+
96+
// Encode satisfies the [Encoder] interface.
97+
func (r ITermEncoder) Encode(w io.Writer, img image.Image) error {
98+
f := png.Encode
99+
if _, ok := img.(*image.Paletted); !ok {
100+
f = jpegEncode
101+
}
102+
buf := new(bytes.Buffer)
103+
enc := base64.NewEncoder(base64.StdEncoding, buf)
104+
if err := f(enc, img); err != nil {
105+
return err
106+
}
107+
if err := enc.Close(); err != nil {
108+
return err
109+
}
110+
if _, err := fmt.Fprintf(w, "\x1b]1337;File=inline=1:%s\a", buf.Bytes()); err != nil {
111+
return err
112+
}
113+
if r.NoNewline {
114+
return nil
115+
}
116+
_, err := fmt.Fprintln(w)
117+
return err
118+
}
119+
120+
// SixelEncoder is a Sixel terminal graphics encoder.
121+
type SixelEncoder struct {
122+
NoNewline bool
123+
}
124+
125+
// NewSixelEncoder creates a Sixel terminal graphics encoder.
126+
func NewSixelEncoder() Encoder {
127+
return SixelEncoder{}
128+
}
129+
130+
// String satisfies the [Encoder] interface.
131+
func (SixelEncoder) String() string {
132+
return "sixel"
133+
}
134+
135+
// Available satisfies the [Encoder] interface.
136+
func (SixelEncoder) Available() bool {
137+
if strings.ToLower(os.Getenv("TERM_GRAPHICS")) == "sixel" {
138+
return true
139+
}
140+
return hasSixelSupport()
141+
}
142+
143+
// Encode satisfies the [Encoder] interface.
144+
func (r SixelEncoder) Encode(w io.Writer, img image.Image) error {
145+
if err := sixel.NewEncoder(w).Encode(img); err != nil {
146+
return err
147+
}
148+
if r.NoNewline {
149+
return nil
150+
}
151+
_, err := fmt.Fprintln(w)
152+
return err
153+
}
154+
155+
// DefaultEncoder wraps multiple terminal graphic encoders.
156+
type DefaultEncoder struct {
157+
v []Encoder
158+
r Encoder
159+
err error
160+
once sync.Once
161+
}
162+
163+
// NewDefaultEncoder creates a wrapper for multiple terminal graphic encoders.
164+
func NewDefaultEncoder(v ...Encoder) *DefaultEncoder {
165+
return &DefaultEncoder{
166+
v: v,
167+
}
168+
}
169+
170+
// init initializes the default encoder.
171+
func (r *DefaultEncoder) init() {
172+
for _, z := range r.v {
173+
if z.Available() {
174+
r.r = z
175+
return
176+
}
177+
}
178+
if r.r == nil {
179+
r.err = ErrTermGraphicsNotAvailable
180+
}
181+
}
182+
183+
// String satisfies the [Encoder] interface.
184+
func (r *DefaultEncoder) String() string {
185+
r.once.Do(r.init)
186+
if r.err == nil && r.r != nil {
187+
return r.r.String()
188+
}
189+
return "<none>"
190+
}
191+
192+
// Available satisfies the [Encoder] interface.
193+
func (r *DefaultEncoder) Available() bool {
194+
r.once.Do(r.init)
195+
return r.r != nil && r.err == nil
196+
}
197+
198+
// Encode satisfies the [Encoder] interface.
199+
func (r *DefaultEncoder) Encode(w io.Writer, img image.Image) error {
200+
switch r.once.Do(r.init); {
201+
case r.err != nil:
202+
return r.err
203+
case r.r != nil:
204+
return r.r.Encode(w, img)
205+
}
206+
return ErrTermGraphicsNotAvailable
207+
}
208+
209+
// jpegEncode encodes a image to w as a jpeg using [DefaultJPEGQuality].
210+
func jpegEncode(w io.Writer, img image.Image) error {
211+
return jpeg.Encode(w, img, &jpeg.Options{
212+
Quality: DefaultJPEGQuality,
213+
})
214+
}
215+
216+
// chunkEncode writes buf to w in chunks.
217+
func chunkEncode(w io.Writer, buf []byte, size int) error {
218+
if _, err := fmt.Fprintf(w, "\x1b_Ga=T,f=100,m=1;\x1b\\"); err != nil {
219+
return err
220+
}
221+
n := len(buf)
222+
for i, j, m := 0, min(size, n), 0; i < n; i, j = j, min(j+size, n) {
223+
if m = 0; j < n {
224+
m = 1
225+
}
226+
if _, err := fmt.Fprintf(w, "\x1b_Gm=%d;%s\x1b\\", m, buf[i:j]); err != nil {
227+
return err
228+
}
229+
}
230+
return nil
231+
}

0 commit comments

Comments
 (0)