Skip to content

Commit fe0d1f2

Browse files
committed
Remove -plain, add preset templates, add color output and scales
1 parent 8bc5faa commit fe0d1f2

File tree

5 files changed

+261
-34
lines changed

5 files changed

+261
-34
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
VERSION = 4.0.2
1+
VERSION = 5.0.0
22

33
PACKAGES := $(shell go list -f {{.Dir}} ./...)
44
GOFILES := $(addsuffix /*.go,$(PACKAGES))

README.md

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- [JSON output](#json-output)
88
- [Time format](#time-format)
99
- [Template output](#template-output)
10+
- [Color output](#color-output)
1011
- [Stopwatch regex](#stopwatch-regex)
1112
- [JSON input](#json-input)
1213
- [Example](#example)
@@ -31,15 +32,20 @@ Usage of tj:
3132
-timeformat string
3233
either a go time format string or one of the predefined format names (https://golang.org/pkg/time/#pkg-constants)
3334
-template string
34-
go template (https://golang.org/pkg/text/template)
35+
either a go template (https://golang.org/pkg/text/template) or one of the predefined template names
3536
-start string
3637
a regex pattern. if given, only lines matching it (re)start the stopwatch
3738
-readjson
3839
parse each stdin line as JSON
3940
-jsontemplate string
4041
go template, used to extract text from json input. implies -readjson
41-
-plain
42-
-template='{{.TimeString}} +{{.DeltaNanos}} {{.Text}}'
42+
-scale string
43+
either a sequence of hex colors or one of the predefined color scale names (colors go from fastto slow)
44+
(default "BlueToRed")
45+
-scale-fast duration
46+
the lower bound for the color scale (default 100ms)
47+
-scale-slow duration
48+
the upper bound for the color scale (default 2s)
4349
```
4450

4551
### JSON output
@@ -72,21 +78,21 @@ The [constant names from pkg/time](https://golang.org/pkg/time/#pkg-constants) a
7278

7379
| Name | Format |
7480
|------------|-------------------------------------|
75-
| RubyDate | Mon Jan 02 15:04:05 -0700 2006 |
76-
| RFC3339 | 2006-01-02T15:04:05Z07:00 |
77-
| Stamp | Jan _2 15:04:05 |
78-
| StampMicro | Jan _2 15:04:05.000000 |
79-
| RFC1123Z | Mon, 02 Jan 2006 15:04:05 -0700 |
80-
| Kitchen | 3:04PM |
81-
| RFC1123 | Mon, 02 Jan 2006 15:04:05 MST |
82-
| RFC3339Nano| 2006-01-02T15:04:05.999999999Z07:00 |
83-
| RFC822 | 02 Jan 06 15:04 MST |
84-
| RFC850 | Monday, 02-Jan-06 15:04:05 MST |
85-
| RFC822Z | 02 Jan 06 15:04 -0700 |
86-
| StampMilli | Jan _2 15:04:05.000 |
87-
| StampNano | Jan _2 15:04:05.000000000 |
88-
| ANSIC | Mon Jan _2 15:04:05 2006 |
89-
| UnixDate | Mon Jan _2 15:04:05 MST 2006 |
81+
| ANSIC | `Mon Jan _2 15:04:05 2006` |
82+
| Kitchen | `3:04PM` |
83+
| RFC1123 | `Mon, 02 Jan 2006 15:04:05 MST` |
84+
| RFC1123Z | `Mon, 02 Jan 2006 15:04:05 -0700` |
85+
| RFC3339 | `2006-01-02T15:04:05Z07:00` |
86+
| RFC3339Nano| `2006-01-02T15:04:05.999999999Z07:00`
87+
| RFC822 | `02 Jan 06 15:04 MST` |
88+
| RFC822Z | `02 Jan 06 15:04 -0700` |
89+
| RFC850 | `Monday, 02-Jan-06 15:04:05 MST` |
90+
| RubyDate | `Mon Jan 02 15:04:05 -0700 2006` |
91+
| Stamp | `Jan _2 15:04:05` |
92+
| StampMicro | `Jan _2 15:04:05.000000` |
93+
| StampMilli | `Jan _2 15:04:05.000` |
94+
| StampNano | `Jan _2 15:04:05.000000000` |
95+
| UnixDate | `Mon Jan _2 15:04:05 MST 2006` |
9096

9197
### Template output
9298

@@ -103,6 +109,53 @@ $ (echo Hello; echo World) | tj -template '{{ .I }} {{.TimeSecs}} {{.Text}}'
103109

104110
The fields available to the template are specified in the [`line` struct](cmd/tj/main.go#L15).
105111

112+
Some templates are pre-defined and can be specified via `-template NAME`:
113+
114+
| Name | Template |
115+
|------------|----------------------------------------------|
116+
| Color | `{{color .}}█{{reset}} {{.Text}}` |
117+
| ColorText | `{{color .}}{{.Text}}{{reset}}` |
118+
| Delta | `{{.DeltaNanos}} {{.Text}}` |
119+
| Time | `{{.TimeString}} {{.Text}}` |
120+
| TimeDelta | `{{.TimeString}} +{{.DeltaNanos}} {{.Text}}` |
121+
122+
### Color output
123+
124+
To help identify durations at a glance, `tj` maps durations to a color scale. The pre-defined templates `Color` and `ColorText` demonstrate this:
125+
126+
```bash
127+
$ (echo fast;
128+
sleep 1;
129+
echo slower;
130+
sleep 1.5;
131+
echo slow;
132+
sleep 2;
133+
echo slowest) | tj -template Color
134+
```
135+
![Color output](docs/images/colors.png)
136+
137+
The terminal foreground color can be set by using `{{color .}}` in the output template. The default terminal color can be restored using `{{reset}}`.
138+
139+
The color scale can be set using the parameters `-scale`, `-scale-fast`, and `-scale-slow`:
140+
141+
- The `-scale` parameter defines the colors used in the scale.
142+
- The `-scale-fast` and `-scale-slow` parameters define the boundaries of the scale: durations shorter than the value of `-scale-fast` are mapped to the leftmost color, durations longer than the value of `-scale-slow` are mapped to the rightmost color.
143+
144+
There are several pre-defined color scales:
145+
146+
| Name | Scale |
147+
|---------------------|----------------------- |
148+
| BlackToPurple | `#000 -> #F700FF` |
149+
| BlackToRed | `#000 -> #F00` |
150+
| BlueToRed | `#00F -> #F00` |
151+
| CyanToRed | `#0FF -> #F00` |
152+
| GreenToRed | `#0F0 -> #F00` |
153+
| WhiteToPurple | `#FFF -> #F700FF` |
154+
| WhiteToRed | `#FFF -> #F00` |
155+
| WhiteToBlueToRed | `#FFF -> #00F -> #F00` |
156+
157+
You can also provide your own color scale using the same syntax as the pre-defined ones.
158+
106159
### Stopwatch regex
107160

108161
Sometimes you need to measure the duration between certain *tokens* in the input.

cmd/tj/main.go

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ import (
88
"fmt"
99
"os"
1010
"regexp"
11+
"sort"
12+
"strings"
1113
"text/template"
1214
"time"
15+
16+
"github.com/sgreben/tj/pkg/color"
1317
)
1418

1519
type line struct {
@@ -34,12 +38,14 @@ type line struct {
3438
}
3539

3640
type configuration struct {
37-
timeFormat string // -timeformat="..."
38-
template string // -template="..."
39-
plain bool // -plain
40-
start string // -start="..."
41-
readJSON bool // -readjson
42-
jsonTemplate string // -jsontemplate="..."
41+
timeFormat string // -timeformat="..."
42+
template string // -template="..."
43+
start string // -start="..."
44+
readJSON bool // -readjson
45+
jsonTemplate string // -jsontemplate="..."
46+
colorScale string // -scale
47+
fast time.Duration // -scale-fast
48+
slow time.Duration // -scale-slow
4349
version string
4450
}
4551

@@ -50,6 +56,7 @@ var (
5056
printer printerFunc
5157
start *regexp.Regexp
5258
jsonTemplate *template.Template
59+
scale color.Scale
5360
)
5461

5562
var timeFormats = map[string]string{
@@ -70,6 +77,35 @@ var timeFormats = map[string]string{
7077
"StampNano": time.StampNano,
7178
}
7279

80+
var templates = map[string]string{
81+
"Time": "{{.TimeString}} {{.Text}}",
82+
"TimeDelta": "{{.TimeString}} +{{.DeltaNanos}} {{.Text}}",
83+
"Delta": "{{.DeltaNanos}} {{.Text}}",
84+
"ColorText": "{{color .}}{{.Text}}{{reset}}",
85+
"Color": "{{color .}}█{{reset}} {{.Text}}",
86+
}
87+
88+
var colorScales = map[string]string{
89+
"GreenToRed": "#0F0 -> #F00",
90+
"BlueToRed": "#00F -> #F00",
91+
"CyanToRed": "#0FF -> #F00",
92+
"WhiteToRed": "#FFF -> #F00",
93+
"WhiteToPurple": "#FFF -> #F700FF",
94+
"BlackToRed": "#000 -> #F00",
95+
"BlackToPurple": "#000 -> #F700FF",
96+
"WhiteToBlueToRed": "#FFF -> #00F -> #F00",
97+
}
98+
99+
var templateFuncs = template.FuncMap{
100+
"color": foregroundColor,
101+
"reset": func() string { return color.Reset },
102+
}
103+
104+
func foregroundColor(line *line) string {
105+
c := float64(line.DeltaNanos-int64(config.fast)) / float64(config.slow-config.fast)
106+
return color.Foreground(scale(c))
107+
}
108+
73109
func jsonPrinter() printerFunc {
74110
enc := json.NewEncoder(os.Stdout)
75111
return func(line *line) error {
@@ -78,7 +114,7 @@ func jsonPrinter() printerFunc {
78114
}
79115

80116
func templatePrinter(t string) printerFunc {
81-
template := template.Must(template.New("-template").Option("missingkey=zero").Parse(t))
117+
template := template.Must(template.New("-template").Funcs(templateFuncs).Option("missingkey=zero").Parse(t))
82118
newline := []byte("\n")
83119
return func(line *line) error {
84120
err := template.Execute(os.Stdout, line)
@@ -88,27 +124,53 @@ func templatePrinter(t string) printerFunc {
88124
}
89125

90126
func timeFormatsHelp() string {
91-
help := "either a go time format string or one of the predefined format names (https://golang.org/pkg/time/#pkg-constants)\n"
92-
buf := bytes.NewBuffer([]byte(help))
127+
help := []string{}
93128
for name, format := range timeFormats {
94-
fmt.Fprintln(buf, "\t", name, "-", format)
129+
help = append(help, fmt.Sprint("\t", name, " - ", format))
95130
}
96-
return buf.String()
131+
sort.Strings(help)
132+
return "either a go time format string or one of the predefined format names (https://golang.org/pkg/time/#pkg-constants)\n" + strings.Join(help, "\n")
133+
}
134+
135+
func templatesHelp() string {
136+
help := []string{}
137+
for name, template := range templates {
138+
help = append(help, fmt.Sprint("\t", name, " - ", template))
139+
}
140+
sort.Strings(help)
141+
return "either a go template (https://golang.org/pkg/text/template) or one of the predefined template names\n" + strings.Join(help, "\n")
142+
}
143+
144+
func colorScalesHelp() string {
145+
help := []string{}
146+
for name, scale := range colorScales {
147+
help = append(help, fmt.Sprint("\t", name, " - ", scale))
148+
}
149+
sort.Strings(help)
150+
return "either a sequence of hex colors or one of the predefined color scale names (colors go from fast to slow)\n" + strings.Join(help, "\n")
97151
}
98152

99153
func init() {
100-
flag.StringVar(&config.template, "template", "", "go template (https://golang.org/pkg/text/template)")
154+
flag.StringVar(&config.template, "template", "", templatesHelp())
101155
flag.StringVar(&config.timeFormat, "timeformat", "RFC3339", timeFormatsHelp())
102-
flag.BoolVar(&config.plain, "plain", false, "-template='{{.TimeString}} +{{.DeltaNanos}} {{.Text}}'")
103156
flag.StringVar(&config.start, "start", "", "a regex pattern. if given, only lines matching it (re)start the stopwatch")
104157
flag.BoolVar(&config.readJSON, "readjson", false, "parse each stdin line as JSON")
105158
flag.StringVar(&config.jsonTemplate, "jsontemplate", "", "go template, used to extract text from json input. implies -readjson")
159+
flag.StringVar(&config.colorScale, "scale", "BlueToRed", colorScalesHelp())
160+
flag.DurationVar(&config.fast, "scale-fast", 100*time.Millisecond, "the lower bound for the color scale")
161+
flag.DurationVar(&config.slow, "scale-slow", 2*time.Second, "the upper bound for the color scale")
106162
flag.Parse()
107163
if knownFormat, ok := timeFormats[config.timeFormat]; ok {
108164
config.timeFormat = knownFormat
109165
}
110-
if config.plain {
111-
config.template = "{{.TimeString}} +{{.DeltaNanos}} {{.Text}}"
166+
if knownTemplate, ok := templates[config.template]; ok {
167+
config.template = knownTemplate
168+
}
169+
if knownScale, ok := colorScales[config.colorScale]; ok {
170+
config.colorScale = knownScale
171+
}
172+
if config.colorScale != "" {
173+
scale = color.NewScale(color.Parse(config.colorScale))
112174
}
113175
if config.template != "" {
114176
printer = templatePrinter(config.template)

docs/images/colors.png

16.3 KB
Loading

pkg/color/color.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package color
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strconv"
7+
"strings"
8+
)
9+
10+
// RGB is an RGB color
11+
type RGB struct{ R, G, B uint8 }
12+
13+
// Scale is a color scale
14+
type Scale func(float64) (r, g, b uint8)
15+
16+
func index(r, g, b uint8) int {
17+
ri := (int(r) * 5) / 0xFF
18+
gi := (int(g) * 5) / 0xFF
19+
bi := (int(b) * 5) / 0xFF
20+
return 36*ri + 6*gi + bi + 16
21+
}
22+
23+
func clamp(c float64) float64 {
24+
if c < 0 {
25+
c = 0
26+
}
27+
if c > 1 {
28+
c = 1
29+
}
30+
return c
31+
}
32+
33+
var notHexChars = regexp.MustCompile("[^0-9a-fA-F]")
34+
var spaces = regexp.MustCompile("\\s+")
35+
36+
func parse3(s string, c *RGB) {
37+
r, _ := strconv.ParseUint(s[0:1], 16, 8)
38+
c.R = uint8((r << 4) | r)
39+
g, _ := strconv.ParseUint(s[1:2], 16, 8)
40+
c.G = uint8((g << 4) | g)
41+
b, _ := strconv.ParseUint(s[2:3], 16, 8)
42+
c.B = uint8((b << 4) | b)
43+
}
44+
45+
func parse6(s string, c *RGB) {
46+
r, _ := strconv.ParseUint(s[0:2], 16, 8)
47+
c.R = uint8(r)
48+
g, _ := strconv.ParseUint(s[2:4], 16, 8)
49+
c.G = uint8(g)
50+
b, _ := strconv.ParseUint(s[4:6], 16, 8)
51+
c.B = uint8(b)
52+
}
53+
54+
func Parse(scale string) []RGB {
55+
hexOnly := notHexChars.ReplaceAllString(scale, " ")
56+
singleSpaced := spaces.ReplaceAllString(hexOnly, " ")
57+
trimmed := strings.TrimSpace(singleSpaced)
58+
lowercase := strings.ToLower(trimmed)
59+
parts := strings.Split(lowercase, " ")
60+
61+
colors := make([]RGB, len(parts))
62+
for i, s := range parts {
63+
switch len(s) {
64+
case 3:
65+
parse3(s, &colors[i])
66+
case 6:
67+
parse6(s, &colors[i])
68+
}
69+
}
70+
return colors
71+
}
72+
73+
func Interpolate2(c float64, r1, g1, b1, r2, g2, b2 uint8) (r, g, b uint8) {
74+
c = clamp(c)
75+
r = uint8(float64(r1)*(1-c) + float64(r2)*c)
76+
g = uint8(float64(g1)*(1-c) + float64(g2)*c)
77+
b = uint8(float64(b1)*(1-c) + float64(b2)*c)
78+
return
79+
}
80+
81+
func Interpolate(c float64, points []RGB) (r, g, b uint8) {
82+
c = clamp(c)
83+
x := float64(len(points)-1) * c
84+
i := int(x)
85+
left := points[i]
86+
j := int(x + 1)
87+
if j >= len(points) {
88+
j = i
89+
}
90+
right := points[j]
91+
c = x - float64(i)
92+
return Interpolate2(c, left.R, left.G, left.B, right.R, right.G, right.B)
93+
}
94+
95+
func NewScale(points []RGB) Scale {
96+
return func(c float64) (r, g, b uint8) {
97+
return Interpolate(c, points)
98+
}
99+
}
100+
101+
// Foreground returns the closest matching terminal foreground color escape sequence
102+
func Foreground(r, g, b uint8) string {
103+
return fmt.Sprintf("\033[38;5;%dm", index(r, g, b))
104+
}
105+
106+
// Background returns the closest matching terminal background color escape sequence
107+
func Background(r, g, b uint8) string {
108+
return fmt.Sprintf("\033[48;5%dm", index(r, g, b))
109+
}
110+
111+
// Reset is the color reset terminal escape sequence
112+
const Reset = "\033[0;00m"

0 commit comments

Comments
 (0)