#!/usr/bin/env perl
use strict;
use warnings;
use autodie;
use IO::Handle;

our $VERSION = '0.01';

# It's not very useful to have the user type in lines to then select...
exit if -t *STDIN;

# We need to talk to the user directly. stdin and stdout are what we're
# filtering.
open my $in,  '<', '/dev/tty';
open my $out, '>', '/dev/tty';
$out->autoflush(1);

my @lines;
my @is_selected;
my $cursor = 0;
my $done = 0;
my $counted = 0;

my %command_table;
%command_table = (
    j => {
        command        => sub { ++$cursor },
        documentation  => "skip to the next line",
    },
    k => {
        command        => sub { --$cursor },
        available_when => sub { $cursor > 0 },
        documentation  => "back up to the previous line",
    },
    y => {
        command        => sub { $is_selected[$cursor] = 1; ++$cursor },
        documentation  => "pass this line through",
    },
    n => {
        command        => sub { $is_selected[$cursor] = 0; ++$cursor },
        documentation  => "don't pass this line through",
    },
    d => {
        command        => sub { $done = 1 },
        documentation  => "print selected lines, skipping all remaining lines",
    },
    q => {
        command        => sub { exit },
        documentation  => "cancel ask, passing no lines through",
    },
    c => {
        command => sub {
            # Force the rest of stdin, so we can get a count of how many lines
            # need to be decided upon
            push @lines, <>;

            $counted = 1;
        },
        available_when => sub { !$counted },
        documentation  => "read the rest of input to count its size",
    },
    '?' => {
        command => sub {
            for my $key (sort keys %command_table) {
                printf "%s: %s\n", $key, $command_table{$key}{documentation};
            }
            print "<Space>: accept the current default (which is capitalized)\n";
        },
        documentation => "show this help",
    },
);

while (!$done) {
    my $c = get_input();
    last if !defined($c);

    run_command($c);
}

# Print selected lines!
print map { $lines[$_] }
      grep { $is_selected[$_] }
      0 .. $#lines;

sub get_input {
    # If the cursor is beyond the already-read lines, lazily read the next
    # line.
    push @lines, scalar <> until @lines > $cursor;

    # We hit EOF, so we're done getting input from the user.
    return if !defined($lines[-1]);

    prompt_user();

    my $c = read_key();

    # We always want to add a blank line before any more output, since our
    # prompt didn't add a newline.
    tell_user("\n");

    return $c;
}

sub run_command {
    my $c = shift;

    # Space uses the default.
    $c = default_command() if $c eq ' ';

    return tell_user("Invalid response, try again!\n")
        if !exists($command_table{$c});

    return tell_user("That command is currently unavailable.\n")
        if exists($command_table{$c}{available_when})
        && !$command_table{$c}{available_when}->();

    $command_table{$c}{command}->();
}

sub read_key {
    # Term::ReadKey operates on STDIN.
    local *STDIN = $in;

    use Term::ReadKey;
    ReadMode(3); # Single-character input, without echo
    my $c = ReadKey;
    ReadMode(0); # Restore regular readline

    return $c;
}

sub tell_user {
    print { $out } @_;
}

sub prompt_user {
    # current line
    tell_user($lines[$cursor]);

    tell_user("Shall I pass this line through? ");

    my $current = $cursor + 1; # start from 1 not 0
    my $max     = $counted ? @lines : '?';
    tell_user("($current/$max) ");

    my $default = default_command();

    # Construct the list of available commands.
    my $keys = join '',

               # Capitalize default command
               map { $_ eq $default ? uc($default) : $_ }

               sort

               # Don't display help command
               grep { $_ ne '?' }

               # Hide unavailable commands
               grep {
                   exists($command_table{$_}{available_when})
                   ? $command_table{$_}{available_when}->()
                   : 1
               }

               # All commands
               keys %command_table;

    tell_user("[$keys]> ");
}

sub default_command { $is_selected[$cursor] ? 'y' : 'n' }

__END__

=head1 NAME

ask - filters stdin by asking the user for each line

=head1 DESCRIPTION

C<ask> is a utility that filters stdin to stdout not by any automatic process,
but by prompting the user about each line.

=head1 AUTHOR

Shawn M Moore, C<sartak at bestpractical dot com>

=head1 PREREQUISITES

autodie

Term::ReadKey

=pod SCRIPT CATEGORIES

UNIX/System_administration

=cut