Skip to content

Commit

Permalink
support DRCS Sixel Graphics (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattn authored Sep 13, 2021
1 parent dbfe7c7 commit bc6bc67
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 70 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
language: go
go:
- '1.8'
- '1.9'
- '1.10'
- '1.11'
- '1.12'
- master
matrix:
allow_failures:
Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# jplot
[![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/rs/jplot/master/LICENSE) [![Build Status](https://travis-ci.org/rs/jplot.svg?branch=master)](https://travis-ci.org/rs/jplot)

Jplot tracks expvar-like (JSON) metrics and plot their evolution over time right into your iTerm2 terminal.
Jplot tracks expvar-like (JSON) metrics and plot their evolution over time right into your iTerm2 terminal (or DRCS Sixel Graphics).

![](doc/demo.gif)

Expand Down Expand Up @@ -33,7 +33,7 @@ From source:
go get -u github.com/rs/jplot
```

This tool does only work with [iTerm2](https://www.iterm2.com).
This tool does only work with [iTerm2](https://www.iterm2.com), or terminals support DRCS Sixel Graphics.

## Usage

Expand Down Expand Up @@ -125,3 +125,14 @@ echo 'GET http://localhost:8080' | \
```

![](doc/vegeta.gif)

### Supported Terminals

* [xterm](http://invisible-island.net/xterm/)
* [iTerm2](https://www.iterm2.com/) on OSX
* [mintty](https://mintty.github.io/) on UNIX OSs via SSH
* [mlterm](https://sourceforge.net/projects/mlterm/) on Linux and Windows
* [RLogin](http://nanno.dip.jp/softlib/man/rlogin/) on Windows
* [yaft](http://uobikiemukot.github.io/yaft/) on Linux console
* [yaft-android](https://github.com/uobikiemukot/yaft-android) on Android
* [Tanasinn](http://saitoha.github.io/tanasinn/) on Firefox/Thunderbird
13 changes: 6 additions & 7 deletions graph/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"github.com/rs/jplot/data"
chart "github.com/wcharczuk/go-chart"
"github.com/wcharczuk/go-chart/drawing"
"github.com/wcharczuk/go-chart/seq"
)

func init() {
Expand Down Expand Up @@ -46,17 +45,17 @@ func newChart(series []chart.Series, markers []chart.GridLine, width, height int
for i, s := range series {
if s, ok := s.(chart.ContinuousSeries); ok {
min, max = minMax(s.YValues, min, max)
s.XValues = seq.Range(0, float64(len(s.YValues)-1))
s.XValues = chart.LinearRange(0, float64(len(s.YValues)-1))
c := chart.GetAlternateColor(i + 4)
s.Style = chart.Style{
Show: true,
Hidden: false,
StrokeWidth: 2,
StrokeColor: c,
FillColor: c.WithAlpha(20),
FontSize: 9,
}
series[i] = s
last := chart.LastValueAnnotation(s, siValueFormater)
last := chart.LastValueAnnotationSeries(s, siValueFormater)
last.Style.FillColor = c
last.Style.FontColor = textColor(c)
last.Style.FontSize = 9
Expand All @@ -71,7 +70,7 @@ func newChart(series []chart.Series, markers []chart.GridLine, width, height int
Padding: chart.NewBox(5, 0, 0, 5),
},
YAxis: chart.YAxis{
Style: chart.StyleShow(),
Style: chart.Shown(),
ValueFormatter: siValueFormater,
},
Series: series,
Expand All @@ -88,13 +87,13 @@ func newChart(series []chart.Series, markers []chart.GridLine, width, height int
if len(markers) > 0 {
graph.Background.Padding.Bottom = 0 // compensate transparent tick space
graph.XAxis = chart.XAxis{
Style: chart.StyleShow(),
Style: chart.Shown(),
TickStyle: chart.Style{
StrokeColor: chart.ColorTransparent,
},
TickPosition: 10, // hide text with non-existing position
GridMajorStyle: chart.Style{
Show: true,
Hidden: false,
StrokeColor: chart.ColorAlternateGray.WithAlpha(100),
StrokeWidth: 2.0,
StrokeDashArray: []float64{2.0, 2.0},
Expand Down
2 changes: 1 addition & 1 deletion graph/legend.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func legend(c *chart.Chart, userDefaults ...chart.Style) chart.Renderable {
var labels []string
var lines []chart.Style
for _, s := range c.Series {
if s.GetStyle().IsZero() || s.GetStyle().Show {
if s.GetStyle().IsZero() || !s.GetStyle().Hidden {
if _, isAnnotationSeries := s.(chart.AnnotationSeries); !isAnnotationSeries {
labels = append(labels, s.GetName())
lines = append(lines, s.GetStyle())
Expand Down
31 changes: 14 additions & 17 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/monochromegane/terminal"
"github.com/rs/jplot/data"
"github.com/rs/jplot/graph"
"github.com/rs/jplot/osc"
"github.com/rs/jplot/term"
)

func main() {
Expand All @@ -38,8 +38,8 @@ func main() {
rows := flag.Int("rows", 0, "Limits the height of the graph output.")
flag.Parse()

if os.Getenv("TERM_PROGRAM") != "iTerm.app" {
fatal("iTerm2 required")
if !term.HasGraphicsSupport() {
fatal("iTerm2 or DRCS Sixel graphics required")
}
if os.Getenv("TERM") == "screen" {
fatal("screen and tmux not supported")
Expand Down Expand Up @@ -89,11 +89,11 @@ func main() {
i++
if i%120 == 0 {
// Clear scrollback to avoid iTerm from eating all the memory.
osc.ClearScrollback()
term.ClearScrollback()
}
osc.CursorSavePosition()
term.CursorSavePosition()
render(dash, *rows)
osc.CursorRestorePosition()
term.CursorRestorePosition()
case <-exit:
if i == 0 {
render(dash, *rows)
Expand All @@ -117,28 +117,28 @@ func fatal(a ...interface{}) {
}

func prepare(rows int) {
osc.HideCursor()
term.HideCursor()
if rows == 0 {
var err error
if rows, err = osc.Rows(); err != nil {
if rows, err = term.Rows(); err != nil {
fatal("Cannot get window size: ", err)
}
}
print(strings.Repeat("\n", rows))
osc.CursorMove(osc.Up, rows)
term.CursorMove(term.Up, rows)
}

func cleanup(rows int) {
osc.ShowCursor()
term.ShowCursor()
if rows == 0 {
rows, _ = osc.Rows()
rows, _ = term.Rows()
}
osc.CursorMove(osc.Down, rows)
term.CursorMove(term.Down, rows)
print("\n")
}

func render(dash graph.Dash, rows int) {
size, err := osc.Size()
size, err := term.Size()
if err != nil {
fatal("Cannot get window size: ", err)
}
Expand All @@ -149,10 +149,7 @@ func render(dash graph.Dash, rows int) {
rows = size.Row
}
// Use iTerm2 image display feature.
term := &osc.ImageWriter{
Width: width,
Height: height,
}
term := term.NewImageWriter(width, height)
defer term.Close()
if err := dash.Render(term, width, height); err != nil {
fatal(fmt.Sprintf("cannot render graph: %v", err.Error()))
Expand Down
69 changes: 31 additions & 38 deletions osc/iterm2.go → term/common.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package osc
package term

import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
Expand All @@ -18,17 +16,17 @@ var st = "\007"

var cellSizeOnce sync.Once
var cellWidth, cellHeight float64
var termWidth, termHeight int

func init() {
if os.Getenv("TERM") == "screen" {
ecsi = "\033Ptmux;\033" + ecsi
st += "\033\\"
}
func HasGraphicsSupport() bool {
return os.Getenv("TERM_PROGRAM") == "iTerm.app" || sixelEnabled
}

// ClearScrollback clears iTerm2 scrollback.
func ClearScrollback() {
print(ecsi + "1337;ClearScrollback" + st)
if !sixelEnabled {
print(ecsi + "1337;ClearScrollback" + st)
}
}

// TermSize contains sizing information of the terminal.
Expand All @@ -45,6 +43,13 @@ func initCellSize() {
return
}
defer terminal.Restore(1, s)
if sixelEnabled {
fmt.Fprint(os.Stdout, "\033[14t")
fileSetReadDeadline(os.Stdout, time.Now().Add(time.Second))
defer fileSetReadDeadline(os.Stdout, time.Time{})
fmt.Fscanf(os.Stdout, "\033[4;%d;%dt", &termHeight, &termWidth)
return
}
fmt.Fprint(os.Stdout, ecsi+"1337;ReportCellSize"+st)
fileSetReadDeadline(os.Stdout, time.Now().Add(time.Second))
defer fileSetReadDeadline(os.Stdout, time.Time{})
Expand All @@ -58,8 +63,13 @@ func Size() (size TermSize, err error) {
return
}
cellSizeOnce.Do(initCellSize)
if termWidth > 0 && termHeight > 0 {
size.Width = int(termWidth/(size.Col-1)) * (size.Col - 1)
size.Height = int(termHeight/(size.Row-1)) * (size.Row - 1)
return
}
if cellWidth+cellHeight == 0 {
err = errors.New("cannot get iTerm2 cell size")
err = errors.New("cannot get terminal cell size")
}
size.Width, size.Height = size.Col*int(cellWidth), size.Row*int(cellHeight)
return
Expand All @@ -71,32 +81,15 @@ func Rows() (rows int, err error) {
return
}

// ImageWriter is a writer that write into iTerm2 terminal the PNG data written
// to it.
type ImageWriter struct {
Name string
Width int
Height int

once sync.Once
b64enc io.WriteCloser
buf *bytes.Buffer
}

func (w *ImageWriter) init() {
w.buf = &bytes.Buffer{}
w.b64enc = base64.NewEncoder(base64.StdEncoding, w.buf)
}

// Write writes the PNG image data into the ImageWriter buffer.
func (w *ImageWriter) Write(p []byte) (n int, err error) {
w.once.Do(w.init)
return w.b64enc.Write(p)
}

// Close flushes the image to the terminal and close the writer.
func (w *ImageWriter) Close() error {
w.once.Do(w.init)
fmt.Printf("%s1337;File=preserveAspectRatio=1;width=%dpx;height=%dpx;inline=1:%s%s", ecsi, w.Width, w.Height, w.buf.Bytes(), st)
return w.b64enc.Close()
func NewImageWriter(width, height int) io.WriteCloser {
if sixelEnabled {
return &sixelWriter{
Width: width,
Height: height,
}
}
return &imageWriter{
Width: width,
Height: height,
}
}
2 changes: 1 addition & 1 deletion osc/go110.go → term/go110.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// +build go1.10

package osc
package term

import (
"os"
Expand Down
46 changes: 46 additions & 0 deletions term/iterm2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package term

import (
"bytes"
"encoding/base64"
"fmt"
"io"
"os"
"sync"
)

func init() {
if os.Getenv("TERM") == "screen" {
ecsi = "\033Ptmux;\033" + ecsi
st += "\033\\"
}
}

// imageWriter is a writer that write into iTerm2 terminal the PNG data written
type imageWriter struct {
Name string
Width int
Height int

once sync.Once
b64enc io.WriteCloser
buf *bytes.Buffer
}

func (w *imageWriter) init() {
w.buf = &bytes.Buffer{}
w.b64enc = base64.NewEncoder(base64.StdEncoding, w.buf)
}

// Write writes the PNG image data into the imageWriter buffer.
func (w *imageWriter) Write(p []byte) (n int, err error) {
w.once.Do(w.init)
return w.b64enc.Write(p)
}

// Close flushes the image to the terminal and close the writer.
func (w *imageWriter) Close() error {
w.once.Do(w.init)
fmt.Printf("%s1337;File=preserveAspectRatio=1;width=%dpx;height=%dpx;inline=1:%s%s", ecsi, w.Width, w.Height, w.buf.Bytes(), st)
return w.b64enc.Close()
}
2 changes: 1 addition & 1 deletion osc/not_go110.go → term/not_go110.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// +build !go1.10

package osc
package term

import (
"os"
Expand Down
Loading

0 comments on commit bc6bc67

Please sign in to comment.