- ...
- 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:
$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;
}
sub run_command {
- my ($screen, $config, $cmd) = @_;
+ my ($screen, $cmd) = @_;
$screen->debug('run_command', "running @{$cmd}");
# closes the window (because the parent process has exited).
local $SIG{HUP} = 'IGNORE';
- # Necessary for GNU screen or it'll keep the window open until the
- # paste command has run.
- close STDIN or die $!;
- close STDOUT or die $!;
- close STDERR or die $!;
+ # Necessary for GNU screen or it'll keep the window open until an
+ # external command has run.
+ 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;
$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
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 {
$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
return {};
}
+sub mapping_paste_now {
+ my ($key, $screen, $config, $input) = @_;
+
+ $screen->debug('mapping_paste_now', 'started');
+
+ $config->{state}{handler} = $config->{handler}{paste};
+
+ return {
+ select_match => 1,
+ };
+}
+
sub mapping_yank {
my ($key, $screen, $config, $input) = @_;
return {};
}
+sub mapping_yank_now {
+ my ($key, $screen, $config, $input) = @_;
+
+ $screen->debug('mapping_yank_now', 'started');
+
+ $config->{state}{handler} = $config->{handler}{yank};
+
+ return {
+ select_match => 1,
+ };
+}
=head2 NORMAL MODES
=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<url mode> select URLs
+=item B<ip mode> select IPv4 and IPv6 addresses
+
+=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)
$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,
# 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') {
# 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 {
# 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;
}
run_in_background($screen, sub {
my @cmd = ( @{$config->{setting}{browser}}, $match->{value} );
- run_command($screen, $config, \@cmd);
+ run_command($screen, \@cmd);
});
return;
}
# 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.
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
=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>)
=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
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
=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") if not autodetected (C<undef>)
+=item B<multiplexer> set multiplexer ("screen" or "tmux"), defaults to autodetection (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<paste_sleep> sleep x us before running paste command (C<100_000>)
+=item B<yank_x11> copy selection also to X11 primary selection when yanking (C<0>)
-=item B<screen_msgwait> GNU Screen's msgwait variable, used when yanking (C<5>)
+=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<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
=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<path> used by C<\&mapping_mode_path()>
+=item B<ipv4> used by C<\&mapping_mode_ip>
+
+=item B<ipv6> used by C<\&mapping_mode_ip>
=back
# 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
=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<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<paste> used to paste selection into window (C<\&handler_paste>)
+=item B<ip> used to handle IPs (C<\&handler_yank>)
-=item B<url> used to open URLs (e.g. in a browser) (C<\&handler_url>)
+=item B<checksum> used to handle checksums (C<\&handler_yank>)
=back
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(@_);
=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 = (
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.
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
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(@_); }
}
# 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;