#! perl -w
# Author:   Bert Muennich
# Website:  http://www.github.com/muennich/urxvt-perls
# License:  GPLv2

# Use keyboard shortcuts to select and copy text.

# Usage: put the following lines in your .Xdefaults/.Xresources:
#   URxvt.perl-ext-common: ...,keyboard-select
#   URxvt.keysym.M-Escape: perl:keyboard-select:activate
# The following line overwrites the default Meta-s binding and allows to
# activate keyboard-select directly in backward search mode:
#   URxvt.keysym.M-s: perl:keyboard-select:search

# Use Meta-Escape to activate selection mode, then use the following keys:
#   h/j/k/l:    Move cursor left/down/up/right (also with arrow keys)
#   g/G/0/^/$/H/M/L/f/F/;/,/w/W/b/B/e/E: More vi-like cursor movement keys
#   '/'/?:      Start forward/backward search
#   n/N:        Repeat last search, N: in reverse direction
#   Ctrl-f/b:   Scroll down/up one screen
#   Ctrl-d/u:   Scroll down/up half a screen
#   v/V/Ctrl-v: Toggle normal/linewise/blockwise selection
#   y/Return:   Copy selection to primary buffer, Return: quit afterwards
#   Y:          Copy selected lines to primary buffer or cursor line and quit
#   q/Escape:   Quit keyboard selection mode

# Options:
#   URxvt.keyboard-select.clipboard: If true, copy to clipboard too


use strict;

sub on_start{
	my ($self) = @_;

	$self->{clipboard} = $self->x_resource_boolean('keyboard-select.clipboard');

	$self->{patterns}{'w'} = qr/\w[^\w\s]|\W\w|\s\S/;
	$self->{patterns}{'W'} = qr/\s\S/;
	$self->{patterns}{'b'} = qr/.*(?:\w[^\w\s]|\W\w|\s\S)/;
	$self->{patterns}{'B'} = qr/.*\s\S/;
	$self->{patterns}{'e'} = qr/[^\w\s](?=\w)|\w(?=\W)|\S(?=\s|$)/;
	$self->{patterns}{'E'} = qr/\S(?=\s|$)/;

	()
}


sub on_action {
    my ($self, $action) = @_;

    on_user_command($self, "keyboard-select:" . $action);
}


sub on_user_command {
	my ($self, $cmd) = @_;

	if (not $self->{active}) {
		if ($cmd eq 'keyboard-select:activate') {
			activate($self);
		} elsif ($cmd eq 'keyboard-select:search') {
			activate($self, 1);
		}
	}

	()
}


