[This file contains a description of the viewer module. See hacking.txt or hacking.html for an overview of the manual.]
As the pager doesn't just follow a static algorithm, but has to react to certain events (keyboard input), the structure is somewhat harder to retrace.
The central pager function is display(), which contains the main loop (keyboard dispatcher) which performs some action(s) on each keypress, as well as the interface to main().
These actions directly performed by the keyboard commands mostly do not have a direct visible effect; instead, they alter the pager state, which becomes visible with the next screen update -- this is performed in each main loop iteration before waiting for the next keypress. Some actions also require temporarily exiting the pager and returning to main(), which is fairly easy and transparent, as the pager state is stored directly in the page structure and the screen contents thus can be easily restored when reentering the pager.
The pager state consists of three basic components: "pager_pos" is the line number (in page) of the first line visible on the screen. "active_link" contains the link number of the currently selected link. (Or -1 if none.) "cursor_x" and "cursor_y" tell where the cursor is on the page. (Not the screen!) The additional "sticky_cursor" flag allows keeping the cursor in its place while scrolling (if possible), instead of moving it to the screen top.
Each of the major parameters is manipulated chiefly by one function: scroll_to() sets "pager_pos", activate_link() sets "active_link", and set_cursor() sets the cursor position (and also manages the "sticky_cursor" status). The variables aren't independent, however -- scroll_to() may need to deactivate the active link if it moved off the screen, and often move the cursor; activate_link may need to scroll if the link is presently not an the screen, and always has to set the cursor position; set_cursor() has to deactivate any previously active link, and also scroll if the desired cursor position is outside the current screen. This interrelation requires the functions calling each other in a quite complicated manner, sometimes even recursively... Maybe we should try to write a generic function for updating all the states at once by a single request.
Note that scroll_to() and activate_link() do not only set the state, but also (re)render the visible page part (using render(), which is described in hacking-layout.*). Probably it would be better to do this in the main loop also. Moreover, activate_link() even modifies the page, which was a very bad idea and surely will be changed in the future.
The viewer basically consists of a keyboard dispatcher that loops until some command was given that requires some action by main(). These include the quit command, link following, history commands, entering the command prompt, or text search.
display() tells main() what action is required, by the return value which is an "enum Pager_ret".
After main() has done it's work, display() will be called again; it will then continue just where it stopped, which is possible because the important pager state information (postion of the currently visible page part, cursor position, currently selected link) are stored in the page structure, which is described under Page List Handling in hacking-page.*. Thus the way over main() is quite seemless; to the user it usually looks as if he was in the pager all the time.
To allow for this, display() needs to do some work to restore the state upon startup.
First, the page is scrolled to the previous position, using scroll_to(). "old_line" is set to PAGE_INVALID (meaning the pager presently displays nothing) before scrolling, so that the screen contents always will be drawn completely anew.
Afterwards, the cursor position is set to the saved one using set_cursor(). Note that the position has to be stored before scroll_to(), as this function might move it out of the screen and adjust the cursor, in case a new cursor position was requested. (By a text search.)
Finally, if there is a selected link, it is (re)activated with activate_link() (described in hacking-links.*). The active link also has to be saved before scroll_to(), as the link position may have changed in the meantime, causing scroll_to() to deactivate the previouly selected link if it is no longer on the screen.
Reactivating the link is omitted when the pager is restarted after a text search command ("search.type" is set) -- in this case the cursor position has higher priority and is not to be overwritten.
Initialization is slightly different if an anchor was activated, which is indicated by "page->active_anchor" being set. In this case, instead of callig scroll_to() directly, activate_anchor() (also described in hacking-links.*) is used. This one also scrolls (but to the anchor position, not the previous pager position), and additionally draws the anchor marks.
After completing these initalizations, the keyboard input loop is entered, which reads one character in each iteration (note that the mvgetch() fuction used for that also updates the curses screen), and then dispatches to the requested command in a big switch.
The cursor position (passed to mvgetch()) is determined from "page->cursor_[xy]", which are page coordinates, and have to be transformed to screen coordinates first.
The scrolling commands are all implemented by simple calls to scroll_to() with different parameters. All relative movement commands can simply add or subtract a constant number of lines to move, as scroll_to() checks for the page boundaries.
This function scrolls the visible page area to the desired position. It moves (or deletes) the screen content, and repaints newly visible areas using render() (see hacking-layout.*).
It is called with the desired new position as argument. This position is described by the line number, of that line of the output page which is displayed in the first screen line.
First the desired new position is checked to be in the valid range.
If it is greater than the page length minus one screenwidth, it is set to that value. (The last page line can't move above the bottom screen line this way.)
If smaller then zero, it is set to zero. (The page top can't move below the screen top.) Note that this will undo the above check, if the output page is smaller than one screenfull -- in this case, the page end is always above the screen end.
Now that we know the real destination position, the difference to the present position (stored in the static "old_line") is calculated.
If the absolute value of that difference is not more than a screenfull, scrolling is used. This is done by the insdelln() curses function, called with the cursor being in the first screen line. (Called with a negative value it deletes lines, causing the screen contents to scroll up; called with a positive argument, it inserts lines, causing the screen contents to scroll down.)
After scrolling, the new lines revealed at the top or bottom of the screen need to be rendered. After scrolling down (negative "scroll_lines"), the first "scroll_lines" of the screen have to be repainted. This is done by calling render() with "scroll_lines" as the height, 0 as the screen position (the first lines of the screen are repainted), and "new_line" as page position. ("new_line" contains the page position of the first screen line.)
After scrolling up, the last screen lines have to be repainted. This is done similar. The hight is "-scroll_lines" ("scroll_lines" is negative when scrolling up), the screen position is the screen end minus "-sroll_lines", and the page position is the page position of the first screen line ("new_line") plus the screen position of the rendered area.
If the difference is too big for scrolling, the whole screen is erased and repainted.
The first time this function is called, "old_line" has the value PAGE_INVALID (defined as the biggest possible positive int value), indicating that the screen contains nothing valid yet. There is no special handling necessary for this case -- "scroll_lines" gets ons to hecks for a possible proxy to send the request to.
Having this, it creates the connect structure "sap", and then looks up the IP address of the connect server -- which is the proxy if one is present, or the target host otherwise. This, among other data, is stored in "sap".
The next step is opening a socket using socket(), and then establishing a TCP connection to the desired server using the "sap" structure prepared before.
Finally, the HTTP request is constructed in get_http_cmd(), and submitted over the socket.
get_http_cmd() normally just puts together a very simple HTTP request, which consists only of the request line with the path (or, for proxies, the full URL), and a "Host:" header containing the target host. (This is required in HTTP/1.1, to allow multiple hosts on one IP.)
Things get slightly more interesting when some form data is to be POSTed. (GET needn't be handled here, as the form data is already encoded into the URL before calling http_init_load() in this case.) Besides of adding the "Content-Type:" and "Content-Encoding:" header fields, the form data has to be submitted inside the body of the request. For that purpose, the form data is encoded using mime_encode() or url_encode() (see hacking-links.*) -- dependending on the desired encoding stored in the "method" field of the form item -- and stored in "form_data", which is a string submitted at the end of the request.
During the whole exection time of get_http_socket(), user breaks are enabled, so that all the slow functions (DNS lookup, connecting to server, submitting request), which are often even waiting for a timeout on failure, can be interrupted.
After the connection has been established with get_http_socket(), parse_header() is used to read and parse the HTTP headers.
Errors in HTTP loading are handled by setting "RES_FAIL" and "PT_INTERNAL", just like file loading errors are handled in init_load().
The parser is very similar to the one in parse_syntax() (see Parsing in hacking-layout.*): The input is processed character-wise by an FSM parser, where each char is read from a buffer which is refilled (more or less) transparently each time all chars have been processed.
The parser first skips the status line (we do not care about the return code, for now...), and afterwards parses all header lines, extracting the name and value of each, and storing them inside the "headers" structure of the HTTP handle so they can be used later. (Presently, the only one used is the "Location:" header, which is cheked in init_load() to handle redirections.)
The parser tries to be as tolerant as possible about broken (or unknown...) syntax. For that matter, '\r' characters are completely ignored, so both the DOS-like '\r\n' linefeeds and unix-like '\n' do work. Illegal characters in the header name are just ignored, as well as a missing space after the ':' separating header name and value, and spaces at the beginning of a line which can't be a folding.
There are a couple of both correct and broken example files in the test/ directory. Use them by piping the contents to tcplisten and pointing netrik to the listening port. (You can also concatenate some HTML file to avoid "No data" errors.)
The data (from the TCP socket) ending before the whole header was parsed is treated as an HTTP loading error, except when a user break was performed, in which case parse_header() simply returns without taking any more action.
The loading of a file or HTTP page can be interrupted by sending SIGINT. interrupt.c contains a couple of functions to faciliate the SIGINT handling. Those are called from various places in load.c and http.c; main() calls init_int() at startup, which sets some constants used in the other functions.
At the beginning of init_load(), hold_int() is called. This function uses sigprocmask() to block SIGINT. The signal is put on hold, i.e. it doesn't have it's usual effect of aborting the program anymore, but it's also not discarded; instead, it's stored, and awaits its release.
The signal is released using enable_load(), which is called in two places: during read() or fread() in load(), and during get_http_socket() in http_init_load(). (These are the functions which may take fairly long, and need a way to be interrupted directly.)
enable_load() sets int_handler() as the handler for SIGINT (using sigaction()), and then unblocks the signal with procsigmask(). However, not only arriving new signals will invoke int_handler() now; if some signal was sent between the hold_int() and enable_int(), it will be deliverd after enable_int(), too. This way, every SIGINT during the whole loading process will cause a break, not only if it was sent during the periods where it can be handled.
Before calling enable_load(), a return point is set using setjmp(), and stored in "label_int". int_handler() does nothing else but immediately jumping to that return point with longjmp(). That means, when an interrupt occurs before or during the read()/fread() in load(), the signal handler jumps to another position in load(). Here, "res->user_break" is set to indicate the interrupt, and load() returns, instead of continuing the read(). Likewise in http_init_load().
At the end of load() and http_init_load(), hold_int() is called, so SIGINT will be put on hold again till the next call of load().
uninit_load() calls disable_int(). This function used sigaction() to set the SIGINT handler to SIG_IGN, and then unblocks the signal. Thus, any SIGINTs arriving after file loading has finished are discarded. They are only enabled again in init_load(), when the next file load beginns.