netrik hacker's manual: pager

# netrik hacker's manual
>========================<

[This file contains a description of the viewer module. See hacking.txt or hacking.html for an overview of the manual.]

## display()

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, or entering the command prompt.

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, 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, if there is a selected link, it is (re)activated with activate_link() (described in hacking-links.*).

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 commands supported presently include vertical scrolling, link selection and following, and history operations.

## Scrolling Commands

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.

### scroll_to()

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 a very big value, and thus the whole screen is always repainted.

scroll_to() returns the adjusted "new_line", so the caller knows what is on the screen, and can use it as the base for any successive commands.

## Link Selection Commands

As explained under display() above, the pager keeps track of the current active link by the "active_link" variable in the "page" struct. (-1 means no link is active.)

All link selection commands are implemented using a generic link search function to find out which link to activate, and then just activating it with activate_link() (see hacking-links.*).

### find_link()

Finding the link to activate is done with the find_link() function. This function is flexible enough to implement almost all link selection commands with only one invocation.

What link to look for is determined by a number of criteria: It is possible to specify at which link number the target range starts, at which link number it ends, at which line number (page coordinates) the range starts, and at which line it ends. (Like in all places dealing with ranges, the lower bound tells the first link or line inside the range, while the upper bound specifies the first one *outside*, i.e. after the last one inside.)

Not all of these criteria need to be specified -- usually, only some make sense in a certain situation. Thus it is possible to pass -1 for any of those parameters, meaning that there is no bound to the respective parameter, i.e. the range extends to the end of the page or link list in that direction. find_link handles all possible combinations in a reasonable manner.

As multiple links can (and often will) match the criteria, a further parameter is necessary: the "search_type" enum (flag) can be either FIND_FIRST or FIND_LAST, meaning to return the first link that is inside both ranges or the last one, respectively. (Here, "first" always means the one with the smaller link number.)

In spite of it's great flexibility, find_link() is actually quite simple in its implementation. The whole search is done in a single loop which scans all links in the link number range, unless it is prior terminated becase the result is already known.

Searching is done forwards, except when an end link is given, in which case we search backwards -- the latter case normally means going back starting from some specific link (typically the current active link in backwards-moving link selection commands), so it's usually more efficient than starting scanning at the beginning of the list, while we look for a link only a few positions back.

As only one loop is used for scanning both forward and backward, the loop start value, end value, and increment have to be set before entering the loop, depending on the direction. When searching forward the increment is positive, the start value is the start of the link number range, and the end value is the end of the link number range; searching backward needs exactly the opposite. Note that the end value has to be matched by equality, not the smaller/greater conditions typically used in for loops, as we can approach the end value from both directions. (Depending on the search direction.) This makes calculating the end value somewhat less elegant.

The links inside the link number range are cheked to be inside the line number range one after the other in this loop. (The check for the upper bound is done unsigned, so -1 (i.e. no bound) for the "end_line" will be treated as a very big positive number, ensuring no link will fail this condition.) Note that the lower bound is compared against the link's start line, while the upper bound is compared against the end line, so multilined links will be accepted only if they are inside the range completely.

If "search_type" is FIND_FIRST, on the first link meeting all criteria the search is terminated. In FIND_LAST mode however the matching link is also memorized, but we continue the scan for possible further matching links. Only when we are already behind the "end_line" when scanning forward or before the "start_line" when scanning backward, we know we won't enter the range anymore and can stop scanning. (Thus keeping the last matching link encountered.)

If no link matching the criteria was found, find_link() returns -1.

### active_start() and active_end()

Cheking whether a link is (or would be) inside the valid screen area for active links, primarily during link selection but also in a few other places, is done using the active_start() and active_end() functions. These simply return the bounds of the valid area (in page coordinates, not screen coordinates!).

Normally, the area starts in the line that shows up at the screen top (i.e. "pager_pos") plus "cfg.link_margin" -- this is one line below the screen top with the default setting. The end is the last line on screen minus the link margin, accordingly. (But note that active_end returns the number of the *first invalid* line, i.e. *after* the end of the valid area, as all end coordinates are specfied this way!)

