[Chapter 16] 16.2 Implementation
Author: Sriram Srinivasan
Date: 1999-07-02T01:39:32Z

#

| Chapter 16 GUI Example: Man Page Viewer |

## 16.2 Implementation

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`.

- Search facility: `search`.

- Screen layout: `create_ui`.

- 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)

- Deleting entire contents:

      $text->delete('1.0', 'end'); # From line 1, column 0, to end

- Creating and configuring a tag:

      $text->tagConfigure('search',
                          'foreground' => yellow, 'background' => 'red');

- Deleting a tag:

      $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);

### 16.2.1 Showing Man Pages

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();
    }

### 16.2.2 Searching for Text

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");
        }
    }

### 16.2.3 Screen Layout

`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).

16.1 man and perlman |  | 16.3 Resources