sub key_press {
	my ($self, $event, $keysym, $char) = @_;
	my $key = chr($keysym);

	if (lc($key) eq 'c' && $event->{state} & urxvt::ControlMask) {
		deactivate($self);
	} elsif ($self->{search}) {
		if ($keysym == 0xff1b) {
			if ($self->{search_mode}) {
				deactivate($self);
			} else {
				$self->{search} = '';
				status_area($self);
			}
		} elsif ($keysym == 0xff08) {
			$self->{search} = substr($self->{search}, 0, -1);
			if (not $self->{search} and $self->{search_mode}) {
				deactivate($self);
			} else {
				status_area($self);
			}
		} elsif ($keysym == 0xff0d ||
				(lc($key) eq 'm' && $event->{state} & urxvt::ControlMask)) {
			my $txt = substr($self->{search}, 1);
			if ($txt) {
				$self->{pattern} = ($txt =~ m/[[:upper:]]/) ? qr/\Q$txt\E/ :
						qr/\Q$txt\E/i;
			} elsif ($self->{pattern}) {
				delete $self->{pattern};
			}
			$self->{search} = '';
			$self->screen_cur($self->{srhcr}, $self->{srhcc});
			if (not find_next($self)) {
				if ($self->{search_mode}) {
					deactivate($self);
				} else {
					status_area($self);
				}
			}
		} elsif (length($char) > 0) {
			$self->{search} .= $self->locale_decode($char);
			my $txt = substr($self->{search}, 1);
			if ($txt) {
				$self->{pattern} = ($txt =~ m/[[:upper:]]/) ? qr/\Q$txt\E/ :
				qr/\Q$txt\E/i;
			} elsif ($self->{pattern}) {
				delete $self->{pattern};
			}
			$self->screen_cur($self->{srhcr}, $self->{srhcc});
			find_next($self);
			status_area($self);
		}
	} elsif ($self->{move_to}) {
		if ($keysym == 0xff1b) {
			$self->{move_to} = 0;
			status_area($self);
		} elsif (length($char) > 0) {
			$self->{move_to} = 0;
			$self->{patterns}{'f-1'} = qr/^.*\Q$key\E/;
			$self->{patterns}{'f+1'} = qr/^.+?\Q$key\E/;
			move_to($self, ';');
			status_area($self);
		}
	} elsif ($keysym == 0xff1b || lc($key) eq 'q') {
		deactivate($self);
	} elsif (lc($key) eq 'y' || $keysym == 0xff0d ||
			(lc($key) eq 'm' && $event->{state} & urxvt::ControlMask)) {
		my $quit = 0;
		if ($key eq 'Y' && $self->{select} ne 'l') {
			$quit = !$self->{select};
			toggle_select($self, 'l');
		}
		if ($self->{select}) {
			my ($br, $bc, $er, $ec) = calc_span($self);
			$ec = $self->line($er)->l if $self->{select} eq 'l';
			$self->selection_beg($br, $bc);
			$self->selection_end($er, $ec);
			$self->selection_make($event->{time}, $self->{select} eq 'b');
			if ($self->{clipboard}) {
				$self->selection($self->selection(), 1);
				$self->selection_grab($event->{time}, 1);
			}
			if (lc($key) eq 'y') {
				$self->selection_beg(1, 0);
				$self->selection_end(1, 0);
				$self->{select} = '';
				status_area($self);
				$self->want_refresh();
			} else {
				$quit = 1;
			}
		}
		if ($quit) {
			deactivate($self); 
		}
	} elsif ($key eq 'V') {
		toggle_select($self, 'l');
	} elsif ($key eq 'v') {
		if ($event->{state} & urxvt::ControlMask) {
			toggle_select($self, 'b');
		} else {
			toggle_select($self, 'n');
		}
	} elsif ($key eq 'k' || $keysym == 0xff52) {
		move_cursor($self, 'k');
	} elsif ($key eq 'j' || $keysym == 0xff54) {
		move_cursor($self, 'j');
	} elsif ($key eq 'h' || $keysym == 0xff51) {
		move_cursor($self, 'h');
	} elsif ($key eq 'l' || $keysym == 0xff53) {
		move_cursor($self, 'l');
	} elsif ($keysym == 0xff57) {
		move_cursor($self, '$');
	} elsif ($keysym == 0xff50) {
		move_cursor($self, '^');
	} elsif ('gG0^$HML' =~ m/\Q$key\E/ ||
			('fbdu' =~ m/\Q$key\E/ && $event->{state} & urxvt::ControlMask)) {
		move_cursor($self, $key);
	} elsif (lc($key) eq 'f') {
		$self->{move_to} = 1;
		$self->{move_dir} = $key eq 'F' ? -1 : 1;
		status_area($self, $key);
	} elsif (';,wWbBeE' =~ m/\Q$key\E/) {
		move_to($self, $key);
	} elsif ($key eq '/' || $key eq '?') {
		$self->{search} = $key;
		$self->{search_dir} = $key eq '?' ? -1 : 1;
		($self->{srhcr}, $self->{srhcc}) = $self->screen_cur();
		status_area($self);
	} elsif (lc($key) eq 'n') {
		find_next($self, $self->{search_dir} * ($key eq 'N' ? -1 : 1));
	}

	return 1;
}


