tt/typer.go
2020-12-25 17:43:46 -05:00

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()
}
}
}