The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/opt/bin/perl
use strict;
#--------------------------------------------------------------
# perlman: man page viewer in Perl
#--------------------------------------------------------------
use Tk;

print STDERR "Scouting man directories\n";
scout_man_dirs();
print STDERR "Starting UI ...";
create_ui();
print STDERR "Done \n";
MainLoop();
exit(0);

#-------------------------------------------------------------------
my $menu_headings;        # "Headings" MenuButton
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 %sections;             # Maps section ('1', '3' ,'3n' etc.)
                          #  to list of topics in that section

sub show_man {
    my $entry = $show->get();   # get entry from $show
    my ($man, $section) = ($entry =~ /^(\w+)(\(.*\))?/);
    if ($section && (!is_valid_section($section))) {
        undef $section ;
    }
    my $cmd_line = get_command_line($man, $section); # used by open

    # Erase everything to do with current page (contents, menus, marks)
    $text->delete('1.0', 'end');  # erase current page
    $text->insert('end', "Formatting \"$man\" .. please wait", 'section');
    $text->update();                  # Flush changes to text widget
    $menu_headings->menu()->delete(0,'end'); # Delete current headings
    my $mark;
    foreach $mark ($text->markNames) {  # remove all marks 
        $text->markUnset($mark);
    }

    # UI is clean now. Open the file
    if (!open (F, $cmd_line)) {
        # Use the text widget for error messages 
        $text->insert('end', "\nError in running man or rman");
        $text->update();
        return;
    }
    # Erase the "Formatting $man ..." message
    $text->delete('1.0', 'end');
    my $lines_added = 0; my $line;
    
    while ($line = <F>) {
        $lines_added = 1;
        # If first character is a capital letter, it's likely a section
        if ($line =~ /^[A-Z]/) {  
            # Likely a section heading
            ($mark = $line) =~ s/\s.*$//g;  # $mark has section title
            my $index = $text->index('end');# note current end location
            # Give 'section' tag to the section title
            $text->insert('end', "$mark\n\n", 'section');
            # Create a menu entry. Have callback invoke text widget's
            # 'see' method to go to the index noted above
            $menu_headings->command(
                    '-label' => $mark,
                    '-command' => [sub {$text->see($_[0])},$index])
        } else {
            $text->insert('end', $line); # Ordinary text. Just insert.
        }
    }
    if ( ! $lines_added ) {
        $text->insert('end', "Sorry. No information found on $man");
    }
    close(F);
}

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

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->separator();
    $menu_file->command('-label' => 'Quit', '-command' => sub {exit(0)});

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

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

    # 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
    my $section_name;
    foreach $section_name (sort keys %sections) {
        $menu_sections->command (
                 '-label' => "($section_name)",
                 '-command' => [\&show_section_contents, $section_name]);
    }
    
    # TEXT STUFF

    $text = $top->Text ('-width' =>  80, 
                        '-height' => 40)->pack();
    $text->tagConfigure('section', 
                        '-font' => '-adobe-helvetica-bold-r-normal--14-140-75-75-p-82-iso8859-1');
    $text->bind('<Double-1>', \&pick_word);
    $top->Label('-text' => 'Show:')->pack('-side' => 'left');

    $show = $top->Entry ('-width'   =>  20,
                         )->pack('-side' => 'left');
    $show->bind('<KeyPress-Return>', \&show_man);

    $top->Label('-text' => 'Search:'
                )->pack('-side' => 'left', '-padx' => 10);
    $search = $top->Entry ('-width' => 20,
                           )->pack('-side' => 'left');
    $search->bind('<KeyPress-Return>', \&search);
}

sub is_valid_section {
    my $section= $_[0];
    return 0 unless $section =~ /\((.*?)\)/;
    my $section = $1;
    my $s;
    foreach $s (keys %sections) {
        if (lc($s) eq lc($section)) {
            return 1;
        }
    }
    0;
}

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 =~ /^(\w+)(\(.*?\))?/); 
    return unless $page;
    $show->delete('0', 'end');
    if ($section && is_valid_section($section)) {
        $show->insert('end', "$page${section}");
    } else {
        $show->insert('end', $page);
    }
    show_man();
}

sub show_section_contents {
    my $current_section = $_[0];
    $text->delete('1.0', 'end');
    $menu_headings->menu()->delete(0,'end');
    my ($i, $len);
    return unless exists $sections{$current_section};
    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;
        }
    }
}

sub search {
    my $search_pattern = $search->get();
    $text->tagDelete('search');
    $text->tagConfigure('search', 
                        '-background' => 'yellow', 
                        '-foreground' => 'red');

    my $current = '1.0'; my $length = '0';
    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');
        }
        last if (!$current);
        $text->tagAdd('search', $current, "$current + $length char");
        $current = $text->index("$current + $length char");
    }
}


use Cwd;
sub scout_man_dirs {
    my (@man_dirs,$man_dir, $section);
    if ($ENV{MANPATH}) {
        @man_dirs = split (/:/, $ENV{MANPATH});
    } else {
        push (@man_dirs, "/usr/man");
    }
    # Convert all relative man paths to fully qualified ones, by
    # prepending with $cwd
    my $cwd = cwd();
    foreach $man_dir (@man_dirs) {
        next if ($man_dir =~ m|^/|);
        $man_dir = "$cwd/$man_dir"; # 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*>;
        my $section_dir;
        # @section_dirs has man1, man2, man3s etc.
        foreach $section_dir (@section_dirs) {
            chdir $section_dir || next;
            ($section = $section_dir) =~ s/^man//;
            push (@{$sections{$section}}, <*.$section>);
            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;
    }
}