aboutsummaryrefslogblamecommitdiffstats
path: root/ido-mode/ido_switcher.pl
blob: eca29ffe151183ab92ab82b9f2cba039016f4d27 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13












                                                                               
                                     
 
                                                       




























                                                                            
                                 
                                                                                  






                                                                                        










                                                                         

                                                                              
 
                        
                       
 
                                                             
 
                        
                        
 
                    
                     

                    


                      
                   
 

                                


                            



                               


                                


                                    
 

                    

                                  
            

                                
                    

                                             
 
                  
                        
                                




                                                  
                              





                                                             




















                                                                               
 










                                                       
 


                                            

 









                                                           
                                         
 
                                                                    
                                       

                                                                
                                                
 


                                                      
                      


                            
                                                                   
                                                                 
                                                                    



                                               



                                                                  
 
                                                                







                                                                    


                                                                   


 

                                                
                                                 





                                                 

                            
                               
 

                                                               
                        
 
                                                                              

                    
 
                     
                                  

 



                                      






























                                                        






                                        
                                                          
                                                       
         
                            
                                       
                                                                           
                                                           
             
                
                                                                         
                                       

         


                                
 

 


                             
 
                                                     
 

                    
 

                                        
 
                                                              
 





                                                                           
 

     

                               
 
                       
 



                                                            
 



                                                   
 


































                                                                          
 

                                                               
 
                                           
 


                                                                      
 

                                                       
 



                                                                   
 




                                                
 
                       
 



                                                              
 
                                                                        
 




                                                                            
     
 
 
 

                                
 


                                                                     
 




                                                                     
 





                                                                  
 







                                             
     
 




                                     
 
                        
 
                                           
 





                                                                                                         
 
                                      
 



                                                              
 
                                 
 



                                                           
 
                
 




                                                               
 
     
 








                                                                
     
 

                       
 

                                   
 
                                                      
 

                                                                              
 
                                 
 


                                           
 
                                    
 
                                       
 


                                                     
 


                                                   
 

                                                                  
 
                    
 

                                                   
 

                          
         
 
                                            
 

                                    
 

                    
 
                    
 





                                               
     
 
                    
 





                                               
 

                                             
     
 

                         
 
                                         
 

                                            
 


                                            
 


                                 
 









                                           
 






                                            
 
                                     
 

                                              
 

                                          
 







                                        
 



                                                          
 



                                 
 



                                                  
 



                                                                             
 











                                             
 


                                                  
 

                                          
 


                                 
 





                                           
 



                                          
 

                                 
 







                                                               
 

                                          
 

                                 
         
 























                                                     
 
                                
                             

     
                      
 
                                      
 










                                          
 


                                           
 
                                                                 
 

                                                        
 

                                                               
 








                                                                            
 
                                       
 


















                                                                            
# Search and select windows similar to ido-mode for emacs
#
# INSTALL:
#
# This script requires that you have first installed and loaded 'uberprompt.pl'
# Uberprompt can be downloaded from:
#
# http://github.com/shabble/irssi-scripts/raw/master/prompt_info/uberprompt.pl
#
# and follow the instructions at the top of that file for installation.
#
# USAGE:
#
# * Setup: /bind ^G /ido_switch_start
#
# * Then type ctrl-G and type what you're searching for
#
# Based in part on window_switcher.pl script Copyright 2007 Wouter Coekaerts
# <coekie@irssi.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

use strict;
use Irssi;
use Irssi::TextUI;
use Data::Dumper;

use vars qw($VERSION %IRSSI);
$VERSION = '2.0';
%IRSSI =
  (
   authors     => 'Tom Feist, Wouter Coekaerts',
   contact     => 'shabble+irssi@metavore.org, shabble@#irssi/freenode',
   name        => 'ido_switcher',
   description => 'Select window[-items] using an ido-mode like search interface',
   license     => 'GPLv2 or later',
   url         => 'http://github.com/shabble/irssi-scripts/tree/master/history-search/',
   changed     => '24/7/2010'
  );


# TODO:
# DONE C-g - cancel
# DONE C-spc - narrow
# DONE flex matching (on by default, but optional)
# TODO server/network narrowing
# DONE colourised output (via uberprompt)
# DONE C-r / C-s rotate matches
# TODO toggle queries/channels
# DONE remove inputline content, restore it afterwards.
# TODO tab - display all possibilities in window (clean up afterwards)
#       how exactly will this work?
# DONE sort by recent activity/recently used windows (separate commands?)
# TODO should space auto-move forward to next window for easy stepping through
#      sequential/active windows?

