~ubuntu-branches/ubuntu/trusty/catfish/trusty

« back to all changes in this revision

Viewing changes to catfish.py

  • Committer: Bazaar Package Importer
  • Author(s): Cody A.W. Somerville
  • Date: 2007-01-12 22:54:55 UTC
  • Revision ID: james.westby@ubuntu.com-20070112225455-tt8bg1ur2984rix3
Tags: upstream-0.1
ImportĀ upstreamĀ versionĀ 0.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
 
 
3
# Copyright (C) 2006 Christian Dywan <software at twotoasts dot de>
 
4
#
 
5
# This program is free software; you can redistribute it and/or
 
6
# modify it under the terms of the GNU General Public License
 
7
# as published by the Free Software Foundation; either version 2
 
8
# of the License, or (at your option) any later version.
 
9
#
 
10
# This program is distributed in the hope that it will be useful,
 
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
13
# GNU General Public License for more details.
 
14
#
 
15
# You should have received a copy of the GNU General Public License
 
16
# along with this program; if not, write to the Free Software
 
17
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 
18
 
 
19
import sys
 
20
 
 
21
try:
 
22
    import pygtk
 
23
    pygtk.require('2.0')
 
24
    import gtk
 
25
except:
 
26
    print "Error: It appears that pyGTK2 is missing."
 
27
    print "Please install it to run this program."
 
28
    sys.exit()
 
29
 
 
30
try:
 
31
    import gtk.glade
 
32
except:
 
33
    print "Error: It appears that Glade-support is missing."
 
34
    print "Please install a version of Gtk+ with Glade-support."
 
35
    sys.exit()
 
36
 
 
37
try:
 
38
    import xdg.Mime
 
39
except:
 
40
    print "Warning: The module xdg was not found. So file icons will be disabled."
 
41
    print "It is recommended to install python-xdg."
 
42
 
 
43
import os
 
44
import os.path
 
45
from optparse import OptionParser
 
46
import stat
 
47
import time
 
48
import gobject
 
49
import md5
 
50
import subprocess
 
51
import fnmatch
 
52
 
 
53
app_version = '0.1'
 
54
 
 
55
class catfish:
 
56
    def __init__(self):
 
57
        """Create the main window."""
 
58
 
 
59
        # Guess default fileman
 
60
        if os.environ.get('DESKTOP_SESSION', os.environ.get('GDMSESSION', ''))[:4] == 'xfce':
 
61
            default_fileman = 'Thunar'
 
62
            self.open_wrapper = 'exo-open'
 
63
        else:
 
64
            default_fileman = 'Nautilus'
 
65
            # Guess suitable file open wrapper
 
66
            for path in os.environ.get('PATH', '/usr/bin').split(os.pathsep):
 
67
                for wrapper in ['gnome-open', 'exo-open', 'xdg-open']:
 
68
                    if os.path.exists(os.path.join(path, wrapper)):
 
69
                        self.open_wrapper = wrapper
 
70
                        break
 
71
 
 
72
        # Parse command line options
 
73
        parser = OptionParser(usage='usage: catfish [options] keywords',
 
74
            version='catfish v' + app_version)
 
75
        parser.add_option('', '--large-icons', action='store_true', dest='icons_large', help='Use large icons')
 
76
        parser.add_option('', '--thumbnails', action='store_true', dest='thumbnails', help='Use thumbnails')
 
77
        parser.add_option('', '--iso-time', action='store_true', dest='time_iso', help='Display time in iso format')
 
78
        parser.add_option('', '--limit', type='int', metavar='LIMIT', dest='limit_results',
 
79
            help='Limit number of results')
 
80
        parser.add_option('', '--path', help='Search in folder PATH')
 
81
        parser.add_option('', '--fileman', help='Use FILEMAN as filemanager')
 
82
        parser.add_option('', '--wrapper', metavar='WRAPPER', dest='open_wrapper', help='Use WRAPPER to open files')
 
83
        parser.set_defaults(icons_large=0, thumbnails=0, time_iso=0,
 
84
            limit_results=0, path=os.path.expanduser('~'), fileman=default_fileman)
 
85
        self.options, args = parser.parse_args()
 
86
        keywords = ''
 
87
        for arg in args:
 
