#! /usr/local/bin/python
#
#
# $Id: pman.py,v 1.1 2000/11/05 19:52:02 idiscovery Exp $
#
# An xman like program. - Sudhir Shenoy, January 1996.
#
# Features:
#
# Can have multiple man pages open at the same time.
#
# Hypertext: Manual page cross references in the Apropos output or a man page
# are highlighted when the mouse moves on top of them. Clicking button 1 over
# the highlighted reference displays the relevant page.
#
# Regexp search in manual page window with wrap around.
#
# Handles MANPATH correctly. If the same man page (e.g. 'make') is in more
# than one directory (/usr/man/man1 and /usr/local/man/man1), precedence is
# decided by which dir appears first in the MANPATH.
#
# BUGS: Doesn't handle the case when the reference is split across two lines.
# This can be fixed by sucking in the whole text from the text widget and then
# doing the search e.g., in class ManWindow but this involves more work.
#
# Page display is slow.
#
import os, regex, regsub, string, sys, Tix
BOLDFONT = '*-Courier-Bold-R-Normal-*-140-*'
ITALICFONT = '*-Courier-Medium-O-Normal-*-140-*'
footer_pat = regex.compile('^ Page [1-9][0-9]*[ \t]+\|^.*Last change:.*[1-9][0-9]*\n')
empty_pat = regex.compile('^[ \t]*\n')
underline_pat = regex.compile('^[ \t]*[Xv!_][Xv!_ \t]*\n')
link_pat = regex.compile('\([A-Za-z0-9._]+\)[ \t]*([ \t]*\([A-Za-z0-9]+\)[ \t]*)')
# Man Page display widget - borrowed from Guido's demos with minor changes.
class ManPageWidget(Tix.ScrolledText):
def __init__(self, master=None, cnf={}):
# Initialize base class
Tix.ScrolledText.__init__(self, master, cnf)
self.text['state'] = 'disabled'
# Define tags for formatting styles
self.text.tag_config('X', {'underline': 1})
self.text.tag_config('!', {'font': BOLDFONT})
self.text.tag_config('_', {'font': ITALICFONT})
# Set state to idle
self.fp = None
self.lineno = 0
self.tagnum = 0
# Test whether we are busy parsing a file
def busy(self):
return self.fp != None
# Ensure we're not busy
def kill(self):
if self.busy():
self._endparser()
# Parse a file, in the background
def asyncparsefile(self, fp):
self._startparser(fp)
self.tk.createfilehandler(fp, Tix.READABLE, self._filehandler)
parsefile = asyncparsefile # Alias
# I/O handler used by background parsing
def _filehandler(self, fp, mask):
nextline = self.fp.readline()
if not nextline:
self._endparser()
return
self._parseline(nextline)
# Parse a file, now (cannot be aborted)
def syncparsefile(self, fp):
from select import select
def avail(fp=fp, tout=0.0, select=select):
return select([fp], [], [], tout)[0]
height = self.getint(self['height'])
self._startparser(fp)
while 1:
nextline = fp.readline()
if not nextline:
break
self._parseline(nextline)
self._endparser()
# Initialize parsing from a particular file -- must not be busy
def _startparser(self, fp):
if self.busy():
raise RuntimeError, 'startparser: still busy'
fp.fileno() # Test for file-ness
self.fp = fp
self.lineno = 0
self.tagnum = 0
self.ok = 0
self.empty = 0
self.buffer = None
self.text['state'] = 'normal'
self.text.delete('1.0', 'end')
self.text['state'] = 'disabled'
# End parsing -- must be busy, need not be at EOF
def _endparser(self):
if not self.busy():
raise RuntimeError, 'endparser: not busy'
if self.buffer:
self._parseline('')
try:
self.tk.deletefilehandler(self.fp)
except Tix.TclError, msg:
pass
self.fp.close()
self.fp = None
del self.ok, self.empty, self.buffer
# Parse a single line
def _parseline(self, nextline):
if not self.buffer:
# Save this line -- we need one line read-ahead
self.buffer = nextline
return
if empty_pat.match(self.buffer) >= 0:
# Buffered line was empty -- set a flag
self.empty = 1
self.buffer = nextline
return
textline = self.buffer
if underline_pat.match(nextline) >= 0:
# Next line is properties for buffered line
propline = nextline
self.buffer = None
else:
# Next line is read-ahead
propline = None
self.buffer = nextline
if not self.ok:
# First non blank line after footer must be header
# -- skip that too
self.ok = 1
self.empty = 0
return
if footer_pat.match(textline) >= 0:
# Footer -- start skipping until next non-blank line
self.ok = 0
self.empty = 0
return
self.text['state'] = 'normal'
if Tix.TkVersion >= 4.0:
self.text.mark_set('insert', 'end-1c')
else:
self.text.mark_set('insert', 'end')
if self.empty:
# One or more previous lines were empty
# -- insert one blank line in the text
self._insert_prop('\n')
self.lineno = self.lineno + 1
self.empty = 0
if not propline:
# No properties
self._insert_prop(textline)
else:
# Search for properties
p = ''
j = 0
for i in range(min(len(propline), len(textline))):
if propline[i] != p:
if j < i:
self._insert_prop(textline[j:i], p)
j = i
p = propline[i]
self._insert_prop(textline[j:])
startpos = 0
line = textline[:]
while 1:
pos = link_pat.search(line)
if pos < 0:
break
pos = pos + startpos
startpos = startpos + link_pat.regs[0][1]
tag = self._w + `self.tagnum`
self.tagnum = self.tagnum + 1
self.text.tag_add(tag, '%d.%d' % (self.lineno + 1, pos),
'%d.%d' % (self.lineno + 1, startpos))
self.text.tag_bind(tag, '<Any-Enter>',
lambda e=None,t=tag,w=self: w._highlight(t, 1))
self.text.tag_bind(tag, '<Any-Leave>',
lambda e=None,t=tag,w=self: w._highlight(t, 0))
self.text.tag_bind(tag, '<1>',
lambda e=None,w=self,t=textline[pos:startpos]:
w._hyper_link(t))
if startpos >= len(textline):
break
line = textline[startpos:]
self.lineno = self.lineno + 1
self.text['state'] = 'disabled'
def _highlight(self, tag, how):
if how:
self.text.tag_config(tag, background="#43ce80", relief=Tix.RAISED)
else:
self.text.tag_config(tag, background="", relief=Tix.FLAT)
def _hyper_link(self, txt):
if link_pat.search(txt) < 0:
print "Invalid man reference string"
return
pagename = txt[link_pat.regs[1][0]:link_pat.regs[1][1]]
section = txt[link_pat.regs[2][0]:link_pat.regs[2][1]]
mandirs = ManDirectories()
pipe = mandirs.FormattedPipe(section, pagename)
self.parsefile(pipe)
# Insert a string at the end, with at most one property (tag)
def _insert_prop(self, str, prop = ' '):
here = self.text.index('insert')
self.text.insert('insert', str)
if prop != ' ':
self.text.tag_add(prop, here, 'insert')
#end class ManPageWidget
class ManDirectories:
"""Find all man directories (using MANPATH if defined)
The section names are kept in the list sections.
Descriptive names are in the dictionary section_names
The full path name(s) for each section are in the dictionary secpaths."""
def __init__(self):
known_names = {'1':'User Commands', '1b':'Commands: BSD',
'1c':'Commands: Communications',
'1f':'Commands: FMLI', '1m':'Commands: Maintenance',
'1s':'Commands: SunOS specific',
'2':'System Calls',
'3':'Subroutines', '3b':'Routines: BSD',
'3c':'Routines: C Library', '3e':'Routines: ELF',
'3g':'Routines: General', '3i':'Routines: Wide Char',
'3k':'Routines: Kernel VM', '3m':'Routines: Math',
'3n':'Routines: Network', '3r':'Routines: Realtime',
'3s':'Routines: Std. I/O', '3t':'Routines: Threads',
'3x':'Routines: Misc.',
'4':'File Formats', '4b':'Files: BSD',
'5':'Miscellaneous',
'6':'Games',
'7':'Devices',
'9':'Device Drivers', '9e':'Drivers: Entry Points',
'9f':'Drivers: Functions',
'9s':'Drivers: Data Structures',
'l':'Local',
'n':'New'}
if os.environ.has_key('MANPATH'):
manpath = os.environ["MANPATH"]
if not manpath:
manpath = "/usr/share/man"
manpath = string.splitfields(manpath, ':')
self.secpaths = {}
for path in manpath:
files = os.listdir(path)
for f in files:
if os.path.isdir(path + '/' + f) and len(f) > 3 and f[:3] == 'man':
sec = f[3:]
if self.secpaths.has_key(sec):
temp = self.secpaths[sec] + ':'
else:
temp = ''
self.secpaths[sec] = temp + path + '/' + f
self.sections = self.secpaths.keys()
self.sections.sort()
self.section_names = {}
for s in self.sections:
if s in known_names.keys():
self.section_names[s + ': ' + known_names[s]] = s
else:
self.section_names[s] = s
def Pages(self, secname):
if not self.secpaths.has_key(secname):
return []
paths = string.splitfields(self.secpaths[secname], ':')
wid = len(secname)
names = []
for path in paths:
files = os.listdir(path)
for file in files:
if file[-(wid + 1):-wid] == '.' and file[-wid:] == secname:
file = file[:-(wid + 1)]
if file not in names:
# if duplicate - preceding path takes precedence
names.append(file)
names.sort()
return names
def FormattedPipe(self, secname, page):
secname = string.lower(secname)
if not self.secpaths.has_key(secname):
raise ValueError
file = page + '.' + secname
paths = string.splitfields(self.secpaths[secname], ':')
cwd = os.getcwd()
for path in paths:
files = os.listdir(path)
if file in files:
file = path + '/' + file
os.chdir(path)
os.chdir('..')
break
pipe = os.popen('nroff -man %s | ul -i' % file)
os.chdir(cwd)
return pipe
#end class ManDirectories
class ManPageWindow:
def __init__(self, pipe):
self.top = Tix.Toplevel()
frame = Tix.Frame(self.top)
frame2 = Tix.Frame(frame)
self.search_str = Tix.StringVar()
self.case_sensitive = Tix.StringVar()
btn = Tix.Button(frame2, text='Regexp Search:', command=self.Search)
entry = Tix.Entry(frame2, relief=Tix.SUNKEN)
entry['textvariable'] = self.search_str
entry.bind('<Return>', self.Search)
casesense = Tix.Checkbutton(frame2, text='Case Sensitive',
relief=Tix.FLAT,
variable=self.case_sensitive)
btn.pack(side=Tix.LEFT, expand=0)
entry.pack(side=Tix.LEFT, expand=1, fill=Tix.X)
casesense.pack(side=Tix.RIGHT, expand=0)
self.man = ManPageWidget(frame)
btn = Tix.Button(frame, text='Close', command=self.Quit)
frame2.pack(side=Tix.TOP, expand=0, fill=Tix.X)
self.man.pack(side=Tix.TOP, expand=1, fill=Tix.BOTH)
btn.pack(side=Tix.BOTTOM, expand=0, fill=Tix.X)
frame.pack(expand=1, fill=Tix.BOTH)
self.man.parsefile(pipe)
def Search(self, event=None):
str = self.search_str.get()
if not str:
self.top.bell()
print "No search string ?"
return
try:
if self.case_sensitive.get() == '1':
pat = regex.compile(str, regex.casefold)
else:
pat = regex.compile(str)
except regex.error, msg:
self.top.bell()
print "regex error"
return
pos = self.man.text.index('insert')
lineno = string.atoi(pos[:string.find(pos, '.')])
endpos = self.man.text.index('end')
endlineno = string.atoi(endpos[:string.find(endpos, '.')])
wraplineno = lineno
found = 0
while 1:
lineno = lineno + 1
if lineno > endlineno:
if wraplineno <= 0:
break
endlineno = wraplineno
lineno = 0
wraplineno = 0
line = self.man.text.get('%d.0 linestart' % lineno,
'%d.0 lineend' % lineno)
i = pat.search(line)
if i >= 0:
found = 1
n = max(1, len(pat.group(0)))
try:
self.man.text.tag_remove('sel', 'sel.first', 'sel.last')
except Tix.TclError:
pass
self.man.text.tag_add('sel', '%d.%d' % (lineno, i),
'%d.%d' % (lineno, i+n))
self.man.text.mark_set('insert', '%d.%d' % (lineno, i))
self.man.text.yview_pickplace('insert')
break
if not found:
self.frame.bell()
def Quit(self):
del self.search_str
del self.case_sensitive
self.top.destroy()
#end class ManPageWindow
class AproposWindow:
def __init__(self):
self.top = Tix.Toplevel()
frame = Tix.Frame(self.top)
frame2 = Tix.Frame(frame)
self.apropos_str = Tix.StringVar()
btn = Tix.Button(frame2, text='Apropos:', command=self.Apropos)
entry = Tix.Entry(frame2, relief=Tix.SUNKEN, width=20)
entry['textvariable'] = self.apropos_str
entry.bind('<Return>', self.Apropos)
btn.pack(side=Tix.LEFT, expand=0)
entry.pack(side=Tix.RIGHT, expand=1, fill=Tix.X)
frame2.pack(side=Tix.TOP, expand=0, fill=Tix.X)
self.stext = Tix.ScrolledText(frame)
self.stext.text.tag_config('!', font=BOLDFONT)
btn = Tix.Button(frame, text='Close', command=self.Quit)
self.stext.pack(side=Tix.TOP, expand=1, fill=Tix.BOTH)
btn.pack(side=Tix.BOTTOM, expand=0, fill=Tix.X)
frame.pack(expand=1, fill=Tix.BOTH)
def Apropos(self, event=None):
str = self.apropos_str.get()
if not str:
self.top.bell()
print "No string ?"
return
pipe = os.popen('apropos ' + str, 'r')
self.stext.text.delete('1.0', Tix.END)
tabs = regex.compile('\011+')
num = 1
while 1:
line = pipe.readline()
if not line:
break
line = regsub.gsub(tabs, '\011', line)
fields = string.splitfields(line, '\011')
if len(fields) == 1:
line = line[string.find(line, ' ') + 1:]
line = regsub.gsub('^ *', '', line)
fields = ['???', line]
if len(fields) == 2:
tmp = string.splitfields(fields[1], '-')
fields = fields[0:1] + tmp
num = num + 1
self.stext.text.insert('insert', fields[0]+'\t', '!')
self.stext.text.insert('insert', fields[1], `num`)
self.stext.text.tag_bind(`num`, '<Any-Enter>',
lambda e=None,t=`num`,w=self:
w._highlight(t, 1))
self.stext.text.tag_bind(`num`, '<Any-Leave>',
lambda e=None,t=`num`,w=self:
w._highlight(t, 0))
self.stext.text.tag_bind(`num`, '<1>',
lambda e=None,w=self,t=fields[1]:
w._hyper_link(t))
self.stext.text.insert('insert', fields[2])
def _highlight(self, tag, how):
if how:
self.stext.text.tag_config(tag, background="#43ce80",
relief=Tix.RAISED)
else:
self.stext.text.tag_config(tag, background="", relief=Tix.FLAT)
def _hyper_link(self, txt):
if link_pat.search(txt) < 0:
print "Invalid man reference string"
return
pagename = txt[link_pat.regs[1][0]:link_pat.regs[1][1]]
section = txt[link_pat.regs[2][0]:link_pat.regs[2][1]]
mandirs = ManDirectories()
pipe = mandirs.FormattedPipe(section, pagename)
disp = ManPageWindow(pipe)
def Quit(self):
del self.apropos_str
self.top.destroy()
class PManWindow:
def __init__(self, master=None):
self.mandirs = ManDirectories()
self.frame = Tix.Frame(master)
self.section = Tix.StringVar()
combo = Tix.ComboBox(self.frame, label='Section: ', dropdown=1,
editable=0, variable=self.section,
command=self.UpdatePageList)
pagelist = Tix.ScrolledListBox(self.frame, scrollbar='auto')
self.listbox = pagelist.listbox
self.listbox.bind('<Double-1>', self.ShowPage)
temp = self.mandirs.section_names.keys()
temp.sort()
for s in temp:
combo.insert(Tix.END, s)
box = Tix.ButtonBox(self.frame, orientation=Tix.HORIZONTAL)
box.add('show', text='Show Page ...', underline=0, width=13,
command=self.ShowPage)
box.add('aprop', text='Apropos ...', underline=0, width=13,
command=self.Apropos)
box.add('quit', text='Quit', underline=0, width=13,
command=self.Quit)
combo.pack(side=Tix.TOP, expand=0, fill=Tix.X)
pagelist.pack(side=Tix.TOP, expand=1, fill=Tix.BOTH)
box.pack(side=Tix.BOTTOM, expand=0, fill=Tix.X)
self.frame.pack(expand=1, fill=Tix.BOTH)
def UpdatePageList(self, event=None):
secname = self.section.get()
if not self.mandirs.section_names.has_key(secname):
return
secname = self.mandirs.section_names[secname]
pages = self.mandirs.Pages(secname)
self.listbox.delete(0, Tix.END)
for page in pages:
self.listbox.insert(Tix.END, page)
def ShowPage(self, event=None):
secname = self.section.get()
secname = self.mandirs.section_names[secname]
idx = self.listbox.curselection()
pagename = self.listbox.get(idx)
pipe = self.mandirs.FormattedPipe(secname, pagename)
page_display = ManPageWindow(pipe)
def Apropos(self):
apropos_disp = AproposWindow()
def Quit(self):
sys.exit()
#end class PManWindow
def main():
root = Tix.Tk()
root.minsize(10, 10)
win = PManWindow(root)
root.mainloop()
if __name__ == '__main__':
main()
|