my $input_copy     = '';
my $input_pos_copy = 0;

my $ido_switch_active = 0;      # for intercepting keystrokes

my @window_cache   = ();
my @search_matches = ();

my $match_index = 0;
my $search_str  = '';
my $active_only = 0;

my $mode_type = 'ALL';
my @mode_cache;

my $need_clear = 0;

my $sort_ordering = "start-asc";

# /set configurable settings
my $ido_show_count;
my $ido_use_flex;

my $DEBUG_ENABLED = 0;
sub DEBUG () { $DEBUG_ENABLED }


sub MODE_WIN () { 0 } # windows
sub MODE_NET () { 1 } # chatnets
#sub MODE_C () { 2 } # channels
#sub MODE_S () { 3 } # select server
#sub MODE_W () { 4 } # select window

my $MODE = MODE_WIN;

# check we have uberprompt loaded.

sub _print {
    my $win = Irssi::active_win;
    my $str = join('', @_);
    $need_clear = 1;
    $win->print($str, Irssi::MSGLEVEL_NEVER);
}

sub _debug_print {
    return unless DEBUG;
    my $win = Irssi::active_win;
    my $str = join('', @_);
    $win->print($str, Irssi::MSGLEVEL_CLIENTCRAP);
}

sub _print_clear {
    return unless $need_clear;
    my $win = Irssi::active_win();
    $win->command('/scrollback levelclear -level NEVER');
}

sub print_all_matches {
    my $msg = join(", ", map { $_->{name} } @search_matches);
    my $message_header = "Windows:";
    my $win = Irssi::active_win();
    my $win_width = $win->{width} || 80;

    # TODO: needs to prefix ambig things with chatnet, or maybe order in groups
    # by chatnet with newlines.

    # Also, colourise the channel list.

    my $col_width;

    for (@search_matches) {
        my $len = length($_->{name});
        $col_width = $len if $len > $col_width;
    }

    my $cols = int($win_width / $col_width);

    my @lines;
    my $i = 0;
    my @line;

    for my $item (@search_matches) {
        my $name = $item->{name};
        push @line, sprintf('%.*s', $col_width, $name);
        if ($i == $cols) {
            push @lines, join ' ', @line;
            @line = ();
            $i = 0;
        }
    }
    # flush rest out.
    push @lines, join ' ', @line;

    _print($message_header);
    _print($_) for (@lines);
    #_print("Longtest name: $longest_name");
}

  sub script_is_loaded {
      my $name = shift;
      _debug_print "Checking if $name is loaded";
      no strict 'refs';
      my $retval = defined %{ "Irssi::Script::${name}::" };
      use strict 'refs';

      return $retval;
  }

unless (script_is_loaded('uberprompt')) {

    _print "This script requires 'uberprompt.pl' in order to work. "
      . "Attempting to load it now...";

    Irssi::signal_add('script error', 'load_uberprompt_failed');
    Irssi::command("script load uberprompt.pl");

    unless(script_is_loaded('uberprompt')) {
        load_uberprompt_failed("File does not exist");
    }
    ido_switch_init();
}

sub load_uberprompt_failed {
    Irssi::signal_remove('script error', 'load_uberprompt_failed');
    _print "Script could not be loaded. Script cannot continue. "
        . "Check you have uberprompt.pl installed in your path and "
        .  "try again.";
    die "Script Load Failed: " . join(" ", @_);
}

sub ido_switch_init {
    Irssi::settings_add_bool('ido_switch', 'ido_switch_debug', 0);
    Irssi::settings_add_bool('ido_switch', 'ido_use_flex',     1);
    Irssi::settings_add_int ('ido_switch', 'ido_show_count',   5);

    Irssi::command_bind('ido_switch_start', \&ido_switch_start);

    Irssi::signal_add      ('setup changed'   => \&setup_changed);
    Irssi::signal_add_first('gui key pressed' => \&handle_keypress);

    setup_changed();
}

sub setup_changed {
    $DEBUG_ENABLED  = Irssi::settings_get_bool('ido_switch_debug');
    $ido_show_count = Irssi::settings_get_int ('ido_show_count');
    $ido_use_flex   = Irssi::settings_get_bool('ido_use_flex');
}


