- Format text by default (opt out via -raw).

- Treat each paragraph in the input as a segment of the test.
- Aggregate results from multiple runs and output them on exit if -csv
  is given.
This commit is contained in:
Aetnaeus
2020-12-24 17:55:09 -05:00
parent 54a63eaac8
commit f05e461fd1
3 changed files with 78 additions and 46 deletions

74
tt.go
View File

@ -7,12 +7,25 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
) )
var scr tcell.Screen
var csvMode bool
var rawMode bool
type result struct {
wpm int
cpm int
accuracy float64
}
var results []result
func readConfig() map[string]string { func readConfig() map[string]string {
cfg := map[string]string{} cfg := map[string]string{}
@ -31,8 +44,20 @@ func readConfig() map[string]string {
return cfg return cfg
} }
func exit() {
scr.Fini()
if csvMode {
for _, r := range results {
fmt.Printf("%d,%d,%.2f\n", r.wpm, r.cpm, r.accuracy)
}
}
os.Exit(0)
}
func showReport(scr tcell.Screen, cpm, wpm int, accuracy float64) { func showReport(scr tcell.Screen, cpm, wpm int, accuracy float64) {
report := fmt.Sprintf("CPM: %d\nWPM: %d\nAccuracy: %.2f%%\n", cpm, wpm, accuracy) report := fmt.Sprintf("WPM: %d\nCPM: %d\nAccuracy: %.2f%%\n", wpm, cpm, accuracy)
scr.Clear() scr.Clear()
drawCellsAtCenter(scr, stringToCells(report), -1) drawCellsAtCenter(scr, stringToCells(report), -1)
@ -43,18 +68,19 @@ func showReport(scr tcell.Screen, cpm, wpm int, accuracy float64) {
if key, ok := scr.PollEvent().(*tcell.EventKey); ok && key.Key() == tcell.KeyEscape { if key, ok := scr.PollEvent().(*tcell.EventKey); ok && key.Key() == tcell.KeyEscape {
return return
} else if ok && key.Key() == tcell.KeyCtrlC { } else if ok && key.Key() == tcell.KeyCtrlC {
scr.Fini() exit()
os.Exit(0)
} }
} }
} }
func main() { func main() {
var n int var n int
var csvMode bool var contentFn func() []string
var err error
flag.IntVar(&n, "n", 50, "The number of random words which constitute the test.") flag.IntVar(&n, "n", 50, "The number of random words which constitute the test.")
flag.BoolVar(&csvMode, "csv", false, "Print the test results to stdout in the form <cpm>,<wpm>,<accuracy>.") 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.Usage = func() { flag.Usage = func() {
fmt.Println(`Usage: tt [options] fmt.Println(`Usage: tt [options]
@ -77,22 +103,30 @@ Options:`)
} }
flag.Parse() flag.Parse()
contentFn := func() string {
return randomText(n)
}
if !isatty.IsTerminal(os.Stdin.Fd()) { if !isatty.IsTerminal(os.Stdin.Fd()) {
b, err := ioutil.ReadAll(os.Stdin) b, err := ioutil.ReadAll(os.Stdin)
if err != nil { if err != nil {
panic(err) panic(err)
} }
contentFn = func() string { if rawMode {
return string(b) contentFn = func() []string { return []string{string(b)} }
} else {
s := strings.Replace(string(b), "\r", "", -1)
s = regexp.MustCompile("\n\n+").ReplaceAllString(s, "\n\n")
content := strings.Split(strings.Trim(s, "\n"), "\n\n")
for i, _ := range content {
content[i] = strings.Replace(wordWrap(strings.Trim(content[i], " "), 80), "\n", " \n", -1)
}
contentFn = func() []string { return content }
} }
} else {
contentFn = func() []string { return []string{randomText(n)} }
} }
scr, err := tcell.NewScreen() scr, err = tcell.NewScreen()
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -101,8 +135,6 @@ Options:`)
panic(err) panic(err)
} }
defer scr.Fini()
fgcol := newTcellColor("#8C8C8C") fgcol := newTcellColor("#8C8C8C")
bgcol := newTcellColor("#282828") bgcol := newTcellColor("#282828")
@ -135,20 +167,18 @@ Options:`)
for { for {
scr.Clear() scr.Clear()
nerrs, ncorrect, t, completed := typer.Start([]string{contentFn()}) nerrs, ncorrect, t, exitKey := typer.Start(contentFn())
if completed {
switch exitKey {
case 0:
cpm := int(float64(ncorrect) / (float64(t) / 60E9)) cpm := int(float64(ncorrect) / (float64(t) / 60E9))
wpm := cpm / 5 wpm := cpm / 5
accuracy := float64(ncorrect) / float64(nerrs+ncorrect) * 100 accuracy := float64(ncorrect) / float64(nerrs+ncorrect) * 100
if csvMode { results = append(results, result{wpm, cpm, accuracy})
scr.Fini()
fmt.Printf("%d,%d,%.2f\n", cpm, wpm, accuracy)
return
}
showReport(scr, cpm, wpm, accuracy) showReport(scr, cpm, wpm, accuracy)
case tcell.KeyCtrlC:
exit()
} }
} }
} }

View File

@ -2,7 +2,6 @@ package main
import ( import (
"fmt" "fmt"
"os"
"time" "time"
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
@ -36,49 +35,50 @@ func NewTyper(scr tcell.Screen, fgcol, bgcol, hicol, hicol2, hicol3, errcol tcel
} }
} }
func (t *typer) highlight(text []cell, idx int, currentWordStyle, nextWorkStyle tcell.Style) { func (t *typer) highlight(text []cell, idx int, currentWordStyle, nextWordStyle tcell.Style) {
for ; idx < len(text) && text[idx].c != ' '; idx++ { for ; idx < len(text) && text[idx].c != ' ' && text[idx].c != '\n'; idx++ {
text[idx].style = currentWordStyle text[idx].style = currentWordStyle
} }
for ; idx < len(text) && text[idx].c == ' '; idx++ { for ; idx < len(text) && (text[idx].c == ' ' || text[idx].c == '\n'); idx++ {
} }
for ; idx < len(text) && text[idx].c != ' '; idx++ { for ; idx < len(text) && text[idx].c != ' ' && text[idx].c != '\n'; idx++ {
text[idx].style = nextWorkStyle text[idx].style = nextWordStyle
} }
} }
func (t *typer) Start(text []string) (nerrs, ncorrect int, tim time.Duration, complete bool) { func (t *typer) Start(text []string) (nerrs, ncorrect int, tim time.Duration, exitKey tcell.Key) {
var startTime time.Time var startTime time.Time
for i, p := range text { for i, p := range text {
var e, c int
var fn func() = nil var fn func() = nil
if i == 0 { if i == 0 {
fn = func() { fn = func() {
startTime = time.Now() startTime = time.Now()
} }
} }
e, c, completed := t.start(p, fn) e, c, exitKey = t.start(p, fn)
nerrs += e nerrs += e
ncorrect += c ncorrect += c
if !completed { if exitKey != 0 {
tim = time.Now().Sub(startTime) tim = time.Now().Sub(startTime)
complete = false
return return
} }
} }
tim = time.Now().Sub(startTime) tim = time.Now().Sub(startTime)
complete = true exitKey = 0
return return
} }
func (t *typer) start(s string, onStart func()) (int, int, bool) { func (t *typer) start(s string, onStart func()) (nerrs int, ncorrect int, exitKey tcell.Key) {
started := false started := false
text := stringToCells(s) text := stringToCells(s)
for i, _ := range text { for i, _ := range text {
@ -145,18 +145,19 @@ func (t *typer) start(s string, onStart func()) (int, int, bool) {
started = true started = true
} }
switch ev.Key() { switch key := ev.Key(); key {
case tcell.KeyCtrlC: case tcell.KeyEscape,
fmt.Printf("\033[2 q") tcell.KeyCtrlC:
t.Scr.Fini()
os.Exit(1) nerrs = -1
case tcell.KeyEscape: ncorrect = -1
return -1, -1, false exitKey = key
return
case tcell.KeyCtrlL: case tcell.KeyCtrlL:
t.Scr.Sync() t.Scr.Sync()
case tcell.KeyCtrlH: case tcell.KeyCtrlH:
deleteWord() deleteWord()
case tcell.KeyBackspace2: case tcell.KeyBackspace2:
if ev.Modifiers() == tcell.ModAlt { if ev.Modifiers() == tcell.ModAlt {
deleteWord() deleteWord()
@ -212,7 +213,7 @@ func (t *typer) start(s string, onStart func()) (int, int, bool) {
} }
if idx == len(text) { if idx == len(text) {
nerrs := 0 nerrs = 0
for _, c := range text { for _, c := range text {
if c.style == t.incorrectStyle || c.style == t.incorrectSpaceStyle { if c.style == t.incorrectStyle || c.style == t.incorrectSpaceStyle {
@ -220,10 +221,11 @@ func (t *typer) start(s string, onStart func()) (int, int, bool) {
} }
} }
ncorrect := len(text) - nerrs ncorrect = len(text) - nerrs
exitKey = 0
t.Scr.Clear() t.Scr.Clear()
return nerrs, ncorrect, true return
} }
} }
} }

View File

@ -263,7 +263,7 @@ func randomText(n int) string {
} }
} }
return wordWrap(r, 80) return strings.Replace(wordWrap(r, 80), "\n", " \n", -1)
} }
func stringToCells(s string) []cell { func stringToCells(s string) []cell {