- 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:
74
tt.go
74
tt.go
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
48
typer.go
48
typer.go
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user