blob: 41574adba70b3dbddeda69e73eade091b394654a [file] [log] [blame]
Guido van Rossum5dd52d31995-04-10 11:47:11 +00001# Browser for "Info files" as used by the Emacs documentation system.
2#
3# Now you can read Info files even if you can't spare the memory, time or
4# disk space to run Emacs. (I have used this extensively on a Macintosh
5# with 1 Megabyte main memory and a 20 Meg harddisk.)
6#
7# You can give this to someone with great fear of complex computer
8# systems, as long as they can use a mouse.
9#
10# Another reason to use this is to encourage the use of Info for on-line
11# documentation of software that is not related to Emacs or GNU.
12# (In particular, I plan to redo the Python and STDWIN documentation
13# in texinfo.)
14
15
16# NB: this is not a self-executing script. You must startup Python,
17# import ibrowse, and call ibrowse.main(). On UNIX, the script 'ib'
18# runs the browser.
19
20
21# Configuration:
22#
23# - The pathname of the directory (or directories) containing
24# the standard Info files should be set by editing the
25# value assigned to INFOPATH in module ifile.py.
26#
27# - The default font should be set by editing the value of FONT
28# in this module (ibrowse.py).
29#
30# - For fastest I/O, you may look at BLOCKSIZE and a few other
31# constants in ifile.py.
32
33
34# This is a fairly large Python program, split in the following modules:
35#
36# ibrowse.py Main program and user interface.
37# This is the only module that imports stdwin.
38#
39# ifile.py This module knows about the format of Info files.
40# It is imported by all of the others.
41#
42# itags.py This module knows how to read prebuilt tag tables,
43# including indirect ones used by large texinfo files.
44#
45# icache.py Caches tag tables and visited nodes.
46
47
48# XXX There should really be a different tutorial, as the user interface
49# XXX differs considerably from Emacs...
50
51
52import sys
53import regexp
54import stdwin
55from stdwinevents import *
56import string
57from ifile import NoSuchFile, NoSuchNode
58import icache
59
60
61# Default font.
62# This should be an acceptable argument for stdwin.setfont();
63# on the Mac, this can be a pair (fontname, pointsize), while
64# under X11 it should be a standard X11 font name.
65# For best results, use a constant width font like Courier;
66# many Info files contain tabs that don't align with other text
67# unless all characters have the same width.
68#
69#FONT = ('Monaco', 9) # Mac
70FONT = '-schumacher-clean-medium-r-normal--14-140-75-75-c-70-iso8859-1' # X11
71
72
73# Try not to destroy the list of windows when reload() is used.
74# This is useful during debugging, and harmless in production...
75#
76try:
77 dummy = windows
78 del dummy
79except NameError:
80 windows = []
81
82
83# Default main function -- start at the '(dir)' node.
84#
85def main():
86 start('(dir)')
87
88
89# Start at an arbitrary node.
90# The default file is 'ibrowse'.
91#
92def start(ref):
93 stdwin.setdefscrollbars(0, 1)
94 stdwin.setfont(FONT)
95 stdwin.setdefwinsize(76*stdwin.textwidth('x'), 22*stdwin.lineheight())
96 makewindow('ibrowse', ref)
97 mainloop()
98
99
100# Open a new browser window.
101# Arguments specify the default file and a node reference
102# (if the node reference specifies a file, the default file is ignored).
103#
104def makewindow(file, ref):
105 win = stdwin.open('Info file Browser, by Guido van Rossum')
106 win.mainmenu = makemainmenu(win)
107 win.navimenu = makenavimenu(win)
108 win.textobj = win.textcreate((0, 0), win.getwinsize())
109 win.file = file
110 win.node = ''
111 win.last = []
112 win.pat = ''
113 win.dispatch = idispatch
114 win.nodemenu = None
115 win.footmenu = None
116 windows.append(win)
117 imove(win, ref)
118
119# Create the 'Ibrowse' menu for a new browser window.
120#
121def makemainmenu(win):
122 mp = win.menucreate('Ibrowse')
123 mp.callback = []
124 additem(mp, 'New window (clone)', 'K', iclone)
125 additem(mp, 'Help (tutorial)', 'H', itutor)
126 additem(mp, 'Command summary', '?', isummary)
127 additem(mp, 'Close this window', 'W', iclose)
128 additem(mp, '', '', None)
129 additem(mp, 'Copy to clipboard', 'C', icopy)
130 additem(mp, '', '', None)
131 additem(mp, 'Search regexp...', 'S', isearch)
132 additem(mp, '', '', None)
133 additem(mp, 'Reset node cache', '', iresetnodecache)
134 additem(mp, 'Reset entire cache', '', iresetcache)
135 additem(mp, '', '', None)
136 additem(mp, 'Quit', 'Q', iquit)
137 return mp
138
139# Create the 'Navigation' menu for a new browser window.
140#
141def makenavimenu(win):
142 mp = win.menucreate('Navigation')
143 mp.callback = []
144 additem(mp, 'Menu item...', 'M', imenu)
145 additem(mp, 'Follow reference...', 'F', ifollow)
146 additem(mp, 'Go to node...', 'G', igoto)
147 additem(mp, '', '', None)
148 additem(mp, 'Next node in tree', 'N', inext)
149 additem(mp, 'Previous node in tree', 'P', iprev)
150 additem(mp, 'Up in tree', 'U', iup)
151 additem(mp, 'Last visited node', 'L', ilast)
152 additem(mp, 'Top of tree', 'T', itop)
153 additem(mp, 'Directory node', 'D', idir)
154 return mp
155
156# Add an item to a menu, and a function to its list of callbacks.
157# (Specifying all in one call is the only way to keep the menu
158# and the list of callbacks in synchrony.)
159#
160def additem(mp, text, shortcut, function):
161 if shortcut:
162 mp.additem(text, shortcut)
163 else:
164 mp.additem(text)
165 mp.callback.append(function)
166
167
168# Stdwin event processing main loop.
169# Return when there are no windows left.
170# Note that windows not in the windows list don't get their events.
171#
172def mainloop():
173 while windows:
174 event = stdwin.getevent()
175 if event[1] in windows:
176 try:
177 event[1].dispatch(event)
178 except KeyboardInterrupt:
179 # The user can type Control-C (or whatever)
180 # to leave the browser without closing
181 # the window. Mainly useful for
182 # debugging.
183 break
184 except:
185 # During debugging, it was annoying if
186 # every mistake in a callback caused the
187 # whole browser to crash, hence this
188 # handler. In a production version
189 # it may be better to disable this.
190 #
191 msg = sys.exc_type
192 if sys.exc_value:
193 val = sys.exc_value
194 if type(val) <> type(''):
195 val = `val`
196 msg = msg + ': ' + val
197 msg = 'Oops, an exception occurred: ' + msg
198 event = None
199 stdwin.message(msg)
200 event = None
201
202
203# Handle one event. The window is taken from the event's window item.
204# This function is placed as a method (named 'dispatch') on the window,
205# so the main loop will be able to handle windows of a different kind
206# as well, as long as they are all placed in the list of windows.
207#
208def idispatch(event):
209 type, win, detail = event
210 if type == WE_CHAR:
211 if not keybindings.has_key(detail):
212 detail = string.lower(detail)
213 if keybindings.has_key(detail):
214 keybindings[detail](win)
215 return
216 if detail in '0123456789':
217 i = eval(detail) - 1
218 if i < 0: i = len(win.menu) + i
219 if 0 <= i < len(win.menu):
220 topic, ref = win.menu[i]
221 imove(win, ref)
222 return
223 stdwin.fleep()
224 return
225 if type == WE_COMMAND:
226 if detail == WC_LEFT:
227 iprev(win)
228 elif detail == WC_RIGHT:
229 inext(win)
230 elif detail == WC_UP:
231 iup(win)
232 elif detail == WC_DOWN:
233 idown(win)
234 elif detail == WC_BACKSPACE:
235 ibackward(win)
236 elif detail == WC_RETURN:
237 idown(win)
238 else:
239 stdwin.fleep()
240 return
241 if type == WE_MENU:
242 mp, item = detail
243 if mp == None:
244 pass # A THINK C console menu was selected
245 elif mp in (win.mainmenu, win.navimenu):
246 mp.callback[item](win)
247 elif mp == win.nodemenu:
248 topic, ref = win.menu[item]
249 imove(win, ref)
250 elif mp == win.footmenu:
251 topic, ref = win.footnotes[item]
252 imove(win, ref)
253 return
254 if type == WE_SIZE:
255 win.textobj.move((0, 0), win.getwinsize())
256 (left, top), (right, bottom) = win.textobj.getrect()
257 win.setdocsize(0, bottom)
258 return
259 if type == WE_CLOSE:
260 iclose(win)
261 return
262 if not win.textobj.event(event):
263 pass
264
265
266# Paging callbacks
267
268def ibeginning(win):
269 win.setorigin(0, 0)
270 win.textobj.setfocus(0, 0) # To restart searches
271
272def iforward(win):
273 lh = stdwin.lineheight() # XXX Should really use the window's...
274 h, v = win.getorigin()
275 docwidth, docheight = win.getdocsize()
276 width, height = win.getwinsize()
277 if v + height >= docheight:
278 stdwin.fleep()
279 return
280 increment = max(lh, ((height - 2*lh) / lh) * lh)
281 v = v + increment
282 win.setorigin(h, v)
283
284def ibackward(win):
285 lh = stdwin.lineheight() # XXX Should really use the window's...
286 h, v = win.getorigin()
287 if v <= 0:
288 stdwin.fleep()
289 return
290 width, height = win.getwinsize()
291 increment = max(lh, ((height - 2*lh) / lh) * lh)
292 v = max(0, v - increment)
293 win.setorigin(h, v)
294
295
296# Ibrowse menu callbacks
297
298def iclone(win):
299 stdwin.setdefwinsize(win.getwinsize())
300 makewindow(win.file, win.node)
301
302def itutor(win):
303 # The course looks best at 76x22...
304 stdwin.setdefwinsize(76*stdwin.textwidth('x'), 22*stdwin.lineheight())
305 makewindow('ibrowse', 'Help')
306
307def isummary(win):
308 stdwin.setdefwinsize(76*stdwin.textwidth('x'), 22*stdwin.lineheight())
309 makewindow('ibrowse', 'Summary')
310
311def iclose(win):
312 #
313 # Remove the window from the windows list so the mainloop
314 # will notice if all windows are gone.
315 # Delete the textobj since it constitutes a circular reference
316 # to the window which would prevent it from being closed.
317 # (Deletion is done by assigning None to avoid crashes
318 # when closing a half-initialized window.)
319 #
320 if win in windows:
321 windows.remove(win)
322 win.textobj = None
323
324def icopy(win):
325 focustext = win.textobj.getfocustext()
326 if not focustext:
327 stdwin.fleep()
328 else:
329 stdwin.rotatecutbuffers(1)
330 stdwin.setcutbuffer(0, focustext)
331 # XXX Should also set the primary selection...
332
333def isearch(win):
334 try:
335 pat = stdwin.askstr('Search pattern:', win.pat)
336 except KeyboardInterrupt:
337 return
338 if not pat:
339 pat = win.pat
340 if not pat:
341 stdwin.message('No previous pattern')
342 return
343 try:
344 cpat = regexp.compile(pat)
345 except regexp.error, msg:
346 stdwin.message('Bad pattern: ' + msg)
347 return
348 win.pat = pat
349 f1, f2 = win.textobj.getfocus()
350 text = win.text
351 match = cpat.match(text, f2)
352 if not match:
353 stdwin.fleep()
354 return
355 a, b = match[0]
356 win.textobj.setfocus(a, b)
357
358
359def iresetnodecache(win):
360 icache.resetnodecache()
361
362def iresetcache(win):
363 icache.resetcache()
364
365def iquit(win):
366 for win in windows[:]:
367 iclose(win)
368
369
370# Navigation menu callbacks
371
372def imenu(win):
373 ichoice(win, 'Menu item (abbreviated):', win.menu, whichmenuitem(win))
374
375def ifollow(win):
376 ichoice(win, 'Follow reference named (abbreviated):', \
377 win.footnotes, whichfootnote(win))
378
379def igoto(win):
380 try:
381 choice = stdwin.askstr('Go to node (full name):', '')
382 except KeyboardInterrupt:
383 return
384 if not choice:
385 stdwin.message('Sorry, Go to has no default')
386 return
387 imove(win, choice)
388
389def inext(win):
390 prev, next, up = win.header
391 if next:
392 imove(win, next)
393 else:
394 stdwin.fleep()
395
396def iprev(win):
397 prev, next, up = win.header
398 if prev:
399 imove(win, prev)
400 else:
401 stdwin.fleep()
402
403def iup(win):
404 prev, next, up = win.header
405 if up:
406 imove(win, up)
407 else:
408 stdwin.fleep()
409
410def ilast(win):
411 if not win.last:
412 stdwin.fleep()
413 else:
414 i = len(win.last)-1
415 lastnode, lastfocus = win.last[i]
416 imove(win, lastnode)
417 if len(win.last) > i+1:
418 # The move succeeded -- restore the focus
419 win.textobj.setfocus(lastfocus)
420 # Delete the stack top even if the move failed,
421 # else the whole stack would remain unreachable
422 del win.last[i:] # Delete the entry pushed by imove as well!
423
424def itop(win):
425 imove(win, '')
426
427def idir(win):
428 imove(win, '(dir)')
429
430
431# Special and generic callbacks
432
433def idown(win):
434 if win.menu:
435 default = whichmenuitem(win)
436 for topic, ref in win.menu:
437 if default == topic:
438 break
439 else:
440 topic, ref = win.menu[0]
441 imove(win, ref)
442 else:
443 inext(win)
444
445def ichoice(win, prompt, list, default):
446 if not list:
447 stdwin.fleep()
448 return
449 if not default:
450 topic, ref = list[0]
451 default = topic
452 try:
453 choice = stdwin.askstr(prompt, default)
454 except KeyboardInterrupt:
455 return
456 if not choice:
457 return
458 choice = string.lower(choice)
459 n = len(choice)
460 for topic, ref in list:
461 topic = string.lower(topic)
462 if topic[:n] == choice:
463 imove(win, ref)
464 return
465 stdwin.message('Sorry, no topic matches ' + `choice`)
466
467
468# Follow a reference, in the same window.
469#
470def imove(win, ref):
471 savetitle = win.gettitle()
472 win.settitle('Looking for ' + ref + '...')
473 #
474 try:
475 file, node, header, menu, footnotes, text = \
476 icache.get_node(win.file, ref)
477 except NoSuchFile, file:
478 win.settitle(savetitle)
479 stdwin.message(\
480 'Sorry, I can\'t find a file named ' + `file` + '.')
481 return
482 except NoSuchNode, node:
483 win.settitle(savetitle)
484 stdwin.message(\
485 'Sorry, I can\'t find a node named ' + `node` + '.')
486 return
487 #
488 win.settitle('Found (' + file + ')' + node + '...')
489 #
490 if win.file and win.node:
491 lastnode = '(' + win.file + ')' + win.node
492 win.last.append(lastnode, win.textobj.getfocus())
493 win.file = file
494 win.node = node
495 win.header = header
496 win.menu = menu
497 win.footnotes = footnotes
498 win.text = text
499 #
500 win.setorigin(0, 0) # Scroll to the beginnning
501 win.textobj.settext(text)
502 win.textobj.setfocus(0, 0)
503 (left, top), (right, bottom) = win.textobj.getrect()
504 win.setdocsize(0, bottom)
505 #
506 if win.footmenu: win.footmenu.close()
507 if win.nodemenu: win.nodemenu.close()
508 win.footmenu = None
509 win.nodemenu = None
510 #
511 win.menu = menu
512 if menu:
513 win.nodemenu = win.menucreate('Menu')
514 digit = 1
515 for topic, ref in menu:
516 if digit < 10:
517 win.nodemenu.additem(topic, `digit`)
518 else:
519 win.nodemenu.additem(topic)
520 digit = digit + 1
521 #
522 win.footnotes = footnotes
523 if footnotes:
524 win.footmenu = win.menucreate('Footnotes')
525 for topic, ref in footnotes:
526 win.footmenu.additem(topic)
527 #
528 win.settitle('(' + win.file + ')' + win.node)
529
530
531# Find menu item at focus
532#
533findmenu = regexp.compile('^\* [mM]enu:').match
534findmenuitem = regexp.compile( \
535 '^\* ([^:]+):[ \t]*(:|\([^\t]*\)[^\t,\n.]*|[^:(][^\t,\n.]*)').match
536#
537def whichmenuitem(win):
538 if not win.menu:
539 return ''
540 match = findmenu(win.text)
541 if not match:
542 return ''
543 a, b = match[0]
544 i = b
545 f1, f2 = win.textobj.getfocus()
546 lastmatch = ''
547 while i < len(win.text):
548 match = findmenuitem(win.text, i)
549 if not match:
550 break
551 (a, b), (a1, b1), (a2, b2) = match
552 if a > f1:
553 break
554 lastmatch = win.text[a1:b1]
555 i = b
556 return lastmatch
557
558
559# Find footnote at focus
560#
561findfootnote = \
562 regexp.compile('\*[nN]ote ([^:]+):[ \t]*(:|[^:][^\t,\n.]*)').match
563#
564def whichfootnote(win):
565 if not win.footnotes:
566 return ''
567 i = 0
568 f1, f2 = win.textobj.getfocus()
569 lastmatch = ''
570 while i < len(win.text):
571 match = findfootnote(win.text, i)
572 if not match:
573 break
574 (a, b), (a1, b1), (a2, b2) = match
575 if a > f1:
576 break
577 lastmatch = win.text[a1:b1]
578 i = b
579 return lastmatch
580
581
582# Now all the "methods" are defined, we can initialize the table
583# of key bindings.
584#
585keybindings = {}
586
587# Window commands
588
589keybindings['k'] = iclone
590keybindings['h'] = itutor
591keybindings['?'] = isummary
592keybindings['w'] = iclose
593
594keybindings['c'] = icopy
595
596keybindings['s'] = isearch
597
598keybindings['q'] = iquit
599
600# Navigation commands
601
602keybindings['m'] = imenu
603keybindings['f'] = ifollow
604keybindings['g'] = igoto
605
606keybindings['n'] = inext
607keybindings['p'] = iprev
608keybindings['u'] = iup
609keybindings['l'] = ilast
610keybindings['d'] = idir
611keybindings['t'] = itop
612
613# Paging commands
614
615keybindings['b'] = ibeginning
616keybindings['.'] = ibeginning
617keybindings[' '] = iforward