Skip to content

Commit

Permalink
ui: add ui package
Browse files Browse the repository at this point in the history
  • Loading branch information
akupila committed Apr 21, 2020
1 parent 42884c6 commit 024ccbe
Show file tree
Hide file tree
Showing 14 changed files with 1,671 additions and 0 deletions.
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ require (
github.com/golangci/golangci-lint v1.24.0
github.com/google/go-cmp v0.4.0
github.com/hashicorp/hcl/v2 v2.3.0
github.com/mattn/go-runewidth v0.0.9
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7
github.com/pkg/errors v0.9.1 // indirect
github.com/smartystreets/assertions v1.0.0 // indirect
github.com/spf13/cobra v0.0.6
github.com/zclconf/go-cty v1.2.0
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
golang.org/x/tools v0.0.0-20200326210457-5d86d385bf88
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaa
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
Expand Down Expand Up @@ -231,6 +233,7 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/securego/gosec v0.0.0-20200103095621-79fbf3af8d83 h1:AtnWoOvTioyDXFvu96MWEeE8qj4COSQnJogzLy/u41A=
github.com/securego/gosec v0.0.0-20200103095621-79fbf3af8d83/go.mod h1:vvbZ2Ae7AzSq3/kywjUDxSNq2SJ27RxCz2un0H3ePqE=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
Expand All @@ -244,6 +247,8 @@ github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
Expand Down Expand Up @@ -311,6 +316,7 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
Expand Down
14 changes: 14 additions & 0 deletions ui/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Package ui provides declarative console based user interface output.
//
// Operation
//
// The target renderer is expected to render the entire desired UI every frame.
// Every consecutive render diffs this string and only the necessary updates
// are flushed to the output.
//
// Terminal size
//
// Every render is passed a current frame, which includes the terminal size. In
// case the terminal is resized, a new render is triggered with the new size,
// based on the output of the previous render.
package ui
25 changes: 25 additions & 0 deletions ui/rows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ui

import "strings"

// Rows joins all non-empty rows with new line.
func Rows(rows ...string) string {
rows = excludeEmpty(rows...)
return strings.Join(rows, "\n")
}

// Cols joins all non-empty columns with a space.
func Cols(cols ...string) string {
cols = excludeEmpty(cols...)
return strings.Join(cols, " ")
}

func excludeEmpty(values ...string) []string {
out := make([]string, 0, len(values))
for _, v := range values {
if len(v) > 0 {
out = append(out, v)
}
}
return out
}
36 changes: 36 additions & 0 deletions ui/rows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package ui_test

import (
"fmt"

"github.com/func/func/ui"
)

func ExampleRows() {
rows := ui.Rows("foo", "bar", "baz")
fmt.Println(rows)
// Output:
// foo
// bar
// baz
}

func ExampleRows_skipEmpty() {
rows := ui.Rows("foo", "", "baz")
fmt.Println(rows)
// Output:
// foo
// baz
}

func ExampleCols() {
cols := ui.Cols("foo", "bar", "baz")
fmt.Println(cols)
// Output: foo bar baz
}

func ExampleCols_skipEmpty() {
cols := ui.Cols("foo", "", "baz")
fmt.Println(cols)
// Output: foo baz
}
75 changes: 75 additions & 0 deletions ui/stack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package ui

import (
"bytes"
"sync"
)

// A Stack maintains a list of child nodes.
//
// All methods are safe for concurrent access.
type Stack struct {
mu sync.Mutex
nodes []Renderer
buf bytes.Buffer
}

// Render renders all children in the stack separated by new lines.
//
// Calling Render() on a nil stack returns an empty string.
func (s *Stack) Render(f Frame) string {
if s == nil {
return ""
}
s.mu.Lock()
defer s.mu.Unlock()
s.buf.Reset()
for i, c := range s.nodes {
line := c.Render(f)
s.buf.WriteString(line)
if i < len(s.nodes)-1 {
s.buf.WriteByte('\n')
}
}
return s.buf.String()
}

// Len returns the number of children in the stack.
func (s *Stack) Len() int {
s.mu.Lock()
defer s.mu.Unlock()
return len(s.nodes)
}

// Push adds a new node to the end of the stack.
func (s *Stack) Push(node Renderer) {
s.mu.Lock()
defer s.mu.Unlock()
s.nodes = append(s.nodes, node)
}

// Insert inserts a node at the given index.
// Panics if the given index does not exist in the stack.
func (s *Stack) Insert(node Renderer, index int) {
s.mu.Lock()
defer s.mu.Unlock()
if index >= len(s.nodes) {
panic("Out of bounds")
}
s.nodes = append(s.nodes, nil)
copy(s.nodes[index+1:], s.nodes[index:])
s.nodes[index] = node
}

// Remove removes a node from the stack.
// No-op if the child does not exist.
func (s *Stack) Remove(node Renderer) {
s.mu.Lock()
defer s.mu.Unlock()
for i, n := range s.nodes {
if n == node {
s.nodes = append(s.nodes[:i], s.nodes[i+1:]...)
return
}
}
}
95 changes: 95 additions & 0 deletions ui/stack_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package ui

import (
"fmt"
"sync"
"testing"
)

func TestStack_nil(t *testing.T) {
var s *Stack
got := s.Render(Frame{}) // Does not panic
if got != "" {
t.Errorf("(*Stack)(nil).Render() returned %q, want \"\"", got)
}
}

func TestStack_Render(t *testing.T) {
s := &Stack{}

s.Push(renderFunc(func(f Frame) string {
return fmt.Sprintf("<%d>", f.Number)
}))

got := s.Render(Frame{Number: 123})
want := "<123>"
if got != want {
t.Errorf("Rendered output does not match; got %q, want %q", got, want)
}
}

func TestStack_io(t *testing.T) {
s := &Stack{}

head := testNode("HEAD")
s.Push(head)
tail := testNode("TAIL")
s.Push(tail)
for i := 0; i < 3; i++ {
s.Insert(testNode('A'+i), 1)
}
s.Remove(head)

got := s.Render(Frame{})
want := "C\nB\nA\nTAIL"
if got != want {
t.Errorf("Rendered output does not match; got %q, want %q", got, want)
}
}

func TestStack_concurrent(t *testing.T) {
s := &Stack{}

var wg sync.WaitGroup
for i := 0; i < 10000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
s.Push(testNode(""))
}()
}
for i := 0; i < 10000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
s.Insert(testNode(""), 0)
}()
}
wg.Wait()

if s.Len() != 20000 {
t.Errorf("Len() does not match; got %d, want %d", s.Len(), 20000)
}
}

func TestStack_Insert_OutOfBounds(t *testing.T) {
defer func() {
p := recover()
if p == nil {
t.Error("Did not panic")
}
}()

s := &Stack{}
s.Insert(testNode(""), 123)
}

type testNode string

func (n testNode) Render(f Frame) string { return string(n) }

type renderFunc func(f Frame) string

func (fn renderFunc) Render(f Frame) string {
return fn(f)
}
Loading

0 comments on commit 024ccbe

Please sign in to comment.