v0.4.0
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:
10
CHANGELOG.md
10
CHANGELOG.md
@ -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:
|
# 0.3.0:
|
||||||
- Added support for custom word lists (`-words).
|
- Added support for custom word lists (`-words).
|
||||||
- `-theme` now accepts a path.
|
- `-theme` now accepts a path.
|
||||||
|
4
Makefile
4
Makefile
@ -2,9 +2,11 @@ all:
|
|||||||
go build -o bin/tt *.go
|
go build -o bin/tt *.go
|
||||||
install:
|
install:
|
||||||
install -m755 bin/tt /usr/local/bin
|
install -m755 bin/tt /usr/local/bin
|
||||||
|
install -m755 tt.1.gz /usr/share/man/man1
|
||||||
assets:
|
assets:
|
||||||
python3 ./scripts/themegen.py
|
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:
|
rel:
|
||||||
GOOS=darwin GOARCH=amd64 go build -o bin/tt-osx *.go
|
GOOS=darwin GOARCH=amd64 go build -o bin/tt-osx *.go
|
||||||
GOOS=windows GOARCH=amd64 go build -o bin/tt.exe *.go
|
GOOS=windows GOARCH=amd64 go build -o bin/tt.exe *.go
|
||||||
|
45
README.md
45
README.md
@ -9,19 +9,17 @@ A terminal based typing test.
|
|||||||
## Linux
|
## 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
|
## 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
|
## From source
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -38,21 +36,33 @@ Best served on a terminal with truecolor and cursor shape support (e.g kitty, it
|
|||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
By default 50 words from the top 1000 words in the English language are used to
|
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
|
constitute the test. Custom text can be supplied by piping arbitrary text to the
|
||||||
the program. Each paragraph in the input is shown as a separate segment of the
|
program. Each paragraph in the input is shown as a separate segment of the text.
|
||||||
text.
|
See `man tt` or `man.md` for a complete description and a comprehensive set of
|
||||||
|
options.
|
||||||
|
|
||||||
## Keys
|
## Keys
|
||||||
|
|
||||||
- Pressing `escape` at any point restarts the test.
|
- Pressing `escape` at any point restarts the test.
|
||||||
- `C-c` exits the test.
|
- `C-c` exits the test.
|
||||||
|
- `right` moves to the next test.
|
||||||
|
- `left` moves to the previous test.
|
||||||
|
|
||||||
## Examples
|
## 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 -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 -t 10` starts a timed test lasting 10 seconds.
|
||||||
- `tt -theme gruvbox` Starts tt with the gruvbox theme
|
- `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`.
|
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
|
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
|
preloaded themes and word lists can be found in `words/` and `themes/` and are
|
||||||
accessible by default using the respective flags.
|
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
7
TODO
Normal 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
30
datatest.go
Normal 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
42
db.go
Normal 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
44
doc
Normal 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
45
filetest.go
Normal 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
1
go.sum
@ -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-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 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
|
||||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
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-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 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
203
man.md
Normal file
203
man.md
Normal 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
|
24
quotetest.go
Normal file
24
quotetest.go
Normal 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, "es)
|
||||||
|
if err != nil {
|
||||||
|
die("Improperly formatted quote file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() []segment {
|
||||||
|
idx := rand.Int() % len(quotes)
|
||||||
|
return quotes[idx : idx+1]
|
||||||
|
}
|
||||||
|
}
|
323
tt.go
323
tt.go
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -17,16 +18,20 @@ import (
|
|||||||
|
|
||||||
var scr tcell.Screen
|
var scr tcell.Screen
|
||||||
var csvMode bool
|
var csvMode bool
|
||||||
|
var jsonMode bool
|
||||||
|
|
||||||
type result struct {
|
type result struct {
|
||||||
wpm int
|
Wpm int `json:"wpm"`
|
||||||
cpm int
|
Cpm int `json:"cpm"`
|
||||||
accuracy float64
|
Accuracy float64 `json:"accuracy"`
|
||||||
timestamp int64
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
Mistakes []mistake `json:"mistakes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func die(format string, args ...interface{}) {
|
func die(format string, args ...interface{}) {
|
||||||
scr.Fini()
|
if scr != nil {
|
||||||
|
scr.Fini()
|
||||||
|
}
|
||||||
fmt.Fprintf(os.Stderr, "ERROR: ")
|
fmt.Fprintf(os.Stderr, "ERROR: ")
|
||||||
fmt.Fprintf(os.Stderr, format, args...)
|
fmt.Fprintf(os.Stderr, format, args...)
|
||||||
fmt.Fprintf(os.Stderr, "\n")
|
fmt.Fprintf(os.Stderr, "\n")
|
||||||
@ -54,17 +59,50 @@ func parseConfig(b []byte) map[string]string {
|
|||||||
func exit(rc int) {
|
func exit(rc int) {
|
||||||
scr.Fini()
|
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 {
|
if csvMode {
|
||||||
for _, r := range results {
|
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)
|
os.Exit(rc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showReport(scr tcell.Screen, cpm, wpm int, accuracy float64) {
|
func showReport(scr tcell.Screen, cpm, wpm int, accuracy float64, attribution string, mistakes []mistake) {
|
||||||
report := fmt.Sprintf("WPM: %d\nCPM: %d\nAccuracy: %.2f%%", wpm, cpm, accuracy)
|
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()
|
scr.Clear()
|
||||||
drawStringAtCenter(scr, report, tcell.StyleDefault)
|
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)
|
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() {
|
func main() {
|
||||||
var n int
|
var n int
|
||||||
var ngroups int
|
var g int
|
||||||
var testFn func() []string
|
|
||||||
var rawMode bool
|
var rawMode bool
|
||||||
var oneShotMode bool
|
var oneShotMode bool
|
||||||
|
var noHighlightCurrent bool
|
||||||
|
var noHighlightNext bool
|
||||||
|
var noHighlight bool
|
||||||
var maxLineLen int
|
var maxLineLen int
|
||||||
var noSkip bool
|
var noSkip bool
|
||||||
|
var noBackspace bool
|
||||||
var noReport bool
|
var noReport bool
|
||||||
var timeout int
|
var timeout int
|
||||||
|
var startParagraph int
|
||||||
|
|
||||||
var listFlag string
|
var listFlag string
|
||||||
var wordList string
|
var wordFile string
|
||||||
var err error
|
var quoteFile string
|
||||||
|
|
||||||
var themeName string
|
var themeName string
|
||||||
var showWpm bool
|
var showWpm bool
|
||||||
var multiMode bool
|
var multiMode bool
|
||||||
var versionFlag bool
|
var versionFlag bool
|
||||||
|
|
||||||
flag.IntVar(&n, "n", 50, "The number of words which constitute a group.")
|
var err error
|
||||||
flag.IntVar(&ngroups, "g", 1, "The number of groups which constitute a generated test.")
|
var testFn func() []segment
|
||||||
|
|
||||||
flag.IntVar(&maxLineLen, "w", 80, "The maximum line length in characters. (ignored if -raw is present).")
|
flag.IntVar(&n, "n", 50, "")
|
||||||
flag.IntVar(&timeout, "t", -1, "Terminate the test after the given number of seconds.")
|
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(&versionFlag, "v", false, "")
|
||||||
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.Usage = func() {
|
flag.StringVar(&wordFile, "words", "", "")
|
||||||
fmt.Fprintf(os.Stderr, `Usage: tt [options]
|
flag.StringVar("eFile, "quotes", "", "")
|
||||||
|
|
||||||
By default tt creates a test consisting of 50 randomly generated words from the
|
flag.BoolVar(&showWpm, "showwpm", false, "")
|
||||||
top 1000 words in the English language. Arbitrary text can also be piped
|
flag.BoolVar(&noSkip, "noskip", false, "")
|
||||||
directly into the program to create a custom test. Each paragraph of the
|
flag.BoolVar(&noBackspace, "nobackspace", false, "")
|
||||||
input is treated as a segment of the test.
|
flag.BoolVar(&oneShotMode, "oneshot", false, "")
|
||||||
|
flag.BoolVar(&noHighlight, "nohighlight", false, "")
|
||||||
Examples:
|
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", "", "")
|
||||||
|
|
||||||
# Equivalent to 'tt -n 40 -words /usr/share/dict/words'
|
flag.Usage = func() { os.Stdout.Write([]byte(usage)) }
|
||||||
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.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if listFlag != "" {
|
if listFlag != "" {
|
||||||
@ -233,55 +302,23 @@ Options:
|
|||||||
"\n", " \n", -1)
|
"\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)
|
b, err := ioutil.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
getParagraphs := func(s string) []string {
|
testFn = generateTestFromData(b, rawMode, multiMode)
|
||||||
s = strings.Replace(s, "\r", "", -1)
|
case len(flag.Args()) > 0:
|
||||||
s = regexp.MustCompile("\n\n+").ReplaceAllString(s, "\n\n")
|
path := flag.Args()[0]
|
||||||
return strings.Split(strings.Trim(s, "\n"), "\n\n")
|
testFn = generateTestFromFile(path, startParagraph)
|
||||||
}
|
default:
|
||||||
|
testFn = generateWordTest("1000en", n, g)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scr, err = tcell.NewScreen()
|
scr, err = tcell.NewScreen()
|
||||||
@ -301,48 +338,70 @@ Options:
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
typer := createTyper(scr, themeName)
|
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.SkipWord = !noSkip
|
||||||
|
typer.DisableBackspace = noBackspace
|
||||||
typer.ShowWpm = showWpm
|
typer.ShowWpm = showWpm
|
||||||
|
|
||||||
if timeout != -1 {
|
if timeout != -1 {
|
||||||
timeout *= 1E9
|
timeout *= 1E9
|
||||||
}
|
}
|
||||||
|
|
||||||
var showNext = true
|
var tests [][]segment
|
||||||
var paragraphs []string
|
var idx = 0
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if showNext {
|
if idx >= len(tests) {
|
||||||
paragraphs = testFn()
|
tests = append(tests, testFn())
|
||||||
|
}
|
||||||
|
|
||||||
if paragraphs == nil {
|
if tests[idx] == nil {
|
||||||
exit(0)
|
exit(0)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !rawMode {
|
if !rawMode {
|
||||||
for i, _ := range paragraphs {
|
for i, _ := range tests[idx] {
|
||||||
paragraphs[i] = reflow(paragraphs[i])
|
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 {
|
switch rc {
|
||||||
|
case TyperNext:
|
||||||
|
idx++
|
||||||
|
case TyperPrevious:
|
||||||
|
if idx > 0 {
|
||||||
|
idx--
|
||||||
|
}
|
||||||
case TyperComplete:
|
case TyperComplete:
|
||||||
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
|
||||||
|
|
||||||
results = append(results, result{wpm, cpm, accuracy, time.Now().Unix()})
|
results = append(results, result{wpm, cpm, accuracy, time.Now().Unix(), mistakes})
|
||||||
if !noReport {
|
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 {
|
if oneShotMode {
|
||||||
exit(0)
|
exit(0)
|
||||||
}
|
}
|
||||||
showNext = true
|
|
||||||
|
idx++
|
||||||
case TyperSigInt:
|
case TyperSigInt:
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
262
typer.go
262
typer.go
@ -16,22 +16,35 @@ const (
|
|||||||
TyperComplete = iota
|
TyperComplete = iota
|
||||||
TyperSigInt
|
TyperSigInt
|
||||||
TyperEscape
|
TyperEscape
|
||||||
|
TyperPrevious
|
||||||
|
TyperNext
|
||||||
TyperResize
|
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 {
|
type typer struct {
|
||||||
Scr tcell.Screen
|
Scr tcell.Screen
|
||||||
OnStart func()
|
OnStart func()
|
||||||
SkipWord bool
|
SkipWord bool
|
||||||
ShowWpm bool
|
ShowWpm bool
|
||||||
tty io.Writer
|
DisableBackspace bool
|
||||||
|
tty io.Writer
|
||||||
|
|
||||||
currentWordStyle tcell.Style
|
currentWordStyle tcell.Style
|
||||||
nextWordStyle tcell.Style
|
nextWordStyle tcell.Style
|
||||||
incorrectSpaceStyle tcell.Style
|
incorrectSpaceStyle tcell.Style
|
||||||
incorrectStyle tcell.Style
|
incorrectStyle tcell.Style
|
||||||
correctStyle 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 {
|
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,
|
SkipWord: true,
|
||||||
tty: tty,
|
tty: tty,
|
||||||
|
|
||||||
backgroundStyle: def,
|
defaultStyle: def,
|
||||||
correctStyle: def.Foreground(hicol),
|
correctStyle: def.Foreground(hicol),
|
||||||
currentWordStyle: def.Foreground(hicol2),
|
currentWordStyle: def.Foreground(hicol2),
|
||||||
nextWordStyle: def.Foreground(hicol3),
|
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) {
|
func (t *typer) Start(text []segment, timeout time.Duration) (nerrs, ncorrect int, duration time.Duration, rc int, mistakes []mistake) {
|
||||||
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
|
timeLeft := timeout
|
||||||
|
|
||||||
for i, p := range text {
|
for i, s := range text {
|
||||||
startImmediately := true
|
startImmediately := true
|
||||||
var d time.Duration
|
var d time.Duration
|
||||||
var e, c int
|
var e, c int
|
||||||
|
var m []mistake
|
||||||
|
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
startImmediately = false
|
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
|
nerrs += e
|
||||||
ncorrect += c
|
ncorrect += c
|
||||||
duration += d
|
duration += d
|
||||||
|
mistakes = append(mistakes, m...)
|
||||||
|
|
||||||
if timeout != -1 {
|
if timeout != -1 {
|
||||||
timeLeft -= d
|
timeLeft -= d
|
||||||
@ -106,37 +108,79 @@ func (t *typer) Start(text []string, timeout time.Duration) (nerrs, ncorrect int
|
|||||||
return
|
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
|
var startTime time.Time
|
||||||
text := stringToCells(s)
|
text := []rune(s)
|
||||||
|
typed := make([]rune, len(text))
|
||||||
|
|
||||||
sw, sh := scr.Size()
|
sw, sh := scr.Size()
|
||||||
nc, nr := calcStringDimensions(s)
|
nc, nr := calcStringDimensions(s)
|
||||||
x := (sw - nc) / 2
|
x := (sw - nc) / 2
|
||||||
y := (sh - nr) / 2
|
y := (sh - nr) / 2
|
||||||
|
|
||||||
for i, _ := range text {
|
|
||||||
text[i].style = t.backgroundStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
t.tty.Write([]byte("\033[5 q"))
|
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
|
//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.
|
//good way to save/restore the shape if the user has changed it from the otcs.
|
||||||
defer t.tty.Write([]byte("\033[2 q"))
|
defer t.tty.Write([]byte("\033[2 q"))
|
||||||
|
|
||||||
t.Scr.SetStyle(t.backgroundStyle)
|
t.Scr.SetStyle(t.defaultStyle)
|
||||||
idx := 0
|
idx := 0
|
||||||
|
|
||||||
calcStats := func() {
|
calcStats := func() {
|
||||||
nerrs = 0
|
nerrs = 0
|
||||||
ncorrect = 0
|
ncorrect = 0
|
||||||
|
|
||||||
for _, c := range text {
|
mistakes = extractMistypedWords(text[:idx], typed[:idx])
|
||||||
if c.style == t.incorrectStyle || c.style == t.incorrectSpaceStyle {
|
|
||||||
nerrs++
|
for i := 0; i < idx; i++ {
|
||||||
} else if c.style == t.correctStyle {
|
if text[i] != '\n' {
|
||||||
ncorrect++
|
if text[i] != typed[i] {
|
||||||
|
nerrs++
|
||||||
|
} else {
|
||||||
|
ncorrect++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,51 +189,92 @@ func (t *typer) start(s string, timeLimit time.Duration, startImmediately bool)
|
|||||||
}
|
}
|
||||||
|
|
||||||
redraw := func() {
|
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() {
|
if timeLimit != -1 && !startTime.IsZero() {
|
||||||
remaining := timeLimit - time.Now().Sub(startTime)
|
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+ah+1, " ", -1, t.defaultStyle)
|
||||||
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, strconv.Itoa(int(remaining/1E9)+1), -1, t.defaultStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.ShowWpm && !startTime.IsZero() {
|
if t.ShowWpm && !startTime.IsZero() {
|
||||||
calcStats()
|
calcStats()
|
||||||
if duration > 1E7 { //Avoid flashing large numbers on test start.
|
if duration > 1E7 { //Avoid flashing large numbers on test start.
|
||||||
wpm := int((float64(ncorrect) / 5) / (float64(duration) / 60E9))
|
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
|
//Potentially inefficient, but seems to be good enough
|
||||||
|
|
||||||
drawCells(t.Scr, x, y, text, idx)
|
|
||||||
|
|
||||||
t.Scr.Show()
|
t.Scr.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteWord := func() {
|
deleteWord := func() {
|
||||||
t.highlight(text, idx, t.backgroundStyle, t.backgroundStyle)
|
|
||||||
|
|
||||||
if idx == 0 {
|
if idx == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
idx--
|
idx--
|
||||||
|
|
||||||
for idx > 0 && (text[idx].c == ' ' || text[idx].c == '\n') {
|
for idx > 0 && (text[idx] == ' ' || text[idx] == '\n') {
|
||||||
text[idx].style = t.backgroundStyle
|
|
||||||
idx--
|
idx--
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx > 0 && text[idx].c != ' ' && text[idx].c != '\n' {
|
for idx > 0 && text[idx] != ' ' && text[idx] != '\n' {
|
||||||
text[idx].style = t.backgroundStyle
|
|
||||||
idx--
|
idx--
|
||||||
}
|
}
|
||||||
|
|
||||||
if text[idx].c == ' ' || text[idx].c == '\n' {
|
if text[idx] == ' ' || text[idx] == '\n' {
|
||||||
|
typed[idx] = text[idx]
|
||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
|
|
||||||
t.highlight(text, idx, t.currentWordStyle, t.nextWordStyle)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tickerCloser := make(chan bool)
|
tickerCloser := make(chan bool)
|
||||||
@ -217,7 +302,6 @@ func (t *typer) start(s string, timeLimit time.Duration, startImmediately bool)
|
|||||||
|
|
||||||
t.Scr.Clear()
|
t.Scr.Clear()
|
||||||
for {
|
for {
|
||||||
t.highlight(text, idx, t.currentWordStyle, t.nextWordStyle)
|
|
||||||
redraw()
|
redraw()
|
||||||
|
|
||||||
ev := t.Scr.PollEvent()
|
ev := t.Scr.PollEvent()
|
||||||
@ -228,7 +312,9 @@ func (t *typer) start(s string, timeLimit time.Duration, startImmediately bool)
|
|||||||
return
|
return
|
||||||
case *tcell.EventKey:
|
case *tcell.EventKey:
|
||||||
if runtime.GOOS != "windows" && ev.Key() == tcell.KeyBackspace { //Control+backspace on unix terms
|
if runtime.GOOS != "windows" && ev.Key() == tcell.KeyBackspace { //Control+backspace on unix terms
|
||||||
deleteWord()
|
if !t.DisableBackspace {
|
||||||
|
deleteWord()
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,60 +333,56 @@ func (t *typer) start(s string, timeLimit time.Duration, startImmediately bool)
|
|||||||
return
|
return
|
||||||
case tcell.KeyCtrlL:
|
case tcell.KeyCtrlL:
|
||||||
t.Scr.Sync()
|
t.Scr.Sync()
|
||||||
|
|
||||||
|
case tcell.KeyRight:
|
||||||
|
rc = TyperNext
|
||||||
|
return
|
||||||
|
|
||||||
|
case tcell.KeyLeft:
|
||||||
|
rc = TyperPrevious
|
||||||
|
return
|
||||||
|
|
||||||
case tcell.KeyBackspace, tcell.KeyBackspace2:
|
case tcell.KeyBackspace, tcell.KeyBackspace2:
|
||||||
if ev.Modifiers() == tcell.ModAlt || ev.Modifiers() == tcell.ModCtrl {
|
if !t.DisableBackspace {
|
||||||
deleteWord()
|
if ev.Modifiers() == tcell.ModAlt || ev.Modifiers() == tcell.ModCtrl {
|
||||||
} else {
|
deleteWord()
|
||||||
t.highlight(text, idx, t.backgroundStyle, t.backgroundStyle)
|
} else {
|
||||||
|
if idx == 0 {
|
||||||
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
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx < len(text) && text[idx].c != ' ' && text[idx].c != '\n' {
|
idx--
|
||||||
text[idx].style = t.incorrectStyle
|
|
||||||
|
for idx > 0 && text[idx] == '\n' {
|
||||||
|
idx--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case tcell.KeyRune:
|
||||||
|
if idx < len(text) {
|
||||||
|
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] != ' ' && text[idx] != '\n' {
|
||||||
|
typed[idx] = 0
|
||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx < len(text) {
|
if idx < len(text) {
|
||||||
text[idx].style = t.incorrectSpaceStyle
|
typed[idx] = text[idx]
|
||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
default:
|
} else {
|
||||||
if text[idx].c == ' ' {
|
typed[idx] = ev.Rune()
|
||||||
text[idx].style = t.incorrectSpaceStyle
|
|
||||||
} else {
|
|
||||||
text[idx].style = t.incorrectStyle
|
|
||||||
}
|
|
||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx < len(text) && text[idx].c == '\n' {
|
for idx < len(text) && text[idx] == '\n' {
|
||||||
|
typed[idx] = text[idx]
|
||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
|
|
||||||
t.highlight(text, idx, t.currentWordStyle, t.nextWordStyle)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx == len(text) {
|
if idx == len(text) {
|
||||||
|
41
util.go
41
util.go
@ -6,6 +6,7 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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)
|
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) {
|
func wordWrapBytes(s []byte, n int) {
|
||||||
sp := 0
|
sp := 0
|
||||||
sz := 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) {
|
func drawStringAtCenter(scr tcell.Screen, s string, style tcell.Style) {
|
||||||
nc, nr := calcStringDimensions(s)
|
nc, nr := calcStringDimensions(s)
|
||||||
sw, sh := scr.Size()
|
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) {
|
func calcStringDimensions(s string) (nc, nr int) {
|
||||||
|
if s == "" {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
c := 0
|
c := 0
|
||||||
|
|
||||||
for _, x := range s {
|
for _, x := range s {
|
||||||
@ -202,6 +191,14 @@ func newTcellColor(s string) (tcell.Color, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readResource(typ, name string) []byte {
|
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 {
|
if b, err := ioutil.ReadFile(name); err == nil {
|
||||||
return b
|
return b
|
||||||
|
106
words.go
106
words.go
@ -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
22
wordtest.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user