# Part of flinks
# (C) Martin Bays 2008
# Released under the terms of the GPLv3

import sys, os
from string import *

from time import sleep, time
import re

import signal

import curses
from curses.wrapper import wrapper
#from curses.textpad import Textbox
from .TextboxPad import TextboxPad
import curses.ascii as ascii

import pickle

from .IndexedHypertext import IndexedHypertext
from .portability import addstrwrapped, _cmp

class QuitException(Exception):
    pass

class Browser:
    def __init__(self, scr, config):

        self.settings = config
        self.scr = scr

        self.setGeom()

        for i in range(1,8):
            curses.init_pair(i, i, curses.COLOR_BLACK)
        curses.init_pair(8, curses.COLOR_BLACK, curses.COLOR_BLACK)

        self.scr.timeout(0)

        self.scr.leaveok(1)
        curses.curs_set(0)

        self.delay = 60.0/self.wpm
        self.paused = True

        self.lastSearch = None

        self.indexedHypertext = None

        self.cachedDocuments = []

        # history *ends* with most recent url
        # unHistory *ends* with *current* url
        self.history = []
        self.unHistory = []

        self.tricky = [""]*self.trickyDisplayNum

        self.counting = False
        self.count = 0

        self.skim = False
        self.normalWpm = self.wpm

        self.interruptible = False

        self.urlHistory = []
        self.commandHistory = []
        self.searchHistory = []

        try:
            marksFile = open(os.getenv("HOME")+"/.flinksMarks", 'rb')
            self.globalMarks = pickle.load(marksFile)
        except IOError:
            self.globalMarks = {}

    # XXX: we use self.wpm as convenient shorthand for
    # self.settings.options["wpm"], etc. The overriding below of __getattr__
    # and __setattr__ just implement this.
    def __getattr__(self, name):
        if name != "settings" and name in self.settings.optionNames:
            return self.settings.options[name]
        else:
            raise AttributeError
    def __setattr__(self, name, value):
        if name != "settings" and name in self.settings.optionNames:
            self.settings.options[name] = value
        else:
            self.__dict__[name] = value

    class Geom:
        def __init__(self, scr, desiredLinkDisplayNum, desiredTrickyDisplayNum):
            (maxy, maxx) = scr.getmaxyx()

            # effective maximums, i.e. with anything bigger scr.move(y,x)
            # returns ERR:
            self.maxy = max(0, maxy-2)
            self.maxx = max(0, maxx-1)

            # we aim for following layout, and squash as necessary
            # desiredLinkDisplayNum lines of links
            #
            # wordline
            #
            # desiredTrickyDisplayNum lines of trickies
            # statusline
            # infoline
            # commandline
            squashed = self.maxy < (desiredLinkDisplayNum +
                    desiredTrickyDisplayNum + 6)

            self.wordline = self.maxy//2

            self.linkDisplayNum = min(desiredLinkDisplayNum,
                    self.wordline-1)
            self.trickyDisplayNum = min(desiredTrickyDisplayNum,
                    (self.maxy - self.wordline) - 2)

            self.linkline = self.wordline - self.linkDisplayNum - (not squashed)

            self.trickyline = self.wordline + (self.maxy > self.wordline) + (not squashed)
            self.statusline = self.trickyline + self.trickyDisplayNum
            self.infoline = self.statusline + (self.maxy > self.statusline)
            self.commandline = self.infoline + (self.maxy > self.infoline)

    def setGeom(self):
        self.geom = self.Geom(self.scr, self.linkDisplayNum,
                self.trickyDisplayNum)

    def blockGetKey(self, promptLine=None, prompt=None):
        if promptLine and prompt:
            self.displayCleanCentred(promptLine, prompt)
        self.scr.timeout(-1)
        key = self.scr.getkey()
        self.scr.timeout(0)
        if promptLine and prompt:
            self.clearLine(promptLine)
        if key == '\x1b' or key == '\x07':
            # esc or ^G: cancel
            return None
        return key
    def displayCentred(self, line, str, attr=0):
        addstrwrapped(self.scr, line,
                max(0,(self.geom.maxx-len(str))//2),
                str[:self.geom.maxx],
                attr)
    def clearLine(self, line):
        self.scr.move(line,0)
        self.scr.clrtoeol()
    def displayCleanCentred(self, line, str, attr=0):
        self.clearLine(line)
        self.displayCentred(line, str, attr)
        self.scr.refresh()
    def displayStatus(self):
        if self.geom.statusline == self.geom.wordline: return
        def getStatusLine(abbreviated=False):
            if not abbreviated:
                text = 'WPM: %d' % self.wpm
                if self.showPoint:
                    text += '; word: %d' % (self.indexedHypertext.atWord+1)
                    text += '; sentence: %d' % (self.indexedHypertext.atSentence+1)
                    text += '; paragraph: %d' % (self.indexedHypertext.atPara+1)
                    text += '; link: %d' % (self.indexedHypertext.lastLink+1)
                if self.paused: text += '; PAUSED'
            else:
                text = '%d ' % self.wpm
                if self.showPoint:
                    text += '%d/' % (self.indexedHypertext.atWord+1)
                    text += '%d/' % (self.indexedHypertext.atSentence+1)
                    text += '%d/' % (self.indexedHypertext.atPara+1)
                    text += '%d ' % (self.indexedHypertext.lastLink+1)
                if self.paused: text += 'P'
            return text

        self.clearLine(self.geom.statusline)

        line = getStatusLine(self.abbreviate)
        if len(line) >= self.geom.maxx and not self.abbreviate:
            line = getStatusLine(True)
        if len(line) >= self.geom.maxx:
            return

        self.displayCentred(self.geom.statusline, line)

    def displayLinkListEntry(self, linknum, emph=False):
        if self.geom.linkDisplayNum <= 0: return
        line = self.geom.linkline + linknum%self.geom.linkDisplayNum
        if linknum < 0:
            self.clearLine(line)
            return
        attr = self.attrOfLinknum(linknum)
        if emph:
            attr |= curses.A_BOLD
            emphstr = '*'
        else:
            emphstr = ''
        link = self.indexedHypertext.links[linknum]
        str = '%s%s "%s": %s' % (
                emphstr, self.strOfLinknum(linknum),
                self.abbreviateStr(self.cleanWord(link["word"]), 10),
                self.abbreviateStr(link["url"])
                )
        self.clearLine(line)
        self.displayCentred(line, str, attr)
    def displayLinkList(self):
        for linknum in range(
                self.indexedHypertext.lastLink - self.geom.linkDisplayNum,
                self.indexedHypertext.lastLink+1):
            emphasize = (self.indexedHypertext.lastLink - linknum <
                    self.linkDisplayEmphNum)
            self.displayLinkListEntry(linknum, emphasize)

    def attrOfLinknum(self, linknum):
        if linknum is None or self.geom.linkDisplayNum <= 0: return 1
        c = (linknum%self.geom.linkDisplayNum)%5+1
        if c >= 4: c+=1 # don't use blue (hard to see against black)
        return curses.color_pair(c)

    def strOfLinknum(self, linknum):
        if self.geom.linkDisplayNum <= 0: return
        return '[%d]' % ((linknum%self.geom.linkDisplayNum)+1)

    def displayTrickyList(self):
        for i in range(self.geom.trickyDisplayNum):
            self.clearLine(self.geom.trickyline + i)
            self.displayCentred(
                    self.geom.trickyline + i,
                    self.tricky[i]
                    )

    def addTricky(self, word):
        if self.geom.trickyDisplayNum <= 0:
            return
        for t in self.tricky:
            if word == t:
                # already in the list; don't repeat
                return
        for i in range(self.geom.trickyDisplayNum-1, 0, -1):
            self.tricky[i] = self.tricky[i-1]
        self.tricky[0] = word
        self.displayTrickyList()

    def isTricky(self, word):
        return (
                bool(re.search('\d', word)) or bool(re.search('\w-+\w', word))
                or len(word) > 15
                )

    def cleanWord(self, word):
        return word.strip(' ,.;:?!(){}')

    def abbreviateStr(self, str, maxLen = None):
        if maxLen is None: maxLen = 2*self.geom.maxx//3
        if len(str) <= maxLen: return str
        return '%s...%s' % (str[:(maxLen//2)-3], str[-((maxLen//2)-3):])

    def displayAll(self):
        self.showCurrentWord()
        self.displayStatus()
        self.displayLinkList()
        self.displayTrickyList()

    def applyUrlShortcuts(self, command, depth=0):
        words = command.split()
        try:
            numArgs, shortcut = self.settings.urlShortcuts[words[0]]
        except (IndexError, KeyError):
            return command

        for arg in range(numArgs):
            i = arg+1
            try:
                if i == numArgs:
                    # last argument gets all remaining words
                    repl = ''.join(words[i:])
                else:
                    repl = words[i]
            except IndexError:
                return shortcut

            shortcut = re.sub('%%%d' % i, repl, shortcut)

        shortcut = re.sub('%c', self.indexedHypertext.url, shortcut)
        shortcut = re.sub('\\$HOME', os.getenv("HOME"), shortcut)
        # apply recursively
        if depth < 10:
            return self.applyUrlShortcuts(shortcut, depth+1)
        else:
            return shortcut

    def go(self, command):
        url = self.applyUrlShortcuts(command)
        self.gotoNewUrl(url)

    def gotoNewUrl(self, url):
        if self.indexedHypertext:
            self.history.append(self.indexedHypertext.url)
        self.gotoUrl(url)
        # Turns out to be neater to have the current document in unHistory:
        self.unHistory = [url]
    def goHistory(self, num=1):
        try:
            self.gotoUrl(self.history[-num])
            for i in range(num):
                self.unHistory.append(self.history.pop())
        except IndexError:
            return None
    def goUnHistory(self, num=1):
        try:
            self.gotoUrl(self.unHistory[-num-1])
            for i in range(num):
                self.history.append(self.unHistory.pop())
        except IndexError:
            return None

    def gotoUrl(self, url, noCache=False):
        shownError = False
        try:
            m = re.match('(.*)#(.*)$', url)
            if m:
                baseUrl, anchor = m.groups()
            else:
                baseUrl, anchor = url, None
            cached = self.findCachedDocument(baseUrl)
            if cached and not noCache:
                self.indexedHypertext = cached
            else:
                self.displayCleanCentred(self.geom.commandline, self.abbreviateStr("Loading %s" % url))
                self.scr.refresh()

                self.interruptible = True
                self.indexedHypertext = IndexedHypertext(baseUrl)
                self.interruptible = False

            if self.indexedHypertext.isEmpty():
                err = self.indexedHypertext.lynxErr.strip()
                line = ';'.join(err.splitlines()[0:2])
                self.displayCleanCentred(self.geom.commandline,
                        self.abbreviateStr("Failed: %s" % line))
                shownError = True
                return False

            if anchor:
                self.interruptible = True
                n = self.indexedHypertext.findAnchor(anchor)
                self.interruptible = False

                if n is not None:
                    self.indexedHypertext.seekWord(n, setMark=True)
                else:
                    self.displayCleanCentred(self.geom.commandline,
                    self.abbreviateStr("No such anchor: %s" % anchor))
                    shownError = True

            self.displayCleanCentred(self.geom.infoline, self.abbreviateStr("%s" % baseUrl))
            self.displayAll()

            if not noCache: self.cacheDocument(self.indexedHypertext)

        finally:
            self.interruptible = False
            if not shownError:
                self.clearLine(self.geom.commandline)

    def cacheDocument(self, doc):
        # remove any old cachings of this url:
        for (u, doc) in self.cachedDocuments:
            if u == doc.url:
                self.cachedDocuments.remove((u, doc))
        self.cachedDocuments.append((doc.url, doc))
        if len(self.cachedDocuments) > self.maxCached:
            self.cachedDocuments.pop(0)
    def findCachedDocument(self, url):
        for (u, doc) in self.cachedDocuments:
            if u == url: return doc
        return None

    def showCurrentWord(self, word=None):
        if word is None:
            word = self.indexedHypertext.currentWord()

        tricky = self.isTricky(word)
        linknum = self.indexedHypertext.currentLink()

        if linknum is None:
            if tricky: attr = curses.A_BOLD
            else: attr = 0
            self.displayCleanCentred(self.geom.wordline, word, attr)
        else:
            self.displayCleanCentred(self.geom.wordline, word,
                    self.attrOfLinknum(linknum) | curses.A_BOLD)

            self.displayLinkListEntry(linknum, True)

            if linknum >= self.linkDisplayEmphNum:
                self.displayLinkListEntry(linknum-self.linkDisplayEmphNum,
                        False)

        if tricky:
            self.addTricky(self.cleanWord(word))

        if self.showContext and self.paused:
            contextBefore = ''
            contextFrom = self.indexedHypertext.atWord
            while True:
                w = self.indexedHypertext.getWord(contextFrom-1)
                if self.indexedHypertext.atSentenceEnd(contextFrom-1):
                    w += '  '
                if self.indexedHypertext.atParaEnd(contextFrom-1):
                    w += '  '
                if w and (len(w) + len(contextBefore)
                        + len(word)//2 + 1) < self.geom.maxx//3:
                    contextBefore = w + ' ' + contextBefore
                    contextFrom -= 1
                else:
                    break

            contextAfter = ''
            contextTo = self.indexedHypertext.atWord
            while True:
                w = ''
                if self.indexedHypertext.atSentenceEnd(contextTo):
                    w += '  '
                if self.indexedHypertext.atParaEnd(contextTo):
                    w += '  '
                w += self.indexedHypertext.getWord(contextTo+1)
                if w and (len(w) + len(contextAfter)
                        + len(word)//2 + 1) < self.geom.maxx//3:
                    contextAfter += ' ' + w
                    contextTo += 1
                else:
                    break

            point = (self.geom.maxx - len(word))//2 - len(contextBefore)
            attr = curses.color_pair(8) | curses.A_BOLD

            addstrwrapped(self.scr, self.geom.wordline, point, contextBefore, attr)
            point = (self.geom.maxx + len(word))//2
            addstrwrapped(self.scr, self.geom.wordline, point, contextAfter, attr)

        if self.showPoint: self.displayStatus()

    def mainLoop(self):
        def sigHandlerQuit(sig, frame):
            raise QuitException
        signal.signal(15, sigHandlerQuit)

        def sigHandlerInt(sig, frame):
            # Occasionally, we can safely allow ^C to interrupt a section of
            # code.
            if self.interruptible:
                raise KeyboardInterrupt
        signal.signal(2, sigHandlerInt)

        def applyCount(method, args):
            # update args, replacing/appending any count argument
            try:
                countarg = self.bindableMethods[method][2]
                if countarg > 0:
                    if len(args) >= countarg:

                        currentArg = args[countarg-1]
                        if (currentArg.__class__ == int and
                                currentArg < 0):
                            self.count = -self.count

                        args[countarg-1] = self.count

                    elif len(args)+1 == countarg:
                        args.append(count)
            except IndexError:
                pass
            endCount()
        def endCount():
            self.counting = False
            self.countWin.erase()
            self.countWin.refresh()
            del self.countWin

        def handleResize():
            self.setGeom()
            self.scr.erase()
            self.scr.refresh()
            self.displayAll()

        self.go(self.initialUrl)
        self.displayStatus()

        self.blankSpace = 0

        def flashWord():
            # handle blanking
            if self.blankSpace > 0:
                self.blankSpace -= 1
                self.displayCleanCentred(self.geom.wordline, "")
                return

            word = self.indexedHypertext.readWord()

            if word is not None:
                if self.indexedHypertext.atParaEnd():
                    self.blankSpace = self.paraBlankSpace
                elif self.indexedHypertext.atSentenceEnd():
                    self.blankSpace = self.sentenceBlankSpace
                else:
                    def isPause(word):
                        try:
                            if word[-1] in "'\"":
                                last = word[-2]
                            else:
                                last = word[-1]
                            return last in ",;:-"
                        except IndexError:
                            return False
                    if isPause(word):
                        self.blankSpace = self.pauseBlankSpace

            self.showCurrentWord(word)
            return
        def handleInput():
            c = None
            escBound = 27 in self.settings.keyBindings
            metaBit = False
            while c != -1:
                c = self.scr.getch()

                if not escBound:
                    if c == 27:
                        metaBit = True
                        continue
                    elif metaBit:
                        if c == -1:
                            c = 27
                        else:
                            # set high bit
                            c |= 0x80
                        metaBit = False

                if c == curses.KEY_RESIZE:
                    handleResize()

                elif self.counting and ord('0') <= c <= ord('9'):
                    self.count = 10*self.count + c - ord('0')
                    self.countWin.addch(chr(c))
                    self.countWin.refresh()

                else:
                    binding = self.settings.keyBindings.get(c)
                    if binding is not None:
                        method = binding[0]
                        args = binding[1][:]

                        if self.counting:
                            applyCount(method, args)

                        getattr(self, 'doKey'+method).__call__(*args)
                    elif self.counting and c in [27,7]:
                        # esc or ^G: cancel count
                        endCount()

        
        minStep = 0.2
        ERRTimeout = 0
        class Event:
            def __init__(self, type, time):
                if type not in ["word", "blank"]:
                    raise "BUG: unknown Event type"
                self.type = type
                self.time = time
        nextEvent = Event("word", time())
        if self.blink: nextBlinkTime = time() + self.blinkDelay
        # main loop:
        while True:
            try:
                if ERRTimeout > 0:
                    ERRTimeout -= 1
                try:
                    while True:
                        wait = nextEvent.time - time()
                        if wait < 0: break
                        sleep(min(wait, minStep))
                        beforeInput = time()

                        try:
                            handleInput()
                        except KeyboardInterrupt:
                            pass

                        inputTime = time() - beforeInput
                        nextEvent.time += inputTime
                        if self.blink: nextBlinkTime += inputTime

                    if self.paused or self.indexedHypertext.atEnd():
                        nextEvent = Event("word",
                                time() + self.delay)
                        if self.blink:
                            nextBlinkTime = time() + self.blinkDelay
                        continue

                    if nextEvent.type == "word":
                        if self.blink and nextBlinkTime < time() and (
                                not self.blinkWaitSentence or
                                self.indexedHypertext.atSentenceEnd()):
                            self.displayCleanCentred(self.geom.wordline,
                                    "{*blink*}")
                            nextBlinkTime = time() + self.blinkDelay
                            waitTime = self.blinkTime
                        else:
                            flashWord()
                            waitTime = self.delay
                        if self.blankBetween: 
                            nextEvent = Event("blank",
                                    time() + waitTime - self.delay/3)
                        else:
                            nextEvent = Event("word",
                                    time() + waitTime)
                    elif nextEvent.type == "blank":
                        self.clearLine(self.geom.wordline)
                        self.scr.refresh()
                        nextEvent = Event("word",
                                time() + self.delay/3)

                except curses.error:
                    if ERRTimeout == 0:
                        # Curses threw an error, but that might just be because we
                        # need to resize and haven't yet processed KEY_RESIZE.
                        # So we resize now, and see if we get another curses error
                        # within the next couple of goes round the main loop.
                        self.setGeom()
                        ERRTimeout = 2
                    else:
                        raise "Curses returned ERR - terminal too short?"
            except KeyboardInterrupt:
                pass

    def onExit(self):
        # emulating vim - global marks '0'-'9' give positions on last 10 exits
        for i in range(9,0,-1):
            try: self.globalMarks[str(i)] = self.globalMarks[str(i-1)][:]
            except KeyError: pass
        self.globalMarks['0'] = \
                [self.indexedHypertext.url, self.indexedHypertext.atWord]

    def writeState(self):
        try:
            marksFile = open(os.getenv("HOME")+"/.flinksMarks", 'wb')
            pickle.dump(self.globalMarks, marksFile)
        except IOError:
            pass

    bindableMethods = {
            # <name> : [<min args>, <max args>, [<count arg>]]
            'ChangeSpeed' : [1,1,1],
            'SeekSentence' : [0,1,1],
            'SeekParagraph' : [0,1,1],
            'SeekWord' : [0,1,1],
            'SeekLink' : [0,1,1],
            'SeekStart' : [0,0],
            'SeekEnd' : [0,0],
            'History' : [0,1,1],
            'FollowLink' : [1,1,1],
            'Go' : [0,0],
            'GoCurrent' : [0,0],
            'Search' : [0,1,1],
            'SearchAgain' : [0,1,1],
            'Count' : [0,0],
            'Mark' : [0,0],
            'GoMark' : [0,0],
            'Reload' : [0,0],
            'Pause' : [0,0],
            'Command' : [0,0],
            'Skim' : [0,0],
            'Refresh' : [0,0],
            'Help' : [0,0],
            'Quit' : [0,0],
            }

    def doKeyChangeSpeed(self, amount):
        self.wpm += amount
        if self.wpm < 30: self.wpm = 30
        self.delay = 60.0/self.wpm
        self.displayStatus()

    def doKeySeekSentence(self, n=1):
        self.indexedHypertext.seekSentenceRelative(n)
        self.displayAll()
    def doKeySeekParagraph(self, n=1):
        self.indexedHypertext.seekParaRelative(n)
        self.displayAll()
    def doKeySeekWord(self, n=1):
        self.indexedHypertext.seekWordRelative(n)
        self.displayAll()
    def doKeySeekLink(self, n=1):
        self.indexedHypertext.seekLinkRelative(n)
        self.displayAll()

    def doKeySeekStart(self):
        self.indexedHypertext.seekWord(0, setMark=True)
        self.displayAll()
    def doKeySeekEnd(self):
        self.indexedHypertext.seekEnd()
        self.displayAll()

    def doKeyFollowLink(self, n):
        if self.geom.linkDisplayNum > 0:
            n -= 1
            nl = self.indexedHypertext.lastLink % self.geom.linkDisplayNum
        else:
            nl = self.indexedHypertext.lastLink

        if n <= nl:
            linknum = self.indexedHypertext.lastLink + (n-nl)
        else:
            linknum = self.indexedHypertext.lastLink - self.geom.linkDisplayNum + (n-nl)
        try:
            url = self.indexedHypertext.links[linknum]["url"]
            self.gotoNewUrl(url)
        except IndexError:
            pass

    def readLine(self, prompt, initial='', shortPrompt=None, history=None,
            getCompletions=None):
        if self.abbreviate or self.geom.maxx <= 10:
            if shortPrompt is not None:
                prompt = shortPrompt
            else:
                prompt = prompt[0]+":"

        promptX = max(1, self.geom.maxx//6 - len(prompt))
        boxX = promptX + len(prompt)
        boxLen = min(self.geom.maxx - boxX - 1, 2*self.geom.maxx//3)

        addstrwrapped(self.scr, self.geom.commandline, promptX,
                prompt[:self.geom.maxx-5])
        self.scr.refresh()

        curses.curs_set(1)

        pad = curses.newpad(1, 200)
        pad.scrollok(1)
        box = TextboxPad(pad,
                self.geom.commandline, boxX,
                self.geom.commandline, boxX + boxLen - 1,
                history=history,
                getCompletions=getCompletions)

        for char in initial:
            box.do_command(char)

        class cancelReadline(Exception):
            pass
        def cancelValidate(ch):
            if ch == ascii.BEL or ch == 27:
                # ^G and escape cancel the readLine
                raise cancelReadline
            return ch
        try:
            try:
                read = box.edit(cancelValidate)
                if history is not None:
                    history.append(read.strip())
                return read
            except cancelReadline:
                return None
        finally:
            curses.curs_set(0)
            self.clearLine(self.geom.commandline)

    def doKeyGo(self):
        def filenameCompletion(partial):
            from glob import glob
            from os.path import isdir

            completions = glob(partial+'*')

            for i in range(len(completions)):
                if isdir(completions[i]):
                    completions[i] += '/'

            return completions

        command = self.readLine("Go: ", history=self.urlHistory,
                getCompletions=filenameCompletion)
        if command:
            self.go(command.strip())

    def doKeyGoCurrent(self):
        command = self.readLine("Go: ", self.indexedHypertext.url, history=self.urlHistory)
        if command:
            self.go(command.strip())

    def charOfSearchDir(self, dir=1):
        if dir == 1: return '/'
        else: return '?'
    def search(self, pattern, n=1):
        dir = _cmp(n,0)
        self.displayCleanCentred(self.geom.commandline,
                self.charOfSearchDir(dir)+pattern)
        try:
            for i in range(abs(n)):
                ret = self.indexedHypertext.search(pattern.strip(), dir)
                if not ret: break
            self.displayAll()
        finally:
            self.clearLine(self.geom.commandline)
    def doKeySearch(self, n=1):
        dir = _cmp(n,0)
        pattern = self.readLine("Search: ",
            shortPrompt = self.charOfSearchDir(dir),
            history=self.searchHistory)
        if pattern:
            self.search(pattern, n)
            self.lastSearch = {"pattern": pattern, "dir": dir}
    def doKeySearchAgain(self, n=1):
        if self.lastSearch:
            self.search(self.lastSearch["pattern"],
                    self.lastSearch["dir"]*n)

    def doKeyPause(self):
        self.paused = not self.paused
        self.displayStatus()
        if self.showContext:
            self.showCurrentWord()

    def doKeyHistory(self, n=-1):
        if n < 0:
            self.goHistory(-n)
        else:
            self.goUnHistory(n)

    def doKeyCount(self):
        self.counting = True
        self.count = 0

        boxX = self.geom.maxx//6
        boxLen = min(self.geom.maxx - boxX - 1, 2*self.geom.maxx//3)
        self.countWin = curses.newwin(1, boxLen, self.geom.commandline, boxX)
        if not self.abbreviate:
            prompt = "Count: "
        else:
            prompt = "C"
        addstrwrapped(self.countWin, None, None, prompt[:self.geom.maxx])
        self.countWin.refresh()

    def doKeyMark(self):
        key = self.blockGetKey(self.geom.commandline,
                self.abbreviate and "m" or "Set mark:")
        if key is None or len(key) != 1:
            return
        if 'a' <= key <= 'z':
            # local mark
            self.indexedHypertext.mark(key)
        elif 'A' <= key <= 'Z':
            # global marks
            self.globalMarks[key] =\
                [self.indexedHypertext.url, self.indexedHypertext.atWord]
    def doKeyGoMark(self):
        key = self.blockGetKey(self.geom.commandline,
                self.abbreviate and "'" or "Go to mark:")
        if key is None or len(key) != 1:
            return
        if 'a' <= key <= 'z' or key == '\'':
            # local mark
            self.indexedHypertext.goMark(key)
            self.displayAll()
        elif key in self.globalMarks:
            # global marks
            url, pos = self.globalMarks[key]
            if url != self.indexedHypertext.url:
                self.gotoNewUrl(url)
            self.indexedHypertext.seekWord(pos, setMark=True)
            self.displayAll()

    def doKeyReload(self):
        n = self.indexedHypertext.atWord
        self.gotoUrl(self.indexedHypertext.url, True)
        self.indexedHypertext.seekWord(n)
        self.displayAll()

    def doKeyCommand(self):
        def handleErr(err):
            self.displayCleanCentred(self.geom.commandline, err)
        command = self.readLine(':', shortPrompt=':',
                history=self.commandHistory)
        if command:
            self.settings.processCommand(command, handleErr)
        self.displayAll()

    def doKeySkim(self):
        if not self.skim:
            self.normalWpm = self.wpm
            self.wpm = self.skimWpm
            self.skim = True
        else:
            self.skimWpm = self.wpm
            self.wpm = self.normalWpm
            self.skim = False
        self.delay = 60.0/self.wpm
        self.displayAll()

    def doKeyRefresh(self):
        self.scr.redrawwin()

    def doKeyHelp(self):
        self.go("special:README")

    def doKeyQuit(self):
        raise QuitException
