The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#  You may distribute under the terms of either the GNU General Public License
#  or the Artistic License (the same terms as Perl itself)
#
#  (C) Paul Evans, 2014 -- leonerd@leonerd.org.uk

package Tickit::Console::Tab;

use strict;
use warnings;
use 5.010; # //
use base qw( Tickit::Widget::Tabbed::Tab );

our $VERSION = '0.07';

use Tickit::Widget::Scroller::Item::Text;
use Tickit::Widget::Scroller::Item::RichText;

use String::Tagged 0.10;

use POSIX ();
use Scalar::Util qw( blessed );

=head1 NAME

C<Tickit::Console::Tab> - represent a single tab on a C<Tickit::Console>

=head1 DESCRIPTION

Objects in this class represent a single switchable tab within a
L<Tickit::Console>. They are not constructed directly, but instead are
returned by the C<add_tab> method of the underlying C<Tickit::Console> object.

=cut

=head1 PARAMETERS

The following extra parameters may be passed to the constructor, or via the
C<add_tab> method on the C<Tickit::Console> object:

=over 8

=item timestamp_format => STRING or String::Tagged

If defined, every line is prefixed with a timestamp built by applying the
C<POSIX::strftime> function to this string. If a L<String::Tagged> instance is
applied it will preserve all the formatting from it.

=item datestamp_format => STRING or String::Tagged

If defined, every time a line is added to the buffer, if it starts a new day
since the previous message (because the format yields a different string),
this message is added as well to the scroller.

=back

=cut

sub new
{
   my $class = shift;
   my ( $tabbed, %args ) = @_;

   my $self = $class->SUPER::new( @_ );

   $self->{timestamp_format} = $args{timestamp_format};
   $self->{datestamp_format} = $args{datestamp_format};

   return $self;
}

=head1 METHODS

=cut

=head2 $name = $tab->name

=head2 $tab->set_name( $name )

Returns or sets the tab name text

=cut

sub name
{
   my $self = shift;
   return $self->label;
}

sub set_name
{
   my $self = shift;
   my ( $name ) = @_;
   $self->set_label( $name );
}

=head2 $tab->append_line( $string, %opts )

Appends a line of text to the tab. C<$string> may either be a plain perl
string, or an instance of L<String::Tagged> containing formatting tags, as
specified by L<Tickit::Widget::Scroller>. Options will be passed to the
L<Tickit::Widget::Scroller::Item::Line> used to contain the string.

Also recognises the following options:

=over 8

=item time => NUM

Overrides the epoch C<time()> value used to generate a timestamp for this line

=item timestamp_format => STRING or String::Tagged

Overrides the stored format for generating a timestamp string.

=item datestamp_format => STRING or String::Tagged

Overrides the stored format for generating a datestamp string.

=back

=cut

sub strftime
{
   my ( $format, @t ) = @_;

   if( blessed $format and $format->isa( "String::Tagged" ) ) {
      my $fplain = $format->str;
      my $ret = String::Tagged->new;

      # Iterate format specifiers and other literal text
      foreach my $m ( $format->matches( qr/%[_0#^-]?[OE]?.|[^%]+/ ) ) {
         if( $m =~ m/^%/ ) {
            # Format specifier
            $ret->append_tagged( POSIX::strftime( $m, @t ),
               %{ $m->get_tags_at( 0 ) }
            );
         }
         else {
            # Literal
            $ret->append( $m );
         }
      }

      return $ret;
   }
   else {
      return POSIX::strftime( $format, @t );
   }
}

sub _make_item
{
   my ( $string, %opts ) = @_;

   if( blessed $string and $string->isa( "String::Tagged" ) ) {
      return Tickit::Widget::Scroller::Item::RichText->new( $string, %opts );
   }
   else {
      return Tickit::Widget::Scroller::Item::Text->new( $string, %opts );
   }
}

sub _make_item_with_timestamp
{
   my $self = shift;
   my ( $string, %opts ) = @_;

   if( my $timestamp_format = delete $opts{timestamp_format} // $self->{timestamp_format} ) {
      my $time = delete $opts{time} // time();
      my $timestamp = strftime( $timestamp_format, localtime $time );

      $string = $timestamp . $string;
   }

   return _make_item( $string, %opts );
}

sub append_line
{
   my $self = shift;
   my ( $string, %opts ) = @_;

   my $scroller = $self->{scroller};

   if( my $datestamp_format = delete $opts{datestamp_format} // $self->{datestamp_format} ) {
      my $time = $opts{time} //= time();
      my $plain = POSIX::strftime( $datestamp_format, my @t = localtime $time );

      if( ( $self->{dusk_datestamp} // "" ) ne $plain ) {
         my $datestamp = strftime( $datestamp_format, @t );
         $scroller->push( _make_item( $datestamp ) );

         $self->{dusk_datestamp} = $plain;
         $self->{dawn_datestamp} //= $plain;
      }
   }

   $scroller->push( $self->_make_item_with_timestamp( $string, %opts ) );
}

*add_line = \&append_line;

=head2 $tab->prepend_line( $string, %opts )

As C<append_line>, but prepends it at the beginning of the scroller.

=cut

sub prepend_line
{
   my $self = shift;
   my ( $string, %opts ) = @_;

   my $scroller = $self->{scroller};

   my $datestamp_item;
   if( my $datestamp_format = delete $opts{datestamp_format} // $self->{datestamp_format} ) {
      my $time = $opts{time} //= time();
      my $plain = POSIX::strftime( $datestamp_format, my @t = localtime $time );

      $scroller->shift if ( $self->{dawn_datestamp} // "" ) eq $plain;

      my $datestamp = strftime( $datestamp_format, @t );
      $datestamp_item = _make_item( $datestamp );

      $self->{dawn_datestamp} = $plain;
      $self->{dusk_datestamp} //= $plain;
   }

   $scroller->unshift( $self->_make_item_with_timestamp( $string, %opts ) );
   $scroller->unshift( $datestamp_item ) if $datestamp_item;
}

=head2 $tab->bind_key( $key, $code )

Installs a callback to invoke if the given key is pressed while this tab has
focus, overwriting any previous callback for the same key. The code block is
invoked as

 $result = $code->( $tab, $key )

If C<$code> is missing or C<undef>, any existing callback is removed.

This callback will be invoked before one defined on the console object itself,
if present. If it returns a false value, then the one on the console will be
invoked instead.

=cut

sub bind_key
{
   my $self = shift;
   my ( $key, $code ) = @_;

   my $console = $self->{console};

   if( not $self->{keybindings}{$key} and $code ) {
      $console->{keybindings}{$key}[1]++;
      $console->_update_key_binding( $key );
   }
   elsif( $self->{keybindings}{$key} and not $code ) {
      $console->{keybindings}{$key}[1]--;
      $console->_update_key_binding( $key );
   }

   $self->{keybindings}{$key} = $code;
}

=head1 AUTHOR

Paul Evans <leonerd@leonerd.org.uk>

=cut

0x55AA;