Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,31 @@ options.
- `C-c` exits the test.
- `right` moves to the next test.
- `left` moves to the previous test.
- On the report screen: `q` quits, `n` advances to the next level (level mode), any other key starts a new test.

## Levels

`tt -level N` starts a progressive typing lesson. Each level introduces new
keys while reinforcing previous ones, starting from the home row anchors (f j)
and building up to the full keyboard across 16 levels. Use `tt -list levels` to
see all available levels.

After completing a level, the report screen shows `[q]` to quit or `[n]` to
advance to the next level.

## Finger Hints

`tt -fingers` displays which finger should be used for the current character.
`tt -visual` shows a visual hand diagram highlighting the active finger.

## Examples

- `tt -quotes en` Starts quote mode with the builtin quote list 'en'.
- `tt -level 1` Starts a progressive lesson beginning with the home row anchors (f j).
- `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 lasting 10 seconds.
- `tt -theme gruvbox` Starts tt with the gruvbox theme.
- `tt -fingers -visual` Starts tt with finger position hints and visual hand diagram.

`tt` is designed to be easily scriptable and integrate nicely with
other *nix tools. With a little shell scripting most features the user can
Expand Down
66 changes: 66 additions & 0 deletions src/fingers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package main

import "strings"

var fingerMap = map[rune]string{
// Left Pinky
'1': "Left Pinky", 'Q': "Left Pinky", 'A': "Left Pinky", 'Z': "Left Pinky",
'!': "Left Pinky", 'q': "Left Pinky", 'a': "Left Pinky", 'z': "Left Pinky",
'`': "Left Pinky", '~': "Left Pinky",

// Left Ring
'2': "Left Ring", 'W': "Left Ring", 'S': "Left Ring", 'X': "Left Ring",
'@': "Left Ring", 'w': "Left Ring", 's': "Left Ring", 'x': "Left Ring",

// Left Middle
'3': "Left Middle", 'E': "Left Middle", 'D': "Left Middle", 'C': "Left Middle",
'#': "Left Middle", 'e': "Left Middle", 'd': "Left Middle", 'c': "Left Middle",

// Left Index
'4': "Left Index", 'R': "Left Index", 'F': "Left Index", 'V': "Left Index",
'$': "Left Index", 'r': "Left Index", 'f': "Left Index", 'v': "Left Index",
'5': "Left Index", 'T': "Left Index", 'G': "Left Index", 'B': "Left Index",
'%': "Left Index", 't': "Left Index", 'g': "Left Index", 'b': "Left Index",

// Thumbs
' ': "Thumb",

// Right Index
'6': "Right Index", 'Y': "Right Index", 'H': "Right Index", 'N': "Right Index",
'^': "Right Index", 'y': "Right Index", 'h': "Right Index", 'n': "Right Index",
'7': "Right Index", 'U': "Right Index", 'J': "Right Index", 'M': "Right Index",
'&': "Right Index", 'u': "Right Index", 'j': "Right Index", 'm': "Right Index",

// Right Middle
'8': "Right Middle", 'I': "Right Middle", 'K': "Right Middle", ',': "Right Middle",
'*': "Right Middle", 'i': "Right Middle", 'k': "Right Middle", '<': "Right Middle",

// Right Ring
'9': "Right Ring", 'O': "Right Ring", 'L': "Right Ring", '.': "Right Ring",
'(': "Right Ring", 'o': "Right Ring", 'l': "Right Ring", '>': "Right Ring",

// Right Pinky
'0': "Right Pinky", 'P': "Right Pinky", ';': "Right Pinky", '/': "Right Pinky",
')': "Right Pinky", 'p': "Right Pinky", ':': "Right Pinky", '?': "Right Pinky",
'-': "Right Pinky", '[': "Right Pinky", '\'': "Right Pinky",
'_': "Right Pinky", '{': "Right Pinky", '"': "Right Pinky",
'=': "Right Pinky", ']': "Right Pinky", '\\': "Right Pinky",
'+': "Right Pinky", '}': "Right Pinky", '|': "Right Pinky",
}

func getFingerForRune(r rune) string {
if finger, ok := fingerMap[r]; ok {
return finger
}
return ""
}

func getHandForFinger(finger string) string {
if strings.Contains(finger, "Left") {
return "Left"
}
if strings.Contains(finger, "Right") {
return "Right"
}
return ""
}
89 changes: 89 additions & 0 deletions src/fingers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package main

import "testing"

