]> ruderich.org/simon Gitweb - fcscs/fcscs.git/blobdiff - bin/fcscs
use helper function to reduce duplication in mappings
[fcscs/fcscs.git] / bin / fcscs
index a2a95b7d567e15b90d9f404b7abc02b506921027..e0c8cb1a05d42eb6e38a678311972626c6b1c086 100755 (executable)
--- a/bin/fcscs
+++ b/bin/fcscs
@@ -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:
@@ -365,12 +367,13 @@ package Screen {
                 $fh = undef; # a failed open still writes a value to $fh
                 return;
             }
+            $fh->autoflush(1);
         }
 
         foreach (@args) {
             $_ = $self->encode($_);
         }
-        say $fh "$module: @args";
+        say $fh "$module: @args" or CORE::die $!;
         return;
     }
 
@@ -445,7 +448,7 @@ sub get_regex_matches {
 
 
 sub run_command {
-    my ($screen, $config, $cmd) = @_;
+    my ($screen, $cmd) = @_;
 
     $screen->debug('run_command', "running @{$cmd}");
 
@@ -502,9 +505,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;
+        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 +559,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 +568,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,6 +584,10 @@ 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
 
@@ -675,31 +682,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 +731,51 @@ The following normal modes are available:
 
 =over 4
 
-=item B<path mode> select relative/absolute paths
+=item B<path mode>     select relative/absolute paths
+
+=item B<url mode>      select URLs
+
+=item B<ip mode>       select IPv4 and IPv6 addresses
 
-=item B<url mode>  select URLs
+=item B<checksum mode> 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)
 
@@ -830,6 +863,11 @@ sub mapping_mode_search {
 
     $screen->cursor(0);
 
+    $screen->prompt(name => undef, value => undef); # clear prompt
+    $screen->draw_prompt($config);
+
+    $screen->debug('mapping_mode_search', 'done');
+
     return {
         select  => 'search',
         matches => \@last_matches,
@@ -856,7 +894,7 @@ sub handler_yank {
     # 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') {
@@ -865,18 +903,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 {
@@ -905,7 +965,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;
 }
@@ -916,7 +976,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;
 }
@@ -945,15 +1005,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
+    # 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.
-    $config{setting}{smartcase} = 1;
+    $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.
@@ -990,7 +1053,7 @@ local $SIG{__WARN__} = sub {
 
 I<NOTE>: 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
@@ -1002,6 +1065,10 @@ them in parentheses):
 
 =item B<u> select URLs (C<\&mapping_mode_url>)
 
+=item B<i> select IPv4 and IPv6 addresses (C<\&mapping_mode_ip>)
+
+=item B<c> select checksums (e.g. MD5, SHA) (C<\&mapping_mode_checksum>)
+
 =item B</> search for regex to get selection (C<\&mapping_mode_search>)
 
 =item B<q> quit fcscs (C<\&mapping_quit>)
@@ -1014,10 +1081,17 @@ The following simple mappings are available by default:
 
 =item B<p> enable pasting (C<\&mapping_paste>)
 
+=item B<P> paste current selection (like C<\n> but paste) (C<\&mapping_paste_now>)
+
 =item B<y> enable yanking (copying) (C<\&mapping_yank>)
 
+=item B<Y> 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<yank_x11> option.
+
 The following additional mappings are available by default:
 
 =over
@@ -1047,12 +1121,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
@@ -1098,23 +1176,25 @@ Defaults in parentheses.
 
 =over
 
-=item B<debug>          enable debug mode, writes to I<~/.config/fcscs/log> (C<0>)
+=item B<debug>              enable debug mode, writes to I<~/.config/fcscs/log> (C<0>)
+
+=item B<initial_mode>       start in this mode, must be a valid mode mapping (C<\&mapping_mode_url>)
 
-=item B<initial_mode>   start in this mode, must be a valid mode mapping (C<\&mapping_mode_url>)
+=item B<multiplexer>        set multiplexer ("screen" or "tmux"), defaults to autodetection (C<undef>)
 
-=item B<multiplexer>    set multiplexer ("screen" or "tmux") if not autodetected (C<undef>)
+=item B<ignorecase>         ignore case when searching (C<0>)
 
-=item B<ignorecase>     ignore case when searching (C<0>)
+=item B<smartcase>          ignore case unless one uppercase character is searched (C<1>)
 
-=item B<smartcase>      ignore case unless one uppercase character is searched (C<1>)
+=item B<yank_x11>           copy selection also to X11 primary selection when yanking (C<0>)
 
-=item B<paste_sleep>    sleep x us before running paste command (C<100_000>)
+=item B<paste_sleep>        sleep x us before running paste command (C<100_000>)
 
-=item B<screen_msgwait> GNU Screen's msgwait variable, used when yanking (C<5>)
+=item B<screen_msgwait>     GNU Screen's msgwait variable, used when yanking (C<5>)
 
 =item B<alternative_return> additional accept key like return, set to C<\n> to disable (C<s>)
 
-=item B<browser>        browser command as array reference (C<['x-www-browser']>)
+=item B<browser>            browser command as array reference (C<['x-www-browser']>)
 
 =back
 
@@ -1126,26 +1206,31 @@ Example:
 =cut
 my %setting = (
     # options
-    debug          => 0,
-    initial_mode   => \&mapping_mode_url,
-    multiplexer    => undef,
-    ignorecase     => 0,
-    smartcase      => 1,
-    paste_sleep    => 100_000,
-    screen_msgwait => 5,
+    debug              => 0,
+    initial_mode       => \&mapping_mode_url,
+    multiplexer        => undef,
+    ignorecase         => 0,
+    smartcase          => 1,
+    yank_x11           => 0,
+    paste_sleep        => 100_000,
+    screen_msgwait     => 5,
     # global mappings
     alternative_return => 's',
     # commands
-    browser        => ['x-www-browser'],
+    browser            => ['x-www-browser'],
 );
 
 =head2 REGEXPS
 
 =over
 
-=item B<url>  used by C<\&mapping_mode_url()>
+=item B<url>  used by C<\&mapping_mode_url>
+
+=item B<path> used by C<\&mapping_mode_path>
+
+=item B<ipv4> used by C<\&mapping_mode_ip>
 
-=item B<path> used by C<\&mapping_mode_path()>
+=item B<ipv6> used by C<\&mapping_mode_ip>
 
 =back
 
@@ -1159,6 +1244,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
@@ -1169,11 +1260,17 @@ The following handlers are available, defaults in parentheses.
 
 =over
 
-=item B<yank>  used to yank (copy) selection to paste buffer (C<\&handler_yank>)
+=item B<yank>     used to yank (copy) selection to paste buffer (C<\&handler_yank>)
 
-=item B<paste> used to paste selection into window (C<\&handler_paste>)
+=item B<paste>    used to paste selection into window (C<\&handler_paste>)
 
-=item B<url>   used to open URLs (e.g. in a browser) (C<\&handler_url>)
+=item B<path>     used to handle paths (C<\&handler_yank>)
+
+=item B<url>      used to open URLs (e.g. in a browser) (C<\&handler_url>)
+
+=item B<ip>       used to handle IPs (C<\&handler_yank>)
+
+=item B<checksum> used to handle checksums (C<\&handler_yank>)
 
 =back
 
@@ -1186,8 +1283,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(@_);
@@ -1195,9 +1291,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 = (
@@ -1219,10 +1318,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</MAPPINGS> above.
@@ -1233,17 +1336,55 @@ Used as mappings, see L</MAPPINGS> 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
 
@@ -1256,18 +1397,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(@_); }
 
@@ -1307,10 +1450,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;