3 # Released under the terms of the GPLv3
5 from __future__
import unicode_literals
10 from time
import sleep
, time
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):
27 def __init__(self
, scr
, config
):
28 self
.settings
= config
33 curses
.use_default_colors()
35 curses
.init_pair(i
, i
, -1)
36 curses
.init_pair(8, curses
.COLOR_BLACK
, -1)
45 self
.lastSearch
= None
49 self
.cachedDocuments
= []
51 # history *ends* with most recent url
52 # unHistory *ends* with *current* url
56 self
.tricky
= [""]*self
.trickyDisplayNum
62 self
.normalWpm
= self
.wpm
64 self
.interruptible
= False
67 self
.commandHistory
= []
68 self
.searchHistory
= []
73 self
.globalMarks
= self
.readMarksFile()
75 def readMarksFile(self
):
77 marksFile
= open(os
.getenv("HOME")+"/.flinksMarks", 'r')
82 m
= re
.match('(.)\s*:?\s*', marksFile
.readline())
85 url
= marksFile
.readline().strip()
86 try: point
= int(marksFile
.readline().strip())
88 marks
[mark
] = [url
, point
]
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
]
104 def __setattr__(self
, name
, value
):
105 if name
!= "settings" and name
in self
.settings
.optionNames
:
106 self
.settings
.options
[name
] = value
108 self
.__dict
__[name
] = value
111 def __init__(self
, scr
, desiredLinkDisplayNum
, desiredTrickyDisplayNum
):
112 (maxy
, maxx
) = scr
.getmaxyx()
114 # effective maximums, i.e. with anything bigger scr.move(y,x)
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
124 # desiredTrickyDisplayNum lines of trickies
128 squashed
= self
.maxy
< (desiredLinkDisplayNum
+
129 desiredTrickyDisplayNum
+ 6)
131 self
.wordline
= self
.maxy
//2
133 self
.linkDisplayNum
= min(desiredLinkDisplayNum
,
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
)
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
)
153 key
= self
.scr
.getkey()
155 if promptLine
and prompt
:
156 self
.clearLine(promptLine
)
157 if key
== '\x1b' or key
== '\x07':
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
],
166 def clearLine(self
, line
):
167 self
.scr
.move(line
,0)
169 def displayCleanCentred(self
, line
, str, attr
=0):
171 self
.displayCentred(line
, str, attr
)
173 def displayStatus(self
):
174 if self
.geom
.statusline
== self
.geom
.wordline
: return
175 def getStatusLine(abbreviated
=False):
177 text
= 'WPM: %d' % self
.wpm
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'
185 text
= '%d ' % self
.wpm
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'
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
:
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
210 attr
= self
.attrOfLinknum(linknum
)
212 attr |
= curses
.A_BOLD
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"])
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
)
246 self
.geom
.trickyline
+ i
,
250 def addTricky(self
, word
):
251 if self
.geom
.trickyDisplayNum
<= 0:
253 for t
in self
.tricky
:
255 # already in the list; don't repeat
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
):
264 bool(re
.search('\d', word
)) or bool(re
.search('\w-+\w', word
))
268 def cleanWord(self
, word
):
269 for (start
, end
) in (('(',')'),('[',']'),('{','}')):
270 # remove non-matching brackets from top and tail:
274 elif word
[-1] == end
and start
not in word
:
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()
286 self
.displayLinkList()
287 self
.displayTrickyList()
289 def applyUrlShortcuts(self
, command
, depth
=0):
290 words
= command
.split()
292 numArgs
, shortcut
= self
.settings
.urlShortcuts
[words
[0]]
293 except (IndexError, KeyError):
296 for arg
in range(numArgs
):
300 # last argument gets all remaining words
301 repl
= ' '.join(words
[i
:])
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
)
313 return self
.applyUrlShortcuts(shortcut
, depth
+1)
317 def go(self
, command
):
318 url
= self
.applyUrlShortcuts(command
)
321 def gotoNewUrl(self
, url
):
323 self
.history
.append(self
.itext
.url
)
325 # Turns out to be neater to have the current document in unHistory:
326 self
.unHistory
= [url
]
327 def goHistory(self
, num
=1):
329 self
.gotoUrl(self
.history
[-num
])
331 self
.unHistory
.append(self
.history
.pop())
334 def goUnHistory(self
, num
=1):
336 self
.gotoUrl(self
.unHistory
[-num
-1])
338 self
.history
.append(self
.unHistory
.pop())
342 def gotoUrl(self
, url
, noCache
=False):
345 m
= re
.match('(.*)#(.*)$', url
)
347 baseUrl
, anchor
= m
.groups()
349 baseUrl
, anchor
= url
, None
350 cached
= self
.findCachedDocument(baseUrl
)
351 if cached
and not noCache
:
354 self
.displayCleanCentred(self
.geom
.commandline
, self
.abbreviateStr("Loading %s" % url
))
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
))
370 self
.interruptible
= True
371 n
= self
.itext
.findAnchor(anchor
)
372 self
.interruptible
= False
375 self
.itext
.seekWord(n
, setMark
=True)
377 self
.displayCleanCentred(self
.geom
.commandline
,
378 self
.abbreviateStr("No such anchor: %s" % anchor
))
381 self
.displayCleanCentred(self
.geom
.infoline
, self
.abbreviateStr("%s" % baseUrl
))
384 self
.cacheDocument(self
.itext
)
387 self
.interruptible
= False
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
404 def interruptSpeech(self
):
406 self
.speechsub
.kill()
407 self
.speechsub
= None
409 def saySentence(self
):
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
)
426 w
= self
.itext
.getWord(i
)
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
)
438 espeakps
= Popen( espeakcmd
.split(),
439 shell
=False, stdin
=PIPE
, stdout
=PIPE
, stderr
=None)
440 espeakps
.stdin
.write(text
.encode("utf-8"))
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()
449 self
.displayCleanCentred(self
.geom
.commandline
,
450 "Speech command failed; disabling. Edit "
451 +"sayGenSpeech and sayPlaySpeech in ~/.flinksrc")
455 def showCurrentWord(self
, word
=None):
457 word
= self
.itext
.currentWord()
459 tricky
= self
.isTricky(word
)
460 linknum
= self
.itext
.currentLink()
461 quote
= self
.itext
.inQuote()
464 if tricky
or (self
.boldQuotes
and quote
):
467 self
.displayCleanCentred(self
.geom
.wordline
, word
, attr
)
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
,
478 if self
.speech
and self
.paused
:
479 self
.sayText(word
, quote
=quote
)
482 self
.addTricky(self
.cleanWord(word
))
484 if self
.showContext
and self
.paused
:
486 contextFrom
= self
.itext
.atWord
488 w
= self
.itext
.getWord(contextFrom
-1)
489 if self
.itext
.atSentenceEnd(contextFrom
-1):
491 if self
.itext
.atParaEnd(contextFrom
-1):
493 if w
and (len(w
) + len(contextBefore
)
494 + len(word
)//2 + 1) < self
.geom
.maxx
//3:
495 contextBefore
= w
+ ' ' + contextBefore
501 contextTo
= self
.itext
.atWord
504 if self
.itext
.atSentenceEnd(contextTo
):
506 if self
.itext
.atParaEnd(contextTo
):
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
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()
526 def sigHandlerQuit(sig
, frame
):
528 signal
.signal(15, sigHandlerQuit
)
530 def sigHandlerInt(sig
, frame
):
531 # Occasionally, we can safely allow ^C to interrupt a section of
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
540 countarg
= self
.bindableMethods
[method
][2]
542 if len(args
) >= countarg
:
544 currentArg
= args
[countarg
-1]
545 if (currentArg
.__class
__ == int and
547 self
.count
= -self
.count
549 args
[countarg
-1] = self
.count
551 elif len(args
)+1 == countarg
:
557 self
.counting
= False
558 self
.countWin
.erase()
559 self
.countWin
.refresh()
568 self
.go(self
.initialUrl
)
575 if self
.blankSpace
> 0:
577 self
.displayCleanCentred(self
.geom
.wordline
, "")
580 word
= self
.itext
.readWord()
583 if self
.itext
.atParaEnd():
584 self
.blankSpace
= self
.paraBlankSpace
585 elif self
.itext
.atSentenceEnd():
586 self
.blankSpace
= self
.sentenceBlankSpace
590 if word
[-1] in "'\"":
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
):
611 escBound
= 27 in self
.settings
.keyBindings
628 if c
== curses
.KEY_RESIZE
:
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()
637 binding
= self
.settings
.keyBindings
.get(c
)
638 if binding
is not None:
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
655 def __init__(self
, type, time
):
656 if type not in ["word", "blank"]:
657 raise "BUG: unknown Event type"
660 nextEvent
= Event("word", time())
661 if self
.blink
: nextBlinkTime
= time() + self
.blinkDelay
663 # compensate for blanking at pauses and so on:
673 wait
= nextEvent
.time
- time()
675 sleep(min(wait
, maxStep
))
680 except KeyboardInterrupt:
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",
693 nextBlinkTime
= time() + self
.blinkDelay
696 if nextEvent
.type == "word":
698 if self
.blink
and nextBlinkTime
< time() and (
699 not self
.blinkWaitSentence
or
700 self
.itext
.atSentenceEnd()):
701 self
.displayCleanCentred(self
.geom
.wordline
,
703 nextBlinkTime
= time() + self
.blinkDelay
704 waitTime
= self
.blinkTime
708 if self
.blankBetween
:
709 nextEvent
= Event("blank",
710 t
+ waitTime
- delay
/3)
712 nextEvent
= Event("word",
714 elif nextEvent
.type == "blank":
715 self
.clearLine(self
.geom
.wordline
)
717 nextEvent
= Event("word",
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.
729 raise "Curses returned ERR - terminal too short?"
730 except KeyboardInterrupt:
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
):
744 self
.writeMarksFile(self
.globalMarks
)
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],
758 'FollowLink' : [1,1,1],
762 'SearchAgain' : [0,1,1],
772 'Toggle' : [1,1,0,True],
777 def doKeyChangeSpeed(self
, amount
):
779 if self
.wpm
< 30: self
.wpm
= 30
782 def doKeySeekSentence(self
, n
=1):
783 self
.itext
.seekIndexRel(self
.itext
.sentenceIndex
, n
)
785 def doKeySeekParagraph(self
, n
=1):
786 self
.itext
.seekIndexRel(self
.itext
.paraIndex
, n
)
788 def doKeySeekWord(self
, n
=1):
789 self
.itext
.seekWordRel(n
)
791 def doKeySeekLink(self
, n
=1):
792 self
.itext
.seekIndexRel(self
.itext
.linkIndex
, n
)
795 def doKeySeekStart(self
):
796 self
.itext
.seekWord(0, setMark
=True)
798 def doKeySeekEnd(self
):
802 def doKeyFollowLink(self
, n
):
803 if not self
.itext
.links
: return
805 if self
.geom
.linkDisplayNum
> 0:
807 nl
= self
.itext
.linkIndex
.at
% self
.geom
.linkDisplayNum
809 nl
= self
.itext
.linkIndex
.at
812 linknum
= self
.itext
.linkIndex
.at
+ (n
-nl
)
814 linknum
= self
.itext
.linkIndex
.at
- self
.geom
.linkDisplayNum
+ (n
-nl
)
816 url
= self
.itext
.links
[linknum
]["url"]
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:
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])
839 pad
= curses
.newpad(1, 200)
841 box
= TextboxPad(pad
,
842 self
.geom
.commandline
, boxX
,
843 self
.geom
.commandline
, boxX
+ boxLen
- 1,
845 getCompletions
=getCompletions
)
850 class cancelReadline(Exception):
852 def cancelValidate(ch
):
853 if ch
== ascii
.BEL
or ch
== 27:
854 # ^G and escape cancel the readLine
859 read
= box
.edit(cancelValidate
)
860 if history
is not None:
861 history
.append(read
.strip())
863 except cancelReadline
:
867 self
.clearLine(self
.geom
.commandline
)
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
] += '/'
882 command
= self
.readLine("Go: ", history
=self
.urlHistory
,
883 getCompletions
=filenameCompletion
)
885 self
.go(command
.strip())
887 def doKeyGoCurrent(self
):
888 command
= self
.readLine("Go: ", self
.itext
.url
, history
=self
.urlHistory
)
890 self
.go(command
.strip())
892 def charOfSearchDir(self
, dir=1):
893 if dir == 1: return '/'
895 def search(self
, pattern
, n
=1):
897 self
.displayCleanCentred(self
.geom
.commandline
,
898 self
.charOfSearchDir(dir)+pattern
)
900 self
.interruptible
= True
901 for i
in range(abs(n
)):
902 ret
= self
.itext
.search(pattern
.strip(), dir)
906 self
.interruptible
= False
907 self
.clearLine(self
.geom
.commandline
)
908 def doKeySearch(self
, n
=1):
910 pattern
= self
.readLine("Search: ",
911 shortPrompt
= self
.charOfSearchDir(dir),
912 history
=self
.searchHistory
)
914 self
.search(pattern
, n
)
915 self
.lastSearch
= {"pattern": pattern
, "dir": dir}
916 def doKeySearchAgain(self
, n
=1):
918 self
.search(self
.lastSearch
["pattern"],
919 self
.lastSearch
["dir"]*n
)
921 def doKeyPause(self
):
922 self
.paused
= not self
.paused
925 self
.showCurrentWord()
927 def doKeyHistory(self
, n
=-1):
933 def doKeyCount(self
):
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
:
944 addstrwrapped(self
.countWin
, None, None, prompt
[:self
.geom
.maxx
])
945 self
.countWin
.refresh()
948 key
= self
.blockGetKey(self
.geom
.commandline
,
949 self
.abbreviate
and "m" or "Set mark:")
950 if key
is None or len(key
) != 1:
952 if 'a' <= key
<= 'z':
955 elif 'A' <= key
<= 'Z':
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:
964 if 'a' <= key
<= 'z' or key
== '\'':
966 self
.itext
.goMark(key
)
968 elif key
in self
.globalMarks
:
970 url
, pos
= self
.globalMarks
[key
]
971 if url
!= self
.itext
.url
:
973 self
.itext
.seekWord(pos
, setMark
=True)
976 def doKeyReload(self
):
977 n
= self
.itext
.atWord
978 self
.gotoUrl(self
.itext
.url
, True)
979 self
.itext
.seekWord(n
)
982 def doKeyCommand(self
):
984 self
.displayCleanCentred(self
.geom
.commandline
, err
)
985 command
= self
.readLine(':', shortPrompt
=':',
986 history
=self
.commandHistory
)
988 self
.settings
.processCommand(command
, handleErr
)
993 self
.normalWpm
= self
.wpm
994 self
.wpm
= self
.skimWpm
997 self
.skimWpm
= self
.wpm
998 self
.wpm
= self
.normalWpm
1002 def doKeySkipLinks(self
):
1003 sInd
= self
.itext
.sentenceIndex
1004 lInd
= self
.itext
.linkIndex
1008 while ns
is not None and lInd
[lInd
.findIndex(ns
-1)] >= ps
:
1013 self
.itext
.seekWord(ns
, setMark
=True)
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
):