sub move_cursor {
	my ($self, $key) = @_;
	my ($cr, $cc) = $self->screen_cur();
	my $line = $self->line($cr);

	if ($key eq 'k' && $line->beg > $self->top_row) {
		$cr = $line->beg - 1;
	} elsif ($key eq 'j' && $line->end < $self->nrow - 1) {
		$cr = $line->end + 1;
	} elsif ($key eq 'h' && $self->{offset} > 0) {
		$self->{offset} = $line->offset_of($cr, $cc) - 1;
		$self->{dollar} = 0;
	} elsif ($key eq 'l' && $self->{offset} < $line->l - 1) {
		++$self->{offset};
	} elsif ($key eq 'f' || $key eq 'd') {
		my $vs = $self->view_start() +
				($key eq 'd' ? $self->nrow / 2 : $self->nrow - 1);
		$vs = 0 if $vs > 0;
		$cr += $vs - $self->view_start($vs);
	} elsif ($key eq 'b' || $key eq 'u') {
		my $vs = $self->view_start() -
				($key eq 'u' ? $self->nrow / 2 : $self->nrow - 1);
		$vs = $self->top_row if $vs < $self->top_row;
		$cr += $vs - $self->view_start($vs);
	} elsif ($key eq 'g') {
		($cr, $self->{offset}) = ($self->top_row, 0);
		$self->{dollar} = 0;
	} elsif ($key eq 'G') {
		($cr, $self->{offset}) = ($self->nrow - 1, 0);
		$self->{dollar} = 0;
	} elsif ($key eq '0') {
		$self->{offset} = 0;
		$self->{dollar} = 0;
	} elsif ($key eq '^') {
		my $ltxt = $self->special_decode($line->t);
		while ($ltxt =~ s/^( *)\t/$1 . " " x (8 - length($1) % 8)/e) {}
		$self->{offset} = $ltxt =~ m/^ +/ ? $+[0] : 0;
		$self->{dollar} = 0;
	} elsif ($key eq '$') {
		my $co = $line->offset_of($cr, $cc);
		$self->{dollar} = $co + 1;
		$self->{offset} = $line->l - 1;
	} elsif ($key eq 'H') {
		$cr = $self->view_start();
	} elsif ($key eq 'M') {
		$cr = $self->view_start() + $self->nrow / 2;
	} elsif ($key eq 'L') {
		$cr = $self->view_start() + $self->nrow - 1;
	}

	$line = $self->line($cr);
	$cc = $self->{dollar} || $self->{offset} >= $line->l ? $line->l - 1 :
			$self->{offset};
	$self->screen_cur($line->coord_of($cc));

	status_area($self);
	$self->want_refresh();
	
	()
}


sub move_to {
	my ($self, $key) = @_;
	my ($cr, $cc) = $self->screen_cur();
	my $line = $self->line($cr);
	my $offset = $self->{offset};
	my ($dir, $pattern);
	my ($wrap, $found) = (0, 0);

	if ($key eq ';' || $key eq ',') {
		$dir = $self->{move_dir} * ($key eq ',' ? -1 : 1);
		$pattern = $self->{patterns}{sprintf('f%+d', $dir)};
		return if not $pattern;
	} else {
		if (lc($key) eq 'b') {
			$dir = -1;
		} else {
			$dir = 1;
			++$offset if lc($key) eq 'e';
		}
		$pattern = $self->{patterns}{$key};
		$wrap = 1;
	}

	if ($dir > 0) {
		NEXTDOWN: my $text = substr($line->t, $offset);
		if ($text =~ m/$pattern/) {
			$offset += $+[0] - 1;
			$found = 1;
		} elsif ($wrap && $line->end + 1 < $self->nrow) {
			$cr = $line->end + 1;
			$line = $self->line($cr);
			$offset = 0;
			if (lc($key) eq 'e') {
				goto NEXTDOWN;
			} else {
				$found = 1;
			}
		}
	} elsif ($dir < 0) {
		NEXTUP: my $text = substr($line->t, 0, $offset);
		if ($text =~ m/$pattern/) {
			$offset += $+[0] - length($text) - 1;
			$found = 1;
		} elsif ($wrap) {
			if ($offset > 0) {
				$offset = 0;
				$found = 1;
			} elsif ($line->beg > $self->top_row) {
				$cr = $line->beg - 1;
				$line = $self->line($cr);
				$offset = $line->l;
				goto NEXTUP;
			}
		}
	}

	if ($found) {
		$self->{dollar} = 0;
		$self->{offset} = $offset;
		$self->screen_cur($line->coord_of($offset));
		$self->want_refresh();
	}

	()
}


