#! perl # ------------------------------------------------------------------------ # Author: Bart Trojanowski # # This script intends to provide functionality similar to screen's # copy-mode, but instead copy to the X clipboard. In order to copy to # the clipboard we need either the Clipboard.pm from CPAN or the xclip # command line utility. # # More details here: http://www.jukie.net/~bart/blog/tag/urxvt # # This script is based on the mark-urls script from the rxvt-unicode # distribution. # # ------------------------------------------------------------------------ # configuration # # Put this in your .Xdefaults # # URxvt.keysym.M-y: perl:mark-and-yank:activate_mark_mode # URxvt.keysym.M-u: perl:mark-and-yank:activate_mark_url_mode # URxvt.perl-lib: /home/jukie/bart/.urxvt/ # URxvt.perl-ext: mark-and-yank # URxvt.urlLauncher: firefox # # you might have to edit the perl-lib line. # # ------------------------------------------------------------------------ use List::Util qw(first max maxstr min minstr reduce shuffle sum); # same url as used in "selection" my $url_matcher = qr{( (?:https?://|ftp://|news://|mailto:|file://)[ab-zA-Z0-9\-\@;\/?:&=%\$_.+!*\x27(),~#]+ [ab-zA-Z0-9\-\@;\/?:&=%\$_+!*\x27()~] # exclude some trailing characters (heuristic) )}x; sub canonicalise_coordinates { if ($_[0] > $_[2] || ($_[0] == $_[2] && $_[1] > $_[3])) { (@_[2,3],@_[0,1]) } else { (@_); } } sub clamp { min( max( $_[0], $_[1] ), $_[2] ) } sub on_start { my ($term) = @_; $term->{have_Clipboard} = eval { require Clipboard; }; if ($term->{have_Clipboard}) { import Clipboard; } $term->{browser} = $term->x_resource ("urlLauncher") || "x-www-browser"; () } sub open_url { my ($term, $url) = @_; $term->exec_async (split(/[[:blank:]]/, $term->{browser}), $url); } sub on_line_update { my ($term, $row) = @_; return unless ($term->x_resource ("underlineURLs") eq "true"); # fetch the line that has changed my $line = $term->line ($row); my $text = $line->t; # find all urls (if any) while ($text =~ /$url_matcher/g) { my $rend = $line->r; # mark all characters as underlined. we _must_ not toggle underline, # as we might get called on an already-marked url. $_ |= urxvt::RS_Uline for @{$rend}[ $-[1] .. $+[1] - 1]; $line->r ($rend); } () } sub on_button_release { my ($term, $event) = @_; my $mask = $term->ModLevel3Mask | $term->ModMetaMask | urxvt::ShiftMask | urxvt::ControlMask; if ($event->{button} == 2 && ($event->{state} & $mask) == 0) { my $row = $event->{row}; my $col = $event->{col}; my $line = $term->line ($row); my $text = $line->t; while ($text =~ /$url_matcher/g) { if ($-[1] <= $col && $+[1] >= $col) { open_url($term, $1); return 1; } } } () } # ------------------------------------------------------------------------ my %key2mod = ( 65505 => urxvt::ShiftMask, 65507 => urxvt::ControlMask, 65513 => urxvt::Mod1Mask, # Alt 65514 => urxvt::Mod1Mask, # Alt 65515 => urxvt::Mod4Mask, # Super 65516 => urxvt::Mod4Mask, # Super ); my $mod = 0; #my %mod = ( 'control' => 0, 'shift' => 0 ); my $mark_mode_active = 0; my @backup_cursor = (); my @visual_start = (); my $visual_mode = 0; # 'v', 'V', or '^v' my @cursor = (); my $url_selected = -1; my @url_db = (); my $first_mark_set = 0; my $msg_timeout = 2; # ------------------------------------------------------------------------ sub do_scan_for_urls { my ($term) = @_; @url_db = (); my $row_start = $term->top_row; my $row_end = $term->nrow; for (my $row=$row_start; $row<=$row_end; $row++) { # fetch the line that has changed my $line = $term->line ($row); my $text = $line->t; # find all urls (if any) while ($text =~ /$url_matcher/g) { my $url = $1; my %h = ( 'row' => $row, 'col_from' => $-[1], 'col_to' => $+[1] - 1, 'url' => $url); push @url_db, \%h; } } # 0 for none, positive count otherwise return $#url_db + 1; } sub status_message { my ($self, $text, $timeout) = @_; $timeout = $msg_timeout unless defined $timeout; $self->{msg} = { ov => $self->overlay (0, -1, length($text)+1, 1, urxvt::OVERLAY_RSTYLE, 1), to => urxvt::timer ->new ->start (urxvt::NOW + $timeout) ->cb (sub { delete $self->{msg}; }), }; $self->{msg}{ov}->set(0,0,$text); } sub on_user_command { my ($term, $cmd) = @_; if ($cmd eq "mark-and-yank:activate_mark_mode") { activate_mark_mode($term); } elsif ($cmd eq "mark-and-yank:activate_mark_url_mode") { activate_mark_url_mode($term); } status_message ($term, "urxvt copy mode started"); () } # ------------------------------------------------------------------------ sub on_key_press { my ($term, $event, $keysym, $octets) = @_; foreach my $key (keys %key2mod) { if ($keysym == $key) { $mod |= $key2mod{$key}; return (); } } # ignore all input when we are active $mark_mode_active && return 1; () } sub on_key_release { my ($term, $event, $keysym) = @_; foreach my $key (keys %key2mod) { if ($keysym == $key) { $mod &= ~$key2mod{$key}; return (); } } return () unless ($mark_mode_active); my $ch = chr($keysym); if ($mod & urxvt::ShiftMask && $ch =~ m/[[:alpha:]]/) { $ch = uc $ch; $mod &= ~urxvt::ShiftMask; } if (!$mod && $keysym == 65307) { # deactivate_mark_mode ($term); visual_mode_disable ($term, @cursor); } elsif (($mod & urxvt::ControlMask) && $ch eq 'c') {# ^c - abort deactivate_mark_mode ($term); visual_mode_disable ($term, @cursor); } elsif (!$mod && $keysym == 65293) { # if ($first_mark_set) { do_copy($term, @visual_start, @cursor); deactivate_mark_mode ($term); visual_mode_disable ($term, @cursor); } else { my %url = get_active_url($term); if (not %url) { $first_mark_set = 1; visual_mode_enable ($term, 'v', @cursor); } else { my $urltext = $url{url}; $urltext =~ s/\(["|><&()]\)/\\$1/; open_url($term, $urltext); deactivate_mark_mode ($term); visual_mode_disable ($term, @cursor); } } } elsif (!$mod && $keysym == 32) { # if ($first_mark_set) { do_copy($term, @visual_start, @cursor); deactivate_mark_mode ($term); visual_mode_disable ($term, @cursor); } else { $first_mark_set = 1; visual_mode_enable ($term, 'v', @cursor); } } elsif (!$mod && $ch eq 'o') { # o - go to other end of region if ($first_mark_set) { my @dest = @visual_start; @visual_start = @cursor; @cursor = @dest; $term->screen_cur (@dest); $term->want_refresh; } } elsif (($mod & urxvt::ControlMask) && $ch eq 'w') {# w - copy the word under the cursor my ($y1, $x1, $y2, $x2) = (@cursor, @cursor); --$x1 while substr($term->ROW_t($y1), $x1 - 1, 1) =~ m/\w/; ++$x2 while substr($term->ROW_t($y2), $x2 + 1, 1) =~ m/\w/; do_copy($term, $y1, $x1, $y2, $x2); deactivate_mark_mode ($term); visual_mode_disable ($term, @cursor); } elsif ($ch eq 'Y') { # Y - yank from cursor to EOL do_copy($term, @cursor, $cursor[0], $term->ROW_l($cursor[0]) - $cursor[0]); deactivate_mark_mode ($term); visual_mode_disable ($term, @cursor); } elsif (!$mod && $ch eq 'y') { # y - yank selected my %url = get_active_url ($term); do_copy($term, $url{row}, $url{col_from}, $url{row}, $url{col_to}) if %url; deactivate_mark_mode ($term); visual_mode_disable ($term, @cursor); } elsif (($mod & urxvt::ControlMask) && (($ch eq 'n') || ($ch eq 'p'))) { # ^n and ^p to cycle list my $dir = ($ch eq 'n') ? 1 : -1; move_url_highlight ($term, $dir); visual_mode_disable ($term, @cursor); } elsif (($mod & urxvt::ControlMask) && (($ch eq 'f') || ($ch eq 'b'))) { # ^f and ^b to scroll my $ofs = ($ch eq 'f') ? 1 : -1; visual_mode_update ($term, \@cursor, [$cursor[0] + $ofs*($term->nrow - 1), $cursor[1]]); } elsif (!$mod && $ch eq 'h') { # left if ($cursor[1] > 0) { visual_mode_update ($term, \@cursor, [$cursor[0], $cursor[1] - 1]); } } elsif (!$mod && $ch eq 'j') { # down if ($cursor[0] < $term->nrow) { visual_mode_update ($term, \@cursor, [$cursor[0] + 1, $cursor[1]]); } } elsif (!$mod && $ch eq 'k') { # up if ($cursor[0] > $term->top_row) { visual_mode_update ($term, \@cursor, [$cursor[0] - 1, $cursor[1]]); } } elsif (!$mod && $ch eq 'l') { # right if ($cursor[1] < ($term->ncol - 1)) { visual_mode_update ($term, \@cursor, [$cursor[0], $cursor[1] + 1]); } } elsif ($ch eq 'H') { visual_mode_update ($term, \@cursor, [0, $cursor[1]]); } elsif ($ch eq 'M') { visual_mode_update ($term, \@cursor, [$term->nrow / 2, $cursor[1]]); } elsif ($ch eq 'L') { visual_mode_update ($term, \@cursor, [$term->nrow, $cursor[1]]); } elsif ($ch eq '^') { $term->ROW_t($cursor[0]) =~ m/^\s*/; visual_mode_update ($term, \@cursor, [$cursor[0], $+[0]]); } elsif ($ch eq '0') { visual_mode_update ($term, \@cursor, [$cursor[0], 0]); } elsif ($ch eq '$') { visual_mode_update ($term, \@cursor, [$cursor[0], $term->ROW_l($cursor[0]) - 1]); } elsif ($ch eq 'g') { visual_mode_update ($term, \@cursor, [$term->top_row, 0]); } elsif ($ch eq 'G') { visual_mode_update ($term, \@cursor, [$term->nrow - 1, $term->ROW_l($term->nrow - 1) - 1]); } elsif ($ch eq 'w') { my @dest = @cursor; my $line = $term->ROW_t($dest[0]); ++$dest[1] while substr($line, $dest[1], 1) =~ m/\w/; until (substr($line, $dest[1], 1) =~ m/\w/) { ++$dest[1]; next if $dest[1] <= $term->ROW_l($dest[0]); ++$dest[0]; return 1 if $dest[0] >= $term->nrow; $dest[1] = 0; $line = $term->ROW_t($dest[0]); } visual_mode_update ($term, \@cursor, \@dest); } elsif ($ch eq 'e') { my @dest = @cursor; my $line = $term->ROW_t($dest[0]); ++$dest[1]; ++$dest[1] while substr($line, $dest[1], 1) =~ m/\W/; until (substr($line, $dest[1], 1) =~ m/\w/) { ++$dest[1]; next if $dest[1] <= $term->ROW_l($dest[0]); ++$dest[0]; return 1 if $dest[0] >= $term->nrow; $dest[1] = 0; $line = $term->ROW_t($dest[0]); } ++$dest[1] while substr($line, $dest[1] + 1, 1) =~ m/\w/; visual_mode_update ($term, \@cursor, \@dest); } elsif ($ch eq 'b') { my @dest = @cursor; my $line = $term->ROW_t($dest[0]); # unless at the beginning of a word, jump to the beginning --$dest[1] while ($dest[1] >= 1 && substr($line, $dest[1] - 1, 1) =~ m/\w/); if ($dest[1] == $cursor[1]) { # at beginning of a word # skip non-word characters until ($dest[1] >= 1 && substr($line, $dest[1] - 1, 1) =~ m/\w/) { --$dest[1]; next if $dest[1] > 0; --$dest[0]; return 1 if $dest[0] < 0; $dest[1] = $term->ROW_l($dest[0]); $line = $term->ROW_t($dest[0]); } --$dest[1] while $dest[1] >= 1 && substr($line, $dest[1] - 1, 1) =~ m/\w/; } visual_mode_update ($term, \@cursor, \@dest); } elsif ($ch eq 'W') { my @dest = @cursor; my $line = $term->ROW_t($dest[0]); ++$dest[1] while substr($line, $dest[1], 1) =~ m/\S/; until (substr($line, $dest[1], 1) =~ m/\S/) { ++$dest[1]; next if $dest[1] <= $term->ROW_l($dest[0]); ++$dest[0]; return 1 if $dest[0] >= $term->nrow; $dest[1] = 0; $line = $term->ROW_t($dest[0]); } visual_mode_update ($term, \@cursor, \@dest); } elsif ($ch eq 'E') { my @dest = @cursor; my $line = $term->ROW_t($dest[0]); ++$dest[1]; ++$dest[1] while substr($line, $dest[1], 1) =~ m/\s/; until (substr($line, $dest[1], 1) =~ m/\S/) { ++$dest[1]; next if $dest[1] <= $term->ROW_l($dest[0]); ++$dest[0]; return 1 if $dest[0] >= $term->nrow; $dest[1] = 0; $line = $term->ROW_t($dest[0]); } ++$dest[1] while substr($line, $dest[1] + 1, 1) =~ m/\S/; visual_mode_update ($term, \@cursor, \@dest); } elsif ($ch eq 'B') { my @dest = @cursor; my $line = $term->ROW_t($dest[0]); --$dest[1] while ($dest[1] >= 1 && substr($line, $dest[1] - 1, 1) =~ m/\S/); until ($dest[1] >= 1 && substr($line, $dest[1] - 1, 1) =~ m/\S/) { --$dest[1]; next if $dest[1] > 0; --$dest[0]; return 1 if $dest[0] < 0; $dest[1] = $term->ROW_l($dest[0]); $line = $term->ROW_t($dest[0]); } --$dest[1] while $dest[1] >= 1 && substr($line, $dest[1] - 1, 1) =~ m/\S/; visual_mode_update ($term, \@cursor, \@dest); } return 1; } # ------------------------------------------------------------------------ sub get_active_url { my ($term) = @_; my $max = $#url_db + 1; return if $url_selected < 0 || $url_selected >= $max; return if not defined $url_db[$url_selected]; return %{$url_db[$url_selected]}; } sub do_copy { my $term = shift; my ($y1, $x1, $y2, $x2) = canonicalise_coordinates(@_); $term->selection_beg($y1, $x1); $term->selection_end($y2, 1 + $x2); $term->selection_make(urxvt::CurrentTime); my $text = $term->selection(); if ($term->{have_Clipboard}) { Clipboard->copy($text); } else { my $pid = open(XCLIP, "|xclip -i"); print XCLIP $text; close(XCLIP) } status_message ($term, "copied ".length($text)." characters into clipboard"); } # ------------------------------------------------------------------------ sub move_url_highlight { my ($term, $dir) = @_; my $max = $#url_db + 1; do_highlight ($term, 0); $url_selected = ($max + $url_selected + $dir) % $max; do_highlight ($term, 1); $term->want_refresh; } sub do_highlight { my ($term, $enable) = @_; my $max = $#url_db + 1; return if $url_selected < 0 || $url_selected >= $max; return if not defined $url_db[$url_selected]; my $o = $url_db[$url_selected]; my %h = %$o; my $row = $h{row}; my $line = $term->line ($row); my $text = $line->t; my $rend = $line->r; if ($enable) { $_ |= urxvt::RS_RVid for @{$rend}[ $h{col_from} .. $h{col_to}]; # make it visible $term->view_start ( $row < 0 ? $row : 0 ); } else { $_ &= ~urxvt::RS_RVid for @{$rend}[ $h{col_from} .. $h{col_to}]; } $line->r ($rend); } # ------------------------------------------------------------------------ sub visual_mode_enable { my ($term, $mode, @cur) = @_; $visual_mode = $mode; @visual_start = @cur; } sub visual_mode_disable { my ($term, @cursor) = @_; if ($visual_mode) { $term->scr_xor_span(canonicalise_coordinates(@visual_start, @cursor)); $visual_mode = 0; $term->want_refresh; } } sub visual_mode_update { my ($term, $oldref, $newref) = @_; my @old = @$oldref; my @new = @$newref; @cursor = @new; $cursor[0] = clamp $term->top_row, $cursor[0], $term->nrow - 1; if ($visual_mode eq 'v') { $term->scr_xor_span(canonicalise_coordinates(@old, @new)); } $term->screen_cur (@cursor); $term->view_start (clamp $cursor[0] - $term->nrow + 1, $term->view_start, min 0, $cursor[0]); $term->want_refresh; } # ------------------------------------------------------------------------ sub activate_mark_mode { my ($term) = @_; print "title: " . ($term->resource("title")) . "\n"; print "name: " . ($term->resource("name")) . "\n"; print "term_name: " . ($term->resource("term_name")) . "\n"; print "color: " . ($term->resource("color")) . "\n"; my ($row, $col) = $term->screen_cur; print "cursor: $row / $col \n"; $mark_mode_active = 1; @backup_cursor = @cursor = $term->screen_cur; } sub activate_mark_url_mode { my ($term) = @_; if ($mark_mode_active) { move_url_highlight ($term, -1); } elsif ( do_scan_for_urls ($term) ) { $term->{save_view_start} = $term->view_start; move_url_highlight ($term, 0); if ($url_selected > -1) { $mark_mode_active = 1; @backup_cursor = @cursor = $term->screen_cur; } } } sub deactivate_mark_mode { my ($term) = @_; do_highlight ($term, 0); $mark_mode_active = 0; $term->screen_cur (@backup_cursor); $first_mark_set = 0; $url_selected = -1; $term->view_start ($term->{save_view_start}); $term->want_refresh; } # vim: set et ts=4 sw=4: