restructure configure so pkg-config derived SSL flags get used
[rofl0r-ixchat.git] / plugins / perl / alt_completion.pl
blob71c1884dfff5251f5abfc081ce98c5d4f5da8514
1 use strict;
2 use warnings;
3 use Xchat ();
4 use File::Spec ();
5 use File::Basename qw(fileparse);
7 # if the last time you addressed someone was greater than this many minutes
8 # ago, ignore it
9 # this avoids having people you have talked to a long time ago coming up too
10 # early in the completion list
11 # Setting this to 0 will disable the check which is effectively the same as
12 # setting it to infinity
13 my $last_use_threshold = 10; # 10 minutes
15 # added to the front of a completion the same way as a suffix, only if
16 # the word is at the beginning of the line
17 my $prefix = '';
19 # ignore leading non-alphanumeric characters: -[\]^_`{|}
20 # Assuming you have the following nicks in a channel:
21 # [SomeNick] _SomeNick_ `SomeNick SomeNick SomeOtherNick
22 # when $ignore_leading_non_alnum is set to 0
23 # s<tab> will cycle through SomeNick and SomeOtherNick
24 # when $ignore_leading_non_alnum is set to 1
25 # s<tab> will cycle through [SomeNick] _SomeNick_ `SomeNick SomeNick
26 # SomeOtherNick
27 my $ignore_leading_non_alnum = 0;
29 # enable path completion
30 my $path_completion = 1;
31 my $base_path = '';
33 # ignore the completion_amount setting and always cycle through nicks with tab
34 my $always_cycle = 0;
36 Xchat::register(
37 "Tab Completion", "1.0500", "Alternative tab completion behavior"
39 Xchat::hook_print( "Key Press", \&complete );
40 Xchat::hook_print( "Close Context", \&close_context );
41 Xchat::hook_print( "Focus Tab", \&focus_tab );
42 Xchat::hook_print( "Part", \&clean_selected );
43 Xchat::hook_print( "Part with Reason", \&clean_selected );
44 Xchat::hook_command( "", \&track_selected );
46 sub SHIFT() { 1 }
47 sub CTRL() { 4 }
48 sub ALT() { 8 }
50 sub TAB() { 0xFF09 }
51 sub LEFT_TAB() { 0xFE20 }
53 my %completions;
54 my %last_visit;
55 my %selected;
56 my %escape_map = (
57 '[' => qr![\[{]!,
58 '{' => qr![\[{]!,
59 '}' => qr![\]}]!,
60 ']' => qr![\]}]!,
61 '\\' => qr![\\\|]!,
62 '|' => qr![\\\|]!,
63 '.' => qr!\.!,
64 '^' => qr!\^!,
65 '$' => qr!\$!,
66 '*' => qr!\*!,
67 '+' => qr!\+!,
68 '?' => qr!\?!,
69 '(' => qr!\(!,
70 ')' => qr!\)!,
71 '-' => qr!\-!,
74 my $escapes = join "", keys %escape_map;
75 $escapes = qr/[\Q$escapes\E]/;
77 # used to determine if a word is the start of a path
78 my $path_pattern = qr{^(?:~|/|[[:alpha:]]:\\)};
80 sub complete {
81 my ($key, $modifiers) = @{$_[0]};
82 # if $_[0][0] contains the value of the key pressed
83 # $_[0][1] contains modifiers
84 # the value for tab is 0xFF09
85 # the value for shift-tab(Left Tab) is 0xFE20
86 # we don't care about other keys
88 # the key must be a tab and left tab
89 return Xchat::EAT_NONE unless $key == TAB || $key == LEFT_TAB;
91 # if it is a tab then it must not have any modifiers
92 return Xchat::EAT_NONE if $key == TAB && $modifiers & (CTRL|ALT|SHIFT);
94 # loop backwards for shift+tab/left tab
95 my $delta = $modifiers & SHIFT ? -1 : 1;
96 my $context = Xchat::get_context;
97 $completions{$context} ||= {};
99 my $completions = $completions{$context};
100 $completions->{pos} ||= -1;
102 my $suffix = Xchat::get_prefs( "completion_suffix" );
103 $suffix =~ s/^\s+//;
105 my $input = Xchat::get_info( "inputbox" );
106 my $cursor_pos = Xchat::get_info( "state_cursor" );
107 my $left = substr( $input, 0, $cursor_pos );
108 my $right = substr( $input, $cursor_pos );
109 my $length = length $left;
111 # trim spaces from the end of $left to avoid grabbing the wrong word
112 # this is mainly needed for completion at the very beginning where a space
113 # is added after the completion
114 $left =~ s/\s+$//;
116 # always add one to the index because
117 # 1) if a space is found we want the position after it
118 # 2) if a space isn't found then we get back -1
119 my $word_start = rindex( $left, " " ) + 1;
120 my $word = substr( $left, $word_start );
121 $left = substr( $left, 0, -length $word );
123 if( $cursor_pos == $completions->{pos} ) {
124 my $previous_word = $completions->{completed};
125 my $new_left = $input;
126 substr( $new_left, $cursor_pos ) = "";
128 if( $previous_word and $new_left =~ s/(\Q$previous_word\E)$// ) {
129 $word = $1;
130 $word_start = length( $new_left );
131 $left = $new_left;
135 my $command_char = Xchat::get_prefs( "input_command_char" );
136 # ignore commands
137 if( ($word !~ m{^[${command_char}]})
138 or ( $word =~ m{^[${command_char}]} and $word_start != 0 ) ) {
140 if( $cursor_pos == length $input # end of input box
141 # not a valid nick char
142 && $input =~ /(?<![\x41-\x5A\x61-\x7A\x30-\x39\x5B-\x60\x7B-\x7D-])$/
143 && $cursor_pos != $completions->{pos} # not continuing a completion
144 && $word !~ m{^(?:[&#/~]|[[:alpha:]]:\\)} # not a channel or path
146 # check for path completion
147 unless( $path_completion and $word =~ $path_pattern ) {
148 $word_start = $cursor_pos;
149 $left = $input;
150 $length = length $length;
151 $right = "";
152 $word = "";
156 if( $word_start == 0 && $prefix && $word =~ /^\Q$prefix/ ) {
157 $word =~ s/^\Q$prefix//;
160 my $completed; # this is going to be the "completed" word
162 # for parital completions and channel names so a : isn't added
163 #$completions->{skip_suffix} = ($word =~ /^[&#]/) ? 1 : 0;
165 # continuing from a previous completion
167 exists $completions->{matches} && @{$completions->{matches}}
168 && $cursor_pos == $completions->{pos}
169 && $word =~ /^\Q$completions->{matches}[$completions->{index}]/
171 $completions->{index} += $delta;
173 if( $completions->{index} < 0 ) {
174 $completions->{index} += @{$completions->{matches}};
175 } else {
176 $completions->{index} %= @{$completions->{matches}};
179 } else {
181 if( $word =~ /^[&#]/ ) {
182 # channel name completion
183 $completions->{matches} = [ matching_channels( $word ) ];
184 $completions->{skip_suffix} = 0;
185 } elsif( $path_completion and $word =~ $path_pattern ) {
186 # file name completion
187 $completions->{matches} = [ matching_files( $word ) ];
188 $completions->{skip_suffix} = 1;
189 } else {
190 # nick completion
191 # fix $word so { equals [, ] equals }, \ equals |
192 # and escape regex metacharacters
193 $word =~ s/($escapes)/$escape_map{$1}/g;
195 $completions->{matches} = [ matching_nicks( $word ) ];
196 $completions->{skip_suffix} = 0;
198 $completions->{index} = 0;
201 $completed = $completions->{matches}[ $completions->{index} ];
202 $completions->{completed} = $completed;
204 my $completion_amount = Xchat::get_prefs( "completion_amount" );
206 # don't cycle if the number of possible completions is greater than
207 # completion_amount
209 !$always_cycle && (
210 @{$completions->{matches}} > $completion_amount
211 && @{$completions->{matches}} != 1 )
213 # don't print if we tabbed in the beginning and the list of possible
214 # completions includes all nicks in the channel
215 my $context_type = Xchat::context_info->{type};
216 if( $context_type != 2 # not a channel
217 or @{$completions->{matches}} < Xchat::get_list("users")
219 Xchat::print( join " ", @{$completions->{matches}}, "\n" );
222 $completed = lcs( $completions->{matches} );
223 $completions->{skip_suffix} = 1;
226 if( $completed ) {
228 if( $word_start == 0 && !$completions->{skip_suffix} ) {
229 # at the start of the line append completion suffix
230 Xchat::command( "settext $prefix$completed$suffix$right");
231 $completions->{pos} = length( "$prefix$completed$suffix" );
232 } else {
233 Xchat::command( "settext $left$completed$right" );
234 $completions->{pos} = length( "$left$completed" );
237 Xchat::command( "setcursor $completions->{pos}" );
240 =begin
241 # debugging stuff
242 local $, = " ";
243 my $input_length = length $input;
244 Xchat::print [
245 qq{input[$input]},
246 qq{input_length[$input_length]},
247 qq{cursor[$cursor_pos]},
248 qq{start[$word_start]},
249 qq{length[$length]},
250 qq{left[$left]},
251 qq{word[$word]}, qq{right[$right]},
252 qq{completed[}. ($completed||""). qq{]},
253 qq{pos[$completions->{pos}]},
255 use Data::Dumper;
256 local $Data::Dumper::Indent = 0;
257 Xchat::print Dumper $completions->{matches};
258 =cut
260 return Xchat::EAT_ALL;
261 } else {
262 return Xchat::EAT_NONE;
267 # all channels starting with $word
268 sub matching_channels {
269 my $word = shift;
271 # for use in compare_channels();
272 our $current_chan;
273 local $current_chan = Xchat::get_info( "channel" );
275 my $conn_id = Xchat::get_info( "id" );
276 $word =~ s/^[&#]+//;
278 return
279 map { $_->[1]->{channel} }
280 sort compare_channels map {
281 my $chan = $_->{channel};
282 $chan =~ s/^[#&]+//;
284 # comparisons will be done based only on the name
285 # matching name, same connection, only channels
286 $chan =~ /^$word/i && $_->{id} == $conn_id ?
287 [ $chan, $_ ] :
289 } channels();
292 sub channels {
293 return grep { $_->{type} == 2 } Xchat::get_list( "channels" );
296 sub compare_channels {
297 # package variable, value set in matching_channels()
298 our $current_chan;
300 # turn off warnings generated from channels that have not yet been visited
301 # since the script was loaded
302 no warnings "uninitialized";
304 # the current channel is always first, then ordered by most recently visited
305 return
306 $a->[1]{channel} eq $current_chan ? -1 :
307 $b->[1]{channel} eq $current_chan ? 1 :
308 $last_visit{ $b->[1]{context} } <=> $last_visit{ $a->[1]{context} }
309 || $a->[1]{channel} cmp $b->[1]{channel};
313 sub matching_nicks {
314 my $word_re = shift;
316 # for use in compare_nicks()
317 our ($my_nick, $selections, $now);
318 local $my_nick = Xchat::get_info( "nick" );
319 local $selections = $selected{ Xchat::get_context() };
320 local $now = time;
322 my $pattern = $ignore_leading_non_alnum ?
323 qr/^[\-\[\]^_`{|}\\]*$word_re/i : qr/^$word_re/i;
324 return
325 map { $_->{nick} }
326 sort compare_nicks grep {
327 $_->{nick} =~ $pattern;
328 } Xchat::get_list( "users" )
332 sub max {
333 return unless @_;
334 my $max = shift;
335 for(@_) {
336 $max = $_ if $_ > $max;
338 return $max;
341 sub compare_times {
342 # package variables set in matching_nicks()
343 our $selections;
344 our $now;
346 for my $nick ( $a->{nick}, $b->{nick} ) {
347 # turn off the warnings that get generated from users who have yet
348 # to speak since the script was loaded
349 no warnings "uninitialized";
351 if( $last_use_threshold
352 && (( $now - $selections->{$nick}) > ($last_use_threshold * 60)) ) {
353 delete $selections->{ $nick }
356 my $a_time = $selections->{ $a->{nick} } || 0 ;
357 my $b_time = $selections->{ $b->{nick} } || 0 ;
359 if( $a_time || $b_time ) {
360 return $b_time <=> $a_time;
361 } elsif( !$a_time && !$b_time ) {
362 return $b->{lasttalk} <=> $a->{lasttalk};
367 sub compare_nicks {
368 # more package variables, value set in matching_nicks()
369 our $my_nick;
371 # our own nick is always last, then ordered by the people we spoke to most
372 # recently and the people who were speaking most recently
373 return
374 $a->{nick} eq $my_nick ? 1 :
375 $b->{nick} eq $my_nick ? -1 :
376 compare_times()
377 || Xchat::nickcmp( $a->{nick}, $b->{nick} );
379 # $selections->{ $b->{nick} } <=> $selections->{ $a->{nick} }
380 # || $b->{lasttalk} <=> $a->{lasttalk}
384 sub matching_files {
385 my $word = shift;
387 my ($file, $input_dir) = fileparse( $word );
389 my $dir = expand_tilde( $input_dir );
391 if( opendir my $dir_handle, $dir ) {
392 my @files;
394 if( $file ) {
395 @files = grep {
396 #Xchat::print( $_ );
397 /^\Q$file/ } readdir $dir_handle;
398 } else {
399 @files = readdir $dir_handle;
402 return map {
403 File::Spec->catfile( $input_dir, $_ );
404 } sort
405 grep { !/^[.]{1,2}$/ } @files;
406 } else {
407 return ();
411 # Remove completion related data for tabs that are closed
412 sub close_context {
413 my $context = Xchat::get_context;
414 delete $completions{$context};
415 delete $last_visit{$context};
416 return Xchat::EAT_NONE;
419 # track visit times
420 sub focus_tab {
421 $last_visit{Xchat::get_context()} = time();
422 return Xchat::EAT_NONE;
425 # keep track of the last time a message was addressed to someone
426 # a message is considered addressed to someone if their nick is used followed
427 # by the completion suffix
428 sub track_selected {
429 my $input = $_[1][0];
430 return Xchat::EAT_NONE unless defined $input;
432 my $suffix = Xchat::get_prefs( "completion_suffix" );
433 for( grep defined, $input =~ /^(.+)\Q$suffix/, $_[0][0] ) {
434 if( in_channel( $_ ) ) {
435 $selected{Xchat::get_context()}{$_} = time();
436 last;
440 return Xchat::EAT_NONE;
443 # if a user is in the current channel
444 # user_info() can also be used instead of the loop
445 sub in_channel {
446 my $target = shift;
447 for my $nick ( nicks() ) {
448 if( $nick eq $target ) {
449 return 1;
453 return 0;
456 # list of nicks in the current channel
457 sub nicks {
458 return map { $_->{nick} } Xchat::get_list( "users" );
461 # remove people from the selected list when they leave the channel
462 sub clean_selected {
463 delete $selected{ Xchat::get_context() }{$_[0][0]};
464 return Xchat::EAT_NONE;
467 # Longest common substring
468 # Used for partial completion when using non-cycling completion
469 sub lcs {
470 my @nicks = @{+shift};
471 return "" if @nicks == 0;
472 return $nicks[0] if @nicks == 1;
474 my $substring = shift @nicks;
476 while(@nicks) {
477 $substring = common_string( $substring, shift @nicks );
480 return $substring;
483 sub common_string {
484 my ($nick1, $nick2) = @_;
485 my $index = 0;
487 $index++ while(
488 ($index < length $nick1) && ($index < length $nick2) &&
489 lc(substr( $nick1, $index, 1 )) eq lc(substr( $nick2, $index, 1 ))
493 return substr( $nick1, 0, $index );
496 sub expand_tilde {
497 my $file = shift;
499 $file =~ s/^~/home_dir()/e;
500 return $file;
503 sub home_dir {
504 return $base_path if $base_path;
506 if ( $^O eq "MSWin32" ) {
507 return $ENV{USERPROFILE};
508 } else {
509 return ((getpwuid($>))[7] || $ENV{HOME} || $ENV{LOGDIR});