#!/usr/bin/perl

# Created on: 2010-09-14 08:51:00
# Create by:  Ivan Wills
# $Id$
# $Revision$, $HeadURL$, $Date$
# $Revision$, $Source$, $Date$

use strict;
use warnings;
use version;
use Getopt::Alt qw/get_options/;
use English qw/ -no_match_vars /;
use Path::Tiny;
use Tail::Tool;
use AnyEvent;
use AnyEvent::Loop;
use TryCatch;
use YAML qw/LoadFile DumpFile Dump/;
use File::HomeDir;

our $VERSION = version->new('0.4.4');
my ($name)   = $PROGRAM_NAME =~ m{^.*/(.*?)$}mxs;

my %option = (
    lines    => 10,
    disabled => [],
);
my %found_plugins;
my $restart = {};
my $tt;
my $config_file = path(File::HomeDir->my_home, '.tailtrc');

main();
exit 0;

sub main {
    my %plugins = get_plugins();

    my $opt = get_options(
        {
            default => \%option,
            helper  => 1,
        },
        [
            'disabled|d=s@',
            'restart|r!',
            'lines|n=i',
            'no_inotify|no-inotify!',
            'config|c=s',
            'Highlight|highlight|h=s@',
            'Match|match|m=s@',
            'Ignore|ignore|i=s@',
            'Replace|replace|r=s@',
            ( map {  "$_|". lc $_. '=s%' } keys %plugins ),
            'verbose|v+',
        ]
    );
    %option = %{ $opt };

    # do stuff here
    for my $key (qw/Highlight Match Ignore Replace/) {
        next if !exists $option{$key};
        $option{$key} = { regex => [@{ $option{$key} }] };
    }

    my $restore;
    if ( $option{config} && -f $config_file ) {
        my $config = LoadFile($config_file);
        $restore = $config->{configs}{ $option{config} };

        delete $option{config};
    }

    if ( $option{restart} ) {
        $restart->{normal} = AE::io *STDIN, 0, sub {
            my $cmd = <STDIN>;

            return if !$cmd || $cmd =~ /^\s*$/;

            chomp $cmd;

            exit 0 if lc $cmd eq 'q' || lc $cmd eq 'bye';

            return if $cmd eq '';

            restart();
        };
        $restart->{int} = AE::signal INT => \&restart;
    }

    my %disabled;
    if ( @{ $option{disabled} } ) {
        %disabled = (
            map {
                $_ => 1
            }
            map {
                @{ get_files($_) }
            }
            map {
                split /[,;]/, $_
            }
            @{ $option{disabled} }
        );
        push @ARGV, keys %disabled;
    }
    delete $option{disabled};

    $tt = Tail::Tool->new(
        files => get_files(@ARGV),
        #printer => \&printer,
        %option,
    );

    if ( %disabled ) {
        for my $file ( @{ $tt->files } ) {
            if ( $disabled{$file->name} ) {
                $file->pause(1);
            }
        }
    }

    if ( $restore ) {
        push @{ $tt->pre_process }, @{ $restore->{pre_process} }
            if @{ $restore->{pre_process} || [] };
        push @{ $tt->post_process }, @{ $restore->{post_process} }
            if @{ $restore->{post_process} || [] };
    }

    my $w;
    if ( grep { $_->{name} eq '-' } @{ $tt->files } ) {
        # check that all files have finished if we are looking at STDIN
        $w = AE::timer 0.1, 0.1, sub {
            my $count = 0;
            for my $file (@{ $tt->files }) {
                $count++ if $file->watcher;
            }
            exit if $count == 0;
        };
    }

    $tt->tail();

    AnyEvent::Loop::run;

    return $w;
}

