Created
May 30, 2015 09:40
-
-
Save max630/ab597c04cf523b42bd71 to your computer and use it in GitHub Desktop.
urxvt keyboard selection
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
URxvt.perl-lib: /home/max/configs/rxvt | |
URxvt.perl-ext-common: default,keyboard-selection,selection-popup | |
URxvt.keysym.C-Tab: perl:keyboard-select:activate | |
URxvt.keysym.M-v: perl:keyboard-select:activate | |
URxvt.utmpInhibit: false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! perl -w | |
# | |
# fetched from https://raw.github.com/muennich/urxvt-perls/c85bb17142fe80010dd1a9895219ea23548d3704/keyboard-select | |
# -- max630 | |
# | |
# Author: Bert Muennich | |
# Website: http://www.github.com/muennich/urxvt-perls | |
# Version: git-20110906 | |
# 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: deactivate afterwards | |
# q/Escape: Deactivate keyboard selection mode | |
use strict; | |
sub on_start{ | |
my ($self) = @_; | |
$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_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 ($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) { | |
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} = ''; | |
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); | |
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 ($key eq 'y' || $keysym == 0xff0d) { | |
if ($self->{select}) { | |
if ($self->{select} eq 'b') { | |
$self->selection($self->{selection}); | |
$self->selection($self->{selection}, 1); | |
$self->selection_grab($event->{time}); | |
} else { | |
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->selection($self->selection(), 1); | |
} | |
$self->selection_grab($event->{time}, 1); | |
if ($key eq 'y') { | |
if ($self->{select} ne 'b') { | |
$self->selection_beg(1, 0); | |
$self->selection_end(1, 0); | |
} | |
$self->{select} = ''; | |
status_area($self); | |
$self->want_refresh(); | |
} else { | |
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 ('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; | |
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 $reverse_cursor = $self->{select} ne 'l'; | |
my ($cr, $cc) = $self->screen_cur(); | |
if ($self->{select}) { | |
my ($br, $bc, $er, $ec) = calc_span($self); | |
if ($self->{select} eq 'b') { | |
delete $self->{selection} if $self->{selection}; | |
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; | |
$self->{selection} .= substr($line->t, $bc, $ec - $bc); | |
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, urxvt::RS_RVid); | |
} elsif ($r == $cr) { | |
$reverse_cursor = 0; | |
} | |
$self->{selection} .= "\n" if $line->end < $er; | |
$r = $line->end + 1; | |
} | |
} else { | |
$self->scr_xor_span($br, $bc, $er, $ec, urxvt::RS_RVid); | |
} | |
if ($reverse_cursor) { | |
# make the cursor visible again | |
$self->scr_xor_span($cr, $cc, $cr, $cc + 1, urxvt::RS_RVid); | |
} | |
} | |
# 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); | |
} | |
() | |
} | |
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->{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}; | |
delete $self->{selection} if $self->{selection}; | |
$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); | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment