Partial rewrite.

  Changes too numerous to list (see the man page).

  Highlights:

 - Added -quotes.
 - Added support for navigating between tests via right/left.
 - Now store the user's position within a file if one is specified.
 - Improved documentation.
This commit is contained in:
Aetnaeus
2021-01-15 22:42:35 -05:00
parent bae57c2cec
commit 3dc96b8f3d
19 changed files with 22528 additions and 382 deletions

View File

@ -1,3 +1,13 @@
# 0.4.0:
Too numerous to list (see the man page)
Highlights:
- Added -quotes.
- Added support for navigating between tests via right/left.
- Now store the user's position within a file if one is specified.
- Improved documentation.
# 0.3.0:
- Added support for custom word lists (`-words).
- `-theme` now accepts a path.

View File

@ -2,9 +2,11 @@ all:
go build -o bin/tt *.go
install:
install -m755 bin/tt /usr/local/bin
install -m755 tt.1.gz /usr/share/man/man1
assets:
python3 ./scripts/themegen.py
./scripts/pack themes/ words/ > packed.go
./scripts/pack themes/ words/ quotes/ > packed.go
pandoc -s -t man -o - man.md|gzip > tt.1.gz
rel:
GOOS=darwin GOARCH=amd64 go build -o bin/tt-osx *.go
GOOS=windows GOARCH=amd64 go build -o bin/tt.exe *.go

View File

@ -9,19 +9,17 @@ A terminal based typing test.
## Linux
```
sudo curl -L https://github.com/lemnos/tt/releases/download/v0.3.0/tt-linux -o /usr/local/bin/tt && sudo chmod +x /usr/local/bin/tt
sudo curl -L https://github.com/lemnos/tt/releases/download/v0.4.0/tt-linux -o /usr/local/bin/tt && sudo chmod +x /usr/local/bin/tt
sudo curl /usr/share/man/man1/tt.1.gz -o https://github.com/lemnos/tt/releases/download/v0.4.0/tt.1.gz -L
```
## OSX
```
sudo curl -L https://github.com/lemnos/tt/releases/download/v0.3.0/tt-osx -o /usr/local/bin/tt && sudo chmod +x /usr/local/bin/tt
sudo curl -L https://github.com/lemnos/tt/releases/download/v0.4.0/tt-osx -o /usr/local/bin/tt && sudo chmod +x /usr/local/bin/tt
sudo curl /usr/share/man/man1/tt.1.gz -o https://github.com/lemnos/tt/releases/download/v0.4.0/tt.1.gz -L
```
## Windows
There is a [windows binary](https://github.com/lemnos/tt/releases/download/0.0.2/tt.exe) but it is largely untested. Using WSL is strongly encouraged (or alternatively switching to a proper OS ;)).
## From source
```
@ -38,21 +36,33 @@ Best served on a terminal with truecolor and cursor shape support (e.g kitty, it
# Usage
By default 50 words from the top 1000 words in the English language are used to
constitute the test. Custom text can be supplied by piping arbitrary text to
the program. Each paragraph in the input is shown as a separate segment of the
text.
constitute the test. Custom text can be supplied by piping arbitrary text to the
program. Each paragraph in the input is shown as a separate segment of the text.
See `man tt` or `man.md` for a complete description and a comprehensive set of
options.
## Keys
- Pressing `escape` at any point restarts the test.
- `C-c` exits the test.
- `right` moves to the next test.
- `left` moves to the previous test.
## Examples
- `tt -n 10` produces a test consisting of 10 randomly drawn English words
- `tt -quotes en` Starts quote mode with the builtin quote list 'en'.
- `tt -n 10 -g 5` produces a test consisting of 50 randomly drawn words in 5 groups of 10 words each.
- `tt -t 10` starts a timed test consisting of 50 words
- `tt -theme gruvbox` Starts tt with the gruvbox theme
- `tt -t 10` starts a timed test lasting 10 seconds.
- `tt -theme gruvbox` Starts tt with the gruvbox theme.
`tt` is designed to be easily scriptable and integrate nicely with
other *nix tools. With a little shell scripting most features the user can
conceive of should be possible to implement. Below are some simple examples of
what can be achieved.
- `shuf -n 40 /usr/share/dict/words|tt` Produces a test consisting of 40 random words drawn from your system's dictionary.
- `curl http://api.quotable.io/random|jq '[.text=.content|.attribution=.author]'|tt -quotes -` Produces a test consisting of a random quote.
- `alias ttd='tt -csv >> ~/wpm.csv'` Creates an alias called ttd which keeps a log of progress in your home directory`.
The default behaviour is equivalent to `tt -n 50`.
@ -64,14 +74,3 @@ Custom themes and word lists can be defined in `~/.tt/themes` and `~/.tt/words`
and used in conjunction with the `-theme` and `-words` flags. A list of
preloaded themes and word lists can be found in `words/` and `themes/` and are
accessible by default using the respective flags.
## Recipes
`tt` is designed to be easily scriptable and integrate nicely with
other *nix tools. With a little shell scripting most features the user can
conceive of should be possible to implement. Below are some simple examples of
what can be achieved.
- `shuf -n 40 /usr/share/dict/words|tt` Produces a test consisting of 40 random words drawn from your system's dictionary.
- `curl http://api.quotable.io/random|jq -r .content|tt` Produces a test consisting of a random quote.
- `alias ttd='tt -csv >> ~/wpm.csv'` Creates an alias called ttd which keeps a log of your progress in your home directory`.

7
TODO Normal file
View File

@ -0,0 +1,7 @@
Keep track of errors.
Generate word sets based on errors
Consolidate documentation.
Add option to export errors.
Add -nobackspace
-adaptive/-tutor mode which learns from your mistake.
Add -blind

30
datatest.go Normal file
View File

@ -0,0 +1,30 @@
package main
func generateTestFromData(data []byte, raw bool, split bool) func() []segment {
if raw {
return func() []segment { return []segment{segment{string(data), ""}} }
} else if split {
paragraphs := getParagraphs(string(data))
i := 0
return func() []segment {
if i < len(paragraphs) {
p := paragraphs[i]
i++
return []segment{segment{p, ""}}
} else {
return nil
}
}
} else {
return func() []segment {
var segments []segment
for _, p := range getParagraphs(string(data)) {
segments = append(segments, segment{p, ""})
}
return segments
}
}
}

42
db.go Normal file
View File

@ -0,0 +1,42 @@
package main
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
)
var FILE_STATE_DB string
var MISTAKE_DB string
func init() {
if home, ok := os.LookupEnv("HOME"); !ok {
die("Could not resolve home directory.")
} else {
FILE_STATE_DB = filepath.Join(home, ".tt/", ".db")
MISTAKE_DB = filepath.Join(home, ".tt/", ".errors")
}
}
func readValue(path string, o interface{}) error {
b, err := ioutil.ReadFile(path)
if err != nil {
return err
}
return json.Unmarshal(b, o)
}
func writeValue(path string, o interface{}) {
b, err := json.Marshal(o)
if err != nil {
panic(err)
}
err = ioutil.WriteFile(path, b, 0600)
if err != nil {
panic(err)
}
}

44
doc Normal file
View File

@ -0,0 +1,44 @@
usage: tt [options] [file]
Modes
-words WORDFILE Specifies the file from which words are randomly
generated (default: 1000en).
-quotes QUOTEFILE Starts quote mode in which quotes are randomly generated
from the given file. The file should be JSON encoded and
have the following form:
[{"text": "foo", attribution: "bar"}]
Word Mode
-n GROUPSZ Sets the number of words which constitute a group.
-g NGROUPS Sets the number of groups which constitute a test.
File Mode
-start PARAGRAPH The offset of the starting paragraph, set this to 0 to
reset progress on a given file.
Aesthetics
-showwpm Display WPM whilst typing.
-theme THEMEFILE The theme to use.
-w The maximum line length in characters. This option is
ignored if -raw is present.
Test Parameters
-t SECONDS Terminate the test after the given number of seconds.
-noskip Disable word skipping when space is pressed.
Scripting
-oneshot Automatically exit after a single run.
-noreport Don't show a report at the end of a test.
-csv Print the test results to stdout in the form:
[wpm],[cpm],[accuracy],[timestamp].
-raw Don't reflow STDIN text or show one paragraph at a time.
Note that line breaks are determined exclusively by the
input.
-multi Treat each input paragraph as a self contained test.
Misc
-list TYPE Lists internal resources of the given type.
TYPE=[themes|quotes|words]
Version
-v Print the current version.

45
filetest.go Normal file
View File

@ -0,0 +1,45 @@
package main
import (
"io/ioutil"
"path/filepath"
)
func generateTestFromFile(path string, startParagraph int) func() []segment {
var paragraphs []string
var db map[string]int
var err error
if path, err = filepath.Abs(path); err != nil {
panic(err)
}
if err := readValue(FILE_STATE_DB, &db); err != nil {
db = map[string]int{}
}
if startParagraph != -1 {
db[path] = startParagraph
writeValue(FILE_STATE_DB, db)
}
idx := db[path] - 1
if b, err := ioutil.ReadFile(path); err != nil {
die("Failed to read %s.", path)
} else {
paragraphs = getParagraphs(string(b))
}
return func() []segment {
idx++
db[path] = idx
writeValue(FILE_STATE_DB, db)
if idx >= len(paragraphs) {
return nil
}
return []segment{segment{paragraphs[idx], ""}}
}
}

1
go.sum
View File

@ -8,7 +8,6 @@ github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHX
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

203
man.md Normal file
View File

@ -0,0 +1,203 @@
% tt(1)
# NAME
tt - A terminal based typing test
# SYNOPSIS
usage: tt \[OPTION\]... \[FILE\]
# DESCRIPTION
By default tt creates a test consisting of 50 randomly generated words from
the top 1000 words in the English language. If provided with a path, tt will
use the given file as input treating each paragraph as a separate segment of
the test. The program will automatically keep track of your position in the
file so subsequent invocations on the same path will place you at the most
recent paragraph (-start 0 can be used to reset your position).
Arbitrary text can also be piped directly into the program to create a custom
test. Each paragraph of the input is treated as a segment unless '-multi' is
supplied in which case each paragraph is treated as a separate test.
# OPTIONS
## Modes
-words *WORDFILE*
: Specifies the file from which words are randomly generated (default: 1000en).
-quotes *QUOTEFILE*
: Starts quote mode in which quotes are randomly generated from the given file. The file should be JSON encoded and have the following form:
[{"text": "foo", attribution: "bar"}]
## Word Mode
-n *GROUPSZ*
: Sets the number of words which constitute a group.
-g *NGROUPS*
: Sets the number of groups which constitute a test.
## File Mode
-start *PARAGRAPH*
: The offset of the starting paragraph, set this to 0 to reset progress on a given file.
## Aesthetics
-showwpm
: Display WPM whilst typing.
-theme *THEMEFILE*
: The theme to use.
-w
: The maximum line length in characters. This option is ignored if -raw is present.
## Test Parameters
-t *SECONDS*
: Terminate the test after the given number of seconds.
-noskip
: Disable word skipping when space is pressed.
-nohighlight
: Disable highlighting.
-highlight1
: Only highlight the current word.
-highlight2
: Only highlight the next word.
## Scripting
-oneshot
: Automatically exit after a single run.
-noreport
: Don't show a report at the end of a test.
-csv
: Print CSV formatted results.
Tests have the form:
```
test,[wpm],[cpm],[accuracy],[timestamp].
```
Mistakes have the form:
```
mistake,[word],[typed]
```
-json
: Print the test output in JSON.
-raw
: Don't reflow STDIN text or show one paragraph at a time. Note that line breaks
are determined exclusively by the input.
-multi
: Treat each input paragraph as a self contained test.
## Misc
**-list** *TYPE*\
Lists internal resources of the given type. TYPE=[themes|quotes|words].
**-v**\
Print the current version.
# EXAMPLES
Creates a series of tests each consisting of a random quote drawn from the
builtin quote file 'en'.
```
tt -quotes en
```
Creates a series of tests each consisting of 10 random words drawn from
words.txt
```
tt -words words.txt -n 10
```
Starts a sequence of tests in which each test consists of a paragraph from war
and peace starting with paragraph 1.
```
tt ~/war_and_peace.txt -start 1
```
Produces a test consisting of 40 random words draw from
the system dictionary (similar to 'tt -n 40').
```
shuf -n 40 /usr/share/dict/words|tt
```
Starts a test consisting of two randomly drawn quotes from api.quotable.io and
prints the output of each test to STDOUT in csv format.
```
curl https://api.quotable.io/quotes|\
jq '[.results[]|.text=.content|.attribution=.author][:2]'|\
tt -quotes - -norreport -csv
```
# PATHS
Some options like **-words** and **-theme** accept a path. If the given path does
not exist, the following directories are searched for a file with the given
name before falling back to internal resources:
~/.tt/words\
~/.tt/themes\
/etc/tt/words\
/etc/tt/themes
# KEYS
**esc: ** Restarts the test\
**C-c: ** Terminates tt\
**C-backspace: ** Deletes the previous word\
**right** Move to the next test.\
**left** Move to the previous test.
# AUTHOR
Aetnaeus (aetnaeus@protonmail.com)
# SEE ALSO
## Project Page
https://github.com/lemnos/tt
# LICENSE
MIT

File diff suppressed because one or more lines are too long

21686
quotes/en Normal file

File diff suppressed because it is too large Load Diff

24
quotetest.go Normal file
View File

@ -0,0 +1,24 @@
package main
import (
"encoding/json"
"math/rand"
)
func generateQuoteTest(name string) func() []segment {
var quotes []segment
if b := readResource("quotes", name); b == nil {
die("%s does not appear to be a valid quote file. See '-list quotes' for a list of builtin quotes.", name)
} else {
err := json.Unmarshal(b, &quotes)
if err != nil {
die("Improperly formatted quote file: %v", err)
}
}
return func() []segment {
idx := rand.Int() % len(quotes)
return quotes[idx : idx+1]
}
}

BIN
tt.1.gz Normal file

Binary file not shown.

321
tt.go
View File

@ -2,6 +2,7 @@ package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
@ -17,16 +18,20 @@ import (
var scr tcell.Screen
var csvMode bool
var jsonMode bool
type result struct {
wpm int
cpm int
accuracy float64
timestamp int64
Wpm int `json:"wpm"`
Cpm int `json:"cpm"`
Accuracy float64 `json:"accuracy"`
Timestamp int64 `json:"timestamp"`
Mistakes []mistake `json:"mistakes"`
}
func die(format string, args ...interface{}) {
if scr != nil {
scr.Fini()
}
fmt.Fprintf(os.Stderr, "ERROR: ")
fmt.Fprintf(os.Stderr, format, args...)
fmt.Fprintf(os.Stderr, "\n")
@ -54,17 +59,50 @@ func parseConfig(b []byte) map[string]string {
func exit(rc int) {
scr.Fini()
if jsonMode {
//Avoid null in serialized JSON.
for i := range results {
if results[i].Mistakes == nil {
results[i].Mistakes = []mistake{}
}
}
b, err := json.Marshal(results)
if err != nil {
panic(err)
}
os.Stdout.Write(b)
}
if csvMode {
for _, r := range results {
fmt.Printf("%d,%d,%.2f,%d\n", r.wpm, r.cpm, r.accuracy, r.timestamp)
fmt.Printf("test,%d,%d,%.2f,%d\n", r.Wpm, r.Cpm, r.Accuracy, r.Timestamp)
for _, m := range r.Mistakes {
fmt.Printf("mistake,%s,%s\n", m.Word, m.Typed)
}
}
}
os.Exit(rc)
}
func showReport(scr tcell.Screen, cpm, wpm int, accuracy float64) {
report := fmt.Sprintf("WPM: %d\nCPM: %d\nAccuracy: %.2f%%", wpm, cpm, accuracy)
func showReport(scr tcell.Screen, cpm, wpm int, accuracy float64, attribution string, mistakes []mistake) {
mistakeStr := ""
if attribution != "" {
attribution = "\n\nAttribution: " + attribution
}
if len(mistakes) > 0 {
mistakeStr = "\nMistakes: "
for i, m := range mistakes {
mistakeStr += m.Word
if i != len(mistakes)-1 {
mistakeStr += ", "
}
}
}
report := fmt.Sprintf("WPM: %d\nCPM: %d\nAccuracy: %.2f%%%s%s", wpm, cpm, accuracy, mistakeStr, attribution)
scr.Clear()
drawStringAtCenter(scr, report, tcell.StyleDefault)
@ -114,92 +152,123 @@ func createTyper(scr tcell.Screen, themeName string) *typer {
return NewTyper(scr, fgcol, bgcol, hicol, hicol2, hicol3, errcol)
}
var usage = `usage: tt [options] [file]
Modes
-words WORDFILE Specifies the file from which words are randomly
generated (default: 1000en).
-quotes QUOTEFILE Starts quote mode in which quotes are randomly generated
from the given file. The file should be JSON encoded and
have the following form:
[{"text": "foo", attribution: "bar"}]
Word Mode
-n GROUPSZ Sets the number of words which constitute a group.
-g NGROUPS Sets the number of groups which constitute a test.
File Mode
-start PARAGRAPH The offset of the starting paragraph, set this to 0 to
reset progress on a given file.
Aesthetics
-showwpm Display WPM whilst typing.
-theme THEMEFILE The theme to use.
-w The maximum line length in characters. This option is
ignored if -raw is present.
Test Parameters
-t SECONDS Terminate the test after the given number of seconds.
-noskip Disable word skipping when space is pressed.
-nobackspace Disable the backspace key.
-nohighlight Disable current and next word highlighting.
-highlight1 Only highlight the current word.
-highlight2 Only highlight the next word.
Scripting
-oneshot Automatically exit after a single run.
-noreport Don't show a report at the end of a test.
-csv Print the test results to stdout in the form:
[type],[wpm],[cpm],[accuracy],[timestamp].
-json Print the test output in JSON.
-raw Don't reflow STDIN text or show one paragraph at a time.
Note that line breaks are determined exclusively by the
input.
-multi Treat each input paragraph as a self contained test.
Misc
-list TYPE Lists internal resources of the given type.
TYPE=[themes|quotes|words]
Version
-v Print the current version.
`
func saveMistakes(mistakes []mistake) {
var db []mistake
if err := readValue(MISTAKE_DB, &db); err != nil {
db = nil
}
db = append(db, mistakes...)
writeValue(MISTAKE_DB, db)
}
func main() {
var n int
var ngroups int
var testFn func() []string
var g int
var rawMode bool
var oneShotMode bool
var noHighlightCurrent bool
var noHighlightNext bool
var noHighlight bool
var maxLineLen int
var noSkip bool
var noBackspace bool
var noReport bool
var timeout int
var startParagraph int
var listFlag string
var wordList string
var err error
var wordFile string
var quoteFile string
var themeName string
var showWpm bool
var multiMode bool
var versionFlag bool
flag.IntVar(&n, "n", 50, "The number of words which constitute a group.")
flag.IntVar(&ngroups, "g", 1, "The number of groups which constitute a generated test.")
var err error
var testFn func() []segment
flag.IntVar(&maxLineLen, "w", 80, "The maximum line length in characters. (ignored if -raw is present).")
flag.IntVar(&timeout, "t", -1, "Terminate the test after the given number of seconds.")
flag.IntVar(&n, "n", 50, "")
flag.IntVar(&g, "g", 1, "")
flag.IntVar(&startParagraph, "start", -1, "")
flag.BoolVar(&versionFlag, "v", false, "Print the current version.")
flag.IntVar(&maxLineLen, "w", 80, "")
flag.IntVar(&timeout, "t", -1, "")
flag.StringVar(&wordList, "words", "1000en", "The name of the word list used to generate random text.")
flag.BoolVar(&showWpm, "showwpm", false, "Display WPM whilst typing.")
flag.BoolVar(&noSkip, "noskip", false, "Disable word skipping when space is pressed.")
flag.BoolVar(&oneShotMode, "oneshot", false, "Automatically exit after a single run (useful for scripts).")
flag.BoolVar(&noReport, "noreport", false, "Don't show a report at the end of the test (useful in conjunction with -o).")
flag.BoolVar(&csvMode, "csv", false, "Print the test results to stdout in the form wpm,cpm,accuracy,time.")
flag.BoolVar(&rawMode, "raw", false, "Don't reflow text or show one paragraph at a time. (note that linebreaks are determined exclusively by the input)")
flag.BoolVar(&multiMode, "multi", false, "Treat each input paragraph as a self contained test.")
flag.StringVar(&themeName, "theme", "default", "The theme to use.")
flag.StringVar(&listFlag, "list", "", "Lists internal resources (e.g -list themes yields a list of builtin themes)")
flag.BoolVar(&versionFlag, "v", false, "")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, `Usage: tt [options]
flag.StringVar(&wordFile, "words", "", "")
flag.StringVar(&quoteFile, "quotes", "", "")
By default tt creates a test consisting of 50 randomly generated words from the
top 1000 words in the English language. Arbitrary text can also be piped
directly into the program to create a custom test. Each paragraph of the
input is treated as a segment of the test.
flag.BoolVar(&showWpm, "showwpm", false, "")
flag.BoolVar(&noSkip, "noskip", false, "")
flag.BoolVar(&noBackspace, "nobackspace", false, "")
flag.BoolVar(&oneShotMode, "oneshot", false, "")
flag.BoolVar(&noHighlight, "nohighlight", false, "")
flag.BoolVar(&noHighlightCurrent, "highlight2", false, "")
flag.BoolVar(&noHighlightNext, "highlight1", false, "")
flag.BoolVar(&noReport, "noreport", false, "")
flag.BoolVar(&csvMode, "csv", false, "")
flag.BoolVar(&jsonMode, "json", false, "")
flag.BoolVar(&rawMode, "raw", false, "")
flag.BoolVar(&multiMode, "multi", false, "")
flag.StringVar(&themeName, "theme", "default", "")
flag.StringVar(&listFlag, "list", "", "")
Examples:
# Equivalent to 'tt -n 40 -words /usr/share/dict/words'
shuf -n 40 /usr/share/dict/words|tt
# Starts a test consisting of a random quote.
curl https://api.quotable.io/random|jq -r .content|tt
# Starts single a test consisting of multiple random quotes.
curl https://api.quotable.io/quotes|jq -r .results[].content|sort -R|sed -e 's/$/\n/'|tt
# Starts multiple tests each consisting of a random quote
curl https://api.quotable.io/quotes|jq -r .results[].content|sort -R|sed -e 's/$/\n/'|tt -multi
Paths:
Some options like '-words' and '-theme' accept a path. If the given path does
not exist, the following directories are searched for a file with the given
name before falling back to the internal resource (if one exists):
-words (See -list words):
~/.tt/words/
/etc/tt/words/
-theme (See -list themes):
~/.tt/themes/
/etc/tt/themes/
Keybindings:
<esc> Restarts the test
<C-c> Terminates tt
<C-backspace> Deletes the previous word
Options:
`)
flag.PrintDefaults()
}
flag.Usage = func() { os.Stdout.Write([]byte(usage)) }
flag.Parse()
if listFlag != "" {
@ -233,55 +302,23 @@ Options:
"\n", " \n", -1)
}
if !isatty.IsTerminal(os.Stdin.Fd()) {
switch {
case wordFile != "":
testFn = generateWordTest(wordFile, n, g)
case quoteFile != "":
testFn = generateQuoteTest(quoteFile)
case !isatty.IsTerminal(os.Stdin.Fd()):
b, err := ioutil.ReadAll(os.Stdin)
if err != nil {
panic(err)
}
getParagraphs := func(s string) []string {
s = strings.Replace(s, "\r", "", -1)
s = regexp.MustCompile("\n\n+").ReplaceAllString(s, "\n\n")
return strings.Split(strings.Trim(s, "\n"), "\n\n")
}
if rawMode {
testFn = func() []string { return []string{string(b)} }
} else if multiMode {
paragraphs := getParagraphs(string(b))
i := 0
testFn = func() []string {
if i < len(paragraphs) {
p := paragraphs[i]
i++
return []string{p}
} else {
return nil
}
}
} else {
testFn = func() []string {
return getParagraphs(string(b))
}
}
} else {
testFn = func() []string {
var b []byte
if b = readResource("words", wordList); b == nil {
die("%s does not appear to be a valid word list. See '-list words' for a list of builtin word lists.", wordList)
}
words := regexp.MustCompile("\\s+").Split(string(b), -1)
r := make([]string, ngroups)
for i := 0; i < ngroups; i++ {
r[i] = randomText(n, words)
}
return r
}
testFn = generateTestFromData(b, rawMode, multiMode)
case len(flag.Args()) > 0:
path := flag.Args()[0]
testFn = generateTestFromFile(path, startParagraph)
default:
testFn = generateWordTest("1000en", n, g)
}
scr, err = tcell.NewScreen()
@ -301,48 +338,70 @@ Options:
}()
typer := createTyper(scr, themeName)
if noHighlightNext || noHighlight {
typer.currentWordStyle = typer.nextWordStyle
typer.nextWordStyle = typer.defaultStyle
}
if noHighlightCurrent || noHighlight {
typer.currentWordStyle = typer.defaultStyle
}
typer.SkipWord = !noSkip
typer.DisableBackspace = noBackspace
typer.ShowWpm = showWpm
if timeout != -1 {
timeout *= 1E9
}
var showNext = true
var paragraphs []string
var tests [][]segment
var idx = 0
for {
if showNext {
paragraphs = testFn()
if paragraphs == nil {
exit(0)
if idx >= len(tests) {
tests = append(tests, testFn())
}
if tests[idx] == nil {
exit(0)
}
if !rawMode {
for i, _ := range paragraphs {
paragraphs[i] = reflow(paragraphs[i])
for i, _ := range tests[idx] {
tests[idx][i].Text = reflow(tests[idx][i].Text)
}
}
nerrs, ncorrect, t, rc := typer.Start(paragraphs, time.Duration(timeout))
nerrs, ncorrect, t, rc, mistakes := typer.Start(tests[idx], time.Duration(timeout))
saveMistakes(mistakes)
showNext = false
switch rc {
case TyperNext:
idx++
case TyperPrevious:
if idx > 0 {
idx--
}
case TyperComplete:
cpm := int(float64(ncorrect) / (float64(t) / 60E9))
wpm := cpm / 5
accuracy := float64(ncorrect) / float64(nerrs+ncorrect) * 100
results = append(results, result{wpm, cpm, accuracy, time.Now().Unix()})
results = append(results, result{wpm, cpm, accuracy, time.Now().Unix(), mistakes})
if !noReport {
showReport(scr, cpm, wpm, accuracy)
attribution := ""
if len(tests[idx]) == 1 {
attribution = tests[idx][0].Attribution
}
showReport(scr, cpm, wpm, accuracy, attribution, mistakes)
}
if oneShotMode {
exit(0)
}
showNext = true
idx++
case TyperSigInt:
exit(1)

216
typer.go
View File

@ -16,14 +16,27 @@ const (
TyperComplete = iota
TyperSigInt
TyperEscape
TyperPrevious
TyperNext
TyperResize
)
type segment struct {
Text string `json:"text"`
Attribution string `json:"attribution"`
}
type mistake struct {
Word string `json:"word"`
Typed string `json:"typed"`
}
type typer struct {
Scr tcell.Screen
OnStart func()
SkipWord bool
ShowWpm bool
DisableBackspace bool
tty io.Writer
currentWordStyle tcell.Style
@ -31,7 +44,7 @@ type typer struct {
incorrectSpaceStyle tcell.Style
incorrectStyle tcell.Style
correctStyle tcell.Style
backgroundStyle tcell.Style
defaultStyle tcell.Style
}
func NewTyper(scr tcell.Screen, fgcol, bgcol, hicol, hicol2, hicol3, errcol tcell.Color) *typer {
@ -51,7 +64,7 @@ func NewTyper(scr tcell.Screen, fgcol, bgcol, hicol, hicol2, hicol3, errcol tcel
SkipWord: true,
tty: tty,
backgroundStyle: def,
defaultStyle: def,
correctStyle: def.Foreground(hicol),
currentWordStyle: def.Foreground(hicol2),
nextWordStyle: def.Foreground(hicol3),
@ -60,36 +73,25 @@ func NewTyper(scr tcell.Screen, fgcol, bgcol, hicol, hicol2, hicol3, errcol tcel
}
}
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) {
func (t *typer) Start(text []segment, timeout time.Duration) (nerrs, ncorrect int, duration time.Duration, rc int, mistakes []mistake) {
timeLeft := timeout
for i, p := range text {
for i, s := range text {
startImmediately := true
var d time.Duration
var e, c int
var m []mistake
if i == 0 {
startImmediately = false
}
e, c, rc, d = t.start(p, timeLeft, startImmediately)
e, c, rc, d, m = t.start(s.Text, timeLeft, startImmediately, s.Attribution)
nerrs += e
ncorrect += c
duration += d
mistakes = append(mistakes, m...)
if timeout != -1 {
timeLeft -= d
@ -106,90 +108,173 @@ func (t *typer) Start(text []string, timeout time.Duration) (nerrs, ncorrect int
return
}
func (t *typer) start(s string, timeLimit time.Duration, startImmediately bool) (nerrs int, ncorrect int, rc int, duration time.Duration) {
func extractMistypedWords(text []rune, typed []rune) (mistakes []mistake) {
var w []rune
var t []rune
f := false
for i := range text {
if text[i] == ' ' {
if f {
mistakes = append(mistakes, mistake{string(w), string(t)})
}
w = w[:0]
t = t[:0]
f = false
continue
}
if text[i] != typed[i] {
f = true
}
if text[i] == 0 {
w = append(w, '_')
} else {
w = append(w, text[i])
}
if typed[i] == 0 {
t = append(t, '_')
} else {
t = append(t, typed[i])
}
}
if f {
mistakes = append(mistakes, mistake{string(w), string(t)})
}
return
}
func (t *typer) start(s string, timeLimit time.Duration, startImmediately bool, attribution string) (nerrs int, ncorrect int, rc int, duration time.Duration, mistakes []mistake) {
var startTime time.Time
text := stringToCells(s)
text := []rune(s)
typed := make([]rune, len(text))
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.Write([]byte("\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.Write([]byte("\033[2 q"))
t.Scr.SetStyle(t.backgroundStyle)
t.Scr.SetStyle(t.defaultStyle)
idx := 0
calcStats := func() {
nerrs = 0
ncorrect = 0
for _, c := range text {
if c.style == t.incorrectStyle || c.style == t.incorrectSpaceStyle {
mistakes = extractMistypedWords(text[:idx], typed[:idx])
for i := 0; i < idx; i++ {
if text[i] != '\n' {
if text[i] != typed[i] {
nerrs++
} else if c.style == t.correctStyle {
} else {
ncorrect++
}
}
}
rc = TyperComplete
duration = time.Now().Sub(startTime)
}
redraw := func() {
cx := x
cy := y
inword := -1
for i := range text {
style := t.defaultStyle
if text[i] == '\n' {
cy++
cx = x
if inword != -1 {
inword++
}
continue
}
if i == idx {
scr.ShowCursor(cx, cy)
inword = 0
}
if i >= idx {
if text[i] == ' ' {
inword++
} else if inword == 0 {
style = t.currentWordStyle
} else if inword == 1 {
style = t.nextWordStyle
} else {
style = t.defaultStyle
}
} else if text[i] != typed[i] {
if text[i] == ' ' {
style = t.incorrectSpaceStyle
} else {
style = t.incorrectStyle
}
} else {
style = t.correctStyle
}
scr.SetContent(cx, cy, text[i], nil, style)
cx++
}
aw, ah := calcStringDimensions(attribution)
drawString(t.Scr, x+nc-aw, y+nr+1, attribution, -1, t.defaultStyle)
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), -1, t.backgroundStyle)
drawString(t.Scr, x+nc/2, y+nr+ah+1, " ", -1, t.defaultStyle)
drawString(t.Scr, x+nc/2, y+nr+ah+1, strconv.Itoa(int(remaining/1E9)+1), -1, t.defaultStyle)
}
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)
drawString(t.Scr, x+nc/2-4, y-2, fmt.Sprintf("WPM: %-10d\n", wpm), -1, t.defaultStyle)
}
}
//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
for idx > 0 && (text[idx] == ' ' || text[idx] == '\n') {
idx--
}
for idx > 0 && text[idx].c != ' ' && text[idx].c != '\n' {
text[idx].style = t.backgroundStyle
for idx > 0 && text[idx] != ' ' && text[idx] != '\n' {
idx--
}
if text[idx].c == ' ' || text[idx].c == '\n' {
if text[idx] == ' ' || text[idx] == '\n' {
typed[idx] = text[idx]
idx++
}
t.highlight(text, idx, t.currentWordStyle, t.nextWordStyle)
}
tickerCloser := make(chan bool)
@ -217,7 +302,6 @@ func (t *typer) start(s string, timeLimit time.Duration, startImmediately bool)
t.Scr.Clear()
for {
t.highlight(text, idx, t.currentWordStyle, t.nextWordStyle)
redraw()
ev := t.Scr.PollEvent()
@ -228,7 +312,9 @@ func (t *typer) start(s string, timeLimit time.Duration, startImmediately bool)
return
case *tcell.EventKey:
if runtime.GOOS != "windows" && ev.Key() == tcell.KeyBackspace { //Control+backspace on unix terms
if !t.DisableBackspace {
deleteWord()
}
continue
}
@ -247,60 +333,56 @@ func (t *typer) start(s string, timeLimit time.Duration, startImmediately bool)
return
case tcell.KeyCtrlL:
t.Scr.Sync()
case tcell.KeyRight:
rc = TyperNext
return
case tcell.KeyLeft:
rc = TyperPrevious
return
case tcell.KeyBackspace, tcell.KeyBackspace2:
if !t.DisableBackspace {
if ev.Modifiers() == tcell.ModAlt || ev.Modifiers() == tcell.ModCtrl {
deleteWord()
} else {
t.highlight(text, idx, t.backgroundStyle, t.backgroundStyle)
if idx == 0 {
break
}
idx--
for idx > 0 && text[idx].c == '\n' {
for idx > 0 && text[idx] == '\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.
if t.SkipWord && ev.Rune() == ' ' {
if idx > 0 && text[idx-1] == ' ' && text[idx] != ' ' { //Do nothing on word boundaries.
break
}
for idx < len(text) && text[idx].c != ' ' && text[idx].c != '\n' {
text[idx].style = t.incorrectStyle
for idx < len(text) && text[idx] != ' ' && text[idx] != '\n' {
typed[idx] = 0
idx++
}
if idx < len(text) {
text[idx].style = t.incorrectSpaceStyle
typed[idx] = text[idx]
idx++
}
default:
if text[idx].c == ' ' {
text[idx].style = t.incorrectSpaceStyle
} else {
text[idx].style = t.incorrectStyle
}
typed[idx] = ev.Rune()
idx++
}
for idx < len(text) && text[idx].c == '\n' {
for idx < len(text) && text[idx] == '\n' {
typed[idx] = text[idx]
idx++
}
t.highlight(text, idx, t.currentWordStyle, t.nextWordStyle)
}
if idx == len(text) {

41
util.go
View File

@ -6,6 +6,7 @@ import (
"math/rand"
"os"
"path/filepath"
"regexp"
"strings"
"time"
@ -37,6 +38,12 @@ func dbgPrintf(scr tcell.Screen, format string, args ...interface{}) {
drawString(scr, 0, 0, fmt.Sprintf(format, args...), -1, tcell.StyleDefault)
}
func getParagraphs(s string) []string {
s = strings.Replace(s, "\r", "", -1)
s = regexp.MustCompile("\n\n+").ReplaceAllString(s, "\n\n")
return strings.Split(strings.Trim(s, "\n"), "\n\n")
}
func wordWrapBytes(s []byte, n int) {
sp := 0
sz := 0
@ -122,28 +129,6 @@ func drawString(scr tcell.Screen, x, y int, s string, cursorIdx int, style tcell
}
}
func drawCells(scr tcell.Screen, x, y int, s []cell, cursorIdx int) {
sx := x
for i, c := range s {
if c.c == '\n' {
y++
x = sx
} else {
scr.SetContent(x, y, c.c, nil, c.style)
if i == cursorIdx {
scr.ShowCursor(x, y)
}
x++
}
}
if cursorIdx == len(s) {
scr.ShowCursor(x, y)
}
}
func drawStringAtCenter(scr tcell.Screen, s string, style tcell.Style) {
nc, nr := calcStringDimensions(s)
sw, sh := scr.Size()
@ -155,6 +140,10 @@ func drawStringAtCenter(scr tcell.Screen, s string, style tcell.Style) {
}
func calcStringDimensions(s string) (nc, nr int) {
if s == "" {
return 0, 0
}
c := 0
for _, x := range s {
@ -202,6 +191,14 @@ func newTcellColor(s string) (tcell.Color, error) {
}
func readResource(typ, name string) []byte {
if name == "-" {
b, err := ioutil.ReadAll(os.Stdin)
if err != nil {
panic(err)
}
return b
}
if b, err := ioutil.ReadFile(name); err == nil {
return b

106
words.go
View File

@ -1,106 +0,0 @@
package main
//Top 1000 words in English.
var defaultWordList = []string{
"as", "I", "his", "that", "he", "was", "for", "on", "are", "with",
"they", "be", "at", "one", "have", "this", "from", "by", "hot", "word",
"but", "what", "some", "is", "it", "you", "or", "had", "the", "of",
"to", "and", "a", "in", "we", "can", "out", "other", "were", "which",
"do", "their", "time", "if", "will", "how", "said", "an", "each", "tell",
"does", "set", "three", "want", "air", "well", "also", "play", "small", "end",
"put", "home", "read", "hand", "port", "large", "spell", "add", "even", "land",
"here", "must", "big", "high", "such", "follow", "act", "why", "ask", "men",
"change", "went", "light", "kind", "off", "need", "house", "picture", "try", "us",
"again", "animal", "point", "mother", "world", "near", "build", "self", "earth", "father",
"any", "new", "work", "part", "take", "get", "place", "made", "live", "where",
"after", "back", "little", "only", "round", "man", "year", "came", "show", "every",
"good", "me", "give", "our", "under", "name", "very", "through", "just", "form",
"sentence", "great", "think", "say", "help", "low", "line", "differ", "turn", "cause",
"much", "mean", "before", "move", "right", "boy", "old", "too", "same", "she",
"all", "there", "when", "up", "use", "your", "way", "about", "many", "then",
"them", "write", "would", "like", "so", "these", "her", "long", "make", "thing",
"see", "him", "two", "has", "look", "more", "day", "could", "go", "come",
"did", "number", "sound", "no", "most", "people", "my", "over", "know", "water",
"than", "call", "first", "who", "may", "down", "side", "been", "now", "find",
"head", "stand", "own", "page", "should", "country", "found", "answer", "school", "grow",
"study", "still", "learn", "plant", "cover", "food", "sun", "four", "between", "state",
"keep", "eye", "never", "last", "let", "thought", "city", "tree", "cross", "farm",
"hard", "start", "might", "story", "saw", "far", "sea", "draw", "left", "late",
"run", "don't", "while", "press", "close", "night", "real", "life", "few", "north",
"book", "carry", "took", "science", "eat", "room", "friend", "began", "idea", "fish",
"mountain", "stop", "once", "base", "hear", "horse", "cut", "sure", "watch", "color",
"face", "wood", "main", "open", "seem", "together", "next", "white", "children", "begin",
"got", "walk", "example", "ease", "paper", "group", "always", "music", "those", "both",
"mark", "often", "letter", "until", "mile", "river", "car", "feet", "care", "second",
"enough", "plain", "girl", "usual", "young", "ready", "above", "ever", "red", "list",
"though", "feel", "talk", "bird", "soon", "body", "dog", "family", "direct", "pose",
"leave", "song", "measure", "door", "product", "black", "short", "numeral", "class", "wind",
"question", "happen", "complete", "ship", "area", "half", "rock", "order", "fire", "south",
"problem", "piece", "told", "knew", "pass", "since", "top", "whole", "king", "street",
"inch", "multiply", "nothing", "course", "stay", "wheel", "full", "force", "blue", "object",
"decide", "surface", "deep", "moon", "island", "foot", "system", "busy", "test", "record",
"boat", "common", "gold", "possible", "plane", "stead", "dry", "wonder", "laugh", "thousand",
"ago", "ran", "check", "game", "shape", "equate", "hot", "miss", "brought", "heat",
"snow", "tire", "bring", "yes", "distant", "fill", "east", "paint", "language", "among",
"unit", "power", "town", "fine", "certain", "fly", "fall", "lead", "cry", "dark",
"machine", "note", "wait", "plan", "figure", "star", "box", "noun", "field", "rest",
"correct", "able", "pound", "done", "beauty", "drive", "stood", "contain", "front", "teach",
"week", "final", "gave", "green", "oh", "quick", "develop", "ocean", "warm", "free",
"minute", "strong", "special", "mind", "behind", "clear", "tail", "produce", "fact", "space",
"heard", "best", "hour", "better", "true", "during", "hundred", "five", "remember", "step",
"early", "hold", "west", "ground", "interest", "reach", "fast", "verb", "sing", "listen",
"six", "table", "travel", "less", "morning", "ten", "simple", "several", "vowel", "toward",
"war", "lay", "against", "pattern", "slow", "center", "love", "person", "money", "serve",
"appear", "road", "map", "rain", "rule", "govern", "pull", "cold", "notice", "voice",
"energy", "hunt", "probable", "bed", "brother", "egg", "ride", "cell", "believe", "perhaps",
"pick", "sudden", "count", "square", "reason", "length", "represent", "art", "subject", "region",
"size", "vary", "settle", "speak", "weight", "general", "ice", "matter", "circle", "pair",
"include", "divide", "syllable", "felt", "grand", "ball", "yet", "wave", "drop", "heart",
"am", "present", "heavy", "dance", "engine", "position", "arm", "wide", "sail", "material",
"fraction", "forest", "sit", "race", "window", "store", "summer", "train", "sleep", "prove",
"lone", "leg", "exercise", "wall", "catch", "mount", "wish", "sky", "board", "joy",
"winter", "sat", "written", "wild", "instrument", "kept", "glass", "grass", "cow", "job",
"edge", "sign", "visit", "past", "soft", "fun", "bright", "gas", "weather", "month",
"million", "bear", "finish", "happy", "hope", "flower", "clothe", "strange", "gone", "trade",
"melody", "trip", "office", "receive", "row", "mouth", "exact", "symbol", "die", "least",
"trouble", "shout", "except", "wrote", "seed", "tone", "join", "suggest", "clean", "break",
"lady", "yard", "rise", "bad", "blow", "oil", "blood", "touch", "grew", "cent",
"mix", "team", "wire", "cost", "lost", "brown", "wear", "garden", "equal", "sent",
"choose", "fell", "fit", "flow", "fair", "bank", "collect", "save", "control", "decimal",
"ear", "else", "quite", "broke", "case", "middle", "kill", "son", "lake", "moment",
"scale", "loud", "spring", "observe", "child", "straight", "consonant", "nation", "dictionary", "milk",
"speed", "method", "organ", "pay", "age", "section", "dress", "cloud", "surprise", "quiet",
"stone", "tiny", "climb", "cool", "design", "poor", "lot", "experiment", "bottom", "key",
"iron", "single", "stick", "flat", "twenty", "skin", "smile", "crease", "hole", "jump",
"baby", "eight", "village", "meet", "root", "buy", "raise", "solve", "metal", "whether",
"push", "seven", "paragraph", "third", "shall", "held", "hair", "describe", "cook", "floor",
"either", "result", "burn", "hill", "safe", "cat", "century", "consider", "type", "law",
"bit", "coast", "copy", "phrase", "silent", "tall", "sand", "soil", "roll", "temperature",
"finger", "industry", "value", "fight", "lie", "beat", "excite", "natural", "view", "sense",
"capital", "won't", "chair", "danger", "fruit", "rich", "thick", "soldier", "process", "operate",
"practice", "separate", "difficult", "doctor", "please", "protect", "noon", "crop", "modern", "element",
"hit", "student", "corner", "party", "supply", "whose", "locate", "ring", "character", "insect",
"caught", "period", "indicate", "radio", "spoke", "atom", "human", "history", "effect", "electric",
"expect", "bone", "rail", "imagine", "provide", "agree", "thus", "gentle", "woman", "captain",
"guess", "necessary", "sharp", "wing", "create", "neighbor", "wash", "bat", "rather", "crowd",
"corn", "compare", "poem", "string", "bell", "depend", "meat", "rub", "tube", "famous",
"dollar", "stream", "fear", "sight", "thin", "triangle", "planet", "hurry", "chief", "colony",
"clock", "mine", "tie", "enter", "major", "fresh", "search", "send", "yellow", "gun",
"allow", "print", "dead", "spot", "desert", "suit", "current", "lift", "rose", "arrive",
"master", "track", "parent", "shore", "division", "sheet", "substance", "favor", "connect", "post",
"spend", "chord", "fat", "glad", "original", "share", "station", "dad", "bread", "charge",
"proper", "bar", "offer", "segment", "slave", "duck", "instant", "market", "degree", "populate",
"chick", "dear", "enemy", "reply", "drink", "occur", "support", "speech", "nature", "range",
"steam", "motion", "path", "liquid", "log", "meant", "quotient", "teeth", "shell", "neck",
"oxygen", "sugar", "death", "pretty", "skill", "women", "season", "solution", "magnet", "silver",
"thank", "branch", "match", "suffix", "especially", "fig", "afraid", "huge", "sister", "steel",
"discuss", "forward", "similar", "guide", "experience", "score", "apple", "bought", "led", "pitch",
"coat", "mass", "card", "band", "rope", "slip", "win", "dream", "evening", "condition",
"feed", "tool", "total", "basic", "smell", "valley", "nor", "double", "seat", "continue",
"block", "chart", "hat", "sell", "success", "company", "subtract", "event", "particular", "deal",
"swim", "term", "opposite", "wife", "shoe", "shoulder", "spread", "arrange", "camp", "invent",
"cotton", "born", "determine", "quart", "nine", "truck", "noise", "level", "chance", "gather",
"shop", "stretch", "throw", "shine", "property", "column", "molecule", "select", "wrong", "gray",
"repeat", "require", "broad", "prepare", "salt", "nose", "plural", "anger", "claim", "continent",
}

22
wordtest.go Normal file
View File

@ -0,0 +1,22 @@
package main
import "regexp"
func generateWordTest(name string, n int, g int) func() []segment {
var b []byte
if b = readResource("words", name); b == nil {
die("%s does not appear to be a valid word list. See '-list words' for a list of builtin word lists.", name)
}
words := regexp.MustCompile("\\s+").Split(string(b), -1)
return func() []segment {
segments := make([]segment, g)
for i := 0; i < g; i++ {
segments[i] = segment{randomText(n, words), ""}
}
return segments
}
}