#!/usr/bin/env python

import argparse
import curses
import os
import signal
import sys


class ExitSuccess(Exception):
    def __init__(self):
        self.exit_code = os.EX_OK


class ExitFailure(Exception):
    def __init__(self, exit_code, msg):
        self.exit_code = exit_code
        self.msg = msg


class TerminalDimensions:
    def __init__(self, screen):
        self.update(screen)

    def update(self, screen):
        term_dimensions = screen.getmaxyx()
        self.rows = term_dimensions[0] - 1
        self.cols = term_dimensions[1]


def draw_screen(screen, next_char_index, term_dims, input_str):
    screen.move(0, 0)
    screen.erase()
    row = 0
    for wrapped_line in wrap(input_str, term_dims.cols):
        if row == term_dims.rows:
            break
        screen.addstr(row, 0, wrapped_line)
        row += 1
    screen.move(0, next_char_index)
    screen.refresh()


def wrap(line, cols):
    return [line[i:i + cols] for i in range(0, len(line), cols)]


def run_curses(screen, input_str):
    curses.use_default_colors()
    VERY_VISIBLE = 2
    curses.curs_set(VERY_VISIBLE)
    screen.timeout(50)
    term_dims = TerminalDimensions(screen)
    next_char_index = 0
    while True:
        try:
            term_dims.update(screen)
            draw_screen(screen, next_char_index, term_dims, input_str)
            user_input = screen.getch()
            if user_input == ord(input_str[next_char_index]):
                next_char_index += 1
            else:
                pass
        except KeyboardInterrupt:
            return os.EX_OK


def readlines_from_file_or_stdin(input_filepath):
    if input_filepath:
        try:
            input_file = open(input_filepath, 'r')
        except EnvironmentError:
            raise ExitFailure(os.EX_NOINPUT, '{}: No such file or directory'.format(input_filepath))
        with input_file:
            return input_file.readlines()
    else:
        return sys.stdin.readlines()


def concat_lines(lines):
    words = []
    for line in lines:
        words += line.replace('\t', ' ').rstrip().split()
    return ' '.join(words)


def run(args):
    description = 'A command line words per minute test'
    arg_parser = argparse.ArgumentParser(description=description)
    arg_parser.add_argument('input_filepath', nargs='?')
    args = arg_parser.parse_args()
    if sys.stdin.isatty() and not args.input_filepath:
        arg_parser.print_help()
        return os.EX_USAGE
    lines = readlines_from_file_or_stdin(args.input_filepath)
    input_str = concat_lines(lines)
    return curses.wrapper(run_curses, input_str)


def main():
    def sigterm_handler(signal, frame):
        raise ExitSuccess()
    signal.signal(signal.SIGTERM, sigterm_handler)
    try:
        exit_code = run(sys.argv[1:])
    except ExitSuccess as e:
        exit_code = e.exit_code
    except ExitFailure as e:
        sys.stderr.write(e.msg + '\n')
        exit_code = e.exit_code
    sys.exit(exit_code)


if __name__ == '__main__':
    main()

@@ -552,69 +552,975 @@
 %5D%0A%0A%0A
-def draw_screen(screen, next_char_index, term_dims, input_str
+class TypingProgress:%0A    def __init__(self, input_str):%0A        self.input_str = input_str%0A        self.input_str_len = len(input_str)%0A        self.next_char_index = 0%0A%0A    def check_user_input(self, user_input):%0A        KEY_DELETE = 127%0A        if user_input == KEY_DELETE or user_input == curses.KEY_BACKSPACE:%0A            self.next_char_index = max(0, self.next_char_index - 1)%0A        elif user_input == ord(self.input_str%5Bself.next_char_index%5D):%0A            self.next_char_index = min(self.input_str_len - 1, self.next_char_index + 1)%0A%0A%0Adef get_cursor_position(typing_progress, cols):%0A    chars_left = typing_progress.next_char_index%0A    for row, wrapped_line in enumerate(wrap(typing_progress.input_str, cols)):%0A        chars_left -= len(wrapped_line)%0A        if chars_left %3C 0:%0A            return (row, chars_left + len(wrapped_line))%0A    raise ExitFailure('Cursor position fell off the end of the screen')%0A%0A%0Adef draw_screen(screen, term_dims, typing_progress
 ):%0A 
@@ -1600,16 +1600,32 @@
 in wrap(
+typing_progress.
 input_st
@@ -1765,38 +1765,124 @@
     
-screen.move(0, next_char_index
+cursor_row, cursor_col = get_cursor_position(typing_progress, term_dims.cols)%0A    screen.move(cursor_row, cursor_col
 )%0A  
@@ -2183,35 +2183,59 @@
 en)%0A    
-next_char_index = 0
+typing_progress = TypingProgress(input_str)
 %0A    whi
@@ -2329,45 +2329,34 @@
 en, 
-next_char_index, term_dims, input_str
+term_dims, typing_progress
 )%0A  
@@ -2409,133 +2409,52 @@
     
-if user_input == ord(input_str%5Bnext_char_index%5D):%0A                next_char_index += 1%0A            else:%0A                pass
+typing_progress.check_user_input(user_input)
 %0A   

Add backspace