3 A Find in Files Utility for the ROX Desktop.
5 Copyright 2005 Kenneth Hayber <ken@hayber.us>
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
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
31 APP_SITE
= 'hayber.us'
32 APP_PATH
= rox
.app_dir
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')
65 _entryClass
= gtk
.ComboBoxEntry
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
74 def __init__(self
, history
=None):
75 self
.history
= history
76 self
.liststore
= gtk
.ListStore(gobject
.TYPE_STRING
)
78 _entryClass
.__init
__(self
, self
.liststore
, 0)
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)
92 '''This is supposed to write the history out upon exit,
93 but it never gets called!!!
99 return self
.get_active_text()
101 return _entryClass
.get_text(self
)
103 def set_text(self
, text
):
104 self
.append_text(text
)
106 index
= self
.find_text(text
)
108 self
.set_active(index
)
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'''
116 item
= self
.liststore
.get_iter_first()
119 old_item
= self
.liststore
.get_value(item
, 0)
122 item
= self
.liststore
.iter_next(item
)
128 def append_text(self
, text
):
129 '''Add item to history (if not duplicate)'''
131 if self
.find_text(text
) != None: return
133 if len(self
.liststore
) >= MAX_HISTORY
:
134 self
.liststore
.remove(self
.liststore
.get_iter_first())
135 self
.liststore
.append([text
])
140 '''Write history to file in config directory'''
144 def func(model
, path
, item
, history_file
):
145 history_file
.write(model
.get_value(item
, 0)+'\n')
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
)
155 '''Read history from file and add to liststore'''
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()
164 self
.liststore
.append([x
[:-1]]) #remove trailing newline
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)
185 self
.selected
= False
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)
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
)
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
)
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
)
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
)
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)
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
)
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'''
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
)
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
)
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
)
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
)
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
):
341 self
.set_sensitives()
344 def clear(self
, *args
):
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
354 blocker
= tasks
.InputBlocker(outfile
)
357 os
.kill(thing
.pid
, signal
.SIGKILL
)
360 line
= outfile
.readline()
362 self
.set_sensitives()
363 iter = self
.store
.append(None)
365 (filename
, lineno
, text
) = string
.split(line
, ':', 2)
366 self
.store
.set(iter, 0, filename
, 1, int(lineno
), 2, text
[:-1])
368 self
.store
.set(iter, 2, line
[:-1])
372 rox
.info(_('There was a problem with this search'))
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)
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
:
407 self
.path
= browser
.get_filename()
408 path_widget
.set_text(self
.path
)
410 rox
.report_exception()
414 def set_selection(self
, *args
):
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
:
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
):
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
):
443 model
, iter = self
.view
.get_selection().get_selected()
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
))
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
460 popen2
.Popen4('%s "%s"' % (handler
, filename
))
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:
469 self
.menu
.popup(self
, event
)
473 def show_dir(self
, *args
):
474 ''' Pops up a filer window. '''
475 model
, iter = self
.view
.get_selection().get_selected()
477 filename
= model
.get_value(iter, 0)
478 filer
.show_file(filename
)
481 def edit_options(self
, *args
):
482 '''Show Options dialog'''
486 def get_options(self
):
487 '''Get changed Options'''
491 def delete_event(self
, ev
, e1
):
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()