88
            keywords = keywords + arg
 
89
            if arg <> args[len(args) - 1]:
 
90
                keywords = keywords + ' '
 
91
 
 
92
        # Guess location of glade file
 
93
        glade_file = 'catfish.glade'
 
94
        glade_path = os.getcwd()
 
95
        if not os.path.exists(os.path.join(glade_path, glade_file)):
 
96
            glade_path = os.path.dirname(sys.argv[0])
 
97
            if not os.path.exists(os.path.join(glade_path, glade_file)):
 
98
                for path in os.environ.get('XDG_DATA_DIRS').split(os.pathsep):
 
99
                    if os.path.exists(os.path.join(path, glade_file)):
 
100
                        glade_path = path
 
101
                        break
 
102
 
 
103
        # Load interface from glade file
 
104
        try:
 
105
            self.xml = gtk.glade.XML(os.path.join(glade_path, glade_file))
 
106
            self.xml.signal_autoconnect(self)
 
107
        except:
 
108
            print "Error:"
 
109
            print "The glade file could not be found."
 
110
            sys.exit()
 
111
 
 
112
        # Prepare significant widgets
 
113
        self.window_search = self.xml.get_widget('window_search')
 
114
        self.entry_find_text = self.xml.get_widget('entry_find_text')
 
115
        self.checkbox_find_exact = self.xml.get_widget('checkbox_find_exact')
 
116
        self.checkbox_find_hidden = self.xml.get_widget('checkbox_find_hidden')
 
117
        self.checkbox_find_limit = self.xml.get_widget('checkbox_find_limit')
 
118
        self.spin_find_limit = self.xml.get_widget('spin_find_limit')
 
119
        self.combobox_find_method = self.xml.get_widget('combobox_find_method')
 
120
        self.button_find_folder = self.xml.get_widget('button_find_folder')
 
121
        self.button_find = self.xml.get_widget('button_find')
 
122
        self.button_findbar_hide = self.xml.get_widget('button_findbar_hide')
 
123
        self.scrolled_findbar = self.xml.get_widget('scrolled_findbar')
 
124
        self.treeview_files = self.xml.get_widget('treeview_files')
 
125
        self.menu_file = self.xml.get_widget('menu_file')
 
126
        self.statusbar = self.xml.get_widget('statusbar')
 
127
 
 
128
        # Retrieve available search methods
 
129
        listmodel = gtk.ListStore(gobject.TYPE_STRING)
 
130
        methods = ('find', 'locate', 'slocate', 'tracker-search', 'beagle-query')
 
131
        bin_dirs = os.environ.get('PATH', '/usr/bin').split(os.pathsep)
 
132
        for method in methods:
 
133
            for path in bin_dirs:
 
134
                if os.path.exists(os.path.join(path, method)):
 
135
                    if not os.path.islink(os.path.join(path, method)):
 
136
                        listmodel.append([method])
 
137
                        break
 
138
        self.combobox_find_method.set_model(listmodel)
 
139
        self.combobox_find_method.set_text_column(0)
 
140
        self.combobox_find_method.child.set_text(listmodel[0][0])
 
141
        self.combobox_find_method.child.set_editable(0)
 
142
 
 
143
        # Set some initial values
 
144
        self.icon_cache = {}
 
145
        self.icon_theme = gtk.icon_theme_get_default()
 
146
        if self.options.limit_results:
 
147
            self.checkbox_find_limit.set_active(1)
 
148
            self.checkbox_find_limit.toggled()
 
149
            self.spin_find_limit.set_value(self.options.limit_results)
 
150
        self.folder_thumbnails = os.path.expanduser('~/.thumbnails/normal/')
 
151
        self.button_find_folder.set_filename(self.options.path)
 
152
 
 
153
        # Initialize treeview
 
154
        self.treeview_files.append_column(gtk.TreeViewColumn('Icon', gtk.CellRendererPixbuf(), pixbuf=0))
 
155
        self.treeview_files.append_column(self.new_column('Filename', 1))
 
156
        if not self.options.icons_large and not self.options.thumbnails:
 
157
            self.treeview_files.append_column(self.new_column('Size', 2, 'filesize'))
 
158
            self.treeview_files.append_column(self.new_column('Last modified', 3))
 
