blob: 323ff5ab77fd11ac0aca5d47c745217a954ae6a7 [file] [log] [blame]
Guido van Rossum9cf8f331992-03-30 10:54:51 +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 windows.append(win)
115 imove(win, ref)
116
117# Create the 'Ibrowse' menu for a new browser window.
118#
119def makemainmenu(win):
120 mp = win.menucreate('Ibrowse')
121 mp.callback = []
122 additem(mp, 'New window (clone)', 'K', iclone)
123 additem(mp, 'Help (tutorial)', 'H', itutor)
124 additem(mp, 'Command summary', '?', isummary)
125 additem(mp, 'Close this window', 'W', iclose)
126 additem(mp, '', '', None)
127 additem(mp, 'Copy to clipboard', 'C', icopy)
128 additem(mp, '', '', None)
129 additem(mp, 'Search regexp...', 'S', isearch)
130 additem(mp, '', '', None)
131 additem(mp, 'Reset node cache', '', iresetnodecache)
132 additem(mp, 'Reset entire cache', '', iresetcache)
133 additem(mp, '', '', None)
134 additem(mp, 'Quit', 'Q', iquit)
135 return mp
136
137# Create the 'Navigation' menu for a new browser window.
138#
139def makenavimenu(win):
140 mp = win.menucreate('Navigation')
141 mp.callback = []
142 additem(mp, 'Menu item...', 'M', imenu)
143 additem(mp, 'Follow reference...', 'F', ifollow)
144 additem(mp, 'Go to node...', 'G', igoto)
145 additem(mp, '', '', None)
146 additem(mp, 'Next node in tree', 'N', inext)
147 additem(mp, 'Previous node in tree', 'P', iprev)
148 additem(mp, 'Up in tree', 'U', iup)
149 additem(mp, 'Last visited node', 'L', ilast)
150 additem(mp, 'Top of tree', 'T', itop)
151 additem(mp, 'Directory node', 'D', idir)
152 return mp
153
154# Add an item to a menu, and a function to its list of callbacks.
155# (Specifying all in one call is the only way to keep the menu
156# and the list of callbacks in synchrony.)
157#
158def additem(mp, text, shortcut, function):
159 if shortcut:
160 mp.additem(text, shortcut)
161 else:
162 mp.additem(text)
163 mp.callback.append(function)
164
165
166# Stdwin event processing main loop.
167# Return when there are no windows left.
168# Note that windows not in the windows list don't get their events.
169#
170def mainloop():
171 while windows:
172 event = stdwin.getevent()
173 if event[1] in windows:
174 try:
175 event[1].dispatch(event)
176 except KeyboardInterrupt:
177 # The user can type Control-C (or whatever)
178 # to leave the browser without closing
179 # the window. Mainly useful for
180 # debugging.
181 break
182 except:
183 # During debugging, it was annoying if
184 # every mistake in a callback caused the
185 # whole browser to crash, hence this
186 # handler. In a production version
187 # it may be better to disable this.
188 #
189 msg = sys.exc_type
190 if sys.exc_value:
191 val = sys.exc_value
192 if type(val) <> type(''):
193 val = `val`
194 msg = msg + ': ' + val
195 msg = 'Oops, an exception occurred: ' + msg
196 event = None
197 stdwin.message(msg)
198 event = None
199
200
201# Handle one event. The window is taken from the event's window item.
202# This function is placed as a method (named 'dispatch') on the window,
203# so the main loop will be able to handle windows of a different kind
204# as well, as long as they are all placed in the list of windows.
205#
206def idispatch(event):
207 type, win, detail = event
208 if type == WE_CHAR:
209 if not keybindings.has_key(detail):
210 detail = string.lower(detail)
211 if keybindings.has_key(detail):
212 keybindings[detail](win)
213 return
214 if detail in '0123456789':
215 i = eval(detail) - 1
216 if 0 <= i < len(win.menu):
217 topic, ref = win.menu[i]
218 imove(win, ref)
219 return
220 stdwin.fleep()
221 return
222 if type == WE_COMMAND:
223 if detail == WC_LEFT:
224 iprev(win)
225 elif detail == WC_RIGHT:
226 inext(win)
227 elif detail == WC_UP:
228 iup(win)
229 elif detail == WC_DOWN:
230 idown(win)
231 elif detail == WC_BACKSPACE:
232 ibackward(win)
233 elif detail == WC_RETURN:
234 idown(win)
235 else:
236 stdwin.fleep()
237 return
238 if type == WE_MENU:
239 mp, item = detail
240 if mp == None:
241 pass # A THINK C console menu was selected
242 elif mp in (win.mainmenu, win.navimenu):
243 mp.callback[item](win)
244 elif mp == win.nodemenu:
245 topic, ref = win.menu[item]
246 imove(win, ref)
247 elif mp == win.footmenu:
248 topic, ref = win.footnotes[item]
249 imove(win, ref)
250 return
251 if type == WE_SIZE:
252 win.textobj.move((0, 0), win.getwinsize())
253 (left, top), (right, bottom) = win.textobj.getrect()
254 win.setdocsize(0, bottom)
255 return
256 if type == WE_CLOSE:
257 iclose(win)
258 return
259 if not win.textobj.event(event):
260 pass
261
262
263# Paging callbacks
264
265def ibeginning(win):
266 win.setorigin(0, 0)
267 win.textobj.setfocus(0, 0) # To restart searches
268
269def iforward(win):
270 lh = stdwin.lineheight() # XXX Should really use the window's...
271 h, v = win.getorigin()
272 docwidth, docheight = win.getdocsize()
273 width, height = win.getwinsize()
274 if v + height >= docheight:
275 stdwin.fleep()
276 return
277 increment = max(lh, ((height - 2*lh) / lh) * lh)
278 v = v + increment
279 win.setorigin(h, v)
280
281def ibackward(win):
282 lh = stdwin.lineheight() # XXX Should really use the window's...
283 h, v = win.getorigin()
284 if v <= 0:
285 stdwin.fleep()
286 return
287 width, height = win.getwinsize()
288 increment = max(lh, ((height - 2*lh) / lh) * lh)
289 v = max(0, v - increment)
290 win.setorigin(h, v)
291
292
293# Ibrowse menu callbacks
294
295def iclone(win):
296 stdwin.setdefwinsize(win.getwinsize())
297 makewindow(win.file, win.node)
298
299def itutor(win):
300 # The course looks best at 76x22...
301 stdwin.setdefwinsize(76*stdwin.textwidth('x'), 22*stdwin.lineheight())
302 makewindow('ibrowse', 'Help')
303
304def isummary(win):
305 stdwin.setdefwinsize(76*stdwin.textwidth('x'), 22*stdwin.lineheight())
306 makewindow('ibrowse', 'Summary')
307
308def iclose(win):
309 #
310 # Remove the window from the windows list so the mainloop
311 # will notice if all windows are gone.
312 # Delete the textobj since it constitutes a circular reference
313 # to the window which would prevent it from being closed.
314 # (Deletion is done by assigning None to avoid crashes
315 # when closing a half-initialized window.)
316 #
317 if win in windows:
318 windows.remove(win)
319 win.textobj = None
320
321def icopy(win):
322 focustext = win.textobj.getfocustext()
323 if not focustext:
324 stdwin.fleep()
325 else:
326 stdwin.rotatecutbuffers(1)
327 stdwin.setcutbuffer(0, focustext)
328 # XXX Should also set the primary selection...
329
330def isearch(win):
331 try:
332 pat = stdwin.askstr('Search pattern:', win.pat)
333 except KeyboardInterrupt:
334 return
335 if not pat:
336 pat = win.pat
337 if not pat:
338 stdwin.message('No previous pattern')
339 return
340 try:
341 cpat = regexp.compile(pat)
342 except regexp.error, msg:
343 stdwin.message('Bad pattern: ' + msg)
344 return
345 win.pat = pat
346 f1, f2 = win.textobj.getfocus()
347 text = win.text
348 match = cpat.match(text, f2)
349 if not match:
350 stdwin.fleep()
351 return
352 a, b = match[0]
353 win.textobj.setfocus(a, b)
354
355
356def iresetnodecache(win):
357 icache.resetnodecache()
358
359def iresetcache(win):
360 icache.resetcache()
361
362def iquit(win):
363 for win in windows[:]:
364 iclose(win)
365
366
367# Navigation menu callbacks
368
369def imenu(win):
370 ichoice(win, 'Menu item (abbreviated):', win.menu, whichmenuitem(win))
371
372def ifollow(win):
373 ichoice(win, 'Follow reference named (abbreviated):', \
374 win.footnotes, whichfootnote(win))
375
376def igoto(win):
377 try:
378 choice = stdwin.askstr('Go to node (full name):', '')
379 except KeyboardInterrupt:
380 return
381 if not choice:
382 stdwin.message('Sorry, Go to has no default')
383 return
384 imove(win, choice)
385
386def inext(win):
387 prev, next, up = win.header
388 if next:
389 imove(win, next)
390 else:
391 stdwin.fleep()
392
393def iprev(win):
394 prev, next, up = win.header
395 if prev:
396 imove(win, prev)
397 else:
398 stdwin.fleep()
399
400def iup(win):
401 prev, next, up = win.header
402 if up:
403 imove(win, up)
404 else:
405 stdwin.fleep()
406
407def ilast(win):
408 if not win.last:
409 stdwin.fleep()
410 else:
411 i = len(win.last)-1
412 lastnode, lastfocus = win.last[i]
413 imove(win, lastnode)
414 if len(win.last) > i+1:
415 # The move succeeded -- restore the focus
416 win.textobj.setfocus(lastfocus)
417 # Delete the stack top even if the move failed,
418 # else the whole stack would remain unreachable
419 del win.last[i:] # Delete the entry pushed by imove as well!
420
421def itop(win):
422 imove(win, '')
423
424def idir(win):
425 imove(win, '(dir)')
426
427
428# Special and generic callbacks
429
430def idown(win):
431 if win.menu:
432 default = whichmenuitem(win)
433 for topic, ref in win.menu:
434 if default == topic:
435 break
436 else:
437 topic, ref = win.menu[0]
438 imove(win, ref)
439 else:
440 inext(win)
441
442def ichoice(win, prompt, list, default):
443 if not list:
444 stdwin.fleep()
445 return
446 if not default:
447 topic, ref = list[0]
448 default = topic
449 try:
450 choice = stdwin.askstr(prompt, default)
451 except KeyboardInterrupt:
452 return
453 if not choice:
454 return
455 choice = string.lower(choice)
456 n = len(choice)
457 for topic, ref in list:
458 topic = string.lower(topic)
459 if topic[:n] == choice:
460 imove(win, ref)
461 return
462 stdwin.message('Sorry, no topic matches ' + `choice`)
463
464
465# Follow a reference, in the same window.
466#
467def imove(win, ref):
468 savetitle = win.gettitle()
469 win.settitle('Looking for ' + ref + '...')
470 #
471 try:
472 file, node, header, menu, footnotes, text = \
473 icache.get_node(win.file, ref)
474 except NoSuchFile, file:
475 win.settitle(savetitle)
476 stdwin.message(\
477 'Sorry, I can\'t find a file named ' + `file` + '.')
478 return
479 except NoSuchNode, node:
480 win.settitle(savetitle)
481 stdwin.message(\
482 'Sorry, I can\'t find a node named ' + `node` + '.')
483 return
484 #
485 win.settitle('Found (' + file + ')' + node + '...')
486 #
487 if win.file and win.node:
488 lastnode = '(' + win.file + ')' + win.node
489 win.last.append(lastnode, win.textobj.getfocus())
490 win.file = file
491 win.node = node
492 win.header = header
493 win.menu = menu
494 win.footnotes = footnotes
495 win.text = text
496 #
497 win.setorigin(0, 0) # Scroll to the beginnning
498 win.textobj.settext(text)
499 win.textobj.setfocus(0, 0)
500 (left, top), (right, bottom) = win.textobj.getrect()
501 win.setdocsize(0, bottom)
502 #
503 win.footmenu = None
504 win.nodemenu = None
505 #
506 win.menu = menu
507 if menu:
508 win.nodemenu = win.menucreate('Menu')
509 digit = 1
510 for topic, ref in menu:
511 if digit < 10:
512 win.nodemenu.additem(topic, `digit`)
513 else:
514 win.nodemenu.additem(topic)
515 digit = digit + 1
516 #
517 win.footnotes = footnotes
518 if footnotes:
519 win.footmenu = win.menucreate('Footnotes')
520 for topic, ref in footnotes:
521 win.footmenu.additem(topic)
522 #
523 win.settitle('(' + win.file + ')' + win.node)
524
525
526# Find menu item at focus
527#
528findmenu = regexp.compile('^\* [mM]enu:').match
529findmenuitem = regexp.compile( \
530 '^\* ([^:]+):[ \t]*(:|\([^\t]*\)[^\t,\n.]*|[^:(][^\t,\n.]*)').match
531#
532def whichmenuitem(win):
533 if not win.menu:
534 return ''
535 match = findmenu(win.text)
536 if not match:
537 return ''
538 a, b = match[0]
539 i = b
540 f1, f2 = win.textobj.getfocus()
541 lastmatch = ''
542 while i < len(win.text):
543 match = findmenuitem(win.text, i)
544 if not match:
545 break
546 (a, b), (a1, b1), (a2, b2) = match
547 if a > f1:
548 break
549 lastmatch = win.text[a1:b1]
550 i = b
551 return lastmatch
552
553
554# Find footnote at focus
555#
556findfootnote = \
557 regexp.compile('\*[nN]ote ([^:]+):[ \t]*(:|[^:][^\t,\n.]*)').match
558#
559def whichfootnote(win):
560 if not win.footnotes:
561 return ''
562 i = 0
563 f1, f2 = win.textobj.getfocus()
564 lastmatch = ''
565 while i < len(win.text):
566 match = findfootnote(win.text, i)
567 if not match:
568 break
569 (a, b), (a1, b1), (a2, b2) = match
570 if a > f1:
571 break
572 lastmatch = win.text[a1:b1]
573 i = b
574 return lastmatch
575
576
577# Now all the "methods" are defined, we can initialize the table
578# of key bindings.
579#
580keybindings = {}
581
582# Window commands
583
584keybindings['k'] = iclone
585keybindings['h'] = itutor
586keybindings['?'] = isummary
587keybindings['w'] = iclose
588
589keybindings['c'] = icopy
590
591keybindings['s'] = isearch
592
593keybindings['q'] = iquit
594
595# Navigation commands
596
597keybindings['m'] = imenu
598keybindings['f'] = ifollow
599keybindings['g'] = igoto
600
601keybindings['n'] = inext
602keybindings['p'] = iprev
603keybindings['u'] = iup
604keybindings['l'] = ilast
605keybindings['d'] = idir
606keybindings['t'] = itop
607
608# Paging commands
609
610keybindings['b'] = ibeginning
611keybindings['.'] = ibeginning
612keybindings[' '] = iforward