16.2 Implementationperlman 's implementation can be logically divided into four chunks:
Before we barrel into each of the subroutines mentioned above, let us briefly study all the capabilities of the text widget used by perlman :
16.2.1 Showing Man PagesLet 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(); } 16.2.2 Searching for TextThe 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"); } } 16.2.3 Screen Layoutcreate_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). |
|