perlman 's implementation can be logically divided into four chunks:
Formatting and displaying man pages in the text widget: the routines show_man and get_command_line .
Displaying the list of help topics available in every section. We will not look at this particular piece of functionality, because it does not have much user interface code.
Before we barrel into each of the subroutines mentioned above, let us briefly study all the capabilities of the text widget used by perlman :
Inserting text at end, and marking it with a tag ("section"):
$text->insert('end', 'sample text', 'section');
Retrieving a stretch of text between two indices:
$line = $text->get($start_index, $end_index);
Ensuring that a particular index is visible:
$text->see($index)
$text->delete('1.0', 'end'); # From line 1, column 0, to end
Creating and configuring a tag:
$text->tagConfigure('search', 'foreground' => yellow, 'background' => 'red');
$text->tagDelete('search');
Applying a tag to a range of text, given an index position and number of characters:
$text->tagAdd('search', $current, "$current + $length char");
Listing all mark names and deleting each of them:
foreach $mark ( $text->markNames() ) { $text->markUnset($mark); }
Getting the line and column number (the index) from logical positions:
# row and col of current end position $index = $text->index('end'); # go to current insert position, then to the beginning of the word # and report the line and column $start_index = $text->index('insert wordstart'); # Go to 10th row, column 3, advance 5 chars, and report the new # row and column $i = $text->index("10.3 + 5 char");
Note that the index method does not change the state of the widget.
Doing an exact or regular expression search, specifying where to start and where to end:
$current = $text->search('-count' => \$length, '-regex', '-nocase','--', # search options $search_pattern, $current, 'end'); # from , to
The search method returns the index of the text at which the search succeeds and sets the variable associated with the -count property. It returns undef if the search failed.
Binding a mouse double-click to a subroutine:
$text->bind('<Double-1>', \&pick_word);
Let us dive into the meat of the application, the procedure show_man . As can be seen in Figure 16.1 , an entry widget labeled "Show:" accepts a topic name. When the user types text into this widget, $show , and hits the Return key, show_man is called. This procedure fetches the string from $show and calls get_command_line to construct a command pipeline to read from man (for open 's purposes). It then reads this pipe a line at a time and examines the line to see whether it is a likely heading (such as "NAME" or "DESCRIPTION"). Headings in man pages are typically in all caps and start from the first column. If a line looks like a heading, show_man inserts the line with a tag called "section"; otherwise, it inserts it as regular untagged text. The "section" tag is preconfigured with a larger size font. In addition, show_man appends a new entry to the "Headings" menu and arranges the callback associated with this entry to scroll the text widget to the line containing that section header.
sub show_man { my $entry = $show->get(); # get entry from $show # $entry can be something like 'csh', or 'csh(1)' 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 # Insert 'working' message; use the 'section' tag because # it has nice large fonts. $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 (defined($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 section heading 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); }
get_command_line takes the name of a man page and an optional section and returns an appropriate command line that can be used for the open command. Different systems might need different command lines, and the following listing shows the command line for Solaris. Since man (actually, nroff ) formats the page for a terminal (inserting escape sequences to show words in bold and headers and footers for every page), we use a freely available utility called rman ("RosettaMan"; see the Section 16.3 " section at the end of this chapter) to filter out this noise.
sub get_command_line { my ($man, $section) = @_; if ($section) { $section =~ s/[()]//g; # remove parens return "man -s $section $man 2> /dev/null | rman |"; } else { return "man $man 2> /dev/null | rman |"; } }
The pick_word procedure is called when you double-click on the text widget. It uses the index method to compute the index of the beginning of the word clicked on, and that of the end of the line, and extracts this range of text. pick_word then looks for an ordinary string (the topic), followed by an optional string within parentheses (the section). Before invoking show_man , it inserts this string into the entry widget, $show , thus pretending to be a user who has typed in that text.
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(); }
The menu bar contains a search menu exactly as described in the example under "Menus" in Section 14.1, "Introduction to GUIs, Tk, and Perl/Tk" . When the "Find" menu item is selected, the subroutine search is called. It first calls tagDelete to remove all highlights (which may be present from a previous search). Then it starts from the top (line 1, column 0) and invokes the widget's search method to find the first piece of matching text. When a match is found, this method updates the variable supplied to the -count parameter with the length of the matched text. This stretch of text is then highlighted using a tag called "search." The cursor is advanced beyond the matching text, and the search is resumed.
sub search { my $search_pattern = $search->get(); $text->tagDelete('search'); # Removing the tag restores the # associated regions of text to their # default style $text->tagConfigure('search', '-background' => 'yellow', '-foreground' => 'red'); my $current = '1.0';# Start at line 1, column 0 (beginning of file) 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); # Tag the matching text range with the tag name 'search' $text->tagAdd('search', $current, "$current + $length char"); # Move the cursor ahead, and continue searching $current = $text->index("$current + $length char"); } }
create_ui sets up the simple user interface. Pay particular attention to the padding options given to pack and the event bindings set up on the text and entry widgets.
sub create_ui { my $top = MainWindow->new(); #----------------------------------------------------------------- # Create menus #----------------------------------------------------------------- # 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]); } #----------------------------------------------------------------- # Create and configure text, and show and search entry widgets #----------------------------------------------------------------- $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'); # Used xlsfonts(1) for this font spec. $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); }
Please take a look at the file perlman.pl , packaged with the rest of this book's software and available from O'Reilly's FTP site. You can, if you wish, make a few valuable (and simple) additions to this utility: Add caching of formatted manual pages and the ability to show all man pages for a given topic name (not just the first one in MANPATH).