sub ido_switch_start {
    # store copy of input line to restore later.
    $input_copy     = Irssi::parse_special('$L');
    $input_pos_copy = Irssi::gui_input_get_pos();

    Irssi::gui_input_set('');

    # set startup flags
    $ido_switch_active = 1;
    $search_str        = '';
    $match_index       = 0;
    $mode_type         = 'ALL';

    # refresh in case we toggled it last time.
    $ido_use_flex   = Irssi::settings_get_bool('ido_use_flex');
    $active_only    = 0;

    _debug_print "Win cache: " . join(", ", map { $_->{name} } @window_cache);

    _update_cache();

    update_matches();
    update_window_select_prompt();
}

sub _update_cache {
    @window_cache = get_all_windows();
}

sub _build_win_obj {
    my ($win, $win_item) = @_;

    my @base = (
                b_pos         => -1,
                e_pos         => -1,
                hilight_field => 'name',
                active        => $win->{data_level} > 0,
                num           => $win->{refnum},
                server        => $win->{active_server},

               );

    if (defined($win_item)) {
        return (
                @base,
                name     => $win_item->{visible_name},
                type     => $win_item->{type},
                itemname => $win_item->{name},
                active   => $win_item->{data_level} > 0,

               )
    } else {
        return (
                @base,
                name => $win->{name},
                type => 'WIN',
               );
    }
}