An exception is the pager being at the top (for active_start()) or bottom (for active_end()) of the page -- in this case the valid area includes the first or last page lines, as these can never move into the normal valid area. (The page top can't ever move below the screen top; same for bottom.)

The "pos" parameter allows specifying some other pager position (relative to the current "page->pager_pos") to check; 0 means using the current position.

### Command Implementations

The 'J' command is very simple: A single invocation of find_link() is enough to find the new active link. The line number range spans from the first line of the current active area to the last line of the active area after scrolling one line. The "start_link" is the one after the current active link, and we look for the first match, so we will always get the following link, if any. If no link is active (page->active_link is -1), 0 will be passed (meaning to start scanning with the first link on page), which is ok -- the first link inside the line number range (i.e. the first link on screen) will be returned. Thus we do not need any special handling for this case.

Knowing which link to activate, we only need to call activate_link() with the new link number to activate it. (Scrolling will be done automatically in activate_link(), if necessary.)

When there is no link to activate we just scroll one line with scroll_to().

The 'K' command works quite similar. Except for some reversed parameters ("end_link" given as current link to get the previous one, no "end_link", "start_line" calculated for scrolling one line up), there is a major difference however: The case of no link being active needs special handling here due to the other behaviour -- we do not take the last link inside the active screen area in this case, but only the last link in the new line(s) becoming active by scrolling. (Which is the difference between the active_start() after scrolling and the one before scrolling.)

Another difference is that due to the seperation, in the case of already having a link we do not need to specify an "end_line" at all, as we search from the current link backwards anyways, which is always inside the active area.

The 'H' command is quite simple: We just need to activate the first link inside the current active screen area.

There is a special case however when some link is already active: In this case we start scanning at this link, so we do not need to scan the whole list. Note that here we start the scanning with the current active link itself, not the one before as in many other commands, as the current link might as well be already the first one on the screen.

'L' is even simpler, as we do not need the special case -- we simply always search forwards starting with the current link; if no link is active, "active_link" is -1 and will be passed as "start_link", which is just what we need anyways.

'M' is also similar, but somewhat more complicated, as we do not know whether the target link is before or after the current one.

First we scan backwards starting from the current link (but not including). If no link is active then find_link() will get -1 as "start_link", and will simply scan from the beginning of the list.

This first scan will fail if there was some link active (so a backward search from that link is done), but the link was in the first screen half, so the search will never reach the second screen half; thus, no result is returned in this case. This condition has to be catched, and we make another scan starting from the current link, this time forward. (And including the current one.)

The <tab> command uses find_link() to look for the next link (i.e. the first link after the current one), with the additional condition that it has to be after the screen start. This additional condition isn't really necessary when there is already an active link, of course; however, it is very convinient as this will automatically get the first link on the screen or somewhere behind it, if there is currently no active link.

'p' is similar, but slightly more complicated, as it has to treat no active link as a special case. Normally, it just looks for the next link without any additional conditions. (Using find_link() here is almost overkill... But not really, as find_link() will automatically do page bound checking etc.) If no link is active, we look for the last link before the active screen area instead.

<backspace> just jumps to the first link with activate_link(), if there are any links on the page.

'+' is similar to 'J' (or actually more to 'K', as it has to handle no active link as a special case). If some link is already active, we simply look on which line it starts, and then look for the first link in the next line having links -- which is just the next link that starts behind the current line. The end of the search area, just as for 'J', is the last line active after scrolling one line, i.e. scrolling one line is allowed if there is no link in the current valid screen area.

When no link is active, we search the same area, only starting from the beginning of the active screen area (and no start link, of course.)

Also like in 'J', we just scroll one line forward if no link meeting the conditions was found.

'-' is a tick more complicated. The search is done in two steps: First we look for the last link on the previous line or before, which is symmetrical to '+'. (With the exception that if there was no link active yet we only search the area that *additionally* becomes active if scrolling one line, just like in 'K'.) This way we find out what is the previous line having any links.

If such a link was found, in the second step we look for the first link on that line by another invocation of find_link(); this time searching back from the link found in the previous search, and the search area starting in the line where that link starts.

'^' is very simple again: It only has to find out on which line the current active link starts (i.e. the cursor line), and then searches backwards for the fist link that starts on that line. The search includes the current link, as it might be already the first one. Nothing is done if there was no active link -- there is no cursor line in this case.

'0' is more tricky (but not really more complicated) because there is no possibility to search for a link ending on or after some specific line directly with find_link(). Instead, we simply look for the last link that ends *before* the current line, and then activate the next one -- which is exactly the desired one.

If there are no links ending before the current line, find_link() will return -1 and we will activate link 0 -- which is the first link on the page, and thus also has to be the first one on the current line. No special handling is necessary for this case.

' on the other hand needs that special handling, i.e. explicitely activating the last link on the page when nothing was found. Otherwise it is symmetrical to '0'.

## History Commands

The history commands all work alike:

'b' goes back to the previous page by setting the current position in the history one backwards (decrementing). The page is then (re)loaded by quitting the pager with "RET_HISTORY" -- main() then invokes the necessary actions for loading the page, before restarting display(). (load_page() and init_load() take care for correct loading of pages from history.)

'f' goes forward in the same manner (incrementing "pos").

'B' searches the history backwards, until it finds some page that is followed either by one with the "absolute" flag set or by an internal page (the internal page is treated like a new site). If nothing is found the search stops at the first entry in the history, and that one is taken.

'F' searches forwards, also looking for a page followed by an absoulte URL. The last entry is taken if nothing was found.

'^r' causes the current page to be reloaded, by returning RET_HISTORY, but not changing the position in the page list. Thus it is not really a history command from the user's point of view -- but technically, it is.

'r' searches backwards for a page with "mark" set. It also takes the first page if nothing was found; 'R' does the same forwards.

The marks are set by the 's' command. This one simply sets the "mark" flag of the current "page" structure.

## Link Following

When a link is followed by typing <return>, "RET_LINK" is returned, and the main program follows the current "active_link". This process is described under main() in hacking-links.*.

## URL commands

The 'u' command causes display() to return "RET_LINK_URL"; main() then shows the URL of the currently active link.

'c' returns "RET_URL", and main() shows the current page URL.

'U' returns "RET_ABSOLUTE_URL", causing main() to print the absolute link target URL which is used when the link is actually followed.

## Other Commands

When 'q' is typed the pager simply returns with "RET_QUIT", indicating that the main program should quit also.

When ':' is typed the pager quits too, but returns "RET_COMMAND"; this means that the main program should enter command mode.