"Fossies" - the Fresh Open Source Software Archive

Member "fslint-2.46/fslint-gui" (2 Feb 2017, 80293 Bytes) of package /linux/privat/fslint-2.46.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "fslint-gui": 2.44_vs_2.46.

    1 #!/usr/bin/env python2
    2 # vim:fileencoding=utf-8
    3 
    4 # Note both python and vim understand the above encoding declaration
    5 
    6 # Note using env above will cause the system to register
    7 # the name (in ps etc.) as "python" rather than fslint-gui
    8 # (because "python" is passed to exec by env).
    9 
   10 """
   11  FSlint - A utility to find File System lint.
   12  Copyright © 2000-2014 by Pádraig Brady <P@draigBrady.com>.
   13 
   14  This program is free software; you can redistribute it and/or modify
   15  it under the terms of the GNU General Public License as published by
   16  the Free Software Foundation; either version 2 of the License, or
   17  any later version.
   18 
   19  This program is distributed in the hope that it will be useful,
   20  but WITHOUT ANY WARRANTY; without even the implied warranty of
   21  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
   22  See the GNU General Public License for more details,
   23  which is available at www.gnu.org
   24 """
   25 
   26 import types, os, sys, pipes, time, stat, tempfile, errno
   27 
   28 import gettext
   29 import locale
   30 
   31 try:
   32     import gtk
   33 except RuntimeError:
   34     etype, emsg, etb = sys.exc_info()
   35     sys.stderr.write(str(emsg)+'\n')
   36     sys.exit(1)
   37 
   38 import gtk.glade
   39 
   40 time_commands=False #print sub commands timing on status line
   41 
   42 def puts(string=""): #print is problematic in python 3
   43     sys.stdout.write(str(string)+'\n')
   44 
   45 liblocation=os.path.dirname(os.path.abspath(sys.argv[0]))
   46 locale_base=liblocation+'/po/locale'
   47 try:
   48     import fslint
   49     if sys.argv[0][0] != '/':
   50         #If relative probably debugging so don't use system files if possible
   51         if not os.path.exists(liblocation+'/fslint'):
   52             liblocation = fslint.liblocation
   53             locale_base = None #sys default
   54     else:
   55         liblocation = fslint.liblocation
   56         locale_base = None #sys default
   57 except:
   58     pass
   59 
   60 class i18n:
   61 
   62     def __init__(self):
   63         self._init_translations()
   64         self._init_locale() #after translations in case they reset codeset
   65         self._init_preferred_encoding()
   66 
   67     def _init_translations(self):
   68         #gtk2 only understands utf-8 so convert to unicode
   69         #which will be automatically converted to utf-8 by pygtk
   70         gettext.install("fslint", locale_base, unicode=1)
   71         global N_ #translate string but not at point of definition
   72         def N_(string): return string
   73         gettext.bindtextdomain("fslint",locale_base)
   74         gettext.textdomain("fslint")
   75         #Note gettext does not set libc's domain as
   76         #libc and Python may have different message catalog
   77         #formats (e.g. on Solaris). So make explicit calls.
   78         locale.bindtextdomain("fslint",locale_base)
   79         locale.textdomain("fslint")
   80 
   81     def _init_locale(self):
   82         try:
   83             locale.setlocale(locale.LC_ALL,'')
   84         except:
   85             #gtk will print warning for a duff locale
   86             pass
   87 
   88     def _init_preferred_encoding(self):
   89         #Note that this encoding name is canonlicalised already
   90         self.preferred_encoding=locale.getpreferredencoding(do_setlocale=False)
   91         #filename encoding can be different from default encoding
   92         filename_encoding = os.environ.get("G_FILENAME_ENCODING")
   93         if filename_encoding:
   94             try:
   95                 #Try out the encoding to avoid exceptions later.
   96                 #For example on Suse 11.4 this is actually a list
   97                 #  @locale,UTF-8,$west_europe_legacy_encoding,CP1252
   98                 #which we don't support for the moment.
   99                 "".encode(filename_encoding)
  100             except:
  101                  filename_encoding = None
  102         if filename_encoding:
  103             filename_encoding=filename_encoding.lower()
  104             if filename_encoding == "utf8": filename_encoding="utf-8"
  105             self.filename_encoding=filename_encoding
  106         else:
  107             self.filename_encoding=self.preferred_encoding
  108 
  109     class unicode_displayable(unicode):
  110         """translate non displayable chars to an appropriate representation"""
  111         translate_table=range(0x2400,0x2420) #control chars
  112         #Since python 2.2 this will be a new style class as
  113         #we are subclassing the builtin (immutable) unicode type.
  114         #Therefore we can make a factory function with the following.
  115         def __new__(cls,string,exclude):
  116             if exclude:
  117                 curr_table=cls.translate_table[:] #copy
  118                 for char in exclude:
  119                     try:
  120                         curr_table[ord(char)]=ord(char)
  121                     except:
  122                         pass #won't be converted anyway
  123             else:
  124                 curr_table=cls.translate_table
  125             translated_string=unicode(string).translate(curr_table)
  126             return unicode.__new__(cls, translated_string)
  127 
  128     def displayable_utf8(self,orig,exclude="",path=False):
  129         """Convert to utf8, and also convert chars that are not
  130         easily displayable (control chars currently).
  131         If orig is not a valid utf8 string,
  132         then try to convert to utf8 using preferred encoding while
  133         also replacing undisplayable or invalid characters.
  134         Exclude contains control chars you don't want to translate."""
  135         if type(orig) == types.UnicodeType:
  136             uc=orig
  137         else:
  138             try:
  139                 uc=unicode(orig,"utf-8")
  140             except:
  141                 try:
  142                     # Note I don't use "replace" here as that
  143                     # replaces multiple bytes with a FFFD in utf8 locales
  144                     # This is silly as then you know it's not utf8 so
  145                     # one should try each character.
  146                     if path:
  147                         uc=unicode(orig,self.filename_encoding)
  148                     else:
  149                         uc=unicode(orig,self.preferred_encoding)
  150                 except:
  151                     uc=unicode(orig,"ascii","replace")
  152                     # Note I don't like the "replacement char" representation
  153                     # on fedora core 3 at least (bitstream-vera doesn't have it,
  154                     # and the nimbus fallback looks like a comma).
  155                     # It's even worse on redhat 9 where there is no
  156                     # representation at all. Note FreeSans does have a nice
  157                     # question mark representation, and dejavu also since 1.12.
  158                     # Alternatively I could: uc=unicode(orig,"latin-1") as that
  159                     # has a representation for each byte, but it's not general.
  160         uc=self.unicode_displayable(uc, exclude)
  161         return uc.encode("utf-8")
  162 
  163     def g_filename_from_utf8(self, string):
  164         if self.filename_encoding != "utf-8":
  165             uc=unicode(string,"utf-8")
  166             string=uc.encode(self.filename_encoding) #can raise exception
  167         return string
  168 
  169 I18N=i18n() #call first so everything is internationalized
  170 
  171 import getopt
  172 def Usage():
  173     puts(_("Usage: %s [OPTION] [PATHS]") % os.path.split(sys.argv[0])[1])
  174     puts(  "    --version       " + _("display version"))
  175     puts(  "    --help          " + _("display help"))
  176 
  177 try:
  178     lOpts, lArgs = getopt.getopt(sys.argv[1:], "", ["help","version"])
  179 
  180     if len(lArgs) == 0:
  181         lArgs = [os.getcwd()]
  182 
  183     if ("--help","") in lOpts:
  184         Usage()
  185         sys.exit(None)
  186 
  187     if ("--version","") in lOpts:
  188         puts("FSlint 2.46")
  189         sys.exit(None)
  190 except getopt.error, msg:
  191     puts(msg)
  192     puts()
  193     Usage()
  194     sys.exit(2)
  195 
  196 def human_num(num, divisor=1, power=""):
  197     num=float(num)
  198     if divisor == 1:
  199         return locale.format("%.f",num,1)
  200     elif divisor == 1000:
  201         powers=[" ","K","M","G","T","P"]
  202     elif divisor == 1024:
  203         powers=["  ","Ki","Mi","Gi","Ti","Pi"]
  204     else:
  205         raise ValueError("Invalid divisor")
  206     if not power: power=powers[0]
  207     while num >= 1000: #4 digits
  208         num /= divisor
  209         power=powers[powers.index(power)+1]
  210     if power.strip():
  211         num = locale.format("%6.1f",num,1)
  212     else:
  213         num = locale.format("%4.f",num,1)+'  '
  214     return "%s%s" % (num,power)
  215 
  216 class pathInfo:
  217     """Gets path info appropriate for display, i.e.
  218         (ls_colour, du, size, uid, gid, ls_date, mtime)"""
  219 
  220     def __init__(self, path):
  221         stat_val = os.lstat(path)
  222 
  223         date = time.ctime(stat_val.st_mtime)
  224         month_time = date[4:16]
  225         year = date[-5:]
  226         timediff = time.time()-stat_val.st_mtime
  227         if timediff > 15552000: #6months
  228             date = month_time[0:6] + year
  229         else:
  230             date = month_time
  231 
  232         mode = stat_val.st_mode
  233         if stat.S_ISREG(mode):
  234             colour = '' #default
  235             if mode & (stat.S_IXGRP|stat.S_IXUSR|stat.S_IXOTH):
  236                 colour = "#00C000"
  237         elif stat.S_ISDIR(mode):
  238             colour = "blue"
  239         elif stat.S_ISLNK(mode):
  240             colour = "cyan"
  241             if not os.path.exists(path):
  242                 colour = "#C00000"
  243         else:
  244             colour = "tan"
  245 
  246         self.ls_colour = colour
  247         self.du = stat_val.st_blocks*512
  248         self.size = stat_val.st_size
  249         self.uid = stat_val.st_uid
  250         self.gid = stat_val.st_gid
  251         self.ls_date = date
  252         self.mtime = stat_val.st_mtime
  253         self.inode = stat_val.st_ino
  254 
  255 class GladeWrapper:
  256     """
  257     Superclass for glade based applications. Just derive from this
  258     and your subclass should create methods whose names correspond to
  259     the signal handlers defined in the glade file. Any other attributes
  260     in your class will be safely ignored.
  261 
  262     This class will give you the ability to do:
  263         subclass_instance.GtkWindow.method(...)
  264         subclass_instance.widget_name...
  265     """
  266     def __init__(self, Filename, WindowName):
  267         #load glade file.
  268         self.widgets = gtk.glade.XML(Filename, WindowName, gettext.textdomain())
  269         self.GtkWindow = getattr(self, WindowName)
  270 
  271         instance_attributes = {}
  272         for attribute in dir(self.__class__):
  273             instance_attributes[attribute] = getattr(self, attribute)
  274         self.widgets.signal_autoconnect(instance_attributes)
  275 
  276     def __getattr__(self, attribute): #Called when no attribute in __dict__
  277         widget = self.widgets.get_widget(attribute)
  278         if widget is None:
  279             raise AttributeError("Widget [" + attribute + "] not found")
  280         self.__dict__[attribute] = widget #add reference to cache
  281         return widget
  282 
  283 # SubProcess Example:
  284 #
  285 #     process = subProcess("your shell command")
  286 #     process.read() #timeout is optional
  287 #     handle(process.outdata, process.errdata)
  288 #     del(process)
  289 import select, signal
  290 class subProcess:
  291     """Class representing a child process. It's like popen2.Popen3
  292        but there are three main differences.
  293        1. This makes the new child process group leader (using setpgrp())
  294           so that all children can be killed.
  295        2. The output function (read) is optionally non blocking returning in
  296           specified timeout if nothing is read, or as close to specified
  297           timeout as possible if data is read.
  298        3. The output from both stdout & stderr is read (into outdata and
  299           errdata). Reading from multiple outputs while not deadlocking
  300           is not trivial and is often done in a non robust manner."""
  301 
  302     def __init__(self, cmd, bufsize=8192):
  303         """The parameter 'cmd' is the shell command to execute in a
  304         sub-process. If the 'bufsize' parameter is specified, it
  305         specifies the size of the I/O buffers from the child process."""
  306         self.cleaned=False
  307         self.BUFSIZ=bufsize
  308         self.outr, self.outw = os.pipe()
  309         self.errr, self.errw = os.pipe()
  310         self.pid = os.fork()
  311         if self.pid == 0:
  312             self._child(cmd)
  313         os.close(self.outw) #parent doesn't write so close
  314         os.close(self.errw)
  315         # Note we could use self.stdout=fdopen(self.outr) here
  316         # to get a higher level file object like popen2.Popen3 uses.
  317         # This would have the advantages of auto handling the BUFSIZ
  318         # and closing the files when deleted. However it would mean
  319         # that it would block waiting for a full BUFSIZ unless we explicitly
  320         # set the files non blocking, and there would be extra uneeded
  321         # overhead like EOL conversion. So I think it's handier to use os.read()
  322         self.outdata = self.errdata = ''
  323         self._outeof = self._erreof = 0
  324 
  325     def _child(self, cmd):
  326         # Note exec doesn't reset SIG_IGN -> SIG_DFL
  327         # and python sets SIGPIPE etc. to SIG_IGN, so reset:
  328         # http://bugs.python.org/issue1652
  329         signals = ('SIGPIPE', 'SIGXFZ', 'SIGXFSZ')
  330         for sig in signals:
  331             if hasattr(signal, sig):
  332                 signal.signal(getattr(signal, sig), signal.SIG_DFL)
  333 
  334         # Note sh below doesn't setup a seperate group (job control)
  335         # for non interactive shells (hmm maybe -m option does?)
  336         os.setpgrp() #seperate group so we can kill it
  337         os.dup2(self.outw,1) #stdout to write side of pipe
  338         os.dup2(self.errw,2) #stderr to write side of pipe
  339         #stdout & stderr connected to pipe, so close all other files
  340         map(os.close,(self.outr,self.outw,self.errr,self.errw))
  341         try:
  342             cmd = ['/bin/sh', '-c', cmd]
  343             os.execvp(cmd[0], cmd)
  344         finally: #exit child on error
  345             os._exit(1)
  346 
  347     def read(self, timeout=None):
  348         """return 0 when finished
  349            else return 1 every timeout seconds
  350            data will be in outdata and errdata"""
  351         currtime=time.time()
  352         while 1:
  353             tocheck=[self.outr]*(not self._outeof)+ \
  354                     [self.errr]*(not self._erreof)
  355             ready = select.select(tocheck,[],[],timeout)
  356             if len(ready[0]) == 0: #no data timeout
  357                 return 1
  358             else:
  359                 if self.outr in ready[0]:
  360                     outchunk = os.read(self.outr,self.BUFSIZ)
  361                     if outchunk == '':
  362                         self._outeof = 1
  363                     self.outdata += outchunk
  364                 if self.errr in ready[0]:
  365                     errchunk = os.read(self.errr,self.BUFSIZ)
  366                     if errchunk == '':
  367                         self._erreof = 1
  368                     self.errdata += errchunk
  369                 if self._outeof and self._erreof:
  370                     return 0
  371                 elif timeout:
  372                     if (time.time()-currtime) > timeout:
  373                         return 1 #may be more data but time to go
  374 
  375     def kill(self):
  376         os.kill(-self.pid, signal.SIGTERM) #kill whole group
  377 
  378     def pause(self):
  379         os.kill(-self.pid, signal.SIGSTOP) #pause whole group
  380 
  381     def resume(self):
  382         os.kill(-self.pid, signal.SIGCONT) #resume whole group
  383 
  384     def cleanup(self):
  385         """Wait for and return the exit status of the child process."""
  386         self.cleaned=True
  387         os.close(self.outr)
  388         os.close(self.errr)
  389         pid, sts = os.waitpid(self.pid, 0)
  390         if pid == self.pid:
  391             self.sts = sts
  392         else:
  393             self.sts = None
  394         return self.sts
  395 
  396     def __del__(self):
  397         if not self.cleaned:
  398             self.cleanup()
  399 
  400 # Determine what type of distro we're on.
  401 class distroType:
  402     def __init__(self):
  403         types = ("rpm", "dpkg", "pacman")
  404         for dist in types:
  405             setattr(self, dist, False)
  406         if os.path.exists("/etc/redhat-release"):
  407             self.rpm = True
  408         elif os.path.exists("/etc/debian_version"):
  409             self.dpkg = True
  410         elif os.path.exists("/etc/arch-release"):
  411             self.pacman = True
  412         else:
  413             for dist in types:
  414                 cmd = dist + " --version >/dev/null 2>&1"
  415                 if os.WEXITSTATUS(os.system(cmd)) == 0:
  416                     setattr(self, dist, True)
  417                     break
  418 dist_type=distroType()
  419 
  420 def human_space_left(where):
  421     (device, total, used_b, avail, used_p, mount)=\
  422     os.popen("df -Ph %s | tail -n1" % where).read().split()
  423     translation_map={"percent":used_p, "disk":mount, "size":avail}
  424     return _("%(percent)s of %(disk)s is used leaving %(size)sB available") %\
  425              translation_map
  426 
  427 # Avoid using clist.get_selectable() which is O(n)
  428 def get_selectable(row, row_data):
  429     return row_data[row][0] != '#'
  430 
  431 class dlgUserInteraction(GladeWrapper):
  432     """
  433     Note input buttons tuple text should not be translated
  434     so that the stock buttons are used if possible. But the
  435     translations should be available, so use N_ for input
  436     buttons tuple text. Note the returned response is not
  437     translated either. Note also, that if you know a stock
  438     button is already available, then there's no point in
  439     translating the passed in string.
  440     """
  441     def init(self, app, message, buttons, entry_default,
  442              again=False, again_val=True):
  443         for text in buttons:
  444             try:
  445                 stock_text=getattr(gtk,"STOCK_"+text.upper())
  446                 button = gtk.Button(stock=stock_text)
  447             except:
  448                 button = gtk.Button(label=_(text))
  449             button.set_data("text", text)
  450             button.connect("clicked", self.button_clicked)
  451             self.GtkWindow.action_area.pack_start(button)
  452             button.show()
  453             button.set_flags(gtk.CAN_DEFAULT)
  454             button.grab_default() #last button is default
  455         self.app = app
  456         self.lblmsg.set_text(message)
  457         self.lblmsg.show()
  458         self.response=None
  459         if message.endswith(":"):
  460             if entry_default:
  461                 self.entry.set_text(entry_default)
  462             self.entry.show()
  463             self.input=None
  464         else:
  465             self.entry.hide()
  466         if again:
  467             self.again=again_val
  468             if type(again) == types.StringType:
  469                 self.chkAgain.set_label(again)
  470                 self.chkAgain.set_active(again_val)
  471             #Dialog seems to give focus to first available widget,
  472             #So undo the focus on the check box
  473             self.GtkWindow.action_area.get_children()[-1].grab_focus()
  474             self.chkAgain.show()
  475 
  476 
  477     def show(self):
  478         self.GtkWindow.set_transient_for(self.app.GtkWindow)#center on main win
  479         self.GtkWindow.show() #Note dialog is initially hidden in glade file
  480         if self.GtkWindow.modal:
  481             gtk.main() #synchronous call from parent
  482         #else: not supported
  483 
  484     #################
  485     # Signal handlers
  486     #################
  487 
  488     def quit(self, *args):
  489         if self.GtkWindow.modal:
  490             gtk.main_quit()
  491 
  492     def button_clicked(self, button):
  493         self.response = button.get_data("text")
  494         if self.entry.flags() & gtk.VISIBLE:
  495             self.input = self.entry.get_text()
  496         if self.chkAgain.flags() & gtk.VISIBLE:
  497             self.again = self.chkAgain.get_active()
  498         self.GtkWindow.destroy()
  499 
  500 class dlgPath(GladeWrapper):
  501 
  502     def init(self, app):
  503         self.app = app
  504         self.pwd = self.app.pwd
  505 
  506     def show(self, fileOps=False, filename=""):
  507         self.canceled = True
  508         self.fileOps = fileOps
  509         if self.fileOps:
  510             # Change to last folder as more likely to want
  511             # to save results there
  512             self.GtkWindow.set_action(gtk.FILE_CHOOSER_ACTION_SAVE)
  513             self.GtkWindow.set_current_folder(self.pwd)
  514             self.GtkWindow.set_current_name(filename)
  515         else:
  516             self.GtkWindow.set_action(gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
  517             # set_filename doesn't highlight dir after first call
  518             # so using select_filename for consistency. Also I think
  519             # select_filename is better than set_current_folder as
  520             # it allows one to more easily select items at the same level
  521             self.GtkWindow.select_filename(self.pwd)
  522         self.GtkWindow.set_transient_for(self.app.GtkWindow)#center on main win
  523         self.GtkWindow.show()
  524         if self.GtkWindow.modal:
  525             gtk.main() #synchronous call from parent
  526         #else: not supported
  527 
  528     #################
  529     # Signal handlers
  530     #################
  531 
  532     def quit(self, *args):
  533         self.GtkWindow.hide()
  534         if not self.canceled:
  535             if self.fileOps:
  536                 self.pwd = self.GtkWindow.get_current_folder()
  537             else:
  538                 user_path = self.GtkWindow.get_filename()
  539                 if os.path.exists(user_path):
  540                     self.pwd = user_path
  541         if self.GtkWindow.modal:
  542             gtk.main_quit()
  543         return True #Don't let window be destroyed
  544 
  545     def on_ok_clicked(self, *args):
  546         file = self.GtkWindow.get_filename()
  547         if not file:
  548             # This can happen for the fileOps case, or otherwise
  549             # if one clicks on the "recently used" group and
  550             # then clicks Ok without selecting anything.
  551             return True #Don't let window be destroyed
  552 
  553         if self.fileOps: #creating new item
  554             if os.path.isdir(file):
  555                 self.GtkWindow.set_current_folder(file)
  556                 return True #Don't let window be destroyed
  557             if os.path.exists(file):
  558                 if os.path.isfile(file):
  559                     (response,) = self.app.msgbox(
  560                       _("Do you want to overwrite?\n") + file,
  561                       ('Yes', 'No')
  562                     )
  563                     if response != "Yes":
  564                         return
  565                 else:
  566                     self.app.msgbox(_("You can't overwrite ") + file)
  567                     return
  568         self.canceled = False
  569         self.quit()
  570 
  571 class fslint(GladeWrapper):
  572 
  573     class UserAbort(Exception):
  574         pass
  575 
  576     def __init__(self, Filename, WindowName):
  577         os.close(0) #don't hang if any sub cmd tries to read from stdin
  578         self.pwd=os.path.realpath(os.curdir)
  579         GladeWrapper.__init__(self, Filename, WindowName)
  580         self.dlgPath = dlgPath(liblocation+"/fslint.glade", "PathChoose")
  581         self.dlgPath.init(self)
  582         self.confirm_delete = True
  583         self.confirm_delete_group = True
  584         self.confirm_clean = True
  585         #Just need to keep this tuple in sync with tabs
  586         (self.mode_up, self.mode_pkgs, self.mode_nl, self.mode_sn,
  587          self.mode_tf, self.mode_bl, self.mode_id, self.mode_ed,
  588          self.mode_ns, self.mode_rs) = range(10)
  589         self.clists = {
  590             self.mode_up:self.clist_dups,
  591             self.mode_pkgs:self.clist_pkgs,
  592             self.mode_nl:self.clist_nl,
  593             self.mode_sn:self.clist_sn,
  594             self.mode_tf:self.clist_tf,
  595             self.mode_bl:self.clist_bl,
  596             self.mode_id:self.clist_id,
  597             self.mode_ed:self.clist_ed,
  598             self.mode_ns:self.clist_ns,
  599             self.mode_rs:self.clist_rs
  600         }
  601         self.mode_descs = {
  602             self.mode_up:_("Files with the same content"),
  603             self.mode_pkgs:_("Installed packages ordered by disk usage"),
  604             self.mode_nl:_("Problematic filenames"),
  605             self.mode_sn:_("Possibly conflicting commands or filenames"),
  606             self.mode_tf:_("Possibly old temporary files"),
  607             self.mode_bl:_("Problematic symbolic links"),
  608             self.mode_id:_("Files with missing user IDs"),
  609             self.mode_ed:_("Directory branches with no files"),
  610             self.mode_ns:_("Executables still containing debugging info"),
  611             self.mode_rs:_("Erroneous whitespace in a text file")
  612         }
  613         for path in lArgs:
  614             if os.path.exists(path):
  615                 self.addDirs(self.ok_dirs, os.path.abspath(path))
  616             else:
  617                 self.ShowErrors(_("Invalid path [") + path + "]")
  618         for bad_dir in ['/lost+found','/dev','/proc','/sys','/tmp',
  619                         '*/.svn','*/CVS', '*/.git', '*/.hg', '*/.bzr']:
  620             self.addDirs(self.bad_dirs, bad_dir)
  621         self._alloc_mouse_cursors()
  622         self.mode=0
  623         self.status.set_text(self.mode_descs[self.mode])
  624         self.bg_colour=self.vpanes.get_style().bg[gtk.STATE_NORMAL]
  625         #make readonly GtkEntry and GtkTextView widgets obviously so,
  626         #by making them the same colour as the surrounding panes
  627         self.errors.modify_base(gtk.STATE_NORMAL,self.bg_colour)
  628         self.status.modify_base(gtk.STATE_NORMAL,self.bg_colour)
  629         self.pkg_info.modify_base(gtk.STATE_NORMAL,self.bg_colour)
  630         #Other GUI stuff that ideally [lib]glade should support
  631         self.clist_pkgs.set_column_visibility(2,0)
  632         self.clist_pkgs.set_column_visibility(3,0)
  633         self.clist_ns.set_column_justification(2,gtk.JUSTIFY_RIGHT)
  634 
  635     def _alloc_mouse_cursors(self):
  636         #The following is the cursor used by mozilla and themed by X/distros
  637         left_ptr_watch = \
  638         "\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x0c\x00\x00\x00"\
  639         "\x1c\x00\x00\x00\x3c\x00\x00\x00\x7c\x00\x00\x00\xfc\x00\x00\x00"\
  640         "\xfc\x01\x00\x00\xfc\x3b\x00\x00\x7c\x38\x00\x00\x6c\x54\x00\x00"\
  641         "\xc4\xdc\x00\x00\xc0\x44\x00\x00\x80\x39\x00\x00\x80\x39"+"\x00"*66
  642         try:
  643             pix = gtk.gdk.bitmap_create_from_data(None, left_ptr_watch, 32, 32)
  644             color = gtk.gdk.Color()
  645             self.LEFT_PTR_WATCH=gtk.gdk.Cursor(pix, pix, color, color, 2, 2)
  646             #Note mask is ignored when doing the hash
  647         except TypeError:
  648             #http://bugzilla.gnome.org/show_bug.cgi?id=103616 #older pygtks
  649             #http://bugzilla.gnome.org/show_bug.cgi?id=318874 #pygtk-2.8.[12]
  650             #Note pygtk-2.8.1 was released with ubuntu breezy :(
  651             self.LEFT_PTR_WATCH=None #default cursor
  652         self.SB_V_DOUBLE_ARROW=gtk.gdk.Cursor(gtk.gdk.SB_V_DOUBLE_ARROW)
  653         self.XTERM=gtk.gdk.Cursor(gtk.gdk.XTERM) #I beam
  654         self.WATCH=gtk.gdk.Cursor(gtk.gdk.WATCH) #hourglass
  655 
  656     def get_fslint(self, command, delim='\n'):
  657         self.fslintproc = subProcess(command)
  658         self.status.set_text(_("searching")+"...")
  659         while self.fslintproc.read(timeout=0.1):
  660             while gtk.events_pending(): gtk.main_iteration(False)
  661             if self.stopflag:
  662                 if self.paused:
  663                     self.fslintproc.resume()
  664                 self.fslintproc.kill()
  665                 break
  666             elif self.pauseflag:
  667                 self.pause_search()
  668         else:
  669             self.fslintproc.outdata=self.fslintproc.outdata.split(delim)[:-1]
  670             ret = (self.fslintproc.outdata, self.fslintproc.errdata)
  671         #we can't control internal processing at present so hide buttons
  672         self.pause.hide()
  673         self.resume.hide()
  674         self.stop.hide()
  675         self.status.set_text(_("processing..."))
  676         while gtk.events_pending(): gtk.main_iteration(False)
  677         del(self.fslintproc)
  678         if self.stopflag:
  679             raise self.UserAbort
  680         else:
  681             return ret
  682 
  683     def ShowErrors(self, lines):
  684         if len(lines) == 0:
  685             return
  686         end=self.errors.get_buffer().get_end_iter()
  687         self.errors.get_buffer().insert(end,I18N.displayable_utf8(lines,"\n"))
  688 
  689     def ClearErrors(self):
  690         self.errors.get_buffer().set_text("")
  691 
  692     def buildFindParameters(self):
  693         if self.mode == self.mode_sn:
  694             if self.chk_sn_path.get_active():
  695                 return ""
  696         elif self.mode == self.mode_ns:
  697             if self.chk_ns_path.get_active():
  698                 return ""
  699         elif self.mode == self.mode_pkgs:
  700             return ""
  701 
  702         if self.ok_dirs.rows == 0:
  703             return _("No search paths specified")
  704 
  705         search_dirs = ""
  706         for row in range(self.ok_dirs.rows):
  707             search_dirs += " " + pipes.quote(self.ok_dirs.get_row_data(row))
  708 
  709         exclude_dirs=""
  710         # One can't remove directories listed as empty if
  711         # they have .git/ dirs etc. so ignore the exluded paths here
  712         if self.mode != self.mode_ed:
  713             for row in range(self.bad_dirs.rows):
  714                 if exclude_dirs == "":
  715                     exclude_dirs = '\(' + ' -path '
  716                 else:
  717                     exclude_dirs += ' -o -path '
  718                 exclude_dirs += pipes.quote(self.bad_dirs.get_row_data(row))
  719         if exclude_dirs != "":
  720             exclude_dirs += ' \) -prune -o '
  721 
  722         if not self.recurseDirs.get_active():
  723             recurseParam=" -r "
  724         else:
  725             recurseParam=" "
  726 
  727         self.findParams = search_dirs + " -f " + recurseParam + exclude_dirs
  728 
  729         if self.mode == self.mode_up:
  730             min_size = self.min_size.get_text()
  731             if min_size == '0' or min_size == '':
  732                 min_size=''
  733             elif min_size[-1].isdigit():
  734                 min_size=str(int(min_size)-1)
  735                 min_size += 'c'
  736             if min_size:
  737                 self.findParams += '-size +' + min_size + ' '
  738 
  739         self.findParams += self.extra_find_params.get_text()
  740 
  741         return ""
  742 
  743     def addDirs(self, dirlist, dirs):
  744         """Adds items to passed clist."""
  745         dirlist.append([I18N.displayable_utf8(dirs,path=True)])
  746         dirlist.set_row_data(dirlist.rows-1, dirs)
  747 
  748     def removeDirs(self, dirlist):
  749         """Removes selected items from passed clist.
  750            If no items selected then list is cleared."""
  751         paths_to_remove = dirlist.selection
  752         if len(paths_to_remove) == 0:
  753             dirlist.clear()
  754         else:
  755             paths_to_remove.sort() #selection order irrelevant/problematic
  756             paths_to_remove.reverse() #so deleting works correctly
  757             for row in paths_to_remove:
  758                 dirlist.remove(row)
  759             dirlist.select_row(dirlist.focus_row,0)
  760 
  761     def clist_alloc_colour(self, clist, colour):
  762         """cache colour allocation for clist
  763            as colour will probably be used multiple times"""
  764         cmap = clist.get_colormap()
  765         cmap_cache=clist.get_data("cmap_cache")
  766         if cmap_cache == None:
  767             cmap_cache={'':None}
  768         if colour not in cmap_cache:
  769             cmap_cache[colour]=cmap.alloc_color(colour)
  770         clist.set_data("cmap_cache",cmap_cache)
  771         return cmap_cache[colour]
  772 
  773     def clist_append_path(self, clist, path, colour, *rest):
  774         """append path to clist, handling utf8 and colour issues"""
  775         colour = self.clist_alloc_colour(clist, colour)
  776         utf8_path=I18N.displayable_utf8(path,path=True)
  777         (dir,file)=os.path.split(utf8_path)
  778         clist.append((file,dir)+rest)
  779         row_data = clist.get_data("row_data")
  780         row_data.append(path+'\n') #add '\n' so can write with writelines
  781         if colour:
  782             clist.set_foreground(clist.rows-1,colour)
  783 
  784     def clist_append_group_row(self, clist, cols):
  785         """append header to clist"""
  786         clist.append(cols)
  787         row_data = clist.get_data("row_data")
  788         row_data.append('#'+"\t".join(cols).rstrip()+'\n')
  789         clist.set_background(clist.rows-1,self.bg_colour)
  790         clist.set_selectable(clist.rows-1,0)
  791 
  792     def clist_remove_handled_groups(self, clist):
  793         row_data = clist.get_data("row_data")
  794         rowver=clist.rows
  795         rows_left = range(rowver)
  796         rows_left.reverse()
  797         for row in rows_left:
  798             if not get_selectable(row, row_data):
  799                 if row == clist.rows-1 or not get_selectable(row+1, row_data):
  800                     clist.remove(row)
  801                     row_data.pop(row)
  802             else: #remove single item groups
  803                 if row == clist.rows-1 and not get_selectable(row-1, row_data):
  804                     clist.remove(row)
  805                     row_data.pop(row)
  806                 elif not ((row!=0 and get_selectable(row-1, row_data)) or
  807                           (row!=clist.rows-1 and get_selectable(row+1, row_data))):
  808                     clist.remove(row)
  809                     row_data.pop(row)
  810 
  811     def whatRequires(self, packages, level=0):
  812         if not packages: return
  813         if level==0: self.checked_pkgs={}
  814         for package in packages:
  815             #puts("\t"*level+package)
  816             self.checked_pkgs[package]=''
  817         if dist_type.rpm:
  818             cmd  = r"rpm -e --test " + ' '.join(packages) + r" 2>&1 | "
  819             cmd += r"sed -n 's/-[0-9]\{1,\}:/-/; " #strip epoch
  820             cmd += r"        s/.*is needed by (installed) \(.*\)/\1/p' | "
  821             cmd += r"LANG=C sort | uniq"
  822         elif dist_type.dpkg:
  823             cmd  = r"dpkg --purge --dry-run " + ' '.join(packages) + r" 2>&1 | "
  824             cmd += r"sed -n 's/ \(.*\) depends on.*/\1/p' | "
  825             cmd += r"LANG=C sort | uniq"
  826         elif dist_type.pacman:
  827             cmd  = r"LC_ALL=C pacman -Qi " + ' '.join(packages)
  828             cmd += r" 2>/dev/null | tr '\n' ' ' | "
  829             cmd += r"sed -e 's/Name .* Required By \{1,\}: \{1,\}//' -e "
  830             cmd += r"'s/Conflicts With .*//' -e 's/ \{18\}//g' -e "
  831             cmd += r"'s/ \{1,\}$/\n/'"
  832         else:
  833             raise RuntimeError("unknown distro")
  834         process = os.popen(cmd)
  835         requires = process.read()
  836         del(process)
  837         new_packages = [p for p in requires.split()
  838                         if p not in self.checked_pkgs]
  839         self.whatRequires(new_packages, level+1)
  840         if level==0: return self.checked_pkgs.keys()
  841 
  842     def findpkgs(self, clist_pkgs):
  843         self.clist_pkgs_order=[False,False] #unordered, descending
  844         self.clist_pkgs_user_input=False
  845         self.pkg_info.get_buffer().set_text("")
  846         if dist_type.dpkg:
  847             #Package names unique on debian
  848             cmd  = r"dpkg-query -W --showformat='${Package}\t${Installed-Size}"
  849             cmd += r"\t${Status}\n' | LANG=C grep -F 'installed' | cut -f1,2 | "
  850             cmd += r"sed 's/[^0-9]$/\t0/'" #packages with missing size = 0
  851         elif dist_type.rpm:
  852             #Must include version names to uniquefy on redhat
  853             cmd  = r"rpm -qa --queryformat '%{N}-%{V}-%{R}.%{ARCH}\t%{SIZE}\n'"
  854         elif dist_type.pacman:
  855             cmd  = r"pacman -Q | sed -n 's/^\([^ ]\{1,\}\) .*/\1/p' | "
  856             cmd += r"LC_ALL=C xargs pacman -Qi | sed -n -e "
  857             cmd += r"'s/Installed Size : \([^ ]*\) \([^ ]*\)B/\1\2/p' | "
  858             cmd += r"numfmt --from=auto"
  859         else:
  860             return ("", _("Sorry, FSlint does not support this functionality \
  861 on your system at present."))
  862         po, pe = self.get_fslint(cmd + " | LANG=C sort -k2,2rn")
  863         pkg_tot = 0
  864         row = 0
  865         for pkg_info in po:
  866             pkg_name, pkg_size= pkg_info.split()
  867             pkg_size = int(float(pkg_size))
  868             if dist_type.dpkg:
  869                 pkg_size *= 1024
  870             pkg_tot += pkg_size
  871             clist_pkgs.append([pkg_name,human_num(pkg_size,1000).strip(),
  872                                "%010ld"%pkg_size,"0"])
  873             clist_pkgs.set_row_data(row, pkg_name)
  874             row += 1
  875 
  876         return (str(row) + _(" packages, ") +
  877                 _("consuming %sB. ") % human_num(pkg_tot,1000).strip() +
  878                 _("Note %s.") % human_space_left('/'), pe)
  879         #Note pkgs generally installed on root partition so report space left.
  880 
  881     def findrs(self, clist_rs):
  882         options=""
  883         if self.chkWhitespace.get_active():
  884             options += "-w "
  885         if self.chkTabs.get_active():
  886             options += "-t%d " % int(self.spinTabs.get_value())
  887         if not options:
  888             return ("","")
  889         po, pe = self.get_fslint("./findrs " + options + self.findParams)
  890         row = 0
  891         for line in po:
  892             pi = pathInfo(line)
  893             self.clist_append_path(clist_rs, line,
  894                                    pi.ls_colour, str(pi.size), pi.ls_date)
  895             row += 1
  896 
  897         return (str(row) + _(" files"), pe)
  898 
  899     def findns(self, clist_ns):
  900         cmd = "./findns "
  901         if not self.chk_ns_path.get_active():
  902             cmd += self.findParams
  903         po, pe = self.get_fslint(cmd)
  904 
  905         unstripped=[]
  906         for line in po:
  907             pi = pathInfo(line)
  908             unstripped.append((pi.size, line, pi.ls_date))
  909         unstripped.sort()
  910         unstripped.reverse()
  911 
  912         row = 0
  913         for size, path, ls_date in unstripped:
  914             self.clist_append_path(clist_ns, path, '', human_num(size), ls_date)
  915             row += 1
  916 
  917         return (str(row) + _(" unstripped binaries"), pe)
  918 
  919     def finded(self, clist_ed):
  920         po, pe = self.get_fslint("./finded " + self.findParams, '\0')
  921         row = 0
  922         for line in po:
  923             self.clist_append_path(clist_ed, line, '', pathInfo(line).ls_date)
  924             row += 1
  925 
  926         return (str(row) + _(" empty directories"), pe)
  927 
  928     def findid(self, clist_id):
  929         po, pe = self.get_fslint("./findid " + self.findParams, '\0')
  930         row = 0
  931         for record in po:
  932             pi = pathInfo(record)
  933             self.clist_append_path(clist_id, record, pi.ls_colour, str(pi.uid),
  934                                    str(pi.gid), str(pi.size), pi.ls_date)
  935             row += 1
  936 
  937         return (str(row) + _(" files"), pe)
  938 
  939     def findbl(self, clist_bl):
  940         cmd = "./findbl " + self.findParams
  941         if self.opt_bl_dangling.get_active():
  942             cmd += " -d"
  943         elif self.opt_bl_suspect.get_active():
  944             cmd += " -s"
  945         elif self.opt_bl_relative.get_active():
  946             cmd += " -l"
  947         elif self.opt_bl_absolute.get_active():
  948             cmd += " -A"
  949         elif self.opt_bl_redundant.get_active():
  950             cmd += " -n"
  951         po, pe = self.get_fslint(cmd)
  952         row = 0
  953         for line in po:
  954             link, target = line.split(" -> ", 2)
  955             self.clist_append_path(clist_bl, link, '', target)
  956             row += 1
  957 
  958         return (str(row) + _(" links"), pe)
  959 
  960     def findtf(self, clist_tf):
  961         cmd = "./findtf " + self.findParams
  962         cmd += " --age=" + str(int(self.spin_tf_core.get_value()))
  963         if self.chk_tf_core.get_active():
  964             cmd += " -c"
  965         po, pe = self.get_fslint(cmd)
  966         row = 0
  967         byteWaste = 0
  968         for line in po:
  969             pi = pathInfo(line)
  970             self.clist_append_path(clist_tf, line, '', str(pi.size), pi.ls_date)
  971             byteWaste += pi.du
  972             row += 1
  973 
  974         return (human_num(byteWaste) + _(" bytes wasted in ") +
  975                 str(row) + _(" files"), pe)
  976 
  977     def findsn(self, clist_sn):
  978         cmd = "./findsn "
  979         if self.chk_sn_path.get_active():
  980             option = self.opt_sn_path.get_children()[0].get()
  981             if option == _("Aliases"):
  982                 cmd += " -A"
  983             elif option == _("Conflicting files"):
  984                 pass #default mode
  985             else:
  986                 raise RuntimeError("glade GtkOptionMenu item not found")
  987         else:
  988             cmd += self.findParams
  989             option = self.opt_sn_paths.get_children()[0].get()
  990             if option == _("Aliases"):
  991                 cmd += " -A"
  992             elif option == _("Same names"):
  993                 pass #default mode
  994             elif option == _("Same names(ignore case)"):
  995                 cmd += " -C"
  996             elif option == _("Case conflicts"):
  997                 cmd += " -c"
  998             else:
  999                 raise RuntimeError("glade GtkOptionMenu item not found")
 1000 
 1001         po, pe = self.get_fslint(cmd,'\0')
 1002         po = po[1:]
 1003         row=0
 1004         findsn_number=0
 1005         findsn_groups=0
 1006         for record in po:
 1007             if record == '':
 1008                 self.clist_append_group_row(clist_sn, ['','',''])
 1009                 findsn_groups += 1
 1010             else:
 1011                 pi = pathInfo(record)
 1012                 self.clist_append_path(clist_sn, record,
 1013                                        pi.ls_colour, str(pi.size))
 1014                 clist_sn.set_row_data(clist_sn.rows-1, pi.mtime)
 1015                 findsn_number += 1
 1016             row += 1
 1017         if findsn_number:
 1018             findsn_groups += 1 #for stripped group head above
 1019 
 1020         return (str(findsn_number) + _(" files (in ") + str(findsn_groups) +
 1021                 _(" groups)"), pe)
 1022 
 1023     def findnl(self, clist_nl):
 1024         if self.chk_findu8.get_active():
 1025             po, pe = self.get_fslint("./findu8 " + self.findParams, '\0')
 1026         else:
 1027             sensitivity = ('1','2','3','p')[
 1028             int(self.hscale_findnl_level.get_adjustment().value)-1]
 1029             po, pe = self.get_fslint("./findnl " + self.findParams + " -" +
 1030                                     sensitivity, '\0')
 1031 
 1032         row=0
 1033         for record in po:
 1034             self.clist_append_path(clist_nl, record, pathInfo(record).ls_colour)
 1035             row += 1
 1036 
 1037         return (str(row) + _(" files"), pe)
 1038 
 1039     def findup(self, clist_dups):
 1040         po, pe = self.get_fslint("./findup --gui" + self.findParams)
 1041 
 1042         numdups = 0
 1043         du = size = 0
 1044         dups = []
 1045         alldups = []
 1046         inodes = {}
 1047         #inodes required to correctly report disk usage of
 1048         #duplicate files with seperate inode groups.
 1049         for line in po:
 1050             if line == '': #grouped == 1
 1051                 if len(inodes)>1:
 1052                     alldups.append(((numdups-1)*du, numdups, size, dups))
 1053                 dups = []
 1054                 numdups = 0
 1055                 inodes = {}
 1056             else:
 1057                 try:
 1058                     pi = pathInfo(line)
 1059                     size, du = (pi.size, pi.du)
 1060                     dups.append((line, pi.ls_date, pi.mtime))
 1061                     if pi.inode not in inodes:
 1062                         inodes[pi.inode] = True
 1063                         numdups += 1
 1064                 except OSError:
 1065                     #file may have been deleted, changed permissions, ...
 1066                     self.ShowErrors(str(sys.exc_info()[1])+'\n')
 1067         else:
 1068             if len(inodes)>1:
 1069                 alldups.append(((numdups-1)*du, numdups, size, dups))
 1070 
 1071         alldups.sort()
 1072         alldups.reverse()
 1073 
 1074         byteWaste = 0
 1075         numWaste = 0
 1076         row=0
 1077         for groupWaste, groupSize, size, files in alldups:
 1078             byteWaste += groupWaste
 1079             numWaste += groupSize - 1
 1080             groupHeader = ["%d x %s" % (groupSize, human_num(size)),
 1081                            "(%s)" % human_num(groupWaste),
 1082                            _("bytes wasted")]
 1083             self.clist_append_group_row(clist_dups, groupHeader)
 1084             row += 1
 1085             for file in files:
 1086                 self.clist_append_path(clist_dups,file[0],'',file[1])
 1087                 clist_dups.set_row_data(row,file[2]) #mtime
 1088                 row += 1
 1089 
 1090         return (human_num(byteWaste) + _(" bytes wasted in ") +
 1091                 str(numWaste) + _(" files (in ") + str(len(alldups)) +
 1092                 _(" groups)"), pe)
 1093 
 1094     find_dispatch = (findup,findpkgs,findnl,findsn,findtf,
 1095                      findbl,findid,finded,findns,findrs) #order NB
 1096 
 1097     def set_cursor(self, requested_cursor):
 1098         #TODO: horizontal arrows for GtkClist column resize handles not set
 1099         cursor=requested_cursor
 1100         if not self.GtkWindow.window: return #window closed
 1101         self.GtkWindow.window.set_cursor(cursor)
 1102         if requested_cursor==None: cursor=self.XTERM #I beam
 1103         for gtkentry in (self.status, self.extra_find_params):
 1104             if not gtkentry.window: continue #not realised yet
 1105             gtkentry.window.set_cursor(cursor)
 1106             gtkentry.window.get_children()[0].set_cursor(cursor)
 1107         for gtkspinbutton in (self.spin_tf_core, self.spinTabs):
 1108             if not gtkspinbutton.window: continue #not realised yet
 1109             gtkspinbutton.window.set_cursor(cursor)
 1110             gtkspinbutton.window.get_children()[0].set_cursor(cursor)
 1111         for gtktextview in (self.errors,):
 1112             if not gtktextview.window: continue #not realised yet
 1113             gtktextview.window.set_cursor(cursor)
 1114             gtktextview.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(cursor)
 1115         if requested_cursor==None: cursor=self.SB_V_DOUBLE_ARROW
 1116         for gtkpaned in (self.vpanes,):
 1117             if not gtkpaned.window: continue #not realised yet
 1118             try:
 1119                 handle_size=gtkpaned.style_get_property("handle-size")#pygtk-2.4
 1120             except:
 1121                 break
 1122             for window in gtkpaned.window.get_children():
 1123                 width,height=window.get_size()
 1124                 if width==handle_size or height==handle_size:
 1125                     window.set_cursor(cursor)
 1126         while gtk.events_pending(): gtk.main_iteration(False)
 1127 
 1128     def look_busy(self):
 1129         self.find.hide()
 1130         self.resume.hide()
 1131         self.stop.show()
 1132         self.pause.show()
 1133         self.stop.grab_focus()
 1134         self.control_buttons.grab_add() #restrict mouse and key presses to here
 1135         self.set_cursor(self.LEFT_PTR_WATCH)
 1136 
 1137     def look_interactive(self):
 1138         self.control_buttons.grab_remove()
 1139         self.stop.hide()
 1140         self.resume.hide()
 1141         self.pause.hide()
 1142         self.find.show()
 1143         self.find.grab_focus() #as it already would have been in focus
 1144         self.set_cursor(None)
 1145 
 1146     def msgbox(self, message, buttons=('Ok',), entry_default=None,
 1147                again=False, again_val=True):
 1148         msgbox=dlgUserInteraction(liblocation+"/fslint.glade","UserInteraction")
 1149         msgbox.init(self, message, buttons, entry_default, again, again_val)
 1150         msgbox.show()
 1151         response = (msgbox.response,)
 1152         if hasattr(msgbox, "input"):
 1153             response += (msgbox.input,)
 1154         if hasattr(msgbox, "again"):
 1155             response += (msgbox.again,)
 1156         return response
 1157 
 1158     #Return tuple of booleans meaning: (user_ok_with_action, ask_again)
 1159     def check_user(self, question):
 1160         self.ClearErrors()
 1161         #remove as status from previous operation could be confusing
 1162         self.status.delete_text(0,-1)
 1163         clist = self.clists[self.mode]
 1164         if not clist.rows:
 1165             return False, True #Be consistent
 1166                          #All actions buttons do nothing if no results
 1167         paths = clist.selection
 1168         if len(paths) == 0:
 1169             self.ShowErrors(_("None selected"))
 1170             return False, True
 1171         else:
 1172             if len(paths) == 1:
 1173                 question += _(" this item?\n")
 1174             else:
 1175                 question += _(" these %d items?\n") % len(paths)
 1176             (response, again) = self.msgbox(question, ('Yes', 'No'), again=True)
 1177             if response != "Yes":
 1178                 return (False, again)
 1179             return (True, again)
 1180 
 1181     #################
 1182     # Signal handlers
 1183     #################
 1184 
 1185     def on_fslint_destroy(self, event):
 1186         try:
 1187             if self.paused:
 1188                 self.fslintproc.resume()
 1189             self.fslintproc.kill()
 1190             del(self.fslintproc)
 1191         except:
 1192             pass #fslint wasn't searching
 1193         gtk.main_quit()
 1194 
 1195     def on_addOkDir_clicked(self, event):
 1196         self.dlgPath.show()
 1197         if not self.dlgPath.canceled:
 1198             path = self.dlgPath.GtkWindow.get_filename()
 1199             path = os.path.normpath(path)
 1200             self.addDirs(self.ok_dirs, path)
 1201 
 1202     def on_addBadDir_clicked(self, event):
 1203         self.dlgPath.show()
 1204         if not self.dlgPath.canceled:
 1205             path = self.dlgPath.GtkWindow.get_filename()
 1206             path = os.path.normpath(path)
 1207             #Note find -path doesn't match dirs with trailing /
 1208             #and normpath strips trailing / among other things
 1209             #XXX: The following is not robust. We really should
 1210             #split specifying wildcards and paths
 1211             if not os.path.exists(path): #probably a wildcard?
 1212                 #hacky way to get just user specified component
 1213                 dirs = path.split('/')
 1214                 if len(dirs) > 2:
 1215                     for i in range(len(dirs)):
 1216                         path = '/'.join(dirs[:i+1])
 1217                         if path and not os.path.exists(path):
 1218                             path = '/'.join(dirs[i:])
 1219                             break
 1220                 if '/' not in path:
 1221                     path = "*/" + path #more accurate
 1222             self.addDirs(self.bad_dirs, path)
 1223 
 1224     def on_removeOkDir_clicked(self, event):
 1225         self.removeDirs(self.ok_dirs)
 1226 
 1227     def on_removeBadDir_clicked(self, event):
 1228         self.removeDirs(self.bad_dirs)
 1229 
 1230     def on_chk_sn_path_toggled(self, event):
 1231         if self.chk_sn_path.get_active():
 1232             self.hbox_sn_paths.hide()
 1233             self.hbox_sn_path.show()
 1234         else:
 1235             self.hbox_sn_path.hide()
 1236             self.hbox_sn_paths.show()
 1237 
 1238     def on_chk_findu8_toggled(self, event):
 1239         if self.chk_findu8.get_active():
 1240             self.hscale_findnl_level.hide()
 1241             self.lbl_findnl_sensitivity.hide()
 1242         else:
 1243             self.hscale_findnl_level.show()
 1244             self.lbl_findnl_sensitivity.show()
 1245 
 1246     def on_fslint_functions_switch_page(self, widget, dummy, pagenum):
 1247         self.ClearErrors()
 1248         self.mode=pagenum
 1249         self.status.set_text(self.mode_descs[self.mode])
 1250         if self.mode == self.mode_up:
 1251             self.autoMerge.show()
 1252             self.autoSymlink.show()
 1253         else:
 1254             self.autoMerge.hide()
 1255             self.autoSymlink.hide()
 1256         if self.mode == self.mode_ns or self.mode == self.mode_rs: #bl in future
 1257             self.autoClean.show()
 1258         else:
 1259             self.autoClean.hide()
 1260 
 1261     def on_selection_menu_button_press_event(self, widget, event):
 1262         if self.mode == self.mode_up or self.mode == self.mode_sn:
 1263             self.groups_menu.show()
 1264         else:
 1265             self.groups_menu.hide()
 1266         if event == None: #standard button click
 1267             self.selection_menu_menu.popup(None,None,None,0,0) #at mouse pointer
 1268         elif type(widget) == gtk.CList and self.mode != self.mode_pkgs:
 1269             if event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS:
 1270                 #Notes:
 1271                 # This clause is not related to selection menu, but this
 1272                 # is the button_press handler for all lists so leaving here.
 1273                 # widget.selection can be empty due to doing this in
 1274                 # button press rather than button release I think.
 1275                 # Ignoring widget.selection allows us to ctrl-doubleclick
 1276                 # to open additional items being added to a selection.
 1277                 sel=widget.get_selection_info(int(event.x),int(event.y))
 1278                 if sel:
 1279                     row,col=sel
 1280                     if widget.get_selectable(row)==True:
 1281                         self.on_open_activate(event,row)
 1282             elif event.button == 3:
 1283                 menu=self.selection_menu_menu
 1284                 sel=widget.get_selection_info(int(event.x),int(event.y))
 1285                 if sel:
 1286                     row,col=sel
 1287                     selected = widget.selection
 1288                     if len(selected)==1 and row in selected:
 1289                         menu=self.edit_menu_menu
 1290                 menu.popup(None,None,None,event.button,event.time)
 1291 
 1292     def on_find_clicked(self, event):
 1293 
 1294         try:
 1295             status=""
 1296             errors=""
 1297             self.ClearErrors()
 1298             clist = self.clists[self.mode]
 1299             os.chdir(liblocation+"/fslint/")
 1300             errors = self.buildFindParameters()
 1301             if errors:
 1302                 self.ShowErrors(errors)
 1303                 return
 1304             self.status.delete_text(0,-1)
 1305             clist.clear()
 1306             #All GtkClist operations seem to be O(n),
 1307             #so doing the following for example will be O((n/2)*(n+1))
 1308             #    for row in xrange(clist.rows):
 1309             #        path = clist.get_row_data(row)
 1310             #Therefore we use a python list to store row data.
 1311             clist.set_data("row_data",[])
 1312             if self.mode == self.mode_pkgs:
 1313                 self.pkg_info.get_buffer().set_text("")
 1314             self.look_busy()
 1315             self.stopflag=self.pauseflag=False
 1316             self.paused = False
 1317             while gtk.events_pending(): gtk.main_iteration(False)#update GUI
 1318             clist.freeze()
 1319             tstart=time.time()
 1320             status, errors=self.__class__.find_dispatch[self.mode](self, clist)
 1321             tend=time.time()
 1322             if time_commands:
 1323                 status += ". Found in %.3fs" % (tend-tstart)
 1324         except self.UserAbort:
 1325             status=_("User aborted")
 1326         except:
 1327             etype, emsg, etb = sys.exc_info()
 1328             errors=str(etype)+': '+str(emsg)+'\n'
 1329         clist.columns_autosize()
 1330         clist.thaw()
 1331         os.chdir(self.pwd)
 1332         self.ShowErrors(errors)
 1333         self.status.set_text(status)
 1334         self.look_interactive()
 1335 
 1336     def on_stop_clicked(self, event):
 1337         self.stopflag=True
 1338 
 1339     def on_pause_clicked(self, event):
 1340         # self.fslintproc may not be created yet,
 1341         # so set the flag and pause in get_fslint()
 1342         self.pauseflag=True
 1343 
 1344     def pause_search(self):
 1345         self.pauseflag=False
 1346         self.paused = not self.paused
 1347         if self.paused:
 1348             self.pause.hide()
 1349             self.resume.show()
 1350             self.resume.grab_focus()
 1351             self.fslintproc.pause()
 1352             self.status.set_text(_("paused"))
 1353         else:
 1354             self.resume.hide()
 1355             self.pause.show()
 1356             self.pause.grab_focus()
 1357             self.fslintproc.resume()
 1358             #We can only pause external searching
 1359             self.status.set_text(_("searching")+"...")
 1360         while gtk.events_pending(): gtk.main_iteration(False)
 1361 
 1362     def on_control_buttons_keypress(self, button, event):
 1363         #ingore capslock and numlock
 1364         state = event.state & ~(gtk.gdk.LOCK_MASK | gtk.gdk.MOD2_MASK)
 1365         ksyms=gtk.keysyms
 1366         abort_keys={
 1367                     int(gtk.gdk.CONTROL_MASK):[ksyms.c,ksyms.C],
 1368                     0                        :[ksyms.Escape]
 1369                    }
 1370         try:
 1371             if event.keyval in abort_keys[state]:
 1372                 self.on_stop_clicked(event)
 1373         except:
 1374             pass
 1375         if event.keyval in (ksyms.Tab, ksyms.Left, ksyms.Right):
 1376             # Unfortunately GTK doesn't restrict widgets
 1377             # to focus to children of the current grab_add widget.
 1378             # So we do it manually here.
 1379             if self.stop.is_focus():
 1380                 if self.paused:
 1381                     self.resume.grab_focus()
 1382                 else:
 1383                     self.pause.grab_focus()
 1384             else:
 1385                 self.stop.grab_focus()
 1386             return True #Don't allow moving to other controls
 1387         elif event.keyval in (ksyms.Return, ksyms.space):
 1388             return False #pass event on to sub button
 1389         else:
 1390             return True #Don't pass on up/down arrows etc.
 1391 
 1392     #Note Ctrl-C as well as doing "copy path" which is handled here,
 1393     #also cancels a find in progress. That doesn't need to be handled here
 1394     #because the control buttons grab focus during the find operation.
 1395     #
 1396     #Note also that, we need a global handler here for all lists
 1397     #as in GTK, keyboard events specifically are sent to the window first,
 1398     #rather than the widget. The alternative is to add handlers to all lists.
 1399     def on_fslint_keypress(self, button, event):
 1400         #ingore capslock and numlock
 1401         state = event.state & ~(gtk.gdk.LOCK_MASK | gtk.gdk.MOD2_MASK)
 1402         ksyms=gtk.keysyms
 1403         if state == int(gtk.gdk.CONTROL_MASK):
 1404             if event.keyval in (ksyms.c, ksyms.C):
 1405                 self.on_copy_activate(event)
 1406             elif event.keyval in (ksyms.o, ksyms.O):
 1407                 self.on_open_activate(event)
 1408         elif state == 0:
 1409             if event.keyval in (ksyms.F2,):
 1410                 self.on_rename_activate(event)
 1411             elif event.keyval in (ksyms.Return,):
 1412                 self.on_open_activate(event)
 1413             elif event.keyval in (ksyms.Delete,):
 1414                 if self.ok_dirs.flags() & gtk.HAS_FOCUS:
 1415                     self.on_removeOkDir_clicked(event)
 1416                 elif self.bad_dirs.flags() & gtk.HAS_FOCUS:
 1417                     self.on_removeBadDir_clicked(event)
 1418                 elif self.clists[self.mode].flags() & gtk.HAS_FOCUS:
 1419                     self.on_delSelected_clicked(event)
 1420 
 1421     def on_saveAs_clicked(self, event):
 1422         clist = self.clists[self.mode]
 1423         if clist.rows == 0:
 1424             return
 1425         self.dlgPath.show(fileOps=True)
 1426         if not self.dlgPath.canceled:
 1427             self.ClearErrors()
 1428             fileSaveAs = self.dlgPath.GtkWindow.get_filename()
 1429             try:
 1430                 fileSaveAs = open(fileSaveAs, 'w')
 1431             except:
 1432                 etype, emsg, etb = sys.exc_info()
 1433                 self.ShowErrors(str(emsg)+'\n')
 1434                 return
 1435             rows_to_save = clist.selection
 1436             if self.mode == self.mode_pkgs:
 1437                 for row in xrange(clist.rows):
 1438                     if len(rows_to_save):
 1439                         if row not in rows_to_save: continue
 1440                     rowtext = ''
 1441                     for col in (0,2): #ignore "human number" col
 1442                         rowtext += clist.get_text(row,col) +'\t'
 1443                     fileSaveAs.write(rowtext[:-1]+'\n')
 1444             else:
 1445                 row_data=clist.get_data("row_data")
 1446                 if len(rows_to_save):
 1447                     for row in xrange(clist.rows):
 1448                         if get_selectable(row, row_data):
 1449                             if row not in rows_to_save: continue
 1450                         else: continue #don't save group headers
 1451                         fileSaveAs.write(row_data[row])
 1452                 else:
 1453                     fileSaveAs.writelines(row_data)
 1454 
 1455     def get_selected_path(self, row=None, folder=False):
 1456         clist = self.clists[self.mode]
 1457         if self.mode==self.mode_pkgs:
 1458             return None
 1459         if row==None and len(clist.selection)!=1:
 1460             return None
 1461 
 1462         row_data=clist.get_data("row_data")
 1463         if row==None:
 1464             row = clist.selection[0] #menu item only enabled if 1 item selected
 1465         disp_path=os.path.join(clist.get_text(row,1), clist.get_text(row,0))
 1466         path=row_data[row][:-1] #actual full path
 1467         urlencode = (path != disp_path)
 1468 
 1469         if folder:
 1470             path=os.path.split(path)[0]
 1471 
 1472         if urlencode:
 1473             import urllib
 1474             path="file://"+urllib.pathname2url(path)
 1475 
 1476         return path
 1477 
 1478     def on_copy_activate(self, src):
 1479         path = self.get_selected_path()
 1480         if path:
 1481             clipboard=self.GtkWindow.get_clipboard("CLIPBOARD")
 1482             clipboard.set_text(path, -1)
 1483 
 1484     def on_open_folder_activate(self, src, row=None):
 1485         self.on_open_activate(src, row, folder=True)
 1486 
 1487     def on_open_activate(self, src, row=None, folder=False):
 1488         path = self.get_selected_path(row, folder)
 1489         if path:
 1490             #Note we can't use subProcess() or os.popen3() to read errors,
 1491             #as stdout & stderr of the resulting GUI program will be
 1492             #connected to its pipes and so we will hang on read().
 1493             #Note also that xdg-open doesn't identify the desktop platform
 1494             #correctly when running (FSlint) on a remote X session.
 1495             path = pipes.quote(path)
 1496             if os.system("xdg-open %s >/dev/null 2>&1" % path):
 1497                 #could run again with os.popen3() to get actual error
 1498                 #but that's a bit hacky I think?
 1499                 self.ShowErrors(_("Error starting xdg-open") + '\n')
 1500 
 1501     # select all other duplicates from selected item folder
 1502     def on_select_from_that_folder_activate(self, src):
 1503         clist = self.clists[self.mode]
 1504         if self.mode==self.mode_pkgs or len(clist.selection)!=1:
 1505             return
 1506         row = clist.selection[0] #menu item only enabled if 1 item selected
 1507         row_data = clist.get_data("row_data")
 1508         path = os.path.split(row_data[row])[0]
 1509         select_func=clist.select_row
 1510         for row in xrange(clist.rows):
 1511             if os.path.split(row_data[row])[0] == path:
 1512                 select_func(row, 0)
 1513 
 1514     def on_rename_activate(self, src):
 1515         clist = self.clists[self.mode]
 1516         if self.mode==self.mode_pkgs or len(clist.selection)!=1:
 1517             return
 1518 
 1519         row_data=clist.get_data("row_data")
 1520         row=clist.selection[0] #menu item only enabled if 1 item selected
 1521         utf8_name=clist.get_text(row,0)
 1522         path_name=row_data[row][:-1]
 1523 
 1524         (response,new_path)=self.msgbox(_("Rename:"),('Cancel','Ok'),utf8_name)
 1525         if response != "Ok":
 1526             return
 1527 
 1528         self.ClearErrors()
 1529         if not (self.mode==self.mode_nl and self.chk_findu8.get_active()):
 1530             try:
 1531                 new_encoded_path=I18N.g_filename_from_utf8(new_path)
 1532             except UnicodeEncodeError:
 1533                 new_encoded_path=None
 1534                 self.ShowErrors(_(
 1535                 "Error: [%s] can not be represented on your file system") %\
 1536                 (new_path) +'\n')
 1537         else:
 1538             new_encoded_path=new_path #leave in (displayable) utf-8
 1539         if new_encoded_path:
 1540             dir,base=os.path.split(path_name)
 1541             new_encoded_path=dir+'/'+new_encoded_path
 1542             if os.path.exists(new_encoded_path):
 1543                 translation_map={"old_path":utf8_name, "new_path":new_path}
 1544                 self.ShowErrors(_(
 1545                 "Error: Can't rename [%(old_path)s] as "\
 1546                 "[%(new_path)s] exists") % translation_map + '\n')
 1547             else:
 1548                 try:
 1549                     os.rename(path_name, new_encoded_path)
 1550                     row_data.pop(row)
 1551                     clist.remove(row)
 1552                     if self.mode!=self.mode_up:
 1553                         # We may want to delete the other item
 1554                         # in a group of two
 1555                         self.clist_remove_handled_groups(clist)
 1556                 except OSError:
 1557                     self.ShowErrors(str(sys.exc_info()[1])+'\n')
 1558 
 1559     def on_unselect_using_wildcard_activate(self, event):
 1560         self.select_using_wildcard(False)
 1561     def on_select_using_wildcard_activate(self, event):
 1562         self.select_using_wildcard(True)
 1563     def select_using_wildcard(self, select):
 1564         clist = self.clists[self.mode]
 1565         if clist.rows == 0:
 1566             return
 1567 
 1568         (response, wildcard) = self.msgbox(_("wildcard:"), ('Cancel', 'Ok'))
 1569         if response!="Ok" or not wildcard:
 1570             return
 1571 
 1572         self.ClearErrors()
 1573         if '/' in wildcard:
 1574             #Convert from utf8 if required so can match non utf-8 paths
 1575             try:
 1576                 wildcard=I18N.g_filename_from_utf8(wildcard)
 1577             except UnicodeEncodeError:
 1578                 self.ShowErrors(_(
 1579                 "Error: [%s] can not be represented on your file system") %\
 1580                 (wildcard) +'\n')
 1581                 return
 1582         else:
 1583             #Convert to unicode to match unicode string below
 1584             wildcard=unicode(wildcard, "utf-8")
 1585         select_func=(select and clist.select_row or clist.unselect_row)
 1586         if '/' not in wildcard or self.mode == self.mode_pkgs:
 1587             #Convert to unicode so ? in glob matching works correctly
 1588             get_text = lambda row: unicode(clist.get_text(row,0),"utf-8")
 1589         else: #Note fnmatch ignores trailing \n
 1590             row_data = clist.get_data("row_data")
 1591             get_text = lambda row: row_data[row]
 1592         import fnmatch
 1593         for row in xrange(clist.rows):
 1594             if fnmatch.fnmatch(get_text(row), wildcard):
 1595                 select_func(row, 0)
 1596 
 1597     def on_select_all_but_first_in_each_group_activate(self, event):
 1598         self.on_select_all_but_one_in_each_group_activate("first")
 1599     def on_select_all_but_newest_in_each_group_activate(self, event):
 1600         self.on_select_all_but_one_in_each_group_activate("newest")
 1601     def on_select_all_but_oldest_in_each_group_activate(self, event):
 1602         self.on_select_all_but_one_in_each_group_activate("oldest")
 1603 
 1604     def on_select_all_but_one_in_each_group_activate(self, which):
 1605 
 1606         def find_row_to_unselect(clist, row, which):
 1607             import operator
 1608             if which == "first":
 1609                 if row==0 and get_selectable(row, row_data):
 1610                     return row #for first row in clist_sn
 1611                 else:
 1612                     return row+1
 1613             elif which == "newest":
 1614                 unselect_mtime=-1
 1615                 comp=operator.gt
 1616             elif which == "oldest":
 1617                 unselect_mtime=2**32
 1618                 comp=operator.lt
 1619             if row!=0 or not get_selectable(row, row_data):
 1620                 row += 1 #for all except first row in clist_sn
 1621             unselect_row = -1 # avoid rh bug 726252 (mtimes for group = -1?)
 1622             while row < clist.rows and get_selectable(row, row_data):
 1623                 mtime = clist.get_row_data(row)
 1624                 if comp(mtime,unselect_mtime):
 1625                     unselect_mtime = mtime
 1626                     unselect_row=row
 1627                 row += 1
 1628             return unselect_row
 1629 
 1630         clist = self.clists[self.mode]
 1631         row_data = clist.get_data("row_data")
 1632         clist.freeze()
 1633         clist.select_all()
 1634         for row in xrange(clist.rows):
 1635             if row==0 or not get_selectable(row, row_data): #New group
 1636                 unselect_row = find_row_to_unselect(clist, row, which)
 1637                 if unselect_row == -1:
 1638                     clist.unselect_all()
 1639                     clist.thaw()
 1640                     self.ShowErrors("Error getting age of group containing ["+
 1641                                     self.get_selected_path(row=row+1)+']\n')
 1642                     return
 1643                 clist.unselect_row(unselect_row, 0)
 1644         clist.thaw()
 1645 
 1646     def find_group_with_all_selected(self, skip_groups=0):
 1647 
 1648         if self.mode != self.mode_up and self.mode != self.mode_sn:
 1649             return False
 1650 
 1651         clist = self.clists[self.mode]
 1652         selected = clist.selection
 1653         if not selected:
 1654             return False
 1655 
 1656         row_data = clist.get_data("row_data")
 1657 
 1658         def group_all_selected(clist, row):
 1659             if row!=0 or not get_selectable(row, row_data): #for first sn row
 1660                 row += 1
 1661             while row < clist.rows and get_selectable(row, row_data):
 1662                 if not row in selected:
 1663                     return False
 1664                 row += 1
 1665             return True
 1666 
 1667         group=1
 1668         for row in xrange(clist.rows):
 1669             if row==0 or not get_selectable(row, row_data): #New group
 1670                 if group_all_selected(clist, row):
 1671                     if group > skip_groups:
 1672                         clist.moveto(row,0,0.0,0.0)
 1673                         return True
 1674                     group += 1
 1675 
 1676         return False
 1677 
 1678     def on_unselect_all_activate(self, event):
 1679         clist = self.clists[self.mode]
 1680         clist.unselect_all()
 1681 
 1682     def on_toggle_selection_activate(self, event):
 1683         clist = self.clists[self.mode]
 1684         clist.freeze()
 1685         selected = clist.selection
 1686         if len(selected) == 0:
 1687             clist.select_all()
 1688         elif len(selected) == clist.rows:
 1689             clist.unselect_all()
 1690         else:
 1691             clist.select_all()
 1692             for row in selected:
 1693                 clist.unselect_row(row, 0)
 1694         clist.thaw()
 1695 
 1696     def on_selection_clicked(self, widget):
 1697         self.on_selection_menu_button_press_event(self.selection, None)
 1698 
 1699     def clist_pkgs_sort_by_selection(self, clist):
 1700         selection = clist.selection
 1701         for row in xrange(clist.rows):
 1702             if row in selection:
 1703                 clist.set_text(row,3,"1")
 1704             else:
 1705                 clist.set_text(row,3,"0")
 1706         clist.set_sort_type(gtk.SORT_DESCENDING)
 1707         clist.set_sort_column(3)
 1708         clist.sort()
 1709         clist.moveto(0,0,0.0,0.0)
 1710 
 1711     def on_clist_pkgs_click_column(self, clist, col):
 1712         self.clist_pkgs_order[col] = not self.clist_pkgs_order[col]
 1713         if self.clist_pkgs_order[col]:
 1714             clist.set_sort_type(gtk.SORT_ASCENDING)
 1715         else:
 1716             clist.set_sort_type(gtk.SORT_DESCENDING)
 1717         try:
 1718             #focus_row is not updated after ordering, so can't use
 1719             #last_selected=clist.get_row_data(clist.focus_row)
 1720             last_selected=self.clist_pkgs_last_selected
 1721         except:
 1722             last_selected=0
 1723         if col==0:
 1724             clist.set_sort_column(0)
 1725         else:
 1726             clist.set_sort_column(2)
 1727         clist.sort() #note ascii sort (locale ignored)
 1728         #could instead just use our "row_data" and
 1729         #repopulate the gtkclist with something like:
 1730         #row_data = clist.get_data("row_data")
 1731         #row_data.sort(key = lambda x:x[col],
 1732         #              reverse = not self.clist_pkgs_order[col])
 1733         if last_selected:
 1734             #Warning! As of pygtk2-2.4.0 at least
 1735             #find_row_from_data only compares pointers, not strings!
 1736             last_selected_row=clist.find_row_from_data(last_selected)
 1737             clist.moveto(last_selected_row,0,0.5,0.0)
 1738             #clist.set_focus_row(last_selected_row)
 1739         else:
 1740             clist.moveto(0,0,0.0,0.0)
 1741 
 1742     #Note the events and event ordering provided by the GtkClist
 1743     #make it very hard to provide robust and performant handlers
 1744     #for selected rows. These flags are an attempt to stop the
 1745     #function below from being called too frequently.
 1746     #Note most users will scoll the list with {Up,Down} rather
 1747     #than Ctrl+{Up,Down}, but at worst this will result in slow scrolling.
 1748     def on_clist_pkgs_button_press(self, list, event):
 1749         if event.button == 3:
 1750             self.on_selection_menu_button_press_event(list, event)
 1751         else:
 1752             self.clist_pkgs_user_input=True
 1753     def on_clist_pkgs_key_press(self, *args):
 1754         self.clist_pkgs_user_input=True
 1755 
 1756     def on_clist_pkgs_selection_changed(self, clist, *args):
 1757         self.pkg_info.get_buffer().set_text("")
 1758         if not self.clist_pkgs_user_input:
 1759             self.clist_pkgs_last_selected=False
 1760             return
 1761         self.clist_pkgs_user_input=False
 1762         if len(clist.selection) == 0:
 1763             return
 1764         pkg=clist.get_row_data(clist.selection[-1])
 1765         self.clist_pkgs_last_selected=pkg #for ordering later
 1766         if dist_type.rpm:
 1767             cmd = "rpm -q --queryformat '%{DESCRIPTION}' " + pkg
 1768         elif dist_type.dpkg:
 1769             cmd = "dpkg-query -W --showformat='${Description}' " + pkg
 1770         elif dist_type.pacman:
 1771             cmd =  "LC_ALL=C pacman -Qi " + pkg
 1772             cmd += " | sed -n 's/Description *: *//p'"
 1773         pkg_process = subProcess(cmd)
 1774         pkg_process.read()
 1775         lines=pkg_process.outdata
 1776         self.pkg_info.get_buffer().set_text(I18N.displayable_utf8(lines,"\n"))
 1777         del(pkg_process)
 1778 
 1779     def on_delSelected_clicked(self, src):
 1780         if self.mode == self.mode_pkgs:
 1781             if os.geteuid() != 0:
 1782                 self.msgbox(
 1783                   _("Sorry, you must be root to delete system packages.")
 1784                 )
 1785                 return
 1786         if self.confirm_delete:
 1787             (response, self.confirm_delete) =\
 1788             self.check_user(_("Are you sure you want to delete"))
 1789             if not response:
 1790                 return
 1791         skip_groups=0
 1792         while self.confirm_delete_group and \
 1793               self.find_group_with_all_selected(skip_groups):
 1794             (response, self.confirm_delete_group) = self.msgbox(
 1795               _("Are you sure you want to delete all items in this group?"),
 1796               ('Yes', 'No'), again=True)
 1797             if response != "Yes":
 1798                 return
 1799             skip_groups += 1
 1800 
 1801         self.set_cursor(self.WATCH)
 1802         clist = self.clists[self.mode]
 1803         row_data = clist.get_data("row_data")
 1804         paths_to_remove = clist.selection
 1805         paths_to_remove.sort() #selection order irrelevant/problematic
 1806         paths_to_remove.reverse() #so deleting works correctly
 1807         numDeleted = 0
 1808         if self.mode == self.mode_pkgs:
 1809             #get pkg names to delete
 1810             pkgs_selected = []
 1811             for row in paths_to_remove:
 1812                 package = clist.get_row_data(row)
 1813                 pkgs_selected.append(package)
 1814             #determine if we need to select more packages
 1815             self.status.set_text(_("Calculating dependencies..."))
 1816             while gtk.events_pending(): gtk.main_iteration(False)
 1817             all_deps=self.whatRequires(pkgs_selected)
 1818             if len(all_deps) > len(pkgs_selected):
 1819                 num_new_pkgs = 0
 1820                 for package in all_deps:
 1821                     if package not in pkgs_selected:
 1822                         num_new_pkgs += 1
 1823                         #Note clist.find_row_from_data() only compares pointers
 1824                         for row in xrange(clist.rows):
 1825                             if package == clist.get_row_data(row):
 1826                                 clist.select_row(row,0)
 1827 
 1828                 #make it easy to review by grouping all selected packages
 1829                 self.clist_pkgs_sort_by_selection(clist)
 1830 
 1831                 self.set_cursor(None)
 1832                 self.msgbox(
 1833                   _("%d extra packages need to be deleted.\n") % num_new_pkgs +
 1834                   _("Please review the updated selection.")
 1835                 )
 1836                 #Note this is not ideal as it's difficult for users
 1837                 #to get info on selected packages (Ctrl click twice).
 1838                 #Should really have a seperate marked_for_deletion  column.
 1839                 self.status.set_text("")
 1840                 return
 1841 
 1842             self.status.set_text(_("Removing packages..."))
 1843             while gtk.events_pending(): gtk.main_iteration(False)
 1844             if dist_type.rpm:
 1845                 cmd="rpm -e "
 1846             elif dist_type.dpkg:
 1847                 cmd="dpkg --purge "
 1848             elif dist_type.pacman:
 1849                 cmd="pacman -R "
 1850             cmd += ' '.join(pkgs_selected) + " >/dev/null 2>&1"
 1851             os.system(cmd)
 1852 
 1853         # Depth first traversal.
 1854         # Any exception aborts.
 1855         # os.walk available since python 2.3, but this is simpler.
 1856         # We don't follow symlinks. They shouldn't be present in any case.
 1857         def rm_branch(branch):
 1858             for name in os.listdir(branch):
 1859                 name = os.path.join(branch, name)
 1860                 if os.path.isdir(name) and not os.path.islink(name):
 1861                     rm_branch(name)
 1862             os.rmdir(branch)
 1863 
 1864         clist.freeze()
 1865         for row in paths_to_remove:
 1866             if self.mode != self.mode_pkgs:
 1867                 try:
 1868                     path=row_data[row][:-1] #strip trailing '\n'
 1869                     if os.path.isdir(path):
 1870                         rm_branch(path)
 1871                     else:
 1872                         os.unlink(path)
 1873                 except:
 1874                     etype, emsg, etb = sys.exc_info()
 1875                     self.ShowErrors(str(emsg)+'\n')
 1876                     continue
 1877                 row_data.pop(row)
 1878             clist.remove(row)
 1879             numDeleted += 1
 1880 
 1881         if self.mode != self.mode_pkgs:
 1882             self.clist_remove_handled_groups(clist)
 1883 
 1884         clist.select_row(clist.focus_row,0)
 1885 
 1886         clist.thaw()
 1887         status = str(numDeleted) + _(" items deleted")
 1888         if self.mode == self.mode_pkgs:
 1889             status += ". " + human_space_left('/')
 1890         self.status.set_text(status)
 1891         self.set_cursor(None)
 1892 
 1893     def on_autoSymlink_clicked(self, event):
 1894         self.on_autoMerge(symlink=True)
 1895 
 1896     def on_autoMerge_clicked(self, event):
 1897         self.on_autoMerge(symlink=False)
 1898 
 1899     def on_autoMerge(self, symlink):
 1900         self.ClearErrors()
 1901         self.status.delete_text(0,-1)
 1902         clist = self.clists[self.mode]
 1903         if clist.rows < 3:
 1904             return
 1905 
 1906         if symlink:
 1907             question=_("Are you sure you want to symlink ALL files?\n")
 1908         else:
 1909             question=_("Are you sure you want to merge ALL files?\n")
 1910 
 1911         paths_to_leave = clist.selection
 1912         if len(paths_to_leave):
 1913             question += _("(Ignoring those selected)\n")
 1914 
 1915         (response,) = self.msgbox(question, ('Yes', 'No'))
 1916         if response != "Yes":
 1917             return
 1918 
 1919         self.set_cursor(self.WATCH)
 1920         newGroup = False
 1921         row_data=clist.get_data("row_data")
 1922         for row in xrange(clist.rows):
 1923             if row in paths_to_leave:
 1924                 continue
 1925             if not get_selectable(row, row_data): #new group
 1926                 newGroup = True
 1927             else:
 1928                 path = row_data[row][:-1] #strip '\n'
 1929                 if newGroup:
 1930                     keepfile = path
 1931                     newGroup = False
 1932                 else:
 1933                     dupfile = path
 1934                     dupdir = os.path.dirname(dupfile)
 1935                     tmpfile = tempfile.mktemp(dir=dupdir)
 1936                     try:
 1937                         try:
 1938                             if symlink:
 1939                                 os.symlink(os.path.realpath(keepfile),tmpfile)
 1940                             else:
 1941                                 os.link(keepfile,tmpfile)
 1942                         except OSError, value:
 1943                             if value.errno == errno.EXDEV and not symlink:
 1944                                 os.symlink(os.path.realpath(keepfile),tmpfile)
 1945                             else:
 1946                                 raise
 1947                         os.rename(tmpfile, dupfile)
 1948                         clist.set_background(row, self.bg_colour)
 1949                     except OSError:
 1950                         self.ShowErrors(str(sys.exc_info()[1])+'\n')
 1951                     try:
 1952                         #always try this as POSIX has bad requirement that
 1953                         #rename(file1,file2) where both are links to the same
 1954                         #file, does nothing, and returns success. So if user
 1955                         #merges files multiple times, tmp files will be left.
 1956                         os.unlink(tmpfile)
 1957                     except:
 1958                         pass
 1959         self.set_cursor(None)
 1960 
 1961     def on_autoClean_clicked(self, event):
 1962         if self.mode == self.mode_rs and self.chkTabs.get_active():
 1963             (response,) = self.msgbox(
 1964               _("Which indenting method do you want to convert to?"),
 1965               (N_('Tabs'), N_('Spaces'), 'Cancel')
 1966             )
 1967             if response == "Spaces":
 1968                 indents="--indent-spaces"
 1969             elif response == "Tabs":
 1970                 indents="--indent-tabs"
 1971             else:
 1972                 return
 1973         else:
 1974             if self.confirm_clean:
 1975                 (response, self.confirm_clean) =\
 1976                 self.check_user(_("Are you sure you want to clean"))
 1977                 if not response:
 1978                     return
 1979 
 1980         self.set_cursor(self.WATCH)
 1981         if self.mode == self.mode_rs:
 1982             if not self.chkTabs.get_active():
 1983                 indents="''"
 1984             if not self.chkWhitespace.get_active():
 1985                 eol="''"
 1986             else:
 1987                 eol="--eol"
 1988             command=liblocation+"/fslint/supprt/rmlint/fix_ws.sh "+\
 1989                     eol+" "+\
 1990                     indents+" "+\
 1991                     str(int(self.spinTabs.get_value()))
 1992         elif self.mode == self.mode_ns:
 1993             command="strip"
 1994 
 1995         clist = self.clists[self.mode]
 1996         paths_to_clean = clist.selection
 1997 
 1998         numCleaned = 0
 1999         totalSaved = 0
 2000         row_data=clist.get_data("row_data")
 2001         for row in paths_to_clean:
 2002             path_to_clean = row_data[row][:-1] #strip '\n'
 2003             try:
 2004                 startlen = os.stat(path_to_clean).st_size
 2005             except OSError:
 2006                 self.ShowErrors(str(sys.exc_info()[1])+'\n')
 2007                 continue
 2008 
 2009             path_param = pipes.quote(path_to_clean)
 2010             cleanProcess = subProcess(command + " " + path_param)
 2011             cleanProcess.read()
 2012             errors = cleanProcess.errdata
 2013             del(cleanProcess)
 2014             if len(errors):
 2015                 self.ShowErrors(errors)
 2016             else:
 2017                 clist.set_background(row, self.bg_colour)
 2018                 clist.unselect_row(row,0)
 2019                 totalSaved += startlen - os.stat(path_to_clean).st_size
 2020                 numCleaned += 1
 2021         status =  str(numCleaned) + _(" items cleaned (") + \
 2022                   human_num(totalSaved) + _(" bytes saved)")
 2023         self.status.set_text(status)
 2024         self.set_cursor(None)
 2025 
 2026 FSlint=fslint(liblocation+"/fslint.glade", "fslint")
 2027 
 2028 gtk.main ()