sub find_next {
	my ($self, $dir) = @_;

	return if not $self->{pattern};
	$dir = $self->{search_dir} if not $dir;

	my ($cr, $cc) = $self->screen_cur();
	my $line = $self->line($cr);
	my $offset = $line->offset_of($cr, $cc);
	my $text;
	my $found = 0;

	++$offset if $dir > 0;

	while (not $found) {
		if ($dir > 0) {
			$text = substr($line->t, $offset);
			if ($text =~ m/$self->{pattern}/) {
				$found = 1;
				$offset += $-[0];
			} else {
				last if $line->end >= $self->nrow;
				$line = $self->line($line->end + 1);
				$offset = 0;
			}
		} else {
			$text = substr($line->t, 0, $offset);
			if ($text =~ m/$self->{pattern}/) {
				$found = 1;
				$offset = $-[0] while $text =~ m/$self->{pattern}/g;
			} else {
				last if $line->beg <= $self->top_row;
				$line = $self->line($line->beg - 1);
				$offset = $line->l;
			}
		}
	}

	if ($found) {
		$self->{dollar} = 0;
		$self->{offset} = $offset;
		$self->screen_cur($line->coord_of($offset));
		status_area($self);
		$self->want_refresh();
	}

	return $found;
}


sub tt_write {
	return 1;
}


sub refresh {
	my ($self) = @_;
	my ($cr, $cc) = $self->screen_cur();

	# scroll the current cursor position into visible area
	if ($cr < $self->view_start()) {
		$self->view_start($cr);
	} elsif ($cr >= $self->view_start() + $self->nrow) {
		$self->view_start($cr - $self->nrow + 1);
	}

	if ($self->{select}) {
		my ($hl, $reverse_cursor);
		my ($br, $bc, $er, $ec) = calc_span($self);

		if ($self->x_resource('highlightColor')) {
			$hl = urxvt::RS_Sel;
			$reverse_cursor = 0;
		} else {
			$hl = urxvt::RS_RVid;
			$reverse_cursor = $self->{select} ne 'l';
		}
		if ($self->{select} eq 'b') {
			my $co = $self->line($cr)->offset_of($cr, $cc);
			my $dollar = $self->{dollar} && $co >= $self->{dollar} - 1;

			my $r = $br;
			while ($r <= $er) {
				my $line = $self->line($r);
				if ($bc < $line->l) {
					$ec = $line->l if $dollar;
					my ($br, $bc) = $line->coord_of($bc);
					my ($er, $ec) = $line->coord_of($ec <= $line->l ? $ec : $line->l);
					$self->scr_xor_span($br, $bc, $er, $ec, $hl);
				} elsif ($r == $cr) {
					$reverse_cursor = 0;
				}
				$r = $line->end + 1;
			}
		} else {
			$self->scr_xor_span($br, $bc, $er, $ec, $hl);
		}

		if ($reverse_cursor) {
			# make the cursor visible again
			$self->scr_xor_span($cr, $cc, $cr, $cc + 1, $hl);
		}
	}

	()
}


sub activate {
	my ($self, $search) = @_;

	$self->{active} = 1;

	$self->{select} = '';
	$self->{dollar} = 0;
	$self->{move_to} = 0;

	if ($search) {
		$self->{search} = '?';
		$self->{search_dir} = -1;
		$self->{search_mode} = 1;
	} else {
		$self->{search} = '';
		$self->{search_mode} = 0;
	}

	($self->{oldcr}, $self->{oldcc}) = $self->screen_cur();
	($self->{srhcr}, $self->{srhcc}) = $self->screen_cur();
	$self->{old_view_start} = $self->view_start();
	$self->{old_pty_ev_events} = $self->pty_ev_events(urxvt::EV_NONE);

	my $line = $self->line($self->{oldcr});
	$self->{offset} = $line->offset_of($self->{oldcr}, $self->{oldcc});

	$self->selection_beg(1, 0);
	$self->selection_end(1, 0);

	$self->enable(
		key_press     => \&key_press,
		refresh_begin => \&refresh,
		refresh_end   => \&refresh,
		tt_write      => \&tt_write,
	);

	if ($self->{offset} >= $line->l) {
		$self->{offset} = $line->l > 0 ? $line->l - 1 : 0;
		$self->screen_cur($line->coord_of($self->{offset}));
		$self->want_refresh();
	}

	$self->{overlay_len} = 0;
	status_area($self);

	()
}