sub get_all_windows {
    my @ret;

    foreach my $win (Irssi::windows()) {
        my @items = $win->items();

        if ($win->{name} ne '') {
            _debug_print "Adding window: " . $win->{name};
            push @ret, { _build_win_obj($win, undef) };
        }
        if (scalar @items) {
            foreach my $item (@items) {
                _debug_print "Adding windowitem: " . $item->{visible_name};
                push @ret, { _build_win_obj($win, $item) };
            }
        } else {
            _debug_print "Error occurred reading info from window: $win";
            #_debug_print Dumper($win);
        }
    }
    @ret = _sort_windows(\@ret);

    return @ret;

}

    sub _sort_windows {
        my $list_ref = shift;
        my @ret = @$list_ref;

        @ret = sort { $a->{num} <=> $b->{num} } @ret;

        return @ret;
    }

    sub ido_switch_select {
        my ($selected, $is_refnum) = @_;

        _debug_print "Selecting window: " . $selected->{name};

        Irssi::command("WINDOW GOTO " . $selected->{name});

        if ($selected->{type} ne 'WINDOW') {
            _debug_print "Selecting window item: " . $selected->{itemname};
            Irssi::command("WINDOW ITEM GOTO " . $selected->{itemname});
        }

    }

    sub ido_switch_exit {
        $ido_switch_active = 0;

        _print_clear();

        Irssi::gui_input_set($input_copy);
        Irssi::gui_input_set_pos($input_pos_copy);
        Irssi::signal_emit('change prompt', '', 'UP_INNER');
    }

    sub _order_matches {
        return @_[$match_index .. $#_,
                  0            .. $match_index - 1]
    }

    sub update_window_select_prompt {

        # take the top $ido_show_count entries and display them.
        my $match_count  = scalar @search_matches;
        my $show_count   = $ido_show_count;
        my $match_string = '[No match';

        $show_count = $match_count if $match_count < $show_count;

        if ($show_count > 0) { # otherwise, default message above.
            _debug_print "Showing: $show_count matches";

            my @ordered_matches = _order_matches(@search_matches);

            my %uniq;
            # determine which items are non-unique, if any.
            foreach my $res (@ordered_matches) {
                my $name = $res->{name};

                if (exists $uniq{$name}) {
                    push @{$uniq{$name}}, $res;
                } else {
                    $uniq{$name} = [];
                    push @{$uniq{$name}}, $res;
                }
            }

            # and set a flag to ensure they have their network tag applied
            # to them when drawn.
            foreach my $name (keys %uniq) {
                my @values = @{$uniq{$name}};
                if (@values > 1) {
                    $_->{display_net} = 1 for @values;
                }
            }

            #
            my @display = @ordered_matches[0..$show_count - 1];

            # show the first entry in green

            my $first = shift @display;
            my $formatted_first = _format_display_entry($first, '%g');
            unshift @display, $formatted_first;

            # and array-slice-map the rest to be red.
            # or yellow, if they have unviewed activity

            @display[1..$#display]
              = map
              {
                  _format_display_entry($_, $_->{active}?'%y':'%r')

              } @display[1..$#display];

            # join em all up
            $match_string = join ', ', @display;
        }

        my @indicators;

        # indicator if flex mode is being used (C-f to toggle)
        push @indicators, $ido_use_flex ? 'Flex' : 'Exact';
        push @indicators, 'Active' if $active_only;
        push @indicators, ucfirst(lc($mode_type));

        my $flex = sprintf(' %%k[%%n%s%%k]%%n ', join ',', @indicators);

        my $search = '';
        $search = (sprintf '`%s\': ', $search_str) if length $search_str;

        Irssi::signal_emit('change prompt', $flex . $search . $match_string,
                           'UP_INNER');
    }



    sub _format_display_entry {
        my ($obj, $colour) = @_;

        my $field     = $obj->{hilight_field};
        my $hilighted = { name => $obj->{name}, num => $obj->{num} };
        my $show_tag  = $obj->{display_net} || 0;

        if ($obj->{b_pos} >= 0 && $obj->{e_pos} > $obj->{b_pos}) {
            substr($hilighted->{$field}, $obj->{e_pos}, 0) = '%_';
            substr($hilighted->{$field}, $obj->{b_pos}, 0) = '%_';
            _debug_print "Showing $field as: " . $hilighted->{$field}
        }

        return sprintf('%s%s:%s%s%%n',
                       $colour,
                       $hilighted->{num},
                       $show_tag ? _format_display_tag($obj) : '',
                       $hilighted->{name});
    }

    sub _format_display_tag {
        my $obj = shift;
        if (defined $obj->{server}) {
            my $server = $obj->{server};
            my $tag = $server->{tag};
            return $tag . '/' if length $tag;
        }
        return '';
    }

    sub _check_active {
        my ($obj) = @_;
        return 1 unless $active_only;
        return $obj->{active};
    }

    sub update_matches {

        _update_cache() unless $search_str;

        if ($mode_type ne 'ALL') {
            @mode_cache = @window_cache;
            @window_cache = grep { print "Type: " . $_->{type}; $_->{type} eq $mode_type } @window_cache;
        } else {
            @window_cache = @mode_cache if @mode_cache;
        }

        if ($search_str =~ m/^\d+$/) {

            @search_matches =
              grep {
                  _check_active($_) and regex_match($_, 'num')
              } @window_cache;

        } elsif ($ido_use_flex) {

            @search_matches =
              grep {
                  _check_active($_) and flex_match($_) >= 0
              } @window_cache;

        } else {

            @search_matches =
              grep {
                  _check_active($_) and regex_match($_, 'name')
              } @window_cache;
        }

    }

    sub regex_match {
        my ($obj, $field) = @_;
        if ($obj->{$field} =~ m/^(.*?)\Q$search_str\E(.*?)$/i) {
            $obj->{hilight_field} = $field;
            $obj->{b_pos} = length $1;
            $obj->{e_pos} = $obj->{b_pos} + length($search_str);
            return 1;
        }
        return 0;
    }

    sub flex_match {
        my ($obj) = @_;

        my $pattern = $search_str;
        my $source  = $obj->{name};

        _debug_print "Flex match: $pattern / $source";

        # default to matching everything if we don't have a pattern to compare
        # against.

        return 0 unless $pattern;

        my @chars = split '', lc($pattern);
        my $ret = -1;
        my $first = 0;

        my $lc_source = lc($source);

        $obj->{hilight_field} = 'name';

        foreach my $char (@chars) {
            my $pos = index($lc_source, $char, $ret);
            if ($pos > -1) {

                # store the beginning of the match
                $obj->{b_pos} = $pos unless $first;
                $first = 1;

                _debug_print("matched: $char at $pos in $source");
                $ret = $pos + 1;

            } else {

                $obj->{b_pos} = $obj->{e_pos} = -1;
                _debug_print "Flex returning: -1";

                return -1;
            }
        }

        _debug_print "Flex returning: $ret";

        #store the end of the match.
        $obj->{e_pos} = $ret;

        return $ret;
    }

    sub prev_match {

        $match_index++;
        if ($match_index > $#search_matches) {
            $match_index = 0;
        }

        _debug_print "index now: $match_index";
    }

    sub next_match {

        $match_index--;
        if ($match_index < 0) {
            $match_index = $#search_matches;
        }
        _debug_print "index now: $match_index";
    }

    sub get_window_match {
        return $search_matches[$match_index];
    }

    sub handle_keypress {
        my ($key) = @_;

        return unless $ido_switch_active;

        if ($key == 0) {        # C-SPC?
            _debug_print "\%_Ctrl-space\%_";

            $search_str = '';
            @window_cache = @search_matches;
            update_window_select_prompt();

            Irssi::signal_stop();
            return;
        }

        if ($key == 3) {        # C-c
            _print_clear();
            Irssi::signal_stop();
            return;
        }
        if ($key == 4) {        # C-d
            update_network_select_prompt();
            Irssi::signal_stop();
            return;
        }

        if ($key == 5) {        # C-e
            $active_only = not $active_only;
            Irssi::signal_stop();
            update_matches();
            update_window_select_prompt();
            return;
        }

        if ($key == 6) {        # C-f

            $ido_use_flex = not $ido_use_flex;
            _update_cache();

            update_matches();
            update_window_select_prompt();

            Irssi::signal_stop();
            return;
        }
        if ($key == 9) {        # TAB
            _debug_print "Tab complete";
            print_all_matches();
            Irssi::signal_stop();
        }

        if ($key == 10) {       # enter
            _debug_print "selecting history and quitting";
            my $selected_win = get_window_match();
            ido_switch_select($selected_win);

            ido_switch_exit();
            Irssi::signal_stop();
            return;
        }

        if ($key == 18) {       # Ctrl-R
            _debug_print "skipping to prev match";
            #update_matches();
            next_match();

            update_window_select_prompt();
            Irssi::signal_stop(); # prevent the bind from being re-triggered.
            return;
        }

        if ($key == 17) {       # Ctrl-q
            if ($mode_type eq 'CHANNEL') {
                $mode_type = 'QUERY';
            } elsif ($mode_type eq 'QUERY') {
                $mode_type = 'ALL';
            } else { # ALL
                $mode_type = 'CHANNEL';
            }
            update_matches();
            update_window_select_prompt();
            Irssi::signal_stop();
        }

        if ($key == 19) {       # Ctrl-s
            _debug_print "skipping to next match";
            prev_match();

            #update_matches();
            update_window_select_prompt();

            Irssi::signal_stop();
            return;
        }

        if ($key == 7) {        # Ctrl-g
            _debug_print "aborting search";
            ido_switch_exit();
            Irssi::signal_stop();
            return;
        }

        if ($key == 21) {       # Ctrl-u
            $search_str = '';
            update_matches();
            update_window_select_prompt();

            Irssi::signal_stop();
            return;

        }

        if ($key == 127) {      # DEL

            if (length $search_str) {
                $search_str = substr($search_str, 0, -1);
                _debug_print "Deleting char, now: $search_str";
            }

            update_matches();
            update_window_select_prompt();

            Irssi::signal_stop();
            return;
        }

        # TODO: handle esc- sequences and arrow-keys?

        if ($key == 27) {       # Esc
            ido_switch_exit();
            return;
        }

        if ($key == 32) {       # space
            my $selected_win = get_window_match();
            ido_switch_select($selected_win);
            Irssi::signal_stop();

            return;
        }

        if ($key > 32) {        # printable
            $search_str .= chr($key);

            update_matches();
            update_window_select_prompt();

            Irssi::signal_stop();
            return;
        }

        # ignore all other keys.
        Irssi::signal_stop();
    }

    ido_switch_init();

    sub update_network_select_prompt {

        my @servers = map
          {
              {
                  name => $_->{tag},
                  type => 'SERVER',
                  active => 0,
                  e_pos => -1,
                  b_pos => -1,
                  hilight_field => 'name',
              }
          } Irssi::servers();

        my $match_count  = scalar @servers;
        my $show_count   = $ido_show_count;
        my $match_string = '(no matches) ';

        $show_count = $match_count if $match_count < $show_count;

        if ($show_count > 0) {
            _debug_print "Showing: $show_count matches";

            my @ordered_matches = _order_matches(@servers);
            my @display = @ordered_matches[0..$show_count - 1];

            # show the first entry in green

            unshift(@display, _format_display_entry(shift(@display), '%g'));

            # and array-slice-map the rest to be red (or yellow for active)
            @display[1..$#display]
              = map
              {
                  _format_display_entry($_, $_->{active}?'%y':'%r')

              } @display[1..$#display];

            # join em all up
            $match_string = join ', ', @display;
        }

        my @indicators;

        # indicator if flex mode is being used (C-f to toggle)
        push @indicators, $ido_use_flex ? 'Flex' : 'Exact';
        push @indicators, 'Active' if $active_only;

        my $flex = sprintf(' %%k[%%n%s%%k]%%n ', join ',', @indicators);

        my $search = '';
        $search = (sprintf '`%s\': ', $search_str) if length $search_str;

        Irssi::signal_emit('change prompt', $flex . $search . $match_string,
                           'UP_INNER');

    }