zgtk-vgrep
zentara
created: 2006-01-19 14:33:40
#!/usr/bin/perl
use warnings;
use strict;
use File::Find;
use Gtk2 -init;
use Glib qw(FALSE TRUE);
use Gtk2::Pango;

$|++;

my $help;
while(){$help .= $_}

my $seeking = 0; #global flag to indicate searching is in progress
my $cancel = 0;  #global flag to cancel a long running search
my %files;       #global to store results
my $current;     #global to hold currently opened file
my ($recurse, $name, $case,$linenums,$quick_start) =(0,0,0,0,0);
if( grep{/\bn\b/} @ARGV ){@ARGV = grep { $_ ne 'n' } @ARGV; $name = 1 };
if( grep{/\br\b/} @ARGV ){@ARGV = grep { $_ ne 'r' } @ARGV; $recurse = 1 };
if( grep{/\bc\b/} @ARGV ){@ARGV = grep { $_ ne 'c' } @ARGV; $case = 1 };

$linenums = 1; #opened file line numbering is on by default
my $search_str = $ARGV[0] or $quick_start = 1;
$search_str ||= undef;
#only accept 1 search string, so quote phrases on commandline

#print "$name $recurse $case @ARGV\n";
my $font = Gtk2::Pango::FontDescription->from_string("Sans Bold 18");
my $hand_cursor = Gtk2::Gdk::Cursor->new ('hand2');
my $regular_cursor = Gtk2::Gdk::Cursor->new ('xterm');
my $hovering_over_link = FALSE;

my $blue = Gtk2::Gdk::Color->new (0,0,0xFFFF);
my $red = Gtk2::Gdk::Color->new (0xFFFF,0xFFFF,0);
my $white = Gtk2::Gdk::Color->new (0xFFFF,0xFFFF,0xFFFF);

my $window = Gtk2::Window->new;
$window->signal_connect( destroy => sub { Gtk2->main_quit; } );
$window->set_default_size( 700, 600 );
$window->signal_connect (key_press_event => \&key_handler);

#main box for display
my $vbox = Gtk2::VBox->new();
$vbox->set( "border_width" => 10 );
$window->add($vbox);

#top main box for search entry and options
my $vboxt = Gtk2::VBox->new();
$vboxt->set( "border_width" => 1 );
$vbox->pack_start( $vboxt, 0, 0, 1 ); # expand?, fill?, padding

#top layer
my $hboxt = Gtk2::HBox->new();
$hboxt->set( "border_width" => 1 );
$vboxt->pack_start( $hboxt, 0, 0, 1 ); # expand?, fill?, padding
#bottom layer
my $hboxb = Gtk2::HBox->new();
$hboxb->set( "border_width" => 1 );
$vboxt->pack_start( $hboxb, 0, 0, 1 ); # expand?, fill?, padding

################################################################
my $vboxC1 = Gtk2::VBox->new();
my $vboxC2 = Gtk2::VBox->new();

my $checkbutton1 = Gtk2::CheckButton->new('Recurse SubDirs');
$checkbutton1->signal_connect( clicked => 
               sub{check_button_callback($checkbutton1, \$recurse, 'recurse') } );
$vboxC1->pack_start( $checkbutton1, 0, 0, 0 );
if($recurse){$checkbutton1->set_active(1);}

my $checkbutton2 = Gtk2::CheckButton->new('Name Search Only');
$checkbutton2->signal_connect( clicked => 
              sub{check_button_callback($checkbutton2, \$name , 'name')});
$vboxC1->pack_start( $checkbutton2, 0, 0, 0 );
if($name){$checkbutton2->set_active(1);}

my $checkbutton3 = Gtk2::CheckButton->new('Case Sensitive');
$checkbutton3->signal_connect( clicked => 
             sub{check_button_callback($checkbutton3, \$case , 'case')});
$vboxC2->pack_start( $checkbutton3, 0, 0, 0 );
if($case){$checkbutton3->set_active(1);}

my $checkbutton4 = Gtk2::CheckButton->new('File Line Numbered');
$checkbutton4->signal_connect( clicked => 
             sub{check_button_callback($checkbutton4, \$linenums , 'linenums')});
$vboxC2->pack_start( $checkbutton4, 0, 0, 0 );
if($linenums){$checkbutton4->set_active(1);}

$hboxt->pack_start( $vboxC1, 0, 0, 0 );
$hboxt->pack_start( $vboxC2, 0, 0, 0 );

#################################################################
# the search button
my $sbutton = Gtk2::Button->new_from_stock('gtk-find');
$sbutton->signal_connect( clicked => sub{ if(! $seeking){ do_dir_search()} });
$hboxt->pack_start( $sbutton, 0, 0, 5 );
##############################################################
# exit button
my $ebutton = Gtk2::Button->new_from_stock('gtk-quit');
$ebutton->signal_connect( clicked => sub { Gtk2->main_quit; } );
$hboxt->pack_end( $ebutton, 0, 0, 5 );
#################################################################
# the help button
my $hbutton = Gtk2::Button->new_from_stock('gtk-help');
$hbutton->signal_connect( clicked => \&print_usage );
$hboxt->pack_end( $hbutton, 0, 0, 5 );
##################################################################
#bottom fields
my $label = Gtk2::Label->new('Search Text:');
$label->modify_font($font);
$hboxb->pack_start( $label, 0, 0, 5 ); # expand?, fill?, padding

my $entry = Gtk2::Entry->new();
$entry->modify_font($font);

if(defined $search_str){ $entry->set_text($search_str) }

$hboxb->pack_start( $entry, 1, 1, 1 ); # expand?, fill?, padding

