293 lines
5.9 KiB
Go
293 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gdamore/tcell"
|
|
)
|
|
|
|
type typer struct {
|
|
Scr tcell.Screen
|
|
OnStart func()
|
|
SkipWord bool
|
|
|
|
currentWordStyle tcell.Style
|
|
nextWordStyle tcell.Style
|
|
incorrectSpaceStyle tcell.Style
|
|
incorrectStyle tcell.Style
|
|
correctStyle tcell.Style
|
|
backgroundStyle tcell.Style
|
|
}
|
|
|
|
func NewTyper(scr tcell.Screen, fgcol, bgcol, hicol, hicol2, hicol3, errcol tcell.Color) *typer {
|
|
def := tcell.StyleDefault.
|
|
Foreground(fgcol).
|
|
Background(bgcol)
|
|
|
|
return &typer{
|
|
Scr: scr,
|
|
SkipWord: true,
|
|
|
|
backgroundStyle: def,
|
|
correctStyle: def.Foreground(hicol),
|
|
currentWordStyle: def.Foreground(hicol2),
|
|
nextWordStyle: def.Foreground(hicol3),
|
|
incorrectStyle: def.Foreground(errcol),
|
|
incorrectSpaceStyle: def.Background(errcol),
|
|
}
|
|
}
|
|
|
|
func (t *typer) highlight(text []cell, idx int, currentWordStyle, nextWordStyle tcell.Style) {
|
|
for ; idx < len(text) && text[idx].c != ' ' && text[idx].c != '\n'; idx++ {
|
|
text[idx].style = currentWordStyle
|
|
}
|
|
|
|
for ; idx < len(text) && (text[idx].c == ' ' || text[idx].c == '\n'); idx++ {
|
|
}
|
|
|
|
for ; idx < len(text) && text[idx].c != ' ' && text[idx].c != '\n'; idx++ {
|
|
text[idx].style = nextWordStyle
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
if i == 0 {
|
|
startImmediately = false
|
|
}
|
|
|
|
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 {
|
|
return
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
fmt.Printf("\033[5 q")
|
|
|
|
//Assumes original cursor shape was a block (the one true cursor shape), there doesn't appear to be a
|
|
//good way to save/restore the shape if the user has changed it from the otcs.
|
|
defer fmt.Printf("\033[2 q")
|
|
|
|
t.Scr.SetStyle(t.backgroundStyle)
|
|
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
|
|
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)
|
|
|
|
if idx == 0 {
|
|
return
|
|
}
|
|
|
|
idx--
|
|
|
|
for idx > 0 && (text[idx].c == ' ' || text[idx].c == '\n') {
|
|
text[idx].style = t.backgroundStyle
|
|
idx--
|
|
}
|
|
|
|
for idx > 0 && text[idx].c != ' ' && text[idx].c != '\n' {
|
|
text[idx].style = t.backgroundStyle
|
|
idx--
|
|
}
|
|
|
|
if text[idx].c == ' ' || text[idx].c == '\n' {
|
|
idx++
|
|
}
|
|
|
|
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()
|
|
|
|
ev := t.Scr.PollEvent()
|
|
|
|
switch ev := ev.(type) {
|
|
case *tcell.EventResize:
|
|
t.Scr.Sync()
|
|
t.Scr.Clear()
|
|
|
|
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 {
|
|
case tcell.KeyEscape,
|
|
tcell.KeyCtrlC:
|
|
|
|
nerrs = -1
|
|
ncorrect = -1
|
|
exitKey = key
|
|
|
|
return
|
|
case tcell.KeyCtrlL:
|
|
t.Scr.Sync()
|
|
case tcell.KeyCtrlH:
|
|
deleteWord()
|
|
case tcell.KeyBackspace2:
|
|
if ev.Modifiers() == tcell.ModAlt {
|
|
deleteWord()
|
|
} else {
|
|
t.highlight(text, idx, t.backgroundStyle, t.backgroundStyle)
|
|
|
|
if idx == 0 {
|
|
break
|
|
}
|
|
|
|
idx--
|
|
|
|
for idx > 0 && text[idx].c == '\n' {
|
|
idx--
|
|
}
|
|
|
|
text[idx].style = t.backgroundStyle
|
|
|
|
t.highlight(text, idx, t.currentWordStyle, t.nextWordStyle)
|
|
}
|
|
case tcell.KeyRune:
|
|
if idx < len(text) {
|
|
switch {
|
|
case ev.Rune() == text[idx].c:
|
|
text[idx].style = t.correctStyle
|
|
idx++
|
|
case ev.Rune() == ' ' && t.SkipWord:
|
|
if idx > 0 && text[idx-1].c == ' ' && text[idx].c != ' ' { //Do nothing on word boundaries.
|
|
break
|
|
}
|
|
|
|
for idx < len(text) && text[idx].c != ' ' && text[idx].c != '\n' {
|
|
text[idx].style = t.incorrectStyle
|
|
idx++
|
|
}
|
|
|
|
if idx < len(text) {
|
|
text[idx].style = t.incorrectSpaceStyle
|
|
idx++
|
|
}
|
|
default:
|
|
if text[idx].c == ' ' {
|
|
text[idx].style = t.incorrectSpaceStyle
|
|
} else {
|
|
text[idx].style = t.incorrectStyle
|
|
}
|
|
idx++
|
|
}
|
|
|
|
for idx < len(text) && text[idx].c == '\n' {
|
|
idx++
|
|
}
|
|
|
|
t.highlight(text, idx, t.currentWordStyle, t.nextWordStyle)
|
|
}
|
|
|
|
if idx == len(text) {
|
|
calcStats()
|
|
return
|
|
}
|
|
}
|
|
default: //tick
|
|
if timeLimit != -1 && !startTime.IsZero() && timeLimit <= time.Now().Sub(startTime) {
|
|
calcStats()
|
|
return
|
|
}
|
|
|
|
redraw()
|
|
}
|
|
}
|
|
}
|