159
            self.treeview_files.append_column(self.new_column('Location', 4))
 
160
 
 
161
        self.entry_find_text.set_text(keywords)
 
162
        self.button_find.activate()
 
163
 
 
164
# -- helper functions --
 
165
 
 
166
    def new_column(self, label, id, special=None):
 
167
        cell_renderer = gtk.CellRendererText()
 
168
        column = gtk.TreeViewColumn(label, cell_renderer, text=id)
 
169
        if special == 'filesize':
 
170
            column.set_cell_data_func(cell_renderer, self.cell_data_func_filesize, id)
 
171
        column.set_sort_column_id(id)
 
172
        column.set_resizable(1)
 
173
        return column
 
174
 
 
175
    def cell_data_func_filesize(self, column, cell_renderer, tree_model, iter, id):
 
176
        filesize = self.format_size(int(tree_model.get_value(iter, id)))
 
177
        cell_renderer.set_property('text', filesize)
 
178
        return
 
179
 
 
180
    def format_size(self, size):
 
181
        if size > 2 ** 30:
 
182
            return "%s GB" % (size / 2 ** 30)
 
183
        elif size > 2 ** 20:
 
184
            return "%s MB" % (size / 2 ** 20)
 
185
        elif size > 2 ** 10:
 
186
            return "%s kB" % (size / 2 ** 10)
 
187
        elif size > -1:
 
188
            return "%s B" % size
 
189
        else:
 
190
            return ""
 
191
 
 
192
    def get_selected_filename(self):
 
193
        model = self.treeview_files.get_model()
 
194
        path = self.treeview_files.get_cursor()[0]
 
195
        if not self.options.icons_large and not self.options.thumbnails:
 
196
            return model.get_value(model.get_iter(path), 4), model.get_value(model.get_iter(path), 1)
 
197
        return model.get_value(model.get_iter(path), 4), model.get_value(model.get_iter(path), 3)
 
198
 
 
199
    def open_file(self, filename):
 
200
        """Open the file with its default app or the file manager"""
 
201
 
 
202
        if stat.S_ISDIR(os.stat(filename).st_mode):
 
203
            command = '%s "%s"' % (self.options.fileman, filename)
 
204
        else:
 
205
            command = '%s "%s"' % (self.open_wrapper, filename)
 
206
        try:
 
207
            subprocess.Popen(command, shell=True)
 
208
        except:
 
209
            print 'Error: Could not open ' + filename + '.'
 
210
            print 'Note:  The wrapper was ' + self.open_wrapper + '.'
 
211
 
 
212
    def string_wild_match(self, string, keyword, exact):
 
213
        return fnmatch.fnmatch(string.lower(), '*' + keyword.lower() + '*')
 
214
 
 
215
    def find(self):
 
216
        """Do the actual search."""
 
217
 
 
218
        self.button_find.set_label('gtk-cancel')
 
219
        self.window_search.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
 
220
        self.window_search.set_title('Searching for "%s"' % self.entry_find_text.get_text())
 
221
        self.statusbar.push(self.statusbar.get_context_id('results'), 'Searching...')
 
222
        while gtk.events_pending(): gtk.main_iteration()
 
223
 
 
224
        # Reset treeview
 
225
        listmodel = gtk.ListStore(gtk.gdk.Pixbuf, gobject.TYPE_STRING,
 
226
            gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_STRING)
 
227
        self.treeview_files.set_model(listmodel)
 
228
        self.treeview_files.columns_autosize()
 
229
 
 
230
        # Retrieve search parameters
 
231
        keyword = self.entry_find_text.get_text().replace(' ', '*')
 
232
        method = self.combobox_find_method.child.get_text()
 
233
        folder = self.button_find_folder.get_filename()
 
234
        exact = self.checkbox_find_exact.get_active()
 
235
        hidden = self.checkbox_find_hidden.get_active()
 
236
        if self.checkbox_find_limit.get_active():
 
237
            limit = self.spin_find_limit.get_value()
 
238
        else:
 
239
            limit = -1
 
240
 
 
241
        # Generate search command
 
242
        if keyword != '':
 
243
            command = method
 
244
            path_check = 1
 
245
            if method == 'find':
 