#setup default focaus chain
$vboxt->set_focus_child ($hboxb); 
$hboxb->set_focus_child ($entry);  
#$entry->has_focus(TRUE);

$entry->signal_connect (key_press_event => \&key_handler);

#################################################################
#box for list
my $hbox1 = Gtk2::HBox->new();
$hbox1->set( "border_width" => 1 );
#box for textview
my $hbox2 = Gtk2::HBox->new();
$hbox2->set( "border_width" => 1 );

#$hbox2->set_size_request(200,500);

my $f1_label = Gtk2::Label->new('Search Results');
$f1_label->modify_fg('normal',$red);
$f1_label->modify_font($font);
my $frame1 = Gtk2::Frame->new();
$frame1->set_label_widget($f1_label);
$vbox->pack_start( $frame1, 1, 1, 1 ); # expand?, fill?, padding
$frame1->set_border_width(1); 
$frame1->add($hbox1);
$frame1->modify_bg('normal', $red); 

my $f2_label = Gtk2::Label->new('File');
$f2_label->modify_fg('normal',$blue);
$f2_label->modify_font($font);
my $frame2 = Gtk2::Frame->new();
$frame2->set_label_widget($f2_label);
$vbox->pack_start( $frame2, 1, 1, 1 ); # expand?, fill?, padding
$frame2->set_border_width(1);
$frame2->add($hbox2);
$frame2->modify_bg('normal', $blue); 

# Create a textbuffer to contain the resultant files
my $textbuffer0 = Gtk2::TextBuffer->new();
create_tags($textbuffer0);

$textbuffer0->set_text('');
$textbuffer0->apply_tag_by_name ('bold',$textbuffer0->get_start_iter,$textbuffer0->get_end_iter);

# Create a textview using that textbuffer
my $textview0 = Gtk2::TextView->new_with_buffer($textbuffer0);
$textview0->set_left_margin (5);

$textview0->signal_connect (event_after => \&event_after);
$textview0->signal_connect (motion_notify_event => \&motion_notify_event);
$textview0->signal_connect (visibility_notify_event => \&visibility_notify_event);

my $swl = Gtk2::ScrolledWindow -> new();
$swl -> add_with_viewport($textview0);
$hbox1->add($swl);
######################################################

# Create a textbuffer to contain the opened files
my $textbuffer = Gtk2::TextBuffer->new();
create_tags($textbuffer);

$textbuffer->set_text('');
$textbuffer->apply_tag_by_name ('bold',$textbuffer->get_start_iter,$textbuffer->get_end_iter);

# Create a textview using that textbuffer
my $textview = Gtk2::TextView->new_with_buffer($textbuffer);
$textview->set_left_margin (5);

# Add the textview to a scrolledwindow
my $scrolledwindow = Gtk2::ScrolledWindow->new( undef, undef );
$scrolledwindow->add($textview);

# add that scrolledwindow to the vbox
$hbox2->add($scrolledwindow);
$window->show_all;

if( $quick_start == 0){
 Glib::Timeout->add (10, sub { do_dir_search(); 0 }); 
}