sub restart {
    print "\n";
    my $files   = join ', ', map { colored( $_->name, $_->pause ? 'red' : 'green' ) } @{ $tt->files };
    my $plugins = '';
    my $i       = 0;
    my %done;
    my @plugins;

    for my $plg ( @{ $tt->pre_process }, @{ $tt->post_process }, sort keys %found_plugins ) {
        my $name = ref $plg || $plg;
        $name =~ s/^(.+::)//xms;

        next if !ref $plg && $done{$name};
        $i++;
        $done{$name} = $plg;
        $plugins[$i] = $plg;

        $plugins .= "\n" if $plugins;
        $plugins .= sprintf "%2d  %s %s", $i, $name eq $plg ? 'Add' : 'Change', $name;
        if ( ref $plg && $plg->can('summarise') ) {
            $plugins .= ' (' . $plg->summarise(1) . ')';
        }
    }

    print <<"MENU";

$plugins
 f  Change tailed files ($files)
 r  Resume tailing
 c  Clear screen and resume tailing
 p  Plugin ordering
 l  Load Config
 s  Save Config
 b  Shell out
 q  Quit
MENU

    my $answer = prompt_menu( 1 .. $i, qw/f r c p l s q Q/ );

    if ( $answer eq 'f' ) {
        update_files();
    }
    elsif ( $answer eq 'r' ) {
        return 1;
    }
    elsif ( $answer eq 'c' ) {
        print "\n" x 2_000;
        return 1;
    }
    elsif ( $answer eq 'p' ) {
        plugin_order();
    }
    elsif ( $answer eq 'l' ) {
        load_config();
    }
    elsif ( $answer eq 's' ) {
        save_config();
    }
    elsif ( $answer eq 'b' ) {
        system $ENV{SHELL} || '/bin/bash';
    }
    elsif ( $answer =~ /^\d+$/ ) {
        update_plugin( $plugins[$answer] );
    }

    exit if !defined $answer || $answer eq '' || lc $answer eq 'q';

    # reinstall interupt handler
    $restart->{int} = AE::signal INT => \&restart;

    return restart();
}

my $spinner;
sub printer {
    my @lines = @_;

    if ( !$spinner ) {
        require Term::Spinner;
        $spinner = Term::Spinner->new();
    }

    if (@lines) {
        $spinner->advance;
    }
    else {
        $spinner->clear;
        print {*STDOUT} @lines;
    }
    die "Why isn't this working?\n".@lines."\n";
}

sub get_plugins {
    my %plugins;

    for my $inc (@INC) {
        my $dir = path($inc, 'Tail', 'Tool', 'Plugin');
        next if !-d $dir;

        my @modules = grep { /[.]pm$/ } $dir->children;

        MODULE:
        for my $module (@modules) {
            my $name = $module->basename;
            $name =~ s/[.]pm//xms;
            next if $found_plugins{$name}++;

            eval { require $module };
            warn $@ if $@;
            next if $EVAL_ERROR;

            next MODULE if $name eq 'Highlight' || $name eq 'Ignore' || $name eq 'Match';

            $module =~ s{$inc/}{}xms;
            $module =~ s{[.]pm}{}xms;
            $module =~ s{/}{::}gxms;
            $plugins{$name} = $module->does('Tail::Tool::RegexRole');
        }
    }

    return %plugins;
}

sub update_files {
    my $i = 0;
    my %map_orig = map { $_->name => $i++ } @{ $tt->files };
    my @files
        = sort {
            my $aname = $a->name;
            my $bname = $b->name;
            $aname =~ s/(\d+)/sprintf "%05d", $1/egxms;
            $bname =~ s/(\d+)/sprintf "%05d", $1/egxms;
            $aname cmp $bname;
        }
        @{ $tt->files };
    $i = 0;
    my %map_new = map { ++$i => $map_orig{$_->name} } @files;
    $i = 0;
    my $files
        = join "\n",
        map {
            sprintf "%2d  Change %s", ++$i, colored( $_->name, $_->pause ? 'red' : 'green' )
        }
        @files;
    print <<"MENU";

$files
 a  Add another file to tail
 r  Return to previous menu
MENU

    my $answer = prompt_menu( 1 .. $i, qw/a r R/ );

    return if $answer eq 'r';

    if ( $answer eq 'a' ) {
        my $new_file = prompt("New file name : ", '-tty') . '';

        my $file = Tail::Tool::File->new( name => $new_file );
        $file->tailer($tt);
        push @{ $tt->files }, $file;
        $tt->tail( 1 );
    }
    else {
        update_file( $map_new{$answer} );
    }
    return update_files();
}

sub update_file {
    my ($i) = @_;
    my $file = $tt->files->[$i];
    my $name = $file->name;
    my $pause = $file->pause ? 'Resume' : 'Pause';

    print <<"MENU";
 d  Delete $name
 p  $pause tailing of $name
 r  Return
MENU

    my $answer = prompt_menu( qw/d p r R/ );

    return if $answer eq 'r';

    if ( $answer eq 'p' ) {
        $file->pause( ! $file->pause );
        $file->watch();
    }
    elsif ( $answer eq 'd' ) {
        my @files = @{ $tt->files };
        if ( $i == 0 ) {
            shift @files;
        }
        elsif ( $i == @files - 1 ) {
            pop @files;
        }
        else {
            @files = ( @files[ 0 .. $i - 1], @files[ $i + 1 .. @files - 1 ] );
        }
        $tt->files(\@files);

        return;
    }

    return update_file($i);
}

