Version 004 (29-05-2005)
[rox-find.git] / filefind.py
blob2655d3affca2353267a5cdb5ab7f1bc4e86e6463
1 """
2 filefind.py
3 A Find in Files Utility for the ROX Desktop.
5 Copyright 2005 Kenneth Hayber <ken@hayber.us>
6 All rights reserved.
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License.
12 This program is distributed in the hope that it will be useful
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 """
22 from __future__ import generators
24 import rox, os, sys, popen2, signal
25 from rox import filer, Menu, tasks, basedir, mime
27 import gtk, os, sys, signal, re, string, socket, time, popen2, Queue, pango, gobject
30 APP_NAME = 'Find'
31 APP_SITE = 'hayber.us'
32 APP_PATH = rox.app_dir
34 MAX_HISTORY = 10
36 #Options.xml processing
37 from rox.options import Option
38 rox.setup_app_options(APP_NAME, site=APP_SITE)
39 Menu.set_save_name(APP_NAME, site=APP_SITE)
41 OPT_FIND_CMD = Option('find_cmd', 'find "$P" $R -name "$F" -exec grep -Hn $C $B "$T" "{}" \;')
42 OPT_EDIT_CMD = Option('edit_cmd', None)
44 OPT_MATCH_CASE = Option('match_case', True)
45 OPT_RECURSE_DIRS = Option('recurse_dirs', True)
46 OPT_IGNORE_BIN = Option('ignore_binary', True)
48 OPT_MATCH_CASE_ON = Option('match_case_on', '')
49 OPT_MATCH_CASE_OFF = Option('match_case_off', '-i')
51 OPT_RECURSE_DIRS_ON = Option('recurse_dirs_on', '')
52 OPT_RECURSE_DIRS_OFF = Option('recurse_dirs_off', '-maxdepth 1')
54 OPT_IGNORE_BIN_ON = Option('ignore_binary_on', '-I')
55 OPT_IGNORE_BIN_OFF = Option('ignore_binary_off', '')
58 rox.app_options.notify()
60 #if you don't like the ComboBox, but want history support
61 #you can hardcode this to False (requires gtk 2.4+)
62 use_combo_box = hasattr(gtk, 'ComboBox')
64 if use_combo_box:
65 _entryClass = gtk.ComboBoxEntry
66 else:
67 _entryClass = gtk.Entry
69 class EntryThing(_entryClass):
70 '''This class does two things.
71 1) it wraps gtk.Entry | gtk.ComboBoxEntry for backwards compatibility
72 2) it adds history support via the ComboBox or EntryCompletion
73 '''
74 def __init__(self, history=None):
75 self.history = history
76 self.liststore = gtk.ListStore(gobject.TYPE_STRING)
77 if use_combo_box:
78 _entryClass.__init__(self, self.liststore, 0)
79 else:
80 _entryClass.__init__(self)
81 try: #for gtk < 2.4 compatibility and in case nothing is saved yet
82 completion = gtk.EntryCompletion()
83 self.set_completion(completion)
84 completion.set_model(self.liststore)
85 completion.set_text_column(0)
86 except:
87 pass
89 self.load()
91 def __del__(self):
92 '''This is supposed to write the history out upon exit,
93 but it never gets called!!!
94 '''
95 self.write()
97 def get_text(self):
98 if use_combo_box:
99 return self.get_active_text()
100 else:
101 return _entryClass.get_text(self)
103 def set_text(self, text):
104 self.append_text(text)
105 if use_combo_box:
106 index = self.find_text(text)
107 if index != None:
108 self.set_active(index)
109 else:
110 _entryClass.set_text(self, text)
112 def find_text(self, text):
113 '''Check history to see if text already exists, return index or None'''
114 if not text: return
115 try:
116 item = self.liststore.get_iter_first()
117 index = 0
118 while item:
119 old_item = self.liststore.get_value(item, 0)
120 if old_item == text:
121 return index
122 item = self.liststore.iter_next(item)
123 index += 1
124 return None
125 except:
126 return None
128 def append_text(self, text):
129 '''Add item to history (if not duplicate)'''
130 if not text: return
131 if self.find_text(text) != None: return
132 try:
133 if len(self.liststore) >= MAX_HISTORY:
134 self.liststore.remove(self.liststore.get_iter_first())
135 self.liststore.append([text])
136 except:
137 pass
139 def write(self):
140 '''Write history to file in config directory'''
141 if not self.history:
142 return
144 def func(model, path, item, history_file):
145 history_file.write(model.get_value(item, 0)+'\n')
147 try:
148 save_dir = basedir.save_config_path(APP_SITE, APP_NAME)
149 history_file = file(os.path.join(save_dir, self.history), 'w')
150 self.liststore.foreach(func, history_file)
151 except:
152 pass
154 def load(self):
155 '''Read history from file and add to liststore'''
156 if not self.history:
157 return
159 try:
160 save_dir = basedir.save_config_path(APP_SITE, APP_NAME)
161 history_file = file(os.path.join(save_dir, self.history), 'r')
162 lines = history_file.readlines()
163 for x in lines:
164 self.liststore.append([x[:-1]]) #remove trailing newline
165 history_file.close()
166 except:
167 pass
171 class FindWindow(rox.Window):
172 '''A Find in Files Utility:
173 Calls external search (e.g. find | grep) tool and parses output.
174 Found files and the matching text are displayed in a list.
175 Activating items in the list opens a Text Editor, optionally jumping to
176 the specific line of text.
178 def __init__(self, in_path = None):
179 rox.Window.__init__(self)
180 self.set_title(APP_NAME)
181 self.set_default_size(550, 500)
183 self.cancel = False
184 self.running = False
185 self.selected = False
186 self.path = ''
187 self.what = ''
188 self.where = ''
190 toolbar = gtk.Toolbar()
191 toolbar.set_style(gtk.TOOLBAR_ICONS)
192 toolbar.insert_stock(gtk.STOCK_CLOSE, _('Close'), None, self.close, None, -1)
193 self.show_btn = toolbar.insert_stock(gtk.STOCK_GO_UP, _('Show file'), None, self.show_dir, None, -1)
194 self.find_btn = toolbar.insert_stock(gtk.STOCK_EXECUTE, _('Find'), None, self.start_find, None, -1)
195 self.clear_btn = toolbar.insert_stock(gtk.STOCK_CLEAR, _('Clear'), None, self.clear, None, -1)
196 self.cancel_btn = toolbar.insert_stock(gtk.STOCK_STOP, _('Cancel'), None, self.cancel_find, None, -1)
197 toolbar.insert_stock(gtk.STOCK_PREFERENCES, _('Settings'), None, self.edit_options, None, -1)
199 self.show_btn.set_sensitive(False)
200 self.find_btn.set_sensitive(False)
201 self.clear_btn.set_sensitive(False)
202 self.cancel_btn.set_sensitive(False)
204 # Create layout, pack and show widgets
205 table = gtk.Table(5, 2, False)
206 x_pad = 2
207 y_pad = 1
209 path = EntryThing('path')
210 table.attach(gtk.Label(_('Path')), 0, 1, 2, 3, 0, 0, 4, y_pad)
211 table.attach(path, 1, 2, 2, 3, gtk.EXPAND|gtk.FILL, 0, x_pad, y_pad)
212 if hasattr(gtk, 'FileChooserDialog'):
213 self.browse = gtk.Button(label='...')
214 self.browse.connect('clicked', self.browser, path)
215 table.attach(self.browse, 2, 3, 2, 3, 0, 0, x_pad, y_pad)
217 what = EntryThing('pattern')
218 table.attach(gtk.Label(_('Pattern')), 0, 1, 3, 4, 0, 0, 4, y_pad)
219 table.attach(what, 1, 2, 3, 4, gtk.EXPAND|gtk.FILL, 0, x_pad, y_pad)
221 where = EntryThing('files')
222 table.attach(gtk.Label(_('Files')), 0, 1, 4, 5, 0, 0, 4, y_pad)
223 table.attach(where, 1, 2, 4, 5, gtk.EXPAND|gtk.FILL, 0, x_pad, y_pad)
225 hbox = gtk.HBox()
226 hbox.set_spacing(5)
228 self.ignore_binary = gtk.CheckButton(label=_('Ignore binary files'))
229 self.ignore_binary.set_active(bool(OPT_IGNORE_BIN.int_value))
230 hbox.pack_end(self.ignore_binary, False, False)
232 self.match_case = gtk.CheckButton(label=_('Match case'))
233 self.match_case.set_active(bool(OPT_MATCH_CASE.int_value))
234 hbox.pack_end(self.match_case,False, False)
236 self.recurse_dirs = gtk.CheckButton(label=_('Search subdirectories'))
237 self.recurse_dirs.set_active(bool(OPT_RECURSE_DIRS.int_value))
238 hbox.pack_end(self.recurse_dirs,False, False)
240 swin = gtk.ScrolledWindow()
241 swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
242 swin.set_shadow_type(gtk.SHADOW_IN)
244 self.store = gtk.ListStore(str, int, str)
245 view = gtk.TreeView(self.store)
246 self.view = view
247 swin.add(view)
248 view.set_rules_hint(True)
250 cell = gtk.CellRendererText()
251 try: #for pre gtk 2.6.0 support
252 cell.set_property('ellipsize_set', True)
253 cell.set_property('ellipsize', pango.ELLIPSIZE_START)
254 except: pass
255 column = gtk.TreeViewColumn(_('Filename'), cell, text = 0)
256 view.append_column(column)
257 column.set_resizable(True)
258 column.set_reorderable(True)
260 cell = gtk.CellRendererText()
261 column = gtk.TreeViewColumn(_('Line'), cell, text = 1)
262 view.append_column(column)
263 column.set_resizable(True)
264 column.set_reorderable(True)
266 cell = gtk.CellRendererText()
267 column = gtk.TreeViewColumn(_('Text'), cell, text = 2)
268 view.append_column(column)
269 column.set_resizable(True)
270 column.set_reorderable(True)
272 view.connect('row-activated', self.activate)
273 self.selection = view.get_selection()
274 self.selection.connect('changed', self.set_selection)
276 vbox = gtk.VBox()
277 self.add(vbox)
278 vbox.pack_start(toolbar, False, False)
279 vbox.pack_start(table, False, False)
280 vbox.pack_start(hbox, False, False)
281 vbox.pack_start(swin, True, True)
282 vbox.show_all()
284 what.connect('changed', self.entry_changed)
285 where.connect('changed', self.entry_changed)
286 path.connect('changed', self.entry_changed)
288 self.connect('key-press-event', self.key_press)
290 if in_path:
291 path.set_text(in_path)
293 self.connect('delete_event', self.delete_event)
294 self.path_entry = path
295 self.what_entry = what
296 self.where_entry = where
299 def start_find(self, *args):
300 '''Execute the find command after applying optional paramters'''
301 self.cancel = False
302 self.running = True
303 self.set_sensitives()
305 self.path_entry.append_text(self.path)
306 self.what_entry.append_text(self.what)
307 self.where_entry.append_text(self.where)
309 cmd = OPT_FIND_CMD.value
310 #long options (deprecated)
311 cmd = string.replace(cmd, '$Path', self.path)
312 cmd = string.replace(cmd, '$Files', self.where)
313 cmd = string.replace(cmd, '$Text', self.what)
314 #short options
315 cmd = string.replace(cmd, '$P', self.path)
316 cmd = string.replace(cmd, '$F', self.where)
317 cmd = string.replace(cmd, '$T', self.what)
319 if self.match_case.get_active():
320 cmd = string.replace(cmd, '$C', OPT_MATCH_CASE_ON.value)
321 else:
322 cmd = string.replace(cmd, '$C', OPT_MATCH_CASE_OFF.value)
324 if self.ignore_binary.get_active():
325 cmd = string.replace(cmd, '$B', OPT_IGNORE_BIN_ON.value)
326 else:
327 cmd = string.replace(cmd, '$B', OPT_IGNORE_BIN_OFF.value)
329 if self.recurse_dirs.get_active():
330 cmd = string.replace(cmd, '$R', OPT_RECURSE_DIRS_ON.value)
331 else:
332 cmd = string.replace(cmd, '$R', OPT_RECURSE_DIRS_OFF.value)
334 thing = popen2.Popen4(cmd)
335 tasks.Task(self.get_status(thing))
338 def cancel_find(self, *args):
339 self.cancel = True
340 self.running = False
341 self.set_sensitives()
344 def clear(self, *args):
345 self.store.clear()
346 self.selected = False
347 self.set_sensitives()
350 def get_status(self, thing):
351 '''Parse the ouput of the find command and fill the listbox.'''
352 outfile = thing.fromchild
353 while True:
354 blocker = tasks.InputBlocker(outfile)
355 yield blocker
356 if self.cancel:
357 os.kill(thing.pid, signal.SIGKILL)
358 self.cancel = False
359 return
360 line = outfile.readline()
361 if line:
362 self.set_sensitives()
363 iter = self.store.append(None)
364 try:
365 (filename, lineno, text) = string.split(line, ':', 2)
366 self.store.set(iter, 0, filename, 1, int(lineno), 2, text[:-1])
367 except:
368 self.store.set(iter, 2, line[:-1])
369 else:
370 code = thing.wait()
371 if code:
372 rox.info(_('There was a problem with this search'))
373 break
375 self.running = False
376 self.set_sensitives()
377 if not len(self.store):
378 rox.info(_('Your search returned no results'))
381 def entry_changed(self, button):
382 self.path = self.path_entry.get_text()
383 self.what = self.what_entry.get_text()
384 self.where = self.where_entry.get_text()
385 self.set_sensitives()
388 def set_sensitives(self):
389 if len(self.what) and len(self.where) and len(self.path) and not self.running:
390 self.find_btn.set_sensitive(True)
391 else:
392 self.find_btn.set_sensitive(False)
394 self.clear_btn.set_sensitive(bool(len(self.store)))
395 self.cancel_btn.set_sensitive(self.running)
396 self.show_btn.set_sensitive(self.selected)
399 def browser(self, button, path_widget):
400 browser = gtk.FileChooserDialog(title=_('Select folder'), buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT))
401 if not len(self.path):
402 self.path = os.path.expanduser('~')
403 browser.set_current_folder(self.path)
404 browser.set_action(gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
405 if browser.run() != gtk.RESPONSE_CANCEL:
406 try:
407 self.path = browser.get_filename()
408 path_widget.set_text(self.path)
409 except:
410 rox.report_exception()
411 browser.hide()
414 def set_selection(self, *args):
415 self.selected = True
416 self.set_sensitives()
419 def key_press(self, text, kev):
420 if kev.keyval == gtk.keysyms.Return or kev.keyval == gtk.keysyms.KP_Enter:
421 if len(self.what) and len(self.where) and len(self.path) and not self.running:
422 self.start_find()
423 return 1
424 return 0
427 def activate(self, *args):
428 '''Launch Editor for selected file/text'''
430 def get_type_handler(dir, mime_type):
431 """Lookup the ROX-defined run action for a given mime type."""
432 path = basedir.load_first_config('MIME-types')
433 handler = os.path.join(path, '%s_%s' % (mime_type.media, mime_type.subtype))
434 if os.path.exists(handler):
435 return handler
436 else: #fall back to the base handler if no subtype handler exists
437 handler = os.path.join(path, '%s' % (mime_type.media,), '')
438 if os.path.exists(handler):
439 return handler
440 else:
441 return None
443 model, iter = self.view.get_selection().get_selected()
444 if iter:
445 filename = model.get_value(iter, 0)
446 line = model.get_value(iter, 1)
448 if len(OPT_EDIT_CMD.value):
449 cmd = OPT_EDIT_CMD.value
450 cmd = string.replace(cmd, '$File', filename)
451 cmd = string.replace(cmd, '$Line', str(line))
452 popen2.Popen4(cmd)
453 else: #use the ROX defined text handler
454 mime_type = rox.mime.lookup('text/plain')
455 handler = get_type_handler('MIME-types', mime_type)
456 handler_appdir = os.path.join(handler, 'AppRun')
457 if os.path.isdir(handler) and os.path.isfile(handler_appdir):
458 handler = handler_appdir
459 if handler:
460 popen2.Popen4('%s "%s"' % (handler, filename))
461 else:
462 rox.info(_('There is no run action defined for text files!'))
465 def button_press(self, text, event):
466 '''Popup menu handler'''
467 if event.button != 3:
468 return 0
469 self.menu.popup(self, event)
470 return 1
473 def show_dir(self, *args):
474 ''' Pops up a filer window. '''
475 model, iter = self.view.get_selection().get_selected()
476 if iter:
477 filename = model.get_value(iter, 0)
478 filer.show_file(filename)
481 def edit_options(self, *args):
482 '''Show Options dialog'''
483 rox.edit_options()
486 def get_options(self):
487 '''Get changed Options'''
488 pass
491 def delete_event(self, ev, e1):
492 '''Bye-bye'''
493 self.close()
496 def close(self, *args):
497 '''We're outta here!'''
498 self.path_entry.write()
499 self.what_entry.write()
500 self.where_entry.write()
501 self.destroy()