diff --git a/README.md b/README.md
index d8ec1d7a..8e1d0be3 100644
--- a/README.md
+++ b/README.md
@@ -6,82 +6,80 @@
# termdash
-This project implements a terminal based dashboard. The feature set is inspired
-by the [gizak/termui](http://github.com/gizak/termui) project, which in turn
-was inspired by a javascript based
-[yaronn/blessed-contrib](http://github.com/yaronn/blessed-contrib). Why the
-rewrite you ask?
+[](termdashdemo/termdashdemo.go)
-1. The above mentioned [gizak/termui](http://github.com/gizak/termui) is
- abandoned and isn't maintained anymore.
-1. The project doesn't follow the design goals outlined below.
+This project implements a cross-platform customizable terminal based dashboard.
+The feature set is inspired by the
+[gizak/termui](http://github.com/gizak/termui) project, which in turn was
+inspired by a javascript based
+[yaronn/blessed-contrib](http://github.com/yaronn/blessed-contrib).
-# Design goals
+This rewrite focuses on code readability, maintainability and testability, see
+the [design goals](doc/design_goals.md). It aims to achieve the following
+[requirements](doc/requirements.md). See the [high-level design](doc/hld.md)
+for more details.
-This effort is focused on good software design and maintainability. By good
-design I mean:
+# Current feature set
-1. Write readable, well documented code.
-1. Only beautiful, simple APIs, no exposed concurrency, channels, internals, etc.
-1. Follow [Effective Go](http://golang.org/doc/effective_go.html).
-1. Provide an infrastructure that allows development of individual dashboard
- components in separation.
-1. The infrastructure must enforce consistency in how the dashboard components
- are implemented.
-1. Focus on maintainability, the infrastructure and dashboard components must
- have good test coverage, the repository must have CI/CD enabled.
+- Full support for terminal window resizing throughout the infrastructure.
+- Customizable layout, widget placement, borders, colors, etc.
+- Focusable containers and widgets.
+- Processing of keyboard and mouse events.
+- Periodic and event driven screen redraw.
+- A library of widgets, see below.
+- UTF-8 for all text elements.
+- Drawing primitives (Go functions) for widget development with character and
+ sub-character resolution.
-On top of that - let's have fun, learn something and become better developers
-together.
+# Installation
-# Requirements
+To install this library, run the following:
-1. Native support of the UTF-8 encoding.
-1. Simple container management to position the widgets and set their size.
-1. Mouse and keyboard input.
-1. Cross-platform terminal based output.
-1. Unit testing framework for simple and readable tests of dashboard elements.
-1. Tooling to streamline addition of new widgets.
-1. Apache-2.0 licence for the project.
+```
+go get -u github.com/mum4k/termdash
-# High-Level design
+```
-See the [design document](doc/hld.md).
+# Usage
-# Contributing
+The usage of most of these elements is demonstrated in
+[termdashdemo.go](termdashdemo/termdashdemo.go). To execute the demo:
-If you are willing to contribute, improve the infrastructure or develop a
-widget, first of all Thank You! Your help is appreciated.
-Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines related
-to the Google's CLA, and code review requirements.
+```
+go run github.com/mum4k/termdash/termdashdemo/termdashdemo.go
+```
-As stated above the primary goal of this project is to develop readable, well
-designed code, the functionality and efficiency come second. This is achieved
-through detailed code reviews, design discussions and following of the [design
-guidelines](doc/design_guidelines.md). Please familiarize yourself with these
-before contributing.
+# Documentation
-## Contributing widgets
+Code documentation can be viewed in
+[godoc](https://godoc.org/github.com/mum4k/termdash).
-If you're developing a new widget, please see the [widget
-development](doc/widget_development.md) section.
+Project documentation is available in the [doc](doc/) directory.
## Implemented Widgets
### The Gauge
Displays the progress of an operation. Run the
-[gaugedemo](widgets/gauge/demo/gaugedemo.go).
+[gaugedemo](widgets/gauge/gaugedemo/gaugedemo.go).
+
+```go
+go run github.com/mum4k/termdash/widgets/gauge/gaugedemo/gaugedemo.go
+```
-[](widgets/gauge/demo/gaugedemo.go)
+[](widgets/gauge/gaugedemo/gaugedemo.go)
### The Text
Displays text content, supports trimming and scrolling of content. Run the
-[textdemo](widgets/text/demo/textdemo.go).
+[textdemo](widgets/text/textdemo/textdemo.go).
-[](widgets/gauge/demo/gaugedemo.go)
+```go
+go run github.com/mum4k/termdash/widgets/text/textdemo/textdemo.go
+```
+
+[](widgets/text/textdemo/textdemo.go)
### The SparkLine
@@ -89,6 +87,10 @@ Draws a graph showing a series of values as vertical bars. The bars can have
sub-cell height. Run the
[sparklinedemo](widgets/sparkline/sparklinedemo/sparklinedemo.go).
+```go
+go run github.com/mum4k/termdash/widgets/sparkline/sparklinedemo/sparklinedemo.go
+```
+
[](widgets/sparkline/sparklinedemo/sparklinedemo.go)
### The BarChart
@@ -96,15 +98,40 @@ sub-cell height. Run the
Displays multiple bars showing relative ratios of values. Run the
[barchartdemo](widgets/barchart/barchartdemo/barchartdemo.go).
+```go
+go run github.com/mum4k/termdash/widgets/barchart/barchartdemo/barchartdemo.go
+```
+
[](widgets/barchart/barchartdemo/barchartdemo.go)
### The LineChart
-Displays series of values as line charts. Run the
+Displays series of values on a line chart. Run the
[linechartdemo](widgets/linechart/linechartdemo/linechartdemo.go).
+```go
+go run github.com/mum4k/termdash/widgets/linechart/linechartdemo/linechartdemo.go
+```
+
[](widgets/linechart/linechartdemo/linechartdemo.go)
+# Contributing
+
+If you are willing to contribute, improve the infrastructure or develop a
+widget, first of all Thank You! Your help is appreciated.
+
+Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines related
+to the Google's CLA, and code review requirements.
+
+As stated above the primary goal of this project is to develop readable, well
+designed code, the functionality and efficiency come second. This is achieved
+through detailed code reviews, design discussions and following of the [design
+guidelines](doc/design_guidelines.md). Please familiarize yourself with these
+before contributing.
+
+If you're developing a new widget, please see the [widget
+development](doc/widget_development.md) section.
+
## Disclaimer
diff --git a/doc/design_goals.md b/doc/design_goals.md
new file mode 100644
index 00000000..14b343cf
--- /dev/null
+++ b/doc/design_goals.md
@@ -0,0 +1,14 @@
+# Design goals
+
+This effort is focused on good software design and maintainability. By good
+design I mean:
+
+1. Write readable, well documented code.
+1. Only beautiful, simple APIs, no exposed concurrency, channels, internals, etc.
+1. Follow [Effective Go](http://golang.org/doc/effective_go.html).
+1. Provide an infrastructure that allows development of individual dashboard
+ components in separation.
+1. The infrastructure must enforce consistency in how the dashboard components
+ are implemented.
+1. Focus on maintainability, the infrastructure and dashboard components must
+ have good test coverage, the repository must have CI/CD enabled.
diff --git a/doc/requirements.md b/doc/requirements.md
new file mode 100644
index 00000000..badc0173
--- /dev/null
+++ b/doc/requirements.md
@@ -0,0 +1,9 @@
+# Requirements
+
+1. Native support of the UTF-8 encoding.
+1. Simple container management to position the widgets and set their size.
+1. Mouse and keyboard input.
+1. Cross-platform terminal based output.
+1. Unit testing framework for simple and readable tests of dashboard elements.
+1. Tooling to streamline addition of new widgets.
+1. Apache-2.0 licence for the project.
diff --git a/images/termdashdemo.gif b/images/termdashdemo.gif
new file mode 100644
index 00000000..e81b90d6
Binary files /dev/null and b/images/termdashdemo.gif differ
diff --git a/termdashdemo/termdashdemo.go b/termdashdemo/termdashdemo.go
new file mode 100644
index 00000000..8d25d458
--- /dev/null
+++ b/termdashdemo/termdashdemo.go
@@ -0,0 +1,385 @@
+// Copyright 2019 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Binary termdashdemo demonstrates the functionality of termdash and its various widgets.
+// Exist when 'q' is pressed.
+package main
+
+import (
+ "context"
+ "fmt"
+ "math"
+ "math/rand"
+ "time"
+
+ "github.com/mum4k/termdash"
+ "github.com/mum4k/termdash/cell"
+ "github.com/mum4k/termdash/container"
+ "github.com/mum4k/termdash/draw"
+ "github.com/mum4k/termdash/terminal/termbox"
+ "github.com/mum4k/termdash/terminalapi"
+ "github.com/mum4k/termdash/widgets/barchart"
+ "github.com/mum4k/termdash/widgets/gauge"
+ "github.com/mum4k/termdash/widgets/linechart"
+ "github.com/mum4k/termdash/widgets/sparkline"
+ "github.com/mum4k/termdash/widgets/text"
+)
+
+// redrawInterval is how often termdash redraws the screen.
+const redrawInterval = 250 * time.Millisecond
+
+// layout prepares the screen layout by creating the container and placing
+// widgets.
+func layout(ctx context.Context, t terminalapi.Terminal) (*container.Container, error) {
+ spGreen, spRed := newSparkLines(ctx)
+ textAndSpark := []container.Option{
+ container.SplitHorizontal(
+ container.Top(
+ container.Border(draw.LineStyleLight),
+ container.BorderTitle("Termdash demo, press Q to quit"),
+ container.BorderColor(cell.ColorNumber(39)),
+ container.PlaceWidget(newTextTime(ctx)),
+ ),
+ container.Bottom(
+ container.SplitVertical(
+ container.Left(
+ container.Border(draw.LineStyleLight),
+ container.BorderTitle("A rolling text"),
+ container.PlaceWidget(newRollText(ctx)),
+ ),
+ container.Right(
+ container.Border(draw.LineStyleLight),
+ container.BorderTitle("A SparkLine group"),
+ container.SplitHorizontal(
+ container.Top(container.PlaceWidget(spGreen)),
+ container.Bottom(container.PlaceWidget(spRed)),
+ ),
+ ),
+ ),
+ ),
+ container.SplitPercent(30),
+ ),
+ }
+
+ gaugeAndHeartbeat := []container.Option{
+ container.SplitHorizontal(
+ container.Top(
+ container.Border(draw.LineStyleLight),
+ container.BorderTitle("A Gauge"),
+ container.BorderColor(cell.ColorNumber(39)),
+ container.PlaceWidget(newGauge(ctx)),
+ ),
+ container.Bottom(
+ container.Border(draw.LineStyleLight),
+ container.BorderTitle("A LineChart"),
+ container.PlaceWidget(newHeartbeat(ctx)),
+ ),
+ container.SplitPercent(20),
+ ),
+ }
+
+ leftSide := []container.Option{
+ container.SplitHorizontal(
+ container.Top(textAndSpark...),
+ container.Bottom(gaugeAndHeartbeat...),
+ container.SplitPercent(50),
+ ),
+ }
+
+ staticText, err := newStaticText()
+ if err != nil {
+ return nil, err
+ }
+
+ rightSide := []container.Option{
+ container.SplitHorizontal(
+ container.Top(
+ container.Border(draw.LineStyleLight),
+ container.BorderTitle("BarChart"),
+ container.PlaceWidget(newBarChart(ctx)),
+ container.BorderTitleAlignRight(),
+ ),
+ container.Bottom(
+ container.SplitHorizontal(
+ container.Top(
+ container.PlaceWidget(staticText),
+ ),
+ container.Bottom(
+ container.Border(draw.LineStyleLight),
+ container.BorderTitle("Multiple series"),
+ container.BorderTitleAlignRight(),
+ container.PlaceWidget(newSines(ctx)),
+ ),
+ container.SplitPercent(30),
+ ),
+ ),
+ container.SplitPercent(30),
+ ),
+ }
+
+ c, err := container.New(
+ t,
+ container.SplitVertical(
+ container.Left(leftSide...),
+ container.Right(rightSide...),
+ container.SplitPercent(70),
+ ),
+ )
+ if err != nil {
+ return nil, err
+ }
+ return c, nil
+}
+
+func main() {
+ t, err := termbox.New(termbox.ColorMode(terminalapi.ColorMode256))
+ if err != nil {
+ panic(err)
+ }
+ defer t.Close()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ c, err := layout(ctx, t)
+ if err != nil {
+ panic(err)
+ }
+
+ quitter := func(k *terminalapi.Keyboard) {
+ if k.Key == 'q' || k.Key == 'Q' {
+ cancel()
+ }
+ }
+ if err := termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter), termdash.RedrawInterval(redrawInterval)); err != nil {
+ panic(err)
+ }
+}
+
+// periodic executes the provided closure periodically every interval.
+// Exits when the context expires.
+func periodic(ctx context.Context, interval time.Duration, fn func() error) {
+ ticker := time.NewTicker(interval)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ticker.C:
+ if err := fn(); err != nil {
+ panic(err)
+ }
+ case <-ctx.Done():
+ return
+ }
+ }
+}
+
+// newTextTime creates a new Text widget that displays the current time.
+func newTextTime(ctx context.Context) *text.Text {
+ t := text.New()
+
+ go periodic(ctx, 1*time.Second, func() error {
+ t.Reset()
+ txt := time.Now().UTC().Format(time.UnixDate)
+ if err := t.Write(fmt.Sprintf("\n%s", txt), text.WriteCellOpts(cell.FgColor(cell.ColorMagenta))); err != nil {
+ return err
+ }
+ return nil
+ })
+ return t
+}
+
+// newRollText creates a new Text widget that displays rolling text.
+func newRollText(ctx context.Context) *text.Text {
+ t := text.New(text.RollContent())
+
+ i := 0
+ go periodic(ctx, 1*time.Second, func() error {
+ if err := t.Write(fmt.Sprintf("Writing line %d.\n", i), text.WriteCellOpts(cell.FgColor(cell.ColorNumber(142)))); err != nil {
+ return err
+ }
+ i++
+ return nil
+ })
+ return t
+}
+
+// newSparkLines creates two new sparklines displaying random values.
+func newSparkLines(ctx context.Context) (*sparkline.SparkLine, *sparkline.SparkLine) {
+ spGreen := sparkline.New(
+ sparkline.Label("Green SparkLine", cell.FgColor(cell.ColorBlue)),
+ sparkline.Color(cell.ColorGreen),
+ )
+
+ const max = 100
+ go periodic(ctx, 250*time.Millisecond, func() error {
+ v := int(rand.Int31n(max + 1))
+ if err := spGreen.Add([]int{v}); err != nil {
+ return err
+ }
+ return nil
+ })
+
+ spRed := sparkline.New(
+ sparkline.Label("Red SparkLine", cell.FgColor(cell.ColorBlue)),
+ sparkline.Color(cell.ColorRed),
+ )
+ go periodic(ctx, 500*time.Millisecond, func() error {
+ v := int(rand.Int31n(max + 1))
+ if err := spRed.Add([]int{v}); err != nil {
+ return err
+ }
+ return nil
+ })
+ return spGreen, spRed
+
+}
+
+// newGauge creates a demo Gauge widget.
+func newGauge(ctx context.Context) *gauge.Gauge {
+ g := gauge.New()
+
+ const start = 35
+ progress := start
+
+ go periodic(ctx, 2*time.Second, func() error {
+ if err := g.Percent(progress); err != nil {
+ return err
+ }
+ progress++
+ if progress > 100 {
+ progress = start
+ }
+ return nil
+ })
+ return g
+}
+
+// newHeartbeat returns a line chart that displays a heartbeat-like progression.
+func newHeartbeat(ctx context.Context) *linechart.LineChart {
+ var inputs []float64
+ for i := 0; i < 100; i++ {
+ v := math.Pow(math.Sin(float64(i)), 63) * math.Sin(float64(i)+1.5) * 8
+ inputs = append(inputs, v)
+ }
+
+ lc := linechart.New(
+ linechart.AxesCellOpts(cell.FgColor(cell.ColorRed)),
+ linechart.YLabelCellOpts(cell.FgColor(cell.ColorGreen)),
+ linechart.XLabelCellOpts(cell.FgColor(cell.ColorGreen)),
+ )
+ step := 0
+ go periodic(ctx, redrawInterval/3, func() error {
+ step = (step + 1) % len(inputs)
+ if err := lc.Series("heartbeat", rotate(inputs, step),
+ linechart.SeriesCellOpts(cell.FgColor(cell.ColorNumber(87))),
+ linechart.SeriesXLabels(map[int]string{
+ 0: "zero",
+ }),
+ ); err != nil {
+ return err
+ }
+ return nil
+ })
+ return lc
+}
+
+// newBarChart returns a BarcChart that displays random values on multiple bars.
+func newBarChart(ctx context.Context) *barchart.BarChart {
+ bc := barchart.New(
+ barchart.BarColors([]cell.Color{
+ cell.ColorNumber(33),
+ cell.ColorNumber(39),
+ cell.ColorNumber(45),
+ cell.ColorNumber(51),
+ cell.ColorNumber(81),
+ cell.ColorNumber(87),
+ }),
+ barchart.ValueColors([]cell.Color{
+ cell.ColorBlack,
+ cell.ColorBlack,
+ cell.ColorBlack,
+ cell.ColorBlack,
+ cell.ColorBlack,
+ cell.ColorBlack,
+ }),
+ barchart.ShowValues(),
+ )
+
+ const (
+ bars = 6
+ max = 100
+ )
+ values := make([]int, bars)
+ go periodic(ctx, 1*time.Second, func() error {
+ for i := range values {
+ values[i] = int(rand.Int31n(max + 1))
+ }
+
+ if err := bc.Values(values, max); err != nil {
+ return err
+ }
+ return nil
+ })
+ return bc
+}
+
+// newStaticText returns a new text widget with static content.
+func newStaticText() (*text.Text, error) {
+ t := text.New(
+ text.WrapAtRunes(),
+ )
+ if err := t.Write("\n\n\nA text widget without a border.", text.WriteCellOpts(cell.FgColor(cell.ColorNumber(201)))); err != nil {
+ return nil, err
+ }
+ return t, nil
+}
+
+// newSines returns a line chart that displays multiple sine series.
+func newSines(ctx context.Context) *linechart.LineChart {
+ var inputs []float64
+ for i := 0; i < 200; i++ {
+ v := math.Sin(float64(i) / 100 * math.Pi)
+ inputs = append(inputs, v)
+ }
+
+ lc := linechart.New(
+ linechart.AxesCellOpts(cell.FgColor(cell.ColorRed)),
+ linechart.YLabelCellOpts(cell.FgColor(cell.ColorGreen)),
+ linechart.XLabelCellOpts(cell.FgColor(cell.ColorGreen)),
+ )
+ step1 := 0
+ go periodic(ctx, redrawInterval/3, func() error {
+ step1 = (step1 + 1) % len(inputs)
+ if err := lc.Series("first", rotate(inputs, step1),
+ linechart.SeriesCellOpts(cell.FgColor(cell.ColorBlue)),
+ ); err != nil {
+ return err
+ }
+
+ step2 := (step1 + 100) % len(inputs)
+ if err := lc.Series("second", rotate(inputs, step2), linechart.SeriesCellOpts(cell.FgColor(cell.ColorWhite))); err != nil {
+ return err
+ }
+
+ return nil
+ })
+ return lc
+}
+
+// rotate returns a new slice with inputs rotated by step.
+// I.e. for a step of one:
+// inputs[0] -> inputs[len(inputs)-1]
+// inputs[1] -> inputs[0]
+// And so on.
+func rotate(inputs []float64, step int) []float64 {
+ return append(inputs[step:], inputs[:step]...)
+}
diff --git a/widgets/gauge/demo/gaugedemo.go b/widgets/gauge/gaugedemo/gaugedemo.go
similarity index 100%
rename from widgets/gauge/demo/gaugedemo.go
rename to widgets/gauge/gaugedemo/gaugedemo.go
diff --git a/widgets/linechart/linechartdemo/linechartdemo.go b/widgets/linechart/linechartdemo/linechartdemo.go
index 9f06a531..063b894f 100644
--- a/widgets/linechart/linechartdemo/linechartdemo.go
+++ b/widgets/linechart/linechartdemo/linechartdemo.go
@@ -80,7 +80,7 @@ func main() {
}
defer t.Close()
- const redrawInterval = 25 * time.Millisecond
+ const redrawInterval = 250 * time.Millisecond
ctx, cancel := context.WithCancel(context.Background())
lc := linechart.New(
linechart.AxesCellOpts(cell.FgColor(cell.ColorRed)),
diff --git a/widgets/text/demo/textdemo.go b/widgets/text/textdemo/textdemo.go
similarity index 100%
rename from widgets/text/demo/textdemo.go
rename to widgets/text/textdemo/textdemo.go