1#! perl -w
2# Author:   Bert Muennich
3# Website:  http://www.github.com/muennich/urxvt-perls
4# Version:  2.0
5# License:  GPLv2
6
7# Use keyboard shortcuts to select and copy text.
8
9# Usage: put the following lines in your .Xdefaults/.Xresources:
10#   URxvt.perl-ext-common: ...,keyboard-select
11#   URxvt.keysym.M-Escape: perl:keyboard-select:activate
12# The following line overwrites the default Meta-s binding and allows to
13# activate keyboard-select directly in backward search mode:
14#   URxvt.keysym.M-s: perl:keyboard-select:search
15
16# Use Meta-Escape to activate selection mode, then use the following keys:
17#   h/j/k/l:    Move cursor left/down/up/right (also with arrow keys)
18#   g/G/0/^/$/H/M/L/f/F/;/,/w/W/b/B/e/E: More vi-like cursor movement keys
19#   '/'/?:      Start forward/backward search
20#   n/N:        Repeat last search, N: in reverse direction
21#   Ctrl-f/b:   Scroll down/up one screen
22#   Ctrl-d/u:   Scroll down/up half a screen
23#   v/V/Ctrl-v: Toggle normal/linewise/blockwise selection
24#   y/Return:   Copy selection to primary buffer, Return: deactivate afterwards
25#   q/Escape:   Deactivate keyboard selection mode
26
27
28use strict;
29
30sub on_start{
31	my ($self) = @_;
32
33	$self->{patterns}{'w'} = qr/\w[^\w\s]|\W\w|\s\S/;
34	$self->{patterns}{'W'} = qr/\s\S/;
35	$self->{patterns}{'b'} = qr/.*(?:\w[^\w\s]|\W\w|\s\S)/;
36	$self->{patterns}{'B'} = qr/.*\s\S/;
37	$self->{patterns}{'e'} = qr/[^\w\s](?=\w)|\w(?=\W)|\S(?=\s|$)/;
38	$self->{patterns}{'E'} = qr/\S(?=\s|$)/;
39
40	()
41}
42
43
44sub on_user_command {
45	my ($self, $cmd) = @_;
46
47	if (not $self->{active}) {
48		if ($cmd eq 'keyboard-select:activate') {
49			activate($self);
50		} elsif ($cmd eq 'keyboard-select:search') {
51			activate($self, 1);
52		}
53	}
54
55	()
56}
57
58
59sub key_press {
60	my ($self, $event, $keysym, $char) = @_;
61	my $key = chr($keysym);
62
63	if ($self->{search}) {
64		if ($keysym == 0xff1b) {
65			if ($self->{search_mode}) {
66				deactivate($self);
67			} else {
68				$self->{search} = '';
69				status_area($self);
70			}
71		} elsif ($keysym == 0xff08) {
72			$self->{search} = substr($self->{search}, 0, -1);
73			if (not $self->{search} and $self->{search_mode}) {
74				deactivate($self);
75			} else {
76				status_area($self);
77			}
78		} elsif ($keysym == 0xff0d) {
79			my $txt = substr($self->{search}, 1);
80			if ($txt) {
81				$self->{pattern} = ($txt =~ m/[[:upper:]]/) ? qr/\Q$txt\E/ :
82						qr/\Q$txt\E/i;
83			} elsif ($self->{pattern}) {
84				delete $self->{pattern};
85			}
86			$self->{search} = '';
87			if (not find_next($self)) {
88				if ($self->{search_mode}) {
89					deactivate($self);
90				} else {
91					status_area($self);
92				}
93			}
94		} elsif (length($char) > 0) {
95			$self->{search} .= $self->locale_decode($char);
96			status_area($self);
97		}
98	} elsif ($self->{move_to}) {
99		if ($keysym == 0xff1b) {
100			$self->{move_to} = 0;
101			status_area($self);
102		} elsif (length($char) > 0) {
103			$self->{move_to} = 0;
104			$self->{patterns}{'f-1'} = qr/^.*\Q$key\E/;
105			$self->{patterns}{'f+1'} = qr/^.+?\Q$key\E/;
106			move_to($self, ';');
107			status_area($self);
108		}
109	} elsif ($keysym == 0xff1b || lc($key) eq 'q') {
110		deactivate($self);
111	} elsif ($key eq 'y' || $keysym == 0xff0d) {
112		if ($self->{select}) {
113			if ($self->{select} eq 'b') {
114				$self->selection($self->{selection});
115				$self->selection_grab($event->{time});
116			} else {
117				my ($br, $bc, $er, $ec) = calc_span($self);
118				$ec = $self->line($er)->l if $self->{select} eq 'l';
119				$self->selection_beg($br, $bc);
120				$self->selection_end($er, $ec);
121				$self->selection_make($event->{time});
122			}
123			if ($key eq 'y') {
124				if ($self->{select} ne 'b') {
125					$self->selection_beg(1, 0);
126					$self->selection_end(1, 0);
127				}
128				$self->{select} = '';
129				status_area($self);
130				$self->want_refresh();
131			} else {
132				deactivate($self);
133			}
134		}
135	} elsif ($key eq 'V') {
136		toggle_select($self, 'l');
137	} elsif ($key eq 'v') {
138		if ($event->{state} & urxvt::ControlMask) {
139			toggle_select($self, 'b');
140		} else {
141			toggle_select($self, 'n');
142		}
143	} elsif ($key eq 'k' || $keysym == 0xff52) {
144		move_cursor($self, 'k');
145	} elsif ($key eq 'j' || $keysym == 0xff54) {
146		move_cursor($self, 'j');
147	} elsif ($key eq 'h' || $keysym == 0xff51) {
148		move_cursor($self, 'h');
149	} elsif ($key eq 'l' || $keysym == 0xff53) {
150		move_cursor($self, 'l');
151	} elsif ('gG0^$HML' =~ m/\Q$key\E/ ||
152			('fbdu' =~ m/\Q$key\E/ && $event->{state} & urxvt::ControlMask)) {
153		move_cursor($self, $key);
154	} elsif (lc($key) eq 'f') {
155		$self->{move_to} = 1;
156		$self->{move_dir} = $key eq 'F' ? -1 : 1;
157		status_area($self, $key);
158	} elsif (';,wWbBeE' =~ m/\Q$key\E/) {
159		move_to($self, $key);
160	} elsif ($key eq '/' || $key eq '?') {
161		$self->{search} = $key;
162		$self->{search_dir} = $key eq '?' ? -1 : 1;
163		status_area($self);
164	} elsif (lc($key) eq 'n') {
165		find_next($self, $self->{search_dir} * ($key eq 'N' ? -1 : 1));
166	}
167
168	return 1;
169}
170
171
172sub move_cursor {
173	my ($self, $key) = @_;
174	my ($cr, $cc) = $self->screen_cur();
175	my $line = $self->line($cr);
176
177	if ($key eq 'k' && $line->beg > $self->top_row) {
178		$cr = $line->beg - 1;
179	} elsif ($key eq 'j' && $line->end < $self->nrow - 1) {
180		$cr = $line->end + 1;
181	} elsif ($key eq 'h' && $self->{offset} > 0) {
182		$self->{offset} = $line->offset_of($cr, $cc) - 1;
183		$self->{dollar} = 0;
184	} elsif ($key eq 'l' && $self->{offset} < $line->l - 1) {
185		++$self->{offset};
186	} elsif ($key eq 'f' || $key eq 'd') {
187		my $vs = $self->view_start() +
188				($key eq 'd' ? $self->nrow / 2 : $self->nrow - 1);
189		$vs = 0 if $vs > 0;
190		$cr += $vs - $self->view_start($vs);
191	} elsif ($key eq 'b' || $key eq 'u') {
192		my $vs = $self->view_start() -
193				($key eq 'u' ? $self->nrow / 2 : $self->nrow - 1);
194		$vs = $self->top_row if $vs < $self->top_row;
195		$cr += $vs - $self->view_start($vs);
196	} elsif ($key eq 'g') {
197		($cr, $self->{offset}) = ($self->top_row, 0);
198		$self->{dollar} = 0;
199	} elsif ($key eq 'G') {
200		($cr, $self->{offset}) = ($self->nrow - 1, 0);
201		$self->{dollar} = 0;
202	} elsif ($key eq '0') {
203		$self->{offset} = 0;
204		$self->{dollar} = 0;
205	} elsif ($key eq '^') {
206		my $ltxt = $self->special_decode($line->t);
207		while ($ltxt =~ s/^( *)\t/$1 . " " x (8 - length($1) % 8)/e) {}
208		$self->{offset} = $ltxt =~ m/^ +/ ? $+[0] : 0;
209		$self->{dollar} = 0;
210	} elsif ($key eq '$') {
211		my $co = $line->offset_of($cr, $cc);
212		$self->{dollar} = $co + 1;
213		$self->{offset} = $line->l - 1;
214	} elsif ($key eq 'H') {
215		$cr = $self->view_start();
216	} elsif ($key eq 'M') {
217		$cr = $self->view_start() + $self->nrow / 2;
218	} elsif ($key eq 'L') {
219		$cr = $self->view_start() + $self->nrow - 1;
220	}
221
222	$line = $self->line($cr);
223	$cc = $self->{dollar} || $self->{offset} >= $line->l ? $line->l - 1 :
224			$self->{offset};
225	$self->screen_cur($line->coord_of($cc));
226
227	status_area($self);
228	$self->want_refresh();
229
230	()
231}
232
233
234sub move_to {
235	my ($self, $key) = @_;
236	my ($cr, $cc) = $self->screen_cur();
237	my $line = $self->line($cr);
238	my $offset = $self->{offset};
239	my ($dir, $pattern);
240	my ($wrap, $found) = (0, 0);
241
242	if ($key eq ';' || $key eq ',') {
243		$dir = $self->{move_dir} * ($key eq ',' ? -1 : 1);
244		$pattern = $self->{patterns}{sprintf('f%+d', $dir)};
245		return if not $pattern;
246	} else {
247		if (lc($key) eq 'b') {
248			$dir = -1;
249		} else {
250			$dir = 1;
251			++$offset if lc($key) eq 'e';
252		}
253		$pattern = $self->{patterns}{$key};
254		$wrap = 1;
255	}
256
257	if ($dir > 0) {
258		NEXTDOWN: my $text = substr($line->t, $offset);
259		if ($text =~ m/$pattern/) {
260			$offset += $+[0] - 1;
261			$found = 1;
262		} elsif ($wrap && $line->end + 1 < $self->nrow) {
263			$cr = $line->end + 1;
264			$line = $self->line($cr);
265			$offset = 0;
266			if (lc($key) eq 'e') {
267				goto NEXTDOWN;
268			} else {
269				$found = 1;
270			}
271		}
272	} elsif ($dir < 0) {
273		NEXTUP: my $text = substr($line->t, 0, $offset);
274		if ($text =~ m/$pattern/) {
275			$offset += $+[0] - length($text) - 1;
276			$found = 1;
277		} elsif ($wrap) {
278			if ($offset > 0) {
279				$offset = 0;
280				$found = 1;
281			} elsif ($line->beg > $self->top_row) {
282				$cr = $line->beg - 1;
283				$line = $self->line($cr);
284				$offset = $line->l;
285				goto NEXTUP;
286			}
287		}
288	}
289
290	if ($found) {
291		$self->{dollar} = 0;
292		$self->{offset} = $offset;
293		$self->screen_cur($line->coord_of($offset));
294		$self->want_refresh();
295	}
296
297	()
298}
299
300
301sub find_next {
302	my ($self, $dir) = @_;
303
304	return if not $self->{pattern};
305	$dir = $self->{search_dir} if not $dir;
306
307	my ($cr, $cc) = $self->screen_cur();
308	my $line = $self->line($cr);
309	my $offset = $line->offset_of($cr, $cc);
310	my $text;
311	my $found = 0;
312
313	++$offset if $dir > 0;
314
315	while (not $found) {
316		if ($dir > 0) {
317			$text = substr($line->t, $offset);
318			if ($text =~ m/$self->{pattern}/) {
319				$found = 1;
320				$offset += $-[0];
321			} else {
322				last if $line->end >= $self->nrow;
323				$line = $self->line($line->end + 1);
324				$offset = 0;
325			}
326		} else {
327			$text = substr($line->t, 0, $offset);
328			if ($text =~ m/$self->{pattern}/) {
329				$found = 1;
330				$offset = $-[0] while $text =~ m/$self->{pattern}/g;
331			} else {
332				last if $line->beg <= $self->top_row;
333				$line = $self->line($line->beg - 1);
334				$offset = $line->l;
335			}
336		}
337	}
338
339	if ($found) {
340		$self->{dollar} = 0;
341		$self->{offset} = $offset;
342		$self->screen_cur($line->coord_of($offset));
343		status_area($self);
344		$self->want_refresh();
345	}
346
347	return $found;
348}
349
350
351sub tt_write {
352	return 1;
353}
354
355
356sub refresh {
357	my ($self) = @_;
358	my $reverse_cursor = $self->{select} ne 'l';
359	my ($cr, $cc) = $self->screen_cur();
360
361	if ($self->{select}) {
362		my ($br, $bc, $er, $ec) = calc_span($self);
363
364		if ($self->{select} eq 'b') {
365			delete $self->{selection} if $self->{selection};
366			my $co = $self->line($cr)->offset_of($cr, $cc);
367			my $dollar = $self->{dollar} && $co >= $self->{dollar} - 1;
368
369			my $r = $br;
370			while ($r <= $er) {
371				my $line = $self->line($r);
372				if ($bc < $line->l) {
373					$ec = $line->l if $dollar;
374					$self->{selection} .= substr($line->t, $bc, $ec - $bc);
375					my ($br, $bc) = $line->coord_of($bc);
376					my ($er, $ec) = $line->coord_of($ec <= $line->l ? $ec : $line->l);
377					$self->scr_xor_span($br, $bc, $er, $ec, urxvt::RS_RVid);
378				} elsif ($r == $cr) {
379					$reverse_cursor = 0;
380				}
381				$self->{selection} .= "\n" if $line->end < $er;
382				$r = $line->end + 1;
383			}
384		} else {
385			$self->scr_xor_span($br, $bc, $er, $ec, urxvt::RS_RVid);
386		}
387
388		if ($reverse_cursor) {
389			# make the cursor visible again
390			$self->scr_xor_span($cr, $cc, $cr, $cc + 1, urxvt::RS_RVid);
391		}
392	}
393
394	# scroll the current cursor position into visible area
395	if ($cr < $self->view_start()) {
396		$self->view_start($cr);
397	} elsif ($cr >= $self->view_start() + $self->nrow) {
398		$self->view_start($cr - $self->nrow + 1);
399	}
400
401	()
402}
403
404
405sub activate {
406	my ($self, $search) = @_;
407
408	$self->{active} = 1;
409
410	$self->{select} = '';
411	$self->{dollar} = 0;
412	$self->{move_to} = 0;
413
414	if ($search) {
415		$self->{search} = '?';
416		$self->{search_dir} = -1;
417		$self->{search_mode} = 1;
418	} else {
419		$self->{search} = '';
420		$self->{search_mode} = 0;
421	}
422
423	($self->{oldcr}, $self->{oldcc}) = $self->screen_cur();
424	$self->{old_view_start} = $self->view_start();
425	$self->{old_pty_ev_events} = $self->pty_ev_events(urxvt::EV_NONE);
426
427	my $line = $self->line($self->{oldcr});
428	$self->{offset} = $line->offset_of($self->{oldcr}, $self->{oldcc});
429
430	$self->selection_beg(1, 0);
431	$self->selection_end(1, 0);
432
433	$self->enable(
434		key_press     => \&key_press,
435		refresh_begin => \&refresh,
436		refresh_end   => \&refresh,
437		tt_write      => \&tt_write,
438	);
439
440	if ($self->{offset} >= $line->l) {
441		$self->{offset} = $line->l > 0 ? $line->l - 1 : 0;
442		$self->screen_cur($line->coord_of($self->{offset}));
443		$self->want_refresh();
444	}
445
446	$self->{overlay_len} = 0;
447	status_area($self);
448
449	()
450}
451
452
453sub deactivate {
454	my ($self) = @_;
455
456	$self->selection_beg(1, 0);
457	$self->selection_end(1, 0);
458
459	delete $self->{overlay} if $self->{overlay};
460	delete $self->{selection} if $self->{selection};
461
462	$self->disable("key_press", "refresh_begin", "refresh_end", "tt_write");
463	$self->screen_cur($self->{oldcr}, $self->{oldcc});
464	$self->view_start($self->{old_view_start});
465	$self->pty_ev_events($self->{old_pty_ev_events});
466
467	$self->want_refresh();
468
469	$self->{active} = 0;
470
471	()
472}
473
474
475sub status_area {
476	my ($self, $extra) = @_;
477	my ($stat, $stat_len);
478
479	if ($self->{search}) {
480		$stat_len = $self->ncol;
481		$stat = $self->{search} . ' ' x ($stat_len - length($self->{search}));
482	} else {
483		if ($self->{select}) {
484			$stat = "-V" . ($self->{select} ne 'n' ? uc($self->{select}) : "") . "- ";
485		}
486
487		if ($self->top_row == 0) {
488			$stat .= "All";
489		} elsif ($self->view_start() == $self->top_row) {
490			$stat .= "Top";
491		} elsif ($self->view_start() == 0) {
492			$stat .= "Bot";
493		} else {
494			$stat .= sprintf("%2d%%",
495					($self->top_row - $self->view_start) * 100 / $self->top_row);
496		}
497
498		$stat = "$extra $stat" if $extra;
499		$stat_len = length($stat);
500	}
501
502	if (!$self->{overlay} || $self->{overlay_len} != $stat_len) {
503		delete $self->{overlay} if $self->{overlay};
504		$self->{overlay} = $self->overlay(-1, -1, $stat_len, 1,
505				urxvt::OVERLAY_RSTYLE, 0);
506		$self->{overlay_len} = $stat_len;
507	}
508
509	$self->{overlay}->set(0, 0, $self->special_encode($stat));
510	$self->{overlay}->show();
511
512	()
513}
514
515
516sub toggle_select {
517	my ($self, $mode) = @_;
518
519	if ($self->{select} eq $mode) {
520		$self->{select} = '';
521	} else {
522		if (not $self->{select}) {
523			($self->{ar}, $self->{ac}) = $self->screen_cur();
524		}
525		$self->{select} = $mode;
526	}
527
528	status_area($self);
529	$self->want_refresh();
530
531	()
532}
533
534
535sub calc_span {
536	my ($self) = @_;
537	my ($cr, $cc) = $self->screen_cur();
538	my ($br, $bc, $er, $ec);
539
540	if ($self->{select} eq 'b') {
541		$br = $self->line($cr)->beg;
542		$bc = $self->line($cr)->offset_of($cr, $cc);
543		$er = $self->line($self->{ar})->beg;
544		$ec = $self->line($self->{ar})->offset_of($self->{ar}, $self->{ac});
545		($br, $er) = ($er, $br) if $br > $er;
546		($bc, $ec) = ($ec, $bc) if $bc > $ec;
547	} else {
548		if ($cr < $self->{ar}) {
549			($br, $bc, $er, $ec) = ($cr, $cc, $self->{ar}, $self->{ac});
550		} elsif ($cr > $self->{ar}) {
551			($br, $bc, $er, $ec) = ($self->{ar}, $self->{ac}, $cr, $cc);
552		} else {
553			($br, $er) = ($cr, $cr);
554			($bc, $ec) = $cc < $self->{ac} ? ($cc, $self->{ac}) : ($self->{ac}, $cc);
555		}
556	}
557
558	if ($self->{select} eq 'l') {
559		($br, $er) = ($self->line($br)->beg, $self->line($er)->end);
560		($bc, $ec) = (0, $self->ncol);
561	} else {
562		++$ec;
563	}
564
565	return ($br, $bc, $er, $ec);
566}
567