Skip to content

Commit

Permalink
Remove -plain, add preset templates, add color output and scales
Browse files Browse the repository at this point in the history
  • Loading branch information
sgreben committed Jan 27, 2018
1 parent 8bc5faa commit fe0d1f2
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 34 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION = 4.0.2
VERSION = 5.0.0

PACKAGES := $(shell go list -f {{.Dir}} ./...)
GOFILES := $(addsuffix /*.go,$(PACKAGES))
Expand Down
89 changes: 71 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- [JSON output](#json-output)
- [Time format](#time-format)
- [Template output](#template-output)
- [Color output](#color-output)
- [Stopwatch regex](#stopwatch-regex)
- [JSON input](#json-input)
- [Example](#example)
Expand All @@ -31,15 +32,20 @@ Usage of tj:
-timeformat string
either a go time format string or one of the predefined format names (https://golang.org/pkg/time/#pkg-constants)
-template string
go template (https://golang.org/pkg/text/template)
either a go template (https://golang.org/pkg/text/template) or one of the predefined template names
-start string
a regex pattern. if given, only lines matching it (re)start the stopwatch
-readjson
parse each stdin line as JSON
-jsontemplate string
go template, used to extract text from json input. implies -readjson
-plain
-template='{{.TimeString}} +{{.DeltaNanos}} {{.Text}}'
-scale string
either a sequence of hex colors or one of the predefined color scale names (colors go from fastto slow)
(default "BlueToRed")
-scale-fast duration
the lower bound for the color scale (default 100ms)
-scale-slow duration
the upper bound for the color scale (default 2s)
```

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

| Name | Format |
|------------|-------------------------------------|
| RubyDate | Mon Jan 02 15:04:05 -0700 2006 |
| RFC3339 | 2006-01-02T15:04:05Z07:00 |
| Stamp | Jan _2 15:04:05 |
| StampMicro | Jan _2 15:04:05.000000 |
| RFC1123Z | Mon, 02 Jan 2006 15:04:05 -0700 |
| Kitchen | 3:04PM |
| RFC1123 | Mon, 02 Jan 2006 15:04:05 MST |
| RFC3339Nano| 2006-01-02T15:04:05.999999999Z07:00 |
| RFC822 | 02 Jan 06 15:04 MST |
| RFC850 | Monday, 02-Jan-06 15:04:05 MST |
| RFC822Z | 02 Jan 06 15:04 -0700 |
| StampMilli | Jan _2 15:04:05.000 |
| StampNano | Jan _2 15:04:05.000000000 |
| ANSIC | Mon Jan _2 15:04:05 2006 |
| UnixDate | Mon Jan _2 15:04:05 MST 2006 |
| ANSIC | `Mon Jan _2 15:04:05 2006` |
| Kitchen | `3:04PM` |
| RFC1123 | `Mon, 02 Jan 2006 15:04:05 MST` |
| RFC1123Z | `Mon, 02 Jan 2006 15:04:05 -0700` |
| RFC3339 | `2006-01-02T15:04:05Z07:00` |
| RFC3339Nano| `2006-01-02T15:04:05.999999999Z07:00`
| RFC822 | `02 Jan 06 15:04 MST` |
| RFC822Z | `02 Jan 06 15:04 -0700` |
| RFC850 | `Monday, 02-Jan-06 15:04:05 MST` |
| RubyDate | `Mon Jan 02 15:04:05 -0700 2006` |
| Stamp | `Jan _2 15:04:05` |
| StampMicro | `Jan _2 15:04:05.000000` |
| StampMilli | `Jan _2 15:04:05.000` |
| StampNano | `Jan _2 15:04:05.000000000` |
| UnixDate | `Mon Jan _2 15:04:05 MST 2006` |

### Template output

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

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

Some templates are pre-defined and can be specified via `-template NAME`:

| Name | Template |
|------------|----------------------------------------------|
| Color | `{{color .}}█{{reset}} {{.Text}}` |
| ColorText | `{{color .}}{{.Text}}{{reset}}` |
| Delta | `{{.DeltaNanos}} {{.Text}}` |
| Time | `{{.TimeString}} {{.Text}}` |
| TimeDelta | `{{.TimeString}} +{{.DeltaNanos}} {{.Text}}` |

### Color output

To help identify durations at a glance, `tj` maps durations to a color scale. The pre-defined templates `Color` and `ColorText` demonstrate this:

```bash
$ (echo fast;
sleep 1;
echo slower;
sleep 1.5;
echo slow;
sleep 2;
echo slowest) | tj -template Color
```
![Color output](docs/images/colors.png)

The terminal foreground color can be set by using `{{color .}}` in the output template. The default terminal color can be restored using `{{reset}}`.

The color scale can be set using the parameters `-scale`, `-scale-fast`, and `-scale-slow`:

- The `-scale` parameter defines the colors used in the scale.
- 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.

There are several pre-defined color scales:

| Name | Scale |
|---------------------|----------------------- |
| BlackToPurple | `#000 -> #F700FF` |
| BlackToRed | `#000 -> #F00` |
| BlueToRed | `#00F -> #F00` |
| CyanToRed | `#0FF -> #F00` |
| GreenToRed | `#0F0 -> #F00` |
| WhiteToPurple | `#FFF -> #F700FF` |
| WhiteToRed | `#FFF -> #F00` |
| WhiteToBlueToRed | `#FFF -> #00F -> #F00` |

You can also provide your own color scale using the same syntax as the pre-defined ones.

### Stopwatch regex

Sometimes you need to measure the duration between certain *tokens* in the input.
Expand Down
92 changes: 77 additions & 15 deletions cmd/tj/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ import (
"fmt"
"os"
"regexp"
"sort"
"strings"
"text/template"
"time"

"github.com/sgreben/tj/pkg/color"
)

type line struct {
Expand All @@ -34,12 +38,14 @@ type line struct {
}

type configuration struct {
timeFormat string // -timeformat="..."
template string // -template="..."
plain bool // -plain
start string // -start="..."
readJSON bool // -readjson
jsonTemplate string // -jsontemplate="..."
timeFormat string // -timeformat="..."
template string // -template="..."
start string // -start="..."
readJSON bool // -readjson
jsonTemplate string // -jsontemplate="..."
colorScale string // -scale
fast time.Duration // -scale-fast
slow time.Duration // -scale-slow
version string
}

Expand All @@ -50,6 +56,7 @@ var (
printer printerFunc
start *regexp.Regexp
jsonTemplate *template.Template
scale color.Scale
)

var timeFormats = map[string]string{
Expand All @@ -70,6 +77,35 @@ var timeFormats = map[string]string{
"StampNano": time.StampNano,
}

var templates = map[string]string{
"Time": "{{.TimeString}} {{.Text}}",
"TimeDelta": "{{.TimeString}} +{{.DeltaNanos}} {{.Text}}",
"Delta": "{{.DeltaNanos}} {{.Text}}",
"ColorText": "{{color .}}{{.Text}}{{reset}}",
"Color": "{{color .}}█{{reset}} {{.Text}}",
}

var colorScales = map[string]string{
"GreenToRed": "#0F0 -> #F00",
"BlueToRed": "#00F -> #F00",
"CyanToRed": "#0FF -> #F00",
"WhiteToRed": "#FFF -> #F00",
"WhiteToPurple": "#FFF -> #F700FF",
"BlackToRed": "#000 -> #F00",
"BlackToPurple": "#000 -> #F700FF",
"WhiteToBlueToRed": "#FFF -> #00F -> #F00",
}

var templateFuncs = template.FuncMap{
"color": foregroundColor,
"reset": func() string { return color.Reset },
}

func foregroundColor(line *line) string {
c := float64(line.DeltaNanos-int64(config.fast)) / float64(config.slow-config.fast)
return color.Foreground(scale(c))
}

func jsonPrinter() printerFunc {
enc := json.NewEncoder(os.Stdout)
return func(line *line) error {
Expand All @@ -78,7 +114,7 @@ func jsonPrinter() printerFunc {
}

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

func timeFormatsHelp() string {
help := "either a go time format string or one of the predefined format names (https://golang.org/pkg/time/#pkg-constants)\n"
buf := bytes.NewBuffer([]byte(help))
help := []string{}
for name, format := range timeFormats {
fmt.Fprintln(buf, "\t", name, "-", format)
help = append(help, fmt.Sprint("\t", name, " - ", format))
}
return buf.String()
sort.Strings(help)
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")
}

func templatesHelp() string {
help := []string{}
for name, template := range templates {
help = append(help, fmt.Sprint("\t", name, " - ", template))
}
sort.Strings(help)
return "either a go template (https://golang.org/pkg/text/template) or one of the predefined template names\n" + strings.Join(help, "\n")
}

func colorScalesHelp() string {
help := []string{}
for name, scale := range colorScales {
help = append(help, fmt.Sprint("\t", name, " - ", scale))
}
sort.Strings(help)
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")
}

func init() {
flag.StringVar(&config.template, "template", "", "go template (https://golang.org/pkg/text/template)")
flag.StringVar(&config.template, "template", "", templatesHelp())
flag.StringVar(&config.timeFormat, "timeformat", "RFC3339", timeFormatsHelp())
flag.BoolVar(&config.plain, "plain", false, "-template='{{.TimeString}} +{{.DeltaNanos}} {{.Text}}'")
flag.StringVar(&config.start, "start", "", "a regex pattern. if given, only lines matching it (re)start the stopwatch")
flag.BoolVar(&config.readJSON, "readjson", false, "parse each stdin line as JSON")
flag.StringVar(&config.jsonTemplate, "jsontemplate", "", "go template, used to extract text from json input. implies -readjson")
flag.StringVar(&config.colorScale, "scale", "BlueToRed", colorScalesHelp())
flag.DurationVar(&config.fast, "scale-fast", 100*time.Millisecond, "the lower bound for the color scale")
flag.DurationVar(&config.slow, "scale-slow", 2*time.Second, "the upper bound for the color scale")
flag.Parse()
if knownFormat, ok := timeFormats[config.timeFormat]; ok {
config.timeFormat = knownFormat
}
if config.plain {
config.template = "{{.TimeString}} +{{.DeltaNanos}} {{.Text}}"
if knownTemplate, ok := templates[config.template]; ok {
config.template = knownTemplate
}
if knownScale, ok := colorScales[config.colorScale]; ok {
config.colorScale = knownScale
}
if config.colorScale != "" {
scale = color.NewScale(color.Parse(config.colorScale))
}
if config.template != "" {
printer = templatePrinter(config.template)
Expand Down
Binary file added docs/images/colors.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
112 changes: 112 additions & 0 deletions pkg/color/color.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package color

import (
"fmt"
"regexp"
"strconv"
"strings"
)

// RGB is an RGB color
type RGB struct{ R, G, B uint8 }

// Scale is a color scale
type Scale func(float64) (r, g, b uint8)

func index(r, g, b uint8) int {
ri := (int(r) * 5) / 0xFF
gi := (int(g) * 5) / 0xFF
bi := (int(b) * 5) / 0xFF
return 36*ri + 6*gi + bi + 16
}

func clamp(c float64) float64 {
if c < 0 {
c = 0
}
if c > 1 {
c = 1
}
return c
}

var notHexChars = regexp.MustCompile("[^0-9a-fA-F]")
var spaces = regexp.MustCompile("\\s+")

func parse3(s string, c *RGB) {
r, _ := strconv.ParseUint(s[0:1], 16, 8)
c.R = uint8((r << 4) | r)
g, _ := strconv.ParseUint(s[1:2], 16, 8)
c.G = uint8((g << 4) | g)
b, _ := strconv.ParseUint(s[2:3], 16, 8)
c.B = uint8((b << 4) | b)
}

func parse6(s string, c *RGB) {
r, _ := strconv.ParseUint(s[0:2], 16, 8)
c.R = uint8(r)
g, _ := strconv.ParseUint(s[2:4], 16, 8)
c.G = uint8(g)
b, _ := strconv.ParseUint(s[4:6], 16, 8)
c.B = uint8(b)
}

func Parse(scale string) []RGB {
hexOnly := notHexChars.ReplaceAllString(scale, " ")
singleSpaced := spaces.ReplaceAllString(hexOnly, " ")
trimmed := strings.TrimSpace(singleSpaced)
lowercase := strings.ToLower(trimmed)
parts := strings.Split(lowercase, " ")

colors := make([]RGB, len(parts))
for i, s := range parts {
switch len(s) {
case 3:
parse3(s, &colors[i])
case 6:
parse6(s, &colors[i])
}
}
return colors
}

func Interpolate2(c float64, r1, g1, b1, r2, g2, b2 uint8) (r, g, b uint8) {
c = clamp(c)
r = uint8(float64(r1)*(1-c) + float64(r2)*c)
g = uint8(float64(g1)*(1-c) + float64(g2)*c)
b = uint8(float64(b1)*(1-c) + float64(b2)*c)
return
}

func Interpolate(c float64, points []RGB) (r, g, b uint8) {
c = clamp(c)
x := float64(len(points)-1) * c
i := int(x)
left := points[i]
j := int(x + 1)
if j >= len(points) {
j = i
}
right := points[j]
c = x - float64(i)
return Interpolate2(c, left.R, left.G, left.B, right.R, right.G, right.B)
}

func NewScale(points []RGB) Scale {
return func(c float64) (r, g, b uint8) {
return Interpolate(c, points)
}
}

// Foreground returns the closest matching terminal foreground color escape sequence
func Foreground(r, g, b uint8) string {
return fmt.Sprintf("\033[38;5;%dm", index(r, g, b))
}

// Background returns the closest matching terminal background color escape sequence
func Background(r, g, b uint8) string {
return fmt.Sprintf("\033[48;5%dm", index(r, g, b))
}

// Reset is the color reset terminal escape sequence
const Reset = "\033[0;00m"

0 comments on commit fe0d1f2

Please sign in to comment.