Gtk2->main;
#############################################################################
sub do_file_search{

  my $file = shift;
  if( ! defined $file ){return}
  
  my @lines = ();

    foreach my $aref( @{$files{$file}} ){
           push @lines, $$aref[0];
          }

  $textbuffer->set_text('');
  
  open (FH,"< $file");
  while(){
     my $line = $.;
     
     if($linenums){
       my $lineI = sprintf "%03d", $line;
       $textbuffer->insert_with_tags_by_name ($textbuffer->get_end_iter, $lineI, 'rmap');
       $textbuffer->insert ($textbuffer->get_end_iter, ' ');
     }

    if( grep {/^$line$/} @lines ){
           $textbuffer->insert_with_tags_by_name ($textbuffer->get_end_iter, $_, 'rmapZ');
       }else{
          $textbuffer->insert_with_tags_by_name ($textbuffer->get_end_iter, $_, 'bold');
       }
  }
 close FH;

#set up where to scroll to when opening file
 my $first;
 if ( $lines[0] > 0 ){ $first = $lines[0] }else{$first = 1} 
 my $start_iter = $textbuffer->get_iter_at_line($first);

my $start_mark = $textbuffer->create_mark('start', $start_iter, 1);
$textview->scroll_to_mark($start_mark ,0.0, 0, 0, 0.0);

#set frame label to file name 
$f2_label->set_text($file);

$current = $file; 
}
################################################################
sub do_dir_search{

#prevent accidental double enter's or double find button presses 
$seeking = 1;
$cancel = 0;

#clear old screens
$textbuffer->delete($textbuffer->get_start_iter,$textbuffer->get_end_iter,);
$textbuffer0->delete($textbuffer0->get_start_iter,$textbuffer0->get_end_iter,);

#clear old results
%files = ();

$f1_label->set_text('Search Results');
$f2_label->set_text('File');

# defaults are case-insensitive, no recurse, open and search files (not filename) 
my $path = '.';

$search_str = $entry->get_text();
if( ! length $search_str){return} 

my $regex;  #defaults to case insensitive
if ($case){$regex =  qr/\Q$search_str\E/}
      else{$regex =  qr/\Q$search_str\E/i}
# use s modifier for multiline match

my $count = 0;
my $count1 = 0;

find (sub { 
      if( $cancel ){ return $File::Find::prune = 1}
      $count1++;
      if( ! $recurse ){
      my $n = ($File::Find::name) =~ tr!/!!; #count slashes in file
      return $File::Find::prune = 1 if ($n > 1);
      }
     return if -d;
     return unless (-f and -T );

    if($name){
          if ($_ =~ /$regex/){ 
	     push @{$files{$File::Find::name}}, [-1,'']; #push into HoA
	  } 
     }else{
         open (FH,"< $_");
            while(){
               if ($_ =~ /$regex/){ 
	           chomp $_;
                   push @{$files{$File::Find::name}}, [$., $_]; #push into HoA
     	       } 
	     }
	 close FH;
     }    
#------	
        #$cancel = 0;
        my $key = $File::Find::name;
        
        if( defined  $files{$key} ){
           $count++;
    	   my $aref = $files{$key};
	   my @larray = @$aref;
           insert_link ($textbuffer0, $key );
	   
         foreach my $aref(@larray){
	    if( $$aref[0] > 0 ){  #don't do for name searches, which are -1
	       #add line number with color
                  my $lineI = sprintf"%03d", $$aref[0];    
                  $textbuffer0->insert_with_tags_by_name( 
	          $textbuffer0->get_end_iter,"\n$lineI",'rmap');
               #add matching line
                  $textbuffer0->insert_with_tags_by_name( 
	          $textbuffer0->get_end_iter,"$$aref[1]",'bold');
	     }
	  }
       $textbuffer0->insert($textbuffer0->get_end_iter,"\n");
       }
       $f1_label->set_text("$count1 checked -- $count matches .. SpaceBar cancels");
       Gtk2->main_iteration while Gtk2->events_pending;
     
 
 #-----      
  }, $path);
     
     $f1_label->set_text("$count matches DONE");
     $seeking = 0;
     $cancel = 0;
     Gtk2->main_iteration while Gtk2->events_pending;
}
##############################################################################
sub insert_link {
  #print "@_\n";
  
  my ($buffer, $file ) = @_;
  
  #create tag here independently, so we can piggyback unique data
  my $tag = $buffer->create_tag (undef, 
				 foreground => "blue", 
				 underline => 'single',
				 size   => 20 * PANGO_SCALE
				 );
# piggyback data onto each tag
  $tag->{file} = $file;
  $buffer->insert_with_tags ($textbuffer0->get_end_iter, $file, $tag);
  
}
###########################################################################
# Looks at all tags covering the position of iter in the text view, 
# and if one of them is a link, follow it by showing the page identified
# by the data attached to it.
#
sub follow_if_link {
  my ($text_view, $iter) = @_;
      
      my $tag = $iter->get_tags;
      my $file = $tag->{file};
    
     if($file){
      do_file_search($file);
      }
}
#################################################################
# Links can be activated by pressing Enter.
#
sub key_press_event {
  my ($text_view, $event) = @_;

  if ($event->keyval == $Gtk2::Gdk::Keysyms{Return} ||
      $event->keyval == $Gtk2::Gdk::Keysyms{KP_Enter})
    {
        my $buffer = $text_view->get_buffer;
        my $iter = $buffer->get_iter_at_mark ($buffer->get_insert);
        follow_if_link ($text_view, $iter);
    }

  return FALSE;
}

#######################################################################3
# Links can also be activated by clicking.
#
sub event_after {
  my ($text_view, $event) = @_;

  return FALSE unless $event->type eq 'button-release'; 
  return FALSE unless $event->button == 1;

  my $buffer = $text_view->get_buffer;

  # we shouldn't follow a link if the user has selected something
  my ($start, $end) = $buffer->get_selection_bounds;
  return FALSE if defined $end
                  and $start->get_offset != $end->get_offset;

  my ($x, $y) = $text_view->window_to_buffer_coords ('widget', #GTK_TEXT_WINDOW_WIDGET,
                                                     $event->x, $event->y);

  my $iter = $text_view->get_iter_at_location ($x, $y);

  follow_if_link ($text_view, $iter);

  return FALSE;
}


#####################################################################
# Looks at all tags covering the position (x, y) in the text view, 
# and if one of them is a link, change the cursor to the "hands" cursor
# typically used by web browsers.
#
sub set_cursor_if_appropriate {
  my ($text_view, $x, $y) = @_;
  my $hovering = FALSE;

  my $buffer = $text_view->get_buffer;

  my $iter = $text_view->get_iter_at_location ($x, $y);
  
  foreach my $tag ($iter->get_tags) {
      if ($tag->{file}) {
          $hovering = TRUE;
          last;
      }
  }

  if ($hovering != $hovering_over_link)
    {
      $hovering_over_link = $hovering;

      $text_view->get_window ('text')->set_cursor
      		($hovering_over_link ? $hand_cursor : $regular_cursor);
    }
}
#######################################################################
# Update the cursor image if the pointer moved. 
#
sub motion_notify_event {
  my ($text_view, $event) = @_;

  my ($x, $y) = $text_view->window_to_buffer_coords ( 
                                         'widget', #GTK_TEXT_WINDOW_WIDGET,
                                         $event->x, $event->y);

  set_cursor_if_appropriate ($text_view, $x, $y);

  $text_view->window->get_pointer;
  return FALSE;
}