func TestGetFingerForRune(t *testing.T) {
tests := []struct {
r rune
expect string
}{
{'1', "Left Pinky"},
{'Q', "Left Pinky"},
{'A', "Left Pinky"},
{'Z', "Left Pinky"},
{'~', "Left Pinky"},
{'2', "Left Ring"},
{'W', "Left Ring"},
{'S', "Left Ring"},
{'X', "Left Ring"},
{'3', "Left Middle"},
{'E', "Left Middle"},
{'D', "Left Middle"},
{'C', "Left Middle"},
{'4', "Left Index"},
{'R', "Left Index"},
{'F', "Left Index"},
{'V', "Left Index"},
{'5', "Left Index"},
{'T', "Left Index"},
{'G', "Left Index"},
{'B', "Left Index"},
{' ', "Thumb"},
{'6', "Right Index"},
{'Y', "Right Index"},
{'H', "Right Index"},
{'N', "Right Index"},
{'7', "Right Index"},
{'U', "Right Index"},
{'J', "Right Index"},
{'M', "Right Index"},
{'8', "Right Middle"},
{'I', "Right Middle"},
{'K', "Right Middle"},
{',', "Right Middle"},
{'9', "Right Ring"},
{'O', "Right Ring"},
{'L', "Right Ring"},
{'.', "Right Ring"},
{'0', "Right Pinky"},
{'P', "Right Pinky"},
{';', "Right Pinky"},
{'/', "Right Pinky"},
{'-', "Right Pinky"},
{'=', "Right Pinky"},
{'[', "Right Pinky"},
{']', "Right Pinky"},
{'\\', "Right Pinky"},
{'\'', "Right Pinky"},
{'`', "Left Pinky"},
{'a', "Left Pinky"},
{'z', "Left Pinky"},
{'!', "Left Pinky"},
{'@', "Left Ring"},
{'#', "Left Middle"},
{'$', "Left Index"},
{'%', "Left Index"},
{'^', "Right Index"},
{'&', "Right Index"},
{'*', "Right Middle"},
{'(', "Right Ring"},
{')', "Right Pinky"},
{'_', "Right Pinky"},
{'+', "Right Pinky"},
{'{', "Right Pinky"},
{'}', "Right Pinky"},
{'|', "Right Pinky"},
{':', "Right Pinky"},
{'"', "Right Pinky"},
{'<', "Right Middle"},
{'>', "Right Ring"},
{'?', "Right Pinky"},
}

for _, tc := range tests {
got := getFingerForRune(tc.r)
if got != tc.expect {
t.Errorf("getFingerForRune(%q): expected %q, got %q", tc.r, tc.expect, got)
}
}
}
60 changes: 60 additions & 0 deletions src/hands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import "github.com/gdamore/tcell"

var handsArt = []string{
" .-. .-. .-. .-. .-. .-. .-. .-.",
" | | | | | | | | | | | | | | | |",
" | | | | | | | | | | | | | | | |",
" |___| |___| |___| |___| ___ ___ |___| |___| |___| |___|",
" | | | | | | | |",
" | |__| |_| |__| |",
" | | | | | | | |",
" '---------------------' '---' '---' '---------------------'",
}

// Finger coordinates (x start, x end, y start, y end) relative to art top-left
type rect struct {
x1, x2, y1, y2 int
}

var fingerCoords = map[string]rect{
"Left Pinky": {5, 9, 1, 3}, // x: 5 to 9
"Left Ring": {11, 15, 1, 3},
"Left Middle": {17, 21, 1, 3},
"Left Index": {23, 27, 1, 3},
"Left Thumb": {30, 34, 4, 6},
"Right Thumb": {36, 40, 4, 6},
"Right Index": {43, 47, 1, 3},
"Right Middle": {49, 53, 1, 3},
"Right Ring": {55, 59, 1, 3},
"Right Pinky": {61, 65, 1, 3},
}

func drawHands(scr tcell.Screen, x, y int, activeFinger string, style tcell.Style, highlightStyle tcell.Style) {
for r, line := range handsArt {
for c, char := range line {
s := style

// Check collision with active finger
inRegion := false

if activeRect, ok := fingerCoords[activeFinger]; ok {
// Simple bounding box check
if c >= activeRect.x1 && c <= activeRect.x2 && r >= activeRect.y1 && r <= activeRect.y2 {
inRegion = true
}
}

// Apply highlight if we are "inside" the finger region
if inRegion {
// Only highlight non-space characters to keep the shape
if char != ' ' {
s = highlightStyle
}
}

scr.SetContent(x+c, y+r, rune(char), nil, s)
}
}
}
Loading