#!/usr/bin/env python

import gettext
import gtk
import gtk.glade
import locale

import os, sys
import time, popen2
import stat, statvfs

liblocation="/usr/share/fslint"
locale_base=liblocation+'/po/locale'
try:
    import fslint
    if sys.argv[0][0] != '/':
        #If relative probably debugging so don't use system files if possible
        if not os.path.exists(liblocation+'/fslint'):
            liblocation = fslint.liblocation
            locale_base = None #sys default
    else:
        liblocation = fslint.liblocation
        locale_base = None #sys default
except:
    pass

def i18n():
    #gtk2 only understands utf-8 so convert to unicode
    #which will be automatically converted to utf-8 by pygtk
    gettext.install("fslint", locale_base, unicode=1)
    global N_ #translate string but not at point of definition
    def N_(string): return string
    td=gettext.bindtextdomain("fslint",locale_base)
    gettext.textdomain("fslint")
    #This call should be redundant
    #(done correctly in gettext) for python 2.3
    try:
        gtk.glade.bindtextdomain("fslint",td) #note sets codeset to utf-8
    except:
        #if no glade.bindtextdomain then we'll do without
        pass

    try:
        locale.setlocale(locale.LC_ALL,'')
    except:
        #gtk will print warning for a duff locale
        pass

i18n() #call first so everything is internationalized

import getopt
def Usage():
    print _("Usage: %s [OPTION] [PATHS]") % os.path.split(sys.argv[0])[1]
    print _("    --version       display version")
    print _("    --help          display help")

try:
    lOpts, lArgs = getopt.getopt(sys.argv[1:], "", ["help","version"])

    if len(lArgs) == 0:
        lArgs = [os.getcwd()]

    if ("--help","") in lOpts:
        Usage()
        sys.exit(None)

    if ("--version","") in lOpts:
        print "FSlint 2.06"
        sys.exit(None)
except getopt.error, msg:
    print msg
    print
    Usage()
    sys.exit(2)

class GladeWrapper:
    """
    Superclass for glade based applications. Just derive from this
    and your subclass should create methods whose names correspond to
    the signal handlers defined in the glade file. Any other attributes
    in your class will be safely ignored.

    This class will give you the ability to do:
        subclass_instance.GtkWindow.method(...)
        subclass_instance.widget_name...
    """
    def __init__(self, Filename, WindowName):
        #load glade file.
        self.widgets = gtk.glade.XML(Filename, WindowName, gettext.textdomain())
        self.GtkWindow = getattr(self, WindowName)

        instance_attributes = {}
        for attribute in dir(self.__class__):
            instance_attributes[attribute] = getattr(self, attribute)
        self.widgets.signal_autoconnect(instance_attributes)

    def __getattr__(self, attribute): #Called when no attribute in __dict__
        widget = self.widgets.get_widget(attribute)
        if widget is None:
            raise AttributeError("Widget [" + attribute + "] not found")
        self.__dict__[attribute] = widget #add reference to cache
        return widget

class dlgUserInteraction(GladeWrapper):
    """
    Note input buttons tuple text should not be translated
    so that the stock buttons are used if possible. But the
    translations should be available, so use N_ for input
    buttons tuple text. Note the returned response is not
    translated either.
    """
    def init(self, app, message, buttons=('Ok',)):
        for text in buttons:
            try:
                stock_text=getattr(gtk,"STOCK_"+text.upper())
                button = gtk.Button(stock=stock_text)
            except:
                button = gtk.Button(label=_(text))
            button.set_data("text", text)
            button.connect("clicked", self.button_clicked)
            self.GtkWindow.action_area.pack_start(button)
            button.show()
        self.app = app
        self.lblmsg.set_text(message)
        self.lblmsg.show()

    def show(self):
        self.response=None
        self.GtkWindow.set_transient_for(self.app.GtkWindow)#center on main win
        self.GtkWindow.show()
        if self.GtkWindow.modal:
            gtk.mainloop() #synchronous call from parent
        #else: not supported

    #################
    # Signal handlers
    #################

    def quit(self, *args):
        self.GtkWindow.hide()
        if self.GtkWindow.modal:
            gtk.mainquit()

    def button_clicked(self, button):
        self.response = button.get_data("text")
        self.quit()

