Skip to content

Commit 9d94c1f

Browse files
authored
Add wordcopy.py trainer script. (#42)
1 parent a33c833 commit 9d94c1f

4 files changed

Lines changed: 207 additions & 1 deletion

File tree

scripts/superkey/interface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def autokey_quick_msg(self, index: int):
148148
self.__send_packet(MessageID.REQUEST_AUTOKEY_QUICK_MSG, struct.pack('<B', index))
149149
self.__check_reply_empty()
150150

151-
def autokey_wait(self, delay: float = 0.25):
151+
def autokey_wait(self, delay: float = 0.1):
152152
"""
153153
Waits until the autokey buffer is empty.
154154
NOTE: This is not an interface call - it periodically polls `autokey_count()`.

scripts/utility/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#
2+
# @file scripts/utility/__init__.py
3+
# @brief Init file for the utility module.
4+
#
5+
# @author Chris Vig (chris@invictus.so)
6+
# @date 2025-09-10
7+
# @cpyrt © 2025 by Chris Vig. Licensed under the GNU General Public License v3 (GPLv3).
8+
#
9+
10+
# ------------------------------------------------------ IMPORTS -------------------------------------------------------
11+
12+
from .wordlist import *

scripts/utility/wordlist.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#
2+
# @file scripts/utility/wordlist.py
3+
# @brief Python module defining the WordList class.
4+
#
5+
# @author Chris Vig (chris@invictus.so)
6+
# @date 2025-09-10
7+
# @cpyrt © 2025 by Chris Vig. Licensed under the GNU General Public License v3 (GPLv3).
8+
#
9+
10+
# ------------------------------------------------------ IMPORTS -------------------------------------------------------
11+
12+
import random
13+
from urllib import request
14+
15+
# ------------------------------------------------------ EXPORTS -------------------------------------------------------
16+
17+
__all__ = [
18+
'WordList'
19+
]
20+
21+
# ----------------------------------------------------- CONSTANTS ------------------------------------------------------
22+
23+
DEFAULT_WORD_LIST_URL = 'https://raw.githubusercontent.com/first20hours/google-10000-english/refs/heads/master/google-10000-english-usa-no-swears.txt'
24+
25+
# ------------------------------------------------------- TYPES --------------------------------------------------------
26+
27+
class WordList:
28+
"""
29+
Class providing access to a list of words which may be selected from randomly.
30+
"""
31+
def __init__(self, url: str = DEFAULT_WORD_LIST_URL):
32+
"""
33+
Initializes a new instance using the file at the specified URL.
34+
"""
35+
# Load file from URL
36+
self.url = url
37+
with request.urlopen(url) as response:
38+
data = response.read()
39+
40+
# Get word list by splitting up lines
41+
self.words = str(data, encoding='utf8').splitlines()
42+
43+
# Get word list sorted by length and determine minimum and maximum
44+
self.words_by_length = sorted(self.words, key=len)
45+
self.min_length = len(self.words_by_length[0])
46+
self.max_length = len(self.words_by_length[-1])
47+
48+
# Find start index for each length
49+
self.words_by_length_indices = { }
50+
for idx, word in enumerate(self.words_by_length):
51+
for length in range(self.min_length, len(word) + 1):
52+
if length not in self.words_by_length_indices:
53+
self.words_by_length_indices[length] = idx
54+
55+
def __len__(self):
56+
"""
57+
Returns the number of words in this word list.
58+
"""
59+
return len(self.words)
60+
61+
def _start_idx(self, length: int) -> int:
62+
"""
63+
Returns the start index in the sorted list for words with the specified length.
64+
"""
65+
if length < self.min_length:
66+
return 0
67+
elif length > self.max_length:
68+
return len(self)
69+
else:
70+
return self.words_by_length_indices[length]
71+
72+
def random(self, min_length: int = None, max_length: int = None):
73+
"""
74+
Returns a randomly selected word from this word list.
75+
"""
76+
# Sanity check range
77+
if min_length is None or min_length < self.min_length:
78+
min_length = self.min_length
79+
if max_length is None or max_length > self.max_length:
80+
max_length = self.max_length
81+
82+
# Get indices in the sorted list for the requested lengths
83+
start_idx = self._start_idx(min_length)
84+
end_idx = self._start_idx(max_length + 1)
85+
return self.words_by_length[random.randrange(start_idx, end_idx)]

scripts/wordcopy.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#
2+
# @file scripts/wordcopy.py
3+
# @brief Trainer script for copying English words.
4+
#
5+
# @author Chris Vig (chris@invictus.so)
6+
# @date 2025-09-10
7+
# @cpyrt © 2025 by Chris Vig. Licensed under the GNU General Public License v3 (GPLv3).
8+
#
9+
10+
# ------------------------------------------------------ IMPORTS -------------------------------------------------------
11+
12+
import argparse
13+
import time
14+
15+
from superkey import *
16+
from utility import wordlist
17+
18+
# ----------------------------------------------------- CONSTANTS ------------------------------------------------------
19+
20+
DEFAULT_COUNT = 20
21+
DEFAULT_DELAY = 3.
22+
DEFAULT_WPM = 20.
23+
DEFAULT_MINLEN = 2
24+
DEFAULT_MAXLEN = 8
25+
26+
# ----------------------------------------------------- PROCEDURES -----------------------------------------------------
27+
28+
def _parse_args():
29+
"""
30+
Parses the command line arguments.
31+
"""
32+
parser = argparse.ArgumentParser(description='Word Copy Trainer')
33+
parser.add_argument('--port', type=str, default=SUPERKEY_DEFAULT_PORT, help='Serial port to connect to.')
34+
parser.add_argument('--baudrate', type=int, default=SUPERKEY_DEFAULT_BAUDRATE, help='Serial port baud rate.')
35+
parser.add_argument('--timeout', type=float, default=SUPERKEY_DEFAULT_TIMEOUT, help='Serial port timeout (s).')
36+
parser.add_argument('--count', type=int, default=DEFAULT_COUNT, help='Number of words to key.')
37+
parser.add_argument('--delay', type=float, default=DEFAULT_DELAY, help='Seconds to delay between each word.')
38+
parser.add_argument('--wpm', type=float, default=DEFAULT_WPM, help='Words per minute.')
39+
parser.add_argument('--minlen', type=int, default=DEFAULT_MINLEN, help='Minimum word length.')
40+
parser.add_argument('--maxlen', type=int, default=DEFAULT_MAXLEN, help='Maximum word length.')
41+
return parser.parse_args()
42+
43+
def _main(port: str,
44+
baudrate: int,
45+
timeout: float,
46+
count: int,
47+
delay: float,
48+
wpm: float,
49+
minlen: int,
50+
maxlen: int):
51+
"""
52+
Runs the trainer.
53+
"""
54+
# Build word list
55+
wl = wordlist.WordList()
56+
57+
# Open SuperKey interface
58+
with Interface(port = port, baudrate = baudrate, timeout = timeout) as intf:
59+
60+
try:
61+
62+
# Get initial settings
63+
initial_wpm = intf.get_wpm()
64+
initial_trainer_mode = intf.get_trainer_mode()
65+
66+
# Override settings
67+
intf.set_wpm(wpm)
68+
intf.set_trainer_mode(True)
69+
70+
# Run as many times as commanded
71+
for _ in range(count):
72+
73+
# Get a random word and key it
74+
word = wl.random(min_length=minlen, max_length=maxlen).upper()
75+
intf.autokey(word, block=True)
76+
77+
# Print the word after a delay
78+
time.sleep(delay)
79+
print(word)
80+
time.sleep(delay * .5)
81+
82+
except KeyboardInterrupt:
83+
84+
# Cancel autokey
85+
intf.panic()
86+
87+
finally:
88+
89+
# Restore initial settings
90+
intf.set_wpm(initial_wpm)
91+
intf.set_trainer_mode(initial_trainer_mode)
92+
93+
94+
# -------------------------------------------------- IMPORT PROCEDURE --------------------------------------------------
95+
96+
if __name__ == '__main__':
97+
98+
# Parse arguments
99+
args = _parse_args()
100+
101+
# Run main procedure
102+
_main(port = args.port,
103+
baudrate = args.baudrate,
104+
timeout = args.timeout,
105+
count = args.count,
106+
delay = args.delay,
107+
wpm = args.wpm,
108+
minlen = args.minlen,
109+
maxlen = args.maxlen)

0 commit comments

Comments
 (0)