sub update_plugin {
    my ($plg) = @_;

    my $plugin = $plg;
    if ( !ref $plugin ) {
        my $module = 'Tail::Tool::Plugin::' . $plugin;
        $plugin = $module->new();
    }

    my $meta = $plugin->meta;
    my $i = 0;
    my @names;

    for my $attrib ( $meta->get_all_attributes ) {
        my $name = $attrib->name;
        next if $name eq 'post';
        next if !$attrib->has_init_arg;
        next if $attrib->{isa} eq 'CodeRef';
        #next if grep { $name eq $_ } qw/last_time/;
        $i++;

        $names[$i] = $attrib;
        my $out = sprintf "%2d  Change $name", $i;

        my $reader = $attrib->reader || $name;
        my $value = $plugin->$reader();

        $out .= ' (' . show_value($value) . ')' if $value;

        print "$out\n";
    }
    print " a  Add a new instance\n" if $plg eq $plugin && $plugin->many;
    print " r  Return to previous menu\n";

    my $answer = prompt_menu( 1 .. $i, qw/a r R/ );

    return if !defined $answer || $answer eq '' || $answer eq 'r';

    if ( $answer eq 'a' ) {
        $plg = ref $plugin;
        $plg =~ s/^.*:://xms;
        return update_plugin( $plg );
    }

    my $updated = update_attribute( $plugin, $names[$answer] );

    if ( $updated && $plugin ne $plg ) {
        if ( $plugin->post ) {
            $tt->post_process( [ @{ $tt->post_process() }, $plugin ] );
        }
        else {
            $tt->pre_process( [ @{ $tt->pre_process() }, $plugin ] );
        }
    }

    return update_plugin($plugin);
}

sub update_attribute {
    my ( $plugin, $attrib ) = @_;
    my $name   = $attrib->name;
    my $reader = $attrib->reader || $name;
    my $writer = $attrib->writer || $name;
    my $value = $plugin->$reader();

    if ( ref $value eq 'ARRAY' ) {
        try {
            $plugin->$writer( update_array( $value ) );
        }
        catch ($e) {
            warn "Error in updating value ($value): $e\n";
        }
        return 1;
    }
    else {
        my $new_value = prompt("Change $name to : ", '-tty') . '';
        try {
            $plugin->$writer( $new_value );
        }
        catch ($e) {
            if ( $e =~ /ArrayRefHashRef/ ) {
                $plugin->$writer( [{ regex => qr/$new_value/, enabled => 1 }] );
            }
            else {
                warn "Could not work out how to add this value: $e";
            }
        }
        return 1;
    }
    return 0;
}

sub update_array {
    my ($array) = @_;

    my $i = 0;
    for my $element ( @{ $array } ) {
        printf "%2d  Update %s\n", $i++, show_value($element);
    }

    print <<"MENU";
 a  Add new element
 d  Delete element
 r  Return to previous menu
MENU

    my $answer = prompt_menu( 0 .. $i - 1, qw/a d r R/ );

    return $array if !defined $answer || lc $answer eq 'r';

    my $regex = 'Tail::Tool::Regex';
    if ( $answer eq 'd' ) {
        my $delete = prompt("Delete which entry : ", '-tty');
        if ( $delete == 0 ) {
            shift @{ $array };
        }
        elsif ( $delete == @{ $array } - 1 ) {
            pop @{ $array };
        }
        else {
            $array = [ @{ $array }[ 0 .. $delete - 1 ], @{ $array }[ $delete + 1 .. @{ $array } - 1 ] ];
        }
    }
    elsif ( $answer eq 'a' ) {
        my $new
            = ref $array->[0] eq 'ARRAY' ? update_array([])
            : ref $array->[0] eq 'HASH'  ? update_hash({})
            : ref $array->[0] eq $regex  ? update_regex( $regex->new(regex=>''), $array->[0] )
            :                              prompt("Enter new element : ", '-tty') . '';
        push @{ $array }, $new;
    }
    else {
        $array->[$answer]
            = ref $array->[$answer] eq 'ARRAY'  ? update_array( $array->[$answer] )
            : ref $array->[$answer] eq 'HASH'   ? update_hash( $array->[$answer] )
            : ref $array->[$answer] eq $regex   ? update_regex( $array->[$answer] )
            :                                     prompt("Enter new value : ", '-tty') . '';
    }

    return $array;
}