class dlgPathSel(GladeWrapper):

    def init(self, app):
        self.app = app

    def show(self, fileOps=0):
        self.canceled=1
        self.fileOps = fileOps
        self.GtkWindow.set_filename("")
        if self.fileOps:
            self.GtkWindow.show_fileop_buttons()
        else:
            self.GtkWindow.hide_fileop_buttons()
        #self.GtkWindow.set_parent(self.app.GtkWindow)#error (on gtk-1.2.10-11?)
        self.GtkWindow.set_transient_for(self.app.GtkWindow)#center on main win
        self.GtkWindow.show()
        if self.GtkWindow.modal:
            gtk.mainloop() #synchronous call from parent
        #else: not supported

    #################
    # Signal handlers
    #################

    def quit(self, *args):
        self.GtkWindow.hide()
        if self.GtkWindow.modal:
            gtk.mainquit()
        return gtk.TRUE #Don't let window be destroyed

    def on_okdirs_clicked(self, *args):
        if self.fileOps: #creating new item
            file = self.GtkWindow.get_filename()
            if os.path.exists(file):
                if os.path.isfile(file):
                    msgbox=dlgUserInteraction(liblocation+"/fslint.glade",
                                              "UserInteraction")
                    msgbox.init(self,
                                _("Do you want to overwrite?\n") + file,
                                (N_('Yes'), N_('No')))
                    msgbox.show()
                    if msgbox.response != "Yes":
                        return
                else:
                    msgbox=dlgUserInteraction(liblocation+"/fslint.glade",
                                              "UserInteraction")
                    msgbox.init(self, _("You can't overwrite ") + file)
                    msgbox.show()
                    return
        self.canceled=0
        self.quit()

