version 0.5.0
[flinks.git] / flinkspkg / Browser.py
blob3a0f9fa3f5edb17aeca162dbba2bf110f101b661
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.textpad import Textbox
17 from .TextboxPad import TextboxPad
18 import curses.ascii as ascii
20 from .IndexedHypertext import IndexedHypertext
21 from .portability import addstrwrapped, _cmp
23 class QuitException(Exception):
24 pass
26 class Browser:
27 def __init__(self, scr, config):
28 self.settings = config
29 self.scr = scr
31 self.setGeom()
33 curses.use_default_colors()
34 for i in range(1,8):
35 curses.init_pair(i, i, -1)
36 curses.init_pair(8, curses.COLOR_BLACK, -1)
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 e.g. use self.wpm as convenient shorthand for
97 # self.settings.options["wpm"], etc. The overriding below of __getattr__
98 # and __setattr__ 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 = [x for x in (s,sq,eq) if x is not None]
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 espeakcmd = (quote and self.sayGenQuotedSpeech or
436 self.sayGenSpeech) % (self.wpm)
437 try:
438 espeakps = Popen( espeakcmd.split(),
439 shell=False, stdin=PIPE, stdout=PIPE, stderr=None)
440 espeakps.stdin.write(text.encode("utf-8"))
441 espeakps.poll()
442 speechps = Popen(self.sayPlaySpeech.split(),
443 stdin=espeakps.stdout, stdout=None, stderr=None,
444 shell=False, close_fds=True) # these two are important!
445 espeakps.stdin.close()
446 espeakps.stdout.close()
447 return speechps
448 except OSError:
449 self.displayCleanCentred(self.geom.commandline,
450 "Speech command failed; disabling. Edit "
451 +"sayGenSpeech and sayPlaySpeech in ~/.flinksrc")
452 self.speech = False
453 return None
455 def showCurrentWord(self, word=None):
456 if word is None:
457 word = self.itext.currentWord()
459 tricky = self.isTricky(word)
460 linknum = self.itext.currentLink()
461 quote = self.itext.inQuote()
463 if linknum is None:
464 if tricky or (self.boldQuotes and quote):
465 attr = curses.A_BOLD
466 else: attr = 0
467 self.displayCleanCentred(self.geom.wordline, word, attr)
468 else:
469 self.displayCleanCentred(self.geom.wordline, word,
470 self.attrOfLinknum(linknum) | curses.A_BOLD)
472 self.displayLinkListEntry(linknum, True)
474 if linknum >= self.linkDisplayEmphNum:
475 self.displayLinkListEntry(linknum-self.linkDisplayEmphNum,
476 False)
478 if self.speech and self.paused:
479 self.sayText(word, quote=quote)
481 if tricky:
482 self.addTricky(self.cleanWord(word))
484 if self.showContext and self.paused:
485 contextBefore = ''
486 contextFrom = self.itext.atWord
487 while True:
488 w = self.itext.getWord(contextFrom-1)
489 if self.itext.atSentenceEnd(contextFrom-1):
490 w += ' '
491 if self.itext.atParaEnd(contextFrom-1):
492 w += ' '
493 if w and (len(w) + len(contextBefore)
494 + len(word)//2 + 1) < self.geom.maxx//3:
495 contextBefore = w + ' ' + contextBefore
496 contextFrom -= 1
497 else:
498 break
500 contextAfter = ''
501 contextTo = self.itext.atWord
502 while True:
503 w = ''
504 if self.itext.atSentenceEnd(contextTo):
505 w += ' '
506 if self.itext.atParaEnd(contextTo):
507 w += ' '
508 w += self.itext.getWord(contextTo+1)
509 if w and (len(w) + len(contextAfter)
510 + len(word)//2 + 1) < self.geom.maxx//3:
511 contextAfter += ' ' + w
512 contextTo += 1
513 else:
514 break
516 point = (self.geom.maxx - len(word))//2 - len(contextBefore)
517 attr = curses.color_pair(8) | curses.A_BOLD
519 addstrwrapped(self.scr, self.geom.wordline, point, contextBefore, attr)
520 point = (self.geom.maxx + len(word))//2
521 addstrwrapped(self.scr, self.geom.wordline, point, contextAfter, attr)
523 if self.showPoint: self.displayStatus()
525 def mainLoop(self):
526 def sigHandlerQuit(sig, frame):
527 raise QuitException
528 signal.signal(15, sigHandlerQuit)
530 def sigHandlerInt(sig, frame):
531 # Occasionally, we can safely allow ^C to interrupt a section of
532 # code.
533 if self.interruptible:
534 raise KeyboardInterrupt
535 signal.signal(2, sigHandlerInt)
537 def applyCount(method, args):
538 # update args, replacing/appending any count argument
539 try:
540 countarg = self.bindableMethods[method][2]
541 if countarg > 0:
542 if len(args) >= countarg:
544 currentArg = args[countarg-1]
545 if (currentArg.__class__ == int and
546 currentArg < 0):
547 self.count = -self.count
549 args[countarg-1] = self.count
551 elif len(args)+1 == countarg:
552 args.append(count)
553 except IndexError:
554 pass
555 endCount()
556 def endCount():
557 self.counting = False
558 self.countWin.erase()
559 self.countWin.refresh()
560 del self.countWin
562 def handleResize():
563 self.setGeom()
564 self.scr.erase()
565 self.scr.refresh()
566 self.displayAll()
568 self.go(self.initialUrl)
569 self.displayStatus()
571 self.blankSpace = 0
573 def flashWord():
574 # handle blanking
575 if self.blankSpace > 0:
576 self.blankSpace -= 1
577 self.displayCleanCentred(self.geom.wordline, "")
578 return
580 word = self.itext.readWord()
582 if word is not None:
583 if self.itext.atParaEnd():
584 self.blankSpace = self.paraBlankSpace
585 elif self.itext.atSentenceEnd():
586 self.blankSpace = self.sentenceBlankSpace
587 else:
588 def isPause(word):
589 try:
590 if word[-1] in "'\"":
591 last = word[-2]
592 else:
593 last = word[-1]
594 return last in ",;:"
595 except IndexError:
596 return False
597 if isPause(word):
598 self.blankSpace = self.pauseBlankSpace
600 self.showCurrentWord(word)
602 if self.speech and not self.paused and (
603 self.itext.atWord > self.spokeTo
604 or not self.speechsub):
605 self.saySentence()
607 return
609 def handleInput():
610 c = None
611 escBound = 27 in self.settings.keyBindings
612 metaBit = False
613 while c != -1:
614 c = self.scr.getch()
616 if not escBound:
617 if c == 27:
618 metaBit = True
619 continue
620 elif metaBit:
621 if c == -1:
622 c = 27
623 else:
624 # set high bit
625 c |= 0x80
626 metaBit = False
628 if c == curses.KEY_RESIZE:
629 handleResize()
631 elif self.counting and ord('0') <= c <= ord('9'):
632 self.count = 10*self.count + c - ord('0')
633 self.countWin.addch(chr(c))
634 self.countWin.refresh()
636 else:
637 binding = self.settings.keyBindings.get(c)
638 if binding is not None:
639 method = binding[0]
640 args = binding[1][:]
642 if self.counting:
643 applyCount(method, args)
645 self.interruptSpeech()
646 getattr(self, 'doKey'+method).__call__(*args)
647 elif self.counting and c in [27,7]:
648 # esc or ^G: cancel count
649 endCount()
652 maxStep = 0.2
653 ERRTimeout = 0
654 class Event:
655 def __init__(self, type, time):
656 if type not in ["word", "blank"]:
657 raise "BUG: unknown Event type"
658 self.type = type
659 self.time = time
660 nextEvent = Event("word", time())
661 if self.blink: nextBlinkTime = time() + self.blinkDelay
663 # compensate for blanking at pauses and so on:
664 WPMFACTOR = 4.0/5
666 # main loop:
667 while True:
668 try:
669 if ERRTimeout > 0:
670 ERRTimeout -= 1
671 try:
672 while True:
673 wait = nextEvent.time - time()
674 if wait < 0: break
675 sleep(min(wait, maxStep))
676 beforeInput = time()
678 try:
679 handleInput()
680 except KeyboardInterrupt:
681 pass
683 inputTime = time() - beforeInput
684 nextEvent.time += inputTime
685 if self.blink: nextBlinkTime += inputTime
687 delay = (60.0 * WPMFACTOR) / self.wpm
689 if self.paused or self.itext.atEnd():
690 nextEvent = Event("word",
691 time() + delay)
692 if self.blink:
693 nextBlinkTime = time() + self.blinkDelay
694 continue
696 if nextEvent.type == "word":
697 t = time()
698 if self.blink and nextBlinkTime < time() and (
699 not self.blinkWaitSentence or
700 self.itext.atSentenceEnd()):
701 self.displayCleanCentred(self.geom.wordline,
702 "{*blink*}")
703 nextBlinkTime = time() + self.blinkDelay
704 waitTime = self.blinkTime
705 else:
706 flashWord()
707 waitTime = delay
708 if self.blankBetween:
709 nextEvent = Event("blank",
710 t + waitTime - delay/3)
711 else:
712 nextEvent = Event("word",
713 t + waitTime)
714 elif nextEvent.type == "blank":
715 self.clearLine(self.geom.wordline)
716 self.scr.refresh()
717 nextEvent = Event("word",
718 time() + delay/3)
720 except curses.error:
721 if ERRTimeout == 0:
722 # Curses threw an error, but that might just be because we
723 # need to resize and haven't yet processed KEY_RESIZE.
724 # So we resize now, and see if we get another curses error
725 # within the next couple of goes round the main loop.
726 self.setGeom()
727 ERRTimeout = 2
728 else:
729 raise "Curses returned ERR - terminal too short?"
730 except KeyboardInterrupt:
731 pass
733 def onExit(self):
734 # emulating vim - global marks '0'-'9' give positions on last 10 exits
735 for i in range(9,0,-1):
736 try: self.globalMarks[str(i)] = self.globalMarks[str(i-1)][:]
737 except KeyError: pass
738 if (self.itext.url and self.itext.url[0] != '!'):
739 self.globalMarks['0'] = \
740 [self.itext.url, self.itext.atWord]
742 def writeState(self):
743 try:
744 self.writeMarksFile(self.globalMarks)
745 except IOError:
746 pass
748 bindableMethods = {
749 # <name> : [<min args>, <max args>, [<count arg>], [<string args>]]
750 'ChangeSpeed' : [1,1,1],
751 'SeekSentence' : [0,1,1],
752 'SeekParagraph' : [0,1,1],
753 'SeekWord' : [0,1,1],
754 'SeekLink' : [0,1,1],
755 'SeekStart' : [0,0],
756 'SeekEnd' : [0,0],
757 'History' : [0,1,1],
758 'FollowLink' : [1,1,1],
759 'Go' : [0,0],
760 'GoCurrent' : [0,0],
761 'Search' : [0,1,1],
762 'SearchAgain' : [0,1,1],
763 'Count' : [0,0],
764 'Mark' : [0,0],
765 'GoMark' : [0,0],
766 'Reload' : [0,0],
767 'Pause' : [0,0],
768 'Command' : [0,0],
769 'Skim' : [0,0],
770 'SkipLinks' : [0,0],
771 'Refresh' : [0,0],
772 'Toggle' : [1,1,0,True],
773 'Help' : [0,0],
774 'Quit' : [0,0],
777 def doKeyChangeSpeed(self, amount):
778 self.wpm += amount
779 if self.wpm < 30: self.wpm = 30
780 self.displayStatus()
782 def doKeySeekSentence(self, n=1):
783 self.itext.seekIndexRel(self.itext.sentenceIndex, n)
784 self.displayAll()
785 def doKeySeekParagraph(self, n=1):
786 self.itext.seekIndexRel(self.itext.paraIndex, n)
787 self.displayAll()
788 def doKeySeekWord(self, n=1):
789 self.itext.seekWordRel(n)
790 self.displayAll()
791 def doKeySeekLink(self, n=1):
792 self.itext.seekIndexRel(self.itext.linkIndex, n)
793 self.displayAll()
795 def doKeySeekStart(self):
796 self.itext.seekWord(0, setMark=True)
797 self.displayAll()
798 def doKeySeekEnd(self):
799 self.itext.seekEnd()
800 self.displayAll()
802 def doKeyFollowLink(self, n):
803 if not self.itext.links: return
805 if self.geom.linkDisplayNum > 0:
806 n -= 1
807 nl = self.itext.linkIndex.at % self.geom.linkDisplayNum
808 else:
809 nl = self.itext.linkIndex.at
811 if n <= nl:
812 linknum = self.itext.linkIndex.at + (n-nl)
813 else:
814 linknum = self.itext.linkIndex.at - self.geom.linkDisplayNum + (n-nl)
815 try:
816 url = self.itext.links[linknum]["url"]
817 self.gotoNewUrl(url)
818 except IndexError:
819 pass
821 def readLine(self, prompt, initial='', shortPrompt=None, history=None,
822 getCompletions=None):
823 if self.abbreviate or self.geom.maxx <= 10:
824 if shortPrompt is not None:
825 prompt = shortPrompt
826 else:
827 prompt = prompt[0]+":"
829 promptX = max(1, self.geom.maxx//6 - len(prompt))
830 boxX = promptX + len(prompt)
831 boxLen = min(self.geom.maxx - boxX - 1, 2*self.geom.maxx//3)
833 addstrwrapped(self.scr, self.geom.commandline, promptX,
834 prompt[:self.geom.maxx-5])
835 self.scr.refresh()
837 curses.curs_set(1)
839 pad = curses.newpad(1, 200)
840 pad.scrollok(1)
841 box = TextboxPad(pad,
842 self.geom.commandline, boxX,
843 self.geom.commandline, boxX + boxLen - 1,
844 history=history,
845 getCompletions=getCompletions)
847 for char in initial:
848 box.do_command(char)
850 class cancelReadline(Exception):
851 pass
852 def cancelValidate(ch):
853 if ch == ascii.BEL or ch == 27:
854 # ^G and escape cancel the readLine
855 raise cancelReadline
856 return ch
857 try:
858 try:
859 read = box.edit(cancelValidate)
860 if history is not None:
861 history.append(read.strip())
862 return read
863 except cancelReadline:
864 return None
865 finally:
866 curses.curs_set(0)
867 self.clearLine(self.geom.commandline)
869 def doKeyGo(self):
870 def filenameCompletion(partial):
871 from glob import glob
872 from os.path import isdir
874 completions = glob(partial+'*')
876 for i in range(len(completions)):
877 if isdir(completions[i]):
878 completions[i] += '/'
880 return completions
882 command = self.readLine("Go: ", history=self.urlHistory,
883 getCompletions=filenameCompletion)
884 if command:
885 self.go(command.strip())
887 def doKeyGoCurrent(self):
888 command = self.readLine("Go: ", self.itext.url, history=self.urlHistory)
889 if command:
890 self.go(command.strip())
892 def charOfSearchDir(self, dir=1):
893 if dir == 1: return '/'
894 else: return '?'
895 def search(self, pattern, n=1):
896 dir = _cmp(n,0)
897 self.displayCleanCentred(self.geom.commandline,
898 self.charOfSearchDir(dir)+pattern)
899 try:
900 self.interruptible = True
901 for i in range(abs(n)):
902 ret = self.itext.search(pattern.strip(), dir)
903 if not ret: break
904 self.displayAll()
905 finally:
906 self.interruptible = False
907 self.clearLine(self.geom.commandline)
908 def doKeySearch(self, n=1):
909 dir = _cmp(n,0)
910 pattern = self.readLine("Search: ",
911 shortPrompt = self.charOfSearchDir(dir),
912 history=self.searchHistory)
913 if pattern:
914 self.search(pattern, n)
915 self.lastSearch = {"pattern": pattern, "dir": dir}
916 def doKeySearchAgain(self, n=1):
917 if self.lastSearch:
918 self.search(self.lastSearch["pattern"],
919 self.lastSearch["dir"]*n)
921 def doKeyPause(self):
922 self.paused = not self.paused
923 self.displayStatus()
924 if self.showContext:
925 self.showCurrentWord()
927 def doKeyHistory(self, n=-1):
928 if n < 0:
929 self.goHistory(-n)
930 else:
931 self.goUnHistory(n)
933 def doKeyCount(self):
934 self.counting = True
935 self.count = 0
937 boxX = self.geom.maxx//6
938 boxLen = min(self.geom.maxx - boxX - 1, 2*self.geom.maxx//3)
939 self.countWin = curses.newwin(1, boxLen, self.geom.commandline, boxX)
940 if not self.abbreviate:
941 prompt = "Count: "
942 else:
943 prompt = "C"
944 addstrwrapped(self.countWin, None, None, prompt[:self.geom.maxx])
945 self.countWin.refresh()
947 def doKeyMark(self):
948 key = self.blockGetKey(self.geom.commandline,
949 self.abbreviate and "m" or "Set mark:")
950 if key is None or len(key) != 1:
951 return
952 if 'a' <= key <= 'z':
953 # local mark
954 self.itext.mark(key)
955 elif 'A' <= key <= 'Z':
956 # global marks
957 self.globalMarks[key] =\
958 [self.itext.url, self.itext.atWord]
959 def doKeyGoMark(self):
960 key = self.blockGetKey(self.geom.commandline,
961 self.abbreviate and "'" or "Go to mark:")
962 if key is None or len(key) != 1:
963 return
964 if 'a' <= key <= 'z' or key == '\'':
965 # local mark
966 self.itext.goMark(key)
967 self.displayAll()
968 elif key in self.globalMarks:
969 # global marks
970 url, pos = self.globalMarks[key]
971 if url != self.itext.url:
972 self.gotoNewUrl(url)
973 self.itext.seekWord(pos, setMark=True)
974 self.displayAll()
976 def doKeyReload(self):
977 n = self.itext.atWord
978 self.gotoUrl(self.itext.url, True)
979 self.itext.seekWord(n)
980 self.displayAll()
982 def doKeyCommand(self):
983 def handleErr(err):
984 self.displayCleanCentred(self.geom.commandline, err)
985 command = self.readLine(':', shortPrompt=':',
986 history=self.commandHistory)
987 if command:
988 self.settings.processCommand(command, handleErr)
989 self.displayAll()
991 def doKeySkim(self):
992 if not self.skim:
993 self.normalWpm = self.wpm
994 self.wpm = self.skimWpm
995 self.skim = True
996 else:
997 self.skimWpm = self.wpm
998 self.wpm = self.normalWpm
999 self.skim = False
1000 self.displayAll()
1002 def doKeySkipLinks(self):
1003 sInd = self.itext.sentenceIndex
1004 lInd = self.itext.linkIndex
1005 si = sInd.at
1006 ps = sInd[si]
1007 ns = sInd[si+1]
1008 while ns is not None and lInd[lInd.findIndex(ns-1)] >= ps:
1009 si+=1
1010 ps = ns
1011 ns = sInd[si+1]
1012 if ns:
1013 self.itext.seekWord(ns, setMark=True)
1014 self.displayAll()
1016 def doKeyRefresh(self):
1017 self.scr.redrawwin()
1019 def doKeyToggle(self, var):
1020 if hasattr(self, var) and type(getattr(self,var)) == bool:
1021 setattr(self, var, not getattr(self,var))
1023 def doKeyHelp(self):
1024 self.go("special:README")
1026 def doKeyQuit(self):
1027 raise QuitException