#!/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} = [()]; 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 ("", \&pick_word); @manstack = (); $mancurrent = -1; $top->Label (-text => "Show:")->pack (-side => "left"); $show = $top->Entry (-width => 19)->pack (-side => "left"); $show->bind ("", \&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 ("", \&search); $top->bind ("/", sub { $search->focus; }); $top->bind ("Meta/", 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 $_} ; # @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