#!/pro/bin/perl

# perlman: man page viewer in Perl
#       Derived from examples of the "Advanced Perl Programming" book
#       from O'Reilly written by Sriram Srinivasan

$version = "1.02";

# Changes by H.Merijn Brand:
#       + Auto background
#       + Added optional scrollbars to ROtext (a.o.t. Text)
#       + Use argument as default man-page
#       + Quit button
#       + Man-page file caching to reduce startup time
#       + Removed rman, implementations inlined to enable underline and bold
#       + Search auto-see, search next and -prev buttons w/ balloons
#       + Man-page stack / History and buttons w/ balloons
#       + Man-page page cache
#       + PostScript (pre-pre-alpha)

use strict;
use GDBM_File;
use Fcntl;
use Cwd;
use Tk;

scan_man_dirs ();
print STDERR "Starting UI\n";
to_background ();
create_ui ();

def_show (shift);

MainLoop ();
exit 0;

#-------------------------------------------------------------------

sub to_background
{
   my $pid = fork;
   if ($pid < 0) {
       print STDERR "Unable to run in the background, cannot fork: $!\n";
       exit $?;
       }
   $pid && exit 0;
   } # to_background

my $menu_headings;      # "Headings" MenuButton
my $menu_stack;         # "History" MenuButton
my @manstack;           # Manpage history
my %manpage;            # Manpage cache
my $mancurrent;         # index to current manpage in history
my $ignore_case;        # 1 if check-button on in Search menu
my $match_type;         # -regexp or -exact.
my $text;               # Main text widget
my $show;               # "Show" entry widget
my $search;             # "Search" entry widget
my $nextSearch;         # "Search Next" button to bind sub
my $prevSearch;         # "Search Prev" button to bind sub
my %sections;           # Maps section ("1", "3" ,"3n" etc.)
                       #  to list of topics in that section
my %manfile;            # Holds the physical file for man page

sub def_show
{
   my $man = shift || return;
   $show->insert ("end", $man);
   show_man ();
   } # def_show

sub show_man
{
   my $entry = $show->get ();   # get entry from $show
   my ($man, $section) = ($entry =~ m/^([\w:]+)(\(.*\))?/);
   $man =~ m/\S/ || return;
   if ($section && (!is_valid_section ($section))) {
       undef $section;
       }

   # Erase everything to do with current page (contents, menus, marks)
   $text->delete ("1.0", "end");  # erase current page
   $text->insert ("end", "Getting \"$man\" .. please wait", "sct");
   $text->update ();

   $menu_headings->menu ()->delete (0, "end");

   my $mark;
   foreach $mark ($text->markNames) {  # remove all marks
       $text->markUnset ($mark);
       }

   # UI is clean now. Get the man page
   $text->configure (-cursor => "watch");
   if (exists $manpage{$entry}) {
       $text->insert ("end", " (Cached)");
       $text->update ();
       }
   else {
       $text->insert ("end", " Formatting ...");
       $text->update ();
       my $cmd_line = get_command_line ($man, $section); # used by open
       unless (open (F, $cmd_line)) {
           # Use the text widget for error messages
           $text->insert ("end", "\nError in running man");
           $text->update ();
           $text->configure (-cursor => "left_ptr");
           return;
           }
       $manpage{$entry} = [(<F>)];
       close F;
       }
   my @F = @{$manpage{$entry}};

   # Erase the "Formatting $man ..." message
   $text->delete ("1.0", "end");
   my $lines_added = 0;
   my $line;
   my $lead;
   my $prev = "";
   my $skip = 0;

   foreach my $line (@F) {
       $skip-- > 0 && next;
       unless ($lines_added) {
           $line =~ m/^\s*$/   && next;
           ($lead) = ($line =~ m/^(\s+)/);
           }
       $line =~ s/^$lead//;

       my $stripped = $line;
       chomp ($stripped);
       $stripped =~ s/\s+$//;
       1 while $stripped =~ s/.(.)/$1/;

       # Strip headers
       if ($stripped =~ m/^(\w+\(\d+\w*?\)|
                            user\s+contributed\s+perl\s+documentation|
                            perl\s+programmers\s+reference\s+guide|
                            gnu\s+tools
                            ) .*? \s\1
                          $
                         /ix) {
           $skip = 1;  # Next line allways junk or empty
           next;
           }
       $prev eq "" && $stripped =~ m/^\s*
                                      (\d{1,2}\s*[a-z]+|
                                       [a-z]+\s*\d{1,2},)
                                      \s*\d{4}
                                     $
                                    /ix && next;
       $prev eq "" && $stripped =~ m/^\s*
                                      perl\s*\d\.\d+
                                      (,\s*patch\s*\d+)?
                                     $
                                    /ix && next;
       # Strip footers
       $stripped =~ m/^[-\.,\w\s]+
                       \s-\s*\d+\s*-\s                 # Page number
                       [-\.,\w\s]+:[-,\w\s]+\s\d{4}    # Date
                      $
                     /x && next;                       # Footer

       # Squeeze multiple blank lines producing only one blank line.
       $stripped eq "" && $prev eq "" && next;

       $lines_added = 1;
       $prev = $stripped;
       if ($line =~ m/^[A-Z]/) {
           # Likely a section heading
           my $idx = $text->index ("end");
           $text->insert ("end", "$stripped\n\n", "sct");
           $menu_headings->command (
               -label   => $stripped,
               -command => [ sub { $text->see ($_[0])}, $idx ]);
           $menu_headings->update ();  # It might be teared off
           }
       else {
           # Underlining, Boldfacing and other unknown shit
           while ($line =~ s/(.)//) {
               my $o = $1;
               $text->insert ("end", $`);
               ($line = $') =~ s/^(.)//;
               if ($1 eq $o) {                 # Overstrike
                   $text->insert ("end", $1, "bd");
                   # $o can be any character
                   # 1 while s/^$o//; i.e. will fail on '/' and '\'
                   substr ($o, 0, 0) = "";
                   while (substr ($line, 0, 2) eq $o) {
                       substr ($line, 0, 2) = "";      # Multiple overstrike
                       }
                   }
               elsif ($o eq "_") {             # Underline
                   $text->insert ("end", $1, "ul");
                   }
               else {                          # NYI
                   $text->insert ("end", $1);
                   }
               }
           $text->insert ("end", $line);
           }
       }

   if ($lines_added) {
       unless ($mancurrent >= 0 && $manstack[$mancurrent] eq $entry) {
           $mancurrent++;
           $manstack[$mancurrent] eq $entry || splice @manstack, $mancurrent;
           $manstack[$mancurrent] = $entry;
           }
       }
   else {
       $text->insert ("end", "Sorry. No information found on $man$section");
       }

   $menu_stack->menu ()->delete (0, "end");
   foreach my $i (0 .. $#manstack) {
       $menu_stack->command (
           -label   => $manstack[$i],
           -command => sub {
               $mancurrent = $i;
               $show->delete ("0", "end");
               $show->insert ("end", $manstack[$i]);
               show_man ();
               });
       }
   $text->configure (-cursor => "left_ptr");
   } # show_man

sub show_ps
{
   my $entry = $show->get ();   # get entry from $show
   $entry =~ s/\((.*)\)$/.$1/;
   my $lst = $manfile{"$entry"} || return;
   my $file = $lst->[0] || return;     # Check for dups?
   # Still have to check if $file is gzipped, compressed and/or preformatted
   system "groff -man $file >/tmp/u.$<";
   # It shows, but after that, I lose control ....
   #my $xx = MainWindow->new ();
   #my $gs = $xx->Ghostscript ()->pack (-expand => "both");
   #$gs->Postscript (`cat /tmp/u.$<`);
   } # show_ps

sub get_command_line
{
   my ($man, $section) = @_; # Given topic and section, construct
                             # Unix command-line
   $section =~ s/[()]//g; # remove parens (will succeed in undef)
   return "man $section $man 2>/dev/null |";
   } # get_command_line

sub create_ui
{
   my $top = MainWindow->new ();

   # MENU STUFF

   # Menu bar
   my $menu_bar = $top->Frame ()->pack (-side => "top", -fill => "x");

   # File menu
   my $menu_file = $menu_bar->Menubutton (
       -text        => "File",
       -relief      => "raised",
       -borderwidth => 2)->pack (
           -side => "left",
           -padx => 2);
   $menu_file->command (
       -label => "Location",
       -command => sub {
           my $msg = MainWindow->new ();
           my $ok = sub { $msg->destroy };
           $msg->Button (
               -text    => "OK",
               -command => $ok)->pack (-side => "left");
           $msg->Message (
               -takefocus => 1,
               -aspect    => 10000,
               -text      => join (", ", @{$manfile{$show->get ()}}))->pack (-side => "right");
           $msg->after (15000, $ok);
           });
   $menu_file->command (-label => "Quit", -command => \&exit);

   # History/Sections Menu
   $menu_stack    = $menu_bar->Menubutton (
       -text        => "History",
       -relief      => "raised",
       -borderwidth => 2)->pack (
           -side => "left",
           -padx => 2);
   $menu_headings = $menu_bar->Menubutton (
       -text        => "Headings",
       -relief      => "raised",
       -borderwidth => 2)->pack (
           -side => "left",
           -padx => 2);

   #Search menu
   $match_type = "-regexp";
   $ignore_case = 1;
   my $search_mb = $menu_bar->Menubutton (
       -text        => "Search",
       -relief      => "raised",
       -borderwidth => 2)->pack (
           -side => "left",
           -padx => 2);

   # Regexp match
   $search_mb->radiobutton (
       -label    => "Regexp match",
       -value    => "-regexp",
       -variable => \$match_type);
   # Exact match
   $search_mb->radiobutton (
       -label    => "Exact match",
       -value    => "-exact",
       -variable => \$match_type);
   $search_mb->separator;
   # Ignore case
   $search_mb->checkbutton (
       -label    => "Ignore case?",
       -variable => \$ignore_case);

   #Sections Menu
   my $menu_sections = $menu_bar->Menubutton (
       -text        => "Sections",
       -relief      => "raised",
       -borderwidth => 2)->pack (
           -side => "left",
           -padx => 2);

   # Populate sections menu with keys of % sections
   foreach my $section_name (sort keys %sections) {
       $menu_sections->command (
           -label   => "($section_name)",
           -command => [\&show_section_contents, $section_name]);
       }

   $menu_bar->Button (
       -text        => "Quit",
       -relief      => "raised",
       -borderwidth => 2,
       -command     => \&exit)->pack (-side => "right");
   $menu_bar->Button (
       -text        => "PS",
       -relief      => "raised",
       -borderwidth => 2,
       -command     => \&show_ps)->pack (-side => "right");

   # TEXT STUFF
   $text = $top->Scrolled ("ROText",
       -scrollbars => "osoe",
       -width      => 80,
       -height     => 40)->pack ();
   $text->configure (-cursor => "left_ptr");
   # Dynamically determine the 'bold' font (users allways want their own)
   my $font = $text->cget (-font);
   my @font = split m/-/, $$font;
   $font[3] = "bold";
   my $boldfont = join "-", @font;
   # Use xterm-color settings for attributes if available
   my $colorUL = $text->cget (-foreground);
   my $colorBD = $colorUL;
   foreach my $xrdb (`xrdb -q`) {
       $xrdb =~ m/\*(colorBD|colorUL)\s*:\s*(\S+)/ || next;
       $1 eq "colorBD" ? $colorBD : $colorUL = $2;
       }
   $text->tagConfigure ("sct", -font => $boldfont, -foreground => $colorBD);
   $text->tagConfigure ("bd",  -font => $boldfont);
   $text->tagConfigure ("ul",  -underline => 1,    -foreground => $colorUL);
   $text->bind ("<Double-1>", \&pick_word);

   @manstack = ();
   $mancurrent = -1;
   $top->Label (-text => "Show:")->pack (-side => "left");
   $show = $top->Entry (-width => 19)->pack (-side => "left");
   $show->bind ("<KeyPress-Return>", \&show_man);
   $top->Balloon (
       -background => "LightYellow2")->attach (
           $top->Button (
               -font    => "spc08x14",
               -text    => "\x11",
               -command => sub {
                   $mancurrent > 0 || return;
                   show_current (--$mancurrent);
                   })->pack (-side => "left"),
           -balloonmsg => "Push to go back in the man page history");
   $top->Balloon (
       -background => "LightYellow2")->attach (
           $top->Button (
               -font    => "spc08x14",
               -text    => "\x10",
               -command => sub {
                   defined $manstack[$mancurrent + 1] || return;
                   show_current (++$mancurrent);
                   })->pack (-side => "left"),
           -balloonmsg => "Push to go forward in the man page history");

   $top->Label (-text => "Search:")->pack (-side => "left");
   $search = $top->Entry (-width => 20)->pack (-side => "left");
   $search->bind ("<KeyPress-Return>", \&search);
   $top->bind ("<Key>/", sub { $search->focus; });
   $top->bind ("Meta<Key>/", sub { $search->focus; });

   $top->Balloon (
       -background => "LightYellow2")->attach (
           $nextSearch = $top->Button (
               -font    => "spc08x14",
               -text    => "\x1E",
               -command => sub {})->pack (-side => "left"),
           -balloonmsg => "Search up/backwards (wrapped)");
   $top->Balloon (
       -background => "LightYellow2")->attach (
           $prevSearch = $top->Button (
               -font    => "spc08x14",
               -text    => "\x1F",
               -command => sub {})->pack (-side => "left"),
           -balloonmsg => "Search down/forewards (wrapped)");
   } # create_ui

sub show_current
{
   my $idx = shift;
   $show->delete ("0", "end");
   $show->insert ("end", $manstack[$idx]);
   show_man ();
   } # show_current

sub is_valid_section
{
   my $section= shift;
   $section =~ m/\((.*?)\)/ || return 0;
   $section = $1;
   my $s;
   foreach $s (keys %sections) {
       if (lc ($s) eq lc ($section)) {
           return 1;
           }
       }
   0;
   } # is_valid_section

sub pick_word
{
   my $start_index = $text->index ("insert wordstart");
   my $end_index = $text->index ("insert lineend");
   my $line = $text->get ($start_index, $end_index);
   my ($page, $section) = ($line =~ m/^([\w:]+)(\(.*?\))?/);
   $page || return ;
   $show->delete ("0", "end");
   if ($section && is_valid_section ($section)) {
       $show->insert ("end", "$page${section}");
       }
   else {
       $show->insert ("end", $page);
       }
   show_man ();
   } # pick_word

sub show_section_contents
{
   my $current_section = shift;
   $text->delete ("1.0", "end");
   $menu_headings->menu ()->delete (0, "end");
   my ($i, $len);
   exists $sections{$current_section} || return;
   my $spaces = " " x 40;
   my $words_in_line = 0;  # New line when this goes to three
   my $man;
   foreach $man (@{$sections{$current_section}}) {
       $text->insert ("end", $man . substr ($spaces, 0, 24 - length ($man)));
       if (++$words_in_line == 3) {
           $text->insert ("end", "\n");
           $words_in_line = 0;
           }
       }
   } #show_section_contents

sub search
{
   my $search_pattern = $search->get ();
   $text->tagDelete ("search");
   $text->tagConfigure ("search",
       -background => "Red4",
       -foreground => "Yellow");

   my $current = "1.0";
   my $length = "0";
   my @tags;
   while (1) {
       if ($ignore_case) {
           $current = $text->search (
               -count => \$length,
               $match_type,
               "-nocase",
               "--",
               $search_pattern,
               $current,
               "end");
           }
       else {
           $current = $text->search (
               -count => \$length,
               $match_type,
               "--",
               $search_pattern,
               $current,
               "end");
           }
       $current || last;
       push @tags, $current;
       $text->tagAdd ("search", $current, "$current + $length char");
       $current = $text->index ("$current + $length char");
       }
   my $searchIndex = 0;
   $text->see ($tags[0]);
   $nextSearch->configure (
       -command => sub {
           @tags &&
               $text->see ($tags[++$searchIndex % scalar @tags]);
           });
   $prevSearch->configure (
       -command => sub {
           @tags &&
               $text->see ($tags[(@tags + --$searchIndex) % scalar @tags]);
           });
   } # search

sub scan_man_dirs
{
   my (@man_dirs, $man_dir, $section, $section_dir, $file, $page);

   if ($ENV{"MANPATH"}) {
       @man_dirs = split m/:/, $ENV{"MANPATH"};
       }
   else {
       push (@man_dirs, "/usr/man");
       }

   my %manfiles;
   tie %manfiles, "GDBM_File", "/tmp/w.2man.manfiles", O_RDWR | O_CREAT, 0666;
   my %sectlist;
   tie %sectlist, "GDBM_File", "/tmp/w.2man.sectlist", O_RDWR | O_CREAT, 0666;

   if (exists $manfiles{"/usr/share/man/man1.Z/man.1"} ||      # HP-UX 10.20
       exists $manfiles{"/usr/man/man1/man.1.gz"}              # DEC Alpha OSF/1
       ) {
       print STDERR "Man pages are cached\n";
       foreach my $fullpath (keys %manfiles) {
           ($man_dir, $section_dir, $file) =
               ($fullpath =~ m:(.*)/([^/]+)/([^/]+)$:);
           ($page = $file) =~ s/\.(\d+\w?)(\.(Z|gz))?$//;
           push @{$manfile{"$page($1)"}}, $fullpath;
           push @{$manfile{$page}}, $fullpath;
           push @{$manfile{$file}}, $fullpath;
           }
       foreach $section (keys %sectlist) {
           $sections{$section} = [ split m:/:, $sectlist{$section} ];
           }
       untie %manfiles;
       untie %sectlist;
       return;
       }

   print STDERR "Scanning man directories\n";
   # Convert all relative man paths to fully qualified ones, by
   # prepending with $cwd
   my $cwd = cwd ();
   foreach (@man_dirs) {
       m:^/: || s:^:$cwd/:; # Modifies entry in man_dirs
       }
   foreach $man_dir (@man_dirs) {
       chdir $man_dir || next;
       # Now, in /usr/man, say. Get all the directories
       my @section_dirs = grep {-d $_} <man*>;
       # @section_dirs has cat1, cat1.Z, man1, man1n, man2, man3s etc.
       foreach $section_dir (@section_dirs) {
           chdir $section_dir || next;
           ($section = $section_dir) =~ s/^man//;
           local *DIR;
           opendir DIR, ".";
           foreach $file (grep -f $_ && -s _ && m/\.\d/, readdir DIR) {
               my $fullpath = "$man_dir/$section_dir/$file";
               push @{$sections{$section}}, $file;
               ($page = $file) =~ s/\.(\d+\w?)(\.(Z|gz))?$//;
               push @{$manfile{"$page($1)"}}, $fullpath;
               push @{$manfile{$page}}, $fullpath;
               push @{$manfile{$file}}, $fullpath;
               $manfiles{$fullpath} = $file;
               }
           closedir DIR;
           chdir "..";
           }
       chdir "..";
       }
   # All sections in all man pages have been slurped in. Remove duplicates
   foreach $section (keys %sections) {
       my @new_list;
       my %seen;
       @new_list = sort (grep (!$seen{$_}++, @{$sections{$section}}));
       # Change all entries like cc.1 to cc(1)
       foreach (@new_list) {
           $_ =~ s/[.](.*)/($section)/;
           }
       $sections{$section} = \@new_list;
       $sectlist{$section} = join "/", @new_list;
       }
   untie %manfiles;
   untie %sectlist;
   } # scan_man_dir