| # Browser for "Info files" as used by the Emacs documentation system. |
| # |
| # Now you can read Info files even if you can't spare the memory, time or |
| # disk space to run Emacs. (I have used this extensively on a Macintosh |
| # with 1 Megabyte main memory and a 20 Meg harddisk.) |
| # |
| # You can give this to someone with great fear of complex computer |
| # systems, as long as they can use a mouse. |
| # |
| # Another reason to use this is to encourage the use of Info for on-line |
| # documentation of software that is not related to Emacs or GNU. |
| # (In particular, I plan to redo the Python and STDWIN documentation |
| # in texinfo.) |
| |
| |
| # NB: this is not a self-executing script. You must startup Python, |
| # import ibrowse, and call ibrowse.main(). On UNIX, the script 'ib' |
| # runs the browser. |
| |
| |
| # Configuration: |
| # |
| # - The pathname of the directory (or directories) containing |
| # the standard Info files should be set by editing the |
| # value assigned to INFOPATH in module ifile.py. |
| # |
| # - The default font should be set by editing the value of FONT |
| # in this module (ibrowse.py). |
| # |
| # - For fastest I/O, you may look at BLOCKSIZE and a few other |
| # constants in ifile.py. |
| |
| |
| # This is a fairly large Python program, split in the following modules: |
| # |
| # ibrowse.py Main program and user interface. |
| # This is the only module that imports stdwin. |
| # |
| # ifile.py This module knows about the format of Info files. |
| # It is imported by all of the others. |
| # |
| # itags.py This module knows how to read prebuilt tag tables, |
| # including indirect ones used by large texinfo files. |
| # |
| # icache.py Caches tag tables and visited nodes. |
| |
| |
| # XXX There should really be a different tutorial, as the user interface |
| # XXX differs considerably from Emacs... |
| |
| |
| import sys |
| import regexp |
| import stdwin |
| from stdwinevents import * |
| import string |
| from ifile import NoSuchFile, NoSuchNode |
| import icache |
| |
| |
| # Default font. |
| # This should be an acceptable argument for stdwin.setfont(); |
| # on the Mac, this can be a pair (fontname, pointsize), while |
| # under X11 it should be a standard X11 font name. |
| # For best results, use a constant width font like Courier; |
| # many Info files contain tabs that don't align with other text |
| # unless all characters have the same width. |
| # |
| #FONT = ('Monaco', 9) # Mac |
| FONT = '-schumacher-clean-medium-r-normal--14-140-75-75-c-70-iso8859-1' # X11 |
| |
| |
| # Try not to destroy the list of windows when reload() is used. |
| # This is useful during debugging, and harmless in production... |
| # |
| try: |
| dummy = windows |
| del dummy |
| except NameError: |
| windows = [] |
| |
| |
| # Default main function -- start at the '(dir)' node. |
| # |
| def main(): |
| start('(dir)') |
| |
| |
| # Start at an arbitrary node. |
| # The default file is 'ibrowse'. |
| # |
| def start(ref): |
| stdwin.setdefscrollbars(0, 1) |
| stdwin.setfont(FONT) |
| stdwin.setdefwinsize(76*stdwin.textwidth('x'), 22*stdwin.lineheight()) |
| makewindow('ibrowse', ref) |
| mainloop() |
| |
| |
| # Open a new browser window. |
| # Arguments specify the default file and a node reference |
| # (if the node reference specifies a file, the default file is ignored). |
| # |
| def makewindow(file, ref): |
| win = stdwin.open('Info file Browser, by Guido van Rossum') |
| win.mainmenu = makemainmenu(win) |
| win.navimenu = makenavimenu(win) |
| win.textobj = win.textcreate((0, 0), win.getwinsize()) |
| win.file = file |
| win.node = '' |
| win.last = [] |
| win.pat = '' |
| win.dispatch = idispatch |
| win.nodemenu = None |
| win.footmenu = None |
| windows.append(win) |
| imove(win, ref) |
| |
| # Create the 'Ibrowse' menu for a new browser window. |
| # |
| def makemainmenu(win): |
| mp = win.menucreate('Ibrowse') |
| mp.callback = [] |
| additem(mp, 'New window (clone)', 'K', iclone) |
| additem(mp, 'Help (tutorial)', 'H', itutor) |
| additem(mp, 'Command summary', '?', isummary) |
| additem(mp, 'Close this window', 'W', iclose) |
| additem(mp, '', '', None) |
| additem(mp, 'Copy to clipboard', 'C', icopy) |
| additem(mp, '', '', None) |
| additem(mp, 'Search regexp...', 'S', isearch) |
| additem(mp, '', '', None) |
| additem(mp, 'Reset node cache', '', iresetnodecache) |
| additem(mp, 'Reset entire cache', '', iresetcache) |
| additem(mp, '', '', None) |
| additem(mp, 'Quit', 'Q', iquit) |
| return mp |
| |
| # Create the 'Navigation' menu for a new browser window. |
| # |
| def makenavimenu(win): |
| mp = win.menucreate('Navigation') |
| mp.callback = [] |
| additem(mp, 'Menu item...', 'M', imenu) |
| additem(mp, 'Follow reference...', 'F', ifollow) |
| additem(mp, 'Go to node...', 'G', igoto) |
| additem(mp, '', '', None) |
| additem(mp, 'Next node in tree', 'N', inext) |
| additem(mp, 'Previous node in tree', 'P', iprev) |
| additem(mp, 'Up in tree', 'U', iup) |
| additem(mp, 'Last visited node', 'L', ilast) |
| additem(mp, 'Top of tree', 'T', itop) |
| additem(mp, 'Directory node', 'D', idir) |
| return mp |
| |
| # Add an item to a menu, and a function to its list of callbacks. |
| # (Specifying all in one call is the only way to keep the menu |
| # and the list of callbacks in synchrony.) |
| # |
| def additem(mp, text, shortcut, function): |
| if shortcut: |
| mp.additem(text, shortcut) |
| else: |
| mp.additem(text) |
| mp.callback.append(function) |
| |
| |
| # Stdwin event processing main loop. |
| # Return when there are no windows left. |
| # Note that windows not in the windows list don't get their events. |
| # |
| def mainloop(): |
| while windows: |
| event = stdwin.getevent() |
| if event[1] in windows: |
| try: |
| event[1].dispatch(event) |
| except KeyboardInterrupt: |
| # The user can type Control-C (or whatever) |
| # to leave the browser without closing |
| # the window. Mainly useful for |
| # debugging. |
| break |
| except: |
| # During debugging, it was annoying if |
| # every mistake in a callback caused the |
| # whole browser to crash, hence this |
| # handler. In a production version |
| # it may be better to disable this. |
| # |
| msg = sys.exc_type |
| if sys.exc_value: |
| val = sys.exc_value |
| if type(val) <> type(''): |
| val = `val` |
| msg = msg + ': ' + val |
| msg = 'Oops, an exception occurred: ' + msg |
| event = None |
| stdwin.message(msg) |
| event = None |
| |
| |
| # Handle one event. The window is taken from the event's window item. |
| # This function is placed as a method (named 'dispatch') on the window, |
| # so the main loop will be able to handle windows of a different kind |
| # as well, as long as they are all placed in the list of windows. |
| # |
| def idispatch(event): |
| type, win, detail = event |
| if type == WE_CHAR: |
| if not keybindings.has_key(detail): |
| detail = string.lower(detail) |
| if keybindings.has_key(detail): |
| keybindings[detail](win) |
| return |
| if detail in '0123456789': |
| i = eval(detail) - 1 |
| if i < 0: i = len(win.menu) + i |
| if 0 <= i < len(win.menu): |
| topic, ref = win.menu[i] |
| imove(win, ref) |
| return |
| stdwin.fleep() |
| return |
| if type == WE_COMMAND: |
| if detail == WC_LEFT: |
| iprev(win) |
| elif detail == WC_RIGHT: |
| inext(win) |
| elif detail == WC_UP: |
| iup(win) |
| elif detail == WC_DOWN: |
| idown(win) |
| elif detail == WC_BACKSPACE: |
| ibackward(win) |
| elif detail == WC_RETURN: |
| idown(win) |
| else: |
| stdwin.fleep() |
| return |
| if type == WE_MENU: |
| mp, item = detail |
| if mp == None: |
| pass # A THINK C console menu was selected |
| elif mp in (win.mainmenu, win.navimenu): |
| mp.callback[item](win) |
| elif mp == win.nodemenu: |
| topic, ref = win.menu[item] |
| imove(win, ref) |
| elif mp == win.footmenu: |
| topic, ref = win.footnotes[item] |
| imove(win, ref) |
| return |
| if type == WE_SIZE: |
| win.textobj.move((0, 0), win.getwinsize()) |
| (left, top), (right, bottom) = win.textobj.getrect() |
| win.setdocsize(0, bottom) |
| return |
| if type == WE_CLOSE: |
| iclose(win) |
| return |
| if not win.textobj.event(event): |
| pass |
| |
| |
| # Paging callbacks |
| |
| def ibeginning(win): |
| win.setorigin(0, 0) |
| win.textobj.setfocus(0, 0) # To restart searches |
| |
| def iforward(win): |
| lh = stdwin.lineheight() # XXX Should really use the window's... |
| h, v = win.getorigin() |
| docwidth, docheight = win.getdocsize() |
| width, height = win.getwinsize() |
| if v + height >= docheight: |
| stdwin.fleep() |
| return |
| increment = max(lh, ((height - 2*lh) / lh) * lh) |
| v = v + increment |
| win.setorigin(h, v) |
| |
| def ibackward(win): |
| lh = stdwin.lineheight() # XXX Should really use the window's... |
| h, v = win.getorigin() |
| if v <= 0: |
| stdwin.fleep() |
| return |
| width, height = win.getwinsize() |
| increment = max(lh, ((height - 2*lh) / lh) * lh) |
| v = max(0, v - increment) |
| win.setorigin(h, v) |
| |
| |
| # Ibrowse menu callbacks |
| |
| def iclone(win): |
| stdwin.setdefwinsize(win.getwinsize()) |
| makewindow(win.file, win.node) |
| |
| def itutor(win): |
| # The course looks best at 76x22... |
| stdwin.setdefwinsize(76*stdwin.textwidth('x'), 22*stdwin.lineheight()) |
| makewindow('ibrowse', 'Help') |
| |
| def isummary(win): |
| stdwin.setdefwinsize(76*stdwin.textwidth('x'), 22*stdwin.lineheight()) |
| makewindow('ibrowse', 'Summary') |
| |
| def iclose(win): |
| # |
| # Remove the window from the windows list so the mainloop |
| # will notice if all windows are gone. |
| # Delete the textobj since it constitutes a circular reference |
| # to the window which would prevent it from being closed. |
| # (Deletion is done by assigning None to avoid crashes |
| # when closing a half-initialized window.) |
| # |
| if win in windows: |
| windows.remove(win) |
| win.textobj = None |
| |
| def icopy(win): |
| focustext = win.textobj.getfocustext() |
| if not focustext: |
| stdwin.fleep() |
| else: |
| stdwin.rotatecutbuffers(1) |
| stdwin.setcutbuffer(0, focustext) |
| # XXX Should also set the primary selection... |
| |
| def isearch(win): |
| try: |
| pat = stdwin.askstr('Search pattern:', win.pat) |
| except KeyboardInterrupt: |
| return |
| if not pat: |
| pat = win.pat |
| if not pat: |
| stdwin.message('No previous pattern') |
| return |
| try: |
| cpat = regexp.compile(pat) |
| except regexp.error, msg: |
| stdwin.message('Bad pattern: ' + msg) |
| return |
| win.pat = pat |
| f1, f2 = win.textobj.getfocus() |
| text = win.text |
| match = cpat.match(text, f2) |
| if not match: |
| stdwin.fleep() |
| return |
| a, b = match[0] |
| win.textobj.setfocus(a, b) |
| |
| |
| def iresetnodecache(win): |
| icache.resetnodecache() |
| |
| def iresetcache(win): |
| icache.resetcache() |
| |
| def iquit(win): |
| for win in windows[:]: |
| iclose(win) |
| |
| |
| # Navigation menu callbacks |
| |
| def imenu(win): |
| ichoice(win, 'Menu item (abbreviated):', win.menu, whichmenuitem(win)) |
| |
| def ifollow(win): |
| ichoice(win, 'Follow reference named (abbreviated):', \ |
| win.footnotes, whichfootnote(win)) |
| |
| def igoto(win): |
| try: |
| choice = stdwin.askstr('Go to node (full name):', '') |
| except KeyboardInterrupt: |
| return |
| if not choice: |
| stdwin.message('Sorry, Go to has no default') |
| return |
| imove(win, choice) |
| |
| def inext(win): |
| prev, next, up = win.header |
| if next: |
| imove(win, next) |
| else: |
| stdwin.fleep() |
| |
| def iprev(win): |
| prev, next, up = win.header |
| if prev: |
| imove(win, prev) |
| else: |
| stdwin.fleep() |
| |
| def iup(win): |
| prev, next, up = win.header |
| if up: |
| imove(win, up) |
| else: |
| stdwin.fleep() |
| |
| def ilast(win): |
| if not win.last: |
| stdwin.fleep() |
| else: |
| i = len(win.last)-1 |
| lastnode, lastfocus = win.last[i] |
| imove(win, lastnode) |
| if len(win.last) > i+1: |
| # The move succeeded -- restore the focus |
| win.textobj.setfocus(lastfocus) |
| # Delete the stack top even if the move failed, |
| # else the whole stack would remain unreachable |
| del win.last[i:] # Delete the entry pushed by imove as well! |
| |
| def itop(win): |
| imove(win, '') |
| |
| def idir(win): |
| imove(win, '(dir)') |
| |
| |
| # Special and generic callbacks |
| |
| def idown(win): |
| if win.menu: |
| default = whichmenuitem(win) |
| for topic, ref in win.menu: |
| if default == topic: |
| break |
| else: |
| topic, ref = win.menu[0] |
| imove(win, ref) |
| else: |
| inext(win) |
| |
| def ichoice(win, prompt, list, default): |
| if not list: |
| stdwin.fleep() |
| return |
| if not default: |
| topic, ref = list[0] |
| default = topic |
| try: |
| choice = stdwin.askstr(prompt, default) |
| except KeyboardInterrupt: |
| return |
| if not choice: |
| return |
| choice = string.lower(choice) |
| n = len(choice) |
| for topic, ref in list: |
| topic = string.lower(topic) |
| if topic[:n] == choice: |
| imove(win, ref) |
| return |
| stdwin.message('Sorry, no topic matches ' + `choice`) |
| |
| |
| # Follow a reference, in the same window. |
| # |
| def imove(win, ref): |
| savetitle = win.gettitle() |
| win.settitle('Looking for ' + ref + '...') |
| # |
| try: |
| file, node, header, menu, footnotes, text = \ |
| icache.get_node(win.file, ref) |
| except NoSuchFile, file: |
| win.settitle(savetitle) |
| stdwin.message(\ |
| 'Sorry, I can\'t find a file named ' + `file` + '.') |
| return |
| except NoSuchNode, node: |
| win.settitle(savetitle) |
| stdwin.message(\ |
| 'Sorry, I can\'t find a node named ' + `node` + '.') |
| return |
| # |
| win.settitle('Found (' + file + ')' + node + '...') |
| # |
| if win.file and win.node: |
| lastnode = '(' + win.file + ')' + win.node |
| win.last.append(lastnode, win.textobj.getfocus()) |
| win.file = file |
| win.node = node |
| win.header = header |
| win.menu = menu |
| win.footnotes = footnotes |
| win.text = text |
| # |
| win.setorigin(0, 0) # Scroll to the beginnning |
| win.textobj.settext(text) |
| win.textobj.setfocus(0, 0) |
| (left, top), (right, bottom) = win.textobj.getrect() |
| win.setdocsize(0, bottom) |
| # |
| if win.footmenu: win.footmenu.close() |
| if win.nodemenu: win.nodemenu.close() |
| win.footmenu = None |
| win.nodemenu = None |
| # |
| win.menu = menu |
| if menu: |
| win.nodemenu = win.menucreate('Menu') |
| digit = 1 |
| for topic, ref in menu: |
| if digit < 10: |
| win.nodemenu.additem(topic, `digit`) |
| else: |
| win.nodemenu.additem(topic) |
| digit = digit + 1 |
| # |
| win.footnotes = footnotes |
| if footnotes: |
| win.footmenu = win.menucreate('Footnotes') |
| for topic, ref in footnotes: |
| win.footmenu.additem(topic) |
| # |
| win.settitle('(' + win.file + ')' + win.node) |
| |
| |
| # Find menu item at focus |
| # |
| findmenu = regexp.compile('^\* [mM]enu:').match |
| findmenuitem = regexp.compile( \ |
| '^\* ([^:]+):[ \t]*(:|\([^\t]*\)[^\t,\n.]*|[^:(][^\t,\n.]*)').match |
| # |
| def whichmenuitem(win): |
| if not win.menu: |
| return '' |
| match = findmenu(win.text) |
| if not match: |
| return '' |
| a, b = match[0] |
| i = b |
| f1, f2 = win.textobj.getfocus() |
| lastmatch = '' |
| while i < len(win.text): |
| match = findmenuitem(win.text, i) |
| if not match: |
| break |
| (a, b), (a1, b1), (a2, b2) = match |
| if a > f1: |
| break |
| lastmatch = win.text[a1:b1] |
| i = b |
| return lastmatch |
| |
| |
| # Find footnote at focus |
| # |
| findfootnote = \ |
| regexp.compile('\*[nN]ote ([^:]+):[ \t]*(:|[^:][^\t,\n.]*)').match |
| # |
| def whichfootnote(win): |
| if not win.footnotes: |
| return '' |
| i = 0 |
| f1, f2 = win.textobj.getfocus() |
| lastmatch = '' |
| while i < len(win.text): |
| match = findfootnote(win.text, i) |
| if not match: |
| break |
| (a, b), (a1, b1), (a2, b2) = match |
| if a > f1: |
| break |
| lastmatch = win.text[a1:b1] |
| i = b |
| return lastmatch |
| |
| |
| # Now all the "methods" are defined, we can initialize the table |
| # of key bindings. |
| # |
| keybindings = {} |
| |
| # Window commands |
| |
| keybindings['k'] = iclone |
| keybindings['h'] = itutor |
| keybindings['?'] = isummary |
| keybindings['w'] = iclose |
| |
| keybindings['c'] = icopy |
| |
| keybindings['s'] = isearch |
| |
| keybindings['q'] = iquit |
| |
| # Navigation commands |
| |
| keybindings['m'] = imenu |
| keybindings['f'] = ifollow |
| keybindings['g'] = igoto |
| |
| keybindings['n'] = inext |
| keybindings['p'] = iprev |
| keybindings['u'] = iup |
| keybindings['l'] = ilast |
| keybindings['d'] = idir |
| keybindings['t'] = itop |
| |
| # Paging commands |
| |
| keybindings['b'] = ibeginning |
| keybindings['.'] = ibeginning |
| keybindings[' '] = iforward |