use different voice for quotations
[flinks.git] / flinkspkg / Browser.py
blob3d3450130e8eeed47fc0e7da4880a25c6574b3d6
1 # Part of flinks
2 # (C) Martin Bays 2008
3 # Released under the terms of the GPLv3
5 from __future__ import unicode_literals
7 import sys, os
8 from string import *
10 from time import sleep, time
11 import re
13 import signal
15 import curses
16 from curses.wrapper import wrapper
17 #from curses.textpad import Textbox
18 from .TextboxPad import TextboxPad
19 import curses.ascii as ascii
21 from .IndexedHypertext import IndexedHypertext
22 from .portability import addstrwrapped, _cmp
24 class QuitException(Exception):
25 pass
27 class Browser:
28 def __init__(self, scr, config):
29 self.settings = config
30 self.scr = scr
32 self.setGeom()
34 for i in range(1,8):
35 curses.init_pair(i, i, curses.COLOR_BLACK)
36 curses.init_pair(8, curses.COLOR_BLACK, curses.COLOR_BLACK)
38 self.scr.timeout(0)
40 self.scr.leaveok(1)
41 curses.curs_set(0)
43 self.paused = True
45 self.lastSearch = None
47 self.itext = None
49 self.cachedDocuments = []
51 # history *ends* with most recent url
52 # unHistory *ends* with *current* url
53 self.history = []
54 self.unHistory = []
56 self.tricky = [""]*self.trickyDisplayNum
58 self.counting = False
59 self.count = 0
61 self.skim = False
62 self.normalWpm = self.wpm
64 self.interruptible = False
66 self.urlHistory = []
67 self.commandHistory = []
68 self.searchHistory = []
70 self.speechsub = None
71 self.spokeTo = -1
73 self.globalMarks = self.readMarksFile()
75 def readMarksFile(self):
76 try:
77 marksFile = open(os.getenv("HOME")+"/.flinksMarks", 'r')
78 except IOError:
79 return {}
80 marks = {}
81 while True:
82 m = re.match('(.)\s*:?\s*', marksFile.readline())
83 if not m: break
84 mark = m.groups()[0]
85 url = marksFile.readline().strip()
86 try: point = int(marksFile.readline().strip())
87 except: break
88 marks[mark] = [url, point]
89 return marks
91 def writeMarksFile(self, marks):
92 marksFile = open(os.getenv("HOME")+"/.flinksMarks", 'w')
93 for (mark,(url,point)) in marks.items():
94 marksFile.write('%s:\n\t%s\n\t%d\n' % (mark, url, point))
96 # XXX: we use self.wpm as convenient shorthand for
97 # self.settings.options["wpm"], etc. The overriding below of __getattr__
98 # and __setattr__ just implement this.
99 def __getattr__(self, name):
100 if name != "settings" and name in self.settings.optionNames:
101 return self.settings.options[name]
102 else:
103 raise AttributeError
104 def __setattr__(self, name, value):
105 if name != "settings" and name in self.settings.optionNames:
106 self.settings.options[name] = value
107 else:
108 self.__dict__[name] = value
110 class Geom:
111 def __init__(self, scr, desiredLinkDisplayNum, desiredTrickyDisplayNum):
112 (maxy, maxx) = scr.getmaxyx()
114 # effective maximums, i.e. with anything bigger scr.move(y,x)
115 # returns ERR:
116 self.maxy = max(0, maxy-2)
117 self.maxx = max(0, maxx-1)
119 # we aim for following layout, and squash as necessary
120 # desiredLinkDisplayNum lines of links
122 # wordline
124 # desiredTrickyDisplayNum lines of trickies
125 # statusline
126 # infoline
127 # commandline
128 squashed = self.maxy < (desiredLinkDisplayNum +
129 desiredTrickyDisplayNum + 6)
131 self.wordline = self.maxy//2
133 self.linkDisplayNum = min(desiredLinkDisplayNum,
134 self.wordline-1)
135 self.trickyDisplayNum = min(desiredTrickyDisplayNum,
136 (self.maxy - self.wordline) - 2)
138 self.linkline = self.wordline - self.linkDisplayNum - (not squashed)
140 self.trickyline = self.wordline + (self.maxy > self.wordline) + (not squashed)
141 self.statusline = self.trickyline + self.trickyDisplayNum
142 self.infoline = self.statusline + (self.maxy > self.statusline)
143 self.commandline = self.infoline + (self.maxy > self.infoline)
145 def setGeom(self):
146 self.geom = self.Geom(self.scr, self.linkDisplayNum,
147 self.trickyDisplayNum)
149 def blockGetKey(self, promptLine=None, prompt=None):
150 if promptLine and prompt:
151 self.displayCleanCentred(promptLine, prompt)
152 self.scr.timeout(-1)
153 key = self.scr.getkey()
154 self.scr.timeout(0)
155 if promptLine and prompt:
156 self.clearLine(promptLine)
157 if key == '\x1b' or key == '\x07':
158 # esc or ^G: cancel
159 return None
160 return key
161 def displayCentred(self, line, str, attr=0):
162 addstrwrapped(self.scr, line,
163 max(0,(self.geom.maxx-len(str))//2),
164 str[:self.geom.maxx],
165 attr)
166 def clearLine(self, line):
167 self.scr.move(line,0)
168 self.scr.clrtoeol()
169 def displayCleanCentred(self, line, str, attr=0):
170 self.clearLine(line)
171 self.displayCentred(line, str, attr)
172 self.scr.refresh()
173 def displayStatus(self):
174 if self.geom.statusline == self.geom.wordline: return
175 def getStatusLine(abbreviated=False):
176 if not abbreviated:
177 text = 'WPM: %d' % self.wpm
178 if self.showPoint:
179 text += '; word: %d' % (self.itext.atWord+1)
180 text += '; sentence: %d' % (self.itext.sentenceIndex.at+1)
181 text += '; paragraph: %d' % (self.itext.paraIndex.at+1)
182 text += '; link: %d' % (self.itext.linkIndex.at+1)
183 if self.paused: text += '; PAUSED'
184 else:
185 text = '%d ' % self.wpm
186 if self.showPoint:
187 text += '%d/' % (self.itext.atWord+1)
188 text += '%d/' % (self.itext.sentenceIndex.at+1)
189 text += '%d/' % (self.itext.paraIndex.at+1)
190 text += '%d ' % (self.itext.linkIndex.at+1)
191 if self.paused: text += 'P'
192 return text
194 self.clearLine(self.geom.statusline)
196 line = getStatusLine(self.abbreviate)
197 if len(line) >= self.geom.maxx and not self.abbreviate:
198 line = getStatusLine(True)
199 if len(line) >= self.geom.maxx:
200 return
202 self.displayCentred(self.geom.statusline, line)
204 def displayLinkListEntry(self, linknum, emph=False):
205 if self.geom.linkDisplayNum <= 0: return
206 line = self.geom.linkline + linknum%self.geom.linkDisplayNum
207 if linknum < 0:
208 self.clearLine(line)
209 return
210 attr = self.attrOfLinknum(linknum)
211 if emph:
212 attr |= curses.A_BOLD
213 emphstr = '*'
214 else:
215 emphstr = ''
216 link = self.itext.links[linknum]
217 str = '%s%s "%s": %s' % (
218 emphstr, self.strOfLinknum(linknum),
219 self.abbreviateStr(self.cleanWord(link["word"]), 10),
220 self.abbreviateStr(link["url"])
222 self.clearLine(line)
223 self.displayCentred(line, str, attr)
224 def displayLinkList(self):
225 for linknum in range(
226 self.itext.linkIndex.at - self.geom.linkDisplayNum,
227 self.itext.linkIndex.at+1):
228 emphasize = (self.itext.linkIndex.at - linknum <
229 self.linkDisplayEmphNum)
230 self.displayLinkListEntry(linknum, emphasize)
232 def attrOfLinknum(self, linknum):
233 if linknum is None or self.geom.linkDisplayNum <= 0: return 1
234 c = (linknum%self.geom.linkDisplayNum)%5+1
235 if c >= 4: c+=1 # don't use blue (hard to see against black)
236 return curses.color_pair(c)
238 def strOfLinknum(self, linknum):
239 if self.geom.linkDisplayNum <= 0: return
240 return '[%d]' % ((linknum%self.geom.linkDisplayNum)+1)
242 def displayTrickyList(self):
243 for i in range(self.geom.trickyDisplayNum):
244 self.clearLine(self.geom.trickyline + i)
245 self.displayCentred(
246 self.geom.trickyline + i,
247 self.tricky[i]
250 def addTricky(self, word):
251 if self.geom.trickyDisplayNum <= 0:
252 return
253 for t in self.tricky:
254 if word == t:
255 # already in the list; don't repeat
256 return
257 for i in range(self.geom.trickyDisplayNum-1, 0, -1):
258 self.tricky[i] = self.tricky[i-1]
259 self.tricky[0] = word
260 self.displayTrickyList()
262 def isTricky(self, word):
263 return (
264 bool(re.search('\d', word)) or bool(re.search('\w-+\w', word))
265 or len(word) > 15
268 def cleanWord(self, word):
269 for (start, end) in (('(',')'),('[',']'),('{','}')):
270 # remove non-matching brackets from top and tail:
271 if word[0] == start:
272 if end not in word:
273 word = word[1:]
274 elif word[-1] == end and start not in word:
275 word = word[:-1]
276 return word.strip(' ,.;:?!')
278 def abbreviateStr(self, str, maxLen = None):
279 if maxLen is None: maxLen = 2*self.geom.maxx//3
280 if len(str) <= maxLen: return str
281 return '%s...%s' % (str[:(maxLen//2)-3], str[-((maxLen//2)-3):])
283 def displayAll(self):
284 self.showCurrentWord()
285 self.displayStatus()
286 self.displayLinkList()
287 self.displayTrickyList()
289 def applyUrlShortcuts(self, command, depth=0):
290 words = command.split()
291 try:
292 numArgs, shortcut = self.settings.urlShortcuts[words[0]]
293 except (IndexError, KeyError):
294 return command
296 for arg in range(numArgs):
297 i = arg+1
298 try:
299 if i == numArgs:
300 # last argument gets all remaining words
301 repl = ' '.join(words[i:])
302 else:
303 repl = words[i]
304 except IndexError:
305 return shortcut
307 shortcut = re.sub('%%%d' % i, repl, shortcut)
309 shortcut = re.sub('%c', self.itext.url, shortcut)
310 shortcut = re.sub('\\$HOME', os.getenv("HOME"), shortcut)
311 # apply recursively
312 if depth < 10:
313 return self.applyUrlShortcuts(shortcut, depth+1)
314 else:
315 return shortcut
317 def go(self, command):
318 url = self.applyUrlShortcuts(command)
319 self.gotoNewUrl(url)
321 def gotoNewUrl(self, url):
322 if self.itext:
323 self.history.append(self.itext.url)
324 self.gotoUrl(url)
325 # Turns out to be neater to have the current document in unHistory:
326 self.unHistory = [url]
327 def goHistory(self, num=1):
328 try:
329 self.gotoUrl(self.history[-num])
330 for i in range(num):
331 self.unHistory.append(self.history.pop())
332 except IndexError:
333 return None
334 def goUnHistory(self, num=1):
335 try:
336 self.gotoUrl(self.unHistory[-num-1])
337 for i in range(num):
338 self.history.append(self.unHistory.pop())
339 except IndexError:
340 return None
342 def gotoUrl(self, url, noCache=False):
343 shownError = False
344 try:
345 m = re.match('(.*)#(.*)$', url)
346 if m:
347 baseUrl, anchor = m.groups()
348 else:
349 baseUrl, anchor = url, None
350 cached = self.findCachedDocument(baseUrl)
351 if cached and not noCache:
352 self.itext = cached
353 else:
354 self.displayCleanCentred(self.geom.commandline, self.abbreviateStr("Loading %s" % url))
355 self.scr.refresh()
357 self.interruptible = True
358 self.itext = IndexedHypertext(baseUrl, notSentenceEnder=self.notSentenceEnder)
359 self.interruptible = False
361 if self.itext.lynxErr:
362 err = self.itext.lynxErr.strip()
363 line = ';'.join(err.splitlines()[0:2])
364 self.displayCleanCentred(self.geom.commandline,
365 self.abbreviateStr("Failed: %s" % line))
366 shownError = True
367 return False
369 if anchor:
370 self.interruptible = True
371 n = self.itext.findAnchor(anchor)
372 self.interruptible = False
374 if n is not None:
375 self.itext.seekWord(n, setMark=True)
376 else:
377 self.displayCleanCentred(self.geom.commandline,
378 self.abbreviateStr("No such anchor: %s" % anchor))
379 shownError = True
381 self.displayCleanCentred(self.geom.infoline, self.abbreviateStr("%s" % baseUrl))
382 self.displayAll()
384 self.cacheDocument(self.itext)
386 finally:
387 self.interruptible = False
388 if not shownError:
389 self.clearLine(self.geom.commandline)
391 def cacheDocument(self, doc):
392 # remove any old cachings of this url:
393 for cached in self.cachedDocuments:
394 if cached[0] == doc.url:
395 self.cachedDocuments.remove(cached)
396 self.cachedDocuments.append((doc.url, doc))
397 if len(self.cachedDocuments) > self.maxCached:
398 self.cachedDocuments.pop(0)
399 def findCachedDocument(self, url):
400 for (u, doc) in self.cachedDocuments:
401 if u == url: return doc
402 return None
404 def interruptSpeech(self):
405 if self.speechsub:
406 self.speechsub.kill()
407 self.speechsub = None
409 def saySentence(self):
410 if self.speechsub:
411 self.speechsub.wait()
412 self.speechsub = None
413 i = self.itext.atWord
414 quote = self.itext.inQuote()
416 s = self.itext.sentenceIndex.nextPos()
417 sq = self.itext.startQuoteIndex.nextPos()
418 eq = self.itext.endQuoteIndex.nextPos()
419 if eq is not None: eq += 1
420 stops = filter(lambda x: x is not None, (s,sq,eq))
421 if stops == (): end=None
422 else: end = min(stops)
424 text=""
425 while i != end:
426 w = self.itext.getWord(i)
427 if not w: break
428 text += w + " "
429 i+=1
430 self.spokeTo = i-1
431 self.speechsub = self.sayText(text, quote=quote)
433 def sayText(self, text, wait=False, quote=False):
434 from subprocess import Popen, PIPE
435 espeakps = Popen(
436 (quote and self.sayGenQuotedSpeech or self.sayGenSpeech) %
437 (self.wpm), shell=True, stdin=PIPE, stdout=PIPE, stderr=None)
438 speechps = Popen(self.sayPlaySpeech.split(),
439 stdin=espeakps.stdout, stdout=None, stderr=None,
440 shell=False, close_fds=True) # these two are important!
441 espeakps.stdin.write(text.encode("utf-8"))
442 espeakps.stdin.close()
443 espeakps.stdout.close()
444 return speechps
446 def showCurrentWord(self, word=None):
447 if word is None:
448 word = self.itext.currentWord()
450 tricky = self.isTricky(word)
451 linknum = self.itext.currentLink()
453 if linknum is None:
454 if tricky: attr = curses.A_BOLD
455 else: attr = 0
456 self.displayCleanCentred(self.geom.wordline, word, attr)
457 else:
458 self.displayCleanCentred(self.geom.wordline, word,
459 self.attrOfLinknum(linknum) | curses.A_BOLD)
461 self.displayLinkListEntry(linknum, True)
463 if linknum >= self.linkDisplayEmphNum:
464 self.displayLinkListEntry(linknum-self.linkDisplayEmphNum,
465 False)
467 if self.speech and self.paused:
468 self.sayText(word, quote=self.itext.inQuote())
470 if tricky:
471 self.addTricky(self.cleanWord(word))
473 if self.showContext and self.paused:
474 contextBefore = ''
475 contextFrom = self.itext.atWord
476 while True:
477 w = self.itext.getWord(contextFrom-1)
478 if self.itext.atSentenceEnd(contextFrom-1):
479 w += ' '
480 if self.itext.atParaEnd(contextFrom-1):
481 w += ' '
482 if w and (len(w) + len(contextBefore)
483 + len(word)//2 + 1) < self.geom.maxx//3:
484 contextBefore = w + ' ' + contextBefore
485 contextFrom -= 1
486 else:
487 break
489 contextAfter = ''
490 contextTo = self.itext.atWord
491 while True:
492 w = ''
493 if self.itext.atSentenceEnd(contextTo):
494 w += ' '
495 if self.itext.atParaEnd(contextTo):
496 w += ' '
497 w += self.itext.getWord(contextTo+1)
498 if w and (len(w) + len(contextAfter)
499 + len(word)//2 + 1) < self.geom.maxx//3:
500 contextAfter += ' ' + w
501 contextTo += 1
502 else:
503 break
505 point = (self.geom.maxx - len(word))//2 - len(contextBefore)
506 attr = curses.color_pair(8) | curses.A_BOLD
508 addstrwrapped(self.scr, self.geom.wordline, point, contextBefore, attr)
509 point = (self.geom.maxx + len(word))//2
510 addstrwrapped(self.scr, self.geom.wordline, point, contextAfter, attr)
512 if self.showPoint: self.displayStatus()
514 def mainLoop(self):
515 def sigHandlerQuit(sig, frame):
516 raise QuitException
517 signal.signal(15, sigHandlerQuit)
519 def sigHandlerInt(sig, frame):
520 # Occasionally, we can safely allow ^C to interrupt a section of
521 # code.
522 if self.interruptible:
523 raise KeyboardInterrupt
524 signal.signal(2, sigHandlerInt)
526 def applyCount(method, args):
527 # update args, replacing/appending any count argument
528 try:
529 countarg = self.bindableMethods[method][2]
530 if countarg > 0:
531 if len(args) >= countarg:
533 currentArg = args[countarg-1]
534 if (currentArg.__class__ == int and
535 currentArg < 0):
536 self.count = -self.count
538 args[countarg-1] = self.count
540 elif len(args)+1 == countarg:
541 args.append(count)
542 except IndexError:
543 pass
544 endCount()
545 def endCount():
546 self.counting = False
547 self.countWin.erase()
548 self.countWin.refresh()
549 del self.countWin
551 def handleResize():
552 self.setGeom()
553 self.scr.erase()
554 self.scr.refresh()
555 self.displayAll()
557 self.go(self.initialUrl)
558 self.displayStatus()
560 self.blankSpace = 0
562 def flashWord():
563 # handle blanking
564 if self.blankSpace > 0:
565 self.blankSpace -= 1
566 self.displayCleanCentred(self.geom.wordline, "")
567 return
569 word = self.itext.readWord()
571 if word is not None:
572 if self.itext.atParaEnd():
573 self.blankSpace = self.paraBlankSpace
574 elif self.itext.atSentenceEnd():
575 self.blankSpace = self.sentenceBlankSpace
576 else:
577 def isPause(word):
578 try:
579 if word[-1] in "'\"":
580 last = word[-2]
581 else:
582 last = word[-1]
583 return last in ",;:"
584 except IndexError:
585 return False
586 if isPause(word):
587 self.blankSpace = self.pauseBlankSpace
589 self.showCurrentWord(word)
591 if self.speech and not self.paused and (
592 self.itext.atWord > self.spokeTo
593 or not self.speechsub):
594 self.saySentence()
596 return
598 def handleInput():
599 c = None
600 escBound = 27 in self.settings.keyBindings
601 metaBit = False
602 while c != -1:
603 c = self.scr.getch()
605 if not escBound:
606 if c == 27:
607 metaBit = True
608 continue
609 elif metaBit:
610 if c == -1:
611 c = 27
612 else:
613 # set high bit
614 c |= 0x80
615 metaBit = False
617 if c == curses.KEY_RESIZE:
618 handleResize()
620 elif self.counting and ord('0') <= c <= ord('9'):
621 self.count = 10*self.count + c - ord('0')
622 self.countWin.addch(chr(c))
623 self.countWin.refresh()
625 else:
626 binding = self.settings.keyBindings.get(c)
627 if binding is not None:
628 method = binding[0]
629 args = binding[1][:]
631 if self.counting:
632 applyCount(method, args)
634 self.interruptSpeech()
635 getattr(self, 'doKey'+method).__call__(*args)
636 elif self.counting and c in [27,7]:
637 # esc or ^G: cancel count
638 endCount()
641 maxStep = 0.2
642 ERRTimeout = 0
643 class Event:
644 def __init__(self, type, time):
645 if type not in ["word", "blank"]:
646 raise "BUG: unknown Event type"
647 self.type = type
648 self.time = time
649 nextEvent = Event("word", time())
650 if self.blink: nextBlinkTime = time() + self.blinkDelay
652 # compensate for blanking at pauses and so on:
653 WPMFACTOR = 4.0/5
655 # main loop:
656 while True:
657 try:
658 if ERRTimeout > 0:
659 ERRTimeout -= 1
660 try:
661 while True:
662 wait = nextEvent.time - time()
663 if wait < 0: break
664 sleep(min(wait, maxStep))
665 beforeInput = time()
667 try:
668 handleInput()
669 except KeyboardInterrupt:
670 pass
672 inputTime = time() - beforeInput
673 nextEvent.time += inputTime
674 if self.blink: nextBlinkTime += inputTime
676 delay = (60.0 * WPMFACTOR) / self.wpm
678 if self.paused or self.itext.atEnd():
679 nextEvent = Event("word",
680 time() + delay)
681 if self.blink:
682 nextBlinkTime = time() + self.blinkDelay
683 continue
685 if nextEvent.type == "word":
686 t = time()
687 if self.blink and nextBlinkTime < time() and (
688 not self.blinkWaitSentence or
689 self.itext.atSentenceEnd()):
690 self.displayCleanCentred(self.geom.wordline,
691 "{*blink*}")
692 nextBlinkTime = time() + self.blinkDelay
693 waitTime = self.blinkTime
694 else:
695 flashWord()
696 waitTime = delay
697 if self.blankBetween:
698 nextEvent = Event("blank",
699 t + waitTime - delay/3)
700 else:
701 nextEvent = Event("word",
702 t + waitTime)
703 elif nextEvent.type == "blank":
704 self.clearLine(self.geom.wordline)
705 self.scr.refresh()
706 nextEvent = Event("word",
707 time() + delay/3)
709 except curses.error:
710 if ERRTimeout == 0:
711 # Curses threw an error, but that might just be because we
712 # need to resize and haven't yet processed KEY_RESIZE.
713 # So we resize now, and see if we get another curses error
714 # within the next couple of goes round the main loop.
715 self.setGeom()
716 ERRTimeout = 2
717 else:
718 raise "Curses returned ERR - terminal too short?"
719 except KeyboardInterrupt:
720 pass
722 def onExit(self):
723 # emulating vim - global marks '0'-'9' give positions on last 10 exits
724 for i in range(9,0,-1):
725 try: self.globalMarks[str(i)] = self.globalMarks[str(i-1)][:]
726 except KeyError: pass
727 if (self.itext.url and self.itext.url[0] != '!'):
728 self.globalMarks['0'] = \
729 [self.itext.url, self.itext.atWord]
731 def writeState(self):
732 try:
733 self.writeMarksFile(self.globalMarks)
734 except IOError:
735 pass
737 bindableMethods = {
738 # <name> : [<min args>, <max args>, [<count arg>], [<string args>]]
739 'ChangeSpeed' : [1,1,1],
740 'SeekSentence' : [0,1,1],
741 'SeekParagraph' : [0,1,1],
742 'SeekWord' : [0,1,1],
743 'SeekLink' : [0,1,1],
744 'SeekStart' : [0,0],
745 'SeekEnd' : [0,0],
746 'History' : [0,1,1],
747 'FollowLink' : [1,1,1],
748 'Go' : [0,0],
749 'GoCurrent' : [0,0],
750 'Search' : [0,1,1],
751 'SearchAgain' : [0,1,1],
752 'Count' : [0,0],
753 'Mark' : [0,0],
754 'GoMark' : [0,0],
755 'Reload' : [0,0],
756 'Pause' : [0,0],
757 'Command' : [0,0],
758 'Skim' : [0,0],
759 'Refresh' : [0,0],
760 'Toggle' : [1,1,0,True],
761 'Help' : [0,0],
762 'Quit' : [0,0],
765 def doKeyChangeSpeed(self, amount):
766 self.wpm += amount
767 if self.wpm < 30: self.wpm = 30
768 self.displayStatus()
770 def doKeySeekSentence(self, n=1):
771 self.itext.seekIndexRel(self.itext.sentenceIndex, n)
772 self.displayAll()
773 def doKeySeekParagraph(self, n=1):
774 self.itext.seekIndexRel(self.itext.paraIndex, n)
775 self.displayAll()
776 def doKeySeekWord(self, n=1):
777 self.itext.seekWordRel(n)
778 self.displayAll()
779 def doKeySeekLink(self, n=1):
780 self.itext.seekIndexRel(self.itext.linkIndex, n)
781 self.displayAll()
783 def doKeySeekStart(self):
784 self.itext.seekWord(0, setMark=True)
785 self.displayAll()
786 def doKeySeekEnd(self):
787 self.itext.seekEnd()
788 self.displayAll()
790 def doKeyFollowLink(self, n):
791 if not self.itext.links: return
793 if self.geom.linkDisplayNum > 0:
794 n -= 1
795 nl = self.itext.linkIndex.at % self.geom.linkDisplayNum
796 else:
797 nl = self.itext.linkIndex.at
799 if n <= nl:
800 linknum = self.itext.linkIndex.at + (n-nl)
801 else:
802 linknum = self.itext.linkIndex.at - self.geom.linkDisplayNum + (n-nl)
803 try:
804 url = self.itext.links[linknum]["url"]
805 self.gotoNewUrl(url)
806 except IndexError:
807 pass
809 def readLine(self, prompt, initial='', shortPrompt=None, history=None,
810 getCompletions=None):
811 if self.abbreviate or self.geom.maxx <= 10:
812 if shortPrompt is not None:
813 prompt = shortPrompt
814 else:
815 prompt = prompt[0]+":"
817 promptX = max(1, self.geom.maxx//6 - len(prompt))
818 boxX = promptX + len(prompt)
819 boxLen = min(self.geom.maxx - boxX - 1, 2*self.geom.maxx//3)
821 addstrwrapped(self.scr, self.geom.commandline, promptX,
822 prompt[:self.geom.maxx-5])
823 self.scr.refresh()
825 curses.curs_set(1)
827 pad = curses.newpad(1, 200)
828 pad.scrollok(1)
829 box = TextboxPad(pad,
830 self.geom.commandline, boxX,
831 self.geom.commandline, boxX + boxLen - 1,
832 history=history,
833 getCompletions=getCompletions)
835 for char in initial:
836 box.do_command(char)
838 class cancelReadline(Exception):
839 pass
840 def cancelValidate(ch):
841 if ch == ascii.BEL or ch == 27:
842 # ^G and escape cancel the readLine
843 raise cancelReadline
844 return ch
845 try:
846 try:
847 read = box.edit(cancelValidate)
848 if history is not None:
849 history.append(read.strip())
850 return read
851 except cancelReadline:
852 return None
853 finally:
854 curses.curs_set(0)
855 self.clearLine(self.geom.commandline)
857 def doKeyGo(self):
858 def filenameCompletion(partial):
859 from glob import glob
860 from os.path import isdir
862 completions = glob(partial+'*')
864 for i in range(len(completions)):
865 if isdir(completions[i]):
866 completions[i] += '/'
868 return completions
870 command = self.readLine("Go: ", history=self.urlHistory,
871 getCompletions=filenameCompletion)
872 if command:
873 self.go(command.strip())
875 def doKeyGoCurrent(self):
876 command = self.readLine("Go: ", self.itext.url, history=self.urlHistory)
877 if command:
878 self.go(command.strip())
880 def charOfSearchDir(self, dir=1):
881 if dir == 1: return '/'
882 else: return '?'
883 def search(self, pattern, n=1):
884 dir = _cmp(n,0)
885 self.displayCleanCentred(self.geom.commandline,
886 self.charOfSearchDir(dir)+pattern)
887 try:
888 self.interruptible = True
889 for i in range(abs(n)):
890 ret = self.itext.search(pattern.strip(), dir)
891 if not ret: break
892 self.displayAll()
893 finally:
894 self.interruptible = False
895 self.clearLine(self.geom.commandline)
896 def doKeySearch(self, n=1):
897 dir = _cmp(n,0)
898 pattern = self.readLine("Search: ",
899 shortPrompt = self.charOfSearchDir(dir),
900 history=self.searchHistory)
901 if pattern:
902 self.search(pattern, n)
903 self.lastSearch = {"pattern": pattern, "dir": dir}
904 def doKeySearchAgain(self, n=1):
905 if self.lastSearch:
906 self.search(self.lastSearch["pattern"],
907 self.lastSearch["dir"]*n)
909 def doKeyPause(self):
910 self.paused = not self.paused
911 self.displayStatus()
912 if self.showContext:
913 self.showCurrentWord()
915 def doKeyHistory(self, n=-1):
916 if n < 0:
917 self.goHistory(-n)
918 else:
919 self.goUnHistory(n)
921 def doKeyCount(self):
922 self.counting = True
923 self.count = 0
925 boxX = self.geom.maxx//6
926 boxLen = min(self.geom.maxx - boxX - 1, 2*self.geom.maxx//3)
927 self.countWin = curses.newwin(1, boxLen, self.geom.commandline, boxX)
928 if not self.abbreviate:
929 prompt = "Count: "
930 else:
931 prompt = "C"
932 addstrwrapped(self.countWin, None, None, prompt[:self.geom.maxx])
933 self.countWin.refresh()
935 def doKeyMark(self):
936 key = self.blockGetKey(self.geom.commandline,
937 self.abbreviate and "m" or "Set mark:")
938 if key is None or len(key) != 1:
939 return
940 if 'a' <= key <= 'z':
941 # local mark
942 self.itext.mark(key)
943 elif 'A' <= key <= 'Z':
944 # global marks
945 self.globalMarks[key] =\
946 [self.itext.url, self.itext.atWord]
947 def doKeyGoMark(self):
948 key = self.blockGetKey(self.geom.commandline,
949 self.abbreviate and "'" or "Go to mark:")
950 if key is None or len(key) != 1:
951 return
952 if 'a' <= key <= 'z' or key == '\'':
953 # local mark
954 self.itext.goMark(key)
955 self.displayAll()
956 elif key in self.globalMarks:
957 # global marks
958 url, pos = self.globalMarks[key]
959 if url != self.itext.url:
960 self.gotoNewUrl(url)
961 self.itext.seekWord(pos, setMark=True)
962 self.displayAll()
964 def doKeyReload(self):
965 n = self.itext.atWord
966 self.gotoUrl(self.itext.url, True)
967 self.itext.seekWord(n)
968 self.displayAll()
970 def doKeyCommand(self):
971 def handleErr(err):
972 self.displayCleanCentred(self.geom.commandline, err)
973 command = self.readLine(':', shortPrompt=':',
974 history=self.commandHistory)
975 if command:
976 self.settings.processCommand(command, handleErr)
977 self.displayAll()
979 def doKeySkim(self):
980 if not self.skim:
981 self.normalWpm = self.wpm
982 self.wpm = self.skimWpm
983 self.skim = True
984 else:
985 self.skimWpm = self.wpm
986 self.wpm = self.normalWpm
987 self.skim = False
988 self.displayAll()
990 def doKeyRefresh(self):
991 self.scr.redrawwin()
993 def doKeyToggle(self, var):
994 if hasattr(self, var) and type(getattr(self,var)) == bool:
995 setattr(self, var, not getattr(self,var))
997 def doKeyHelp(self):
998 self.go("special:README")
1000 def doKeyQuit(self):
1001 raise QuitException