Initial Revision
[ohcount] / test / detect_files / py_script
1 #!/usr/bin/env python
2 # vim:fileencoding=utf8
3 # Note both python and vim understand the above encoding declaration
4
5 # Note using env above will cause the system to register
6 # the name (in ps etc.) as "python" rather than fslint-gui
7 # (because "python" is passed to exec by env).
8
9 """
10  FSlint - A utility to find File System lint.
11  Copyright © 2000-2006 by Pádraig Brady <P@draigBrady.com>.
12
13  This program is free software; you can redistribute it and/or modify
14  it under the terms of the GNU General Public License as published by
15  the Free Software Foundation; either version 2 of the License, or
16  any later version.
17
18  This program is distributed in the hope that it will be useful,
19  but WITHOUT ANY WARRANTY; without even the implied warranty of
20  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
21  See the GNU General Public License for more details,
22  which is available at www.gnu.org
23 """
24
25 import gettext
26 import locale
27 import gtk
28 import gtk.glade
29
30 import os, sys, time, stat
31
32 # Maintain compatibility with Python 2.2 and below,
33 # while getting rid of warnings in newer pyGtks.
34 try:
35     True, False
36 except NameError:
37     True, False = 1, 0
38
39 time_commands=False #print sub commands timing on status line
40
41 liblocation=os.path.dirname(os.path.abspath(sys.argv[0]))
42 locale_base=liblocation+'/po/locale'
43 try:
44     import fslint
45     if sys.argv[0][0] != '/':
46         #If relative probably debugging so don't use system files if possible
47         if not os.path.exists(liblocation+'/fslint'):
48             liblocation = fslint.liblocation
49             locale_base = None #sys default
50     else:
51         liblocation = fslint.liblocation
52         locale_base = None #sys default
53 except:
54     pass
55
56 class i18n:
57
58     def __init__(self):
59         #gtk2 only understands utf-8 so convert to unicode
60         #which will be automatically converted to utf-8 by pygtk
61         gettext.install("fslint", locale_base, unicode=1)
62         global N_ #translate string but not at point of definition
63         def N_(string): return string
64         td=gettext.bindtextdomain("fslint",locale_base)
65         gettext.textdomain("fslint")
66         #This call should be redundant
67         #(done correctly in gettext) for python 2.3
68         try:
69             gtk.glade.bindtextdomain("fslint",td) #note sets codeset to utf-8
70         except:
71             #if no glade.bindtextdomain then we'll do without
72             pass
73
74         try:
75             locale.setlocale(locale.LC_ALL,'')
76         except:
77             #gtk will print warning for a duff locale
78             pass
79         #Note python 2.3 has the nicer locale.getpreferredencoding()
80         self.preferred_encoding=locale.nl_langinfo(locale.CODESET)
81
82     class unicode_displayable(unicode):
83         """translate non displayable chars to an appropriate representation"""
84         translate_table=range(0x2400,0x2420) #control chars
85         def displayable(self,exclude):
86             if exclude:
87                 curr_table=self.__class__.translate_table[:] #copy
88                 for char in exclude:
89                     try:
90                         curr_table[ord(char)]=ord(char)
91                     except:
92                         pass #won't be converted anyway
93             else:
94                 curr_table=self.__class__.translate_table
95             return self.translate(curr_table)
96         #Note in python 2.2 this will be a new style class as
97         #we are subclassing the builtin (immutable) unicode type.
98         #Therefore you can make a factory function with the following.
99         #I.E. you can do uc=self.unicode_displayable(uc) below
100         #(the __class__ is redundant in python 2.2 also).
101         #def __new__(cls,string):
102         #    translated_string=unicode(string).translate(cls.translate_table)
103         #    return unicode.__new__(cls, translated_string)
104
105     #The following is used for display strings only
106     def utf8(self,orig,exclude=""):
107         """If orig is not a valid utf8 string,
108         then try to convert to utf8 using preferred encoding while
109         also replacing undisplayable or invalid characters.
110         Exclude contains control chars you don't want to translate."""
111         import types
112         if type(orig) == types.UnicodeType:
113             uc=orig
114         else:
115             try:
116                 uc=unicode(orig,"utf-8")
117             except:
118                 try:
119                     # Note I don't use "replace" here as that
120                     # replaces multiple bytes with a FFFD in utf8 locales
121                     # This is silly as then you know it's not utf8 so
122                     # one should try each character.
123                     uc=unicode(orig,self.preferred_encoding)
124                 except:
125                     uc=unicode(orig,"ascii","replace")
126                     # Note I don't like the "replacement char" representation
127                     # on fedora core 3 at least (bitstream-vera doesn't have it,
128                     # and the nimbus fallback looks like a comma).
129                     # It's even worse on redhat 9 where there is no
130                     # representation at all. Note FreeSans does have a nice
131                     # question mark representation, and dejavu also since 1.12.
132                     # Alternatively I could: uc=unicode(orig,"latin-1") as that
133                     # has a representation for each byte, but it's not general.
134         uc=self.__class__.unicode_displayable(uc).displayable(exclude)
135         return uc.encode("utf-8")
136
137 I18N=i18n() #call first so everything is internationalized
138
139 import getopt
140 def Usage():
141     print _("Usage: %s [OPTION] [PATHS]") % os.path.split(sys.argv[0])[1]
142     print _("    --version       display version")
143     print _("    --help          display help")
144
145 try:
146     lOpts, lArgs = getopt.getopt(sys.argv[1:], "", ["help","version"])
147
148     if len(lArgs) == 0:
149         lArgs = [os.getcwd()]
150
151     if ("--help","") in lOpts:
152         Usage()
153         sys.exit(None)
154
155     if ("--version","") in lOpts:
156         print "FSlint 2.19"
157         sys.exit(None)
158 except getopt.error, msg:
159     print msg
160     print
161     Usage()
162     sys.exit(2)
163
164 def human_num(num, divisor=1, power=""):
165     num=float(num)
166     if divisor == 1:
167         return locale.format("%ld",int(num),1)
168     elif divisor == 1000:
169         powers=[" ","K","M","G","T","P"]
170     elif divisor == 1024:
171         powers=["  ","Ki","Mi","Gi","Ti","Pi"]
172     else:
173         raise ValueError, "Invalid divisor"
174     if not power: power=powers[0]
175     while num >= 1000: #4 digits
176         num /= divisor
177         power=powers[powers.index(power)+1]
178         human_num(num,divisor,power)
179     if power.strip():
180         return "%6.1f%s" % (num,power)
181     else:
182         return "%4ld  %s" % (num,power)
183
184 class GladeWrapper:
185     """
186     Superclass for glade based applications. Just derive from this
187     and your subclass should create methods whose names correspond to
188     the signal handlers defined in the glade file. Any other attributes
189     in your class will be safely ignored.
190
191     This class will give you the ability to do:
192         subclass_instance.GtkWindow.method(...)
193         subclass_instance.widget_name...
194     """
195     def __init__(self, Filename, WindowName):
196         #load glade file.
197         self.widgets = gtk.glade.XML(Filename, WindowName, gettext.textdomain())
198         self.GtkWindow = getattr(self, WindowName)
199
200         instance_attributes = {}
201         for attribute in dir(self.__class__):
202             instance_attributes[attribute] = getattr(self, attribute)
203         self.widgets.signal_autoconnect(instance_attributes)
204
205     def __getattr__(self, attribute): #Called when no attribute in __dict__
206         widget = self.widgets.get_widget(attribute)
207         if widget is None:
208             raise AttributeError("Widget [" + attribute + "] not found")
209         self.__dict__[attribute] = widget #add reference to cache
210         return widget
211
212 # SubProcess Example:
213 #
214 #     process = subProcess("your shell command")
215 #     process.read() #timeout is optional
216 #     handle(process.outdata, process.errdata)
217 #     del(process)
218 import time, os, select, signal
219 class subProcess:
220     """Class representing a child process. It's like popen2.Popen3
221        but there are three main differences.
222        1. This makes the new child process group leader (using setpgrp())
223           so that all children can be killed.
224        2. The output function (read) is optionally non blocking returning in
225           specified timeout if nothing is read, or as close to specified
226           timeout as possible if data is read.
227        3. The output from both stdout & stderr is read (into outdata and
228           errdata). Reading from multiple outputs while not deadlocking
229           is not trivial and is often done in a non robust manner."""
230
231     def __init__(self, cmd, bufsize=8192):
232         """The parameter 'cmd' is the shell command to execute in a
233         sub-process. If the 'bufsize' parameter is specified, it
234         specifies the size of the I/O buffers from the child process."""
235         self.cleaned=False
236         self.BUFSIZ=bufsize
237         self.outr, self.outw = os.pipe()
238         self.errr, self.errw = os.pipe()
239         self.pid = os.fork()
240         if self.pid == 0:
241             self._child(cmd)
242         os.close(self.outw) #parent doesn't write so close
243         os.close(self.errw)
244         # Note we could use self.stdout=fdopen(self.outr) here
245         # to get a higher level file object like popen2.Popen3 uses.
246         # This would have the advantages of auto handling the BUFSIZ
247         # and closing the files when deleted. However it would mean
248         # that it would block waiting for a full BUFSIZ unless we explicitly
249         # set the files non blocking, and there would be extra uneeded
250         # overhead like EOL conversion. So I think it's handier to use os.read()
251         self.outdata = self.errdata = ''
252         self._outeof = self._erreof = 0
253
254     def _child(self, cmd):
255         # Note sh below doesn't setup a seperate group (job control)
256         # for non interactive shells (hmm maybe -m option does?)
257         os.setpgrp() #seperate group so we can kill it
258         os.dup2(self.outw,1) #stdout to write side of pipe
259         os.dup2(self.errw,2) #stderr to write side of pipe
260         #stdout & stderr connected to pipe, so close all other files
261         map(os.close,[self.outr,self.outw,self.errr,self.errw])
262         try:
263             cmd = ['/bin/sh', '-c', cmd]
264             os.execvp(cmd[0], cmd)
265         finally: #exit child on error
266             os._exit(1)
267
268     def read(self, timeout=None):
269         """return 0 when finished
270            else return 1 every timeout seconds
271            data will be in outdata and errdata"""
272         currtime=time.time()
273         while 1:
274             tocheck=[self.outr]*(not self._outeof)+ \
275                     [self.errr]*(not self._erreof)
276             ready = select.select(tocheck,[],[],timeout)
277             if len(ready[0]) == 0: #no data timeout
278                 return 1
279             else:
280                 if self.outr in ready[0]:
281                     outchunk = os.read(self.outr,self.BUFSIZ)
282                     if outchunk == '':
283                         self._outeof = 1
284                     self.outdata += outchunk
285                 if self.errr in ready[0]:
286                     errchunk = os.read(self.errr,self.BUFSIZ)
287                     if errchunk == '':
288                         self._erreof = 1
289                     self.errdata += errchunk
290                 if self._outeof and self._erreof:
291                     return 0
292                 elif timeout:
293                     if (time.time()-currtime) > timeout:
294                         return 1 #may be more data but time to go
295
296     def kill(self):
297         os.kill(-self.pid, signal.SIGTERM) #kill whole group
298
299     def cleanup(self):
300         """Wait for and return the exit status of the child process."""
301         self.cleaned=True
302         os.close(self.outr)
303         os.close(self.errr)
304         pid, sts = os.waitpid(self.pid, 0)
305         if pid == self.pid:
306             self.sts = sts
307         return self.sts
308
309     def __del__(self):
310         if not self.cleaned:
311             self.cleanup()
312
313 # Determine what type of distro we're on.
314 class distroType:
315     def __init__(self):
316         self.rpm = self.deb = False
317         if os.path.exists("/etc/redhat-release"):
318             self.rpm = True
319         elif os.path.exists("/etc/debian_version"):
320             self.deb = True
321         else:
322             self.rpm = (os.system("rpm --version >/dev/null 2>&1") == 0)
323             if not self.rpm:
324                 self.deb = (os.system("dpkg --version >/dev/null 2>&1") == 0)
325 dist_type=distroType()
326
327 def human_space_left(where):
328     (device, total, used_b, avail, used_p, mount)=\
329     os.popen("df -h %s | tail -1"%where).read().split()
330     return _("%s of %s is used leaving %sB available") % (used_p, mount, avail)
331
332 class dlgUserInteraction(GladeWrapper):
333     """
334     Note input buttons tuple text should not be translated
335     so that the stock buttons are used if possible. But the
336     translations should be available, so use N_ for input
337     buttons tuple text. Note the returned response is not
338     translated either.
339     """
340     def init(self, app, message, buttons=('Ok',)):
341         for text in buttons:
342             try:
343                 stock_text=getattr(gtk,"STOCK_"+text.upper())
344                 button = gtk.Button(stock=stock_text)
345             except:
346                 button = gtk.Button(label=_(text))
347             button.set_data("text", text)
348             button.connect("clicked", self.button_clicked)
349             self.GtkWindow.action_area.pack_start(button)
350             button.show()
351             button.set_flags(gtk.CAN_DEFAULT)
352             button.grab_default() #last button is default
353         self.app = app
354         self.lblmsg.set_text(message)
355         self.lblmsg.show()
356         if message.endswith(":"):
357             self.entry.show()
358         else:
359             self.entry.hide()
360
361     def show(self):
362         self.response=None
363         self.GtkWindow.set_transient_for(self.app.GtkWindow)#center on main win
364         self.GtkWindow.show()
365         if self.GtkWindow.modal:
366             gtk.main() #synchronous call from parent
367         #else: not supported
368
369     #################
370     # Signal handlers
371     #################
372
373     def quit(self, *args):
374         self.GtkWindow.hide()
375         if self.GtkWindow.modal:
376             gtk.main_quit()
377
378     def button_clicked(self, button):
379         self.input = self.entry.get_text()
380         self.response = button.get_data("text")
381         self.quit()
382
383 class dlgPathSel(GladeWrapper):
384
385     def init(self, app):
386         self.app = app
387         self.pwd = self.app.pwd
388
389     def show(self, fileOps=0):
390         self.canceled=1
391         self.fileOps = fileOps
392         self.GtkWindow.set_filename(self.pwd+'/')
393         if self.fileOps:
394             self.GtkWindow.show_fileop_buttons()
395         else:
396             self.GtkWindow.hide_fileop_buttons()
397         #self.GtkWindow.set_parent(self.app.GtkWindow)#error (on gtk-1.2.10-11?)
398         self.GtkWindow.set_transient_for(self.app.GtkWindow)#center on main win
399         self.GtkWindow.show()
400         if self.GtkWindow.modal:
401             gtk.main() #synchronous call from parent
402         #else: not supported
403
404     #################
405     # Signal handlers
406     #################
407
408     def quit(self, *args):
409         self.GtkWindow.hide()
410         if not self.canceled:
411             self.pwd = self.GtkWindow.history_pulldown.get_children()[0].get()
412         if self.GtkWindow.modal:
413             gtk.main_quit()
414         return True #Don't let window be destroyed
415
416     def on_okdirs_clicked(self, *args):
417         if self.fileOps: #creating new item
418             file = self.GtkWindow.get_filename()
419             if os.path.exists(file):
420                 if os.path.isfile(file):
421                     msgbox=dlgUserInteraction(liblocation+"/fslint.glade",
422                                               "UserInteraction")
423                     msgbox.init(self,
424                                 _("Do you want to overwrite?\n") + file,
425                                 (N_('Yes'), N_('No')))
426                     msgbox.show()
427                     if msgbox.response != "Yes":
428                         return
429                 else:
430                     msgbox=dlgUserInteraction(liblocation+"/fslint.glade",
431                                               "UserInteraction")
432                     msgbox.init(self, _("You can't overwrite ") + file)
433                     msgbox.show()
434                     return
435         self.canceled=0
436         self.quit()
437
438 class fslint(GladeWrapper):
439
440     class UserAbort(Exception):
441         pass
442
443     def __init__(self, Filename, WindowName):
444         os.close(0) #don't hang if any sub cmd tries to read from stdin
445         self.pwd=os.path.realpath(os.curdir)
446         GladeWrapper.__init__(self, Filename, WindowName)
447         self.dlgPathSel = dlgPathSel(liblocation+"/fslint.glade", "PathSel")
448         self.dlgPathSel.init(self)
449         #Just need to keep this tuple in sync with tabs
450         (self.mode_up, self.mode_pkgs, self.mode_nl, self.mode_sn,
451          self.mode_tf, self.mode_bl, self.mode_id, self.mode_ed,
452          self.mode_ns, self.mode_rs) = range(10)
453         self.clists = {
454             self.mode_up:self.clist_dups,
455             self.mode_pkgs:self.clist_pkgs,
456             self.mode_nl:self.clist_nl,
457             self.mode_sn:self.clist_sn,
458             self.mode_tf:self.clist_tf,
459             self.mode_bl:self.clist_bl,
460             self.mode_id:self.clist_id,
461             self.mode_ed:self.clist_ed,
462             self.mode_ns:self.clist_ns,
463             self.mode_rs:self.clist_rs
464         }
465         self.mode_descs = {
466             self.mode_up:_("Files with the same content"),
467             self.mode_pkgs:_("Installed packages ordered by disk usage"),
468             self.mode_nl:_("Problematic filenames"),
469             self.mode_sn:_("Possibly conflicting commands or filenames"),
470             self.mode_tf:_("Possibly old temporary files"),
471             self.mode_bl:_("Problematic symbolic links"),
472             self.mode_id:_("Files with missing user IDs"),
473             self.mode_ed:_("Empty directories"),
474             self.mode_ns:_("Executables still containing debugging info"),
475             self.mode_rs:_("Whitespace at the end of lines in a text file")
476         }
477         for path in lArgs:
478             if os.path.exists(path):
479                 self.addDirs(self.ok_dirs, os.path.abspath(path))
480             else:
481                 self.ShowErrors(_("Invalid path [") + path + "]")
482         for bad_dir in ['/lost+found','/dev','/proc','/sys','/tmp',
483                         '/*.svn','/*CVS']:
484             self.addDirs(self.bad_dirs, bad_dir)
485         self.mode=0
486         self.status.set_text(self.mode_descs[self.mode])
487         self.bg_colour=self.vpanes.get_style().bg[gtk.STATE_NORMAL]
488         #make readonly GtkEntry and GtkTextView widgets obviously so,
489         #by making them the same colour as the surrounding panes
490         self.errors.modify_base(gtk.STATE_NORMAL,self.bg_colour)
491         self.status.modify_base(gtk.STATE_NORMAL,self.bg_colour)
492         self.pkg_info.modify_base(gtk.STATE_NORMAL,self.bg_colour)
493         #Other GUI stuff that ideally [lib]glade should support
494         self.clist_pkgs.set_column_visibility(2,0)
495         self.clist_ns.set_column_justification(2,gtk.JUSTIFY_RIGHT)
496
497     def get_fslint(self, command, delim='\n'):
498         self.fslintproc = subProcess(command)
499         count=0
500         while self.fslintproc.read(timeout=0.1):
501             self.status.set_text(_("searching")+"."*(count%10))
502             count+=1
503             while gtk.events_pending(): gtk.main_iteration(False)
504             if self.stopflag:
505                 self.fslintproc.kill()
506                 break
507         else:
508             self.fslintproc.outdata=self.fslintproc.outdata.split(delim)[:-1]
509             ret = (self.fslintproc.outdata, self.fslintproc.errdata)
510         del(self.fslintproc)
511         self.status.set_text(_("processing..."))
512         while gtk.events_pending(): gtk.main_iteration(False)
513         if self.stopflag:
514             raise self.UserAbort
515         else:
516             return ret
517
518     def ShowErrors(self, lines):
519         if len(lines) == 0:
520             return
521         end=self.errors.get_buffer().get_end_iter()
522         self.errors.get_buffer().insert(end,I18N.utf8(lines,"\n"))
523
524     def ClearErrors(self):
525         self.errors.get_buffer().set_text("")
526
527     def buildFindParameters(self):
528         if self.mode == self.mode_sn:
529             if self.chk_sn_path.get_active():
530                 return ""
531         elif self.mode == self.mode_ns:
532             if self.chk_ns_path.get_active():
533                 return ""
534         elif self.mode == self.mode_pkgs:
535             return ""
536
537         if self.ok_dirs.rows == 0:
538             #raise _("No search paths specified") #py 2.2.2 can't raise unicode
539             return _("No search paths specified")
540
541         search_dirs = ""
542         for row in range(self.ok_dirs.rows):
543             search_dirs = search_dirs + \
544                           " '" + self.ok_dirs.get_row_data(row) + "'"
545         exclude_dirs=""
546
547         for row in range(self.bad_dirs.rows):
548             if exclude_dirs == "":
549                 exclude_dirs = '\(' + ' -path "'
550             else:
551                 exclude_dirs += ' -o -path "'
552             exclude_dirs += self.bad_dirs.get_row_data(row) + '"'
553         if exclude_dirs != "":
554             exclude_dirs += ' \) -prune -o '
555
556         if not self.recurseDirs.get_active():
557             recurseParam=" -r "
558         else:
559             recurseParam=" "
560
561         self.findParams = search_dirs + " -f " + recurseParam + exclude_dirs
562         self.findParams += self.extra_find_params.get_text()
563
564         return ""
565
566     def addDirs(self, dirlist, dirs):
567         """Adds items to passed clist."""
568         dirlist.append([I18N.utf8(dirs)])
569         dirlist.set_row_data(dirlist.rows-1, dirs)
570
571     def removeDirs(self, dirlist):
572         """Removes selected items from passed clist.
573            If no items selected then list is cleared."""
574         paths_to_remove = dirlist.selection
575         if len(paths_to_remove) == 0:
576             dirlist.clear()
577         else:
578             paths_to_remove.sort() #selection order irrelevant/problematic
579             paths_to_remove.reverse() #so deleting works correctly
580             for row in paths_to_remove:
581                 dirlist.remove(row)
582
583     def clist_alloc_colour(self, clist, colour):
584         """cache colour allocation for clist
585            as colour will probably be used multiple times"""
586         cmap = clist.get_colormap()
587         cmap_cache=clist.get_data("cmap_cache")
588         if cmap_cache == None:
589             cmap_cache={'':None}
590         if not cmap_cache.has_key(colour):
591             cmap_cache[colour]=cmap.alloc_color(colour)
592         clist.set_data("cmap_cache",cmap_cache)
593         return cmap_cache[colour]
594
595     def clist_append_path(self, clist, path, colour, *rest):
596         """append path to clist, handling utf8 and colour issues"""
597         colour = self.clist_alloc_colour(clist, colour)
598         utf8_path=I18N.utf8(path)
599         (dir,file)=os.path.split(utf8_path)
600         clist.append((file,dir)+rest)
601         row_data = clist.get_data("row_data")
602         row_data.append(path+'\n') #add '\n' so can write with writelines
603         if colour:
604             clist.set_foreground(clist.rows-1,colour)
605
606     def clist_append_group_row(self, clist, cols):
607         """append header to clist"""
608         clist.append(cols)
609         row_data = clist.get_data("row_data")
610         row_data.append('#'+"\t".join(cols).rstrip()+'\n')
611         clist.set_background(clist.rows-1,self.bg_colour)
612         clist.set_selectable(clist.rows-1,0)
613
614     def get_path_info(self, path):
615         """Returns path info appropriate for display, i.e.
616            (color, size, ondisk_size, username, groupname, mtime_ls_date)"""
617
618         stat_val = os.lstat(path)
619
620         date = time.ctime(stat_val[stat.ST_MTIME])
621         month_time = date[4:16]
622         year = date[-5:]
623         timediff = time.time()-stat_val[stat.ST_MTIME]
624         if timediff > 15552000: #6months
625             date = month_time[0:6] + year
626         else:
627             date = month_time
628
629         mode = stat_val[stat.ST_MODE]
630         if stat.S_ISREG(mode):
631             colour = '' #default
632             if mode & (stat.S_IXGRP|stat.S_IXUSR|stat.S_IXOTH):
633                 colour = "#00C000"
634         elif stat.S_ISDIR(mode):
635             colour = "blue"
636         elif stat.S_ISLNK(mode):
637             colour = "cyan"
638             if not os.path.exists(path):
639                 colour = "#C00000"
640         else:
641             colour = "tan"
642
643         size=stat_val.st_blocks*512
644
645         return (colour, stat_val[stat.ST_SIZE], size, stat_val[stat.ST_UID],
646                 stat_val[stat.ST_GID], date, stat_val[stat.ST_MTIME])
647
648     def whatRequires(self, packages, level=0):
649         if not packages: return
650         if level==0: self.checked_pkgs={}
651         for package in packages:
652             #print "\t"*level+package
653             self.checked_pkgs[package]=''
654         if dist_type.rpm:
655             cmd  = r"rpm -e --test " + ' '.join(packages) + r" 2>&1 | "
656             cmd += r"sed -n 's/.*is needed by (installed) \(.*\)/\1/p' | "
657             cmd += r"LANG=C sort | uniq"
658         elif dist_type.deb:
659             cmd  = r"dpkg --purge --dry-run " + ' '.join(packages) + r" 2>&1 | "
660             cmd += r"sed -n 's/ \(.*\) depends on.*/\1/p' | "
661             cmd += r"LANG=C sort | uniq"
662         else:
663             raise "unknown distro"
664         process = os.popen(cmd)
665         requires = process.read()
666         del(process)
667         new_packages = [p for p in requires.split()
668                         if not self.checked_pkgs.has_key(p)]
669         self.whatRequires(new_packages, level+1)
670         if level==0: return self.checked_pkgs.keys()
671
672     def findpkgs(self, clist_pkgs):
673         self.clist_pkgs_order=[False,False] #unordered, descending
674         self.clist_pkgs_user_input=False
675         self.pkg_info.get_buffer().set_text("")
676         if dist_type.deb:
677             #Package names unique on debian
678             cmd=r"dpkg-query -W --showformat='${Package}\t${Installed-Size}"
679             cmd+=r"\t${Status}\n' | LANG=C grep -F 'installed' | cut -f1,2"
680         elif dist_type.rpm:
681             #Must include version names to uniquefy on redhat
682             cmd=r"rpm -q -a --queryformat '%{N}-%{V}-%{R}.%{ARCH}\t%{SIZE}\n'"
683         else:
684             return ("", _("Sorry, FSlint does not support this functionality \
685 on your system at present."))
686         po, pe = self.get_fslint(cmd + " | LANG=C sort -k2,2rn")
687         pkg_tot = 0
688         row = 0
689         for pkg_info in po:
690             pkg_name, pkg_size= pkg_info.split()
691             pkg_size = int(pkg_size)
692             if dist_type.deb:
693                 pkg_size = pkg_size*1024
694             pkg_tot += pkg_size
695             clist_pkgs.append([pkg_name,human_num(pkg_size,1000).strip(),
696                                "%010ld"%pkg_size])
697             clist_pkgs.set_row_data(row, pkg_name)
698             row=row+1
699
700         return (str(row) + _(" packages, ") +
701                 _("consuming %sB. ") % human_num(pkg_tot,1000).strip() +
702                 _("Note %s.") % human_space_left('/'), pe)
703         #Note pkgs generally installed on root partition so report space left.
704
705     def findrs(self, clist_rs):
706         po, pe = self.get_fslint("./findrs " + self.findParams)
707         row = 0
708         for line in po:
709             colour,fsize,size,uid,gid,date,mtime = self.get_path_info(line)
710             self.clist_append_path(clist_rs, line, colour, str(fsize), date)
711             row=row+1
712
713         return (str(row) + _(" files"), pe)
714
715     def findns(self, clist_ns):
716         cmd = "./findns "
717         if not self.chk_ns_path.get_active():
718             cmd += self.findParams
719         po, pe = self.get_fslint(cmd)
720
721         unstripped=[]
722         for line in po:
723             colour,fsize,size,uid,gid,date,mtime = self.get_path_info(line)
724             unstripped.append((fsize, line, date))
725         unstripped.sort()
726         unstripped.reverse()
727
728         row = 0
729         for fsize, path, date in unstripped:
730             self.clist_append_path(clist_ns, path, '', human_num(fsize), date)
731             row += 1
732
733         return (str(row) + _(" unstripped binaries"), pe)
734
735     def finded(self, clist_ed):
736         po, pe = self.get_fslint("./finded " + self.findParams)
737         row = 0
738         for line in po:
739             colour,fsize,size,uid,gid,date,mtime = self.get_path_info(line)
740             self.clist_append_path(clist_ed, line, '', date)
741             row += 1
742
743         return (str(row) + _(" empty directories"), pe)
744
745     def findid(self, clist_id):
746         po, pe = self.get_fslint("./findid " + self.findParams, '\0')
747         row = 0
748         for record in po:
749             colour,fsize,size,uid,gid,date,mtime = self.get_path_info(record)
750             self.clist_append_path(clist_id, record, colour,
751                                    str(uid), str(gid), str(fsize), date)
752             row += 1
753
754         return (str(row) + _(" files"), pe)
755
756     def findbl(self, clist_bl):
757         cmd = "./findbl " + self.findParams
758         if self.opt_bl_dangling.get_active():
759             cmd += " -d"
760         elif self.opt_bl_suspect.get_active():
761             cmd += " -s"
762         elif self.opt_bl_relative.get_active():
763             cmd += " -l"
764         elif self.opt_bl_absolute.get_active():
765             cmd += " -A"
766         elif self.opt_bl_redundant.get_active():
767             cmd += " -n"
768         po, pe = self.get_fslint(cmd)
769         row = 0
770         for line in po:
771             link, target = line.split(" -> ", 2)
772             self.clist_append_path(clist_bl, link, '', target)
773             row += 1
774
775         return (str(row) + _(" links"), pe)
776
777     def findtf(self, clist_tf):
778         cmd = "./findtf " + self.findParams
779         cmd += " --age=" + str(int(self.spin_tf_core.get_value()))
780         if self.chk_tf_core.get_active():
781             cmd += " -c"
782         po, pe = self.get_fslint(cmd)
783         row = 0
784         byteWaste = 0
785         for line in po:
786             colour,fsize,size,uid,gid,date,mtime = self.get_path_info(line)
787             self.clist_append_path(clist_tf, line, '', str(fsize), date)
788             byteWaste+=size
789             row += 1
790
791         return (human_num(byteWaste) + _(" bytes wasted in ") +
792                 str(row) + _(" files"), pe)
793
794     def findsn(self, clist_sn):
795         cmd = "./findsn "
796         if self.chk_sn_path.get_active():
797             option = self.opt_sn_path.get_children()[0].get()
798             if option == _("Aliases"):
799                 cmd += " -A"
800             elif option == _("Conflicting files"):
801                 pass #default mode
802             else:
803                 raise "glade GtkOptionMenu item not found"
804         else:
805             cmd += self.findParams
806             option = self.opt_sn_paths.get_children()[0].get()
807             if option == _("Aliases"):
808                 cmd += " -A"
809             elif option == _("Same names"):
810                 pass #default mode
811             elif option == _("Same names(ignore case)"):
812                 cmd += " -C"
813             elif option == _("Case conflicts"):
814                 cmd += " -c"
815             else:
816                 raise "glade GtkOptionMenu item not found"
817
818         po, pe = self.get_fslint(cmd,'\0')
819         po = po[1:]
820         row=0
821         findsn_number=0
822         findsn_groups=0
823         for record in po:
824             if record == '':
825                 self.clist_append_group_row(clist_sn, ['','',''])
826                 findsn_groups += 1
827             else:
828                 colour,fsize,size,uid,gid,date,mtime=self.get_path_info(record)
829                 self.clist_append_path(clist_sn, record, colour, str(fsize))
830                 clist_sn.set_row_data(clist_sn.rows-1,mtime)
831                 findsn_number += 1
832             row += 1
833         if findsn_number:
834             findsn_groups += 1 #for stripped group head above
835
836         return (str(findsn_number) + _(" files (in ") + str(findsn_groups) +
837                 _(" groups)"), pe)
838
839     def findnl(self, clist_nl):
840         if self.chk_findu8.get_active():
841             po, pe = self.get_fslint("./findu8 " + self.findParams, '\0')
842         else:
843             sensitivity = ('1','2','3','p')[
844             int(self.hscale_findnl_level.get_adjustment().value)-1]
845             po, pe = self.get_fslint("./findnl " + self.findParams + " -" +
846                                     sensitivity, '\0')
847
848         row=0
849         for record in po:
850             colour,fsize,size,uid,gid,date,mtime = self.get_path_info(record)
851             self.clist_append_path(clist_nl, record, colour)
852             row += 1
853
854         return (str(row) + _(" files"), pe)
855
856     def findup(self, clist_dups):
857         po, pe = self.get_fslint("./findup " + self.findParams + " --gui")
858
859         numdups = size = fsize = 0
860         alldups = []
861         inodes = {}
862         #inodes required to correctly report disk usage of
863         #duplicate files with seperate inode groups.
864         for line in po:
865             line = line.strip()
866             if line == '': #grouped == 1
867                 if len(inodes)>1:
868                     alldups.append(((numdups-1)*size, numdups, fsize, dups))
869                 dups = []
870                 numdups = 0
871                 inodes = {}
872             else:
873                 colour,fsize,size,uid,gid,date,mtime = self.get_path_info(line)
874                 dups.append((line,date,mtime))
875                 inode = os.lstat(line)[stat.ST_INO]
876                 if not inodes.has_key(inode):
877                     inodes[inode] = True
878                     numdups = numdups + 1
879         else:
880             if len(inodes)>1:
881                 alldups.append(((numdups-1)*size, numdups, fsize, dups))
882
883         alldups.sort()
884         alldups.reverse()
885
886         byteWaste = 0
887         numWaste = 0
888         row=0
889         for groupWaste, groupSize, FilesSize, files in alldups:
890             byteWaste += groupWaste
891             numWaste += groupSize - 1
892             groupHeader = ["%d x %s" % (groupSize, human_num(FilesSize)),
893                            "=> %s" % human_num(groupWaste),
894                            _("bytes wasted")]
895             self.clist_append_group_row(clist_dups, groupHeader)
896             row += 1
897             for file in files:
898                 self.clist_append_path(clist_dups,file[0],'',file[1])
899                 clist_dups.set_row_data(row,file[2]) #mtime
900                 row += 1
901
902         return (human_num(byteWaste) + _(" bytes wasted in ") +
903                 str(numWaste) + _(" files (in ") + str(len(alldups)) +
904                 _(" groups)"), pe)
905
906     find_dispatch = (findup,findpkgs,findnl,findsn,findtf,
907                      findbl,findid,finded,findns,findrs) #order NB
908
909     def enable_stop(self):
910         self.find.hide()
911         self.stop.show()
912         self.stop.grab_focus()
913         self.stop.grab_add() #Just allow app to stop
914
915     def disable_stop(self):
916         self.stop.grab_remove()
917         self.stop.hide()
918         self.find.show()
919         self.find.grab_focus() #as it already would have been in focus
920
921     def check_user(self, question):
922         self.ClearErrors()
923         #remove as status from previous operation could be confusing
924         self.status.delete_text(0,-1)
925         clist = self.clists[self.mode]
926         if not clist.rows:
927             return False #Be consistent
928                          #All actions buttons do nothing if no results
929         paths = clist.selection
930         if len(paths) == 0:
931             self.ShowErrors(_("None selected"))
932             return False
933         else:
934             msgbox = dlgUserInteraction(liblocation+"/fslint.glade",
935                                         "UserInteraction")
936             if len(paths) == 1:
937                 question += _(" this item?\n")
938             else:
939                 question += _(" these %d items?\n") %  len(paths)
940             msgbox.init(self, question, (N_('Yes'), N_('No')))
941             msgbox.show()
942             if msgbox.response != "Yes":
943                 return False
944             return True
945
946     #################
947     # Signal handlers
948     #################
949
950     def on_fslint_destroy(self, event):
951         try:
952             self.fslintproc.kill()
953             del(self.fslintproc)
954         except:
955             pass #fslint wasn't searching
956         gtk.main_quit()
957
958     def on_addOkDir_clicked(self, event):
959         self.dlgPathSel.show()
960         if not self.dlgPathSel.canceled:
961             path = self.dlgPathSel.GtkWindow.get_filename()
962             self.addDirs(self.ok_dirs, path)
963
964     def on_addBadDir_clicked(self, event):
965         self.dlgPathSel.show()
966         if not self.dlgPathSel.canceled:
967             path = self.dlgPathSel.GtkWindow.get_filename()
968             self.addDirs(self.bad_dirs, path)
969
970     def on_removeOkDir_clicked(self, event):
971         self.removeDirs(self.ok_dirs)
972
973     def on_removeBadDir_clicked(self, event):
974         self.removeDirs(self.bad_dirs)
975
976     def on_chk_sn_path_toggled(self, event):
977         if self.chk_sn_path.get_active():
978             self.hbox_sn_paths.hide()
979             self.hbox_sn_path.show()
980         else:
981             self.hbox_sn_path.hide()
982             self.hbox_sn_paths.show()
983
984     def on_chk_findu8_toggled(self, event):
985         if self.chk_findu8.get_active():
986             self.hscale_findnl_level.hide()
987             self.lbl_findnl_sensitivity.hide()
988         else:
989             self.hscale_findnl_level.show()
990             self.lbl_findnl_sensitivity.show()
991
992     def on_fslint_functions_switch_page(self, widget, dummy, pagenum):
993         self.ClearErrors()
994         self.mode=pagenum
995         self.status.set_text(self.mode_descs[self.mode])
996         if self.mode == self.mode_up:
997             self.autoMerge.show()
998         else:
999             self.autoMerge.hide()
1000         if self.mode == self.mode_ns or self.mode == self.mode_rs: #bl in future
1001             self.autoClean.show()
1002         else:
1003             self.autoClean.hide()
1004
1005     def on_selection_menu_button_press_event(self, widget, event):
1006         if self.mode == self.mode_up or self.mode == self.mode_sn:
1007             self.groups_menu.show()
1008         else:
1009             self.groups_menu.hide()
1010         if event == None: #keyboard click
1011             self.selection_menu_menu.popup(None,None,None,3,0) #at mouse pointer
1012         elif type(widget) == gtk.Button or \
1013              (type(widget) == gtk.CList and event.button == 3):
1014             self.selection_menu_menu.popup(None,None,None,event.button,0)
1015             if type(widget) == gtk.Button:
1016                 widget.grab_focus()
1017             return True #Don't fire click event
1018
1019     def on_find_clicked(self, event):
1020
1021         try:
1022             self.ClearErrors()
1023             errors=""
1024             clist = self.clists[self.mode]
1025             os.chdir(liblocation+"/fslint/")
1026             errors = self.buildFindParameters()
1027             if errors:
1028                 self.ShowErrors(errors)
1029                 return
1030             self.status.delete_text(0,-1)
1031             status=""
1032             clist.clear()
1033             #All GtkClist operations seem to be O(n),
1034             #so doing the following for example will be O((n/2)*(n+1))
1035             #    for row in range(clist.rows):
1036             #        path = clist.get_row_data(row)
1037             #Therefore we use a python list to store row data.
1038             clist.set_data("row_data",[])
1039             if self.mode == self.mode_pkgs:
1040                 self.pkg_info.get_buffer().set_text("")
1041             self.enable_stop()
1042             self.stopflag=0
1043             while gtk.events_pending(): gtk.main_iteration(False)#update GUI
1044             clist.freeze()
1045             tstart=time.time()
1046             status, errors=self.__class__.find_dispatch[self.mode](self, clist)
1047             tend=time.time()
1048             if time_commands:
1049                 status += ". Found in %.3fs" % (tend-tstart)
1050         except self.UserAbort:
1051             status=_("User aborted")
1052         except:
1053             etype, emsg, etb = sys.exc_info()
1054             errors=str(etype)+': '+str(emsg)+'\n'
1055         clist.columns_autosize()
1056         clist.thaw()
1057         os.chdir(self.pwd)
1058         self.ShowErrors(errors)
1059         self.status.set_text(status)
1060         self.disable_stop()
1061
1062     def on_stop_clicked(self, event):
1063         self.disable_stop()
1064         self.stopflag=1
1065
1066     def on_stop_keypress(self, button, event):
1067         #ingore capslock and numlock
1068         state = event.state & ~(gtk.gdk.LOCK_MASK | gtk.gdk.MOD2_MASK)
1069         ksyms=gtk.keysyms
1070         abort_keys={
1071                     int(gtk.gdk.CONTROL_MASK):[ksyms.c,ksyms.C],
1072                     0                   :[ksyms.space,ksyms.Escape,ksyms.Return]
1073                    }
1074         try:
1075             if event.keyval in abort_keys[state]:
1076                 self.on_stop_clicked(event)
1077         except:
1078             pass
1079         return True
1080
1081     def on_saveAs_clicked(self, event):
1082         clist = self.clists[self.mode]
1083         if clist.rows == 0:
1084             return
1085         self.dlgPathSel.show(1)
1086         if not self.dlgPathSel.canceled:
1087             self.ClearErrors()
1088             fileSaveAs = self.dlgPathSel.GtkWindow.get_filename()
1089             try:
1090                 fileSaveAs = open(fileSaveAs, 'w')
1091             except:
1092                 etype, emsg, etb = sys.exc_info()
1093                 self.ShowErrors(str(emsg)+'\n')
1094                 return
1095             rows_to_save = clist.selection
1096             if self.mode == self.mode_pkgs:
1097                 for row in range(clist.rows):
1098                     if len(rows_to_save):
1099                         if row not in rows_to_save: continue
1100                     rowtext = ''
1101                     for col in (0,2): #ignore "human number" col
1102                         rowtext += clist.get_text(row,col) +'\t'
1103                     fileSaveAs.write(rowtext[:-1]+'\n')
1104             else:
1105                 row_data=clist.get_data("row_data")
1106                 if len(rows_to_save):
1107                     for row in range(clist.rows):
1108                         if clist.get_selectable(row):
1109                             if row not in rows_to_save: continue
1110                         else: continue #don't save group headers
1111                         fileSaveAs.write(row_data[row])
1112                 else:
1113                     fileSaveAs.writelines(row_data)
1114
1115     def on_unselect_using_wildcard_activate(self, event):
1116         self.select_using_wildcard(False)
1117     def on_select_using_wildcard_activate(self, event):
1118         self.select_using_wildcard(True)
1119     def select_using_wildcard(self, select):
1120         msgbox=dlgUserInteraction(liblocation+"/fslint.glade",
1121                                     "UserInteraction")
1122         msgbox.init(self, _("wildcard:"), (N_('Cancel'), N_('Ok')))
1123         msgbox.show()
1124         if msgbox.response != "Ok":
1125             return
1126         wildcard=msgbox.input
1127         if wildcard != "":
1128             clist = self.clists[self.mode]
1129             if clist.rows == 0:
1130                 return
1131             select_func=select and clist.select_row or clist.unselect_row
1132             import fnmatch
1133             if self.mode == self.mode_pkgs:
1134                 for row in range(clist.rows):
1135                     pkgname = clist.get_text(row,0)
1136                     if fnmatch.fnmatch(pkgname,wildcard):
1137                         select_func(row, 0)
1138             else:
1139                 row=0
1140                 for path in clist.get_data("row_data"):
1141                     if fnmatch.fnmatch(path,wildcard): #trailing \n ignored
1142                         select_func(row, 0)
1143                     row+=1
1144
1145     def on_select_all_but_first_in_each_group_activate(self, event):
1146         self.on_select_all_but_one_in_each_group_activate("first")
1147     def on_select_all_but_newest_in_each_group_activate(self, event):
1148         self.on_select_all_but_one_in_each_group_activate("newest")
1149     def on_select_all_but_oldest_in_each_group_activate(self, event):
1150         self.on_select_all_but_one_in_each_group_activate("oldest")
1151
1152     def on_select_all_but_one_in_each_group_activate(self, which):
1153
1154         def find_row_to_unselect(clist, row, which):
1155             import operator
1156             if which == "first":
1157                 if clist.get_selectable(row)==False:
1158                    return row+1
1159                 else:
1160                     return row #for first row in clist_sn
1161             elif which == "newest":
1162                 unselect_mtime=-1
1163                 comp=operator.gt
1164             elif which == "oldest":
1165                 unselect_mtime=2**32
1166                 comp=operator.lt
1167             if clist.get_selectable(row)==False: #not the case for first sn row
1168                 row=row+1
1169             while clist.get_selectable(row) == True and row < clist.rows:
1170                 mtime = clist.get_row_data(row)
1171                 if comp(mtime,unselect_mtime):
1172                     unselect_mtime = mtime
1173                     unselect_row=row
1174                 row=row+1
1175             return unselect_row
1176
1177         clist = self.clists[self.mode]
1178         clist.freeze()
1179         clist.select_all()
1180         for row in range(clist.rows):
1181             if row==0 or clist.get_selectable(row)==False: #New group
1182                 unselect_row = find_row_to_unselect(clist, row, which)
1183                 clist.unselect_row(unselect_row, 0)
1184         clist.thaw()
1185
1186     def on_unselect_all_activate(self, event):
1187         clist = self.clists[self.mode]
1188         clist.unselect_all()
1189
1190     def on_toggle_selection_activate(self, event):
1191         clist = self.clists[self.mode]
1192         clist.freeze()
1193         selected = clist.selection
1194         if len(selected) == 0:
1195             clist.select_all()
1196         elif len(selected) == clist.rows:
1197             clist.unselect_all()
1198         else:
1199             clist.select_all()
1200             for row in selected:
1201                 clist.unselect_row(row, 0)
1202         clist.thaw()
1203
1204     def on_selection_clicked(self, widget):
1205         self.on_selection_menu_button_press_event(self.selection, None)
1206
1207     def on_delSelected_clicked(self, event):
1208         if self.mode == self.mode_pkgs:
1209             if os.geteuid() != 0:
1210                 msgbox=dlgUserInteraction(liblocation+"/fslint.glade",
1211                                           "UserInteraction")
1212                 msgbox.init(
1213                   self,
1214                   _("Sorry, you must be root to delete system packages.")
1215                 )
1216                 msgbox.show()
1217                 return
1218         if not self.check_user(_("Are you sure you want to delete")):
1219             return
1220         clist = self.clists[self.mode]
1221         row_data = clist.get_data("row_data")
1222         paths_to_remove = clist.selection
1223         paths_to_remove.sort() #selection order irrelevant/problematic
1224         paths_to_remove.reverse() #so deleting works correctly
1225         numDeleted = 0
1226         if self.mode == self.mode_pkgs:
1227             pkgs_selected = []
1228             for row in paths_to_remove:
1229                 package = clist.get_row_data(row)
1230                 pkgs_selected.append(package)
1231             self.status.set_text(_("Calculating dependencies..."))
1232             while gtk.events_pending(): gtk.main_iteration(False)
1233             all_deps=self.whatRequires(pkgs_selected)
1234             if len(all_deps) > len(pkgs_selected):
1235                 num_new_pkgs = 0
1236                 for package in all_deps:
1237                     if package not in pkgs_selected:
1238                         num_new_pkgs += 1
1239                         #Note clist.find_row_from_data() only compares pointers
1240                         for row in range(clist.rows):
1241                             if package == clist.get_row_data(row):
1242                                 clist.select_row(row,0)
1243                 msgbox=dlgUserInteraction(liblocation+"/fslint.glade",
1244                                           "UserInteraction")
1245                 msgbox.init(
1246                   self,
1247                   _("%d extra packages need to be deleted.\n") % num_new_pkgs +
1248                   _("Please review the updated selection.")
1249                 )
1250                 msgbox.show()
1251                 #Note this is not ideal as it's difficult for users
1252                 #to get info on selected packages (Ctrl click twice).
1253                 #Should really have a seperate marked_for_deletion  column.
1254                 self.status.set_text("")
1255                 return
1256
1257             self.status.set_text(_("Removing packages..."))
1258             while gtk.events_pending(): gtk.main_iteration(False)
1259             if dist_type.rpm:
1260                 cmd="rpm -e "
1261             elif dist_type.deb:
1262                 cmd="dpkg --purge "
1263             cmd+=' '.join(pkgs_selected) + " >/dev/null 2>&1"
1264             os.system(cmd)
1265
1266         clist.freeze()
1267         for row in paths_to_remove:
1268             if self.mode != self.mode_pkgs:
1269                 try:
1270                     path=row_data[row][:-1] #strip trailing '\n'
1271                     if os.path.isdir(path):
1272                         os.rmdir(path)
1273                     else:
1274                         os.unlink(path)
1275                 except:
1276                     etype, emsg, etb = sys.exc_info()
1277                     self.ShowErrors(str(emsg)+'\n')
1278                     continue
1279                 row_data.pop(row)
1280             clist.remove(row)
1281             numDeleted += 1
1282
1283         #Remove any redundant grouping rows
1284         rowver=clist.rows
1285         rows_left = range(rowver)
1286         rows_left.reverse()
1287         for row in rows_left:
1288             if clist.get_selectable(row) == False:
1289                 if (row == clist.rows-1) or (clist.get_selectable(row+1) ==
1290                     False):
1291                     clist.remove(row)
1292                     if self.mode != self.mode_pkgs:
1293                         row_data.pop(row)
1294
1295         clist.columns_autosize()
1296         clist.thaw()
1297         status = str(numDeleted) + _(" items deleted")
1298         if self.mode == self.mode_pkgs:
1299            status += ". " + human_space_left('/')
1300         self.status.set_text(status)
1301
1302     def on_clist_pkgs_click_column(self, clist, col):
1303         self.clist_pkgs_order[col] = not self.clist_pkgs_order[col]
1304         if self.clist_pkgs_order[col]:
1305             clist.set_sort_type(gtk.SORT_ASCENDING)
1306         else:
1307             clist.set_sort_type(gtk.SORT_DESCENDING)
1308         try:
1309             #focus_row is not updated after ordering, so can't use
1310             #last_selected=clist.get_row_data(clist.focus_row)
1311             last_selected=self.clist_pkgs_last_selected
1312         except:
1313             last_selected=0
1314         if col==0:
1315             clist.set_sort_column(0)
1316         else:
1317             clist.set_sort_column(2)
1318         clist.sort()
1319         #could instead just use our "row_data" and
1320         #repopulate the gtkclist with something like:
1321         #row_data = clist.get_data("row_data")
1322         #row_data.sort(key = lambda x:x[col],
1323         #              reverse = not self.clist_pkgs_order[col])
1324         if last_selected:
1325             #Warning! As of pygtk2-2.4.0 at least
1326             #find_row_from_data only compares pointers, not strings!
1327             last_selected_row=clist.find_row_from_data(last_selected)
1328             clist.moveto(last_selected_row,0,0.5,0.0)
1329             #clist.set_focus_row(last_selected_row)
1330         else:
1331             clist.moveto(0,0,0.0,0.0)
1332
1333
1334     #Note the events and event ordering provided by the GtkClist
1335     #make it very hard to provide robust and performant handlers
1336     #for selected rows. These flags are an attempt to stop the
1337     #function below from being called too frequently.
1338     #Note most users will scoll the list with {Up,Down} rather
1339     #than Ctrl+{Up,Down}, but at worst this will result in slow scrolling.
1340     def on_clist_pkgs_button_press(self, list, event):
1341         if event.button == 3:
1342             self.on_selection_menu_button_press_event(list, event)
1343         else:
1344             self.clist_pkgs_user_input=True
1345     def on_clist_pkgs_key_press(self, *args):
1346         self.clist_pkgs_user_input=True
1347
1348     def on_clist_pkgs_selection_changed(self, clist, *args):
1349         self.pkg_info.get_buffer().set_text("")
1350         if not self.clist_pkgs_user_input:
1351             self.clist_pkgs_last_selected=False
1352             return
1353         self.clist_pkgs_user_input=False
1354         if len(clist.selection) == 0:
1355             return
1356         pkg=clist.get_row_data(clist.selection[-1])
1357         self.clist_pkgs_last_selected=pkg #for ordering later
1358         if dist_type.rpm:
1359             cmd = "rpm -q --queryformat '%{DESCRIPTION}' "
1360         elif dist_type.deb:
1361             cmd = "dpkg-query -W --showformat='${Description}' "
1362         cmd += pkg
1363         pkg_process = subProcess(cmd)
1364         pkg_process.read()
1365         lines=pkg_process.outdata
1366         self.pkg_info.get_buffer().set_text(I18N.utf8(lines,"\n"))
1367         del(pkg_process)
1368
1369     def on_autoMerge_clicked(self, event):
1370         self.ClearErrors()
1371         self.status.delete_text(0,-1)
1372         clist = self.clists[self.mode]
1373         if clist.rows < 3:
1374             return
1375
1376         question=_("Are you sure you want to merge ALL files?\n")
1377
1378         paths_to_leave = clist.selection
1379         if len(paths_to_leave):
1380             question+=_("(Ignoring those selected)\n")
1381
1382         msgbox = dlgUserInteraction(liblocation+"/fslint.glade",
1383                                     "UserInteraction")
1384         msgbox.init(self, question, (N_('Yes'), N_('No')))
1385         msgbox.show()
1386         if msgbox.response != "Yes":
1387             return
1388
1389         newGroup = 0
1390         row_data=clist.get_data("row_data")
1391         for row in range(clist.rows):
1392             if row in paths_to_leave:
1393                 continue
1394             if clist.get_selectable(row) == False: #new group
1395                 newGroup = 1
1396             else:
1397                 path = row_data[row][:-1] #strip '\n'
1398                 if newGroup:
1399                     keepfile = path
1400                     newGroup = 0
1401                 else:
1402                     dupfile = path
1403                     try:
1404                         os.unlink(dupfile)
1405                         try: #Don't do this for autodelete
1406                             os.link(keepfile,dupfile)
1407                         except OSError, value:
1408                             if value.errno == 18: #EXDEV
1409                                 os.symlink(os.path.realpath(keepfile),dupfile)
1410                             else:
1411                                 raise
1412                         clist.set_background(row, self.bg_colour)
1413                     except OSError:
1414                         self.ShowErrors(str(sys.exc_value)+'\n')
1415
1416     def on_autoClean_clicked(self, event):
1417         if not self.check_user(_("Are you sure you want to clean")):
1418             return
1419         clist = self.clists[self.mode]
1420         paths_to_clean = clist.selection
1421
1422         numCleaned = 0
1423         totalSaved = 0
1424         row_data=clist.get_data("row_data")
1425         for row in paths_to_clean:
1426             try:
1427                 path_to_clean = row_data[row][:-1] #strip '\n'
1428                 startlen = os.stat(path_to_clean)[stat.ST_SIZE]
1429                 if self.mode == self.mode_ns:
1430                     stripProcess = subProcess("strip "+path_to_clean)
1431                     stripProcess.read()
1432                     errors = stripProcess.errdata
1433                     del(stripProcess)
1434                     if len(errors):
1435                         raise '', errors[:-1]
1436                 elif self.mode == self.mode_rs:
1437                     file_to_clean = open(path_to_clean,'r')
1438                     lines = open(path_to_clean,'r').readlines()
1439                     file_to_clean.close()
1440                     file_to_clean = open(path_to_clean,'w')
1441                     lines = map(lambda s: s.rstrip()+'\n', lines)
1442                     file_to_clean.writelines(lines)
1443                     file_to_clean.close()
1444                 clist.set_background(row, self.bg_colour)
1445                 clist.unselect_row(row,0)
1446                 totalSaved += startlen - os.stat(path_to_clean)[stat.ST_SIZE]
1447                 numCleaned += 1
1448             except:
1449                 etype, emsg, etb = sys.exc_info()
1450                 self.ShowErrors(str(emsg)+'\n')
1451         status =  str(numCleaned) + _(" items cleaned (") + \
1452                   human_num(totalSaved) + _(" bytes saved)")
1453         self.status.set_text(status)
1454
1455 FSlint = fslint(liblocation+"/fslint.glade", "fslint")
1456
1457 gtk.main ()