Fix an amazing number of typos & malformed sentences reported by Detlef
[python/dscho.git] / Demo / stdwin / wdiff.py
blob50ca0325d9670854fc52243eada1ac2c7bab2194
1 #! /usr/bin/env python
3 # A window-oriented recursive diff utility.
4 # NB: This uses undocumented window classing modules.
6 # TO DO:
7 # - faster update after moving/copying one file
8 # - diff flags (-b, etc.) should be global or maintained per window
9 # - use a few fixed windows instead of creating new ones all the time
10 # - ways to specify patterns to skip
11 # (best by pointing at a file and clicking a special menu entry!)
12 # - add rcsdiff menu commands
13 # - add a way to view status of selected files without opening them
14 # - add a way to diff two files with different names
15 # - add a way to rename files
16 # - keep backups of overwritten/deleted files
17 # - a way to mark specified files as uninteresting for dircmp
19 import sys
20 import os
21 import rand
22 import commands
23 import dircache
24 import statcache
25 import cmp
26 import cmpcache
27 import stdwin
28 import gwin
29 import textwin
30 import filewin
31 import tablewin
32 import anywin
34 mkarg = commands.mkarg
35 mk2arg = commands.mk2arg
37 # List of names to ignore in dircmp()
39 skiplist = ['RCS', 'CVS', '.Amake', 'tags', 'TAGS', '.', '..']
41 # Function to determine whether a name should be ignored in dircmp().
43 def skipthis(file):
44 return file[-1:] == '~' or file in skiplist
47 def anydiff(a, b, flags): # Display differences between any two objects
48 print 'diff', flags, a, b
49 if os.path.isdir(a) and os.path.isdir(b):
50 w = dirdiff(a, b, flags)
51 else:
52 w = filediff(a, b, flags)
53 addstatmenu(w, [a, b])
54 w.original_close = w.close
55 w.close = close_dirwin
56 return w
58 def close_dirwin(w):
59 close_subwindows(w, (), 0)
60 w.original_close(w)
62 def filediff(a, b, flags): # Display differences between two text files
63 diffcmd = 'diff'
64 if flags: diffcmd = diffcmd + mkarg(flags)
65 diffcmd = diffcmd + mkarg(a) + mkarg(b)
66 difftext = commands.getoutput(diffcmd)
67 return textwin.open_readonly(mktitle(a, b), difftext)
69 def dirdiff(a, b, flags): # Display differences between two directories
70 data = diffdata(a, b, flags)
71 w = tablewin.open(mktitle(a, b), data)
72 w.flags = flags
73 w.a = a
74 w.b = b
75 addviewmenu(w)
76 addactionmenu(w)
77 return w
79 def diffdata(a, b, flags): # Compute directory differences.
81 a_only = [('A only:', header_action), ('', header_action)]
82 b_only = [('B only:', header_action), ('', header_action)]
83 ab_diff = [('A <> B:', header_action), ('', header_action)]
84 ab_same = [('A == B:', header_action), ('', header_action)]
85 data = [a_only, b_only, ab_diff, ab_same]
87 a_list = dircache.listdir(a)[:]
88 b_list = dircache.listdir(b)[:]
89 dircache.annotate(a, a_list)
90 dircache.annotate(b, b_list)
91 a_list.sort()
92 b_list.sort()
94 for x in a_list:
95 if x in ['./', '../']:
96 pass
97 elif x not in b_list:
98 a_only.append((x, a_only_action))
99 else:
100 ax = os.path.join(a, x)
101 bx = os.path.join(b, x)
102 if os.path.isdir(ax) and os.path.isdir(bx):
103 if flags == '-r':
104 same = dircmp(ax, bx)
105 else:
106 same = 0
107 else:
108 try:
109 same = cmp.cmp(ax, bx)
110 except (RuntimeError, os.error):
111 same = 0
112 if same:
113 ab_same.append((x, ab_same_action))
114 else:
115 ab_diff.append((x, ab_diff_action))
117 for x in b_list:
118 if x in ['./', '../']:
119 pass
120 elif x not in a_list:
121 b_only.append((x, b_only_action))
123 return data
125 # Re-read the directory.
126 # Attempt to find the selected item back.
128 def update(w):
129 setbusy(w)
130 icol, irow = w.selection
131 if 0 <= icol < len(w.data) and 2 <= irow < len(w.data[icol]):
132 selname = w.data[icol][irow][0]
133 else:
134 selname = ''
135 statcache.forget_dir(w.a)
136 statcache.forget_dir(w.b)
137 tablewin.select(w, (-1, -1))
138 tablewin.update(w, diffdata(w.a, w.b, w.flags))
139 if selname:
140 for icol in range(len(w.data)):
141 for irow in range(2, len(w.data[icol])):
142 if w.data[icol][irow][0] == selname:
143 tablewin.select(w, (icol, irow))
144 break
146 # Action functions for table items in directory diff windows
148 def header_action(w, string, (icol, irow), (pos, clicks, button, mask)):
149 tablewin.select(w, (-1, -1))
151 def a_only_action(w, string, (icol, irow), (pos, clicks, button, mask)):
152 tablewin.select(w, (icol, irow))
153 if clicks == 2:
154 w2 = anyopen(os.path.join(w.a, string))
155 if w2:
156 w2.parent = w
158 def b_only_action(w, string, (icol, irow), (pos, clicks, button, mask)):
159 tablewin.select(w, (icol, irow))
160 if clicks == 2:
161 w2 = anyopen(os.path.join(w.b, string))
162 if w2:
163 w2.parent = w
165 def ab_diff_action(w, string, (icol, irow), (pos, clicks, button, mask)):
166 tablewin.select(w, (icol, irow))
167 if clicks == 2:
168 w2 = anydiff(os.path.join(w.a, string), os.path.join(w.b, string),'')
169 w2.parent = w
171 def ab_same_action(w, string, sel, detail):
172 ax = os.path.join(w.a, string)
173 if os.path.isdir(ax):
174 ab_diff_action(w, string, sel, detail)
175 else:
176 a_only_action(w, string, sel, detail)
178 def anyopen(name): # Open any kind of document, ignore errors
179 try:
180 w = anywin.open(name)
181 except (RuntimeError, os.error):
182 stdwin.message('Can\'t open ' + name)
183 return 0
184 addstatmenu(w, [name])
185 return w
187 def dircmp(a, b): # Compare whether two directories are the same
188 # To make this as fast as possible, it uses the statcache
189 print ' dircmp', a, b
190 a_list = dircache.listdir(a)
191 b_list = dircache.listdir(b)
192 for x in a_list:
193 if skipthis(x):
194 pass
195 elif x not in b_list:
196 return 0
197 else:
198 ax = os.path.join(a, x)
199 bx = os.path.join(b, x)
200 if statcache.isdir(ax) and statcache.isdir(bx):
201 if not dircmp(ax, bx): return 0
202 else:
203 try:
204 if not cmpcache.cmp(ax, bx): return 0
205 except (RuntimeError, os.error):
206 return 0
207 for x in b_list:
208 if skipthis(x):
209 pass
210 elif x not in a_list:
211 return 0
212 return 1
215 # View menu (for dir diff windows only)
217 def addviewmenu(w):
218 w.viewmenu = m = w.menucreate('View')
219 m.action = []
220 add(m, 'diff -r A B', diffr_ab)
221 add(m, 'diff A B', diff_ab)
222 add(m, 'diff -b A B', diffb_ab)
223 add(m, 'diff -c A B', diffc_ab)
224 add(m, 'gdiff A B', gdiff_ab)
225 add(m, ('Open A ', 'A'), open_a)
226 add(m, ('Open B ', 'B'), open_b)
227 add(m, 'Rescan', rescan)
228 add(m, 'Rescan -r', rescan_r)
230 # Action menu (for dir diff windows only)
232 def addactionmenu(w):
233 w.actionmenu = m = w.menucreate('Action')
234 m.action = []
235 add(m, 'cp A B', cp_ab)
236 add(m, 'rm B', rm_b)
237 add(m, '', nop)
238 add(m, 'cp B A', cp_ba)
239 add(m, 'rm A', rm_a)
241 # Main menu (global):
243 def mainmenu():
244 m = stdwin.menucreate('Wdiff')
245 m.action = []
246 add(m, ('Quit wdiff', 'Q'), quit_wdiff)
247 add(m, 'Close subwindows', close_subwindows)
248 return m
250 def add(m, text, action):
251 m.additem(text)
252 m.action.append(action)
254 def quit_wdiff(w, m, item):
255 if askyesno('Really quit wdiff altogether?', 1):
256 sys.exit(0)
258 def close_subwindows(w, m, item):
259 while 1:
260 for w2 in gwin.windows:
261 if w2.parent == w:
262 close_subwindows(w2, m, item)
263 w2.close(w2)
264 break # inner loop, continue outer loop
265 else:
266 break # outer loop
268 def diffr_ab(w, m, item):
269 dodiff(w, '-r')
271 def diff_ab(w, m, item):
272 dodiff(w, '')
274 def diffb_ab(w, m, item):
275 dodiff(w, '-b')
277 def diffc_ab(w, m, item):
278 dodiff(w, '-c')
280 def gdiff_ab(w, m, item): # Call SGI's gdiff utility
281 x = getselection(w)
282 if x:
283 a, b = os.path.join(w.a, x), os.path.join(w.b, x)
284 if os.path.isdir(a) or os.path.isdir(b):
285 stdwin.fleep() # This is for files only
286 else:
287 diffcmd = 'gdiff'
288 diffcmd = diffcmd + mkarg(a) + mkarg(b) + ' &'
289 print diffcmd
290 sts = os.system(diffcmd)
291 if sts: print 'Exit status', sts
293 def dodiff(w, flags):
294 x = getselection(w)
295 if x:
296 w2 = anydiff(os.path.join(w.a, x), os.path.join(w.b, x), flags)
297 w2.parent = w
299 def open_a(w, m, item):
300 x = getselection(w)
301 if x:
302 w2 = anyopen(os.path.join(w.a, x))
303 if w2:
304 w2.parent = w
306 def open_b(w, m, item):
307 x = getselection(w)
308 if x:
309 w2 = anyopen(os.path.join(w.b, x))
310 if w2:
311 w2.parent = w
313 def rescan(w, m, item):
314 w.flags = ''
315 update(w)
317 def rescan_r(w, m, item):
318 w.flags = '-r'
319 update(w)
321 def rm_a(w, m, item):
322 x = getselection(w)
323 if x:
324 if x[-1:] == '/': x = x[:-1]
325 x = os.path.join(w.a, x)
326 if os.path.isdir(x):
327 if askyesno('Recursively remove A directory ' + x, 1):
328 runcmd('rm -rf' + mkarg(x))
329 else:
330 runcmd('rm -f' + mkarg(x))
331 update(w)
333 def rm_b(w, m, item):
334 x = getselection(w)
335 if x:
336 if x[-1:] == '/': x = x[:-1]
337 x = os.path.join(w.b, x)
338 if os.path.isdir(x):
339 if askyesno('Recursively remove B directory ' + x, 1):
340 runcmd('rm -rf' + mkarg(x))
341 else:
342 runcmd('rm -f' + mkarg(x))
343 update(w)
345 def cp_ab(w, m, item):
346 x = getselection(w)
347 if x:
348 if x[-1:] == '/': x = x[:-1]
349 ax = os.path.join(w.a, x)
350 bx = os.path.join(w.b, x)
351 if os.path.isdir(ax):
352 if os.path.exists(bx):
353 m = 'Can\'t copy directory to existing target'
354 stdwin.message(m)
355 return
356 runcmd('cp -r' + mkarg(ax) + mkarg(w.b))
357 else:
358 runcmd('cp' + mkarg(ax) + mk2arg(w.b, x))
359 update(w)
361 def cp_ba(w, m, item):
362 x = getselection(w)
363 if x:
364 if x[-1:] == '/': x = x[:-1]
365 ax = os.path.join(w.a, x)
366 bx = os.path.join(w.b, x)
367 if os.path.isdir(bx):
368 if os.path.exists(ax):
369 m = 'Can\'t copy directory to existing target'
370 stdwin.message(m)
371 return
372 runcmd('cp -r' + mkarg(bx) + mkarg(w.a))
373 else:
374 runcmd('cp' + mk2arg(w.b, x) + mkarg(ax))
375 update(w)
377 def nop(args):
378 pass
380 def getselection(w):
381 icol, irow = w.selection
382 if 0 <= icol < len(w.data):
383 if 0 <= irow < len(w.data[icol]):
384 return w.data[icol][irow][0]
385 stdwin.message('no selection')
386 return ''
388 def runcmd(cmd):
389 print cmd
390 sts, output = commands.getstatusoutput(cmd)
391 if sts or output:
392 if not output:
393 output = 'Exit status ' + `sts`
394 stdwin.message(output)
397 # Status menu (for all kinds of windows)
399 def addstatmenu(w, files):
400 w.statmenu = m = w.menucreate('Stat')
401 m.files = files
402 m.action = []
403 for file in files:
404 m.additem(commands.getstatus(file))
405 m.action.append(stataction)
407 def stataction(w, m, item): # Menu item action for stat menu
408 file = m.files[item]
409 try:
410 m.setitem(item, commands.getstatus(file))
411 except os.error:
412 stdwin.message('Can\'t get status for ' + file)
415 # Compute a suitable window title from two paths
417 def mktitle(a, b):
418 if a == b: return a
419 i = 1
420 while a[-i:] == b[-i:]: i = i+1
421 i = i-1
422 if not i:
423 return a + ' ' + b
424 else:
425 return '{' + a[:-i] + ',' + b[:-i] + '}' + a[-i:]
428 # Ask a confirmation question
430 def askyesno(prompt, default):
431 try:
432 return stdwin.askync(prompt, default)
433 except KeyboardInterrupt:
434 return 0
437 # Display a message "busy" in a window, and mark it for updating
439 def setbusy(w):
440 left, top = w.getorigin()
441 width, height = w.getwinsize()
442 right, bottom = left + width, top + height
443 d = w.begindrawing()
444 d.erase((0, 0), (10000, 10000))
445 text = 'Busy...'
446 textwidth = d.textwidth(text)
447 textheight = d.lineheight()
448 h, v = left + (width-textwidth)/2, top + (height-textheight)/2
449 d.text((h, v), text)
450 del d
451 w.change((0, 0), (10000, 10000))
454 # Main function
456 def main():
457 print 'wdiff: warning: this program does NOT make backups'
458 argv = sys.argv
459 flags = ''
460 if len(argv) >= 2 and argv[1][:1] == '-':
461 flags = argv[1]
462 del argv[1]
463 stdwin.setdefscrollbars(0, 1)
464 m = mainmenu() # Create menu earlier than windows
465 if len(argv) == 2: # 1 argument
466 w = anyopen(argv[1])
467 if not w: return
468 elif len(argv) == 3: # 2 arguments
469 w = anydiff(argv[1], argv[2], flags)
470 w.parent = ()
471 else:
472 sys.stdout = sys.stderr
473 print 'usage:', argv[0], '[diff-flags] dir-1 [dir-2]'
474 sys.exit(2)
475 del w # It's preserved in gwin.windows
476 while 1:
477 try:
478 gwin.mainloop()
479 break
480 except KeyboardInterrupt:
481 pass # Just continue...
483 # Start the main function (this is a script)
484 main()