diff --git a/tt.go b/tt.go
index bb533df..ce84c49 100644
--- a/tt.go
+++ b/tt.go
@@ -9,6 +9,7 @@ import (
 	"path/filepath"
 	"regexp"
 	"strings"
+	"time"
 
 	"github.com/gdamore/tcell"
 	"github.com/mattn/go-isatty"
@@ -57,10 +58,10 @@ func exit() {
 }
 
 func showReport(scr tcell.Screen, cpm, wpm int, accuracy float64) {
-	report := fmt.Sprintf("WPM: %d\nCPM: %d\nAccuracy: %.2f%%\n", wpm, cpm, accuracy)
+	report := fmt.Sprintf("WPM: %d\nCPM: %d\nAccuracy: %.2f%%", wpm, cpm, accuracy)
 
 	scr.Clear()
-	drawCellsAtCenter(scr, stringToCells(report), -1)
+	drawStringAtCenter(scr, report, tcell.StyleDefault)
 	scr.HideCursor()
 	scr.Show()
 
@@ -79,15 +80,17 @@ func main() {
 	var oneShotMode bool
 	var wrapSz int
 	var noSkip bool
+	var timeout int
 	var err error
 
 	flag.IntVar(&n, "n", 50, "The number of random words which constitute the test.")
-	flag.IntVar(&wrapSz, "w", 80, "Wraps the input text at the given number of columns (ignored if -raw is present)")
+	flag.IntVar(&wrapSz, "w", 80, "Wraps the input text at the given number of columns (ignored if -raw is present).")
+	flag.IntVar(&timeout, "t", -1, "Terminate the test after the given number of seconds.")
 
 	flag.BoolVar(&noSkip, "noskip", false, "Disable word skipping when space is pressed.")
 	flag.BoolVar(&csvMode, "csv", false, "Print the test results to stdout in the form <wpm>,<cpm>,<accuracy>.")
 	flag.BoolVar(&rawMode, "raw", false, "Don't reflow text or show one paragraph at a time.")
-	flag.BoolVar(&oneShotMode, "o", false, "Automatically exit after a single run.")
+	flag.BoolVar(&oneShotMode, "o", false, "Automatically exit after a single run (useful for scripts).")
 
 	flag.Usage = func() {
 		fmt.Println(`Usage: tt [options]
@@ -177,10 +180,12 @@ Options:`)
 	if noSkip {
 		typer.SkipWord = false
 	}
+	if timeout != -1 {
+		timeout *= 1E9
+	}
 
 	for {
-		scr.Clear()
-		nerrs, ncorrect, t, exitKey := typer.Start(contentFn())
+		nerrs, ncorrect, t, exitKey := typer.Start(contentFn(), time.Duration(timeout))
 
 		switch exitKey {
 		case 0:
diff --git a/typer.go b/typer.go
index 188adee..e1fbd09 100644
--- a/typer.go
+++ b/typer.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"fmt"
+	"strconv"
 	"time"
 
 	"github.com/gdamore/tcell"
@@ -51,39 +52,49 @@ func (t *typer) highlight(text []cell, idx int, currentWordStyle, nextWordStyle
 	}
 }
 
-func (t *typer) Start(text []string) (nerrs, ncorrect int, tim time.Duration, exitKey tcell.Key) {
-	var startTime time.Time
+func (t *typer) Start(text []string, timeout time.Duration) (nerrs, ncorrect int, duration time.Duration, exitKey tcell.Key) {
+	timeLeft := timeout
 
 	for i, p := range text {
+		startImmediately := true
+		var d time.Duration
 		var e, c int
-		var fn func() = nil
 
 		if i == 0 {
-			fn = func() {
-				startTime = time.Now()
-			}
+			startImmediately = false
 		}
 
-		e, c, exitKey = t.start(p, fn)
+		e, c, exitKey, d = t.start(p, timeLeft, startImmediately)
 
 		nerrs += e
 		ncorrect += c
+		duration += d
+
+		if timeout != -1 {
+			timeLeft -= d
+			if timeLeft <= 0 {
+				return
+			}
+		}
 
 		if exitKey != 0 {
-			tim = time.Now().Sub(startTime)
 			return
 		}
 	}
 
-	tim = time.Now().Sub(startTime)
-	exitKey = 0
-
 	return
 }
 
-func (t *typer) start(s string, onStart func()) (nerrs int, ncorrect int, exitKey tcell.Key) {
-	started := false
+func (t *typer) start(s string, timeLimit time.Duration, startImmediately bool) (nerrs int, ncorrect int, exitKey tcell.Key, duration time.Duration) {
+
+	var startTime time.Time
 	text := stringToCells(s)
+
+	nc, nr := calcStringDimensions(s)
+	sw, sh := scr.Size()
+	x := (sw - nc) / 2
+	y := (sh - nr) / 2
+
 	for i, _ := range text {
 		text[i].style = t.backgroundStyle
 	}
@@ -98,11 +109,33 @@ func (t *typer) start(s string, onStart func()) (nerrs int, ncorrect int, exitKe
 	idx := 0
 
 	redraw := func() {
+		if timeLimit != -1 && !startTime.IsZero() {
+			remaining := timeLimit - time.Now().Sub(startTime)
+			drawString(t.Scr, x+nc/2, y+nr+1, strconv.Itoa(int(remaining/1E9)), -1, t.backgroundStyle)
+		}
+
 		//Potentially inefficient, but seems to be good enough
-		drawCellsAtCenter(t.Scr, text, idx)
+		drawCells(t.Scr, x, y, text, idx)
+
 		t.Scr.Show()
 	}
 
+	calcStats := func() {
+		nerrs = 0
+		ncorrect = 0
+
+		for _, c := range text {
+			if c.style == t.incorrectStyle || c.style == t.incorrectSpaceStyle {
+				nerrs++
+			} else if c.style == t.correctStyle {
+				ncorrect++
+			}
+		}
+
+		exitKey = 0
+		duration = time.Now().Sub(startTime)
+	}
+
 	deleteWord := func() {
 		t.highlight(text, idx, t.backgroundStyle, t.backgroundStyle)
 
@@ -129,6 +162,30 @@ func (t *typer) start(s string, onStart func()) (nerrs int, ncorrect int, exitKe
 		t.highlight(text, idx, t.currentWordStyle, t.nextWordStyle)
 	}
 
+	tickerCloser := make(chan bool)
+
+	//Inject nil events into the main event loop at regular invervals to force an update
+	ticker := func() {
+		for {
+			select {
+			case <-tickerCloser:
+				return
+			default:
+			}
+
+			time.Sleep(time.Duration(1E8))
+			t.Scr.PostEventWait(nil)
+		}
+	}
+
+	go ticker()
+	defer close(tickerCloser)
+
+	if startImmediately {
+		startTime = time.Now()
+	}
+
+	t.Scr.Clear()
 	for {
 		t.highlight(text, idx, t.currentWordStyle, t.nextWordStyle)
 		redraw()
@@ -139,13 +196,14 @@ func (t *typer) start(s string, onStart func()) (nerrs int, ncorrect int, exitKe
 		case *tcell.EventResize:
 			t.Scr.Sync()
 			t.Scr.Clear()
-		case *tcell.EventKey:
-			if !started {
-				if onStart != nil {
-					onStart()
-				}
 
-				started = true
+			nc, nr = calcStringDimensions(s)
+			sw, sh = scr.Size()
+			x = (sw - nc) / 2
+			y = (sh - nr) / 2
+		case *tcell.EventKey:
+			if startTime.IsZero() {
+				startTime = time.Now()
 			}
 
 			switch key := ev.Key(); key {
@@ -218,21 +276,17 @@ func (t *typer) start(s string, onStart func()) (nerrs int, ncorrect int, exitKe
 				}
 
 				if idx == len(text) {
-					nerrs = 0
-
-					for _, c := range text {
-						if c.style == t.incorrectStyle || c.style == t.incorrectSpaceStyle {
-							nerrs++
-						}
-					}
-
-					ncorrect = len(text) - nerrs
-					exitKey = 0
-
-					t.Scr.Clear()
+					calcStats()
 					return
 				}
 			}
+		default: //tick
+			if timeLimit != -1 && !startTime.IsZero() && timeLimit <= time.Now().Sub(startTime) {
+				calcStats()
+				return
+			}
+
+			redraw()
 		}
 	}
 }
diff --git a/util.go b/util.go
index 84a6d56..08f61e8 100644
--- a/util.go
+++ b/util.go
@@ -280,6 +280,28 @@ func stringToCells(s string) []cell {
 	return a[:len]
 }
 
+func drawString(scr tcell.Screen, x, y int, s string, cursorIdx int, style tcell.Style) {
+	sx := x
+
+	for i, c := range s {
+		if c == '\n' {
+			y++
+			x = sx
+		} else {
+			scr.SetContent(x, y, c, nil, style)
+			if i == cursorIdx {
+				scr.ShowCursor(x, y)
+			}
+
+			x++
+		}
+	}
+
+	if cursorIdx == len(s) {
+		scr.ShowCursor(x, y)
+	}
+}
+
 func drawCells(scr tcell.Screen, x, y int, s []cell, cursorIdx int) {
 	sx := x
 
@@ -302,16 +324,24 @@ func drawCells(scr tcell.Screen, x, y int, s []cell, cursorIdx int) {
 	}
 }
 
-func drawCellsAtCenter(scr tcell.Screen, s []cell, cursorIdx int) {
-	rows := 0
-	cols := 0
+func drawStringAtCenter(scr tcell.Screen, s string, style tcell.Style) {
+	nc, nr := calcStringDimensions(s)
+	sw, sh := scr.Size()
+
+	x := (sw - nc) / 2
+	y := (sh - nr) / 2
+
+	drawString(scr, x, y, s, -1, style)
+}
+
+func calcStringDimensions(s string) (nc, nr int) {
 	c := 0
 
 	for _, x := range s {
-		if x.c == '\n' {
-			rows++
-			if c > cols {
-				cols = c
+		if x == '\n' {
+			nr++
+			if c > nc {
+				nc = c
 			}
 			c = 0
 		} else {
@@ -319,16 +349,12 @@ func drawCellsAtCenter(scr tcell.Screen, s []cell, cursorIdx int) {
 		}
 	}
 
-	rows++
-	if c > cols {
-		cols = c
+	nr++
+	if c > nc {
+		nc = c
 	}
 
-	w, h := scr.Size()
-	x := (w - cols) / 2
-	y := (h - rows) / 2
-
-	drawCells(scr, x, y, s, cursorIdx)
+	return
 }
 
 func newTcellColor(s string) tcell.Color {