tt/typer.go
Aetnaeus 4eae6a11f4 Various improvements.
- Added -showwpm
 - Added -g
 - Added -noreport
 - Updated generated themes.
 - Improved automatic line wrapping.
 - Fixed timer bug.
 - Moved -o to -oneshot
 - The exit code now corresponds to the exit action
 - Updated documentation.
2020-12-28 01:04:29 -05:00

313 lines
6.3 KiB
Go

package main
import (
"fmt"
"os"
"strconv"
"time"
"github.com/gdamore/tcell"
)
const (
TyperComplete = iota
TyperSigInt
TyperEscape
TyperResize
)
type typer struct {
Scr tcell.Screen
OnStart func()
SkipWord bool
ShowWpm bool
tty *os.File
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)
tty, err := os.OpenFile("/dev/tty", os.O_WRONLY, 0)
if err != nil {
panic(err)
}
return &typer{
Scr: scr,
SkipWord: true,
tty: tty,
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, rc int) {
timeLeft := timeout
for i, p := range text {
startImmediately := true
var d time.Duration
var e, c int
if i == 0 {
startImmediately = false
}
e, c, rc, d = t.start(p, timeLeft, startImmediately)
nerrs += e
ncorrect += c
duration += d
if timeout != -1 {
timeLeft -= d
if timeLeft <= 0 {
return
}
}
if rc != TyperComplete {
return
}
}
return
}
func (t *typer) start(s string, timeLimit time.Duration, startImmediately bool) (nerrs int, ncorrect int, rc int, duration time.Duration) {
var startTime time.Time
text := stringToCells(s)
sw, sh := scr.Size()
nc, nr := calcStringDimensions(s)
x := (sw - nc) / 2
y := (sh - nr) / 2
for i, _ := range text {
text[i].style = t.backgroundStyle
}
t.tty.WriteString("\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 t.tty.WriteString("\033[2 q")
t.Scr.SetStyle(t.backgroundStyle)
idx := 0
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++
}
}
rc = TyperComplete
duration = time.Now().Sub(startTime)
}
redraw := func() {
if timeLimit != -1 && !startTime.IsZero() {
remaining := timeLimit - time.Now().Sub(startTime)
drawString(t.Scr, x+nc/2, y+nr+1, " ", -1, t.backgroundStyle)
drawString(t.Scr, x+nc/2, y+nr+1, strconv.Itoa(int(remaining/1E9)), -1, t.backgroundStyle)
}
if t.ShowWpm && !startTime.IsZero() {
calcStats()
if duration > 1E7 { //Avoid flashing large numbers on test start.
wpm := int((float64(ncorrect) / 5) / (float64(duration) / 60E9))
drawString(t.Scr, x+nc/2-4, y-2, fmt.Sprintf("WPM: %-10d\n", wpm), -1, t.backgroundStyle)
}
}
//Potentially inefficient, but seems to be good enough
drawCells(t.Scr, x, y, text, idx)
t.Scr.Show()
}
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(5E8))
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:
rc = TyperResize
return
case *tcell.EventKey:
if startTime.IsZero() {
startTime = time.Now()
}
switch key := ev.Key(); key {
case tcell.KeyCtrlC:
rc = TyperSigInt
return
case tcell.KeyEscape:
rc = TyperEscape
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()
}
}
}