|
1 # Widget to display a man page |
|
2 |
|
3 import re |
|
4 from Tkinter import * |
|
5 from Tkinter import _tkinter |
|
6 from ScrolledText import ScrolledText |
|
7 |
|
8 # XXX These fonts may have to be changed to match your system |
|
9 BOLDFONT = '*-Courier-Bold-R-Normal-*-120-*' |
|
10 ITALICFONT = '*-Courier-Medium-O-Normal-*-120-*' |
|
11 |
|
12 # XXX Recognizing footers is system dependent |
|
13 # (This one works for IRIX 5.2 and Solaris 2.2) |
|
14 footerprog = re.compile( |
|
15 '^ Page [1-9][0-9]*[ \t]+\|^.*Last change:.*[1-9][0-9]*\n') |
|
16 emptyprog = re.compile('^[ \t]*\n') |
|
17 ulprog = re.compile('^[ \t]*[Xv!_][Xv!_ \t]*\n') |
|
18 |
|
19 # Basic Man Page class -- does not disable editing |
|
20 class EditableManPage(ScrolledText): |
|
21 |
|
22 # Initialize instance |
|
23 def __init__(self, master=None, **cnf): |
|
24 # Initialize base class |
|
25 apply(ScrolledText.__init__, (self, master), cnf) |
|
26 |
|
27 # Define tags for formatting styles |
|
28 self.tag_config('X', underline=1) |
|
29 self.tag_config('!', font=BOLDFONT) |
|
30 self.tag_config('_', font=ITALICFONT) |
|
31 |
|
32 # Set state to idle |
|
33 self.fp = None |
|
34 self.lineno = 0 |
|
35 |
|
36 # Test whether we are busy parsing a file |
|
37 def busy(self): |
|
38 return self.fp != None |
|
39 |
|
40 # Ensure we're not busy |
|
41 def kill(self): |
|
42 if self.busy(): |
|
43 self._endparser() |
|
44 |
|
45 # Parse a file, in the background |
|
46 def asyncparsefile(self, fp): |
|
47 self._startparser(fp) |
|
48 self.tk.createfilehandler(fp, _tkinter.READABLE, |
|
49 self._filehandler) |
|
50 |
|
51 parsefile = asyncparsefile # Alias |
|
52 |
|
53 # I/O handler used by background parsing |
|
54 def _filehandler(self, fp, mask): |
|
55 nextline = self.fp.readline() |
|
56 if not nextline: |
|
57 self._endparser() |
|
58 return |
|
59 self._parseline(nextline) |
|
60 |
|
61 # Parse a file, now (cannot be aborted) |
|
62 def syncparsefile(self, fp): |
|
63 from select import select |
|
64 def avail(fp=fp, tout=0.0, select=select): |
|
65 return select([fp], [], [], tout)[0] |
|
66 height = self.getint(self['height']) |
|
67 self._startparser(fp) |
|
68 while 1: |
|
69 nextline = fp.readline() |
|
70 if not nextline: |
|
71 break |
|
72 self._parseline(nextline) |
|
73 self._endparser() |
|
74 |
|
75 # Initialize parsing from a particular file -- must not be busy |
|
76 def _startparser(self, fp): |
|
77 if self.busy(): |
|
78 raise RuntimeError, 'startparser: still busy' |
|
79 fp.fileno() # Test for file-ness |
|
80 self.fp = fp |
|
81 self.lineno = 0 |
|
82 self.ok = 0 |
|
83 self.empty = 0 |
|
84 self.buffer = None |
|
85 savestate = self['state'] |
|
86 self['state'] = NORMAL |
|
87 self.delete('1.0', END) |
|
88 self['state'] = savestate |
|
89 |
|
90 # End parsing -- must be busy, need not be at EOF |
|
91 def _endparser(self): |
|
92 if not self.busy(): |
|
93 raise RuntimeError, 'endparser: not busy' |
|
94 if self.buffer: |
|
95 self._parseline('') |
|
96 try: |
|
97 self.tk.deletefilehandler(self.fp) |
|
98 except TclError, msg: |
|
99 pass |
|
100 self.fp.close() |
|
101 self.fp = None |
|
102 del self.ok, self.empty, self.buffer |
|
103 |
|
104 # Parse a single line |
|
105 def _parseline(self, nextline): |
|
106 if not self.buffer: |
|
107 # Save this line -- we need one line read-ahead |
|
108 self.buffer = nextline |
|
109 return |
|
110 if emptyprog.match(self.buffer) >= 0: |
|
111 # Buffered line was empty -- set a flag |
|
112 self.empty = 1 |
|
113 self.buffer = nextline |
|
114 return |
|
115 textline = self.buffer |
|
116 if ulprog.match(nextline) >= 0: |
|
117 # Next line is properties for buffered line |
|
118 propline = nextline |
|
119 self.buffer = None |
|
120 else: |
|
121 # Next line is read-ahead |
|
122 propline = None |
|
123 self.buffer = nextline |
|
124 if not self.ok: |
|
125 # First non blank line after footer must be header |
|
126 # -- skip that too |
|
127 self.ok = 1 |
|
128 self.empty = 0 |
|
129 return |
|
130 if footerprog.match(textline) >= 0: |
|
131 # Footer -- start skipping until next non-blank line |
|
132 self.ok = 0 |
|
133 self.empty = 0 |
|
134 return |
|
135 savestate = self['state'] |
|
136 self['state'] = NORMAL |
|
137 if TkVersion >= 4.0: |
|
138 self.mark_set('insert', 'end-1c') |
|
139 else: |
|
140 self.mark_set('insert', END) |
|
141 if self.empty: |
|
142 # One or more previous lines were empty |
|
143 # -- insert one blank line in the text |
|
144 self._insert_prop('\n') |
|
145 self.lineno = self.lineno + 1 |
|
146 self.empty = 0 |
|
147 if not propline: |
|
148 # No properties |
|
149 self._insert_prop(textline) |
|
150 else: |
|
151 # Search for properties |
|
152 p = '' |
|
153 j = 0 |
|
154 for i in range(min(len(propline), len(textline))): |
|
155 if propline[i] != p: |
|
156 if j < i: |
|
157 self._insert_prop(textline[j:i], p) |
|
158 j = i |
|
159 p = propline[i] |
|
160 self._insert_prop(textline[j:]) |
|
161 self.lineno = self.lineno + 1 |
|
162 self['state'] = savestate |
|
163 |
|
164 # Insert a string at the end, with at most one property (tag) |
|
165 def _insert_prop(self, str, prop = ' '): |
|
166 here = self.index(AtInsert()) |
|
167 self.insert(AtInsert(), str) |
|
168 if TkVersion <= 4.0: |
|
169 tags = self.tag_names(here) |
|
170 for tag in tags: |
|
171 self.tag_remove(tag, here, AtInsert()) |
|
172 if prop != ' ': |
|
173 self.tag_add(prop, here, AtInsert()) |
|
174 |
|
175 # Readonly Man Page class -- disables editing, otherwise the same |
|
176 class ReadonlyManPage(EditableManPage): |
|
177 |
|
178 # Initialize instance |
|
179 def __init__(self, master=None, **cnf): |
|
180 cnf['state'] = DISABLED |
|
181 apply(EditableManPage.__init__, (self, master), cnf) |
|
182 |
|
183 # Alias |
|
184 ManPage = ReadonlyManPage |
|
185 |
|
186 # Test program. |
|
187 # usage: ManPage [manpage]; or ManPage [-f] file |
|
188 # -f means that the file is nroff -man output run through ul -i |
|
189 def test(): |
|
190 import os |
|
191 import sys |
|
192 # XXX This directory may be different on your system |
|
193 MANDIR = '/usr/local/man/mann' |
|
194 DEFAULTPAGE = 'Tcl' |
|
195 formatted = 0 |
|
196 if sys.argv[1:] and sys.argv[1] == '-f': |
|
197 formatted = 1 |
|
198 del sys.argv[1] |
|
199 if sys.argv[1:]: |
|
200 name = sys.argv[1] |
|
201 else: |
|
202 name = DEFAULTPAGE |
|
203 if not formatted: |
|
204 if name[-2:-1] != '.': |
|
205 name = name + '.n' |
|
206 name = os.path.join(MANDIR, name) |
|
207 root = Tk() |
|
208 root.minsize(1, 1) |
|
209 manpage = ManPage(root, relief=SUNKEN, borderwidth=2) |
|
210 manpage.pack(expand=1, fill=BOTH) |
|
211 if formatted: |
|
212 fp = open(name, 'r') |
|
213 else: |
|
214 fp = os.popen('nroff -man %s | ul -i' % name, 'r') |
|
215 manpage.parsefile(fp) |
|
216 root.mainloop() |
|
217 |
|
218 # Run the test program when called as a script |
|
219 if __name__ == '__main__': |
|
220 test() |