246
                command = '%s %s -ignore_readdir_race -noleaf' % (command, folder)
 
247
                if exact:
 
248
                    command = '%s -iwholename' % command
 
249
                else:
 
250
                    command = '%s -iwholename' % command
 
251
                command = '%s "*%s*"' % (command, keyword)
 
252
                path_check = 0
 
253
            elif method in ('locate', 'slocate'):
 
254
                if not exact:
 
255
                    command += ' -i'
 
256
                if limit and hidden:
 
257
                    command += ' -n %s' % int(limit)
 
258
                command = '%s "%s"' % (command, keyword)
 
259
            elif method == 'tracker-search' and limit and hidden:
 
260
                command = '%s -l %s "%s"' % (command, limit, keyword)
 
261
            elif method == 'beagle-query' and limit and hidden:
 
262
                command = '%s --max-hits=%s "%s"' % (command, limit, keyword)
 
263
            else:
 
264
                command = '%s "%s"' % (command, keyword)
 
265
 
 
266
            # Set display options
 
267
            if not self.options.icons_large and not self.options.thumbnails:
 
268
                icon_size = 24
 
269
            else:
 
270
                icon_size = 0
 
271
            if not self.options.time_iso:
 
272
                time_format = '%x %X'
 
273
            else:
 
274
                time_format = '%Y-%m-%w %H:%M %Z'
 
275
 
 
276
            # Run search command and capture the results
 
277
            search_process = subprocess.Popen(command, stdout=subprocess.PIPE, bufsize=1, shell=True)
 
278
            for filename in search_process.stdout:
 
279
                while gtk.events_pending(): gtk.main_iteration()
 
280
                if self.abort_find or len(listmodel) == limit:
 
281
                    break
 
282
                filename = filename.split(os.linesep)[0]
 
283
                if filename[:7] == 'file://':
 
284
                    filename = filename[7:]
 
285
                path, name = os.path.split(filename)
 
286
                try:
 
287
                    if path_check:
 
288
                        path_valid = self.string_wild_match(name, keyword, exact)
 
289
                    else:
 
290
                        path_valid = 1
 
291
                    if path_valid  and (not self.file_is_hidden(filename) or hidden):
 
292
                        if self.options.thumbnails:
 
293
                            icon = self.get_thumbnail(filename, icon_size)
 
294
                        else:
 
295
                            icon = self.get_file_icon(filename, icon_size)
 
296
                        filestat = os.stat(filename)
 
297
                        size = filestat.st_size
 
298
                        modified = time.strftime(time_format, time.gmtime(filestat.st_mtime))
 
299
                        if not self.options.icons_large and not self.options.thumbnails:
 
300
                            listmodel.append([icon, name, size, modified, path])
 
301
                        else:
 
302
                            listmodel.append([icon, '%s (%s) %s%s%s%s'
 
303
                                % (name, size, os.linesep, modified, os.linesep, path), -1, name, path])
 
304
                except:
 
305
                    pass # ignore inaccessible or outdated files
 
306
                self.treeview_files.set_model(listmodel)
 
307
                yield True
 
308
            if len(listmodel) == 0:
 
309
                if search_process.poll() <> 0 and method != 'find':
 
310
                    message = 'Fatal error, search was aborted.'
 
311
                else:
 
312
                    message = 'No files were found.'
 
313
                listmodel.append([None, message, -1, '', ''])
 
314
                status = 'No files found.'
 
315
            else:
 
316
                status = '%s files found.' % len(listmodel)
 
317
            self.statusbar.push(self.statusbar.get_context_id('results'), status)
 
318
        self.treeview_files.set_model(listmodel)
 
319
 
 
320
        self.window_search.window.set_cursor(None)
 
321
        self.button_find.set_label('gtk-find')
 
322
        yield False
 
323
 
 
324
    def file_is_hidden(self, filename):
 
325
        """Determine if a file is hidden or in a hidden folder"""
 
326
 
 
327
        if filename == '': return 0
 
328
        path, name = os.path.split(filename)
 
329
        if name[0] == '.':
 
330
            return 1
 
331
        for folder in path.split(os.path.sep):
 
332
            if len(folder):
 
333
                if folder[0] == '.':
 
334
                    return 1
 
