├── .gitignore ├── README.rst └── vim-scrollback /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | /README.html 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Overview 3 | ======== 4 | 5 | vim-scrollback is an extension for urxvt which provides a vim like scrollback 6 | mode and pasting 7 | 8 | ===== 9 | Usage 10 | ===== 11 | 12 | While not in vim scrollback mode 13 | 14 | - ctrl-v - enter vim-scrollback mode 15 | - ctrl-r * - pastes the primary clipboard onto the command line 16 | - ctrl-r + - pastes the secondary clipboard onto the command line 17 | 18 | Note: both ctrl-v and ctrl-r can be configured to different values 19 | as described in the Configuration section below. 20 | 21 | While in the vim scrollback mode the following key bindings are available: 22 | 23 | - motions: 24 | 25 | - h j k l 26 | - w e b 27 | - 0 _ $ 28 | - ctrl-u ctrl-d ctrl-f ctrl-b 29 | - gg G 30 | - f - jump to next occurrence of on the current line 31 | - F - jump to prev occurrence of on the current line 32 | 33 | - visual mode: 34 | 35 | - V v ctrl-v 36 | - gv - reselect last selection 37 | 38 | - yank visual selection (requires xclip): 39 | 40 | - y - yank to primary clipboard (*) 41 | - Y - yank to secondary clipboard (+) 42 | 43 | - paste: 44 | 45 | - p - pastes onto the end of the command line 46 | 47 | - undo / redo: 48 | 49 | - u / ctrl-R - undo / redo pastes to command line 50 | 51 | - marks: 52 | 53 | - m[a-z] '[a-z] '' 54 | 55 | - search: 56 | 57 | - / - searches up 58 | - ? - searches down 59 | - n - next in current direction 60 | - N - next in opposite direction 61 | - \* - search for word under the cursor 62 | 63 | - misc: 64 | 65 | - gf - behave like the shipped matcher plugin for urxvt. Allows you to open a 66 | url or other configured pattern and launcher. See matcher docs for info on 67 | configuring: 68 | http://cvs.schmorp.de/rxvt-unicode/doc/rxvtperl.3.html#PREPACKAGED_EXTENSIONS 69 | 70 | Note: counts can be supplied to most commands like in vim 71 | 72 | ============ 73 | Installation 74 | ============ 75 | 76 | After cloning the repository to the location of your choice, you must then 77 | enable this extension in urxvt vi your .Xresources file (or ~/.Xdefaults). 78 | 79 | :: 80 | 81 | ! enable the extension (note that you must use an absolute path, no ~/...) 82 | urxvt*perl-lib: /home/user/urxvt-vim-scrollback 83 | urxvt*perl-ext-common: vim-scrollback 84 | 85 | ============= 86 | Configuration 87 | ============= 88 | 89 | Vim scrollback supports various configuration settings which can be supplied 90 | in your ~/.Xresources (or ~/.Xdefaults) file. 91 | 92 | :: 93 | 94 | ; configure alt-s as the keybinding to enter vim scrollback mode. 95 | urxvt.vim-scrollback: M-s 96 | 97 | ; configure alt-p as the keybinding to paste mode. 98 | urxvt.vim-scrollback-paste: M-p 99 | 100 | ; configure the background and foreground colors used for the status bar while 101 | ; in scrollback mode. 102 | urxvt.vim-scrollback-bg: 16 103 | urxvt.vim-scrollback-fg: 10 104 | 105 | ; configure vim-scrollback specific matchers 106 | urxvt.vim-scrollback.pattern.1: \\B(/\\S+?):(\\d+)(?=:|$) 107 | urxvt.vim-scrollback.launcher.1: gvim +$2 $1 108 | 109 | ; configure the command used when opening urls. Note: uses the same 110 | ; configuration as the url launcher script shipped with urxvt. 111 | urxvt*urlLauncher: firefox 112 | -------------------------------------------------------------------------------- /vim-scrollback: -------------------------------------------------------------------------------- 1 | #! perl 2 | 3 | # This extension implements vim like scrollback buffer navigation, search, and 4 | # paste. Initially based on the searchable-scrollback extension and some gf 5 | # related code borrowed from the matcher extension. 6 | # 7 | # Copyright (C) 2007 - 2013 Eric Van Dewoestine 8 | # 9 | # This program is free software; you can redistribute it and/or 10 | # modify it under the terms of the GNU General Public License 11 | # as published by the Free Software Foundation; either version 2 12 | # of the License, or (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program; if not, write to the Free Software 21 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 22 | 23 | # TODO: 24 | # - really learn perl and clean this nasty script up. 25 | # - search history: support filtering by user's typed results. 26 | # - motion based yank: y 27 | # 28 | # Known Issues: 29 | # - Some weirdness may occur if terminal is resized while in vim mode. 30 | # - Multi line pasting in vim mode attempts to write the current 31 | # term/interpreter's line continuation prompt, but this may not work 32 | # in all cases. 33 | # Tested with bash, irb, mysql, psql, and python 34 | # 35 | # Api: http://cvs.schmorp.de/rxvt-unicode/doc/rxvtperl.3.html 36 | 37 | ################################################################################ 38 | # Usage 39 | # 40 | # While not in vim scrollback mode 41 | # ctrl-v - enter vim-scrollback mode 42 | # ctrl-r * - pastes the primary clipboard onto the command line 43 | # ctrl-r + - pastes the secondary clipboard onto the command line 44 | # 45 | # Note: both ctrl-v and ctrl-r can be configured to different values 46 | # (configuration examples further down). 47 | # 48 | # While in the vim scrollback mode the following key bindings work: 49 | # motions: 50 | # h j k l 51 | # w e b 52 | # 0 _ $ 53 | # ctrl-u ctrl-d ctrl-f ctrl-b 54 | # gg G 55 | # 56 | # f - jump to next occurrence of on the current line 57 | # F - jump to prev occurrence of on the current line 58 | # 59 | # visual mode: 60 | # V v ctrl-v 61 | # gv - reselect last selection 62 | # 63 | # yank: 64 | # y - yank to primary clipboard (*) 65 | # Y - yank to secondary clipboard (+) 66 | # 67 | # paste: 68 | # p - pastes onto the end of the command line 69 | # 70 | # undo / redo: 71 | # u / ctrl-R - undo / redo pastes to command line 72 | # 73 | # marks: 74 | # m[a-z] '[a-z] '' 75 | # 76 | # search: 77 | # / - searches up 78 | # ? - searches down 79 | # n - next in current direction 80 | # N - next in opposite direction 81 | # * - search for word under the cursor 82 | # 83 | # misc: 84 | # gf - behave like the shipped matcher plugin for urxvt. allows you to open a 85 | # url or other configured pattern and launcher. See matcher docs for info 86 | # on configuring: 87 | # http://cvs.schmorp.de/rxvt-unicode/doc/rxvtperl.3.html#PREPACKAGED_EXTENSIONS 88 | # 89 | # Note: counts can be supplied to most commands like in vim 90 | # 91 | ################################################################################ 92 | # Configuration 93 | # 94 | # Vim scrollback supports various configuration settings which can be supplied 95 | # in your ~/.Xdefaults file. 96 | # 97 | # ; configure alt-s as the keybinding to enter vim scrollback mode. 98 | # urxvt.vim-scrollback: M-s 99 | # 100 | # ; configure alt-p as the keybinding to paste mode. 101 | # urxvt.vim-scrollback-paste: M-p 102 | # 103 | # ; configure the background and foreground colors used for the status bar while 104 | # ; in scrollback mode. 105 | # urxvt.vim-scrollback-bg: 16 106 | # urxvt.vim-scrollback-fg: 10 107 | # 108 | # ; configure vim-scrollback specific matchers 109 | # urxvt.vim-scrollback.pattern.1: \\B(/\\S+?):(\\d+)(?=:|$) 110 | # urxvt.vim-scrollback.launcher.1: gvim +$2 $1 111 | # 112 | # ; configure the command used when opening urls. Note, uses the same 113 | # ; configuration as the url launcher script shipped with urxvt. 114 | # urxvt*urlLauncher: firefox 115 | # 116 | ################################################################################ 117 | 118 | my $DEBUG = 1; 119 | my $DEBUG_FILE = '/tmp/urxvt_vim_scrollback.log'; 120 | my $DEBUG_FH; 121 | 122 | # copied from matcher 123 | my $url = 124 | qr{ 125 | (?:https?://|ftp://|news://|mailto:|file://|\bwww\.) 126 | [a-zA-Z0-9\-\@;\/?:&=%\$_.+!*\x27,~#]* 127 | ( 128 | \([a-zA-Z0-9\-\@;\/?:&=%\$_.+!*\x27,~#]*\)| # Allow a pair of matched parentheses 129 | [a-zA-Z0-9\-\@;\/?:&=%\$_+*~] # exclude some trailing characters (heuristic) 130 | )+ 131 | }x; 132 | 133 | sub on_init { 134 | my ($self) = @_; 135 | 136 | my $hotkey = $self->x_resource("vim-scrollback") || "M-v"; 137 | $self->parse_keysym($hotkey, "perl:vim-scrollback:start") 138 | or warn "unable to register '$hotkey' as vim scrollback start hotkey\n"; 139 | 140 | $hotkey = $self->x_resource("vim-scrollback-paste") || "M-r"; 141 | $self->parse_keysym($hotkey, "perl:vim-scrollback:paste") 142 | or warn "unable to register '$hotkey' as vim paste hotkey\n"; 143 | 144 | () 145 | } 146 | 147 | sub on_user_command { 148 | my ($self, $cmd) = @_; 149 | $cmd eq "vim-scrollback:start" and $self->enter; 150 | $cmd eq "vim-scrollback:paste" and $self->paste; 151 | () 152 | } 153 | 154 | # entry point for vim mode 155 | sub enter { 156 | my ($self) = @_; 157 | 158 | return if $self->{overlay}; 159 | 160 | if ($DEBUG){ 161 | open($DEBUG_FH, ">$DEBUG_FILE"); 162 | select((select($DEBUG_FH), $| = 1)[0]); 163 | } 164 | 165 | ($self->{name} = __PACKAGE__) =~ s/.*:://; 166 | $self->{name} =~ tr/_/-/; 167 | 168 | $self->{view_start} = $self->view_start; 169 | $self->{pty_ev_events} = $self->pty_ev_events(urxvt::EV_NONE); 170 | $self->enable( 171 | key_press => \&key_press, 172 | resize_all_windows => \&resize_window 173 | ); 174 | 175 | my ($lnum, $cnum) = $self->screen_cur(); 176 | $self->{orig_nrow} = $self->nrow; 177 | $self->{orig_lnum} = $lnum; 178 | $self->{orig_cnum} = $cnum; 179 | $self->{out_lnum} = $lnum; 180 | $self->{out_cnum} = $cnum; 181 | $self->{visual_mode}; 182 | $self->{visual_clnum}; 183 | $self->{visual_ccnum}; 184 | $self->{visual_slnum}; 185 | $self->{visual_scnum}; 186 | $self->{visual_elnum}; 187 | $self->{visual_ecnum}; 188 | $self->{visual_rectangle}; 189 | 190 | my $end = $self->line($lnum)->end; 191 | if ($end == $self->nrow - 1){ 192 | $self->screen_cur($end, 0); 193 | $self->scr_add_lines("\n"); 194 | $self->{orig_lnum} = $lnum - 1; 195 | $self->{out_lnum} = $lnum - 1; 196 | $self->screen_cur($self->{orig_lnum}, $cnum); 197 | } 198 | 199 | $self->{term_buffer} = []; 200 | $self->{term_undo} = []; 201 | $self->{term_redo} = []; 202 | $self->{mode} = "normal"; 203 | $self->{mode_info} = ""; 204 | $self->{registers} = {}; 205 | $self->{marks} = {}; 206 | $self->{search_history} = []; 207 | $self->{key_buffer} = ""; 208 | 209 | my $fg_color = $self->x_resource("vim-scrollback-fg"); 210 | my $bg_color = $self->x_resource("vim-scrollback-bg"); 211 | my $style = urxvt::OVERLAY_RSTYLE | urxvt::RS_Bold; 212 | $style = urxvt::SET_FGCOLOR($style, $fg_color) if $fg_color; 213 | $style = urxvt::SET_BGCOLOR($style, $bg_color) if $bg_color; 214 | $self->{msg_style} = $style; 215 | 216 | # modified from matcher 217 | my $urlLauncher = $self->my_resource("launcher") || 218 | $self->x_resource("urlLauncher") || 219 | "sensible-browser"; 220 | # handle shell variable 221 | if ($urlLauncher =~ /^\$/) { 222 | $urlLauncher =~ s/\$//; 223 | $urlLauncher = $ENV{$urlLauncher}; 224 | } 225 | $self->{launcher} = $urlLauncher; 226 | 227 | # copied from matcher 228 | my @defaults = ($url); 229 | my @matchers; 230 | for (my $idx = 0; defined (my $res = $self->my_resource("pattern.$idx") || $defaults[$idx]); $idx++) { 231 | $res = $self->locale_decode($res); 232 | utf8::encode $res; 233 | my $launcher = $self->my_resource("launcher.$idx"); 234 | $launcher =~ s/\$&|\$\{&\}/\${0}/g if ($launcher); 235 | # handle shell variable 236 | if ($launcher =~ /^\$/) { 237 | $launcher =~ s/\$//; 238 | $launcher = $ENV{$launcher}; 239 | } 240 | push @matchers, [qr($res)x, $launcher]; 241 | } 242 | $self->{matchers} = \@matchers; 243 | 244 | $self->msg(""); 245 | } 246 | 247 | sub leave { 248 | my ($self, $event) = @_; 249 | 250 | $self->term_write_flush(); 251 | $self->disable("key_press", "resize_all_windows"); 252 | $self->pty_ev_events($self->{pty_ev_events}); 253 | $self->clear_selection($event); 254 | $self->screen_cur($self->{orig_lnum}, $self->{orig_cnum}); 255 | $self->view_start($self->{view_start}); 256 | $self->want_refresh; 257 | 258 | delete $self->{overlay}; 259 | delete $self->{search}; 260 | delete $self->{search_history}; 261 | delete $self->{mode}; 262 | delete $self->{mode_info}; 263 | delete $self->{orig_lnum}; 264 | delete $self->{orig_cnum}; 265 | delete $self->{out_lnum}; 266 | delete $self->{out_cnum}; 267 | delete $self->{term_buffer}; 268 | delete $self->{term_undo}; 269 | delete $self->{term_redo}; 270 | delete $self->{visual_mode}; 271 | delete $self->{launcher}; 272 | delete $self->{matchers}; 273 | 274 | if ($DEBUG){ 275 | close($DEBUG_FH); 276 | } 277 | } 278 | 279 | sub resize_window { 280 | my ($self, $t_width, $t_height) = @_; 281 | 282 | # prompt at bottom of term 283 | if($self->{orig_lnum} == $self->{orig_nrow} - 2){ 284 | # adjust location of the prompt 285 | my $adj = $self->nrow - $self->{orig_nrow}; 286 | $self->{orig_lnum} += $adj; 287 | 288 | # adjust location of last visual mode if necessary. 289 | if ($self->{visual_mode}){ 290 | $self->{visual_slnum} += $adj; 291 | $self->{visual_elnum} += $adj; 292 | $self->{visual_clnum} += $adj; 293 | } 294 | 295 | # adjust all marks 296 | for my $key (keys %{$self->{marks}}){ 297 | $self->{marks}{$key}[0] += $adj; 298 | } 299 | 300 | # failed attempt to reposition the cursor. 301 | #my ($lnum, $cnum) = $self->screen_cur(); 302 | #$self->screen_cur($lnum + $adj, $cnum); 303 | #$self->debug("adj = $adj lnum = $lnum cnum = $cnum"); 304 | #$self->view_start(List::Util::min 0, $lnum - ($self->nrow >> 1)); 305 | #$self->want_refresh; 306 | } 307 | 308 | $self->{orig_nrow} = $self->nrow; 309 | } 310 | 311 | sub key_press { 312 | my ($self, $event, $keysym, $string) = @_; 313 | 314 | #delete $self->{manpage_overlay}; 315 | 316 | if ($self->{mode} eq "search"){ 317 | return $self->key_press_search($event, $keysym, $string); 318 | }elsif ($self->{mode} =~ /visual/){ 319 | return $self->key_press_visual($event, $keysym, $string); 320 | } 321 | 322 | return $self->key_press_normal($event, $keysym, $string); 323 | } 324 | 325 | sub key_press_normal { 326 | my ($self, $event, $keysym, $string) = @_; 327 | 328 | $self->msg(""); 329 | if ($keysym == 0xff1b || 330 | $string =~ /^\cC$/ || 331 | ($self->{key_buffer} =~ /^$/ && $keysym == 0xff0d)) 332 | { # esc, ctrl-c, 333 | if($self->{key_buffer}){ 334 | $self->{key_buffer} = ""; 335 | $self->msg(""); 336 | }else{ 337 | $self->leave($event); 338 | } 339 | return 1; 340 | } 341 | 342 | # arbitrary limit on the number of keys we'll allow the user to press 343 | # without matching a command before we just stop recording them. 344 | if (length($self->{key_buffer}) < 5){ 345 | $self->{key_buffer} .= $string; 346 | } 347 | $string = $self->{key_buffer}; 348 | 349 | # parse out count if necessary 350 | $self->{repeat} = 1; 351 | if ($string =~ /^\d+\D/){ 352 | (my $repeat = $string) =~ s/^(\d+)\D.*$/\1/; 353 | $self->{repeat} = $repeat; 354 | $string =~ s/^\d+(.*)/\1/; 355 | } 356 | 357 | # searching 358 | if ($string =~ /^[\/?]$/) { # start search mode with / or ? 359 | ($self->{search_lnum}, $self->{search_cnum}) = $self->screen_cur(); 360 | ($self->{search_slnum}, $self->{search_scnum}) = $self->screen_cur(); 361 | my $line = $self->line($self->{search_lnum}); 362 | $self->{search_cnum} = $line->offset_of($self->{search_lnum}, $self->{search_cnum}); 363 | $self->{search_scnum} = $self->{search_cnum}; 364 | ($self->{search_olnum}, $self->{search_ocnum}) = $self->screen_cur(); 365 | $self->{mode} = "search"; 366 | $self->{search} = "(?i)"; 367 | $self->{search_save} = $self->{search}; 368 | $self->{search_history_index} = -1; 369 | $self->{search_dir} = $string eq '/' ? -1 : 1; 370 | $self->{search_wrap} = 0; 371 | $self->search_prompt; 372 | } elsif ($string eq "*") { # find word under the cursor 373 | my ($lnum, $cnum) = $self->screen_cur(); 374 | my $line = $self->line($lnum); 375 | my $index = $cnum + ($self->ncol * ($lnum - $line->beg)); 376 | 377 | my $prefix = substr($line->t, 0, $index + 1); 378 | $prefix =~ s/.*\W(\w.*?)/\1/; 379 | my $suffix = substr($line->t, $index + 1); 380 | $suffix =~ s/(\w.*?)\W.*/\1/; 381 | if ($suffix =~ /^\s/){ 382 | $suffix = ''; 383 | } 384 | my $word = $prefix . $suffix; 385 | $self->{search} = "\\b$word\\b"; 386 | unshift(@{$self->{search_history}}, $self->{search}); 387 | $self->{search_dir} = -1; 388 | $self->{search_wrap} = 0; 389 | ($self->{search_lnum}, $self->{search_cnum}) = $self->screen_cur(); 390 | ($self->{search_slnum}, $self->{search_scnum}) = $self->screen_cur(); 391 | my $line = $self->line($self->{search_lnum}); 392 | $self->{search_cnum} = $line->offset_of($self->{search_lnum}, $self->{search_cnum}); 393 | $self->{search_scnum} = $self->{search_cnum}; 394 | $self->search($event, $self->{search_dir}, 1); 395 | $self->{search_wrap} = 0; 396 | 397 | } elsif ($string eq "n") { # next search result (up) 398 | ($self->{search_lnum}, $self->{search_cnum}) = $self->screen_cur(); 399 | ($self->{search_slnum}, $self->{search_scnum}) = $self->screen_cur(); 400 | my $line = $self->line($self->{search_lnum}); 401 | $self->{search_cnum} = $line->offset_of($self->{search_lnum}, $self->{search_cnum}); 402 | $self->{search_scnum} = $self->{search_cnum}; 403 | $self->search($event, $self->{search_dir}, 1); 404 | $self->{search_wrap} = 0; 405 | } elsif ($string eq "N") { # previous search result (down) 406 | ($self->{search_lnum}, $self->{search_cnum}) = $self->screen_cur(); 407 | ($self->{search_slnum}, $self->{search_scnum}) = $self->screen_cur(); 408 | my $line = $self->line($self->{search_lnum}); 409 | $self->{search_cnum} = $line->offset_of($self->{search_lnum}, $self->{search_cnum}); 410 | $self->{search_scnum} = $self->{search_cnum}; 411 | $self->search($event, 0 - $self->{search_dir}, 1); 412 | $self->{search_wrap} = 0; 413 | 414 | # visual mode 415 | } elsif ($string eq "v") { # start char based visual selection mode 416 | $self->{mode} = "visual"; 417 | $self->{mode_info} = "-- VISUAL -- "; 418 | ($self->{visual_lnum}, $self->{visual_cnum}) = $self->screen_cur(); 419 | my ($lnum, $cnum) = $self->screen_cur(); 420 | $self->move_cursor($event, $lnum, $cnum); 421 | $self->msg(""); 422 | } elsif ($string eq "V") { # start line based visual selection mode 423 | $self->{mode} = "visual_line"; 424 | $self->{mode_info} = "-- VISUAL LINE -- "; 425 | ($self->{visual_lnum}, $self->{visual_cnum}) = $self->screen_cur(); 426 | my ($lnum, $cnum) = $self->screen_cur(); 427 | $self->move_cursor($event, $lnum, $cnum); 428 | $self->msg(""); 429 | } elsif ($string =~ /^\cV$/) { # start block based visual selection mode 430 | $self->{mode} = "visual_block"; 431 | $self->{mode_info} = "-- VISUAL BLOCK -- "; 432 | ($self->{visual_lnum}, $self->{visual_cnum}) = $self->screen_cur(); 433 | my ($lnum, $cnum) = $self->screen_cur(); 434 | $self->move_cursor($event, $lnum, $cnum); 435 | $self->msg(""); 436 | 437 | # paste 438 | } elsif ($string eq "p") { 439 | $self->set_mark("'"); 440 | $self->term_write($event, $self->{registers}{'"'}); 441 | 442 | # motions 443 | } elsif ($string eq "j") { 444 | my ($lnum, $cnum) = $self->screen_cur(); 445 | for(my $ii = 0; $ii < $self->{repeat}; $ii++){ 446 | if ($lnum < $self->nrow - 2){ 447 | $self->move_cursor($event, ++$lnum, $cnum); 448 | } 449 | } 450 | } elsif ($string eq "k") { 451 | my ($lnum, $cnum) = $self->screen_cur(); 452 | for(my $ii = 0; $ii < $self->{repeat}; $ii++){ 453 | if ($lnum > $self->top_row){ 454 | $self->move_cursor($event, --$lnum, $cnum); 455 | } 456 | } 457 | } elsif ($string eq "h") { 458 | my ($lnum, $cnum) = $self->screen_cur(); 459 | for(my $ii = 0; $ii < $self->{repeat}; $ii++){ 460 | if ($cnum > 0){ 461 | $self->move_cursor($event, $lnum, --$cnum); 462 | } 463 | } 464 | } elsif ($string eq "l") { 465 | my ($lnum, $cnum) = $self->screen_cur(); 466 | for(my $ii = 0; $ii < $self->{repeat}; $ii++){ 467 | if ($cnum < $self->ncol - 1){ 468 | $self->move_cursor($event, $lnum, ++$cnum); 469 | } 470 | } 471 | } elsif ($string eq "0") { 472 | my ($lnum, $cnum) = $self->screen_cur(); 473 | my $line = $self->line($lnum); 474 | $self->move_cursor($event, $line->coord_of(0)); 475 | } elsif ($string eq "_") { 476 | my ($lnum, $cnum) = $self->screen_cur(); 477 | my $line = $self->line($lnum); 478 | my $ltrimmed = $line->t; 479 | $ltrimmed =~ s/^\s+//; 480 | my $offset = (length($line->t) - length($ltrimmed)); 481 | $self->move_cursor($event, $line->coord_of(0 + $offset)); 482 | } elsif ($string eq '$') { 483 | my ($lnum, $cnum) = $self->screen_cur(); 484 | my $line = $self->line($lnum); 485 | $self->move_cursor($event, $line->coord_of($line->l - 1)); 486 | } elsif ($string eq "gg") { 487 | $self->set_mark("'"); 488 | $self->move_cursor($event, $self->top_row, 0); 489 | } elsif ($string eq "G") { 490 | $self->set_mark("'"); 491 | $self->move_cursor($event, $self->{orig_lnum}, $self->{orig_cnum}); 492 | } elsif ($string =~ /^\cD$/) { 493 | my ($lnum, $cnum) = $self->screen_cur(); 494 | my $row = $lnum + $self->nrow / 2; 495 | $row = $row < $self->{orig_lnum} ? $row : $self->{orig_lnum}; 496 | $self->move_cursor($event, $row, $cnum); 497 | } elsif ($string =~ /^\cU$/) { 498 | my ($lnum, $cnum) = $self->screen_cur(); 499 | my $row = $lnum - $self->nrow / 2; 500 | $row = $row > $self->top_row ? $row : $self->top_row; 501 | $self->move_cursor($event, $row, $cnum); 502 | } elsif ($string =~ /^\cF$/) { 503 | my ($lnum, $cnum) = $self->screen_cur(); 504 | my $row = $lnum + $self->nrow - 1; 505 | $row = $row < $self->{orig_lnum} ? $row : $self->{orig_lnum}; 506 | $self->move_cursor($event, $row, $cnum); 507 | } elsif ($string =~ /^\cB$/) { 508 | my ($lnum, $cnum) = $self->screen_cur(); 509 | my $row = $lnum - $self->nrow + 1; 510 | $row = $row > $self->top_row ? $row : $self->top_row; 511 | $self->move_cursor($event, $row, $cnum); 512 | } elsif ($string eq "w" or $string eq "e") { 513 | my $pattern = $string eq "w" ? '\W\w' : '\w\W'; 514 | my $adj = $string eq "w" ? 0 : -1; 515 | for(my $ii = 0; $ii < $self->{repeat}; $ii++){ 516 | my ($lnum, $cnum) = $self->screen_cur(); 517 | my $line = $self->line($lnum); 518 | my $index = $cnum + ($self->ncol * ($lnum - $line->beg)); 519 | my $suffix = substr($line->t, $index + 1); 520 | if($suffix =~ /$pattern/g){ 521 | my $offset = $index + pos($suffix) + $adj; 522 | $self->move_cursor($event, $line->coord_of($offset)); 523 | }elsif ($lnum < $self->nrow - 2){ 524 | $self->move_cursor($event, $line->end + 1, 0); 525 | } 526 | } 527 | } elsif ($string eq "b") { 528 | for(my $ii = 0; $ii < $self->{repeat}; $ii++){ 529 | my ($lnum, $cnum) = $self->screen_cur(); 530 | my $line = $self->line($lnum); 531 | my $index = $cnum - 1 + ($self->ncol * ($lnum - $line->beg)); 532 | if($index > 0){ 533 | my $prefix = substr($line->t, 0, $index); 534 | if($prefix =~ /.*\W\w/g){ 535 | my $offset = pos($prefix) - 1; 536 | $self->move_cursor($event, $line->coord_of($offset)); 537 | }elsif($cnum != 0){ 538 | $self->move_cursor($event, $lnum, 0); 539 | } 540 | }else{ 541 | $self->screen_cur($lnum - 1, $self->ncol); 542 | $self->key_press_normal($event, $keysym, $string); 543 | } 544 | } 545 | } elsif ($string =~ /^f./) { 546 | for(my $ii = 0; $ii < $self->{repeat}; $ii++){ 547 | my ($lnum, $cnum) = $self->screen_cur(); 548 | (my $char = $string) =~ s/^f(.)/\1/; 549 | $char = quotemeta($char); 550 | my $line = $self->line($lnum); 551 | my $index = $cnum + ($self->ncol * ($lnum - $line->beg)); 552 | my $suffix = substr($line->t, $index + 1); 553 | if($suffix =~ /$char/g){ 554 | my $offset = $index + pos($suffix); 555 | $self->move_cursor($event, $line->coord_of($offset)); 556 | } 557 | } 558 | } elsif ($string =~ /^F./) { 559 | for(my $ii = 0; $ii < $self->{repeat}; $ii++){ 560 | my ($lnum, $cnum) = $self->screen_cur(); 561 | (my $char = $string) =~ s/^F(.)/\1/; 562 | $char = quotemeta($char); 563 | my $line = $self->line($lnum); 564 | my $index = $cnum - 1 + ($self->ncol * ($lnum - $line->beg)); 565 | if($index > 0){ 566 | my $prefix = substr($line->t, 0, $index); 567 | if($prefix =~ /.*$char/g){ 568 | my $offset = pos($prefix) - 1; 569 | $self->move_cursor($event, $line->coord_of($offset)); 570 | } 571 | } 572 | } 573 | 574 | # undo / redo 575 | } elsif ($string eq "u") { 576 | $self->set_mark("'"); 577 | push(@{$self->{term_redo}}, pop(@{$self->{term_buffer}})); 578 | my ($slnum, $scnum, $elnum, $ecnum) = @{pop(@{$self->{term_undo}})}; 579 | $self->screen_cur($slnum, $scnum); 580 | 581 | my ($beg, $end) = (0, 0); 582 | for(my $ii = $slnum; $ii <= $elnum; $ii++){ 583 | $beg = ($ii == $slnum) ? $scnum : 0; 584 | $end = ($ii == $elnum) ? $ecnum : $self->ncol; 585 | for(my $jj = $beg; $jj <= $end; $jj++){ 586 | $self->scr_add_lines(" "); 587 | } 588 | } 589 | 590 | $self->{out_lnum} = $slnum; 591 | $self->{out_cnum} = $scnum; 592 | $self->move_cursor($event, $slnum, $scnum); 593 | } elsif ($string =~ /^\cR$/) { 594 | $self->set_mark("'"); 595 | $self->term_write($event, pop(@{$self->{term_redo}})); 596 | 597 | } elsif ($string =~ /^\cG$/) { 598 | if ($DEBUG){ 599 | my ($lnum, $cnum) = $self->screen_cur(); 600 | } 601 | 602 | # marks 603 | } elsif ($string =~ /^m[a-z']$/) { 604 | (my $mark = $string) =~ s/^m(.)/\1/; 605 | $self->set_mark($mark); 606 | } elsif ($string =~ /^'[a-z']$/) { 607 | (my $mark = $string) =~ s/^'(.)/\1/; 608 | my $lnum = $self->{marks}{$mark}[0]; 609 | my $cnum = $self->{marks}{$mark}[1]; 610 | $self->set_mark("'"); 611 | $self->move_cursor($event, $lnum, $cnum); 612 | 613 | # g mappings 614 | } elsif ($string eq "gv") { 615 | if ($self->{visual_mode}){ 616 | my ($sl, $sc) = ($self->{visual_slnum}, $self->{visual_scnum}); 617 | my ($el, $ec) = ($self->{visual_elnum}, $self->{visual_ecnum}); 618 | $self->move_cursor($event, $self->{visual_clnum}, $self->{visual_ccnum}); 619 | $self->make_selection($event, $sl, $sc, $el, $ec, $self->{visual_rectangle}); 620 | $self->{mode} = $self->{visual_mode}; 621 | } 622 | } elsif ($string eq "gf") { 623 | my ($lnum, $cnum) = $self->screen_cur(); 624 | my @exec = $self->command_for($lnum, $cnum); 625 | 626 | # handle ticket numbers 627 | # TODO: convert to generic url variable substitution support. 628 | if (@exec[1] =~ /^#\d+$/){ 629 | (my $ticket = @exec[1]) =~ s/^#//; 630 | (my $url = @exec[0]) =~ s//$ticket/; 631 | @exec = ($self->{launcher}, $url); 632 | } 633 | 634 | if (@exec){ 635 | $self->exec_async(@exec); 636 | } 637 | 638 | }elsif ($string =~ /^([0-9]+|[gm'])$/) { 639 | $self->msg($self->{msg}); 640 | return 1; 641 | } 642 | 643 | $self->{key_buffer} = ""; 644 | $self->msg($self->{msg}); 645 | return 1; 646 | } 647 | 648 | sub key_press_search { 649 | my ($self, $event, $keysym, $string) = @_; 650 | 651 | if ($keysym == 0xff0d || $keysym == 0xff8d) { # enter 652 | $self->normal_mode($event); 653 | unshift(@{$self->{search_history}}, $self->{search}); 654 | if ($self->{search_found}){ 655 | $self->set_mark("'"); 656 | my ($lnum, $cnum) = ($self->{search_found}[0], $self->{search_found}[1]); 657 | if ($lnum){ 658 | my $line = $self->line($lnum); 659 | ($lnum, $cnum) = $line->coord_of($cnum); 660 | $self->move_cursor($event, $lnum, $cnum); 661 | } 662 | } 663 | } elsif ($keysym == 0xff1b || $string =~ /^\cC$/) { # escape or ctrl-c 664 | $self->normal_mode($event, 1); 665 | $self->move_cursor($event, $self->{search_olnum}, $self->{search_ocnum}); 666 | } elsif ($keysym == 0xff08) { # backspace 667 | substr $self->{search}, -1, 1, ""; 668 | $self->{search_save} = $self->{search}; 669 | $self->incremental_search($event); 670 | } elsif ($string !~ /[\x00-\x1f\x80-\xaf]/) { 671 | $self->{search} .= $self->locale_decode($string); 672 | $self->{search_save} = $self->{search}; 673 | $self->incremental_search($event); 674 | } elsif ($keysym == 0xff52) { # up 675 | my $length = length(@{$self->{search_history}}); 676 | if ($self->{search_history_index} < $length and $length > 0) { 677 | $self->{search_history_index}++; 678 | $self->{search} = @{$self->{search_history}}[$self->{search_history_index}]; 679 | $self->incremental_search($event); 680 | } 681 | } elsif ($keysym == 0xff54) { # down 682 | if ($self->{search_history_index} > 0){ 683 | $self->{search_history_index}--; 684 | $self->{search} = @{$self->{search_history}}[$self->{search_history_index}]; 685 | $self->incremental_search($event); 686 | } elsif ($self->{search_history_index} == 0){ 687 | $self->{search_history_index}--; 688 | $self->{search} = $self->{search_save}; 689 | $self->incremental_search($event); 690 | } 691 | } 692 | 693 | 1 694 | } 695 | 696 | sub key_press_visual { 697 | my ($self, $event, $keysym, $string) = @_; 698 | 699 | if ($keysym == 0xff1b || $string =~ /^\cC$/) { # escape or ctrl-c 700 | $self->normal_mode($event, 1); 701 | } elsif ($string eq "v") { 702 | if ($self->{mode} eq "visual") { 703 | $self->normal_mode($event, 1); 704 | }else{ 705 | $self->{mode} = "visual"; 706 | $self->{mode_info} = "-- VISUAL -- "; 707 | my ($lnum, $cnum) = $self->screen_cur(); 708 | $self->move_cursor($event, $lnum, $cnum); 709 | $self->msg(""); 710 | } 711 | } elsif ($string eq "V") { 712 | if ($self->{mode} eq "visual_line") { 713 | $self->normal_mode($event, 1); 714 | }else{ 715 | $self->{mode} = "visual_line"; 716 | $self->{mode_info} = "-- VISUAL LINE -- "; 717 | my ($lnum, $cnum) = $self->screen_cur(); 718 | $self->move_cursor($event, $lnum, $cnum); 719 | $self->msg(""); 720 | } 721 | } elsif ($string =~ /^\cV$/) { 722 | if ($self->{mode} eq "visual_block") { 723 | $self->normal_mode($event, 1); 724 | }else{ 725 | $self->{mode} = "visual_block"; 726 | $self->{mode_info} = "-- VISUAL BLOCK -- "; 727 | my ($lnum, $cnum) = $self->screen_cur(); 728 | $self->move_cursor($event, $lnum, $cnum); 729 | $self->msg(""); 730 | } 731 | } elsif ($string eq "y") { 732 | $self->{registers}{'"'} = $self->selection(); 733 | $self->normal_mode($event, 1); 734 | } elsif ($string eq "Y") { 735 | # the act of selecting puts the text on the primary, so just need to copy 736 | # to secondary. 737 | if (open(my $process, "| xclip -i -selection clipboard")){ 738 | print $process $self->selection(); 739 | } 740 | $self->normal_mode($event, 1); 741 | #} elsif ($string !~ /[\x00-\x1f\x80-\xaf]/) { 742 | }else{ 743 | # pass to key_press_normal 744 | return $self->key_press_normal($event, $keysym, $string); 745 | } 746 | 747 | 1 748 | } 749 | 750 | sub normal_mode { 751 | my ($self, $event, $clear_selection) = @_; 752 | if ($self->{mode} =~ /^visual/){ 753 | ($self->{visual_clnum}, $self->{visual_ccnum}) = $self->screen_cur; 754 | ($self->{visual_slnum}, $self->{visual_scnum}) = $self->selection_beg; 755 | ($self->{visual_elnum}, $self->{visual_ecnum}) = $self->selection_end; 756 | $self->{visual_mode} = $self->{mode}; 757 | $self->{visual_rectangle} = $self->{mode} eq 'visual_block'; 758 | } 759 | $self->{mode} = "normal"; 760 | $self->{mode_info} = ""; 761 | $self->msg(""); 762 | if ($clear_selection){ 763 | $self->clear_selection($event); 764 | } 765 | } 766 | 767 | sub search { 768 | my ($self, $event, $dir, $move_cursor) = @_; 769 | 770 | my ($search_row, $search_col) = ($self->{search_lnum}, $self->{search_cnum}); 771 | my ($search_srow, $search_scol) = ($self->{search_slnum}, $self->{search_scnum}); 772 | my ($top_row, $bot_row) = ($self->top_row, $self->nrow); 773 | my $search = $self->special_encode($self->{search}); 774 | $self->{search_found} = []; 775 | 776 | no re 'eval'; # just to be sure 777 | if (my $re = eval { qr/$search/ }) { 778 | while (($dir < 0 and ((not $self->{search_wrap} and $search_row >= $top_row) or 779 | $search_row > $search_srow)) or 780 | ($dir > 0 and ((not $self->{search_wrap} and $search_row <= $bot_row) or 781 | $search_row < $search_srow))) 782 | { 783 | my $line = $self->line($search_row) 784 | or last; 785 | 786 | my $text = $line->t; 787 | if ($text =~ /$re/g) { 788 | my ($slnum, $selscnum) = $line->coord_of($-[0]); 789 | my ($elnum, $selecnum) = $line->coord_of($+[0]); 790 | my $scnum = $-[0]; 791 | my $ecnum = $+[0]; 792 | 793 | while ($text =~ /$re/g and 794 | ($dir < 0 or ($dir > 0 and $scnum <= $search_col))) 795 | { 796 | my ($nlnum, $ncnum) = $line->coord_of($-[0]); 797 | my $ncnum = $-[0]; 798 | if (($dir < 0 and $ncnum < $search_col) or 799 | ($dir > 0 and $ncnum > $search_col)) 800 | { 801 | ($slnum, $selscnum) = $line->coord_of($-[0]); 802 | ($elnum, $selecnum) = $line->coord_of($+[0]); 803 | $scnum = $-[0]; 804 | $ecnum = $+[0]; 805 | } 806 | } 807 | 808 | if (($dir < 0 and ($scnum < $search_col or $slnum < $search_row)) or 809 | ($dir > 0 and ($scnum > $search_col or $slnum > $search_row))) 810 | { 811 | $self->make_selection($event, $slnum, $selscnum, $elnum, $selecnum); 812 | $self->{search_found} = [$slnum, $scnum]; 813 | $self->view_start(List::Util::min 0, $slnum - ($self->nrow >> 1)); 814 | if ($move_cursor){ 815 | $self->set_mark("'"); 816 | $self->{search_lnum} = $slnum; 817 | $self->{search_cnum} = $scnum; 818 | $self->move_cursor($event, $slnum, $selscnum); 819 | 820 | if ($dir < 0 and $search_row > $search_srow 821 | and not $self->{search_wrap_notify}){ 822 | $self->msg("search hit TOP, continuing at BOTTOM"); 823 | $self->{search_wrap_notify} = 1; 824 | }elsif ($dir > 0 and $search_row < $search_srow 825 | and not $self->{search_wrap_notify}){ 826 | $self->msg("search hit BOTTOM, continuing at TOP"); 827 | $self->{search_wrap_notify} = 1; 828 | } 829 | } 830 | return $self->{search_found}; 831 | } 832 | } 833 | if ($dir < 0 and $search_row == $top_row){ 834 | $search_row = $bot_row; 835 | $search_col = $self->line($search_row)->l; 836 | $self->{search_wrap} = 1; 837 | $self->{search_wrap_notify} = 0; 838 | }elsif ($dir > 0 and $search_row == $bot_row){ 839 | ($search_row, $search_col) = ($top_row, 0); 840 | $self->{search_wrap} = 1; 841 | $self->{search_wrap_notify} = 0; 842 | }else{ 843 | $search_row = $dir < 0 ? $line->beg - 1 : $line->end + 1; 844 | $search_col = $dir < 0 ? $self->line($search_row)->l : -1; 845 | } 846 | } 847 | } 848 | 849 | return 0; 850 | } 851 | 852 | sub incremental_search { 853 | my ($self, $event) = @_; 854 | 855 | $self->{search} =~ s/^\(\?i\)// 856 | if $self->{search} =~ /^\(.*[[:upper:]]/; 857 | 858 | if(not $self->search($event, $self->{search_dir})){ 859 | $self->clear_selection($event); 860 | my $line = $self->line($self->{search_slnum}); 861 | my ($lnum, $cnum) = $line->coord_of($self->{search_scnum}); 862 | $self->move_cursor($event, $lnum, $cnum); 863 | } 864 | $self->{search_lnum} = $self->{search_slnum}; 865 | $self->{search_cnum} = $self->{search_scnum}; 866 | $self->{search_wrap} = 0; 867 | $self->search_prompt; 868 | 869 | 1 870 | } 871 | 872 | sub search_prompt { 873 | my ($self) = @_; 874 | my $key = $self->{search_dir} == -1 ? '/' : '?'; 875 | $self->msg("$key$self->{search}█"); 876 | } 877 | 878 | sub move_cursor { 879 | my ($self, $event, $lnum, $cnum) = @_; 880 | $self->screen_cur($lnum, $cnum); 881 | 882 | if ($self->{mode} eq "visual"){ 883 | my $vlnum = $self->{visual_lnum}; 884 | my $vcnum = $self->{visual_cnum}; 885 | if ($lnum > $vlnum or ($vlnum == $lnum and $cnum >= $vcnum)){ 886 | $self->make_selection($event, $vlnum, $vcnum, $lnum, $cnum + 1); 887 | }else{ 888 | $self->make_selection($event, $lnum, $cnum, $vlnum, $vcnum + 1); 889 | } 890 | }elsif ($self->{mode} eq "visual_line"){ 891 | my $vlnum = $self->{visual_lnum}; 892 | if ($vlnum > $lnum){ 893 | $self->make_selection($event, $lnum, 0, $vlnum, $self->ncol); 894 | }else{ 895 | $self->make_selection($event, $vlnum, 0, $lnum, $self->ncol); 896 | } 897 | }elsif ($self->{mode} eq "visual_block"){ 898 | my $vlnum = $self->{visual_lnum}; 899 | my $vcnum = $self->{visual_cnum}; 900 | 901 | my ($sl, $el) = $lnum < $vlnum ? ($lnum, $vlnum) : ($vlnum, $lnum); 902 | my ($sc, $ec) = $cnum < $vcnum ? ($cnum, $vcnum + 1) : ($vcnum, $cnum + 1); 903 | 904 | $self->make_selection($event, $sl, $sc, $el, $ec, 1); 905 | } 906 | $self->view_start(List::Util::min 0, $lnum - ($self->nrow >> 1)); 907 | $self->want_refresh; 908 | } 909 | 910 | sub make_selection { 911 | my ($self, $event, $br, $bc, $er, $ec, $block) = @_; 912 | $self->selection_beg($br, $bc); 913 | $self->selection_end($er, $ec); 914 | $self->selection_make($event->{time}, $block); 915 | } 916 | 917 | sub clear_selection { 918 | my ($self, $event) = @_; 919 | $self->make_selection($event, -1, -1, -1, -1); 920 | } 921 | 922 | sub set_mark { 923 | my ($self, $mark) = @_; 924 | my ($lnum, $cnum) = $self->screen_cur(); 925 | my @loc = ($lnum, $cnum); 926 | $self->{marks}{$mark} = \@loc; 927 | } 928 | 929 | sub term_write { 930 | my ($self, $event, $string) = @_; 931 | 932 | # detect shell (mysql, postgres, python, etc) and use proper line 933 | # continuation 934 | my $shell = $self->line($self->{orig_lnum})->t; 935 | my $shell_continuation_start = "> "; 936 | my $shell_continuation_end = " \\"; 937 | if ($shell =~ /^(>>>|...) /){ # python 938 | $shell_continuation_start = ">>> "; 939 | $shell_continuation_end = ""; 940 | }elsif ($shell =~ /^(mysql>|\s*->) /){ # mysql 941 | $shell_continuation_start = " -> "; 942 | $shell_continuation_end = ""; 943 | }elsif ($shell =~ /^\w+(=|-)(#|\>) /){ # postgres 944 | ($shell_continuation_start = $shell) =~ s/^(\w+)(=|-)(#|\>).*/$1-$3 /; 945 | $shell_continuation_end = ""; 946 | }elsif ($shell =~ /^irb.*?(>|\*) /){ # irb 947 | (my $prefix = $shell) =~ s/^(irb.*?:).*/$1/; 948 | (my $suffix = $shell) =~ s/^irb.*?:[0-9]+(:.*?)(>|\*).*/$1/; 949 | (my $irbnum = $shell) =~ s/^irb.*?:([0-9]+):.*/$1/; 950 | $irbnum = sprintf('%03d', int($irbnum) + 1); 951 | $shell_continuation_start = "$prefix$irbnum$suffix> "; 952 | $shell_continuation_end = ""; 953 | }elsif ($shell =~ /^(\?|>)> /){ # irb --simple-prompt 954 | $shell_continuation_start = ">> "; 955 | $shell_continuation_end = ""; 956 | } 957 | 958 | $string =~ s/([^\\])\n/$1 $shell_continuation_end\n/g; 959 | push(@{$self->{term_buffer}}, $string); 960 | 961 | my ($undo_slnum, $undo_scnum) = ($self->{out_lnum}, $self->{out_cnum}); 962 | if ($string =~ /\n/g) { 963 | my @lines = split(/\n/, $string); 964 | foreach(@lines){ 965 | $self->term_write_line($event, $_); 966 | $self->scr_add_lines("\n"); 967 | $self->move_cursor($event, $self->{out_lnum} + 1, 0); 968 | $self->scr_add_lines($shell_continuation_start); 969 | $self->{out_lnum} += 1; 970 | $self->{out_cnum} = length($shell_continuation_start); 971 | } 972 | }else{ 973 | $self->term_write_line($event, $string); 974 | } 975 | push(@{$self->{term_undo}}, 976 | [$undo_slnum, $undo_scnum, $self->{out_lnum}, $self->{out_cnum}]); 977 | } 978 | 979 | # should only be called by term_write. 980 | sub term_write_line { 981 | my ($self, $event, $string) = @_; 982 | 983 | my $data = $self->locale_encode($string); 984 | my $newline = ""; 985 | $self->move_cursor($event, $self->{out_lnum}, $self->{out_cnum}); 986 | $self->{out_cnum} += length($data); 987 | 988 | # account for edge case where scr_add_lines screws up on the next write if 989 | # the current data is up to the last column. 990 | if ($self->{out_cnum} == $self->ncol){ 991 | $newline = "\n"; 992 | } 993 | 994 | # adjust line and column where next write will start. 995 | while ($self->{out_cnum} >= $self->ncol){ 996 | $self->{out_lnum} += 1; 997 | $self->{out_cnum} -= $self->ncol; 998 | } 999 | 1000 | $self->scr_add_lines($string); 1001 | 1002 | # continuation of scr_add_lines edge case. 1003 | if ($newline){ 1004 | $self->scr_add_lines($newline); 1005 | $self->move_cursor($event, $self->{out_lnum}, $self->{out_cnum}); 1006 | } 1007 | 1008 | # hack to keep last line and cursor visible. 1009 | if ($self->{out_lnum} == $self->nrow - 1){ 1010 | $self->scr_add_lines("\n\n"); 1011 | $self->{orig_lnum} -= 2; 1012 | $self->{out_lnum} -= 2; 1013 | $self->screen_cur($self->{out_lnum}, $self->{out_cnum}); 1014 | $self->view_start(List::Util::min 0, $self->{out_lnum} - ($self->nrow >> 1)); 1015 | } 1016 | 1017 | $self->want_refresh; 1018 | } 1019 | 1020 | # should only be called by leave. 1021 | sub term_write_flush () { 1022 | my ($self) = @_; 1023 | my $data = $self->locale_encode(join('', @{$self->{term_buffer}})); 1024 | my @lines = split(/\n/, $data); 1025 | my $length = scalar(@lines); 1026 | foreach(@lines){ 1027 | $self->tt_write($_); 1028 | if ($length > 1){ 1029 | $self->tt_write("\n"); 1030 | } 1031 | } 1032 | } 1033 | 1034 | sub msg { 1035 | my ($self, $msg) = @_; 1036 | my $mode_info = $self->{mode_info}; 1037 | 1038 | $self->{msg} = $msg; 1039 | $msg = $self->special_encode("$mode_info" . $msg); 1040 | 1041 | my $start = $self->view_start(); 1042 | my $end = $start - $self->nrow; 1043 | my $label = ''; 1044 | if ($start == $self->top_row){ 1045 | $label = 'Top'; 1046 | }elsif ($start == 0){ 1047 | $label = 'Bot'; 1048 | }else{ 1049 | $label = sprintf("%i%%", (abs($start) / abs($self->top_row)) * 100); 1050 | } 1051 | my $percentage = sprintf(" %3s", $label); 1052 | 1053 | my $pad = $self->ncol - length($msg) - 3; 1054 | $msg = sprintf("%s %${pad}s", $msg, $self->{key_buffer} . $percentage); 1055 | 1056 | $self->{overlay} = $self->overlay(0, -1, $self->ncol, 1, $self->{msg_style}, 0); 1057 | $self->{overlay}->set(0, 0, $msg); 1058 | } 1059 | 1060 | # copied from matcher 1061 | sub command_for { 1062 | my ($self, $row, $col) = @_; 1063 | my $line = $self->line ($row); 1064 | my $text = $line->t; 1065 | 1066 | for my $matcher (@{$self->{matchers}}) { 1067 | my $launcher = $matcher->[1] || $self->{launcher}; 1068 | while (($text =~ /$matcher->[0]/g)) { 1069 | my $match = $&; 1070 | my @begin = @-; 1071 | my @end = @+; 1072 | if (!defined($col) || ($-[0] <= $col && $+[0] >= $col)) { 1073 | if ($launcher !~ /\$/) { 1074 | return ($launcher, $match); 1075 | } else { 1076 | # It'd be nice to just access a list like ($&,$1,$2...), 1077 | # but alas, m//g behaves differently in list context. 1078 | my @exec = map { s/\$(\d+)|\$\{(\d+)\}/ 1079 | substr($text,$begin[$1||$2],$end[$1||$2]-$begin[$1||$2]) 1080 | /egx; $_ } split(/\s+/, $launcher); 1081 | return @exec; 1082 | } 1083 | } 1084 | } 1085 | } 1086 | 1087 | () 1088 | } 1089 | 1090 | # copied from matcher 1091 | sub my_resource { 1092 | my $self = shift; 1093 | $self->x_resource ("$self->{name}.$_[0]"); 1094 | } 1095 | 1096 | sub debug { 1097 | my ($self, $msg) = @_; 1098 | print $DEBUG_FH "$msg\n"; 1099 | } 1100 | 1101 | #################################### 1102 | # Vim like pasting to terminal. 1103 | #################################### 1104 | 1105 | # entry point for paste mode 1106 | sub paste { 1107 | my ($self, $selection) = @_; 1108 | $self->enable(key_press => \&key_press_paste); 1109 | } 1110 | 1111 | sub key_press_paste { 1112 | my ($self, $event, $keysym, $string) = @_; 1113 | 1114 | if ($string){ 1115 | my $selection = ''; 1116 | if ($string eq "*"){ 1117 | $selection = 'primary'; 1118 | }elsif ($string eq "+"){ 1119 | $selection = 'clipboard'; 1120 | } 1121 | 1122 | if ($selection){ 1123 | # avoid xclip hang when using primary with an active selection in the 1124 | # current terminal. 1125 | my $text = $self->selection(); 1126 | if($selection eq "primary" && $text){ 1127 | $self->tt_write($text); 1128 | }else{ 1129 | if(open(my $process, "xclip -o -selection $selection |")){ 1130 | while ($text = <$process>){ 1131 | # between rxvt-unicode 9.14 and 9.15 (or some other factor) 1132 | # writing out new lines doesn't work (they seem to be 1133 | # stripped), but using carriage returns does, so replace \n 1134 | # with \r so pasting works with both versions (removing of \n 1135 | # mostly to accommodate paste into vim since the term won't 1136 | # issue both \n and \r, but vim will, resulting in extra lines 1137 | # between pasted text... may affect other terminal apps as 1138 | # well). 1139 | $text =~ s/\n$/\r/; 1140 | $self->tt_write($text); 1141 | } 1142 | #$text = join("\r", <$process>); 1143 | #$self->tt_write($text); 1144 | close $process; 1145 | } 1146 | } 1147 | } 1148 | $self->disable("key_press"); 1149 | 1 1150 | } 1151 | } 1152 | 1153 | # vim:shiftwidth=3:tabstop=3 1154 | --------------------------------------------------------------------------------