X-Git-Url: https://ruderich.org/simon/gitweb/?p=fcscs%2Ffcscs.git;a=blobdiff_plain;f=bin%2Ffcscs;h=42b63db010c192edd6baa14187f14fc94a485742;hp=922d755330ae5d5cbbd56a8d10550ce76e849e3d;hb=HEAD;hpb=be663aa5b8f29667cc166c73a0d98f04a7a14191 diff --git a/bin/fcscs b/bin/fcscs index 922d755..42b63db 100755 --- a/bin/fcscs +++ b/bin/fcscs @@ -2,7 +2,7 @@ # fcscs - fast curses screen content select -# Copyright (C) 2013-2016 Simon Ruderich +# Copyright (C) 2013-2017 Simon Ruderich # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -44,9 +44,9 @@ B [I] I B is a small tool which allows quick selection of terminal screen contents (like URLs, paths, regex matches, etc.) and passes the selection to -GNU Screen's or Tmux's buffer or any other program. The selection can then -quickly be pasted, e.g. in the shell. Requires GNU Screen or Tmux. It's -licensed under the GPL 3 or later. +GNU Screen's or Tmux's paste buffer or any other program. The selection can +then quickly be pasted, e.g. in the shell. Requires GNU Screen or Tmux to +capture the terminal content. It's licensed under the GPL 3 or later. =head1 OPTIONS @@ -62,7 +62,9 @@ Short overview of the general usage, details below: - ... - select mode (optional, URL mode is used on startup): - f: file paths + - i: IPs - u: URLs + - c: checksums (e.g. MD5, SHA1, ..) - ... - /: search mode - for `normal' modes: @@ -109,7 +111,9 @@ explained below. Per default URLs are selected, see options for a way to change this. I: Opening URLs in the browser passes the URL via the command line which -leaks URLs to other users on the current system via C or C. +might leak URLs to other users on the current system via C or C +(mount C with C on Linux to prevent this information +leakage). I: When yanking (copying) a temporary file is used to pass the data to GNU screen/Tmux without exposing it to C or C. However this may @@ -321,6 +325,39 @@ package Screen { return; } + + sub prompt { + my ($self, %settings) = @_; + + foreach (keys %settings) { + die if not exists $self->{prompt}{$_}; + $self->{prompt}{$_} = $settings{$_}; + } + return; + } + + + sub debug { + my ($self, $module, @args) = @_; + + return if not $self->{debug}; + + state $fh; # only open the file once per run + if (not defined $fh) { + # Ignore errors if the directory doesn't exist. + if (not open $fh, '>', "$ENV{HOME}/.config/fcscs/log") { + $fh = undef; # a failed open still writes a value to $fh + return; + } + $fh->autoflush(1); + } + + foreach (@args) { + $_ = $self->encode($_); + } + say $fh "$module: @args" or die $!; + return; + } sub die { my ($self, @args) = @_; @@ -353,37 +390,6 @@ package Screen { $self->deinit; exit 1; } - sub debug { - my ($self, $module, @args) = @_; - - return if not $self->{debug}; - - state $fh; # only open the file once per run - if (not defined $fh) { - # Ignore errors if the directory doesn't exist. - if (not open $fh, '>', "$ENV{HOME}/.config/fcscs/log") { - $fh = undef; # a failed open still writes a value to $fh - return; - } - } - - foreach (@args) { - $_ = $self->encode($_); - } - say $fh "$module: @args"; - return; - } - - - sub prompt { - my ($self, %settings) = @_; - - foreach (keys %settings) { - CORE::die if not exists $self->{prompt}{$_}; - $self->{prompt}{$_} = $settings{$_}; - } - return; - } # Wrapper for Curses. sub width { return $Curses::COLS; } @@ -445,7 +451,7 @@ sub get_regex_matches { sub run_command { - my ($screen, $config, $cmd) = @_; + my ($screen, $cmd) = @_; $screen->debug('run_command', "running @{$cmd}"); @@ -502,9 +508,11 @@ sub run_in_background { # Necessary for GNU screen or it'll keep the window open until an # external command has run. - close STDIN or die $!; - close STDOUT or die $!; - close STDERR or die $!; + require File::Spec; # load here to speedup startup + my $devnull = File::Spec->devnull(); + open STDIN, '<', $devnull or die $!; + open STDOUT, '>', $devnull or die $!; + open STDERR, '>', $devnull or die $!; # Double-fork to prevent zombies. my $pid = fork; @@ -554,9 +562,6 @@ sub select_match { $number = int($number / 10); } elsif ($char eq "\n" or $char eq $config->{setting}{alternative_return}) { - if ($number == 0) { # number without selection matches last entry - $number = 1; - } last; # Selecting a new mode requires falling through into the main input @@ -566,7 +571,8 @@ sub select_match { return { key => $char }; # All other mappings stay in the current mode. } elsif (defined (my $m = $config->{mapping}{simple}{$char})) { - $m->($char, $screen, $config, $input); + my $result = $m->($char, $screen, $config, $input); + last if defined $result->{select_match}; next; } else { @@ -581,8 +587,12 @@ sub select_match { $screen->draw_matches($config, $matches, \@remaining); $screen->refresh; } + # Number without selection matches last entry. + if ($number == 0) { + $number = 1; + } - $screen->draw_matches($config, $matches, []); # remove matches + $screen->draw_matches($config, $matches, []); # clear matches foreach (@{$matches}) { return { match => $_ } if $_->{id} == $number; @@ -656,7 +666,7 @@ sub extend_match { } elsif ($char eq '$') { # select to end of line extend_match_regex_right($line, $match, qr/.+/); - # Allow mode changes if not overwritten by local mappings. + # Allow mode changes if not overwritten by above mappings. } elsif (defined $config->{mapping}{mode}{$char}) { $screen->draw_matches($config, [$match_old], []); # clear match return { key => $char }; @@ -675,31 +685,43 @@ sub extend_match { } -sub mapping_paste { - my ($key, $screen, $config, $input) = @_; +sub mapping_state_helper { + my ($name, $flags, $key, $screen, $config, $input) = @_; - $screen->debug('mapping_paste', 'started'); + $screen->debug("mapping_$name", 'started'); - $config->{state}{handler} = $config->{handler}{paste}; + $config->{state}{handler} = $config->{handler}{$name}; - $screen->prompt(flags => 'P'); # paste + $screen->prompt(flags => $flags); $screen->draw_prompt($config); $screen->refresh; return {}; } -sub mapping_yank { - my ($key, $screen, $config, $input) = @_; +sub mapping_state_now_helper { + my ($name, $key, $screen, $config, $input) = @_; - $screen->debug('mapping_yank', 'started'); + $screen->debug("mapping_${name}_now", 'started'); - $config->{state}{handler} = $config->{handler}{yank}; + $config->{state}{handler} = $config->{handler}{$name}; - $screen->prompt(flags => 'Y'); # yank - $screen->draw_prompt($config); - $screen->refresh; + return { + select_match => 1, + }; +} - return {}; +sub mapping_paste { + return mapping_state_helper('paste', 'P', @_); +} +sub mapping_paste_now { + return mapping_state_now_helper('paste', @_); +} + +sub mapping_yank { + return mapping_state_helper('yank', 'Y', @_); +} +sub mapping_yank_now { + return mapping_state_now_helper('yank', @_); } @@ -712,37 +734,51 @@ The following normal modes are available: =over 4 -=item B select relative/absolute paths +=item B select relative/absolute paths + +=item B select URLs + +=item B select IPv4 and IPv6 addresses -=item B select URLs +=item B select checksums (MD5, SHA1, SHA256, SHA512) =back =cut -sub mapping_mode_path { - my ($key, $screen, $config, $input) = @_; +sub mapping_mode_helper { + my ($name, $select, $key, $screen, $config, $input) = @_; - $screen->debug('mapping_mode_path', 'started'); + $screen->debug("mapping_mode_$name", 'started'); - my @matches = get_regex_matches($input, $config->{regex}{path}); + my @matches = get_regex_matches($input, $config->{regex}{$name}); return { - select => 'path select', + select => $select, matches => \@matches, - handler => $config->{handler}{yank}, + handler => $config->{handler}{$name}, }; } +sub mapping_mode_path { + return mapping_mode_helper('path', 'path select', @_); +} sub mapping_mode_url { + return mapping_mode_helper('url', 'url select', @_); +} +sub mapping_mode_ip { my ($key, $screen, $config, $input) = @_; - $screen->debug('mapping_mode_url', 'started'); + $screen->debug('mapping_mode_ip', 'started'); - my @matches = get_regex_matches($input, $config->{regex}{url}); + my @ipv4 = get_regex_matches($input, $config->{regex}{ipv4}); + my @ipv6 = get_regex_matches($input, $config->{regex}{ipv6}); return { - select => 'url select', - matches => \@matches, - handler => $config->{handler}{url}, + select => 'ip select', + matches => [@ipv4, @ipv6], + handler => $config->{handler}{ip}, }; } +sub mapping_mode_checksum { + return mapping_mode_helper('checksum', 'checksum select', @_); +} =head2 SEARCH MODE (AND EXTEND MODE) @@ -856,12 +892,12 @@ sub handler_yank { $screen->debug('handler_yank', 'started'); - require File::Temp; + require File::Temp; # load here to speedup startup # Use a temporary file to prevent leaking the yanked data to other users # with the command line, e.g. ps aux or top. my ($fh, $tmp) = File::Temp::tempfile(); # dies on its own - print $fh $screen->encode($match->{value}); + print $fh $screen->encode($match->{value}) or die $!; close $fh or die $!; if ($config->{setting}{multiplexer} eq 'screen') { @@ -870,18 +906,40 @@ sub handler_yank { # GNU screen displays an annoying "Slurping X characters into buffer". # Use 'msgwait 0' as a hack to disable it. my $msgwait = $config->{setting}{screen_msgwait}; - run_command($screen, $config, ['screen', '-X', 'msgwait', 0]); - run_command($screen, $config, ['screen', '-X', 'readbuf', $tmp]); - run_command($screen, $config, ['screen', '-X', 'msgwait', $msgwait]); + run_command($screen, ['screen', '-X', 'msgwait', 0]); + run_command($screen, ['screen', '-X', 'readbuf', $tmp]); + run_command($screen, ['screen', '-X', 'msgwait', $msgwait]); } elsif ($config->{setting}{multiplexer} eq 'tmux') { $screen->debug('handler_yank', 'using tmux'); - run_command($screen, $config, ['tmux', 'load-buffer', $tmp]); + run_command($screen, ['tmux', 'load-buffer', $tmp]); } else { die 'unsupported multiplexer'; } unlink $tmp or die $!; + + if ($config->{setting}{yank_x11}) { + $screen->debug('handler_yank', 'setting X11 selection'); + + my @xsel_cmd = qw( xsel --input --primary ); + my @xclip_cmd = qw( xclip -in -selection primary ); + + my $fh; + { + # We don't care if a program doesn't exist. + no warnings; + + if (not open $fh, '|-', @xsel_cmd) { + if (not open $fh, '|-', @xclip_cmd) { + die "install xsel or xlip to yank to X11 selection\n"; + } + } + } + print $fh $match->{value} or die $!; + close $fh or die $!; + } + return; } sub handler_paste { @@ -889,7 +947,7 @@ sub handler_paste { $screen->debug('handler_paste', 'started'); - require Time::HiRes; + require Time::HiRes; # load here to speedup startup my @cmd; if ($config->{setting}{multiplexer} eq 'screen') { @@ -910,7 +968,7 @@ sub handler_paste { # Sleep until we switch back to the current window. Time::HiRes::usleep($config->{setting}{paste_sleep}); - run_command($screen, $config, \@cmd); + run_command($screen, \@cmd); }); return; } @@ -921,7 +979,7 @@ sub handler_url { run_in_background($screen, sub { my @cmd = ( @{$config->{setting}{browser}}, $match->{value} ); - run_command($screen, $config, \@cmd); + run_command($screen, \@cmd); }); return; } @@ -932,8 +990,9 @@ sub handler_url { =head1 CONFIGURATION -fcscs is configured through F<~/.fcscsrc> or F<~/.config/fcscs/fcscsrc> which -is a normal Perl script with all of Perl's usual features. +fcscs is configured through F<~/.fcscsrc> (preferred) or +F<~/.config/fcscs/fcscsrc> which is a normal Perl script with all of Perl's +usual features (only loaded if not writable by others). All configuration values are stored in the hash C<%config>. All manually defined keys overwrite the default settings. @@ -950,15 +1009,18 @@ settings see below): # Draw matches in blue. $config{attribute}{match_string} = color_pair(COLOR_BLUE, -1); - # Enable Vim-like 'smartcase', ignore case until an upper character is - # searched. - $config{setting}{smartcase} = 1; + # Draw numbers in bold yellow. + $config{attribute}{match_id} = color_pair(COLOR_YELLOW, -1) + | A_BOLD; + # Disable Vim-like 'smartcase' (ignore case until an upper character is + # searched) which is enabled by default. + $config{setting}{smartcase} = 0; # Use chromium to open URLs if running under X, elinks otherwise. if (defined $ENV{DISPLAY}) { $config{setting}{browser} = ['chromium']; } else { - $config{setting}{browser} = ['elinks']; + $config{setting}{browser} = ['elinks', '-remote']; } # Let fcscs know the file was loaded successfully. @@ -995,7 +1057,7 @@ local $SIG{__WARN__} = sub { I: Mappings are split in two categories: Mode mappings which change the selection and may receive additional input (e.g. a search string) and simple -mappings which only change some value. Mode mappings are configured via +mappings which only change some config value. Mode mappings are configured via C<$config{mapping}{mode}>, simple mappings via C<$config{mapping}{simple}>. The following mode mappings are available by default (the function to remap @@ -1007,6 +1069,10 @@ them in parentheses): =item B select URLs (C<\&mapping_mode_url>) +=item B select IPv4 and IPv6 addresses (C<\&mapping_mode_ip>) + +=item B select checksums (e.g. MD5, SHA) (C<\&mapping_mode_checksum>) + =item B search for regex to get selection (C<\&mapping_mode_search>) =item B quit fcscs (C<\&mapping_quit>) @@ -1019,10 +1085,17 @@ The following simple mappings are available by default: =item B

enable pasting (C<\&mapping_paste>) +=item B

paste current selection (like C<\n> but paste) (C<\&mapping_paste_now>) + =item B enable yanking (copying) (C<\&mapping_yank>) +=item B yank current selection (like C<\n> but yank) (C<\&mapping_yank_now>) + =back +Note that yanking only uses the GNU screen or Tmux paste buffer by default. To +also copy to X11 selection, enable the B option. + The following additional mappings are available by default: =over @@ -1052,12 +1125,16 @@ Example: my %mapping_mode = ( f => \&mapping_mode_path, u => \&mapping_mode_url, + i => \&mapping_mode_ip, + c => \&mapping_mode_checksum, '/' => \&mapping_mode_search, q => \&mapping_quit, ); my %mapping_simple = ( p => \&mapping_paste, + P => \&mapping_paste_now, y => \&mapping_yank, + Y => \&mapping_yank_now, ); =head2 ATTRIBUTES @@ -1107,15 +1184,17 @@ Defaults in parentheses. =item B start in this mode, must be a valid mode mapping (C<\&mapping_mode_url>) -=item B set multiplexer ("screen" or "tmux") if not autodetected (C) +=item B set multiplexer ("screen" or "tmux"), defaults to autodetection (C) =item B ignore case when searching (C<0>) =item B ignore case unless one uppercase character is searched (C<1>) +=item B copy selection also to X11 primary selection when yanking (C<0>) + =item B sleep x us before running paste command (C<100_000>) -=item B GNU Screen's msgwait variable, used when yanking (C<5>) +=item B GNU Screen's msgwait variable, overwritten with this value when yanking (C<5>) =item B additional accept key like return, set to C<\n> to disable (C) @@ -1136,6 +1215,7 @@ my %setting = ( multiplexer => undef, ignorecase => 0, smartcase => 1, + yank_x11 => 0, paste_sleep => 100_000, screen_msgwait => 5, # global mappings @@ -1152,6 +1232,10 @@ my %setting = ( =item B used by C<\&mapping_mode_path> +=item B used by C<\&mapping_mode_ip> + +=item B used by C<\&mapping_mode_ip> + =back Example: @@ -1164,6 +1248,12 @@ my %regex = ( # Taken from urlview's default configuration file, thanks. url => qr{((?:(?:(?:http|https|ftp|gopher)|mailto):(?://)?[^ <>"\t]*|(?:www|ftp)[0-9]?\.[-a-z0-9.]+)[^ .,;\t\n\r<">\):]?[^, <>"\t]*[^ .,;\t\n\r<">\):])}, path => qr{(~?[a-zA-Z0-9_./-]*/[a-zA-Z0-9_./-]+)}, + # IP addresses with optional prefix. Not perfectly accurate but good + # enough. + ipv4 => qr!\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?:/\d{1,2})?)\b!, + ipv6 => qr!\b((?:[0-9a-fA-F]{1,4})?(?::+[0-9a-fA-F]{1,4})+(?:/\d{1,3})?)\b!, + # MD5, SHA1, SHA256, SHA512 + checksum => qr!\b([0-9a-fA-F]{32}|[0-9a-fA-F]{40}|[0-9a-fA-F]{64}|[0-9a-fA-F]{128})\b!, ); =head2 HANDLERS @@ -1174,11 +1264,17 @@ The following handlers are available, defaults in parentheses. =over -=item B used to yank (copy) selection to paste buffer (C<\&handler_yank>) +=item B used to yank (copy) selection to paste buffer (C<\&handler_yank>) + +=item B used to paste selection into window (C<\&handler_paste>) -=item B used to paste selection into window (C<\&handler_paste>) +=item B used to handle paths (C<\&handler_yank>) -=item B used to open URLs (e.g. in a browser) (C<\&handler_url>) +=item B used to open URLs (e.g. in a browser) (C<\&handler_url>) + +=item B used to handle IPs (C<\&handler_yank>) + +=item B used to handle checksums (C<\&handler_yank>) =back @@ -1191,8 +1287,7 @@ Example: if ($match->{value} =~ m{^https://www.youtube.com/}) { return run_in_background($screen, sub { - run_command($screen, $config, - ['youtube-dl-wrapper', $match->{value}]); + run_command($screen, ['youtube-dl-wrapper', $match->{value}]); }); } handler_url(@_); @@ -1200,9 +1295,12 @@ Example: =cut my %handler = ( - yank => \&handler_yank, - paste => \&handler_paste, - url => \&handler_url, + yank => \&handler_yank, + paste => \&handler_paste, + path => \&handler_yank, + url => \&handler_url, + ip => \&handler_yank, + checksum => \&handler_yank, ); my %state = ( @@ -1224,10 +1322,14 @@ Create a new Curses attribute with the given fore- and background color. mapping_mode_path() mapping_mode_url() + mapping_mode_ip() + mapping_mode_checksum() mapping_mode_search() mapping_paste() + mapping_paste_now() mapping_yank() + mapping_yank_now() mapping_quit() Used as mappings, see L above. @@ -1238,17 +1340,55 @@ Used as mappings, see L above. Used as handler to yank, paste selection or open URL in browser. - debug() get_regex_matches() select_match() run_command() run_in_background() -Helper functions when writing custom mappings, see the source for details. +Helper functions when writing custom mappings, see the source and example for +details. Example: - TODO + # Enhance URL mode by updating the mapping. + $config{mapping}{mode}{u} = sub { + my ($key, $screen, $config, $input) = @_; + + # First get matches of normal URL mode. + my $result = mapping_mode_url(@_); + + # Add all strings matching "CVE-1234-1234" with URLs pointing to the + # Debian security tracker. "->{value}" is the string which is used as + # result of the match (e.g. the URL in this case). + my @matches = get_regex_matches($input, qr/\b(CVE-\d+-\d+)\b/); + foreach (@matches) { + $_->{value} = "https://security-tracker.debian.org/$_->{string}"; + } + push @{$result->{matches}}, @matches; + + # Change all YouTube links to use the custom "youtube" handler (see + # below). This will allow us to automatically open YouTube URLs with a + # custom program, like `youtube-dl` or `mpv`. + foreach (@{$result->{matches}}) { + if ($_->{string} =~ m{^https://www.youtube.com/}) { + $_->{handler} = $config{handler}{youtube}; + } + } + + return $result; + }; + # Also update initial mode to use our new "URL mode". + $config{setting}{initial_mode} = $config{mapping}{mode}{u}; + + # Special handler to download YouTube URLs with `youtube-dl`. You could + # also use `mpv` here to immediately play them. + $config{handler}{youtube} = sub { + my ($screen, $config, $match) = @_; + + return run_in_background($screen, sub { + run_command($screen, ['youtube-dl', $match->{value}]); + }); + }; =cut @@ -1261,18 +1401,20 @@ package Fcscs { sub mapping_mode_path { return main::mapping_mode_path(@_); } sub mapping_mode_url { return main::mapping_mode_url(@_); } + sub mapping_mode_ip { return main::mapping_mode_ip(@_); } + sub mapping_mode_checksum { return main::mapping_mode_checksum(@_); } sub mapping_mode_search { return main::mapping_mode_search(@_); } sub mapping_paste { return main::mapping_paste(@_); } + sub mapping_paste_now { return main::mapping_paste_now(@_); } sub mapping_yank { return main::mapping_yank(@_); } + sub mapping_yank_now { return main::mapping_yank_now(@_); } sub mapping_quit { return main::mapping_quit(@_); } sub handler_yank { return main::handler_yank(@_); } sub handler_paste { return main::handler_paste(@_); } sub handler_url { return main::handler_url(@_); } - sub debug { return main::debug(@_); } - sub get_regex_matches { return main::get_regex_matches(@_); } sub select_match { return main::select_match(@_); } @@ -1312,10 +1454,10 @@ package Fcscs { } # Make sure the file is not writable by other users. Doesn't handle # ACLs and see comment above about race conditions. - my @stat = stat $path or die $!; + my @stat = stat $path or $screen->die("Config '$decoded': $!"); my $mode = $stat[2]; if (($mode & Fcntl::S_IWGRP) or ($mode & Fcntl::S_IWOTH)) { - die "Config '$decoded' must not be writable by other users."; + $screen->die("Config '$decoded' must not be writable by other users."); } my $result = do $path; @@ -1477,7 +1619,7 @@ Simon Ruderich Esimon@ruderich.orgE =head1 LICENSE AND COPYRIGHT -Copyright (C) 2013-2016 by Simon Ruderich +Copyright (C) 2013-2017 by Simon Ruderich This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by