blob: 750c6758d6306229ddcd406fc13027b00705c4bb [file] [log] [blame]
Guido van Rossumf09b7701994-07-06 21:17:21 +00001# Widget to display a man page
2
Georg Brandlf19ff1e2010-10-26 10:39:14 +00003import os
Thomas Wouters49fd7fa2006-04-21 10:40:58 +00004import re
Georg Brandlf19ff1e2010-10-26 10:39:14 +00005import sys
Guido van Rossumf09b7701994-07-06 21:17:21 +00006
Georg Brandlf19ff1e2010-10-26 10:39:14 +00007from tkinter import *
8from tkinter.font import Font
9from tkinter.scrolledtext import ScrolledText
Guido van Rossumf09b7701994-07-06 21:17:21 +000010
11# XXX Recognizing footers is system dependent
12# (This one works for IRIX 5.2 and Solaris 2.2)
Thomas Wouters49fd7fa2006-04-21 10:40:58 +000013footerprog = re.compile(
Tim Peters182b5ac2004-07-18 06:16:08 +000014 '^ Page [1-9][0-9]*[ \t]+\|^.*Last change:.*[1-9][0-9]*\n')
Thomas Wouters49fd7fa2006-04-21 10:40:58 +000015emptyprog = re.compile('^[ \t]*\n')
16ulprog = re.compile('^[ \t]*[Xv!_][Xv!_ \t]*\n')
Guido van Rossumf09b7701994-07-06 21:17:21 +000017
Guido van Rossumf09b7701994-07-06 21:17:21 +000018
Georg Brandlf19ff1e2010-10-26 10:39:14 +000019class EditableManPage(ScrolledText):
20 """Basic Man Page class -- does not disable editing."""
21
Tim Peters182b5ac2004-07-18 06:16:08 +000022 def __init__(self, master=None, **cnf):
Neal Norwitzd9108552006-03-17 08:00:19 +000023 ScrolledText.__init__(self, master, **cnf)
Guido van Rossumf09b7701994-07-06 21:17:21 +000024
Georg Brandlf19ff1e2010-10-26 10:39:14 +000025 bold = Font(font=self['font']).copy()
26 bold.config(weight='bold')
27 italic = Font(font=self['font']).copy()
28 italic.config(slant='italic')
29
Tim Peters182b5ac2004-07-18 06:16:08 +000030 # Define tags for formatting styles
31 self.tag_config('X', underline=1)
Georg Brandlf19ff1e2010-10-26 10:39:14 +000032 self.tag_config('!', font=bold)
33 self.tag_config('_', font=italic)
Guido van Rossumf09b7701994-07-06 21:17:21 +000034
Tim Peters182b5ac2004-07-18 06:16:08 +000035 # Set state to idle
36 self.fp = None
37 self.lineno = 0
Guido van Rossum7d5e4211994-07-08 14:15:05 +000038
Tim Peters182b5ac2004-07-18 06:16:08 +000039 def busy(self):
Georg Brandlf19ff1e2010-10-26 10:39:14 +000040 """Test whether we are busy parsing a file."""
Tim Peters182b5ac2004-07-18 06:16:08 +000041 return self.fp != None
Guido van Rossum7d5e4211994-07-08 14:15:05 +000042
Tim Peters182b5ac2004-07-18 06:16:08 +000043 def kill(self):
Georg Brandlf19ff1e2010-10-26 10:39:14 +000044 """Ensure we're not busy."""
Tim Peters182b5ac2004-07-18 06:16:08 +000045 if self.busy():
46 self._endparser()
Guido van Rossum7d5e4211994-07-08 14:15:05 +000047
Tim Peters182b5ac2004-07-18 06:16:08 +000048 def asyncparsefile(self, fp):
Georg Brandlf19ff1e2010-10-26 10:39:14 +000049 """Parse a file, in the background."""
Tim Peters182b5ac2004-07-18 06:16:08 +000050 self._startparser(fp)
Georg Brandlf19ff1e2010-10-26 10:39:14 +000051 self.tk.createfilehandler(fp, READABLE,
Tim Peters182b5ac2004-07-18 06:16:08 +000052 self._filehandler)
Guido van Rossum7d5e4211994-07-08 14:15:05 +000053
Georg Brandlf19ff1e2010-10-26 10:39:14 +000054 parsefile = asyncparsefile # Alias
Guido van Rossum7d5e4211994-07-08 14:15:05 +000055
Tim Peters182b5ac2004-07-18 06:16:08 +000056 def _filehandler(self, fp, mask):
Georg Brandlf19ff1e2010-10-26 10:39:14 +000057 """I/O handler used by background parsing."""
Tim Peters182b5ac2004-07-18 06:16:08 +000058 nextline = self.fp.readline()
59 if not nextline:
60 self._endparser()
61 return
62 self._parseline(nextline)
Guido van Rossum7d5e4211994-07-08 14:15:05 +000063
Tim Peters182b5ac2004-07-18 06:16:08 +000064 def syncparsefile(self, fp):
Georg Brandlf19ff1e2010-10-26 10:39:14 +000065 """Parse a file, now (cannot be aborted)."""
Tim Peters182b5ac2004-07-18 06:16:08 +000066 self._startparser(fp)
Georg Brandlf19ff1e2010-10-26 10:39:14 +000067 while True:
Tim Peters182b5ac2004-07-18 06:16:08 +000068 nextline = fp.readline()
69 if not nextline:
70 break
71 self._parseline(nextline)
72 self._endparser()
Guido van Rossumfdfa2b51994-07-08 09:14:54 +000073
Tim Peters182b5ac2004-07-18 06:16:08 +000074 def _startparser(self, fp):
Georg Brandlf19ff1e2010-10-26 10:39:14 +000075 """Initialize parsing from a particular file -- must not be busy."""
Tim Peters182b5ac2004-07-18 06:16:08 +000076 if self.busy():
Collin Winter6f2df4d2007-07-17 20:59:35 +000077 raise RuntimeError('startparser: still busy')
Tim Peters182b5ac2004-07-18 06:16:08 +000078 fp.fileno() # Test for file-ness
79 self.fp = fp
80 self.lineno = 0
81 self.ok = 0
82 self.empty = 0
83 self.buffer = None
84 savestate = self['state']
85 self['state'] = NORMAL
86 self.delete('1.0', END)
87 self['state'] = savestate
Guido van Rossumfdfa2b51994-07-08 09:14:54 +000088
Tim Peters182b5ac2004-07-18 06:16:08 +000089 def _endparser(self):
Georg Brandlf19ff1e2010-10-26 10:39:14 +000090 """End parsing -- must be busy, need not be at EOF."""
Tim Peters182b5ac2004-07-18 06:16:08 +000091 if not self.busy():
Collin Winter6f2df4d2007-07-17 20:59:35 +000092 raise RuntimeError('endparser: not busy')
Tim Peters182b5ac2004-07-18 06:16:08 +000093 if self.buffer:
94 self._parseline('')
95 try:
96 self.tk.deletefilehandler(self.fp)
Georg Brandlf19ff1e2010-10-26 10:39:14 +000097 except TclError:
Tim Peters182b5ac2004-07-18 06:16:08 +000098 pass
99 self.fp.close()
100 self.fp = None
101 del self.ok, self.empty, self.buffer
Guido van Rossumfdfa2b51994-07-08 09:14:54 +0000102
Tim Peters182b5ac2004-07-18 06:16:08 +0000103 def _parseline(self, nextline):
Georg Brandlf19ff1e2010-10-26 10:39:14 +0000104 """Parse a single line."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000105 if not self.buffer:
106 # Save this line -- we need one line read-ahead
107 self.buffer = nextline
108 return
Georg Brandl0db85e52010-08-02 23:30:09 +0000109 if emptyprog.match(self.buffer):
Tim Peters182b5ac2004-07-18 06:16:08 +0000110 # Buffered line was empty -- set a flag
111 self.empty = 1
112 self.buffer = nextline
113 return
114 textline = self.buffer
Georg Brandl0db85e52010-08-02 23:30:09 +0000115 if ulprog.match(nextline):
Tim Peters182b5ac2004-07-18 06:16:08 +0000116 # Next line is properties for buffered line
117 propline = nextline
118 self.buffer = None
119 else:
120 # Next line is read-ahead
121 propline = None
122 self.buffer = nextline
123 if not self.ok:
124 # First non blank line after footer must be header
125 # -- skip that too
126 self.ok = 1
127 self.empty = 0
128 return
Georg Brandl0db85e52010-08-02 23:30:09 +0000129 if footerprog.match(textline):
Tim Peters182b5ac2004-07-18 06:16:08 +0000130 # Footer -- start skipping until next non-blank line
131 self.ok = 0
132 self.empty = 0
133 return
134 savestate = self['state']
135 self['state'] = NORMAL
136 if TkVersion >= 4.0:
137 self.mark_set('insert', 'end-1c')
138 else:
139 self.mark_set('insert', END)
140 if self.empty:
141 # One or more previous lines were empty
142 # -- insert one blank line in the text
143 self._insert_prop('\n')
144 self.lineno = self.lineno + 1
145 self.empty = 0
146 if not propline:
147 # No properties
148 self._insert_prop(textline)
149 else:
150 # Search for properties
151 p = ''
152 j = 0
153 for i in range(min(len(propline), len(textline))):
154 if propline[i] != p:
155 if j < i:
156 self._insert_prop(textline[j:i], p)
157 j = i
158 p = propline[i]
159 self._insert_prop(textline[j:])
160 self.lineno = self.lineno + 1
161 self['state'] = savestate
Guido van Rossumf09b7701994-07-06 21:17:21 +0000162
Tim Peters182b5ac2004-07-18 06:16:08 +0000163 def _insert_prop(self, str, prop = ' '):
Georg Brandlf19ff1e2010-10-26 10:39:14 +0000164 """Insert a string at the end, with at most one property (tag)."""
Tim Peters182b5ac2004-07-18 06:16:08 +0000165 here = self.index(AtInsert())
166 self.insert(AtInsert(), str)
167 if TkVersion <= 4.0:
168 tags = self.tag_names(here)
169 for tag in tags:
170 self.tag_remove(tag, here, AtInsert())
171 if prop != ' ':
172 self.tag_add(prop, here, AtInsert())
Guido van Rossumf09b7701994-07-06 21:17:21 +0000173
Guido van Rossumf09b7701994-07-06 21:17:21 +0000174
Georg Brandlf19ff1e2010-10-26 10:39:14 +0000175class ReadonlyManPage(EditableManPage):
176 """Readonly Man Page class -- disables editing, otherwise the same."""
177
Tim Peters182b5ac2004-07-18 06:16:08 +0000178 def __init__(self, master=None, **cnf):
179 cnf['state'] = DISABLED
Neal Norwitzd9108552006-03-17 08:00:19 +0000180 EditableManPage.__init__(self, master, **cnf)
Guido van Rossumf09b7701994-07-06 21:17:21 +0000181
182# Alias
183ManPage = ReadonlyManPage
184
Guido van Rossumf09b7701994-07-06 21:17:21 +0000185# usage: ManPage [manpage]; or ManPage [-f] file
186# -f means that the file is nroff -man output run through ul -i
Georg Brandlf19ff1e2010-10-26 10:39:14 +0000187def main():
Tim Peters182b5ac2004-07-18 06:16:08 +0000188 # XXX This directory may be different on your system
Georg Brandl0db85e52010-08-02 23:30:09 +0000189 MANDIR = ''
Tim Peters182b5ac2004-07-18 06:16:08 +0000190 DEFAULTPAGE = 'Tcl'
191 formatted = 0
192 if sys.argv[1:] and sys.argv[1] == '-f':
193 formatted = 1
194 del sys.argv[1]
195 if sys.argv[1:]:
196 name = sys.argv[1]
197 else:
198 name = DEFAULTPAGE
199 if not formatted:
200 if name[-2:-1] != '.':
201 name = name + '.n'
202 name = os.path.join(MANDIR, name)
203 root = Tk()
204 root.minsize(1, 1)
205 manpage = ManPage(root, relief=SUNKEN, borderwidth=2)
206 manpage.pack(expand=1, fill=BOTH)
207 if formatted:
208 fp = open(name, 'r')
209 else:
Georg Brandlf19ff1e2010-10-26 10:39:14 +0000210 fp = os.popen('nroff -man -c %s | ul -i' % name, 'r')
Tim Peters182b5ac2004-07-18 06:16:08 +0000211 manpage.parsefile(fp)
212 root.mainloop()
Guido van Rossumf09b7701994-07-06 21:17:21 +0000213
Guido van Rossumf09b7701994-07-06 21:17:21 +0000214if __name__ == '__main__':
Georg Brandlf19ff1e2010-10-26 10:39:14 +0000215 main()