Skip to content

Instantly share code, notes, and snippets.

@juliusgeo
Last active April 16, 2023 02:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save juliusgeo/1f11f9d43f436a4a81238ac29b07005b to your computer and use it in GitHub Desktop.
Save juliusgeo/1f11f9d43f436a4a81238ac29b07005b to your computer and use it in GitHub Desktop.
A pretty nice typing test in 80 lines of Python.
import os,select,tty,sys
from time import time
from functools import partial
# Need this because Unix-esque OSes are different from Windows.
try:
from msvcrt import getch as cf
except ImportError:
tty.setcbreak(sys.stdin.fileno())
cf=partial(sys.stdin.read,1)
# Our class containing our funcs that format text.
class EscChars:
bold=lambda i:f"\033[1m{i}\033[0m"
boldred=lambda i:f"\033[1m\033[91m{i}\033[0m"
blink=lambda i:f"\033[4m{i}\033[0m"
# Read our fortune that we will be testing on.
fortune=os.popen(f"fortune {' '.join(sys.argv[1:])}").read().rstrip()
wrong_buffer,start_time, typed, fortune_idx="",0,[],0
# The function that draws the thing at the bottom showing your current WPM.
banner=lambda typed_length, delta, num_mistakes:f"\nWPM: {(typed_length/5.0)/delta:.2f}, raw WPM: {((typed_length+num_mistakes)/5.0)/delta:.2f}, errors: {num_mistakes}"
# Format our rights and wrongs in the best way.
def formatted_output(wrong_buffer_str,fortune_str,fortune_idx=0):
text=''.join([EscChars.bold(s) if type==True else EscChars.boldred(s) for s,type in typed])
if wrong_buffer_str:
text+=EscChars.boldred(wrong_buffer_str)
if fortune_idx<len(fortune):
text+=EscChars.blink(fortune_str[fortune_idx])
text+=fortune_str[fortune_idx+1:]
return text
# We define a single string variable to hold the current "screen buffer" contents. This allows us to call
# `os.system("clear")` and then immediately print the string with no delay. This reduces the flickering effect on some
# terminals.
while fortune_idx<len(fortune):
i,_,_=select.select([sys.stdin],[],[],.1)
if i:
cycle=""
# If we're at the beginning (in the temporal or spatial sense), reset
if start_time==0 or fortune_idx == 0:
start_time=time()
cur=cf()
# Deletion logic
if ord(cur)==127:
if wrong_buffer:
wrong_buffer=wrong_buffer[:-1]
elif typed:
s,type=typed.pop(-1)
if len(s)>1:
typed.append((s[:-1],type))
fortune_idx-=1
# Correct addition logic
elif cur==fortune[fortune_idx]:
if wrong_buffer:
typed.append((wrong_buffer,False))
wrong_buffer=""
typed.append((cur,True))
fortune_idx+=1
# Incorrect addition logic
else:
if cur.isprintable():
fortune_idx+=1
wrong_buffer+=cur
# Calculate all of our stats BEFORE calling "clear".
typed_length=sum([len(i[0]) if i[1]==True else 0 for i in typed])+len(wrong_buffer)
fortune_idx=max(fortune_idx,0)
num_mistakes=sum([len(i[0]) if i[1]==False else 0 for i in typed])+len(wrong_buffer)
cycle=formatted_output(wrong_buffer,fortune,fortune_idx)
delta=(time()-start_time)/60
cycle+=banner(typed_length, delta, num_mistakes)
# Redraw the screen.
os.system("clear")
print(cycle)
os.system("clear")
print(
f"Summary:\n{banner(typed_length, delta, num_mistakes)}"
)
@juliusgeo
Copy link
Author

juliusgeo commented Apr 15, 2023

It requires the fortune utility. If you're on MacOS you can do brew install fortune, otherwise if you're using a BSD variant you will likely already have the utility. If you're on Windows you're on your own. Here's a demo!
asciicast

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment