home *** CD-ROM | disk | FTP | other *** search
/ OS/2 Shareware BBS: 14 Text / 14-Text.zip / EN0703.ZIP / ENV.703
Text File  |  1988-04-23  |  26KB  |  341 lines

  1.  
  2. Understanding the OS/2 Keyboard
  3.  
  4. Many of the OS/2 kernel functions for keyboard input will seem very familiar,  though some new facilities have been added.   The Presentation Manager keyboard and mouse inputs are a new story.
  5.  
  6.  
  7. From the viewpoint of an application program, the keyboard is not a particularly complex device.  The keyboard simply generates a stream of codes that the operating system stores in a circular buffer and then passes on to the program.  Consequently, the part of the operating system's application program interface (API) devoted to the keyboard is usually simple and straight-forward.  
  8.     The OS/2 kernel is no exception to this rule.  If you've worked with the keyboard in MS-DOS and with the PC BIOS, you'll find that the OS/2 keyboard interface are quite similar.
  9.     Under MS-DOS a program can obtain keyboard input or clear the keyboard buffer through several low-number MS-DOS functions calls (01h, 06h, 07h, 08h, 0Ah, 0Bh, and 0Ch).  These calls date back to DOS 1.0 and actually originated in CP/M.  DOS supports reading standard input through function call 3Fh ("Read from a file or device").  Standard input is normally keyboard input, but standard input can also be redirected so it comes from a file or from another program through a pipe.
  10.     Many application programs that run under DOS obtain keyboard input through Interrupt 16h, which is part of the PC BIOS.  Interrupt 16h has three function calls that let a program read the next key from the buffer, peek at the key without removing it from the buffer, and obtain the state of the shift and the toggle keys on the keyboard.  RAM-resident popups (and some application programs), on the other hand, obtain keyboard information directly from the hardware.  These programs intercept interrupt 09h and access the keyboard I/O ports.
  11.     OS/2 has facilities that duplicate and improve upon the functionality of all these DOS and BIOS keyboard functions.  One improvement I'm sure will be appreciated is in the size of the keyboard buffer.  The PC BIOS has a 15- key buffer; under OS/2 the buffer is expanded to 61 keys.
  12.  
  13. READING STANDARD INPUT  An OS/2 program can read standard input using the DosRead function.  In C, a call to DosRead looks like this:
  14.  
  15. DosRead (0, &Buffer, BufferLen, &BytesRead);
  16.  
  17. The first parameter of 0 means to read standard input, which is normally the keyboard.  The function reads up to BufferLen bytes into the area of memory pointed to by the Buffer address.  The last parameter is a pointer to a variable that receives the number of bytes actually read by the function.  
  18.     In general, however, DosRead is not a good function for simply obtaining keyboard information.  It is most valuable in programs designed to work with redirected standard input, such as the PAGE program presented later in this column.
  19.  
  20. READING KEYBOARD INPUT  The OS/2 functions devoted to the keyboard all begin with the letters Kbd.  The most important keyboard input function is KbdCharIn:
  21.  
  22. KbdCharIn (&KeyData, NoWaitFlag, 0) ;
  23.  
  24. This function obtains the next key from the keyboard buffer.  The function is roughly equivalent to a BIOS interrupt 16h call with AH equal to 0.  
  25.     The first parameter to KbdCharIn is a pointer to a structure of type KeyData.  For C programmers, this structure is defined in the SUBCALLS.H header file included with the June, 1987 release of the OS/2 Software Development Kit.  (The names and contents of these header files may be different in the OS/2 Software Development Kit expected out in 1988.)
  26.     The KeyData structure has six fields that OS/2 uses to return keyboard information to the program.  These fields are:
  27.  
  28. Field          Data Type
  29. -----          ---------
  30. char_code      unsigned char (1 byte)
  31. scan_code      unsigned char (1 byte)
  32. status         unsigned char (1 byte)
  33. nls_shift      unsigned char (1 byte)
  34. shift_state    unsigned int (2 bytes)
  35. time           unsigned long (4 bytes)
  36.  
  37. The char_code and scan_code fields are equivalent to the two codes returned in registers AL and AH when a DOS program calls interrupt 16h with AH equal to 0.  The char_code field is usually the ASCII character code of the key pressed and the scan_code field is the hardware scan code.  However, if the char_code field is 00h, then the key is a non-character key, such as a function key or a cursor movement key.  In that case, the scan_code field contains the extended keyboard code.  These extended codes are the same as those used by the PC BIOS under DOS.
  38.     The status and nls_shift fields are used for supporting double-byte character sets for some foreign-language keyboards.  ("NLS" stands for "national language support.")  These two fields are not yet documented well enough to make head or tail of how they work.
  39.     The 16-bit shift_state field provides the state of all the keyboard shift and toggle keys at the time the key was pressed.  The definition of the shift_state bits is shown in Figure 1.
  40.     There seems to be a little confusion about the time field in the KeyData structure.  The pre-release documentation accompanying the OS/2 Software Development Kit indicates that the four bytes give the time in terms of an hour, minute, second, and hundredths of a second.  However, under the beta version of the OS/2 Kernel, the time field reports an elapsed time in milliseconds between the time the system was booted and the time the key was pressed.
  41.     A program can use the time field to determine how long the keystroke has been waiting in the keyboard buffer.  The current elapsed time (measured from the system boot) is stored in the "global information segment," the address of which is available from the DosGetInfoSeg function.
  42.     The second parameter to KbdCharIn is called the "no-wait flag."  Normally, the KbdCharIn function waits for a keystroke if the keyboard buffer is currently empty.  The function does not return control to the program unless it returns the next key.  However, if you set the no-wait flag to 1, then KbdCharIn returns immediately even if the keyboard buffer is empty.  If both the char_code and scan_code fields are set to 0, then no keystroke was available from the buffer.  This parameter has important implications for multitasking, as I'll discuss later in this column.
  43.     The last parameter to KbdCharIn is a "keyboard handle."  Under the OS/2 Kernel, this must be set to 0 .
  44.     Like the PC BIOS interrupt 16h, KbdCharIn returns only key presses, not key releases.  The key is not echoed to the screen.  The toggle keys and shift keys do not generate key codes that are stored in the buffer.  Instead,  you obtain the current status of these keys from the shift_state field of the KeyData structure.  The Insert key is an exception:  it generates an extended keyboard code and affects the shift_state word.
  45.  
  46. OTHER KEYBOARD FUNCTIONS  If a program wants to look at the next keystroke in the keyboard buffer without removing it, the KbdPeek function can be used instead of KbdCharIn:
  47.  
  48. KbdPeek (&KeyData, 0) ;
  49.  
  50. The char_code and scan_code fields of the KeyData structure are set to 0 if no key is waiting in the buffer.  This function replaces the BIOS interrupt 16h call with AH equal to 1.  
  51.     A program can clear the keyboard buffer by calling:
  52.  
  53. KbdFlushBuffer (0) ;
  54.  
  55. The single parameter is the keyboard handle, which must be set to 0.
  56.     A program can obtain the state of the toggle and shift keys by a call to KbdGetStatus.  The state of the toggle keys can be changed using KbdSetStatus.  (Under DOS, changing the state of the toggle keys requires the program to access the BIOS data area directly.)
  57.     The KbdStringIn function is useful for obtaining a string of characters from the keyboard:
  58.  
  59. KbdStringIn (&Buffer, &KbdStringInLength, NoWaitFlag, 0) ;
  60.  
  61. This function replaces the DOS function call 0Ah.  The second parameter is a pointer to a structure of type KbdStringInLength.  It contains an input length on entry to the function,  and it contains the number of bytes entered by the user on return to the program.  During the KbdStringIn call the normal DOS editing keys (F3 and so forth) can be used to edit text already in the buffer.
  62.     Like DOS, OS/2 allows the redefinition of keys using ANSI control sequences.  These keyboard redefinitions are recognized only by the DosRead and KbdStringIn functions.  But don't assume that KbdStringIn reads standard input.  KbdStringIn always reads the keyboard regardless of the redirection of standard input.
  63.     In one sense, DosRead and KbdStringIn play a unique role in keyboard handling.  It is analogous to the role of DosWrite and VioWrtTTY in screen output.  This is summarized in Figure 2.
  64.  
  65. THE PAGE PROGRAM  Let's now look at a simple example of DosRead and KbdCharIn in a program that uses both functions.
  66.     The PAGE.C program shown in Figure 3 is similar to the DOS (and OS/2) MORE program.  It displays standard input one screenful at a time.  However, unlike the teletype output of MORE, PAGE uses the OS/2 Vio functions to pop each page to the full screen.
  67.     Using the C compiler included with the beta version of the OS/2 Software Development Kit, you can compile and link PAGE with the following command:
  68.  
  69. CL -G2 -Zp PAGE.C
  70.  
  71.     You can use PAGE to look at the contents of a large file thus:
  72.  
  73. PAGE <filename
  74.  
  75. Or you can display standard output originating from another program through a pipe.  For example, if you have a long directory list, you can look at it a screen at a time with
  76.  
  77. DIR | PAGE
  78.  
  79. PAGE is easy to use.  Pressing any key goes to the next page.  The Escape key exits.
  80.     PAGE first determines whether standard input is coming from a source other than the keyboard by calling DosQHandType ("query handle type").  If standard input is coming from the keyboard, PAGE exits with an error message.  Otherwise, the program saves the initial contents of the screen with VioReadCellStr and later restores it with VioWrtCellStr.  PAGE writes to the screen using the OS/2 virtual screen buffer.  The VioGetBuf function obtains a selector (segment address) for this buffer.  The program then stores its screen output in the buffer and updates the screen through a call to VioShowBuf.
  81.     PAGE reads standard input using DosRead with a first parameter of 0.  PAGE reads the keyboard with KbdCharIn.  The keyboard handling is very simple.  When KbdCharIn returns with the next key, PAGE checks only if the char_code field of the KeyData structure is «PT3»\x1B«PT2» (the Escape key).  This causes PAGE to terminate.  PAGE also terminates when it runs out of standard input to display.  This is indicated by a zero value for BytesRead following the call to DosRead.
  82.  
  83. OS/2 PIPES  Piping of standard output from one program to standard input of another program is significantly different in OS/2.  In DOS, if you run the command 
  84.  
  85. DIR | MORE
  86.  
  87. DOS first creates a temporary file for the output from the DIR command.  Standard output from DIR is redirected to this file.  When the DIR command is finished, DOS closes the file.  DOS then runs the MORE program.  The standard input to MORE is redirected from the file.  When MORE is finished, DOS deletes the temporary file.
  88.     Under OS/2, pipes are memory blocks rather than files.  As you would expect, this speeds up piping considerably.  
  89.     Even more important, the programs on each side of the pipe run simultaneously.  While the DIR command is writing its standard output to the pipe, MORE (or PAGE) can read its standard input from the pipe.  This means that PAGE does not have to wait for the DIR command (or whatever) to finish before it displays the first screenful of standard input to the screen.
  90.     You'll notice that near the end of PAGE.C (just before the program terminates) PAGE continues reading standard input using DosRead until the BytesRead variable is zero.  An earlier version of PAGE did not have this code.  With this earlier version if I executed:
  91.  
  92. DIR | PAGE
  93.  
  94. for a long directory and ended PAGE by pressing the Escape key, the DIR command would give me an error message that the disk was full.  What DIR really meant was that the pipe was broken because PAGE stopped reading DIR's output.  Adding the two lines of code at the end of PAGE.C fixed this problem.
  95.  
  96. KEYSTROKES AND MULTITASKING  
  97.     Now for a lecture.
  98.     There's been a lot of disinformation about OS/2 circulated in recent months.  Most of this stuff apparently orginates with the CEOs of companies who compete with Microsoft in the applications and languages market.  Lazy press people who have never run OS/2 and don't know any better pass these falsehoods on to the public.
  99.     One quite persistent piece of disinformation concerns multitasking.  It is said that if you have two programs running under OS/2, they'll both run at half speed.  After all, OS/2 must continually switch between the two programs, so each program gets only half the microprocessor time it got under DOS.  Seems obvious, doesn't it?
  100.     Well, no.
  101.     It is obvious only if both programs are actually doing something at the same time, such as recalculating a spreadsheet, running a spelling check, or sorting a database.  However, most programs spend much of their time doing nothing except waiting for the next keystroke from the user.
  102.     If an OS/2 program is running in a background screen group and is waiting for a keystroke with a call to KbdCharIn or KbdStringIn, then that program implicitly forfeits its normal time slice.  Because the program is not going to get a keystroke until the screen group is moved to the foreground there's no reason for the program to eat up valuable time doing nothing.  OS/2 knows this and functions accordingly.  OS/2 is simply not as stupid as some people think.  I wouldn't be wasting my time learning about OS/2 and writing about OS/2 if it were.
  103.     Anybody who's spent time with OS/2 knows this:  You can have a bunch of screen groups active, each running a program, and if each of these programs is waiting for a keystroke, there is no significant speed degradation.  
  104.     However, in order for OS/2 to work this way, it is important that application programs call KbdCharIn with the no-wait flag set to 0.  This allows OS/2 to recognize that it should not give the program a normal time slice if the keyboard buffer is empty.  If a program instead sits in a loop and continually calls KbdCharIn with the no-wait flag parameter to 1, then the program will indeed get a normal time slice and it will slow down the whole system.
  105.     But why would a programmer want to set the no-wait flag to 1 anyway?  Well, suppose the program used both keyboard input and mouse input.  An OS/2 program obtains mouse events (movement of the mouse or mouse button depressions) by calling the MouReadEventQue function.  This function has its own no-wait flag parameter.  Setting this flag to 1 causes MouReadEventQue to return control to the program even if the mouse queue is empty.
  106.     One way to handle both keyboard and mouse input is to alternate between reading the keyboard with KbdCharIn and reading the mouse queue with MouReadEventQue.  The no-wait flag in both functions is set to 1 so the program won't miss mouse input while it's hung up in a KbdCharIn call and won't miss keyboard input while waiting for MouReadEventQue to return.
  107.     While such a method sounds reasonable, it's wrong, wrong, wrong!  A program that does this will slow down the whole system even when the program is running in a background screen group and cannot possibly get keyboard or mouse input.  And I promise you that any commercial OS/2 program we see that works in this way will be classified personally by me as an "Editor's Reject." 
  108.     But now we apparently have a problem.  What recourse does a program have when it must read both keyboard and mouse input?  
  109.     Simple.  The program reads the keyboard and mouse from two separate threads of execution.  These two threads run simultaneously.  In both the KbdCharIn and MouReadEventQue calls, the no-wait parameter is set to 0.  This ensures that both threads give up their time slices when the program is running in a background screen group.
  110.  
  111. THE PRESENTATION MANAGER  The various Kbd functions are designed for programs written for the OS/2 Kernel.  They are not used by programs written for the OS/2 Presentation Manager.
  112.     Instead, Presentation Manager programs receive keyboard input in the form of "messages."  These messages are stored in the program's message queue along with other messages such as those relating to mouse input.  Under the Presentation Manager, a program does not need to use separate threads of execution for processing keyboard and mouse input because both forms of input are stored in the same queue.
  113.     A Presentation Manager program obtains more information about keystrokes than does an OS/2 Kernel program.  In particular, every keyboard event (key releases as well as depressions) is reported to the program.  Presentation Manager programs also obtain key combinations that are ignored by the OS/2 Kernel, such as the Ctrl key in combination with the period.
  114.  
  115. COMING UP: KEYBOARD MONITORS  So far we've seen how OS/2 duplicates the functionality of the DOS interrupt 21h and BIOS interrupt 16h keyboard functions.  However, we haven't seen anything yet that lets a program intercept keyboard information in the same way that a DOS RAM-Resident programs does with interrupt 09h.
  116.     Is such a thing possible under OS/2?  It sure is.  In fact, unlike DOS, OS/2 has a documented, built-in facility that lets a program intercept and (optionally) alter keyboard input before it gets to other programs.
  117.     This is certainly an important topic, which is why I'll devote all of next issue's Environments columns to the subject of "keyboard monitors."
  118.  
  119.  
  120. FIGURES:
  121.  
  122.      
  123.      +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  124.      |15|14|13|12|11|10| 9| 8| 7| 6| 5| 4| 3| 2| 1| 0|
  125.      +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  126.        |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
  127.        |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  +--- Right SHIFT down
  128.        |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
  129.        |  |  |  |  |  |  |  |  |  |  |  |  |  |  +------ Left SHIFT down
  130.        |  |  |  |  |  |  |  |  |  |  |  |  |  |
  131.        |  |  |  |  |  |  |  |  |  |  |  |  |  +--------- Either CTRL down
  132.        |  |  |  |  |  |  |  |  |  |  |  |  |
  133.        |  |  |  |  |  |  |  |  |  |  |  |  +------------ Either CTRL down
  134.        |  |  |  |  |  |  |  |  |  |  |  |
  135.        |  |  |  |  |  |  |  |  |  |  |  +--------------- SCROLL LOCK on
  136.        |  |  |  |  |  |  |  |  |  |  |
  137.        |  |  |  |  |  |  |  |  |  |  +------------------ NUM LOCK on
  138.        |  |  |  |  |  |  |  |  |  |
  139.        |  |  |  |  |  |  |  |  |  +--------------------- CAPS LOCK on
  140.        |  |  |  |  |  |  |  |  |
  141.        |  |  |  |  |  |  |  |  +------------------------ INSERT on
  142.        |  |  |  |  |  |  |  |
  143.        |  |  |  |  |  |  |  +--------------------------- Left CTRL down
  144.        |  |  |  |  |  |  |
  145.        |  |  |  |  |  |  +------------------------------ Left ALT down
  146.        |  |  |  |  |  |
  147.        |  |  |  |  |  +--------------------------------- Right CTRL down
  148.        |  |  |  |  |
  149.        |  |  |  |  +------------------------------------ Right ALT down
  150.        |  |  |  |
  151.        |  |  |  +--------------------------------------- SCROLL LOCK down
  152.        |  |  |
  153.        |  |  +------------------------------------------ NUM LOCK down
  154.        |  |
  155.        |  +--------------------------------------------- CAPS LOCK down
  156.        |
  157.        +------------------------------------------------ SYSREQ down
  158.  
  159.  
  160.  
  161. Caption:
  162. Figure 1.  Shift and toggle key information obtained with the OS/2 KbdCharIn function.
  163.  
  164.  
  165.                     Can be redirected?  Works With ANSI?
  166.                     ------------------  ----------------
  167.      Screen Output
  168.      -------------
  169.      DosWrite              Yes                 Yes
  170.      VioWrtTTY             No                  Yes
  171.      All other Vio         No                  No
  172.        functions
  173.  
  174.      Keyboard Input
  175.      --------------
  176.      DosRead               Yes                 Yes
  177.      KbdStringIn           No                  Yes
  178.      All other Kbd         No                  No
  179.        functions
  180.  
  181.  
  182.  
  183. Caption:
  184. Figure 2.  How OS/2 screen output and keyboard input functions work with redirection and ANSI control sequences.
  185. «PG»
  186.  
  187. /*-------------------------------------------------------
  188.              (C) 1987, Ziff-Davis Communications Company
  189.              Programmed by Charles Petzold, 10/87.
  190.   -------------------------------------------------------*/
  191.  
  192. #include <doscalls.h>
  193. #include <subcalls.h>
  194.  
  195. main ()
  196.      {
  197.      static char     ErrorMsg [] = "PAGE: Requires piped standard input" ;
  198.      struct KeyData  kd ;
  199.      struct ModeData md ;
  200.      unsigned int    HandleType, FlagWord, BytesWritten,
  201.                      ScreenSize, ScreenSaveSel, BytesRead,
  202.                      AnsiMode, Row, Col, InputIndex ;
  203.      unsigned long   VirtualScreen ;
  204.      char far        *ScreenSavePtr ;
  205.      char far        *VirtualScreenPtr ;
  206.      char            Buffer [1024] ;
  207.  
  208.                          /*-------------------------------------
  209.                             Check if Standard Input is keyboard
  210.                            -------------------------------------*/
  211.  
  212.      DOSQHANDTYPE (0, &HandleType, &FlagWord) ;
  213.      if (HandleType == 1)
  214.           {
  215.           DOSWRITE (2, ErrorMsg, sizeof ErrorMsg - 1, &BytesWritten) ;
  216.           return 1 ;
  217.           }
  218.                          /*----------------------------------------
  219.                             Get video mode & calculate screen size
  220.                            ----------------------------------------*/
  221.  
  222.      md.length = sizeof (md) ;
  223.      VIOGETMODE (&md, 0) ;
  224.      ScreenSize = md.col * md.row * 2 ;
  225.  
  226.                          /*------------------------------------------
  227.                             Save current screen in allocated segment
  228.                            ------------------------------------------*/
  229.  
  230.      DOSALLOCSEG (ScreenSize, &ScreenSaveSel, 0) ;
  231.      ScreenSavePtr = (char far *) ((unsigned long) ScreenSaveSel << 16) ;
  232.      VIOREADCELLSTR (ScreenSavePtr, &ScreenSize, 0, 0, 0) ;
  233.  
  234.                          /*-----------------------------------------------
  235.                             Use ANSI to clear screen to current attribute
  236.                            -----------------------------------------------*/
  237.  
  238.      VIOGETANSI (&AnsiMode, 0) ;
  239.      VIOSETANSI (1, 0) ;
  240.      VIOWRTTTY ("\x1B[s\x1B[2J\x1B[u", 10, 0) ;
  241.      VIOSETANSI (AnsiMode, 0) ;
  242.  
  243.                          /*---------------------------
  244.                             Get virtual screen buffer
  245.                            ---------------------------*/
  246.  
  247.      VIOGETBUF (&VirtualScreen, &ScreenSize, 0) ;
  248.      VirtualScreenPtr = (char far *) VirtualScreen ;
  249.  
  250.      InputIndex = 0 ;
  251.  
  252.      do
  253.                               /*----------------------------------------
  254.                                  Clear virtual screen buffer characters
  255.                                 ----------------------------------------*/
  256.           {
  257.           for (Col = 0 ; Col < 2 * md.row * md.col ; Col += 2)
  258.                VirtualScreenPtr [Col] = ' ' ;
  259.  
  260.           Row = 0 ;
  261.           Col = 0 ;
  262.  
  263.           while (Row < md.row)
  264.                {
  265.                               /*---------------------
  266.                                  Read standard input
  267.                                 ---------------------*/
  268.  
  269.                if (InputIndex == 0 || InputIndex == BytesRead)
  270.                     {
  271.                     DOSREAD (0, Buffer, sizeof (Buffer), &BytesRead) ;
  272.  
  273.                     if (BytesRead == 0)
  274.                          break ;
  275.  
  276.                     InputIndex = 0 ;
  277.                     }
  278.                               /*-------------------------------
  279.                                  Fill up virtual screen buffer
  280.                                 -------------------------------*/
  281.  
  282.                switch (Buffer [InputIndex])
  283.                     {
  284.                     case 0x08:
  285.                          if (Col != 0)
  286.                               Col -- ;
  287.                          break ;
  288.  
  289.                     case 0x09:
  290.                          Col = (Col + 8) & ~7 ;
  291.                          break ;
  292.  
  293.                     case 0x0A:
  294.                          Row ++ ;
  295.                          break ;
  296.  
  297.                     case 0x0D:
  298.                          Col = 0 ;
  299.                          break ;
  300.  
  301.                     default:
  302.                          VirtualScreenPtr [2 * (Row * md.col + Col)] =
  303.                                    Buffer [InputIndex] ;
  304.                          Col ++ ;
  305.                          break ;
  306.                     }
  307.                if (Col == md.col)
  308.                     {
  309.                     Col = 0 ;
  310.                     Row ++ ;
  311.                     }
  312.                InputIndex ++ ;
  313.                }
  314.                               /*-------------------------------
  315.                                  Update screen & get character
  316.                                 -------------------------------*/
  317.  
  318.           VIOSHOWBUF (0, ScreenSize, 0) ;
  319.           KBDCHARIN (&kd, 0, 0) ;
  320.           }
  321.      while (BytesRead > 0 && kd.char_code != '\x1B') ;
  322.  
  323.                          /*-----------------------------------------
  324.                             Restore screen & "empty" Standard Input
  325.                            -----------------------------------------*/
  326.  
  327.      VIOWRTCELLSTR (ScreenSavePtr, md.col * md.row * 2, 0, 0, 0) ;
  328.      DOSFREESEG (ScreenSaveSel) ;
  329.  
  330.      while (BytesRead > 0)
  331.           DOSREAD (0, Buffer, sizeof (Buffer), &BytesRead) ;
  332.  
  333.      return 0 ;
  334.      }
  335.  
  336.  
  337.  
  338. Caption:
  339. Figure 3:  PAGE.C, a program that displays Standard Input in Pages.
  340.  
  341.