#########################################################################
# Also update the cursor image if the window becomes visible
# (e.g. when a window covering it got iconified).
#
sub visibility_notify_event {
  my ($text_view, $event) = @_;

  my (undef, $wx, $wy, undef) = $text_view->window->get_pointer;
  
  my ($bx, $by) = $text_view->window_to_buffer_coords ( 
                                         'widget', #GTK_TEXT_WINDOW_WIDGET,
                                         $wx, $wy);

  set_cursor_if_appropriate ($text_view, $bx, $by);

  return FALSE;
}
#################################################################################
sub check_button_callback {
  my ($button,$ident,$name) = @_;

     if ($button->get_active) {
        # if control reaches here, the check button is down
         $$ident = 1;
     } else {
        # if control reaches here, the check button is up
         $$ident = 0;
     }   

  #reload current file with(or without) linenums
  #useful when copying for pasting to other apps
  if( $name eq 'linenums' ){ do_file_search($current) } 


} 
##################################################################################
sub create_tags{
  my $buffer = shift; 

   $buffer->create_tag( "italic", style  => 'italic' );
   $buffer->create_tag( "bold",   weight => PANGO_WEIGHT_BOLD );
   $buffer->create_tag( "big",    size   => 20 * PANGO_SCALE );
   # points times the PANGO_SCALE factor
   $buffer->create_tag( "x-large", scale  => PANGO_SCALE_X_LARGE );
 

   my $gray25_bits = pack 'CC', 0x02, 0x01, 0x01;
   my $stipple = Gtk2::Gdk::Bitmap->create_from_data( undef, $gray25_bits, 3, 3);

   $buffer->create_tag( "background_stipple", background_stipple => $stipple );

   $buffer->create_tag('rmap',
          background => 'red',
          background_stipple => $stipple , 
	  weight => PANGO_WEIGHT_BOLD,
	 );

   $buffer->create_tag('rmapX',
          background => 'red',
          background_stipple => $stipple , 
	  scale => PANGO_SCALE_X_LARGE,
	 );

   $buffer->create_tag('rmapZ',
           foreground => 'red',
           weight => PANGO_WEIGHT_BOLD,
	   size   => 15 * PANGO_SCALE
	 );

   $buffer->create_tag('blue',
           background => 'blue',
           );

}
######################################################################
sub key_handler {
my ($widget, $event) = @_;
    # match the keyval --- in general you don't want to use magic
    # numbers here, use values from %Gtk2::Gdk::Keysyms instead.

    if (($seeking) && ($event->keyval == 32)) {  #detect spacebar hit to cancel search 
       $cancel = 1;       
       $seeking = 0;
       return TRUE;  # tell the system we handled this
     }

    if ($event->keyval == 65293) {  #detect Enter press
       do_dir_search() unless $seeking;
       return TRUE;  # tell the system we handled this
     }

    if ($event->keyval == 99) {  #detect Control-C 
       Gtk2->main_quit;
       return TRUE;  # tell the system we handled this
     }



return FALSE;  # tell the system we did not handle this
}
######################################################################
sub print_usage{
  $textbuffer->delete($textbuffer->get_start_iter,$textbuffer->get_end_iter,);  
  $textbuffer->set_text($help);  
}
######################################################################
__DATA__
# USAGE: 
# -- takes a quoted string on the command line to search for
# -- you can give options n r c, separately on the commandline
# -- n ->search filenames only
# -- r ->recurse into sub directories 
# -- c ->case sensitive 
# Will open with no options or search string, in empty mode.

# Found files, will be displayed as blue underlined hyperlinks.
# Under each link, will be the line numbers and matched lines. 
# Clicking blue links will open the file, and all matched lines will
# be displayed in red text.

#  Control-c from anywhere will kill the program.
#  SpaceBar cancels a long search

#  Multi-word search strings on the command line should be
#  single quoted.
#  Search strings entered in the entry box, do not need quotes  
#  and will be submitted by an enter keypress, or the find button.

#  The line numbering can be turned off for copying and pasting from
#  the opened files. A right click in the lower textbox, will open
#  the textbox menu for Copying to the Clipboard for pasting with
#  the paste options from other apps (Control-v). Otherwise, the selected
#  text is only in the mouse clipboard buffer.
__END__

Re: Gtk2 Visual Grep
created: 2006-01-20 05:15:57
Hi,
great, thanks zentara++ !
Added it to my standard tools!
Regards,
svenXY
Re^2: Gtk2 Visual Grep
created: 2006-01-20 12:40:27
Hi, I fixed a small bug which locks up the search when entering empty search strings; plus removed some useless subs. Please download again.

I'm not really a human, but I play one on earth. flash japh
Re^3: Gtk2 Visual Grep
created: 2006-01-24 05:42:19
Hi,
thanks again. The following patch adds capability of searching with regexes (I get some utf8 errors, but it works. Do you happen to know how to fix them?):
18c18
< my ($recurse, $name, $case,$linenums,$quick_start) =(0,0,0,0,0);
---
> my ($recurse, $name, $case,$linenums,$quick_start, $use_regex) =(0,0,0,0,0);
21a22
> if( grep{/\bR\b/} @ARGV ){@ARGV = grep { $_ ne 'R' } @ARGV; $use_regex = 1 };
64a66
> my $vboxC3 = Gtk2::VBox->new();
83a86,91
> my $checkbutton5 = Gtk2::CheckButton->new('Use RegEx');
> $checkbutton5->signal_connect( clicked =>
>              sub{check_button_callback($checkbutton5, \$use_regex , 'use_regex')});
> $vboxC2->pack_start( $checkbutton5, 0, 0, 0 );
>
> if($case){$checkbutton5->set_active(1);}
87c95
< $vboxC2->pack_start( $checkbutton4, 0, 0, 0 );
---
> $vboxC3->pack_start( $checkbutton4, 0, 0, 0 );
91a100
> $hboxt->pack_start( $vboxC3, 0, 0, 0 );
284,285c293,307
< if ($case){$regex =  qr/\Q$search_str\E/}
<       else{$regex =  qr/\Q$search_str\E/i}
---
> if ($case){
>   if ($use_regex) {
>     $regex =  qr/$search_str/;
>   }
>   else {
>       $regex =  qr/\Q$search_str\E/}
> }
> else {
>   if ($use_regex) {
>     $regex =  qr/$search_str/i;
>   }
>   else {
>       $regex =  qr/\Q$search_str\E/i;
>   }
> }

