Skip to content

Commit

Permalink
terminal -> writer/stringwriter
Browse files Browse the repository at this point in the history
* migrated spinner to depend on a stringwritter rather than a terminal for easier adoption
* migrated matrix to return io.writer/stringwriter instead of a proprietary interface
* updated demo app
  • Loading branch information
Shai Nagar committed May 27, 2021
1 parent 224f97f commit ef2ed6e
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 57 deletions.
14 changes: 5 additions & 9 deletions fake_terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ func (t *fakeTerm) StdErr() io.Writer {
}

func (t *fakeTerm) Print(e interface{}) {
t.writeString(fmt.Sprintf("%v", e))
t.WriteString(fmt.Sprintf("%v", e))
}

func (t *fakeTerm) Println(e interface{}) {
t.writeString(fmt.Sprintf("%v\r\n", e))
t.WriteString(fmt.Sprintf("%v\r\n", e))
}

func (t *fakeTerm) EraseLine() {
t.writeString(TermControlEraseLine)
t.WriteString(TermControlEraseLine)
}

func (t *fakeTerm) OverwriteLine(e interface{}) {
Expand All @@ -62,13 +62,9 @@ func (t *fakeTerm) Clear() {
t.Out.Reset()
}

func (t *fakeTerm) writeString(s string) (n int) {
func (t *fakeTerm) WriteString(s string) (n int, err error) {
t.outLock.Lock()
defer t.outLock.Unlock()

if n, err := t.Out.WriteString(s); err == nil {
return n
}

return 0
return t.Out.WriteString(s)
}
23 changes: 21 additions & 2 deletions internal/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"io"
"strings"
"time"

Expand All @@ -26,25 +27,43 @@ func main() {
}

func demo(t termite.Terminal) {

c := termite.NewCursor(t)
c.Hide()
defer c.Show()

demoMatrix(t)
demoSpinner(t)
demoCursor(t)
demoProgressBars(t)
demoConcurrentProgressBars(t)
}

func demoMatrix(t termite.Terminal) {
printTitle("Matrix", t)

m := termite.NewMatrix(t)
cancel := m.Start()

lines := []io.StringWriter{
m.NewLineStringWriter(), m.NewLineStringWriter(), m.NewLineStringWriter(), m.NewLineStringWriter(), m.NewLineStringWriter(),
}

for i := 0; i < 100; i++ {
time.Sleep(time.Millisecond * 10)
lines[i%len(lines)].WriteString(fmt.Sprintf("- Matrix Line -> version %d", i+1))
}

cancel()
t.Println("")
}

func printTitle(s string, t termite.Terminal) {
chars := len(s)
border := strings.Repeat("-", chars+2)
t.Println(border)
t.Println(fmt.Sprintf(" %s ", color.GreenString(strings.Title(s))))
t.Println(border)
t.Println("")

}

func demoSpinner(t termite.Terminal) {
Expand Down
76 changes: 57 additions & 19 deletions matrix.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package termite
import (
"context"
"fmt"
"io"
"sync"
"time"
)
Expand All @@ -11,7 +12,8 @@ import (
type Matrix interface {
Terminal() Terminal
RefreshInterval() time.Duration
NewLine() MatrixLine
NewLineStringWriter() io.StringWriter
NewLineWriter() io.Writer
Start() context.CancelFunc
}

Expand All @@ -27,7 +29,7 @@ type terminalMatrix struct {
mx *sync.RWMutex
}

type terminalLine struct {
type matrixLineWriter struct {
index int
matrix *terminalMatrix
}
Expand All @@ -53,50 +55,86 @@ func (m *terminalMatrix) RefreshInterval() time.Duration {
// Start starts the matrix update process.
// Returns a cancel handle to stop the matrix updates.
func (m *terminalMatrix) Start() context.CancelFunc {
c := NewCursor(m.terminal)
context, cancel := context.WithCancel(context.Background())

waitStart := &sync.WaitGroup{}
waitStart.Add(1)
var drainWaitGroup *sync.WaitGroup

go func() {
timer := time.NewTicker(m.refreshInterval)
drainWaitGroup = &sync.WaitGroup{}
drainWaitGroup.Add(1)
// now that we loaded the drain wait group, we can release the caller
waitStart.Done()

for {
select {
case <-context.Done():
timer.Stop()
m.updateTerminal(false)
drainWaitGroup.Done()
return

case <-timer.C:
if len(m.lines) == 0 {
continue
}

m.mx.Lock()
for _, line := range m.lines {
m.terminal.OverwriteLine(fmt.Sprintf("%s\r\n", line))
}
c.Up(len(m.lines))
m.mx.Unlock()
m.updateTerminal(true)
}
}
}()

return cancel
waitStart.Wait()

return func() {
cancel()
// Wait for the final update to complete
drainWaitGroup.Wait()
}
}

func (m *terminalMatrix) updateTerminal(resetCursorPosition bool) {
c := NewCursor(m.terminal)
m.mx.Lock()
defer m.mx.Unlock()

if len(m.lines) == 0 {
return
}

for _, line := range m.lines {
m.terminal.OverwriteLine(fmt.Sprintf("%s\r\n", line))
}

if resetCursorPosition {
c.Up(len(m.lines))
}
}

// NewRow creates a new matrix row
func (m *terminalMatrix) NewLine() MatrixLine {
// NewLineStringWriter returns a new string writter to interact with a single matrix line
func (m *terminalMatrix) NewLineStringWriter() io.StringWriter {
m.mx.Lock()
defer m.mx.Unlock()

index := len(m.lines)
m.lines = append(m.lines, "")
return &terminalLine{
return &matrixLineWriter{
index: index,
matrix: m,
}
}

func (l *terminalLine) WriteString(s string) {
// NewLineWriter returns a new writer interface to interact with a single matrix line.
func (m *terminalMatrix) NewLineWriter() io.Writer {
return m.NewLineStringWriter().(*matrixLineWriter)
}

func (l *matrixLineWriter) WriteString(s string) (n int, err error) {
return l.Write([]byte(s))
}

func (l *matrixLineWriter) Write(b []byte) (n int, err error) {
l.matrix.mx.Lock()
defer l.matrix.mx.Unlock()

l.matrix.lines[l.index] = s
l.matrix.lines[l.index] = string(b)
return len(b), nil
}
33 changes: 24 additions & 9 deletions matrix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ func TestMatrixWritesToTerminalOutput(t *testing.T) {
matrix, cancel := startMatrix()
defer cancel()

matrix.NewLine().WriteString(examples[0])
matrix.NewLine().WriteString(examples[1])
matrix.NewLine().WriteString(examples[2])
matrix.NewLineStringWriter().WriteString(examples[0])
matrix.NewLineStringWriter().WriteString(examples[1])
matrix.NewLineStringWriter().WriteString(examples[2])

assertEventualSequence(t, matrix, examples)
}
Expand All @@ -31,11 +31,11 @@ func TestMatrixUpdatesTerminalOutput(t *testing.T) {
matrix, cancel := startMatrix()
defer cancel()

matrix.NewLine().WriteString(examples[0])
line2 := matrix.NewLine()
matrix.NewLineStringWriter().WriteString(examples[0])
line2 := matrix.NewLineStringWriter()
line2.WriteString(examples[1])
examples[1] = generateRandomString()
matrix.NewLine().WriteString(examples[2])
matrix.NewLineStringWriter().WriteString(examples[2])
line2.WriteString(examples[1])

assertEventualSequence(t, matrix, examples)
Expand All @@ -47,13 +47,28 @@ func TestMatrixStructure(t *testing.T) {
matrix, cancel := startMatrix()
defer cancel()

matrix.NewLine().WriteString(examples[0])
matrix.NewLine().WriteString(examples[1])
matrix.NewLine().WriteString(examples[2])
matrix.NewLineStringWriter().WriteString(examples[0])
matrix.NewLineStringWriter().WriteString(examples[1])
matrix.NewLineStringWriter().WriteString(examples[2])

assert.Equal(t, examples, matrix.(*terminalMatrix).lines)
}

func TestWriterLineInterface(t *testing.T) {
example := generateRandomString()

matrix1, cancel1 := startMatrix()
defer cancel1()

matrix2, cancel2 := startMatrix()
defer cancel2()

matrix1.NewLineStringWriter().WriteString(example)
matrix2.NewLineWriter().Write([]byte(example))

assert.Equal(t, matrix1.(*terminalMatrix).lines, matrix2.(*terminalMatrix).lines)
}

func assertEventualSequence(t *testing.T, matrix Matrix, examples []string) {
contantsAllExamplesInOrderFn := func() bool {
return strings.Contains(
Expand Down
18 changes: 9 additions & 9 deletions spinner.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"errors"
"fmt"
"io"
"sync"
"time"
)
Expand All @@ -20,18 +21,17 @@ type Spinner interface {
}

type spinner struct {
terminal Terminal
cursor Cursor
writer io.StringWriter
interval time.Duration
mx *sync.RWMutex
active bool
stopC chan bool
}

// NewSpinner creates a new Spinner with the specified update interval
func NewSpinner(t Terminal, interval int32) Spinner {
func NewSpinner(writer io.StringWriter, interval int32) Spinner {
return &spinner{
terminal: t,
writer: writer,
interval: time.Duration(interval),
mx: &sync.RWMutex{},
active: false,
Expand All @@ -40,8 +40,8 @@ func NewSpinner(t Terminal, interval int32) Spinner {
}

// NewDefaultSpinner creates a new Spinner with a default update interval
func NewDefaultSpinner(t Terminal) Spinner {
return NewSpinner(t, 500)
func NewDefaultSpinner(writer io.StringWriter) Spinner {
return NewSpinner(writer, 500)
}

// Start starts the spinner in the background and returns a cancellation handle and an error in case the spinner is already running.
Expand Down Expand Up @@ -84,7 +84,7 @@ func (s *spinner) Start() (cancel context.CancelFunc, err error) {

case <-timer.C:
spinring = spinring.Next()
s.terminal.OverwriteLine(fmt.Sprintf("%s", spinring.Value))
s.writer.WriteString(fmt.Sprintf("%s%s", TermControlEraseLine, spinring.Value))
}
}
}()
Expand Down Expand Up @@ -118,8 +118,8 @@ func (s *spinner) Stop(message string) (err error) {
}

func (s *spinner) printExitMessage(message string) {
s.terminal.EraseLine()
s.terminal.Println(message)
s.writer.WriteString(TermControlEraseLine)
s.writer.WriteString(message)
}

func createSpinnerRing() *ring.Ring {
Expand Down
16 changes: 7 additions & 9 deletions terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const TermControlCRLF = "\r\n"
type Terminal interface {
StdOut() io.Writer
StdErr() io.Writer
WriteString(s string) (int, error)

Width() int
Height() int
Expand Down Expand Up @@ -87,35 +88,32 @@ func (t *term) StdErr() io.Writer {
}

func (t *term) Print(e interface{}) {
t.writeString(fmt.Sprintf("%v", e))
t.WriteString(fmt.Sprintf("%v", e))
}

func (t *term) Println(e interface{}) {
t.writeString(fmt.Sprintf("%v%s", e, TermControlCRLF))
t.WriteString(fmt.Sprintf("%v%s", e, TermControlCRLF))
}

func (t *term) EraseLine() {
t.writeString(TermControlEraseLine)
t.WriteString(TermControlEraseLine)
}

func (t *term) OverwriteLine(e interface{}) {
t.Print(fmt.Sprintf("%s%v", TermControlEraseLine, e))
}

func (t *term) Clear() {
t.writeString(TermControlClearScreen)
t.WriteString(TermControlClearScreen)
}

func (t *term) writeString(s string) (n int) {
func (t *term) WriteString(s string) (n int, err error) {
t.outLock.Lock()
defer t.outLock.Unlock()

if t.autoFlush {
defer t.Out.Flush()
}

if n, err := t.Out.WriteString(s); err == nil {
return n
}
return 0
return t.Out.WriteString(s)
}

0 comments on commit ef2ed6e

Please sign in to comment.