3
# Copyright (C) 2006 Christian Dywan <software at twotoasts dot de>
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.
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.
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.
26
print "Error: It appears that pyGTK2 is missing."
27
print "Please install it to run this program."
33
print "Error: It appears that Glade-support is missing."
34
print "Please install a version of Gtk+ with Glade-support."
40
print "Warning: The module xdg was not found. So file icons will be disabled."
41
print "It is recommended to install python-xdg."
45
from optparse import OptionParser
57
"""Create the main window."""
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'
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
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()
88
keywords = keywords + arg
89
if arg <> args[len(args) - 1]:
90
keywords = keywords + ' '
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)):
103
# Load interface from glade file
105
self.xml = gtk.glade.XML(os.path.join(glade_path, glade_file))
106
self.xml.signal_autoconnect(self)
109
print "The glade file could not be found."
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')
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])
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)
143
# Set some initial values
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)
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))
161
self.entry_find_text.set_text(keywords)
162
self.button_find.activate()
164
# -- helper functions --
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)
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)
180
def format_size(self, size):
182
return "%s GB" % (size / 2 ** 30)
184
return "%s MB" % (size / 2 ** 20)
186
return "%s kB" % (size / 2 ** 10)
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)
199
def open_file(self, filename):
200
"""Open the file with its default app or the file manager"""
202
if stat.S_ISDIR(os.stat(filename).st_mode):
203
command = '%s "%s"' % (self.options.fileman, filename)
205
command = '%s "%s"' % (self.open_wrapper, filename)
207
subprocess.Popen(command, shell=True)
209
print 'Error: Could not open ' + filename + '.'
210
print 'Note: The wrapper was ' + self.open_wrapper + '.'
212
def string_wild_match(self, string, keyword, exact):
213
return fnmatch.fnmatch(string.lower(), '*' + keyword.lower() + '*')
216
"""Do the actual search."""
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()
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()
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()
241
# Generate search command
246
command = '%s %s -ignore_readdir_race -noleaf' % (command, folder)
248
command = '%s -iwholename' % command
250
command = '%s -iwholename' % command
251
command = '%s "*%s*"' % (command, keyword)
253
elif method in ('locate', 'slocate'):
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)
264
command = '%s "%s"' % (command, keyword)
266
# Set display options
267
if not self.options.icons_large and not self.options.thumbnails:
271
if not self.options.time_iso:
272
time_format = '%x %X'
274
time_format = '%Y-%m-%w %H:%M %Z'
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:
282
filename = filename.split(os.linesep)[0]
283
if filename[:7] == 'file://':
284
filename = filename[7:]
285
path, name = os.path.split(filename)
288
path_valid = self.string_wild_match(name, keyword, exact)
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)
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])
302
listmodel.append([icon, '%s (%s) %s%s%s%s'
303
% (name, size, os.linesep, modified, os.linesep, path), -1, name, path])
305
pass # ignore inaccessible or outdated files
306
self.treeview_files.set_model(listmodel)
308
if len(listmodel) == 0:
309
if search_process.poll() <> 0 and method != 'find':
310
message = 'Fatal error, search was aborted.'
312
message = 'No files were found.'
313
listmodel.append([None, message, -1, '', ''])
314
status = 'No files found.'
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)
320
self.window_search.window.set_cursor(None)
321
self.button_find.set_label('gtk-find')
324
def file_is_hidden(self, filename):
325
"""Determine if a file is hidden or in a hidden folder"""
327
if filename == '': return 0
328
path, name = os.path.split(filename)
331
for folder in path.split(os.path.sep):
337
def get_icon_pixbuf(self, name, icon_size=0):
339
return self.icon_cache[name]
343
icon = self.icon_theme.load_icon(name, icon_size, 0)
344
self.icon_cache[name] = icon
347
def get_thumbnail(self, path, icon_size=0, use_mime=1):
348
"""Try to fetch a small thumbnail."""
350
filename = '%s%s.png' % (self.folder_thumbnails, md5.new('file://' + path).hexdigest())
352
return gtk.gdk.pixbuf_new_from_file(filename)
354
return self.get_file_icon(path, icon_size, use_mime)
356
def get_file_icon(self, path, icon_size=0 ,use_mime=1):
357
"""Retrieve the file icon."""
360
is_folder = stat.S_ISDIR(os.stat(path).st_mode)
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)
374
# Then try generic icon
375
icon_name = '%s-x-generic' % mime.media
376
return self.get_icon_pixbuf(icon_name, icon_size)
379
icon_name = 'gnome-fs-regular'
381
icon_name = 'gnome-fs-regular'
382
return self.get_icon_pixbuf(icon_name, icon_size)
385
def on_window_search_destroy(self, window):
388
def on_button_close_clicked(self, button):
389
self.window_search.destroy()
391
def on_checkbox_find_limit_toggled(self, checkbox):
392
self.spin_find_limit.set_sensitive(checkbox.get_active())
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')
400
def on_button_find_clicked(self, button):
401
"""Initiate the search thread."""
403
if self.button_find.get_label() == 'gtk-find' and self.entry_find_text.get_text() <> '':
406
gobject.idle_add(task.next)
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)
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)
419
def on_menu_open_activate(self, menu):
420
folder, filename = self.get_selected_filename()
421
self.open_file(os.path.join(folder, filename))
423
def on_menu_goto_activate(self, menu):
424
folder, filename = self.get_selected_filename()
425
self.open_file(folder)
b'\\ No newline at end of file'