Regards,
svenXY
Re^4: Gtk2 Visual Grep
created: 2006-01-24 06:04:51
My first guess would be to put "use utf8" at the top of the script. But you might want to post a new toplevel question for this? I will test it today and see what I can figure out.

I'm not really a human, but I play one on earth. flash japh
Re^4: Gtk2 Visual Grep
created: 2006-01-24 07:07:23
Hi, I believe I found a bug in your patch , which may be causing your problem. Your line
> if($case){$checkbutton5->set_active(1);}
probably should be
> if($use_regex){$checkbutton5->set_active(1);}
That would screw up the if-else clause, by cross-linking $case and $use_regex; but I'm still testing it. :-)

I'm not really a human, but I play one on earth. flash japh
Re^4: Gtk2 Visual Grep
created: 2006-01-24 08:36:52
I "think" I found the answer to the "malformed utf8 errors". It probably only works for those using latin1 encoding. Possibly, it can be adjusted to use whatever encoding you use? Anyways, change the line which opens the file from
open (FH,"<", $_); 
# to
open (FH,"<:encoding(latin1)", $_);
Anyways, it works for me in stopping the errors. I can't say how the regex search works in all it's complexity, for instance a really complex regex may need some special handling, but so far, so good. :-)

Here is a complete patched snippet. I added another feature to skip search directories that begin with the number 1. (The code is commented out, but you may activate it). I store alot of html files for my categories, in 1DOCS, and I prefer not to delve into them on recursive searches. Modify to fit your needs.

New script:

#!/usr/bin/perl 
use warnings;
use strict;
use File::Find;
use Gtk2 -init;
use Glib qw(FALSE TRUE);
use Gtk2::Pango;

$|++;

my $help;
while(){$help .= $_}

my $seeking = 0; #global flag to indicate searching is in progress
my $cancel = 0;  #global flag to cancel a long running search
my %files;       #global to store results
my $current;     #global to hold currently opened file
my ($recurse, $name, $case,$linenums,$quick_start,$use_regex) =(0,0,0,0,0,0);
if( grep{/\bn\b/} @ARGV ){@ARGV = grep { $_ ne 'n' } @ARGV; $name = 1 };
if( grep{/\br\b/} @ARGV ){@ARGV = grep { $_ ne 'r' } @ARGV; $recurse = 1 };
if( grep{/\bc\b/} @ARGV ){@ARGV = grep { $_ ne 'c' } @ARGV; $case = 1 };
if( grep{/\bR\b/} @ARGV ){@ARGV = grep { $_ ne 'R' } @ARGV; $use_regex = 1 };

$linenums = 1; #opened file line numbering is on by default
my $search_str = $ARGV[0] or $quick_start = 1;
$search_str ||= undef;
#only accept 1 search string, so quote phrases on commandline

#print "$name $recurse $case @ARGV\n";
my $font = Gtk2::Pango::FontDescription->from_string("Sans Bold 18");
my $hand_cursor = Gtk2::Gdk::Cursor->new ('hand2');
my $regular_cursor = Gtk2::Gdk::Cursor->new ('xterm');
my $hovering_over_link = FALSE;

my $blue = Gtk2::Gdk::Color->new (0,0,0xFFFF);
my $red = Gtk2::Gdk::Color->new (0xFFFF,0xFFFF,0);
my $white = Gtk2::Gdk::Color->new (0xFFFF,0xFFFF,0xFFFF);

my $window = Gtk2::Window->new;
$window->signal_connect( destroy => sub { Gtk2->main_quit; } );
$window->set_default_size( 700, 600 );
$window->signal_connect (key_press_event => \&key_handler);

#main box for display
my $vbox = Gtk2::VBox->new();
$vbox->set( "border_width" => 10 );
$window->add($vbox);

#top main box for search entry and options
my $vboxt = Gtk2::VBox->new();
$vboxt->set( "border_width" => 1 );
$vbox->pack_start( $vboxt, 0, 0, 1 ); # expand?, fill?, padding

#top layer
my $hboxt = Gtk2::HBox->new();
$hboxt->set( "border_width" => 1 );
$vboxt->pack_start( $hboxt, 0, 0, 1 ); # expand?, fill?, padding
#bottom layer
my $hboxb = Gtk2::HBox->new();
$hboxb->set( "border_width" => 1 );
$vboxt->pack_start( $hboxb, 0, 0, 1 ); # expand?, fill?, padding

################################################################
my $vboxC1 = Gtk2::VBox->new();
my $vboxC2 = Gtk2::VBox->new();
my $vboxC3 = Gtk2::VBox->new();

my $checkbutton1 = Gtk2::CheckButton->new('Recurse SubDirs');
$checkbutton1->signal_connect( clicked => 
               sub{check_button_callback($checkbutton1, \$recurse, 'recurse') } );
$vboxC1->pack_start( $checkbutton1, 0, 0, 0 );
if($recurse){$checkbutton1->set_active(1);}

my $checkbutton2 = Gtk2::CheckButton->new('Name Search Only');
$checkbutton2->signal_connect( clicked => 
              sub{check_button_callback($checkbutton2, \$name , 'name')});
$vboxC1->pack_start( $checkbutton2, 0, 0, 0 );
if($name){$checkbutton2->set_active(1);}

my $checkbutton3 = Gtk2::CheckButton->new('Case Sensitive');
$checkbutton3->signal_connect( clicked => 
             sub{check_button_callback($checkbutton3, \$case , 'case')});
$vboxC2->pack_start( $checkbutton3, 0, 0, 0 );
if($case){$checkbutton3->set_active(1);}

my $checkbutton4 = Gtk2::CheckButton->new('File Line Numbered');
$checkbutton4->signal_connect( clicked => 
             sub{check_button_callback($checkbutton4, \$linenums , 'linenums')});