class fslint(GladeWrapper):

    class UserAbort(Exception):
        pass

    def __init__(self, Filename, WindowName):
        GladeWrapper.__init__(self, Filename, WindowName)
        self.dlgPathSel = dlgPathSel(liblocation+"/fslint.glade", "PathSel")
        self.dlgPathSel.init(self)
        self.status_id = self.fslint_status.get_context_id("fslint")
        #Just need to keep this tuple in sync with tabs
        (self.mode_up, self.mode_nl, self.mode_sn, self.mode_tf, self.mode_bl,
         self.mode_id, self.mode_ed, self.mode_ns, self.mode_rs) = range(9)
        self.clists = {
            self.mode_up:self.clist_dups,
            self.mode_nl:self.clist_nl,
            self.mode_sn:self.clist_sn,
            self.mode_tf:self.clist_tf,
            self.mode_bl:self.clist_bl,
            self.mode_id:self.clist_id,
            self.mode_ed:self.clist_ed,
            self.mode_ns:self.clist_ns,
            self.mode_rs:self.clist_rs
        }
        for path in lArgs:
            if os.path.exists(path):
                abspath = os.path.abspath(path)
                self.ok_dirs.append([abspath])
            else:
                self.ShowErrors(_("Invalid path [") + path + "]")
        for bad_dir in ['/lost+found','/dev','/proc','/tmp']:
            self.bad_dirs.append([bad_dir])
        self.mode=0

    def get_fslint(self, command, delim='\n'):
        fslintproc = fslint_backend(command)
        while fslintproc.getOutput():
            while gtk.events_pending(): gtk.mainiteration(gtk.FALSE)
            if self.stopflag:
                fslintproc.kill()
                break
        else:
            fslintproc.outdata=fslintproc.outdata.split(delim)[:-1]
            ret = (fslintproc.outdata, fslintproc.errdata)
        fslintproc.wait() #cleanup
        if self.stopflag:
            raise self.UserAbort
        else:
            return ret

    def ShowErrors(self, lines):
        lines = lines.split('\n')
        for line in lines:
            if len(line):
                self.errors.append([line])

    def buildFindParameters(self):
        if self.mode == self.mode_sn:
            if self.chk_sn_path.get_active():
                return ""
        elif self.mode == self.mode_ns:
            if self.chk_ns_path.get_active():
                return ""

        if self.ok_dirs.rows == 0:
            #raise _("No search directories") #python 2.2.2 can't raise unicode
            return _("No search paths specified")

        search_dirs = ""
        for row in range(self.ok_dirs.rows):
            search_dirs = search_dirs + " " + self.ok_dirs.get_text(row,0)
        exclude_dirs=""

        for row in range(self.bad_dirs.rows):
            if exclude_dirs == "":
                exclude_dirs = '\(' + ' -path "'
            else:
                exclude_dirs += ' -o -path "'
            exclude_dirs += self.bad_dirs.get_text(row,0) + '"'
        if exclude_dirs != "":
            exclude_dirs += ' \) -prune -o '

        if not self.recurseDirs.get_active():
            recurseParam=" -r "
        else:
            recurseParam=" "

        self.findParams = search_dirs + " -f " + recurseParam + exclude_dirs
        self.findParams += self.extra_find_params.get_text()

        return ""

    def removeDirs(self, dirlist):
        """Removes selected items from passed clist.
           If no items selected then list is cleared."""
        paths_to_remove = dirlist.selection
        if len(paths_to_remove) == 0:
            dirlist.clear()
        else:
            paths_to_remove.sort() #selection order irrelevant/problematic
            paths_to_remove.reverse() #so deleting works correctly
            for row in paths_to_remove:
                dirlist.remove(row)

    def get_path_info(self, path):
        """Returns (colorname, size, disk_size, uid, gid, mtime_date_str)"""

        stat_val = os.lstat(path)

        date = time.ctime(stat_val[stat.ST_MTIME])
        month_time = date[4:16]
        year = date[-5:]
        timediff = time.time()-stat_val[stat.ST_MTIME]
        if timediff > 15552000: #6months
            date = month_time[0:6] + year
        else:
            date = month_time

        mode = stat_val[stat.ST_MODE]
        if stat.S_ISREG(mode):
            colour = '' #default
            if mode & (stat.S_IXGRP|stat.S_IXUSR|stat.S_IXOTH):
                colour = "#00C000"
        elif stat.S_ISDIR(mode):
            colour = "blue"
        elif stat.S_ISLNK(mode):
            colour = "cyan"
            if not os.path.exists(path):
                colour = "#C00000"
        else:
            colour = "tan"

        size=stat_val.st_blksize*(stat_val.st_blocks/(stat_val.st_blksize/512))

        return (colour, stat_val[stat.ST_SIZE], size, stat_val[stat.ST_UID],
                stat_val[stat.ST_GID], date)

    def findrs(self, clist_rs):
        cmap = clist_rs.get_colormap()
        cmap_cache={'':None}

        po, pe = self.get_fslint("./findrs " + self.findParams)
        row = 0
        for line in po:
            colour, fsize, size, uid, gid, date = self.get_path_info(line)
            if not cmap_cache.has_key(colour):
                cmap_cache[colour]=cmap.alloc_color(colour)
            line = os.path.split(line)
            clist_rs.append([line[1], line[0], str(fsize), date])
            if cmap_cache[colour]:
                clist_rs.set_foreground(row,cmap_cache[colour])
            row=row+1

        return (str(row) + _(" files"), pe)

    def findns(self, clist_ns):
        cmd = "./findns "
        if not self.chk_ns_path.get_active():
            cmd += self.findParams
        po, pe = self.get_fslint(cmd)

        unstripped=[]
        for line in po:
            colour, fsize, size, uid, gid, date = self.get_path_info(line)
            line = os.path.split(line)
            unstripped.append((fsize, line[1], line[0], date))
        unstripped.sort()
        unstripped.reverse()

        row = 0
        for fsize, file, directory, date in unstripped:
            clist_ns.append([file, directory, str(fsize), date])
            row += 1

        return (str(row) + _(" unstripped binaries"), pe)

    def finded(self, clist_ed):
        po, pe = self.get_fslint("./finded " + self.findParams)
        row = 0
        for line in po:
            colour, fsize, size, uid, gid, date = self.get_path_info(line)
            line = os.path.split(line)
            clist_ed.append([line[1], line[0], date])
            row += 1

        return (str(row) + _(" empty directories"), pe)

    def findid(self, clist_id):
        cmap = clist_id.get_colormap()
        cmap_cache={'':None}

        po, pe = self.get_fslint("./findid " + self.findParams, '\0')
        row = 0
        for record in po:
            colour, fsize, size, uid, gid, date = self.get_path_info(record)
            if not cmap_cache.has_key(colour):
                cmap_cache[colour]=cmap.alloc_color(colour)
            record=os.path.split(record)
            clist_id.append([record[1], record[0],
                            str(uid), str(gid), str(fsize), date])
            if cmap_cache[colour]:
                clist_id.set_foreground(row,cmap_cache[colour])
            row += 1

        return (str(row) + _(" files"), pe)

    def findbl(self, clist_bl):
        cmd = "./findbl " + self.findParams
        if self.opt_bl_dangling.get_active():
            cmd += " -d"
        elif self.opt_bl_suspect.get_active():
            cmd += " -s"
        elif self.opt_bl_relative.get_active():
            cmd += " -l"
        elif self.opt_bl_absolute.get_active():
            cmd += " -A"
        elif self.opt_bl_redundant.get_active():
            cmd += " -n"
        po, pe = self.get_fslint(cmd)
        row = 0
        for line in po:
            link, target = line.split(" -> ", 2)
            line=os.path.split(link)
            clist_bl.append([line[1],line[0], target])
            row += 1

        return (str(row) + _(" links"), pe)

    def findtf(self, clist_tf):
        cmap = clist_tf.get_colormap()
        cmap_cache={'':None}

        cmd = "./findtf " + self.findParams
        cmd += " --age=" + str(int(self.spin_tf_core.get_value()))
        if self.chk_tf_core.get_active():
            cmd += " -c"
        po, pe = self.get_fslint(cmd)
        row = 0
        byteWaste = 0
        for line in po:
            colour, fsize, size, uid, gid, date = self.get_path_info(line)
            if not cmap_cache.has_key(colour):
                cmap_cache[colour]=cmap.alloc_color(colour)
            line=os.path.split(line)
            clist_tf.append([line[1],line[0], str(fsize), date])
            byteWaste = byteWaste + size
            if cmap_cache[colour]:
                clist_nl.set_foreground(row,cmap_cache[colour])
            row += 1

        return (locale.format("%d",byteWaste,1) + _(" bytes wasted in ") +
                str(row) + _(" files"), pe)

    def findsn(self, clist_sn):
        cmap = clist_sn.get_colormap()
        GDKgrey = cmap.alloc_color("gray84")
        cmap_cache={'':None}
        cmap_cache['gray84']=GDKgrey

        cmd = "./findsn "
        if self.chk_sn_path.get_active():
            option = self.opt_sn_path.get_children()[0].get()
            if option == _("Aliases"):
                cmd += " -A"
            elif option == _("Conflicting files"):
                pass #default mode
            else:
                raise "glade GtkOptionMenu item not found"
        else:
            cmd += self.findParams
            option = self.opt_sn_paths.get_children()[0].get()
            if option == _("Aliases"):
                cmd += " -A"
            elif option == _("Same names"):
                pass #default mode
            elif option == _("Same names(ignore case)"):
                cmd += " -C"
            elif option == _("Case conflicts"):
                cmd += " -c"
            else:
                raise "glade GtkOptionMenu item not found"

        po, pe = self.get_fslint(cmd,'\0')
        po = po[1:]
        row=0
        findsn_number=0
        findsn_groups=0
        for record in po:
            if record == '': #process group
                clist_sn.append(['','',''])
                clist_sn.set_background(row,GDKgrey)
                clist_sn.set_selectable(row,0)
                findsn_groups += 1
            else:
                colour, fsize, size, uid, gid, date = self.get_path_info(record)
                if not cmap_cache.has_key(colour):
                    cmap_cache[colour]=cmap.alloc_color(colour)
                record=os.path.split(record)
                clist_sn.append([record[1],record[0],str(fsize)])
                if cmap_cache[colour]:
                    clist_sn.set_foreground(row,cmap_cache[colour])
                findsn_number += 1
            row += 1
        if findsn_number:
            findsn_groups += 1 #for stripped group head above

        return (str(findsn_number) + _(" files (in ") + str(findsn_groups) +
                _(" groups)"), pe)

    def findnl(self, clist_nl):
        cmap = clist_nl.get_colormap()
        cmap_cache={'':None}

        sensitivity = ('1','2','3','p')[
        int(self.hscale_findnl_level.get_adjustment().value)-1]
        po, pe = self.get_fslint("./findnl " + self.findParams + " -" +
                                   sensitivity, '\0')
        row = 0
        for record in po:
            colour, fsize, size, uid, gid, date = self.get_path_info(record)
            if not cmap_cache.has_key(colour):
                cmap_cache[colour]=cmap.alloc_color(colour)
            record=os.path.split(record)
            clist_nl.append([record[1],record[0]])
            if cmap_cache[colour]:
                clist_nl.set_foreground(row,cmap_cache[colour])
            row += 1

        return (str(row) + _(" files"), pe)

    def findup(self, clist_dups):
        cmap = clist_dups.get_colormap()
        GDKgrey = cmap.alloc_color("gray84")

        po, pe = self.get_fslint("./findup " + self.findParams + " --gui")

        numdups = size = bsize = fsize = 0
        alldups = []
        for line in po:
            line = line.strip()
            if line == '': #grouped == 1
                if numdups != 0:
                    alldups.append(((numdups-1)*size, numdups, fsize, dups))
                dups = []
                numdups = 0
            else:
                colour, fsize, size, uid, gid, date = self.get_path_info(line)
                line=os.path.split(line)
                dups.append((line[1],line[0],date))
                numdups = numdups + 1
        else:
            if numdups != 0:
                alldups.append(((numdups-1)*size, numdups, fsize, dups))

        alldups.sort()
        alldups.reverse()

        byteWaste = 0
        numWaste = 0
        row=0
        for groupWaste, groupSize, FilesSize, files in alldups:
            byteWaste += groupWaste
            numWaste += groupSize - 1
            groupHeader = ["%d x %s" % (groupSize,
                                        locale.format("%d",FilesSize,1)),
                           "=> %s" % locale.format("%d",groupWaste,1),
                           _("bytes wasted")]
            clist_dups.append(groupHeader)
            clist_dups.set_background(row,GDKgrey)
            clist_dups.set_selectable(row,0)
            row += 1
            for file in files:
                clist_dups.append(file)
                row += 1

        return (locale.format("%d",byteWaste,1) + _(" bytes wasted in ") +
                str(numWaste) + _(" files (in ") + str(len(alldups)) +
                _(" groups)"), pe)

    find_dispatch = (findup,findnl,findsn,findtf,findbl,
                     findid,finded,findns,findrs)

    def enable_stop(self):
        self.find.hide()
        self.stop.show()
        self.stop.grab_focus()
        self.stop.grab_add() #Just allow app to stop

    def disable_stop(self):
        self.stop.grab_remove()
        self.stop.hide()
        self.find.show()
        self.find.grab_focus() #as it already would have been in focus

    def check_user(self, question):
        self.errors.clear()
        #remove as status from previous operation could be confusing
        self.fslint_status.pop(self.status_id)
        clist = self.clists[self.mode]
        if not clist.rows:
            return False #Be consistent
                         #All actions buttons do nothing if no results
        paths = clist.selection
        if len(paths) == 0:
            self.ShowErrors(_("None selected"))
            return False
        else:
            msgbox = dlgUserInteraction(liblocation+"/fslint.glade",
                                        "UserInteraction")
            if len(paths) == 1:
                question += _(" this item?\n")
            else:
                question += _(" these %d items?\n") %  len(paths)
            msgbox.init(self, question, (N_('Yes'), N_('No')))
            msgbox.show()
            if msgbox.response != "Yes":
                return False
            return True

    #################
    # Signal handlers
    #################

    def on_fslint_destroy(self, event):
        gtk.mainquit()

    def on_addOkDir_clicked(self, event):
        self.dlgPathSel.show()
        if not self.dlgPathSel.canceled:
            self.ok_dirs.append([self.dlgPathSel.GtkWindow.get_filename()])

    def on_addBadDir_clicked(self, event):
        self.dlgPathSel.show()
        if not self.dlgPathSel.canceled:
            self.bad_dirs.append([self.dlgPathSel.GtkWindow.get_filename()])

    def on_removeOkDir_clicked(self, event):
        self.removeDirs(self.ok_dirs)

    def on_removeBadDir_clicked(self, event):
        self.removeDirs(self.bad_dirs)

    def on_chk_sn_path_toggled(self, event):
        if self.chk_sn_path.get_active():
            self.hbox_sn_paths.hide()
            self.hbox_sn_path.show()
        else:
            self.hbox_sn_path.hide()
            self.hbox_sn_paths.show()

    def on_fslint_functions_switch_page(self, widget, dummy, pagenum):
        self.fslint_status.pop(self.status_id)
        self.errors.clear()
        self.mode=pagenum
        if self.mode == self.mode_up:
            self.autoMerge.show()
        else:
            self.autoMerge.hide()
        if self.mode == self.mode_ns or self.mode == self.mode_rs: #bl in future
            self.autoClean.show()
        else:
            self.autoClean.hide()

    def on_find_clicked(self, event):

        try:
            self.errors.clear()
            errors=""
            clist = self.clists[self.mode]
            os.chdir(liblocation+"/fslint/")
            errors = self.buildFindParameters()
            if errors:
                self.ShowErrors(errors)
                return
            self.fslint_status.pop(self.status_id)
            status=""
            clist.clear()
            self.enable_stop()
            self.stopflag=0
            while gtk.events_pending(): gtk.mainiteration(gtk.FALSE)#update GUI
            clist.freeze()
            status, errors=self.__class__.find_dispatch[self.mode](self, clist)
        except self.UserAbort:
            status=_("User aborted")
        except:
            etype, emsg, etb = sys.exc_info()
            errors=str(etype)+': '+str(emsg)+'\n'
        clist.columns_autosize()
        clist.thaw()
        self.ShowErrors(errors)
        self.fslint_status.push(self.status_id, status)
        self.disable_stop()

    def on_stop_clicked(self, event):
        self.disable_stop()
        self.stopflag=1

    def on_stop_keypress(self, button, event):
        ksyms=gtk.keysyms
        abort_keys={
                    gtk.gdk.CONTROL_MASK:[ksyms.c],
                    0                   :[ksyms.space,ksyms.Escape,ksyms.Return]
                   }
        try:
            if event.keyval in abort_keys[event.state]:
                self.on_stop_clicked(event)
        except:
            pass
        return gtk.TRUE

    def on_saveAs_clicked(self, event):
        clist = self.clists[self.mode]
        if clist.rows == 0:
            return
        self.dlgPathSel.show(1)
        if not self.dlgPathSel.canceled:
            fileSaveAs = self.dlgPathSel.GtkWindow.get_filename()
            fileSaveAs = open(fileSaveAs, 'w')
            cols = clist.columns
            for row in range(clist.rows):
                 if clist.get_selectable(row) == gtk.FALSE:
                     rowtext = ''
                     for col in range(cols):
                         rowtext += clist.get_text(row,col) + '\t'
                     rowtext = rowtext[:-1] #remove trailing \t
                     fileSaveAs.write('#'+rowtext+'\n')
                 else:
                     fileSaveAs.write(clist.get_text(row,1)+'/'+
                                      clist.get_text(row,0)+'\n')

    def on_toggleSelection_clicked(self, event):
        clist = self.clists[self.mode]
        clist.freeze()
        selected = clist.selection
        if len(selected) == 0:
            clist.select_all()
        elif len(selected) == clist.rows:
            clist.unselect_all()
        else:
            clist.select_all()
            for row in selected:
                clist.unselect_row(row, 0)
        clist.thaw()

    def on_delSelected_clicked(self, event):
        if not self.check_user(_("Are you sure you want to delete")):
            return
        clist = self.clists[self.mode]
        paths_to_remove = clist.selection
        paths_to_remove.sort() #selection order irrelevant/problematic
        paths_to_remove.reverse() #so deleting works correctly
        numDeleted = 0
        clist.freeze()
        for row in paths_to_remove:
            try:
                path=clist.get_text(row,1)+'/'+clist.get_text(row,0)
                if os.path.isdir(path):
                    os.rmdir(path)
                else:
                    os.unlink(path)
                clist.remove(row)
                numDeleted += 1
            except:
                etype, emsg, etb = sys.exc_info()
                self.ShowErrors(str(emsg)+'\n')

        #Remove any redundant grouping rows
        rowver=clist.rows
        rows_left = range(rowver)
        rows_left.reverse()
        for row in rows_left:
            if clist.get_selectable(row) == gtk.FALSE:
                if (row == clist.rows-1) or (clist.get_selectable(row+1) ==
                    gtk.FALSE):
                    clist.remove(row)

        clist.columns_autosize()
        clist.thaw()
        self.fslint_status.push(self.status_id,str(numDeleted) +
                                _(" items deleted"))

    def on_autoMerge_clicked(self, event):
        self.errors.clear()
        self.fslint_status.pop(self.status_id)
        clist = self.clists[self.mode]
        if clist.rows < 3:
            return

        question=_("Are you sure you want to merge ALL files?\n")

        paths_to_leave = clist.selection
        if len(paths_to_leave):
            question+=_("(Ignoring those selected)\n")

        msgbox = dlgUserInteraction(liblocation+"/fslint.glade",
                                    "UserInteraction")
        msgbox.init(self, question, (N_('Yes'), N_('No')))
        msgbox.show()
        if msgbox.response != "Yes":
            return

        cmap = clist.get_colormap()
        GDKgrey = cmap.alloc_color("gray84")

        newGroup = 0
        for row in range(clist.rows):
            if row in paths_to_leave:
                continue
            if clist.get_selectable(row) == gtk.FALSE: #new group
                newGroup = 1
            else:
                path = clist.get_text(row,1)+'/'+clist.get_text(row,0)
                if newGroup:
                    keepfile = path
                    newGroup = 0
                else:
                    dupfile = path
                    try:
                        os.unlink(dupfile)
                        os.link(keepfile,dupfile) #Don't do this for autodelete
                        clist.set_background(row, GDKgrey)
                    except OSError:
                        self.ShowErrors(str(sys.exc_value)+'\n')

    def on_autoClean_clicked(self, event):
        if not self.check_user(_("Are you sure you want to clean")):
            return
        clist = self.clists[self.mode]
        paths_to_clean = clist.selection
        cmap = clist.get_colormap()
        GDKgrey = cmap.alloc_color("gray84")

        numCleaned = 0
        totalSaved = 0
        for row in paths_to_clean:
            try:
                path_to_clean = clist.get_text(row,1)+'/'+clist.get_text(row,0)
                startlen = os.stat(path_to_clean)[stat.ST_SIZE]
                if self.mode == self.mode_ns:
                    po, pi, pe = popen2.popen3("strip " + path_to_clean)
                    errors = pe.read()
                    po.close()
                    pi.close()
                    pe.close()
                    if len(errors):
                        raise '', errors[:-1]
                elif self.mode == self.mode_rs:
                    file_to_clean = open(path_to_clean,'r')
                    lines = open(path_to_clean,'r').readlines()
                    file_to_clean.close()
                    file_to_clean = open(path_to_clean,'w')
                    lines = map(lambda s: s.rstrip()+'\n', lines)
                    file_to_clean.writelines(lines)
                    file_to_clean.close()
                clist.set_background(row, GDKgrey)
                clist.unselect_row(row,0)
                totalSaved += startlen - os.stat(path_to_clean)[stat.ST_SIZE]
                numCleaned += 1
            except:
                etype, emsg, etb = sys.exc_info()
                self.ShowErrors(str(emsg)+'\n')
        self.fslint_status.push(self.status_id, str(numCleaned) +
                                _(" items cleaned (") +
                                locale.format("%d",totalSaved,1) +
                                _(" bytes saved)"))

