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_MATCH_WORDS
= Option('match_words', False)
46 OPT_RECURSE_DIRS
= Option('recurse_dirs', True)
47 OPT_IGNORE_BIN
= Option('ignore_binary', True)
49 OPT_MATCH_CASE_ON
= Option('match_case_on', '')
50 OPT_MATCH_CASE_OFF
= Option('match_case_off', '-i')
52 OPT_MATCH_WORDS_ON
= Option('match_words_on', '-w')
53 OPT_MATCH_WORDS_OFF
= Option('match_words_off', '')
55 OPT_RECURSE_DIRS_ON
= Option('recurse_dirs_on', '')
56 OPT_RECURSE_DIRS_OFF
= Option('recurse_dirs_off', '-maxdepth 1')
58 OPT_IGNORE_BIN_ON
= Option('ignore_binary_on', '-I')
59 OPT_IGNORE_BIN_OFF
= Option('ignore_binary_off', '')
62 rox
.app_options
.notify()
64 #if you don't like the ComboBox, but want history support
65 #you can hardcode this to False (requires gtk 2.4+)
66 use_combo_box
= hasattr(gtk
, 'ComboBox')
69 _entryClass
= gtk
.ComboBoxEntry
71 _entryClass
= gtk
.Entry
73 class EntryThing(_entryClass
):
74 '''This class does two things.
75 1) it wraps gtk.Entry | gtk.ComboBoxEntry for backwards compatibility
76 2) it adds history support via the ComboBox or EntryCompletion
78 def __init__(self
, history
=None):
79 self
.history
= history
80 self
.liststore
= gtk
.ListStore(gobject
.TYPE_STRING
)
82 _entryClass
.__init
__(self
, self
.liststore
, 0)
83 self
.entry
= self
.child
85 _entryClass
.__init
__(self
)
87 try: #for gtk < 2.4 compatibility and in case nothing is saved yet
88 completion
= gtk
.EntryCompletion()
89 self
.set_completion(completion
)
90 completion
.set_model(self
.liststore
)
91 completion
.set_text_column(0)
98 '''This is supposed to write the history out upon exit,
99 but it never gets called!!!
105 return self
.child
.get_text()
107 return _entryClass
.get_text(self
)
109 def set_text(self
, text
):
112 index
= self
.find_text(text
)
114 self
.set_active(index
)
116 _entryClass
.set_text(self
, text
)
118 def find_text(self
, text
):
119 '''Check history to see if text already exists, return index or None'''
122 item
= self
.liststore
.get_iter_first()
125 old_item
= self
.liststore
.get_value(item
, 0)
128 item
= self
.liststore
.iter_next(item
)
134 def add_text(self
, text
):
135 '''Add item to history (if not duplicate)'''
137 if self
.find_text(text
) != None: return
139 n
= len(self
.liststore
)
141 self
.liststore
.remove(self
.liststore
.get_iter((n
-1,)))
142 self
.liststore
.insert(0, [text
])
147 '''Write history to file in config directory'''
151 def func(model
, path
, item
, history_file
):
152 history_file
.write(model
.get_value(item
, 0)+'\n')
155 save_dir
= basedir
.save_config_path(APP_SITE
, APP_NAME
)
156 history_file
= file(os
.path
.join(save_dir
, self
.history
), 'w')
157 self
.liststore
.foreach(func
, history_file
)
162 '''Read history from file and add to liststore'''
167 save_dir
= basedir
.save_config_path(APP_SITE
, APP_NAME
)
168 history_file
= file(os
.path
.join(save_dir
, self
.history
), 'r')
169 lines
= history_file
.readlines()
171 self
.liststore
.append([x
[:-1]]) #remove trailing newline
181 class FindWindow(rox
.Dialog
):
182 '''A Find in Files Utility:
183 Calls external search (e.g. find | grep) tool and parses output.
184 Found files and the matching text are displayed in a list.
185 Activating items in the list opens a Text Editor, optionally jumping to
186 the specific line of text.
189 updating_button_state
= False # Ignore events
193 def __init__(self
, in_path
=None, in_text
=None, in_files
=None):
194 rox
.Dialog
.__init
__(self
)
195 self
.set_has_separator(False)
196 self
.set_title(APP_NAME
)
197 self
.set_default_size(550, 500)
199 self
.selected
= False
204 self
.add_button(gtk
.STOCK_CLEAR
, RESPONSE_CLEAR
)
205 self
.add_button(gtk
.STOCK_CLOSE
, gtk
.RESPONSE_CANCEL
) # Must be Cancel for Escape to work
206 self
.add_button(gtk
.STOCK_HELP
, gtk
.RESPONSE_HELP
)
207 self
.find_btn
= gtk
.ToggleButton(gtk
.STOCK_FIND
)
208 self
.find_btn
.set_use_stock(True)
209 self
.find_btn
.set_flags(gtk
.CAN_DEFAULT
)
210 self
.add_action_widget(self
.find_btn
, RESPONSE_FIND
)
212 self
.set_default_response(RESPONSE_FIND
)
213 def resp(box
, action
):
214 if self
.updating_button_state
: return
216 if action
== gtk
.RESPONSE_CANCEL
:
218 elif action
== RESPONSE_CLEAR
:
220 elif action
== RESPONSE_FIND
:
221 if self
.find_process
is None:
225 elif action
== gtk
.RESPONSE_HELP
:
226 filer
.open_dir(os
.path
.join(rox
.app_dir
, 'Help'))
227 self
.connect('response', resp
)
228 self
.connect('delete_event', self
.close
)
230 self
.set_response_sensitive(RESPONSE_CLEAR
, False)
232 # Create layout, pack and show widgets
233 table
= gtk
.Table(5, 2, False)
237 path
= EntryThing('path')
238 table
.attach(gtk
.Label(_('Path')), 0, 1, 2, 3, 0, 0, 4, y_pad
)
239 table
.attach(path
, 1, 2, 2, 3, gtk
.EXPAND|gtk
.FILL
, 0, x_pad
, y_pad
)
240 if hasattr(gtk
, 'FileChooserDialog'):
241 self
.browse
= gtk
.Button(label
='...')
242 self
.browse
.connect('clicked', self
.browser
, path
)
243 table
.attach(self
.browse
, 2, 3, 2, 3, 0, 0, x_pad
, y_pad
)
245 what
= EntryThing('pattern')
246 path
.entry
.connect('activate', lambda p
: what
.grab_focus())
247 table
.attach(gtk
.Label(_('Pattern')), 0, 1, 3, 4, 0, 0, 4, y_pad
)
248 table
.attach(what
, 1, 2, 3, 4, gtk
.EXPAND|gtk
.FILL
, 0, x_pad
, y_pad
)
250 where
= EntryThing('files')
251 what
.entry
.connect('activate', lambda p
: where
.grab_focus())
252 where
.entry
.set_activates_default(True)
253 table
.attach(gtk
.Label(_('Files')), 0, 1, 4, 5, 0, 0, 4, y_pad
)
254 table
.attach(where
, 1, 2, 4, 5, gtk
.EXPAND|gtk
.FILL
, 0, x_pad
, y_pad
)
258 self
.match_case
= gtk
.CheckButton(label
=_('Match case'))
259 self
.match_case
.set_active(bool(OPT_MATCH_CASE
.int_value
))
260 hbox1
.pack_start(self
.match_case
,False, False, 5)
262 self
.match_words
= gtk
.CheckButton(label
=_('Match whole words'))
263 self
.match_words
.set_active(bool(OPT_MATCH_WORDS
.int_value
))
264 hbox1
.pack_start(self
.match_words
,False, False, 5)
266 self
.ignore_binary
= gtk
.CheckButton(label
=_('Ignore binary files'))
267 self
.ignore_binary
.set_active(bool(OPT_IGNORE_BIN
.int_value
))
268 hbox1
.pack_start(self
.ignore_binary
, False, False, 5)
272 self
.recurse_dirs
= gtk
.CheckButton(label
=_('Search subdirectories'))
273 self
.recurse_dirs
.set_active(bool(OPT_RECURSE_DIRS
.int_value
))
274 hbox2
.pack_start(self
.recurse_dirs
,False, False, 5)
276 prefs_btn
= gtk
.Button(stock
= gtk
.STOCK_PREFERENCES
)
277 hbox2
.pack_start(prefs_btn
,False, False, 5)
278 prefs_btn
.connect('clicked', self
.edit_options
)
280 swin
= gtk
.ScrolledWindow()
281 swin
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
282 swin
.set_shadow_type(gtk
.SHADOW_IN
)
284 self
.store
= gtk
.TreeStore(str, int, str)
285 view
= gtk
.TreeView(self
.store
)
288 view
.set_rules_hint(False)
290 # def func(model, iter, data):
291 # if len(model.get_path(iter)) < 2:
295 # view.set_row_separator_func(func, None)
297 cell
= gtk
.CellRendererText()
298 try: #for pre gtk 2.6.0 support
299 cell
.set_property('ellipsize_set', True)
300 cell
.set_property('ellipsize', pango
.ELLIPSIZE_START
)
302 self
.filename_column
= gtk
.TreeViewColumn(_('Filename'), cell
, text
= 0)
303 view
.append_column(self
.filename_column
)
304 self
.filename_column
.set_resizable(True)
305 self
.filename_column
.set_reorderable(True)
306 self
.filename_column
.set_min_width(130)
308 cell
= gtk
.CellRendererText()
309 column
= gtk
.TreeViewColumn(_('Line'), cell
, text
= 1)
310 view
.append_column(column
)
311 column
.set_resizable(True)
312 column
.set_reorderable(True)
314 cell
= gtk
.CellRendererText()
315 column
= gtk
.TreeViewColumn(_('Text'), cell
, text
= 2)
316 view
.append_column(column
)
317 column
.set_resizable(True)
318 column
.set_reorderable(True)
320 view
.connect('row-activated', self
.activate
)
321 self
.selection
= view
.get_selection()
322 self
.selection
.connect('changed', self
.set_selection
)
326 vbox
.pack_start(table
, False, False)
327 vbox
.pack_start(hbox1
, False, False)
328 vbox
.pack_start(hbox2
, False, False)
329 vbox
.pack_start(swin
, True, True)
332 what
.connect('changed', self
.entry_changed
)
333 where
.connect('changed', self
.entry_changed
)
334 path
.connect('changed', self
.entry_changed
)
336 self
.path_entry
= path
337 self
.what_entry
= what
338 self
.where_entry
= where
340 if in_path
: path
.set_text(in_path
)
341 if in_text
: what
.set_text(in_text
)
342 if in_files
: where
.set_text(in_files
)
344 self
.set_sensitives()
347 def start_find(self
, *args
):
348 '''Execute the find command after applying optional paramters'''
349 self
.view
.grab_focus() # Take focus from Find button!
352 self
.path_entry
.add_text(self
.path
)
353 self
.what_entry
.add_text(self
.what
)
354 self
.where_entry
.add_text(self
.where
)
356 self
.tree_node
= self
.store
.append(None)
357 self
.store
.set(self
.tree_node
,
358 0, os
.path
.join(self
.path
, self
.where
),
360 self
.view
.set_cursor(self
.store
.get_path(self
.tree_node
))
362 path
= os
.path
.expanduser(self
.path
)
364 cmd
= OPT_FIND_CMD
.value
365 #long options (deprecated)
366 cmd
= string
.replace(cmd
, '$Path', path
)
367 cmd
= string
.replace(cmd
, '$Files', self
.where
)
368 cmd
= string
.replace(cmd
, '$Text', self
.what
)
370 cmd
= string
.replace(cmd
, '$P', path
)
371 cmd
= string
.replace(cmd
, '$F', self
.where
)
372 cmd
= string
.replace(cmd
, '$T', self
.what
)
374 cmd
= string
.replace(cmd
, '$C', [OPT_MATCH_CASE_OFF
.value
,
375 OPT_MATCH_CASE_ON
.value
]
376 [self
.match_case
.get_active()])
378 cmd
= string
.replace(cmd
, '$W', [OPT_MATCH_WORDS_OFF
.value
,
379 OPT_MATCH_WORDS_ON
.value
]
380 [self
.match_words
.get_active()])
382 cmd
= string
.replace(cmd
, '$B', [OPT_IGNORE_BIN_OFF
.value
,
383 OPT_IGNORE_BIN_ON
.value
]
384 [self
.ignore_binary
.get_active()])
386 cmd
= string
.replace(cmd
, '$R', [OPT_RECURSE_DIRS_OFF
.value
,
387 OPT_RECURSE_DIRS_ON
.value
]
388 [self
.recurse_dirs
.get_active()])
390 self
.find_process
= popen2
.Popen3(cmd
)
391 tasks
.Task(self
.get_status(self
.find_process
))
392 self
.set_sensitives()
395 def cancel_find(self
, *args
):
397 os
.kill(self
.find_process
.pid
, signal
.SIGKILL
)
400 def clear(self
, *args
):
402 self
.selected
= False
403 self
.set_sensitives()
406 def close(self
, *args
):
407 if self
.find_process
is not None:
409 self
.path_entry
.write()
410 self
.what_entry
.write()
411 self
.where_entry
.write()
415 def get_status(self
, thing
):
416 '''Parse the ouput of the find command and fill the listbox.'''
417 outfile
= thing
.fromchild
420 blocker
= tasks
.InputBlocker(outfile
)
424 self
.find_process
= None
426 line
= outfile
.readline()
430 self
.set_sensitives()
431 iter = self
.store
.append(self
.tree_node
)
432 self
.view
.expand_row(self
.store
.get_path(self
.tree_node
), False)
434 (filename
, lineno
, text
) = string
.split(line
, ':', 2)
435 self
.store
.set(iter, 0, filename
, 1, int(lineno
), 2, text
[:-1])
437 self
.store
.set(iter, 2, line
[:-1])
440 self
.find_process
= None
443 rox
.info(_('There was a problem with this search'))
446 self
.set_sensitives()
448 rox
.info(_('Your search returned no results'))
451 def entry_changed(self
, button
):
452 self
.path
= self
.path_entry
.get_text()
453 self
.what
= self
.what_entry
.get_text()
454 self
.where
= self
.where_entry
.get_text()
455 self
.set_sensitives()
458 def set_sensitives(self
):
459 if self
.find_process
or (len(self
.what
) and len(self
.where
) and len(self
.path
)):
460 self
.find_btn
.set_sensitive(True)
462 self
.find_btn
.set_sensitive(False)
464 self
.set_response_sensitive(RESPONSE_CLEAR
, bool(len(self
.store
)))
466 self
.updating_button_state
= True
467 self
.find_btn
.set_active(self
.find_process
is not None)
468 self
.updating_button_state
= False
471 def browser(self
, button
, path_widget
):
472 browser
= gtk
.FileChooserDialog(title
=_('Select folder'), buttons
=(gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
, gtk
.STOCK_OPEN
, gtk
.RESPONSE_ACCEPT
))
473 if not len(self
.path
):
474 self
.path
= os
.path
.expanduser('~')
475 browser
.set_current_folder(self
.path
)
476 browser
.set_action(gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
)
477 if browser
.run() != gtk
.RESPONSE_CANCEL
:
479 self
.path
= browser
.get_filename()
480 path_widget
.set_text(self
.path
)
482 rox
.report_exception()
486 def set_selection(self
, *args
):
488 self
.set_sensitives()
491 def activate(self
, view
, path
, column
):
492 '''Launch Editor for selected file/text'''
494 def get_type_handler(mime_type
, handler_type
= 'MIME-types'):
495 """Lookup the ROX-defined run action for a given mime type.
496 mime_type is an object returned by lookup().
497 handler_type is a config directory leaf (e.g.'MIME-types')."""
499 handler
= basedir
.load_first_config('rox.sourceforge.net', handler_type
,
500 mime_type
.media
+ '_' + mime_type
.subtype
)
502 # Fall back to the base handler if no subtype handler exists
503 handler
= basedir
.load_first_config('rox.sourceforge.net', handler_type
,
507 if not path
: return True
509 # Expand/Collapse search section rows
511 if self
.view
.row_expanded(path
):
512 self
.view
.collapse_row(path
)
514 self
.view
.expand_row(path
, False)
517 model
= view
.get_model()
518 filename
= model
[path
][0]
520 event
= gtk
.get_current_event()
522 if column
is self
.filename_column
and event
and event
.type != gtk
.gdk
.KEY_PRESS
:
523 filer
.show_file(filename
)
525 line
= model
[path
][1]
527 if len(OPT_EDIT_CMD
.value
):
528 cmd
= OPT_EDIT_CMD
.value
529 cmd
= string
.replace(cmd
, '$File', filename
)
530 cmd
= string
.replace(cmd
, '$Line', str(line
))
532 else: #use the ROX defined text handler
533 mime_type
= rox
.mime
.lookup('text/plain')
534 handler
= get_type_handler(mime_type
)
535 handler_appdir
= os
.path
.join(handler
, 'AppRun')
536 if os
.path
.isdir(handler
) and os
.path
.isfile(handler_appdir
):
537 handler
= handler_appdir
539 popen2
.Popen4('%s "%s"' % (handler
, filename
))
541 rox
.info(_('There is no run action defined for text files!'))
545 def button_press(self
, text
, event
):
546 '''Popup menu handler'''
547 if event
.button
!= 3:
549 self
.menu
.popup(self
, event
)
553 def edit_options(self
, *args
):
554 '''Show Options dialog'''
558 def get_options(self
):
559 '''Get changed Options'''