$vboxC2->pack_start( $checkbutton4, 0, 0, 0 );
if($linenums){$checkbutton4->set_active(1);}

my $checkbutton5 = Gtk2::CheckButton->new('Use Regex');
$checkbutton5->signal_connect( clicked => 
             sub{check_button_callback($checkbutton5, \$use_regex , 'use_regex')});
$vboxC3->pack_start( $checkbutton5, 0, 0, 0 );
if($use_regex){$checkbutton5->set_active(1);}


$hboxt->pack_start( $vboxC1, 0, 0, 0 );
$hboxt->pack_start( $vboxC2, 0, 0, 0 );
$hboxt->pack_start( $vboxC3, 0, 0, 0 );

#################################################################
# the search button
my $sbutton = Gtk2::Button->new_from_stock('gtk-find');
$sbutton->signal_connect( clicked => sub{ if(! $seeking){ do_dir_search()} });
$hboxt->pack_start( $sbutton, 0, 0, 5 );
##############################################################
# exit button
my $ebutton = Gtk2::Button->new_from_stock('gtk-quit');
$ebutton->signal_connect( clicked => sub { Gtk2->main_quit; } );
$hboxt->pack_end( $ebutton, 0, 0, 5 );
#################################################################
# the help button
my $hbutton = Gtk2::Button->new_from_stock('gtk-help');
$hbutton->signal_connect( clicked => \&print_usage );
$hboxt->pack_end( $hbutton, 0, 0, 5 );
##################################################################
#bottom fields
my $label = Gtk2::Label->new('Search Text:');
$label->modify_font($font);
$hboxb->pack_start( $label, 0, 0, 5 ); # expand?, fill?, padding

my $entry = Gtk2::Entry->new();
$entry->modify_font($font);

if(defined $search_str){ $entry->set_text($search_str) }

$hboxb->pack_start( $entry, 1, 1, 1 ); # expand?, fill?, padding

#setup default focaus chain
$vboxt->set_focus_child ($hboxb); 
$hboxb->set_focus_child ($entry);  
#$entry->has_focus(TRUE);

$entry->signal_connect (key_press_event => \&key_handler);

#################################################################
#box for list
my $hbox1 = Gtk2::HBox->new();
$hbox1->set( "border_width" => 1 );
#box for textview
my $hbox2 = Gtk2::HBox->new();
$hbox2->set( "border_width" => 1 );

#$hbox2->set_size_request(200,500);

my $f1_label = Gtk2::Label->new('Search Results');
$f1_label->modify_fg('normal',$red);
$f1_label->modify_font($font);
my $frame1 = Gtk2::Frame->new();
$frame1->set_label_widget($f1_label);
$vbox->pack_start( $frame1, 1, 1, 1 ); # expand?, fill?, padding
$frame1->set_border_width(1); 
$frame1->add($hbox1);
$frame1->modify_bg('normal', $red); 

my $f2_label = Gtk2::Label->new('File');
$f2_label->modify_fg('normal',$blue);
$f2_label->modify_font($font);
my $frame2 = Gtk2::Frame->new();
$frame2->set_label_widget($f2_label);
$vbox->pack_start( $frame2, 1, 1, 1 ); # expand?, fill?, padding
$frame2->set_border_width(1);
$frame2->add($hbox2);
$frame2->modify_bg('normal', $blue); 

# Create a textbuffer to contain the resultant files
my $textbuffer0 = Gtk2::TextBuffer->new();
create_tags($textbuffer0);

$textbuffer0->set_text('');
$textbuffer0->apply_tag_by_name ('bold',$textbuffer0->get_start_iter,$textbuffer0->get_end_iter);

# Create a textview using that textbuffer
my $textview0 = Gtk2::TextView->new_with_buffer($textbuffer0);
$textview0->set_left_margin (5);

$textview0->signal_connect (event_after => \&event_after);
$textview0->signal_connect (motion_notify_event => \&motion_notify_event);

my $swl = Gtk2::ScrolledWindow -> new();
$swl -> add_with_viewport($textview0);
$hbox1->add($swl);
######################################################

# Create a textbuffer to contain the opened files
my $textbuffer = Gtk2::TextBuffer->new();
create_tags($textbuffer);

$textbuffer->set_text('');
$textbuffer->apply_tag_by_name ('bold',$textbuffer->get_start_iter,$textbuffer->get_end_iter);

# Create a textview using that textbuffer
my $textview = Gtk2::TextView->new_with_buffer($textbuffer);
$textview->set_left_margin (5);

# Add the textview to a scrolledwindow
my $scrolledwindow = Gtk2::ScrolledWindow->new( undef, undef );
$scrolledwindow->add($textview);

# add that scrolledwindow to the vbox
$hbox2->add($scrolledwindow);
$window->show_all;

#setup a control-c exit---------- 
my @accels = (
        { key => 'C', mod => 'control-mask', func => sub{ Gtk2->main_quit } },
        );

my $accel_group = Gtk2::AccelGroup->new;
use Gtk2::Gdk::Keysyms;
      foreach my $a (@accels) {
        $accel_group->connect ($Gtk2::Gdk::Keysyms{$a->{key}}, $a->{mod},
                       'visible', $a->{func});
       }
$window->add_accel_group ($accel_group);
##############------------------- 

if( $quick_start == 0){
 Glib::Timeout->add (10, sub { do_dir_search(); 0 }); 
}