import fcntl, select, signal
class fslint_backend:
    """Class representing a child process. Based on popen2.Popen3
       The main change is that this makes the new child process
       group leader (using setpgrp()) so that all children can
       be killed by sending a signal to the group. Note sh doesn't
       handle this for non interactive shells.

       Also it only supports output and the output function (getOutput)
       is non blocking returning in 100ms if nothing is read"""

    def __init__(self, cmd, bufsize=-1):
        """The parameter 'cmd' is the shell command to execute in a
        sub-process. If the 'bufsize' parameter is specified, it
        specifies the size of the I/O buffers to/from the child process."""
        self.outfd, c2pwrite = os.pipe()
        self.errfd, errin = os.pipe()
        self.pid = os.fork()
        if self.pid == 0: # Child
            os.setpgrp() #seperate group so we can kill it in the face
            os.dup2(c2pwrite, 1)
            os.dup2(errin, 2)
            self._run_child(cmd)
        os.close(c2pwrite)
        self.stdout = os.fdopen(self.outfd, 'r', bufsize)
        os.close(errin)
        self.stderr = os.fdopen(self.errfd, 'r', bufsize)
        self._makeNonBlocking(self.outfd)
        self._makeNonBlocking(self.errfd)
        self.outdata = self.errdata = ''
        self._outeof = self._erreof = 0

    def _makeNonBlocking(self, fd):
        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
        fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NDELAY)

    def _run_child(self, cmd):
        cmd = ['/bin/sh', '-c', cmd]
        for i in range(3, 256): #reasonable attempt to close duff fds
            try:
                os.close(i)
            except:
                pass
        try:
            os.execvp(cmd[0], cmd)
        finally:
            os._exit(1)

    def getOutput(self):
        """return 0 when finished
           else return 1 every 100ms
           data will be in outdata and errdata"""

        ready = select.select([self.outfd,self.errfd],[],[],.1)
        if len(ready[0]) == 0:
            return 1
        else:
            if self.outfd in ready[0]:
                outchunk = self.stdout.read()
                if outchunk == '':
                    self._outeof = 1
                self.outdata = self.outdata + outchunk
            if self.errfd in ready[0]:
                errchunk = self.stderr.read()
                if errchunk == '':
                    self._erreof = 1
                self.errdata = self.errdata + errchunk
            if self._outeof and self._erreof:
                return 0
            else:
                return 1

    def kill(self):
        os.kill(-self.pid, signal.SIGTERM) #kill whole group

    def wait(self):
        """Wait for and return the exit status of the child process."""
        pid, sts = os.waitpid(self.pid, 0)
        if pid == self.pid:
            self.sts = sts
        return self.sts

fslint_styles="""
style "action_button"
{
  fg[PRELIGHT] = { 1.0, 1.0, 1.0 }
  bg[PRELIGHT] = { 0, 0.75, 0 }
}
widget "fslint.*.find*" style "action_button"
widget "fslint.*.stop*" style "action_button"
widget "fslint.*.toggleSelection*" style "action_button"
widget "fslint.*.saveAs*" style "action_button"

style "warning_button"
{
  fg[PRELIGHT] = { 1.0, 1.0, 1.0 }
  bg[PRELIGHT] = { 0.75, 0, 0 }
}
widget "fslint.*.delSelected*" style "warning_button"
widget "fslint.*.autoMerge*" style "warning_button"
widget "fslint.*.autoClean*" style "warning_button"

style "errors"
{
  fg[NORMAL] = { 0.75, 0, 0 }
}
widget "fslint.*.errors" style "errors"
"""
gtk.rc_parse_string(fslint_styles)

FSlint = fslint(liblocation+"/fslint.glade", "fslint")

gtk.mainloop ()
