Skip to content

Commit 924a336

Browse files
committed
feat: add kitty/Warp support
1 parent 08d5477 commit 924a336

File tree

4 files changed

+122
-4
lines changed

4 files changed

+122
-4
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# jplot
22
[![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/rs/jplot/master/LICENSE)
33

4-
Jplot tracks expvar-like (JSON) metrics and plot their evolution over time right into your iTerm2 terminal (or DRCS Sixel Graphics).
4+
Jplot tracks expvar-like (JSON) metrics and plot their evolution over time right into your iTerm2, Kitty terminal, or terminals with DRCS Sixel Graphics support.
55

66
![](doc/demo.gif)
77

@@ -33,7 +33,7 @@ From source:
3333
go install github.com/rs/jplot@latest
3434
```
3535

36-
This tool does only work with [iTerm2](https://www.iterm2.com), or terminals support DRCS Sixel Graphics.
36+
This tool works with [iTerm2](https://www.iterm2.com), [Kitty](https://sw.kovidgoyal.net/kitty/), [Warp](https://www.warp.dev/), or terminals that support DRCS Sixel Graphics.
3737

3838
## Usage
3939

@@ -130,6 +130,8 @@ echo 'GET http://localhost:8080' | \
130130

131131
* [xterm](http://invisible-island.net/xterm/)
132132
* [iTerm2](https://www.iterm2.com/) on OSX
133+
* [Kitty](https://sw.kovidgoyal.net/kitty/) on Linux/macOS/Windows
134+
* [Warp](https://www.warp.dev/) on Linux/macOS/Windows
133135
* [mintty](https://mintty.github.io/) on UNIX OSs via SSH
134136
* [mlterm](https://sourceforge.net/projects/mlterm/) on Linux and Windows
135137
* [RLogin](http://nanno.dip.jp/softlib/man/rlogin/) on Windows

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func main() {
3939
flag.Parse()
4040

4141
if !term.HasGraphicsSupport() {
42-
fatal("iTerm2 or DRCS Sixel graphics required")
42+
fatal("iTerm2, Kitty, or DRCS Sixel graphics required")
4343
}
4444
if os.Getenv("TERM") == "screen" {
4545
fatal("screen and tmux not supported")

term/common.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ var cellWidth, cellHeight float64
1919
var termWidth, termHeight int
2020

2121
func HasGraphicsSupport() bool {
22-
return os.Getenv("TERM_PROGRAM") == "iTerm.app" || sixelEnabled
22+
return os.Getenv("TERM_PROGRAM") == "iTerm.app" ||
23+
kittyEnabled ||
24+
sixelEnabled
2325
}
2426

2527
// ClearScrollback clears iTerm2 scrollback.
@@ -50,6 +52,14 @@ func initCellSize() {
5052
fmt.Fscanf(os.Stdout, "\033[4;%d;%dt", &termHeight, &termWidth)
5153
return
5254
}
55+
if kittyEnabled {
56+
// For Kitty terminals, use the standard terminal size query
57+
fmt.Fprint(os.Stdout, "\033[14t")
58+
fileSetReadDeadline(os.Stdout, time.Now().Add(time.Second))
59+
defer fileSetReadDeadline(os.Stdout, time.Time{})
60+
fmt.Fscanf(os.Stdout, "\033[4;%d;%dt", &termHeight, &termWidth)
61+
return
62+
}
5363
fmt.Fprint(os.Stdout, ecsi+"1337;ReportCellSize"+st)
5464
fileSetReadDeadline(os.Stdout, time.Now().Add(time.Second))
5565
defer fileSetReadDeadline(os.Stdout, time.Time{})
@@ -88,6 +98,12 @@ func NewImageWriter(width, height int) io.WriteCloser {
8898
Height: height,
8999
}
90100
}
101+
if kittyEnabled {
102+
return &kittyWriter{
103+
Width: width,
104+
Height: height,
105+
}
106+
}
91107
return &imageWriter{
92108
Width: width,
93109
Height: height,

term/kitty.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package term
2+
3+
import (
4+
"bytes"
5+
"encoding/base64"
6+
"fmt"
7+
"os"
8+
"sync"
9+
"time"
10+
11+
"golang.org/x/crypto/ssh/terminal"
12+
)
13+
14+
var kittyEnabled = false
15+
var kittyCurrentID = 1 // Track current image ID globally
16+
var kittyFirstImage = true // Track if this is the first image
17+
18+
func init() {
19+
if os.Getenv("TERM_PROGRAM") != "iTerm.app" {
20+
kittyEnabled = checkKitty()
21+
}
22+
}
23+
24+
// checkKitty detects if the terminal supports Kitty graphics protocol
25+
func checkKitty() bool {
26+
s, err := terminal.MakeRaw(1)
27+
if err != nil {
28+
return false
29+
}
30+
defer terminal.Restore(1, s)
31+
32+
// Send Kitty graphics query to check if graphics are supported
33+
// Use a simple query that Kitty will respond to if it supports graphics
34+
_, err = os.Stdout.Write([]byte("\033_Gi=1,a=q,t=d,f=24\033\\"))
35+
if err != nil {
36+
return false
37+
}
38+
39+
fileSetReadDeadline(os.Stdout, time.Now().Add(500*time.Millisecond))
40+
defer fileSetReadDeadline(os.Stdout, time.Time{})
41+
42+
var b [200]byte
43+
n, err := os.Stdout.Read(b[:])
44+
if err != nil {
45+
return false
46+
}
47+
48+
// Look for Kitty's graphics response
49+
// Kitty will respond with \033_Gi=1;...\033\ if it supports graphics
50+
response := b[:n]
51+
return bytes.Contains(response, []byte("_G")) && bytes.Contains(response, []byte("i=1"))
52+
}
53+
54+
// kittyWriter is a writer that displays PNG images in Kitty terminal using the Kitty graphics protocol
55+
type kittyWriter struct {
56+
Name string
57+
Width int
58+
Height int
59+
60+
once sync.Once
61+
buf *bytes.Buffer
62+
}
63+
64+
func (w *kittyWriter) init() {
65+
w.buf = &bytes.Buffer{}
66+
}
67+
68+
// Write writes the PNG image data into the kittyWriter buffer.
69+
func (w *kittyWriter) Write(p []byte) (n int, err error) {
70+
w.once.Do(w.init)
71+
return w.buf.Write(p)
72+
}
73+
74+
// Close flushes the image to the terminal using Kitty's graphics protocol and closes the writer.
75+
func (w *kittyWriter) Close() error {
76+
w.once.Do(w.init)
77+
78+
// Encode the image data as base64
79+
b64data := base64.StdEncoding.EncodeToString(w.buf.Bytes())
80+
81+
// Calculate next image ID (alternate between 1 and 2)
82+
nextID := 3 - kittyCurrentID
83+
84+
// Step 1: Display the new image with absolute positioning
85+
// a=T (transmit and display), f=100 (PNG), t=d (direct), i=nextID (image ID),
86+
// X=0,Y=0 (absolute position), q=2 (suppress responses)
87+
fmt.Printf("\033_Ga=T,f=100,t=d,i=%d,X=0,Y=0,q=2;%s\033\\", nextID, b64data)
88+
89+
// Step 2: Delete the previously displayed image (skip deletion only on very first image)
90+
// Remove q=2 from delete to ensure it executes properly
91+
if !kittyFirstImage {
92+
fmt.Printf("\033_Ga=d,d=i,i=%d,q=2\033\\", kittyCurrentID)
93+
}
94+
kittyFirstImage = false // After first image, we always delete
95+
96+
// Update current ID for next time
97+
kittyCurrentID = nextID
98+
99+
return nil
100+
}

0 commit comments

Comments
 (0)