Gtk2->main;
#############################################################################
sub do_file_search{

  my $file = shift;
  if( ! defined $file ){return}
  
  my @lines = ();

    foreach my $aref( @{$files{$file}} ){
           push @lines, $$aref[0];
          }

  $textbuffer->set_text('');
  
  open (FH,"< $file");
  while(){
     my $line = $.;
     
     if($linenums){
       my $lineI = sprintf "%03d", $line;
       $textbuffer->insert_with_tags_by_name ($textbuffer->get_end_iter, $lineI, 'rmap');
       $textbuffer->insert ($textbuffer->get_end_iter, ' ');
     }

    if( grep {/^$line$/} @lines ){
           $textbuffer->insert_with_tags_by_name ($textbuffer->get_end_iter, $_, 'rmapZ');
       }else{
          $textbuffer->insert_with_tags_by_name ($textbuffer->get_end_iter, $_, 'bold');
       }
  }
 close FH;

#set up where to scroll to when opening file
 my $first;
 if ( $lines[0] > 0 ){ $first = $lines[0] }else{$first = 1} 
 my $start_iter = $textbuffer->get_iter_at_line($first);

my $start_mark = $textbuffer->create_mark('start', $start_iter, 1);
$textview->scroll_to_mark($start_mark ,0.0, 0, 0, 0.0);

#set frame label to file name 
$f2_label->set_text($file);

$current = $file; 
}
################################################################
sub do_dir_search{

#prevent accidental double enter's or double find button presses 
$seeking = 1;
$cancel = 0;

#clear old screens
$textbuffer->delete($textbuffer->get_start_iter,$textbuffer->get_end_iter,);
$textbuffer0->delete($textbuffer0->get_start_iter,$textbuffer0->get_end_iter,);

#clear old results
%files = ();

$f1_label->set_text('Search Results');
$f2_label->set_text('File');

# defaults are case-insensitive, no recurse, open and search files (not filename) 
my $path = '.';

$search_str = $entry->get_text();
if( ! length $search_str){$seeking = 0; $cancel = 0; return}

my $regex;  #defaults to case insensitive
 if ($case){

       if ($use_regex) {
            $regex =  qr/$search_str/;
           } else { 
	       $regex =  qr/\Q$search_str\E/
	     }
  
  }else {
    
         if ($use_regex) {
             $regex =  qr/$search_str/i;
            }else{
               $regex =  qr/\Q$search_str\E/i; 
	    }
  }


# use s modifier for multiline match

my $count = 0;
my $count1 = 0;

find (sub { 
      if( $cancel ){ return $File::Find::prune = 1}
      
   # skip directories which begin with 1
      #  if (-d && $_ =~ /^1.*$/) {
      #                  $File::Find::prune = 1;
      #		                return;
      #                           }
      
      $count1++;
      if( ! $recurse ){
      my $n = ($File::Find::name) =~ tr!/!!; #count slashes in file
      return $File::Find::prune = 1 if ($n > 1);
      }
     return if -d;
     return unless (-f and -T );

    if($name){
          if ($_ =~ /$regex/){ 
	     push @{$files{$File::Find::name}}, [-1,'']; #push into HoA
	  } 
     }else{
     
#      open (FH,"<", $_);
#        binmode(FH, ":encoding(latin1)");
      open (FH,"<:encoding(latin1)", $_);
         while(){
               if ($_ =~ /$regex/){ 
	           chomp $_;
                   push @{$files{$File::Find::name}}, [$., $_]; #push into HoA
     	       } 
	     }
	 close FH;
     }    
#------	
        my $key = $File::Find::name;
        
        if( defined  $files{$key} ){
           $count++;
    	   my $aref = $files{$key};
	   my @larray = @$aref;
           insert_link ($textbuffer0, $key );
	   
         foreach my $aref(@larray){
	    if( $$aref[0] > 0 ){  #don't do for name searches, which are -1
	       #add line number with color
                  my $lineI = sprintf"%03d", $$aref[0];    
                  $textbuffer0->insert_with_tags_by_name( 
	          $textbuffer0->get_end_iter,"\n$lineI",'rmap');
               #add matching line
                  $textbuffer0->insert_with_tags_by_name( 
	          $textbuffer0->get_end_iter,"$$aref[1]",'bold');
	     }
	  }
       $textbuffer0->insert($textbuffer0->get_end_iter,"\n");
       }
       $f1_label->set_text("$count1 checked -- $count matches .. SpaceBar cancels");
       Gtk2->main_iteration while Gtk2->events_pending;
     
 
 #-----      
  }, $path);
     
     $f1_label->set_text("$count matches DONE");
     $seeking = 0;
     $cancel = 0;
     Gtk2->main_iteration while Gtk2->events_pending;
}
##############################################################################
sub insert_link {
  #print "@_\n";
  
  my ($buffer, $file ) = @_;
  
  #create tag here independently, so we can piggyback unique data
  my $tag = $buffer->create_tag (undef, 
				 foreground => "blue", 
				 underline => 'single',
				 size   => 20 * PANGO_SCALE
				 );
# piggyback data onto each tag
  $tag->{file} = $file;
  $buffer->insert_with_tags ($textbuffer0->get_end_iter, $file, $tag);
  
}
###########################################################################
# Looks at all tags covering the position of iter in the text view, 
# and if one of them is a link, follow it by showing the page identified
# by the data attached to it.
#
sub follow_if_link {
  my ($text_view, $iter) = @_;
      
      my $tag = $iter->get_tags;
      my $file = $tag->{file};
    
     if($file){
      do_file_search($file);
      }
}
#################################################################
# Links can also be activated by clicking.
sub event_after {
  my ($text_view, $event) = @_;

  return FALSE unless $event->type eq 'button-release'; 
  return FALSE unless $event->button == 1;

  my $buffer = $text_view->get_buffer;

  # we shouldn't follow a link if the user has selected something
  my ($start, $end) = $buffer->get_selection_bounds;
  return FALSE if defined $end
                  and $start->get_offset != $end->get_offset;

  my ($x, $y) = $text_view->window_to_buffer_coords ('widget', #GTK_TEXT_WINDOW_WIDGET,
                                                     $event->x, $event->y);

  my $iter = $text_view->get_iter_at_location ($x, $y);

  follow_if_link ($text_view, $iter);

  return FALSE;
}