335
        return 0
 
336
 
 
337
    def get_icon_pixbuf(self, name, icon_size=0):
 
338
        try:
 
339
            return self.icon_cache[name]
 
340
        except KeyError:
 
341
            if icon_size == 0:
 
342
                icon_size = 48
 
343
            icon = self.icon_theme.load_icon(name, icon_size, 0)
 
344
            self.icon_cache[name] = icon
 
345
            return icon
 
346
 
 
347
    def get_thumbnail(self, path, icon_size=0, use_mime=1):
 
348
        """Try to fetch a small thumbnail."""
 
349
 
 
350
        filename = '%s%s.png' % (self.folder_thumbnails, md5.new('file://' + path).hexdigest())
 
351
        try:
 
352
            return gtk.gdk.pixbuf_new_from_file(filename)
 
353
        except:
 
354
            return self.get_file_icon(path, icon_size, use_mime)
 
355
 
 
356
    def get_file_icon(self, path, icon_size=0 ,use_mime=1):
 
357
        """Retrieve the file icon."""
 
358
 
 
359
        try:
 
360
            is_folder = stat.S_ISDIR(os.stat(path).st_mode)
 
361
        except:
 
362
            is_folder = 0
 
363
        if is_folder:
 
364
            icon_name='folder'
 
365
        else:
 
366
            if use_mime:
 
367
                try:
 
368
                    # Get icon from mimetype
 
369
                    mime = xdg.Mime.get_type(path)
 
370
                    icon_name = 'gnome-mime-%s-%s' % (mime.media, mime.subtype)
 
371
                    return self.get_icon_pixbuf(icon_name, icon_size)
 
372
                except:
 
373
                    try:
 
374
                        # Then try generic icon
 
375
                        icon_name = '%s-x-generic' % mime.media
 
376
                        return self.get_icon_pixbuf(icon_name, icon_size)
 
377
                    except:
 
378
                        # Use default icon
 
379
                        icon_name = 'gnome-fs-regular'
 
380
            else:
 
381
                icon_name = 'gnome-fs-regular'
 
382
        return self.get_icon_pixbuf(icon_name, icon_size)
 
383
 
 
384
# -- events --
 
385
    def on_window_search_destroy(self, window):
 
386
        gtk.main_quit()
 
387
 
 
388
    def on_button_close_clicked(self, button):
 
389
        self.window_search.destroy()
 
390
 
 
391
    def on_checkbox_find_limit_toggled(self, checkbox):
 
392
        self.spin_find_limit.set_sensitive(checkbox.get_active())
 
393
 
 
394
    def on_combobox_find_method_changed(self, combobox):
 
395
        self.checkbox_find_exact.set_sensitive(
 
396
            self.combobox_find_method.child.get_text() in ('find', 'locate', 'slocate'))
 
397
        self.button_find_folder.set_sensitive(
 
398
            self.combobox_find_method.child.get_text() == 'find')
 
399
 
 
400
    def on_button_find_clicked(self, button):
 
401
        """Initiate the search thread."""
 
402
 
 
403
        if self.button_find.get_label() == 'gtk-find' and self.entry_find_text.get_text() <> '':
 
404
            self.abort_find = 0
 
405
            task = self.find()
 
406
            gobject.idle_add(task.next)
 
407
        else:
 
408
            self.abort_find = 1
 
409
 
 
410
    def on_treeview_files_row_activated(self, treeview, path, view_column):
 
411
        folder, filename = self.get_selected_filename()
 
412
        filename = os.path.join(folder, filename)
 
413
        self.open_file(filename)
 
414
 
 
415
    def on_treeview_files_button_pressed(self, treeview, event):        
 
416
        if event.button == 3:
 
417
            self.menu_file.popup(None, None, None, event.button, event.time)
 
418
 
 
419
    def on_menu_open_activate(self, menu):
 
420
        folder, filename = self.get_selected_filename()
 
421
        self.open_file(os.path.join(folder, filename))
 
422
 
 
423
    def on_menu_goto_activate(self, menu):
 
424
        folder, filename = self.get_selected_filename()
 
425
        self.open_file(folder)
 
426
 
 
427
catfish()
 
428
gtk.main()
 
 
b'\\ No newline at end of file'