sub deactivate {
	my ($self) = @_;

	$self->selection_beg(1, 0);
	$self->selection_end(1, 0);

	delete $self->{overlay} if $self->{overlay};

	$self->disable("key_press", "refresh_begin", "refresh_end", "tt_write");
	$self->screen_cur($self->{oldcr}, $self->{oldcc});
	$self->view_start($self->{old_view_start});
	$self->pty_ev_events($self->{old_pty_ev_events});

	$self->want_refresh();

	$self->{active} = 0;

	()
}


sub status_area {
	my ($self, $extra) = @_;
	my ($stat, $stat_len);

	if ($self->{search}) {
		$stat_len = $self->ncol;
		$stat = $self->{search} . ' ' x ($stat_len - length($self->{search}));
	} else {
		if ($self->{select}) {
			$stat = "-V" . ($self->{select} ne 'n' ? uc($self->{select}) : "") . "- ";
		}

		if ($self->top_row == 0) {
			$stat .= "All";
		} elsif ($self->view_start() == $self->top_row) {
			$stat .= "Top";
		} elsif ($self->view_start() == 0) {
			$stat .= "Bot";
		} else {
			$stat .= sprintf("%2d%%",
					($self->top_row - $self->view_start) * 100 / $self->top_row);
		}
		
		$stat = "$extra $stat" if $extra;
		$stat_len = length($stat);
	}

	if (!$self->{overlay} || $self->{overlay_len} != $stat_len) {
		delete $self->{overlay} if $self->{overlay};
		$self->{overlay} = $self->overlay(-1, -1, $stat_len, 1,
				urxvt::OVERLAY_RSTYLE, 0);
		$self->{overlay_len} = $stat_len;
	}

	$self->{overlay}->set(0, 0, $self->special_encode($stat));
	$self->{overlay}->show();

	()
}


sub toggle_select {
	my ($self, $mode) = @_;

	if ($self->{select} eq $mode) {
		$self->{select} = '';
	} else {
		if (not $self->{select}) {
			($self->{ar}, $self->{ac}) = $self->screen_cur();
		}
		$self->{select} = $mode;
	}

	status_area($self);
	$self->want_refresh();

	()
}


sub calc_span {
	my ($self) = @_;
	my ($cr, $cc) = $self->screen_cur();
	my ($br, $bc, $er, $ec);
	
	if ($self->{select} eq 'b') {
		$br = $self->line($cr)->beg;
		$bc = $self->line($cr)->offset_of($cr, $cc);
		$er = $self->line($self->{ar})->beg;
		$ec = $self->line($self->{ar})->offset_of($self->{ar}, $self->{ac});
		($br, $er) = ($er, $br) if $br > $er;
		($bc, $ec) = ($ec, $bc) if $bc > $ec;
	} else {
		if ($cr < $self->{ar}) {
			($br, $bc, $er, $ec) = ($cr, $cc, $self->{ar}, $self->{ac});
		} elsif ($cr > $self->{ar}) {
			($br, $bc, $er, $ec) = ($self->{ar}, $self->{ac}, $cr, $cc);
		} else {
			($br, $er) = ($cr, $cr);
			($bc, $ec) = $cc < $self->{ac} ? ($cc, $self->{ac}) : ($self->{ac}, $cc);
		}
	}

	if ($self->{select} eq 'l') {
		($br, $er) = ($self->line($br)->beg, $self->line($er)->end);
		($bc, $ec) = (0, $self->ncol);
	} else {
		++$ec;
	}

	return ($br, $bc, $er, $ec);
}
