"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 ()