diff --git a/tt.go b/tt.go index bb533df..ce84c49 100644 --- a/tt.go +++ b/tt.go @@ -9,6 +9,7 @@ import ( "path/filepath" "regexp" "strings" + "time" "github.com/gdamore/tcell" "github.com/mattn/go-isatty" @@ -57,10 +58,10 @@ func exit() { } func showReport(scr tcell.Screen, cpm, wpm int, accuracy float64) { - report := fmt.Sprintf("WPM: %d\nCPM: %d\nAccuracy: %.2f%%\n", wpm, cpm, accuracy) + report := fmt.Sprintf("WPM: %d\nCPM: %d\nAccuracy: %.2f%%", wpm, cpm, accuracy) scr.Clear() - drawCellsAtCenter(scr, stringToCells(report), -1) + drawStringAtCenter(scr, report, tcell.StyleDefault) scr.HideCursor() scr.Show() @@ -79,15 +80,17 @@ func main() { var oneShotMode bool var wrapSz int var noSkip bool + var timeout int var err error flag.IntVar(&n, "n", 50, "The number of random words which constitute the test.") - flag.IntVar(&wrapSz, "w", 80, "Wraps the input text at the given number of columns (ignored if -raw is present)") + flag.IntVar(&wrapSz, "w", 80, "Wraps the input text at the given number of columns (ignored if -raw is present).") + flag.IntVar(&timeout, "t", -1, "Terminate the test after the given number of seconds.") flag.BoolVar(&noSkip, "noskip", false, "Disable word skipping when space is pressed.") flag.BoolVar(&csvMode, "csv", false, "Print the test results to stdout in the form <wpm>,<cpm>,<accuracy>.") flag.BoolVar(&rawMode, "raw", false, "Don't reflow text or show one paragraph at a time.") - flag.BoolVar(&oneShotMode, "o", false, "Automatically exit after a single run.") + flag.BoolVar(&oneShotMode, "o", false, "Automatically exit after a single run (useful for scripts).") flag.Usage = func() { fmt.Println(`Usage: tt [options] @@ -177,10 +180,12 @@ Options:`) if noSkip { typer.SkipWord = false } + if timeout != -1 { + timeout *= 1E9 + } for { - scr.Clear() - nerrs, ncorrect, t, exitKey := typer.Start(contentFn()) + nerrs, ncorrect, t, exitKey := typer.Start(contentFn(), time.Duration(timeout)) switch exitKey { case 0: diff --git a/typer.go b/typer.go index 188adee..e1fbd09 100644 --- a/typer.go +++ b/typer.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "strconv" "time" "github.com/gdamore/tcell" @@ -51,39 +52,49 @@ func (t *typer) highlight(text []cell, idx int, currentWordStyle, nextWordStyle } } -func (t *typer) Start(text []string) (nerrs, ncorrect int, tim time.Duration, exitKey tcell.Key) { - var startTime time.Time +func (t *typer) Start(text []string, timeout time.Duration) (nerrs, ncorrect int, duration time.Duration, exitKey tcell.Key) { + timeLeft := timeout for i, p := range text { + startImmediately := true + var d time.Duration var e, c int - var fn func() = nil if i == 0 { - fn = func() { - startTime = time.Now() - } + startImmediately = false } - e, c, exitKey = t.start(p, fn) + e, c, exitKey, d = t.start(p, timeLeft, startImmediately) nerrs += e ncorrect += c + duration += d + + if timeout != -1 { + timeLeft -= d + if timeLeft <= 0 { + return + } + } if exitKey != 0 { - tim = time.Now().Sub(startTime) return } } - tim = time.Now().Sub(startTime) - exitKey = 0 - return } -func (t *typer) start(s string, onStart func()) (nerrs int, ncorrect int, exitKey tcell.Key) { - started := false +func (t *typer) start(s string, timeLimit time.Duration, startImmediately bool) (nerrs int, ncorrect int, exitKey tcell.Key, duration time.Duration) { + + var startTime time.Time text := stringToCells(s) + + nc, nr := calcStringDimensions(s) + sw, sh := scr.Size() + x := (sw - nc) / 2 + y := (sh - nr) / 2 + for i, _ := range text { text[i].style = t.backgroundStyle } @@ -98,11 +109,33 @@ func (t *typer) start(s string, onStart func()) (nerrs int, ncorrect int, exitKe idx := 0 redraw := func() { + if timeLimit != -1 && !startTime.IsZero() { + remaining := timeLimit - time.Now().Sub(startTime) + drawString(t.Scr, x+nc/2, y+nr+1, strconv.Itoa(int(remaining/1E9)), -1, t.backgroundStyle) + } + //Potentially inefficient, but seems to be good enough - drawCellsAtCenter(t.Scr, text, idx) + drawCells(t.Scr, x, y, text, idx) + t.Scr.Show() } + calcStats := func() { + nerrs = 0 + ncorrect = 0 + + for _, c := range text { + if c.style == t.incorrectStyle || c.style == t.incorrectSpaceStyle { + nerrs++ + } else if c.style == t.correctStyle { + ncorrect++ + } + } + + exitKey = 0 + duration = time.Now().Sub(startTime) + } + deleteWord := func() { t.highlight(text, idx, t.backgroundStyle, t.backgroundStyle) @@ -129,6 +162,30 @@ func (t *typer) start(s string, onStart func()) (nerrs int, ncorrect int, exitKe t.highlight(text, idx, t.currentWordStyle, t.nextWordStyle) } + tickerCloser := make(chan bool) + + //Inject nil events into the main event loop at regular invervals to force an update + ticker := func() { + for { + select { + case <-tickerCloser: + return + default: + } + + time.Sleep(time.Duration(1E8)) + t.Scr.PostEventWait(nil) + } + } + + go ticker() + defer close(tickerCloser) + + if startImmediately { + startTime = time.Now() + } + + t.Scr.Clear() for { t.highlight(text, idx, t.currentWordStyle, t.nextWordStyle) redraw() @@ -139,13 +196,14 @@ func (t *typer) start(s string, onStart func()) (nerrs int, ncorrect int, exitKe case *tcell.EventResize: t.Scr.Sync() t.Scr.Clear() - case *tcell.EventKey: - if !started { - if onStart != nil { - onStart() - } - started = true + nc, nr = calcStringDimensions(s) + sw, sh = scr.Size() + x = (sw - nc) / 2 + y = (sh - nr) / 2 + case *tcell.EventKey: + if startTime.IsZero() { + startTime = time.Now() } switch key := ev.Key(); key { @@ -218,21 +276,17 @@ func (t *typer) start(s string, onStart func()) (nerrs int, ncorrect int, exitKe } if idx == len(text) { - nerrs = 0 - - for _, c := range text { - if c.style == t.incorrectStyle || c.style == t.incorrectSpaceStyle { - nerrs++ - } - } - - ncorrect = len(text) - nerrs - exitKey = 0 - - t.Scr.Clear() + calcStats() return } } + default: //tick + if timeLimit != -1 && !startTime.IsZero() && timeLimit <= time.Now().Sub(startTime) { + calcStats() + return + } + + redraw() } } } diff --git a/util.go b/util.go index 84a6d56..08f61e8 100644 --- a/util.go +++ b/util.go @@ -280,6 +280,28 @@ func stringToCells(s string) []cell { return a[:len] } +func drawString(scr tcell.Screen, x, y int, s string, cursorIdx int, style tcell.Style) { + sx := x + + for i, c := range s { + if c == '\n' { + y++ + x = sx + } else { + scr.SetContent(x, y, c, nil, style) + if i == cursorIdx { + scr.ShowCursor(x, y) + } + + x++ + } + } + + if cursorIdx == len(s) { + scr.ShowCursor(x, y) + } +} + func drawCells(scr tcell.Screen, x, y int, s []cell, cursorIdx int) { sx := x @@ -302,16 +324,24 @@ func drawCells(scr tcell.Screen, x, y int, s []cell, cursorIdx int) { } } -func drawCellsAtCenter(scr tcell.Screen, s []cell, cursorIdx int) { - rows := 0 - cols := 0 +func drawStringAtCenter(scr tcell.Screen, s string, style tcell.Style) { + nc, nr := calcStringDimensions(s) + sw, sh := scr.Size() + + x := (sw - nc) / 2 + y := (sh - nr) / 2 + + drawString(scr, x, y, s, -1, style) +} + +func calcStringDimensions(s string) (nc, nr int) { c := 0 for _, x := range s { - if x.c == '\n' { - rows++ - if c > cols { - cols = c + if x == '\n' { + nr++ + if c > nc { + nc = c } c = 0 } else { @@ -319,16 +349,12 @@ func drawCellsAtCenter(scr tcell.Screen, s []cell, cursorIdx int) { } } - rows++ - if c > cols { - cols = c + nr++ + if c > nc { + nc = c } - w, h := scr.Size() - x := (w - cols) / 2 - y := (h - rows) / 2 - - drawCells(scr, x, y, s, cursorIdx) + return } func newTcellColor(s string) tcell.Color {