#####################################################################
# Looks at all tags covering the position (x, y) in the text view, 
# and if one of them is a link, change the cursor to the "hands" cursor
# typically used by web browsers.
#
sub set_cursor_if_appropriate {
  my ($text_view, $x, $y) = @_;
  my $hovering = FALSE;

  my $buffer = $text_view->get_buffer;

  my $iter = $text_view->get_iter_at_location ($x, $y);
  
  foreach my $tag ($iter->get_tags) {
      if ($tag->{file}) {
           $hovering = TRUE;
          last;
      }
  }

  if ($hovering != $hovering_over_link)
    {
      $hovering_over_link = $hovering;

      $text_view->get_window ('text')->set_cursor
      		($hovering_over_link ? $hand_cursor : $regular_cursor);
    }
}
#######################################################################
# Update the cursor image if the pointer moved. 
#
sub motion_notify_event {
  my ($text_view, $event) = @_;

  my ($x, $y) = $text_view->window_to_buffer_coords ( 
                                         'widget', #GTK_TEXT_WINDOW_WIDGET,
                                         $event->x, $event->y);

  set_cursor_if_appropriate ($text_view, $x, $y);

  $text_view->window->get_pointer;
  return FALSE;
}

#########################################################################

sub check_button_callback {
  my ($button,$ident,$name) = @_;

     if ($button->get_active) {
        # if control reaches here, the check button is down
         $$ident = 1;
     } else {
        # if control reaches here, the check button is up
         $$ident = 0;
     }   

  #reload current file with(or without) linenums
  #useful when copying for pasting to other apps
  if( $name eq 'linenums' ){ do_file_search($current) } 


} 
##################################################################################
sub create_tags{
  my $buffer = shift; 

   $buffer->create_tag( "italic", style  => 'italic' );
   $buffer->create_tag( "bold",   weight => PANGO_WEIGHT_BOLD );
   $buffer->create_tag( "big",    size   => 20 * PANGO_SCALE );
   # points times the PANGO_SCALE factor
   $buffer->create_tag( "x-large", scale  => PANGO_SCALE_X_LARGE );
 

   my $gray25_bits = pack 'CC', 0x02, 0x01, 0x01;
   my $stipple = Gtk2::Gdk::Bitmap->create_from_data( undef, $gray25_bits, 3, 3);

   $buffer->create_tag( "background_stipple", background_stipple => $stipple );

   $buffer->create_tag('rmap',
          background => 'red',
          background_stipple => $stipple , 
	  weight => PANGO_WEIGHT_BOLD,
	 );

   $buffer->create_tag('rmapX',
          background => 'red',
          background_stipple => $stipple , 
	  scale => PANGO_SCALE_X_LARGE,
	 );

   $buffer->create_tag('rmapZ',
           foreground => 'red',
           weight => PANGO_WEIGHT_BOLD,
	   size   => 15 * PANGO_SCALE
	 );

   $buffer->create_tag('blue',
           background => 'blue',
           );

}
######################################################################
sub key_handler {
my ($widget, $event) = @_;
    # match the keyval --- in general you don't want to use magic
    # numbers here, use values from %Gtk2::Gdk::Keysyms instead.

    if (($seeking) && ($event->keyval == 32)) {  #detect spacebar hit to cancel search 
       $cancel = 1;       
       $seeking = 0;
       return TRUE;  # tell the system we handled this
     }

    if ($event->keyval == 65293) {  #detect Enter press
       do_dir_search() unless $seeking;
       return TRUE;  # tell the system we handled this
     }


return FALSE;  # tell the system we did not handle this
}
######################################################################
sub print_usage{
  $textbuffer->delete($textbuffer->get_start_iter,$textbuffer->get_end_iter,);  
  $textbuffer->set_text($help);  
}
######################################################################
__DATA__
# USAGE: 
# -- takes a quoted string on the command line to search for
# -- you can give options n r c, separately on the commandline
# -- n ->search filenames only
# -- r ->recurse into sub directories 
# -- c ->case sensitive 
# Will open with no options or search string, in empty mode.

# Found files, will be displayed as blue underlined hyperlinks.
# Under each link, will be the line numbers and matched lines. 
# Clicking blue links will open the file, and all matched lines will
# be displayed in red text.

#  Control-c from anywhere will kill the program.
#  SpaceBar cancels a long search

#  Multi-word search strings on the command line should be
#  single quoted.
#  Search strings entered in the entry box, do not need quotes  
#  and will be submitted by an enter keypress, or the find button.

#  The line numbering can be turned off for copying and pasting from
#  the opened files. A right click in the lower textbox, will open
#  the textbox menu for Copying to the Clipboard for pasting with
#  the paste options from other apps (Control-v). Otherwise, the selected
#  text is only in the mouse clipboard buffer.
__END__


I'm not really a human, but I play one on earth. flash japh
Re^5: Gtk2 Visual Grep
created: 2006-01-24 09:42:12
Thanks a bunch, ++zentara!

Last thing I was thinking about (except excluding dirs (for me it would be .svn dirs or so)) would be a start dir parameter. But I can easily do that myself.

Regards,
svenXY

perlmonks.org content © perlmonks.org and svenXY, zentara

prlmnks.org © 2006 edmund von der burg (eccles & toad)

v 0.03