App-sdview-Output-Tickit

 view release on metacpan or  search on metacpan

lib/App/sdview/Output/Tickit.pm  view on Meta::CPAN


=item *

C<]> - scroll up to the previous item

=item *

C<PageDown> - scroll down half a page

=item *

C<Space> - scroll down a page

=item *

C<End> or C<< > >> - scroll to bottom

=item *

C<F9> - open the outline view popup. See Below.

=item *

C</> - start a regexp search in the document body. See Below.

=item

C<q> - exit

=back

=head2 Outline View

The outline view displays an overview of all the section headings in the
document.

Within the outline view, the mouse wheel will scroll the list, and clicking an
entry will jump directly to it, dismissing the view.

Typing text with the outline view open will filter it to just those headings
matching the typed text. Pressing the C<< <Enter> >> key will jump directly to
the first highlighted heading, again dismissing the view.

=head2 Regexp Searching

Typing into the main search box enters text that forms a (perl) regexp pattern
to be tested against the body text of the document. Each paragraph is tested
individually and all matches are highlighted. Pressing C<< <Enter> >> will
select the first match. Use the C<< <n> >> and C<< <p> >> keys to jump between
them. Press C<< <Escape> >> to clear the highlights. Press C<< <Alt-i> >> to
toggle case-insensitivity. Press C<< <Alt-w> >> to toggle whole-word matching.

=cut

# Override default output format
require App::sdview;
$App::sdview::DEFAULT_OUTPUT = "tickit"
   if $App::sdview::DEFAULT_OUTPUT eq "terminal" and -t STDOUT;

my @HIGHLIGHT_PEN = (
   fg => 16, # avoid bold-black
   bg => "magenta",
   b  => 1,
);

my @SELECT_PEN = (
   bg => "green",
);

field $t;
field $scroller;
field $outlinetree;
field @items;

# Related to searching
field $matchposstatic;
field $matchidx;
field @matches;

ADJUST
{
   # Lazy load all the Tickit modules in here

   require Tickit;
   require Tickit::Utils;

   $t = Tickit->new;
   $t->term->await_started( 0.050 );

   $t->bind_key( q => sub { $t->stop; } );

   require Tickit::Widget::Scroller;
   Tickit::Widget::Scroller->VERSION( '0.33' );
   $scroller = Tickit::Widget::Scroller->new;

   $scroller->set_gen_bottom_indicator(
      sub ( $scroller ) {
         # It feels better for progress if we claim the percentage of ofscreen
         # lines that are above the screen.
         my $lines_above = $scroller->lines_above;
         my $lines_total = $lines_above + $scroller->lines_below;
         return "" if $lines_total < 1;
         return sprintf( "%d of %d (%d%%)",
            $lines_above, $lines_total, 100 * $lines_above / $lines_total );
      }
   );

   # Ugh
   $scroller->set_style(
      '<Home>'      => "scroll_to_top",
      '<Space>'     => "scroll_down_page",
      '<Backspace>' => "scroll_up_page",
      '<End>'       => "scroll_to_bottom",
      '<<>'         => "scroll_to_top",
      '<>>'         => "scroll_to_bottom",
   );

   $outlinetree = App::sdview::Output::Tickit::_OutlineTree->new;
}

method output ( @paragraphs )

lib/App/sdview/Output/Tickit.pm  view on Meta::CPAN

            $item->apply_highlight( undef );
         }
         $self->select_and_jump( undef );
         undef @matches;
         $scroller->redraw;
      },
   );

   $t->run;
}

method select_and_jump ( $new_idx )
{
   if( defined $matchidx ) {
      $matches[$matchidx][2] = 0;
   }

   $matchidx = $new_idx;

   if( defined $matchidx ) {
      $matches[$matchidx][2] = 1;

      $matchposstatic->set_text( sprintf "%d of %d", $matchidx+1, scalar @matches );

      my ( $item, $pos ) = $matches[$matchidx]->@*;
      my $startline = $item->line_for_pos( $pos );

      $scroller->scroll_to_visible( $item, $startline, margin => 2 );
      $scroller->redraw; # to adjust highlights
   }
}

# Most paragraphs are handled in a uniform way
*output_head1 = \&_output_para;
*output_head2 = \&_output_para;
*output_head3 = \&_output_para;
*output_head4 = \&_output_para;

*output_plain = \&_output_para;

*output_verbatim = \&_output_para;

*output_table = \&_output_para;

*output_item = \&_output_para;

field $_nextblank;

method _output_para ( $para, %opts )
{
   my $margin = $opts{margin} // 0;
   my $leader = $opts{leader};
   my $indent = $opts{indent};

   $scroller->push( Tickit::Widget::Scroller::Item::Text->new( "" ) ) if $_nextblank;

   my %parastyle = App::sdview::Style->para_style( $para->type )->%*;


   my $itempen = Tickit::Pen->new;
   $itempen->chattr( b  => 1 ) if defined $parastyle{bold};
   $itempen->chattr( u  => 1 ) if defined $parastyle{under};
   $itempen->chattr( i  => 1 ) if defined $parastyle{italic};
   $itempen->chattr( af => 1 ) if defined $parastyle{monospace};

   $itempen->chattr( fg => $parastyle{fg}->as_xterm->index )
      if defined $parastyle{fg};
   $itempen->chattr( bg => $parastyle{bg}->as_xterm->index )
      if defined $parastyle{bg};

   $margin += ( $parastyle{margin} // 0 );
   $indent //= 0;

   if( defined $leader ) {
      my $leaderlen = Tickit::Utils::textwidth( $leader );

      my %leaderstyle = App::sdview::Style->para_style( "leader" )->%*;
      $leaderstyle{$_} and $leader->apply_tag( 0, $leaderlen, $_ => $leaderstyle{$_} )
         for qw( fg bg bold under italic monospace );
   }

   my $item;
   if( $para->type eq "verbatim" ) {
      # This handily deals with the paragraph bg too
      my $text = App::sdview::Style->convert_str( $para->text );

      $item = App::sdview::Output::Tickit::_FixedWidthItem->new(
         text         => $text,
         margin_left  => $margin + $indent,
         margin_right => 1,
         pen          => $itempen,
      );
   }
   elsif( $para->type eq "table" ) {
      $item = App::sdview::Output::Tickit::_TableItem->new(
         rows         => [ $para->rows ],
         margin_left  => $margin + $indent,
         margin_right => 1,
         pen          => $itempen,
      );
   }
   else {
      my $text = App::sdview::Style->convert_str( $para->text );

      $item = App::sdview::Output::Tickit::_ParagraphItem->new(
         text         => $text,
         leader       => $leader,
         indent       => $indent,
         margin_left  => $margin,
         margin_right => 1,
         pen          => $itempen,
      )
   }

   if( $para->type =~ m/^head(\d+)/ ) {
      my @lines = $para->text->split( qr/\n/ );
      my $level = $1;
      my $itemidx = $scroller->items;
      $outlinetree->add_item( "$lines[0]", $level, $itemidx );
   }

   push @items, $item;
   $scroller->push( $item );

   $_nextblank = !!$parastyle{blank_after};
}

method output_list_bullet ( $para, %opts ) { $self->_output_list( bullet => $para, %opts ); }
method output_list_number ( $para, %opts ) { $self->_output_list( number => $para, %opts ); }
method output_list_text   ( $para, %opts ) { $self->_output_list( text   => $para, %opts ); }

method _output_list ( $listtype, $para, %opts )
{
   my $n = $para->initial;

   my $margin = $opts{margin} // 0;
   $margin += App::sdview::Style->para_style( "list" )->{margin} // 0;

   foreach my $item ( $para->items ) {
      my $leader;
      if( $item->type eq "plain" ) {
         # plain paragraphs in list are treated like items with no leader
         $self->output_item( $item,
            # make sure not to double-count the margin
            margin => $margin - App::sdview::Style->para_style( "plain" )->{margin},
            indent => $para->indent,
         );
         next;
      }
      elsif( $item->type ne "item" ) {
         # non-items just stand as they are + indent
      }
      elsif( $listtype eq "bullet" ) {
         $leader = String::Tagged->new( "•" );
      }
      elsif( $listtype eq "number" ) {
         $leader = String::Tagged->from_sprintf( "%d.", $n++ );
      }
      elsif( $listtype eq "text" ) {
         $leader = App::sdview::Style->convert_str( $item->term );
      }

      my $code = $self->can( "output_" . ( $item->type =~ s/-/_/gr ) ) or
         die "TODO: Unhandled item type " . $item->type;

      $self->$code( $item,
         margin => $margin,
         indent => $para->indent,
         leader => $leader,
      );
   }
}

# Logic kindof stolen from T:W:Scroller::Item::(Rich)Text but modified

sub _convert_color_tag ($n, $v)
{
   return $n => $v->as_xterm->index;
}

my %convert_tags = (
   bold      => "b",
   under     => "u",
   italic    => "i",
   strike    => "strike",
   blink     => "blink",
   monospace => sub ($, $v) { "af" => ( $v ? 1 : 0 ) },
   reverse   => "rv",
   fg        => \&_convert_color_tag,
   bg        => \&_convert_color_tag,
);

class App::sdview::Output::Tickit::_ParagraphItem
   :strict(params)
{
   use Tickit::Utils qw( textwidth );

   field $_pen          :param = undef;
   field $_margin_left  :param = 0;
   field $_margin_right :param = 0;
   field $_indent       :param = 0;

   field $_leader;
   ADJUST :params ( :$leader = undef ) 
   {
      $_leader = $leader->clone( convert_tags => \%convert_tags ) if defined $leader;
   }

   field $_leaderlen;
   field $_has_leaderline      = 0;
   ADJUST {
      $_leaderlen = Tickit::Utils::textwidth( $_leader ) if defined $_leader;
      $_has_leaderline = 1 if $_leaderlen and $_leaderlen + 1 > $_indent;
   }

   field $_text;
   field @_chunks; # => [ $start, $end, $width, $is_softhyphen ]
   ADJUST :params ( :$text )
   {
      $_text = String::Tagged->new( "" );

      my $textplain = "$text";
      pos( $textplain ) = 0;

      while( pos( $textplain ) < length $textplain ) {
         $textplain =~ m/\G\s+/gc and next; # skip whitespace
         $textplain =~ m/\G\xAD/gc and
            $_chunks[-1][3] = 1, next;

         my $chunkstart = pos( $textplain );
         # Find the next chunk by ignoring NBSP
         $textplain =~ m/\G(?[ \S & !\xAD + \xA0 ])+/gc or last;
         my $chunklen = pos( $textplain ) - $chunkstart;

         $_text .= " " if @_chunks and !$_chunks[-1][3];

         my $chunk = $text->substr( $chunkstart, $chunklen )
            ->clone( convert_tags => \%convert_tags );
         my $chunkwidth = textwidth $chunk;

         my $pos = length $_text;

lib/App/sdview/Output/Tickit.pm  view on Meta::CPAN

      }

      $rb->vline_at( $rect->top, $rect->bottom, $rect->right-1,
         Tickit::RenderBuffer::LINE_SINGLE, undef, Tickit::RenderBuffer::CAP_BOTH );
   }

   method on_key ( $ev )
   {
      if( $ev->type eq "text" ) {
         $filter .= $ev->str;
         $self->_update_filter;
         return 1;
      }

      my $key = $ev->str;
      if( $key eq "Backspace" ) {
         substr( $filter, -1, 1 ) = "" if length $filter;
         $self->_update_filter;
      }
      elsif( $key eq "Enter" ) {
         my $item = $displayed_items[0];
         $filter = "";
         $self->_update_filter;
         $on_select_item->( itemidx => $item->itemidx );
      }
      else {
         return 0;
      }

      return 1;
   }

   method on_mouse ( $ev )
   {
      if( $ev->type eq "wheel" ) {
         $scrolloff += 5 if $ev->button eq "down";
         $scrolloff -= 5 if $ev->button eq "up";
         $self->_normalize_scrolloff;
         $self->redraw;
      }
      elsif( $ev->type eq "press" && $ev->button == 1 ) {
         if( my $item = $displayed_items[$ev->line + $scrolloff] ) {
            $on_select_item->( itemidx => $item->itemidx );
         }
      }

      return 1;
   }
}

class App::sdview::Output::Tickit::_SearchBox
   :isa(Tickit::Widget)
{
   use constant WIDGET_PEN_FROM_STYLE => 1;

   use constant CAN_FOCUS => 1;

   use Tickit::Style;
   style_definition base =>
      bg => "grey",
      fg => 16, # colour 16 is black but doesn't become grey on bold
      b  => 1,

      bad_fg => 1;

   method lines { 1 }
   method cols  { 1 }

   field $float :writer;

   field $leader = "Search: ";
   field $text = "";

   field $is_ignorecase;
   field $is_wholeword;

   field $ok = 1;

   field $matchcount = 0;
   method set_matchcount ( $_count ) { $matchcount = $_count; $self->redraw; }

   field $searchre;

   field $on_incremental :param;
   field $on_enter       :param;

   method show ()
   {
      $text = "";
      $float->show;
      $self->window->cursor_at( 0, length($leader) );
      $self->take_focus;
   }

   method dismiss ()
   {
      $float->hide;
   }

   method render_to_rb ( $rb, $rect )
   {
      $rb->eraserect( $rect );

      $rb->goto( 0, 0 );
      $rb->text( $leader );

      $rb->text( $text, $ok ? undef : $self->get_style_pen( "bad" ) );
      $self->window->cursor_at( 0, length($leader) + length($text) );

      my $counttext = sprintf " (%d)", $matchcount;
      if( $is_ignorecase ) {
         $rb->goto( 0, $self->window->right - 2 - length $counttext );
         $rb->text( "/i" );
      }
      if( $is_wholeword ) {
         $rb->goto( 0, $self->window->right - 4 - length $counttext );
         $rb->text( "W" );
      }
      $rb->goto( 0, $self->window->right - length $counttext );
      $rb->text( $counttext );
   }



( run in 1.345 second using v1.01-cache-2.11-cpan-cdf2f3d4e48 )