sub update_hash {
    my ( $hash ) = @_;
    my @keys;

    for my $key ( keys %{ $hash } ) {
        printf "%2d  Change %s => %s\n", ( scalar @keys ), $key, show_value($hash->{$key});
        push @keys, $key;
    }
    print <<"MENU";
 a  Add new key
 d  Delete key
 r  Return
MENU

    my $answer = prompt_menu( 0 .. @keys - 1, qw/a d r R/ );

    return $hash if !defined $answer || lc $answer eq 'r';

    if ( $answer eq 'd' ) {
        print "Select which key to delete: ";
        my $answer = prompt_menu( 0 .. @keys - 1 );
        delete $hash->{ $keys[ $answer ] };
    }
    elsif ( $answer eq 'a' ) {
        my $key = prompt("Enter new key : ", '-tty') . '';
        my $value = prompt("Enter new value : ", '-tty') . '';
        $hash->{$key} = $value;
    }
    else {
        my $key = $keys[ $answer ];
        my $value
            = ref $hash->{$key} eq 'ARRAY' ? update_array( $hash->{$key} )
            : ref $hash->{$key} eq 'HASH'  ? update_hash( $hash->{$key} )
            :                                prompt("Enter new value : ", '-tty') . '';
        $hash->{$key} = $value;
    }

    return update_hash( $hash );
}

sub update_regex {
    my ( $regex, $other ) = @_;

    my @choice = ('x');
    print " x  Change regex (" . $regex->regex .")\n";

    if ( $regex->has_colour || ( $other && $other->has_colour ) ) {
        print " c  Change colour (" . ( join ', ', @{ $regex->colour || [] } ) . ")\n";
        push @choice, 'c';
    }

    if ( $regex->has_replace || ( $other && $other->has_replace ) ) {
        print " p  Change replace value (" . $regex->replace . ")\n";
        push @choice, 'p';
    }

    my $enabled = $regex->enabled ? 'Disable' : 'Enable';
    print <<"MENU";
 e  $enabled
 r  Return
MENU

    my $answer = prompt_menu( @choice, qw/e r R/ );

    if ( $answer eq 'r' ) {
        return $regex;
    }
    elsif ( $answer eq 'x' ) {
        my $new = prompt("Enter new regexp : ", '-tty');
        $regex->regex(qr/$new/);
    }
    elsif ( $answer eq 'c' ) {
        print "Possible colours: red green yellow blue magenta cyan on_red on_green on_yellow on_blue on_magenta on_cyan & bold\n";
        my $new = update_array( $regex->colour || [] );
        $regex->colour($new);
    }
    elsif ( $answer eq 'p' ) {
        my $new = prompt("Enter new replace value : ", '-tty');
        $regex->replace($new);
    }
    elsif ( $answer eq 'e' ) {
        $regex->enabled( !$regex->enabled );
    }

    return update_regex($regex);
}

sub show_value {
    my ($value) = @_;

    if ( !ref $value ) {
        return "'$value'";
    }
    elsif ( ref $value eq 'ARRAY' ) {
        return '[' . ( join ', ', map { show_value($_) } @{ $value } ) . ']';
    }
    elsif ( ref $value eq 'HASH' ) {
        return '{ ' . ( join ', ', map { "$_=>" . show_value($value->{$_}) } keys %{ $value } ) . ' }';
    }
    elsif ( ref $value eq 'Regexp' ) {
        return "qr/$value/";
    }
    elsif ( eval { $value->can('summarise') } ) {
        return $value->summarise(1);
    }
    else {
        warn "Don't yet display " . ( ref $value ) . " values\n";
    }

    return '';
}

sub prompt_menu {
    my @choices = @_;
    my @onechar = ('-one_char');
    for my $choice (@choices) {
        @onechar = () if length $choice > 1;
    }
    my $match
        = @onechar
        ? '^[' . ( join '',  @choices ) . ']?$'
        : '^(' . ( join '|', @choices ) . ')?$';

    my $answer = prompt(
        -prompt => 'Enter your choice [' . ( join ',', @choices ) . '] ',
        @onechar,
        '-tty',
        -require => {
            'Must be one of [' . ( join ', ', @choices ) . '] ' => qr/$match/,
        },
    );
    print "\n" if @onechar;

    return $answer;
}

sub plugin_order {
    print "\nPlugins:\n";
    print "1.  Pre  Processing: ";
    print join ", ", map {$a = ref $_; $a =~ s/^.*:://; $a} @{$tt->pre_process};
    print "\n";
    print "2.  Post Processing: ";
    print join ", ", map {$a = ref $_; $a =~ s/^.*:://; $a} @{$tt->post_process};
    print "\n";

    my $answer = prompt_menu( qw/ 1 2 r R/ );
    print "\n";
    if ( !$answer || lc $answer eq 'r' ) {
        return;
    }

    plugin_reorder( $answer == 1 ? $tt->pre_process : $tt->post_process );

    return plugin_order();
}

sub plugin_reorder {
    my ($plugins) = @_;

    print "\n";
    print join ", ", map {$a = ref $_; $a =~ s/^.*:://; $a} @{$plugins};
    print "\n";
    my $i = 0;
    for my $plugin (@{$plugins}) {
        my $name = ref $plugin;
        $name =~ s/^.*:://;

        printf "%2d  %s (%s)\n", ++$i, $name, $plugin->can('summarise') ? $plugin->summarise(1) : '';
    }

    return if $1 == 1;

    my $answer = prompt_menu( 1 .. $i, qw/r R/ );
    print "\n";

    return if !$answer || lc $answer eq 'r';

    my ($first, $second);
    if ( $answer == 1 ) {
        $first  = 1;
        $second = 2;
    }
    elsif ( $answer == $i ) {
        $first  = $i - 1;
        $second = $i;
    }
    else {
        my $dir = prompt(
            "Move (u)p or (d)own : ",
            '-one_char',
            '-tty',
            -require => {
                'Please enter either u or p' => qr/^[ud]$/,
            },
        ) . '';
        if ( $dir eq 'u' ) {
            $first  = $i;
            $second = $i + 1;
        }
        else {
            $first  = $i - 1;
            $second = $i;
        }
    }

    warn "Swapping $first => $second\n";
    my $tmp = $plugins->[$first - 1];
    $plugins->[$first - 1]  = $plugins->[$second - 1];
    $plugins->[$second - 1] = $tmp;

    return plugin_reorder($plugins);
}

sub load_config {
    my $config = -f $config_file ? LoadFile($config_file) : { configs => {} };
    my @saves;
    my $save;

    for my $key ( keys %{ $config->{configs} } ) {
        printf "%2d  Load \"%s\"\n", ( scalar @saves ), $key;
        push @saves, $key;
    }

    if ( !@saves ) {
        print "No saved configs\n";
        return;
    }

    print " r  Return\n";
    my $answer = prompt_menu( 0 .. @saves - 1, qw/r R/ );

    return if $answer eq 'r';

    my $restore = $config->{configs}{ $saves[ $answer ] };

    push @{ $tt->pre_process }, @{ $restore->{pre_process} }
        if @{ $restore->{pre_process} || [] };
    push @{ $tt->post_process }, @{ $restore->{post_process} }
        if @{ $restore->{post_process} || [] };
}

sub save_config {
    my $config = -f $config_file ? LoadFile($config_file) : { configs => {} };
    my @saves;
    my $save;

    for my $key ( keys %{ $config->{configs} } ) {
        printf "%2d  Save over \"%s\"\n", ( scalar @saves ), $key;
        push @saves, $key;
    }

    if ( @saves ) {
        print " n  Save as new name\n";
        print " r  Return\n";
        my $answer = prompt_menu( 0 .. @saves - 1, qw/n r R/ );
        if ( $answer eq 'n' ) {
            $save = prompt("Save AS : ", '-tty') . '';
        }
        elsif ( $answer ne 'r' ) {
            $save = $saves[ $answer ];
        }
    }
    else {
        $save = prompt("Save AS : ", '-tty') . '';
    }

    return if !$save;

    $config->{configs}{$save} = {
        pre_process  => $tt->pre_process,
        post_process => $tt->post_process,
    };

    DumpFile($config_file, $config);
    return;
}

sub get_files {
    my (@files) = @_;
    my @all;

    for my $file (@files) {
        push @all,
            $file =~ m{^ssh://} ? get_hosts([$file])
            :                     glob($file);
    }

    return @all ? \@all : ['-'];
}

# converts host ranges to actual host names
sub get_hosts {
    my ($hosts) = @_;
    my @hosts;

    my $int_re       = qr/ [0-9a-zA-Z] /xms;
    my $range_re     = qr/ ($int_re) (?:[.][.]|-) ($int_re)/xms;
    my $group_re     = qr/ (?: $int_re | $range_re )       /xms;
    my $seperated_re = qr/ $group_re (?: , $group_re )  *  /xms;
    my $num_range_re = qr/ [[{] ( $seperated_re ) [\]}]    /xms;

    while ( my $host_range = shift @{$hosts} ) {
        my ($num_range) = $host_range =~ /$num_range_re/;

        if (!$num_range) {
            push @hosts, $host_range;
            next;
            #if ( is_host($host_range) ) {
            #    push @hosts, $host_range;
            #    next;
            #}
            #else {
            #    unshift @{$hosts}, $host_range;
            #    last;
            #}
        }

        my @numbs    = map { /$range_re/ ? ($1 .. $2) : ($_) } split /,/, $num_range;
        my @hostmaps = map { $a=$host_range; $a =~ s/$num_range_re/$_/e; $a } @numbs;

        if ( $hostmaps[0] =~ /$num_range_re/ ) {
            push @{$option{host}}, @hostmaps;
        }
        else {
            push @hosts, @hostmaps;
        }
    }

    return @hosts;
}

sub prompt {
    require IO::Prompt;
    return IO::Prompt::prompt(@_);
}

sub colored {
    require Term::ANSIColor;
    return Term::ANSIColor::colored(@_);
}

__DATA__

=head1 NAME

tailt - Tail files using the Tail::Tool library

=head1 VERSION

This documentation refers to tailt version 0.4.4.

=head1 SYNOPSIS

   tailt [option] file1 [ file2 ...]
   tailt --help | --man | --VERSION

 OPTIONS:
  file             This can be a local file or a remote file specified by an
                   ssh URI eg ssh://user@example.com:22//var/log/error.log
  -r --restart     Turn on menu, which allows chnaging of options/files/plugin
                   configuration on the fly. To see the menu type any thing
                   other than q and press enter, typing q & enter quit.
  -n --lines=int   The number of lines form the end of a file to start tailing
                   The default is 10.
  -c --config=str  Use the str config option from previously save config
     --no_inotify  Inotify works wonderfully usually but if a file is on a network
                   networked drive it sometimes doesn't fire when a tailed file
                   changes, this option turns off inotify and uses the polling
                   option
  -d --disable=file
                   Add the file to the list of files but don't automatically
                   start tailing it. This can be specified more than once for
                   multiple disabled files or comma/semi-comma seperated

  -v --verbose       Show more detailed option
     --VERSION       Prints the version information
     --help          Prints this help information
     --man           Prints the full documentation for tailt

 PLUGIN OPTIONS:
  -h --highlight   Sets up the hightlight plugin options
  -m --match       Sets up the match plugin option to only show lines that natch
                   the regexp.
  -i --ignore      Sets up the ignore plugin options to hide all lines that
                   match the regexp.
  -r --replace     Sets op the replace plugin option which chnages match values.
     --spacing key=value

=head1 DESCRIPTION

=head2 Files

You can specify local files either relatively or absolutely. Remote files uses
a vim like syntax for specifying remote files, it uses the ssh protocol which
may mean that you may have issues if you don't use ssh keys. The format for
the URI is:

 ssh://[user@]host[:port]/(home/relative/file|/absolute/file)

Note if you want a absolute file location you must have two slashes at the
start of the path. One slash means that the file is relative to the user
that you are logging in as.

=head1 SUBROUTINES/METHODS

=head1 DIAGNOSTICS

=head1 CONFIGURATION AND ENVIRONMENT

=over 4

=item ~/.tailtrc

Stores the saved configuration options (stored in YAML format)

=back

=head1 DEPENDENCIES

=head1 INCOMPATIBILITIES

=head1 BUGS AND LIMITATIONS

There are no known bugs in this module.

Please report problems to Ivan Wills (ivan.wills@gamil.com).

Patches are welcome.

=head1 AUTHOR

Ivan Wills - (ivan.wills@gamil.com)

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2010 Ivan Wills (14 Mullion Close, Hornsby Heights, NSW, Australia, 2077).
All rights reserved.

This module is free software; you can redistribute it and/or modify it under
the same terms as Perl itself. See L<perlartistic>.  This program is
distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.

=cut
