]> ruderich.org/simon Gitweb - fcscs/fcscs.git/blob - bin/fcscs
run_in_background: fix execution when debug mode is enabled
[fcscs/fcscs.git] / bin / fcscs
1 #!/usr/bin/perl
2
3 # fcscs - fast curses screen content select
4
5 # Copyright (C) 2013-2016  Simon Ruderich
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20
21 use strict;
22 use warnings;
23
24 use v5.10; # say, state
25
26 use Encode ();
27 use Fcntl ();
28 use I18N::Langinfo ();
29
30 use Curses ();
31
32 our $VERSION = '0.01';
33
34
35 =head1 NAME
36
37 fcscs - fast curses screen content select
38
39 =head1 SYNOPSIS
40
41 B<fcscs> [I<options>] I<path/to/screen/capture/file>
42
43 =head1 DESCRIPTION
44
45 B<fcscs> is a small tool which allows quick selection of terminal screen
46 contents (like URLs, paths, regex matches, etc.) and passes the selection to
47 GNU Screen's or Tmux's buffer or any other program. The selection can then
48 quickly be pasted, e.g. in the shell. Requires GNU Screen or Tmux. It's
49 licensed under the GPL 3 or later.
50
51 =head1 OPTIONS
52
53 None so far.
54
55 =head1 USAGE
56
57 GNU Screen setup (add to F<~/.screenrc>):
58
59     bind ^B eval "hardcopy $HOME/.tmp/screen-fcscs" "screen fcscs $HOME/.tmp/screen-fcscs"
60
61 Tmux setup (add to F<~/.tmux.conf>):
62
63     bind-key C-b capture-pane \; save-buffer ~/.tmp/tmux-fcscs \; delete-buffer \; new-window "fcscs ~/.tmp/tmux-fcscs"
64
65 This requires a writable ~/.tmp directory. Adapt the mapping according to your
66 preferences. Ensure these files are not readable by others as they can contain
67 private data (umask or protected directory). B<fcscs> must be in your C<$PATH>
68 for the above mappings to work.
69
70 Pressing the configured mapping (Prefix Ctrl-B in this example) opens B<fcscs>
71 in a new GNU screen/Tmux window. After selection, the content is either passed
72 to external programs (e.g. for URLs) or copied to the paste buffer or directly
73 pasted in your previous window and the new window is closed.
74
75 To select a match just type its number. If the match is unique, the entry is
76 automatically selected (e.g. you press 2 and there are only 19 matches). If
77 there are multiple matches left (e.g. you press 1 and there are more than ten
78 matches available), press return to select the current match (1 in this case)
79 or another number to select the longer match. Use backspace to remove the last
80 entered number.
81
82 Press return before entering a number to select the last (lowest numbered)
83 match. To abort without selecting any match either use "q".
84
85 To change the selection mode (e.g. paths, files, etc.) use one of the mappings
86 explained below. Per default URLs are selected, see options for a way to
87 change this.
88
89 I<NOTE>: When yanking (copying) a temporary file is used to pass the data to
90 GNU screen/Tmux without exposing it to C<ps ux> or C<top>. However this may
91 leak data if those temporary files are written to disk. To prevent this change
92 your C<$TMP> accordingly to point to a memory-only location or encrypted
93 storage.
94
95 If no window appears, try running B<fcscs> manually to catch the error message
96 and please report the bug:
97
98     fcscs /path/to/screen-or-tmux-fcscs-file
99
100 =cut
101
102
103 # CLASSES
104
105 # Helper class for drawing on the screen using Curses.
106 package Screen {
107     sub init {
108         my ($class, $encoding) = @_;
109
110         # Prefer strict UTF-8 handling (see perldoc Encode); just in case.
111         if (lc $encoding eq 'utf8') {
112             $encoding = 'UTF-8';
113         }
114         # Get Encode object to speed up decode()/encode().
115         my $encoding_object = Encode::find_encoding($encoding);
116         die "unsupported encoding '$encoding'" unless ref $encoding_object;
117
118         my $curses = Curses->new or die $!;
119
120         my $self = {
121             encoding        => $encoding,
122             encoding_object => $encoding_object,
123             curses          => $curses,
124             debug           => 0,
125             prompt          => {
126                 flags => undef,
127                 name  => undef,
128                 value => undef,
129             },
130         };
131         bless $self, $class;
132
133         Curses::start_color;
134         # Allow default colors by passing -1 to init_pair. A default color is
135         # not drawn on screen thus allowing terminals with pseudo-transparency
136         # to use the transparent background in place of the default color.
137         Curses::use_default_colors;
138
139         Curses::cbreak;
140         Curses::noecho;
141         $self->cursor(0);
142
143         return $self;
144     }
145     sub deinit {
146         my ($self) = @_;
147
148         Curses::nocbreak;
149         Curses::echo;
150         $self->cursor(1);
151
152         Curses::endwin;
153         return;
154     }
155
156     # Convert between Perl's internal encoding and the terminal's encoding.
157     sub encode {
158         my ($self, $string) = @_;
159         return $self->{encoding_object}->encode($string);
160     }
161     sub decode {
162         my ($self, $string) = @_;
163         return eval { # returns undef on decode failure
164             $self->{encoding_object}->decode($string, Encode::FB_CROAK);
165         };
166     }
167
168     # Create attribute for the given fore-/background colors.
169     sub color_pair {
170         my ($self, $fg, $bg) = @_;
171
172         state $next_color_pair = 1; # must start at 1 for init_pair()
173
174         Curses::init_pair($next_color_pair, $fg, $bg);
175         return Curses::COLOR_PAIR($next_color_pair++);
176     }
177
178     # Draw a string which must fit in the current line. Wrapping/clipping is
179     # not supported and must be handled by the caller.
180     sub draw_simple {
181         my ($self, $y, $x, $attributes, $string) = @_;
182
183         die if $string =~ /\n/;
184         # FIXME: wide characters
185         die if $x + length $string > $self->width;
186
187         $self->{curses}->attron($attributes) if defined $attributes;
188         $self->{curses}->addstr($y, $x, $self->encode($string));
189         $self->{curses}->attroff($attributes) if defined $attributes;
190         return;
191     }
192     # Like draw_simple(), but the string is automatically clipped.
193     sub draw_clipped {
194         my ($self, $y, $x, $attributes, $string) = @_;
195
196         # FIXME: wide characters
197         $string = substr $string, 0, $self->width - $x;
198         $self->draw_simple($y, $x, $attributes, $string);
199         return;
200     }
201     sub draw {
202         my ($self, $y, $x, $attributes, $string) = @_;
203
204         die unless defined $string;
205
206         while (1) {
207             my $offset;
208             # We must print each line separately. Search for next newline or
209             # line end, whichever is closer.
210             if ($string =~ /\n/) {
211                 $offset = $-[0];
212             }
213             # FIXME: wide characters
214             if ($x + length $string > $self->width) {
215                 my $new_offset = $self->width - $x;
216                 if (not defined $offset or $offset > $new_offset) {
217                     $offset = $new_offset;
218                 }
219             }
220             last unless defined $offset;
221
222             # FIXME: wide characters
223             $self->draw_simple($y, $x, $attributes, substr $string, 0, $offset);
224
225             # Don't draw "\n" itself.
226             if ("\n" eq substr $string, $offset, 1) {
227                 $offset++;
228             }
229
230             $string = substr $string, $offset;
231
232             $y++;
233             $x = 0;
234         }
235
236         $self->draw_simple($y, $x, $attributes, $string);
237         return $y;
238     }
239
240     sub draw_prompt {
241         my ($self, $config) = @_;
242
243         my $x = 0;
244         my $y = $self->height - 1;
245
246         # Clear line for better visibility.
247         $self->draw_simple($y, $x, undef, ' ' x $self->width);
248
249         # Draw prompt flags.
250         if (defined (my $s = $self->{prompt}{flags})) {
251             $s = "[$s]";
252             $self->draw_clipped($y, $x, $config->{attribute}{prompt_flags}, $s);
253             $x += length($s) + 1; # space between next element
254         }
255         # Draw prompt name.
256         if (defined (my $s = $self->{prompt}{name})) {
257             $s = "[$s]";
258             $self->draw_clipped($y, $x, $config->{attribute}{prompt_name}, $s);
259             $x += length($s) + 1;
260         }
261         # Draw prompt value, e.g. a search field.
262         if (defined (my $s = $self->{prompt}{value})) {
263             $self->draw_clipped($y, $x, undef, $s);
264             $x += length($s) + 1;
265         }
266         return;
267     }
268
269     sub draw_matches {
270         my ($self, $config, $matches_remove, $matches_add) = @_;
271
272         foreach (@{$matches_remove}) {
273             $self->draw($_->{y}, $_->{x}, Curses::A_NORMAL, $_->{string});
274         }
275
276         my $attr_id     = $config->{attribute}{match_id};
277         my $attr_string = $config->{attribute}{match_string};
278
279         foreach (@{$matches_add}) {
280             $self->draw($_->{y}, $_->{x}, $attr_string, $_->{string});
281             if (defined $_->{id}) {
282                 $self->draw($_->{y}, $_->{x}, $attr_id, $_->{id});
283             }
284         }
285         return;
286     }
287
288     sub die {
289         my ($self, @args) = @_;
290
291         my $attr = $self->color_pair(Curses::COLOR_RED, -1) | Curses::A_BOLD;
292
293         # Clear the screen to improve visibility of the error message.
294         $self->{curses}->clear;
295
296         my $y = $self->draw(0, 0, $attr, "@args");
297
298         if ($self->{debug}) {
299             my $msg;
300             eval {
301                 require Devel::StackTrace;
302             };
303             if ($@) {
304                 $msg = "Devel::StackTrace missing, no stack trace.\n";
305             } else {
306                 my $trace = Devel::StackTrace->new;
307                 $msg = "Stack trace:\n" . $trace->as_string;
308             }
309             $y = $self->draw($y + 1, 0, Curses::A_NORMAL, $msg);
310         }
311
312         $self->draw($y + 1, 0, Curses::A_NORMAL,
313                     'Press any key to terminate fcscs.');
314         $self->refresh;
315
316         $self->getch;
317         $self->deinit;
318         exit 1;
319     }
320
321
322     sub prompt {
323         my ($self, %settings) = @_;
324
325         foreach (keys %settings) {
326             CORE::die if not exists $self->{prompt}{$_};
327             $self->{prompt}{$_} = $settings{$_};
328         }
329         return;
330     }
331
332     # Wrapper for Curses.
333     sub width   { return $Curses::COLS; }
334     sub height  { return $Curses::LINES; }
335     sub refresh { return $_[0]->{curses}->refresh; }
336     sub getch   { return $_[0]->{curses}->getch; }
337     sub cursor  { Curses::curs_set($_[1]); return; }
338 }
339
340
341
342 # FUNCTIONS
343
344 sub debug {
345     my ($config, $module, @args) = @_;
346
347     say STDERR "$module: @args" if $config->{setting}{debug};
348     return;
349 }
350
351
352 sub prepare_input {
353     my ($screen, $input_ref) = @_;
354
355     # Make sure the input fits on the screen by removing the top lines if
356     # necessary.
357     splice @{$input_ref}, 0, -$screen->height;
358
359     # Pad each line with spaces to the screen width to correctly handle
360     # multi-line regexes.
361     # FIXME: wide characters
362     my @padded = map { sprintf '%-*s', $screen->width, $_ } @{$input_ref};
363
364     my $string = join "\n", @padded;
365     return {
366         string => $string,
367         lines  => $input_ref,
368         width  => $screen->width + 1,
369                   # + 1 = "\n", used in input_match_offset_to_coordinates
370     };
371 }
372
373 sub input_match_offset_to_coordinates {
374     my ($width, $offset) = @_;
375
376     die unless defined $offset;
377
378     my $y = int($offset / $width);
379     my $x = $offset - $y * $width;
380     return ($x, $y);
381 }
382
383 sub get_regex_matches {
384     my ($input, $regex) = @_;
385
386     my @matches;
387     while ($input->{string} =~ /$regex/g) {
388         my ($x, $y) = input_match_offset_to_coordinates($input->{width},
389                                                         $-[0]);
390         push @matches, { x => $x, y => $y, string => $1 };
391     }
392     return @matches;
393 }
394
395
396 sub run_command {
397     my ($config, $cmd) = @_;
398
399     debug $config, 'run_command', "running @{$cmd}";
400
401     my $exit = do {
402         # Perl's system() combined with a $SIG{__WARN__} which die()s has
403         # issues due to the fork. The die() in the __WARN__ handler doesn't
404         # die but the program continues after the system().
405         #
406         # If the forked process fails to exec (e.g. program not found) then
407         # the __WARN__ handler is called (because a warning is about to be
408         # displayed) and the die() should display a message and terminate the
409         # process. But due to the fork it doesn't terminate the parent process
410         # and instead changes the return value of system(); it's no longer -1
411         # which makes it impossible to detect that case.
412         #
413         # Perl < 5.18 (found in 5.14) doesn't setup $$ during system() which
414         # makes it impossible to detect if the handler was called from inside
415         # the child.
416         #
417         # Instead, just ignore any warnings during the system(). Thanks to
418         # mauke in #perl on Freenode (2013-10-29 23:30 CET) for the idea to
419         # use no warnings and anno for testing a more recent Perl version with
420         # a working $$.
421         no warnings;
422
423         system { $cmd->[0] } @{$cmd};
424     };
425     if ($exit != 0) {
426         my $msg;
427         if ($? == -1) {
428             $msg = 'failed to execute: ' . $!;
429         } elsif ($? & 127) {
430             $msg = 'killed by signal ' . ($? & 127);
431         } else {
432             $msg = 'exited with code ' . ($? >> 8);
433         }
434         die "system(@{$cmd}) $msg.";
435     }
436     return;
437 }
438 sub run_in_background {
439     my ($config, $sub) = @_;
440
441     debug $config, 'run_in_background', "running $sub";
442
443     my $pid = fork;
444     defined $pid or die $!;
445
446     if ($pid == 0) {
447         # The terminal multiplexer sends a SIGHUP to the process when it
448         # closes the window (because the parent process has exited).
449         local $SIG{HUP} = 'IGNORE';
450
451         # Necessary for GNU screen or it'll keep the window open until the
452         # paste command has run.
453         close STDIN  or die $!;
454         close STDOUT or die $!;
455         close STDERR or die $!;
456
457         # Double-fork to prevent zombies.
458         my $pid = fork;
459         defined $pid or die $!;
460         if ($pid == 0) { # child
461             # Disable debug mode as writing will fail with closed STDERR.
462             $config->{setting}{debug} = 0;
463
464             $sub->();
465         }
466         exit;
467     }
468     waitpid $pid, 0 or die $!;
469     return;
470 }
471
472
473 sub select_match {
474     my ($name, $screen, $config, $input, $matches) = @_;
475
476     debug $config, 'select_match', 'started';
477
478     return if @{$matches} == 0;
479     # Don't return on initial run to give the user a chance to select another
480     # mode, e.g. to switch from URL selection to search selection.
481     if (@{$matches} == 1 and not $config->{state}->{initial}) {
482         return { match => $matches->[0] };
483     }
484     $config->{state}{initial} = 0;
485
486     my @sorted = sort { $b->{y} <=> $a->{y} or $b->{x} <=> $a->{x} } @{$matches};
487
488     my $i = 1;
489     foreach (@sorted) {
490         $_->{id} = $i++;
491     }
492
493     $screen->prompt(name => $name, value => undef);
494     $screen->draw_prompt($config);
495
496     $screen->draw_matches($config, [], $matches);
497     $screen->refresh;
498
499     my $number = 0;
500     while (1) {
501         my $char = $screen->getch;
502         if ($char =~ /^\d$/) {
503             $number = $number * 10 + $char;
504         } elsif ($char eq "\b" or $char eq "\x7f") { # backspace
505             $number = int($number / 10);
506         } elsif ($char eq "\n") {
507             if ($number == 0) { # number without selection matches last entry
508                 $number = 1;
509             }
510             last;
511
512         # Selecting a new mode requires falling through into the main input
513         # loop and then starting the new mode.
514         } elsif (defined $config->{mapping}{mode}{$char}) {
515             $screen->draw_matches($config, $matches, []); # clear matches
516             return { key => $char };
517         # All other mappings stay in the current mode.
518         } elsif (defined (my $m = $config->{mapping}{simple}{$char})) {
519             $m->($char, $screen, $config, $input);
520             next;
521
522         } else {
523             next; # ignore unknown mappings
524         }
525
526         last if $number > 0 and $number * 10 > @{$matches}; # unique match
527
528         my @remaining = $number == 0
529                       ? @{$matches}
530                       : grep { $_->{id} =~ /^$number/ } @{$matches};
531         $screen->draw_matches($config, $matches, \@remaining);
532         $screen->refresh;
533     }
534
535     foreach (@{$matches}) {
536         return { match => $_ } if $_->{id} == $number;
537     }
538     debug $config, 'select_match', 'no match selected';
539     return { match => undef };
540 }
541
542
543 sub mapping_paste {
544     my ($key, $screen, $config, $input) = @_;
545
546     debug $config, 'mapping_paste', 'started';
547
548     $config->{state}{handler} = \&handler_paste;
549
550     $screen->prompt(flags => 'P'); # paste
551     $screen->draw_prompt($config);
552     $screen->refresh;
553
554     return {};
555 }
556 sub mapping_yank {
557     my ($key, $screen, $config, $input) = @_;
558
559     debug $config, 'mapping_yank', 'started';
560
561     $config->{state}{handler} = \&handler_yank;
562
563     $screen->prompt(flags => 'Y'); # yank
564     $screen->draw_prompt($config);
565     $screen->refresh;
566
567     return {};
568 }
569
570
571 sub mapping_mode_path {
572     my ($key, $screen, $config, $input) = @_;
573
574     debug $config, 'mapping_mode_path', 'started';
575
576     my @matches = get_regex_matches($input, $config->{regex}{path});
577     return {
578         select  => 'path select',
579         matches => \@matches,
580         handler => \&handler_yank,
581     };
582 }
583 sub mapping_mode_url {
584     my ($key, $screen, $config, $input) = @_;
585
586     debug $config, 'mapping_mode_url', 'started';
587
588     my @matches = get_regex_matches($input, $config->{regex}{url});
589     return {
590         select  => 'url select',
591         matches => \@matches,
592         handler => \&handler_url,
593     };
594 }
595
596 sub mapping_mode_search {
597     my ($key, $screen, $config, $input) = @_;
598
599     debug $config, 'mapping_mode_search', 'started';
600
601     $screen->cursor(1);
602
603     my $search = ''; # encoded
604     my @last_matches;
605     while (1) {
606         # getch doesn't return decoded characters but raw input bytes. Wait
607         # until the input character is complete.
608         my $value = $screen->decode($search);
609         $value = '' unless defined $value; # undef on decode failure
610
611         $screen->prompt(name => 'search', value => $value);
612         $screen->draw_prompt($config);
613         $screen->refresh;
614
615         my $char = $screen->getch;
616         # TODO: readline editing support
617         if ($char eq "\n") {
618             last;
619         } elsif ($char eq "\b" or $char eq "\x7f") { # backspace
620             # Remove a character, not a byte.
621             $search = $screen->decode($search);
622             chop $search;
623             $search = $screen->encode($search);
624         } else {
625             $search .= $char;
626             next unless defined $screen->decode($search);
627         }
628
629         my @matches;
630         if ($search ne '') {
631             my $case = '';
632             if (($config->{setting}{smartcase} and $search eq lc $search)
633                     or $config->{setting}{ignorecase}) {
634                 $case = '(?i)';
635             }
636             # Ignore invalid regexps.
637             # TODO: display warning on error?
638             eval {
639                 @matches = get_regex_matches($input, qr/($case$search)/);
640             };
641         }
642         $screen->draw_matches($config, \@last_matches, \@matches);
643         @last_matches = @matches;
644     }
645
646     $screen->cursor(0);
647
648     return {
649         select  => 'search',
650         matches => \@last_matches,
651         handler => \&handler_yank,
652     };
653 }
654
655 sub mapping_quit {
656     my ($key, $screen, $config, $input) = @_;
657
658     # Key is necessary to fall through to main event loop which then quits.
659     return { key => $key, quit => 1 };
660 }
661
662
663 sub handler_yank {
664     my ($screen, $config, $match) = @_;
665
666     debug $config, 'handler_yank', 'started';
667
668     require File::Temp;
669
670     # Use a temporary file to prevent leaking the yanked data to other users
671     # with the command line, e.g. ps aux or top.
672     my ($fh, $tmp) = File::Temp::tempfile(); # dies on its own
673     print $fh $screen->encode($match->{string});
674     close $fh or die $!;
675
676     if ($config->{setting}{multiplexer} eq 'screen') {
677         debug $config, 'handler_yank', 'using screen';
678
679         # GNU screen displays an annoying "Slurping X characters into buffer".
680         # Use 'msgwait 0' as a hack to disable it.
681         my $msgwait = $config->{setting}{screen_msgwait};
682         run_command($config, ['screen', '-X', 'msgwait', 0]);
683         run_command($config, ['screen', '-X', 'readbuf', $tmp]);
684         run_command($config, ['screen', '-X', 'msgwait', $msgwait]);
685     } elsif ($config->{setting}{multiplexer} eq 'tmux') {
686         debug $config, 'handler_yank', 'using tmux';
687
688         run_command($config, ['tmux', 'load-buffer', $tmp]);
689     } else {
690         die 'unsupported multiplexer';
691     }
692
693     unlink $tmp or die $!;
694     return;
695 }
696 sub handler_paste {
697     my ($screen, $config, $match) = @_;
698
699     debug $config, 'handler_paste', 'started';
700
701     require Time::HiRes;
702
703     my @cmd;
704     if ($config->{setting}{multiplexer} eq 'screen') {
705         debug $config, 'handler_paste', 'using screen';
706         @cmd = qw( screen -X paste . );
707     } elsif ($config->{setting}{multiplexer} eq 'tmux') {
708         debug $config, 'handler_paste', 'using tmux';
709         @cmd = qw( tmux paste-buffer );
710     } else {
711         die 'unsupported multiplexer';
712     }
713
714     run_in_background($config, sub {
715         # We need to get the data in the paste buffer before we can paste
716         # it.
717         handler_yank($screen, $config, $match);
718
719         # Sleep until we switch back to the current window.
720         Time::HiRes::usleep($config->{setting}{paste_sleep});
721
722         run_command($config, \@cmd);
723     });
724     return;
725 }
726 sub handler_url {
727     my ($screen, $config, $match) = @_;
728
729     debug $config, 'handler_url', 'started';
730
731     run_in_background($config, sub {
732         my $url = defined $match->{url}
733                 ? $match->{url}
734                 : $match->{string};
735
736         my @cmd = map { $screen->encode($_) } (
737             @{$config->{setting}{browser}},
738             $url,
739         );
740         run_command($config, \@cmd);
741     });
742     return;
743 }
744
745
746
747 # CONFIGURATION DEFAULTS
748
749 =head1 CONFIGURATION
750
751 fcscs is configured through F<~/.fcscsrc> or F<~/.config/fcscs/fcscsrc> which
752 is a normal Perl script with all of Perl's usual features.
753
754 All configuration values are stored in the hash C<%config>. All manually
755 defined keys overwrite the default settings.
756
757 A simple F<~/.fcscsrc> could look like this (for details about the used
758 settings see below):
759
760     use strict;
761     use warnings;
762
763     use Curses; # for COLOR_* and A_* constants
764
765     our %config;
766
767     # Draw matches in blue.
768     $config{attribute}{match_string} = color_pair(COLOR_BLUE, -1);
769     # Enable Vim-like 'smartcase', ignore case until an upper character is
770     # searched.
771     $config{setting}{smartcase} = 1;
772
773     # Use chromium to open URLs if running under X, elinks otherwise.
774     if (defined $ENV{DISPLAY}) {
775         $config{setting}{browser} = ['chromium'];
776     } else {
777         $config{setting}{browser} = ['elinks'];
778     }
779
780     # Let fcscs know the file was loaded successfully.
781     1;
782
783 =cut
784
785
786 if (@ARGV != 1) {
787     require Pod::Usage;
788     Pod::Usage::pod2usage(2);
789 }
790
791
792 # Determine terminal encoding from the environment ($ENV{LANG} or similar).
793 my $encoding = I18N::Langinfo::langinfo(I18N::Langinfo::CODESET);
794
795 my $screen = Screen->init($encoding);
796
797 # We must restore the screen before exiting.
798 local $SIG{INT} = sub {
799     $screen->deinit;
800     exit 128 + 2;
801 };
802 # Make all warnings fatal to make sure they don't get lost (stderr is normally
803 # not displayed).
804 local $SIG{__WARN__} = sub {
805     $screen->die('warning', @_);
806 };
807
808
809
810 =head2 MAPPINGS
811
812 I<NOTE>: Mappings are split in two categories: Mode mappings which change the
813 selection and may receive additional input (e.g. a search string) and simple
814 mappings which only change some value. Mode mappings are configured via
815 C<$config{mapping}{mode}>, simple mappings via C<$config{mapping}{simple}>.
816
817 The following mode mappings are available by default (the function to remap
818 them in parentheses):
819
820 =over
821
822 =item B<f> select absolute/relative paths (C<\&mapping_mode_path>)
823
824 =item B<u> select URLs (C<\&mapping_mode_url>)
825
826 =item B</> search for regex to get selection (C<\&mapping_mode_search>)
827
828 =item B<q> quit fcscs (C<\&mapping_quit>)
829
830 =back
831
832 The following simple mappings are available by default:
833
834 =over
835
836 =item B<p> enable pasting (C<\&mapping_paste>)
837
838 =item B<y> enable yanking (copying) (C<\&mapping_yank>)
839
840 =back
841
842 All (single-byte) keys except numbers, backspace and return can be mapped.
843
844 Unknown mappings are ignored when pressing keys.
845
846 To remove a default mapping, delete it from the mapping hash.
847
848 Example:
849
850     # Map 'p' to select paths, 'P' to enable pasting.
851     $config{mapping}{mode}{p} = \&mapping_mode_path;
852     $config{mapping}{simple}{P} = \&mapping_paste;
853
854     # Disable 'f' mapping.
855     delete $config{mapping}{mode}{f};
856
857 =cut
858 my %mapping_mode = (
859     f   => \&mapping_mode_path,
860     u   => \&mapping_mode_url,
861     '/' => \&mapping_mode_search,
862     q   => \&mapping_quit,
863 );
864 my %mapping_simple = (
865     p => \&mapping_paste,
866     y => \&mapping_yank,
867 );
868
869 =head2 ATTRIBUTES
870
871 Attributes are used to style the output. They must be Curses attributes.
872 Defaults in parentheses (foreground, background, attribute).
873
874 =over
875
876 =item B<match_id>      attribute for match numbers (red, default, bold)
877
878 =item B<match_string>  attribute for matches (yellow, default, normal)
879
880 =item B<prompt_name>   attribute for prompt name (standout)
881
882 =item B<prompt_flags>  attribute for prompt flags (standout)
883
884 =back
885
886 Example:
887
888     # Draw prompt flags in bold red with default background color.
889     $config{attribute}{prompt_flags}
890         = Curses::A_BOLD
891         | color_pair(Curses::COLOR_RED, -1);
892
893 =cut
894 my %attribute = (
895     match_id     => $screen->color_pair(Curses::COLOR_RED, -1)
896                     | Curses::A_BOLD,
897     match_string => $screen->color_pair(Curses::COLOR_YELLOW, -1),
898     prompt_name  => Curses::A_STANDOUT,
899     prompt_flags => Curses::A_STANDOUT,
900 );
901
902 =head2 SETTINGS
903
904 Defaults in parentheses.
905
906 =over
907
908 =item B<debug>          enable debug mode (redirect stderr when enabling) (C<0>)
909
910 =item B<initial_mode>   start in this mode, must be a valid mode mapping (C<\&mapping_mode_url>)
911
912 =item B<multiplexer>    set multiplexer ("screen" or "tmux") if not autodetected (C<undef>)
913
914 =item B<ignorecase>     ignore case when searching (C<0>)
915
916 =item B<smartcase>      ignore case unless one uppercase character is searched (C<1>)
917
918 =item B<paste_sleep>    sleep x us before running paste command (C<100_000>)
919
920 =item B<screen_msgwait> GNU Screen's msgwait variable, used when yanking (C<5>)
921
922 =item B<browser>        browser command as array reference (C<['x-www-browser']>)
923
924 =back
925
926 Example:
927
928     # Select paths on startup instead of URLs.
929     $config{setting}{initial_mode} = \&mapping_mode_path;
930
931 =cut
932 my %setting = (
933     # options
934     debug          => 0,
935     initial_mode   => \&mapping_mode_url,
936     multiplexer    => undef,
937     ignorecase     => 0,
938     smartcase      => 1,
939     paste_sleep    => 100_000,
940     screen_msgwait => 5,
941     # commands
942     browser        => ['x-www-browser'],
943 );
944
945 =head2 REGEXPS
946
947 =over
948
949 =item B<url> used by C<\&mapping_mode_url()>
950
951 =item B<path> used by C<\&mapping_mode_path()>
952
953 =back
954
955 Example:
956
957     # Select all non-whitespace characters when searching for paths.
958     $config{regex}{path} = qr{\S+};
959
960 =cut
961 my %regex = (
962     # Taken from urlview's default configuration file, thanks.
963     url  => qr{((?:(?:(?:http|https|ftp|gopher)|mailto):(?://)?[^ <>"\t]*|(?:www|ftp)[0-9]?\.[-a-z0-9.]+)[^ .,;\t\n\r<">\):]?[^, <>"\t]*[^ .,;\t\n\r<">\):])},
964     path => qr{(~?[a-zA-Z0-9_./-]*/[a-zA-Z0-9_./-]+)},
965 );
966
967 my %state = (
968     initial => 1, # used by select_match() for 'initial_mode'
969     handler => undef,
970 );
971
972
973
974 # CONFIGURATION "API"
975
976 =head2 FUNCTIONS
977
978 The following functions are available:
979
980     color_pair($fg, $bg)
981
982 Create a new Curses attribute with the given fore- and background color.
983
984     mapping_mode_path()
985     mapping_mode_url()
986     mapping_mode_search()
987
988     mapping_paste()
989     mapping_yank()
990     mapping_quit()
991
992 Used as mappings, see L</MAPPINGS> above.
993
994     handler_yank()
995     handler_paste()
996     handler_url()
997
998 Used as handler to yank, paste selection or open URL in browser. They are
999 either set by the matching mapping function (C<mapping_paste()>, etc.) or
1000 configured by the current mode.
1001
1002     get_regex_matches()
1003     select_match()
1004     run_command()
1005     run_in_background()
1006
1007 Helper functions when writing custom mappings, see the source for details.
1008
1009 Example:
1010
1011     TODO
1012
1013 =cut
1014
1015 # All variables and functions which are usable by ~/.fcscsrc.
1016 package Fcscs {
1017     our $screen; # "private"
1018     our %config;
1019
1020     sub color_pair { return $screen->color_pair(@_); }
1021
1022     sub mapping_mode_path { return main::mapping_mode_path(@_); }
1023     sub mapping_mode_url { return main::mapping_mode_url(@_); }
1024     sub mapping_mode_search { return main::mapping_mode_search(@_); }
1025
1026     sub mapping_paste { return main::mapping_paste(@_); }
1027     sub mapping_yank { return main::mapping_yank(@_); }
1028     sub mapping_quit { return main::mapping_quit(@_); }
1029
1030     sub handler_yank { return main::handler_yank(@_); }
1031     sub handler_paste { return main::handler_paste(@_); }
1032     sub handler_url { return main::handler_url(@_); }
1033
1034     sub get_regex_matches { return main::get_regex_matches(@_); }
1035     sub select_match { return main::select_match(@_); }
1036
1037     sub run_command { return main::run_command(@_); }
1038     sub run_in_background { return main::run_in_background(@_); }
1039 }
1040 $Fcscs::screen = $screen;
1041
1042
1043
1044 # LOAD USER CONFIG
1045
1046 # Alias %config and %Fcscs::config. %config is less to type.
1047 our %config;
1048 local *config = \%Fcscs::config;
1049
1050 $config{mapping}{mode}   = \%mapping_mode;
1051 $config{mapping}{simple} = \%mapping_simple;
1052 $config{attribute}       = \%attribute;
1053 $config{setting}         = \%setting;
1054 $config{regex}           = \%regex;
1055 $config{state}           = \%state;
1056
1057 package Fcscs {
1058     my @configs = ("$ENV{HOME}/.fcscsrc",
1059                    "$ENV{HOME}/.config/fcscs/fcscsrc");
1060     foreach my $path (@configs) {
1061         my $decoded = $screen->decode($path);
1062
1063         # Load configuration file. Checks have a race condition if the home
1064         # directory is writable by an attacker (but then the user is screwed
1065         # anyway).
1066         next unless -e $path;
1067         if (not -O $path) {
1068             $screen->die("Config '$decoded' not owned by current user!");
1069         }
1070         # Make sure the file is not writable by other users. Doesn't handle
1071         # ACLs and see comment above about race conditions.
1072         my @stat = stat $path or die $!;
1073         my $mode = $stat[2];
1074         if (($mode & Fcntl::S_IWGRP) or ($mode & Fcntl::S_IWOTH)) {
1075             die "Config '$decoded' must not be writable by other users.";
1076         }
1077
1078         my $result = do $path;
1079         if (not $result) {
1080             $screen->die("Failed to parse '$decoded': $@") if $@;
1081             $screen->die("Failed to do '$decoded': $!") unless defined $result;
1082             $screen->die("Failed to run '$decoded'.");
1083         }
1084
1085         last; # success, don't load more files
1086     }
1087 }
1088 $screen->{debug} = $config{setting}{debug};
1089
1090
1091 # MAIN
1092
1093 eval {
1094     # Auto-detect current multiplexer.
1095     if (not defined $config{setting}{multiplexer}) {
1096         if (defined $ENV{STY} and defined $ENV{TMUX}) {
1097             die 'Found both $STY and $TMUX, set $config{setting}{multiplexer}.';
1098         } elsif (defined $ENV{STY}) {
1099             $config{setting}{multiplexer} = 'screen';
1100         } elsif (defined $ENV{TMUX}) {
1101             $config{setting}{multiplexer} = 'tmux';
1102         } else {
1103             die 'No multiplexer found.';
1104         }
1105     }
1106
1107     my $binmode = $encoding;
1108     # GNU screen stores the screen dump for unknown reasons as ISO-8859-1
1109     # instead of the currently active encoding.
1110     if ($config{setting}{multiplexer} eq 'screen') {
1111         $binmode = 'ISO-8859-1';
1112     }
1113
1114     my @input_lines;
1115     open my $fh, '<', $ARGV[0] or die $!;
1116     binmode $fh, ":encoding($binmode)" or die $!;
1117     while (<$fh>) {
1118         chomp;
1119         push @input_lines, $_;
1120     }
1121     close $fh or die $!;
1122
1123     my $input = prepare_input($screen, \@input_lines);
1124
1125     # Display original screen content.
1126     my $y = 0;
1127     foreach (@{$input->{lines}}) {
1128         $screen->draw_simple($y++, 0, undef, $_);
1129     }
1130     $screen->refresh;
1131
1132
1133     my $mapping = $config{setting}{initial_mode};
1134
1135     my $key;
1136     while (1) {
1137         if (not defined $mapping) {
1138             $key = $screen->getch unless defined $key;
1139             debug \%config, 'input', "got key '$key'";
1140
1141             $mapping = $config{mapping}{mode}{$key};
1142             $mapping = $config{mapping}{simple}{$key} unless defined $mapping;
1143             if (not defined $mapping) { # ignore unknown mappings
1144                 $key = undef;
1145                 next;
1146             }
1147         }
1148
1149         debug \%config, 'input', 'running mapping';
1150         my $result = $mapping->($key, $screen, \%config, $input);
1151         $mapping = undef;
1152
1153 RESULT:
1154         if (defined $result->{quit}) {
1155             debug \%config, 'input', 'quitting';
1156             last;
1157         }
1158         if (defined $result->{key}) {
1159             $key = $result->{key}; # lookup another mapping
1160             debug \%config, 'input', "processing new key: '$key'";
1161             next;
1162         }
1163         if (defined $result->{select}) {
1164             debug \%config, 'input', 'selecting match';
1165             my $tmp = $result;
1166             $result = select_match($result->{select},
1167                                 $screen, \%config, $input,
1168                                 $result->{matches});
1169             $result->{handler} = $tmp->{handler};
1170             goto RESULT; # reprocess special entries in result
1171         }
1172         if (defined $result->{match}) {
1173             debug \%config, 'input', 'running handler';
1174             my $handler = $config{state}{handler};                 # set by user
1175             $handler = $result->{handler} unless defined $handler; # set by mapping
1176             $handler = \&handler_yank     unless defined $handler; # fallback
1177             $handler->($screen, \%config, $result->{match});
1178             last;
1179         }
1180
1181         $key = undef; # get next key from user
1182     }
1183 };
1184 if ($@) {
1185     $screen->die("$@");
1186 }
1187
1188 $screen->deinit;
1189
1190 __END__
1191
1192 =head1 EXIT STATUS
1193
1194 =over 4
1195
1196 =item B<0>
1197
1198 Success.
1199
1200 =item B<1>
1201
1202 An error occurred.
1203
1204 =item B<2>
1205
1206 Invalid arguments/options.
1207
1208 =back
1209
1210 =head1 AUTHOR
1211
1212 Simon Ruderich E<lt>simon@ruderich.orgE<gt>
1213
1214 =head1 LICENSE AND COPYRIGHT
1215
1216 Copyright (C) 2013-2016 by Simon Ruderich
1217
1218 This program is free software: you can redistribute it and/or modify
1219 it under the terms of the GNU General Public License as published by
1220 the Free Software Foundation, either version 3 of the License, or
1221 (at your option) any later version.
1222
1223 This program is distributed in the hope that it will be useful,
1224 but WITHOUT ANY WARRANTY; without even the implied warranty of
1225 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
1226 GNU General Public License for more details.
1227
1228 You should have received a copy of the GNU General Public License
1229 along with this program.  If not, see E<lt>http://www.gnu.org/licenses/E<gt>.
1230
1231 =head1 SEE ALSO
1232
1233 L<screen(1)>, L<tmux(1)>, L<urlview(1)>
1234
1235 =cut