ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/rxvt-unicode/src/perl/matcher
Revision: 1.19
Committed: Sat Jun 7 20:07:38 2014 UTC (9 years, 11 months ago) by sf-exg
Branch: MAIN
Changes since 1.18: +8 -1 lines
Log Message:
Make matcher:list really compute the 10 'most recent' matches.

File Contents

# User Rev Content
1 root 1.1 #! perl
2    
3 sf-tpope 1.7 # Author: Tim Pope <rxvt-unicodeNOSPAM@tpope.org>
4 root 1.14 # Bob Farrell <robertanthonyfarrell@gmail.com>
5 root 1.1
6 root 1.15 #:META:RESOURCE:%.launcher:string:default launcher command
7     #:META:RESOURCE:%.button:string:the button, yeah
8     #:META:RESOURCE:%.pattern.:string:extra pattern to match
9     #:META:RESOURCE:%.launcher.:string:custom launcher for pattern
10     #:META:RESOURCE:%.rend.:string:custom rednition for pattern
11 root 1.8
12 root 1.10 =head1 NAME
13    
14 root 1.13 matcher - match strings in terminal output and change their rendition
15 root 1.10
16 root 1.11 =head1 DESCRIPTION
17 root 1.10
18     Uses per-line display filtering (C<on_line_update>) to underline text
19     matching a certain pattern and make it clickable. When clicked with the
20     mouse button specified in the C<matcher.button> resource (default 2, or
21     middle), the program specified in the C<matcher.launcher> resource
22 sf-exg 1.16 (default, the C<url-launcher> resource, C<sensible-browser>) will be started
23 root 1.10 with the matched text as first argument. The default configuration is
24     suitable for matching URLs and launching a web browser, like the
25     former "mark-urls" extension.
26    
27     The default pattern to match URLs can be overridden with the
28     C<matcher.pattern.0> resource, and additional patterns can be specified
29     with numbered patterns, in a manner similar to the "selection" extension.
30     The launcher can also be overridden on a per-pattern basis.
31    
32     It is possible to activate the most recently seen match or a list of matches
33     from the keyboard. Simply bind a keysym to "perl:matcher:last" or
34     "perl:matcher:list" as seen in the example below.
35    
36 root 1.14 Example: load and use the matcher extension with defaults.
37 root 1.10
38     URxvt.perl-ext: default,matcher
39 root 1.14
40     Example: use a custom configuration.
41    
42 root 1.10 URxvt.url-launcher: sensible-browser
43     URxvt.keysym.C-Delete: perl:matcher:last
44     URxvt.keysym.M-Delete: perl:matcher:list
45     URxvt.matcher.button: 1
46     URxvt.matcher.pattern.1: \\bwww\\.[\\w-]+\\.[\\w./?&@#-]*[\\w/-]
47     URxvt.matcher.pattern.2: \\B(/\\S+?):(\\d+)(?=:|$)
48     URxvt.matcher.launcher.2: gvim +$2 $1
49    
50     =cut
51    
52 root 1.1 my $url =
53     qr{
54 tpope 1.2 (?:https?://|ftp://|news://|mailto:|file://|\bwww\.)
55 sf-tpope 1.12 [\w\-\@;\/?:&=%\$.+!*\x27,~#]*
56 tpope 1.2 (
57 sf-tpope 1.12 \([\w\-\@;\/?:&=%\$.+!*\x27,~#]*\)| # Allow a pair of matched parentheses
58     [\w\-\@;\/?:&=%\$+*~] # exclude some trailing characters (heuristic)
59 tpope 1.2 )+
60 root 1.1 }x;
61    
62 sf-tpope 1.6 sub on_key_press {
63     my ($self, $event, $keysym, $octets) = @_;
64    
65 sf-exg 1.18 return unless $self->{overlay};
66    
67     delete $self->{overlay};
68 sf-tpope 1.6
69     my $i = ($keysym == 96 ? 0 : $keysym - 48);
70 sf-exg 1.18 if ($i >= 0 && $i < @{ $self->{matches} }) {
71     my @exec = @{ $self->{matches}[$i] };
72     shift @exec;
73     $self->exec_async (@exec);
74 sf-tpope 1.6 }
75    
76 sf-exg 1.18 1
77 sf-tpope 1.6 }
78    
79 root 1.14 # backwards compat
80 tpope 1.4 sub on_user_command {
81     my ($self, $cmd) = @_;
82 sf-tpope 1.6
83 root 1.14 if ($cmd =~ s/^matcher:list\b//) {
84     $self->matchlist;
85 sf-tpope 1.6 } else {
86 root 1.14 if ($cmd =~ s/^matcher:last\b//) {
87 sf-tpope 1.6 $self->most_recent;
88 root 1.14 } elsif ($cmd =~ s/^matcher\b//) {
89     # for backward compatibility
90 sf-tpope 1.6 $self->most_recent;
91     }
92 tpope 1.4 }
93 root 1.14
94 tpope 1.4 ()
95     }
96    
97 sf-tpope 1.6 sub matchlist {
98     my ($self) = @_;
99 sf-exg 1.17
100 sf-exg 1.18 @{ $self->{matches} } = ();
101     my $row = $self->nrow - 1;
102     while ($row >= 0 && @{ $self->{matches} } < 10) {
103     my $line = $self->line ($row);
104     my $text = $line->t;
105    
106     # FIXME: code duplicated from 'command_for'
107 sf-exg 1.19 my @matches;
108 sf-exg 1.18 for my $matcher (@{$self->{matchers}}) {
109     my $launcher = $matcher->[1] || $self->{launcher};
110     while ($text =~ /$matcher->[0]/g) {
111     my $match = substr ($text, $-[0], $+[0] - $-[0]);
112     my @beg = @-;
113     my @end = @+;
114     my @exec;
115 sf-exg 1.17
116 sf-exg 1.18 if ($launcher !~ /\$/) {
117     @exec = ($launcher, $match);
118     } else {
119     @exec = map { s/\$(\d+)|\$\{(\d+)\}/
120     substr ($text, $beg[$1 || $2], $end[$1 || $2] - $beg[$1 || $2])
121     /egx; $_ } split /\s+/, $launcher;
122     }
123 sf-exg 1.17
124 sf-exg 1.19 push @matches, [ $beg[0], $match, @exec ];
125 sf-exg 1.18 }
126     }
127 sf-exg 1.17
128 sf-exg 1.19 for (sort { $b->[0] <=> $a->[0] } @matches) {
129     shift @$_;
130     push @{ $self->{matches} }, $_;
131     last if @{ $self->{matches} } == 10;
132     }
133    
134 sf-exg 1.18 $row = $line->beg - 1;
135     }
136 sf-exg 1.17
137 sf-exg 1.18 return unless @{ $self->{matches} };
138 sf-exg 1.17
139 sf-exg 1.18 my $width = 0;
140 sf-exg 1.17
141 sf-exg 1.18 my $i = 0;
142     for my $match (@{ $self->{matches} }) {
143     my $text = $match->[0];
144     my $w = $self->strwidth ("$i-$text");
145 sf-exg 1.17
146 sf-exg 1.18 $width = $w if $w > $width;
147     $i++;
148 sf-exg 1.17 }
149    
150 sf-exg 1.18 $width = $self->ncol - 2 if $width > $self->ncol - 2;
151 sf-exg 1.17
152 sf-exg 1.18 $self->{overlay} = $self->overlay (0, 0, $width, scalar (@{ $self->{matches} }), urxvt::OVERLAY_RSTYLE, 2);
153 sf-exg 1.17 my $i = 0;
154 sf-exg 1.18 for my $match (@{ $self->{matches} }) {
155     my $text = $match->[0];
156    
157     $self->{overlay}->set (0, $i, "$i-$text");
158 sf-exg 1.17 $i++;
159     }
160 sf-tpope 1.6
161     }
162    
163 tpope 1.4 sub most_recent {
164     my ($self) = shift;
165     my $row = $self->nrow;
166     my @exec;
167     while($row-- > $self->top_row) {
168     @exec = $self->command_for($row);
169     last if(@exec);
170     }
171     if(@exec) {
172     return $self->exec_async (@exec);
173     }
174     ()
175     }
176 tpope 1.2
177 root 1.1 sub my_resource {
178 root 1.9 $_[0]->x_resource ("%.$_[1]")
179 root 1.1 }
180    
181 tpope 1.2 # turn a rendition spec in the resource into a sub that implements it on $_
182     sub parse_rend {
183     my ($self, $str) = @_;
184 sf-tpope 1.6 my ($mask, $fg, $bg, $failed) = $str ? urxvt::rend2mask($str)
185 tpope 1.2 : (urxvt::RS_Uline, undef, undef, []);
186     warn "Failed to parse rendition string: " . join(',', @$failed) if @$failed;
187     my @rend;
188     push @rend, sub { $_ |= $mask } if $mask;
189     push @rend, sub { $_ = urxvt::SET_FGCOLOR($_, $fg) } if defined $fg;
190     push @rend, sub { $_ = urxvt::SET_BGCOLOR($_, $bg) } if defined $bg;
191     sub {
192     for my $s ( @rend ) { &$s };
193     }
194     }
195    
196 root 1.1 sub on_start {
197     my ($self) = @_;
198    
199 root 1.8 $self->{launcher} = $self->my_resource ("launcher") || $self->x_resource("url-launcher") || "sensible-browser";
200 root 1.1
201 sf-exg 1.18 $self->{matches} = [];
202 root 1.1 $self->{button} = 2;
203     $self->{state} = 0;
204 root 1.8 if($self->{argv}[0] || $self->my_resource ("button")) {
205     my @mods = split '', $self->{argv}[0] || $self->my_resource ("button");
206 root 1.1 for my $mod (@mods) {
207     if($mod =~ /^\d+$/) {
208     $self->{button} = $mod;
209     } elsif($mod eq "C") {
210     $self->{state} |= urxvt::ControlMask;
211     } elsif($mod eq "S") {
212     $self->{state} |= urxvt::ShiftMask;
213     } elsif($mod eq "M") {
214     $self->{state} |= $self->ModMetaMask;
215     } elsif($mod ne "-" && $mod ne " ") {
216 root 1.9 warn("$mod is invalid in $self->{_name}<$self->{argv}[0]>\n");
217 root 1.1 }
218     }
219     }
220    
221     my @defaults = ($url);
222     my @matchers;
223 root 1.8 for (my $idx = 0; defined (my $res = $self->my_resource ("pattern.$idx") || $defaults[$idx]); $idx++) {
224 root 1.1 $res = $self->locale_decode ($res);
225     utf8::encode $res;
226 root 1.8 my $launcher = $self->my_resource ("launcher.$idx");
227     $launcher =~ s/\$&|\$\{&\}/\${0}/g if $launcher;
228     my $rend = $self->parse_rend($self->my_resource ("rend.$idx"));
229 tpope 1.2 unshift @matchers, [qr($res)x,$launcher,$rend];
230 root 1.1 }
231     $self->{matchers} = \@matchers;
232    
233     ()
234     }
235    
236     sub on_line_update {
237     my ($self, $row) = @_;
238    
239     # fetch the line that has changed
240     my $line = $self->line ($row);
241     my $text = $line->t;
242     my $i = 0;
243    
244     # find all urls (if any)
245     for my $matcher (@{$self->{matchers}}) {
246     while ($text =~ /$matcher->[0]/g) {
247 tpope 1.2 #print "$&\n";
248 root 1.1 my $rend = $line->r;
249    
250     # mark all characters as underlined. we _must_ not toggle underline,
251     # as we might get called on an already-marked url.
252 tpope 1.2 &{$matcher->[2]}
253 root 1.1 for @{$rend}[ $-[0] .. $+[0] - 1];
254    
255     $line->r ($rend);
256     }
257     }
258    
259     ()
260     }
261    
262     sub valid_button {
263     my ($self, $event) = @_;
264     my $mask = $self->ModLevel3Mask | $self->ModMetaMask
265     | urxvt::ShiftMask | urxvt::ControlMask;
266     return ($event->{button} == $self->{button} &&
267     ($event->{state} & $mask) == $self->{state});
268     }
269    
270     sub command_for {
271     my ($self, $row, $col) = @_;
272     my $line = $self->line ($row);
273     my $text = $line->t;
274    
275     for my $matcher (@{$self->{matchers}}) {
276     my $launcher = $matcher->[1] || $self->{launcher};
277     while (($text =~ /$matcher->[0]/g)) {
278     my $match = $&;
279     my @begin = @-;
280     my @end = @+;
281 tpope 1.2 if (!defined($col) || ($-[0] <= $col && $+[0] >= $col)) {
282 root 1.1 if ($launcher !~ /\$/) {
283     return ($launcher,$match);
284     } else {
285     # It'd be nice to just access a list like ($&,$1,$2...),
286     # but alas, m//g behaves differently in list context.
287     my @exec = map { s/\$(\d+)|\$\{(\d+)\}/
288     substr($text,$begin[$1||$2],$end[$1||$2]-$begin[$1||$2])
289     /egx; $_ } split(/\s+/, $launcher);
290     return @exec;
291     }
292     }
293     }
294     }
295    
296     ()
297     }
298    
299     sub on_button_press {
300     my ($self, $event) = @_;
301 sf-tpope 1.5 if($self->valid_button($event)
302     && (my @exec = $self->command_for($event->{row},$event->{col}))) {
303 root 1.1 $self->{row} = $event->{row};
304     $self->{col} = $event->{col};
305 sf-tpope 1.5 $self->{cmd} = \@exec;
306     return 1;
307 root 1.1 } else {
308     delete $self->{row};
309     delete $self->{col};
310 sf-tpope 1.5 delete $self->{cmd};
311 root 1.1 }
312    
313     ()
314     }
315    
316     sub on_button_release {
317     my ($self, $event) = @_;
318    
319     my $row = delete $self->{row};
320     my $col = delete $self->{col};
321 sf-tpope 1.5 my $cmd = delete $self->{cmd};
322 root 1.1
323 sf-tpope 1.5 return if !defined $row;
324    
325     if($row == $event->{row} && abs($col-$event->{col}) < 2
326     && join("\x00", @$cmd) eq join("\x00", $self->command_for($row,$col))) {
327 root 1.1 if($self->valid_button($event)) {
328    
329 sf-exg 1.17 $self->exec_async (@$cmd);
330 root 1.1
331     }
332     }
333    
334 sf-tpope 1.5 1;
335 root 1.1 }
336    
337     # vim:set sw=3 sts=3 et: