freebsd-nq/usr.bin/more/prim.c
Brian Feldman c8b1db2ed8 In the modern world, things are much faster than when more(1) was created.
Scrolling sideways is fast, and a "...skipping..." message making everything
blink does much more harm than good.
2000-05-21 18:35:28 +00:00

843 lines
18 KiB
C

/*
* Copyright (c) 1988 Mark Nudleman
* Copyright (c) 1988, 1993
* The Regents of the University of California. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* This product includes software developed by the University of
* California, Berkeley and its contributors.
* 4. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#ifndef lint
static char sccsid[] = "@(#)prim.c 8.1 (Berkeley) 6/6/93";
#endif /* not lint */
#ifndef lint
static const char rcsid[] =
"$FreeBSD$";
#endif /* not lint */
/*
* Primitives for displaying the file on the screen.
*/
#include <sys/types.h>
#include <assert.h>
#include <ctype.h>
#include <limits.h>
#include <regex.h>
#include <stdio.h>
#include "less.h"
int back_scroll = -1;
int hit_eof; /* true if we're displaying the end of the input */
int screen_trashed;
static int squished;
extern int sigs;
extern int top_scroll;
extern int sc_width, sc_height;
extern int horiz_off;
extern int wraplines;
extern int caseless;
extern int linenums;
extern int tagoption;
extern char *line;
extern int retain_below;
off_t position(), forw_line(), back_line(), forw_raw_line(), back_raw_line();
off_t ch_length(), ch_tell();
/*
* Check to see if the end of file is currently "displayed".
*/
static
eof_check()
{
off_t pos;
if (sigs)
return;
/*
* If the bottom line is empty, we are at EOF.
* If the bottom line ends at the file length,
* we must be just at EOF.
*/
pos = position(BOTTOM_PLUS_ONE);
if (pos == NULL_POSITION || pos == ch_length())
hit_eof++;
}
/*
* If the screen is "squished", repaint it.
* "Squished" means the first displayed line is not at the top
* of the screen; this can happen when we display a short file
* for the first time.
*/
static
squish_check()
{
if (squished) {
squished = 0;
repaint();
}
}
/*
* Display n lines, scrolling forward, starting at position pos in the
* input file. "only_last" means display only the last screenful if
* n > screen size.
*/
static
forw(n, pos, only_last)
register int n;
off_t pos;
int only_last;
{
extern int short_file;
static int first_time = 1;
int eof = 0, do_repaint;
squish_check();
/*
* do_repaint tells us not to display anything till the end,
* then just repaint the entire screen.
*/
do_repaint = (only_last && n > sc_height-1);
if (!do_repaint) {
if (top_scroll && n >= sc_height - 1) {
/*
* Start a new screen.
* {{ This is not really desirable if we happen
* to hit eof in the middle of this screen,
* but we don't yet know if that will happen. }}
*/
clear();
home();
} else {
lower_left();
clear_eol();
}
/*
* This is not contiguous with what is currently displayed.
* Clear the screen image (position table) and start a new
* screen.
*/
if (pos != position(BOTTOM_PLUS_ONE)) {
pos_clear();
add_forw_pos(pos);
if (top_scroll) {
clear();
home();
}
}
}
for (short_file = 0; --n >= 0;) {
/*
* Read the next line of input.
*/
pos = forw_line(pos);
if (pos == NULL_POSITION) {
/*
* end of file; copy the table if the file was
* too small for an entire screen.
*/
eof = 1;
if (position(TOP) == NULL_POSITION) {
copytable();
if (!position(TOP))
short_file = 1;
}
break;
}
/*
* Add the position of the next line to the position table.
* Display the current line on the screen.
*/
add_forw_pos(pos);
if (do_repaint)
continue;
/*
* If this is the first screen displayed and we hit an early
* EOF (i.e. before the requested number of lines), we
* "squish" the display down at the bottom of the screen.
* But don't do this if a -t option was given; it can cause
* us to start the display after the beginning of the file,
* and it is not appropriate to squish in that case.
*/
if (first_time && line == NULL && !top_scroll && !tagoption) {
squished = 1;
continue;
}
put_line();
}
if (eof && !sigs)
hit_eof++;
else
eof_check();
if (do_repaint)
repaint();
first_time = 0;
(void) currline(BOTTOM);
}
/*
* Display n lines, scrolling backward.
*/
static
back(n, pos, only_last)
register int n;
off_t pos;
int only_last;
{
int do_repaint;
squish_check();
do_repaint = (n > get_back_scroll() || (only_last && n > sc_height-1));
hit_eof = 0;
while (--n >= 0)
{
/*
* Get the previous line of input.
*/
pos = back_line(pos);
if (pos == NULL_POSITION)
break;
/*
* Add the position of the previous line to the position table.
* Display the line on the screen.
*/
add_back_pos(pos);
if (!do_repaint)
{
if (retain_below)
{
lower_left();
clear_eol();
}
home();
add_line();
put_line();
}
}
eof_check();
if (do_repaint)
repaint();
(void) currline(BOTTOM);
}
/*
* Display n more lines, forward.
* Start just after the line currently displayed at the bottom of the screen.
*/
forward(n, only_last)
int n;
int only_last;
{
off_t pos;
if (hit_eof) {
/*
* If we're trying to go forward from end-of-file,
* go on to the next file.
*/
next_file(1);
return;
}
pos = position(BOTTOM_PLUS_ONE);
if (pos == NULL_POSITION)
{
hit_eof++;
return;
}
forw(n, pos, only_last);
}
/*
* Display n more lines, backward.
* Start just before the line currently displayed at the top of the screen.
*/
backward(n, only_last)
int n;
int only_last;
{
off_t pos;
pos = position(TOP);
/*
* This will almost never happen, because the top line is almost
* never empty.
*/
if (pos == NULL_POSITION)
return;
back(n, pos, only_last);
}
/*
* Repaint the screen, starting from a specified position.
*/
prepaint(pos)
off_t pos;
{
hit_eof = 0;
forw(sc_height - 1, pos, 0);
screen_trashed = 0;
}
/*
* Repaint the screen.
*/
repaint()
{
/*
* Start at the line currently at the top of the screen
* and redisplay the screen.
*/
prepaint(position(TOP));
}
/*
* Jump to the end of the file.
* It is more convenient to paint the screen backward,
* from the end of the file toward the beginning.
*/
jump_forw()
{
off_t pos;
if (ch_end_seek())
{
error("Cannot seek to end of file");
return;
}
lastmark();
pos = ch_tell();
clear();
pos_clear();
add_back_pos(pos);
back(sc_height - 1, pos, 0);
}
/*
* Jump to line n in the file.
*/
jump_back(n)
register int n;
{
register int c, nlines;
/*
* This is done the slow way, by starting at the beginning
* of the file and counting newlines.
*
* {{ Now that we have line numbering (in linenum.c),
* we could improve on this by starting at the
* nearest known line rather than at the beginning. }}
*/
if (ch_seek((off_t)0)) {
/*
* Probably a pipe with beginning of file no longer buffered.
* If he wants to go to line 1, we do the best we can,
* by going to the first line which is still buffered.
*/
if (n <= 1 && ch_beg_seek() == 0)
jump_loc(ch_tell());
error("Cannot get to beginning of file");
return;
}
/*
* Start counting lines.
*/
for (nlines = 1; nlines < n; nlines++)
while ((c = ch_forw_get()) != '\n')
if (c == EOI) {
char message[40];
(void)snprintf(message, sizeof(message),
"File has only %d lines", nlines - 1);
error(message);
return;
}
jump_loc(ch_tell());
}
/*
* Jump to a specified percentage into the file.
* This is a poor compensation for not being able to
* quickly jump to a specific line number.
*/
jump_percent(percent)
int percent;
{
off_t pos, len, ch_length();
register int c;
/*
* Determine the position in the file
* (the specified percentage of the file's length).
*/
if ((len = ch_length()) == NULL_POSITION)
{
error("Don't know length of file");
return;
}
pos = (percent * len) / 100;
/*
* Back up to the beginning of the line.
*/
if (ch_seek(pos) == 0)
{
while ((c = ch_back_get()) != '\n' && c != EOI)
;
if (c == '\n')
(void) ch_forw_get();
pos = ch_tell();
}
jump_loc(pos);
}
/*
* Jump to a specified position in the file.
*/
jump_loc(pos)
off_t pos;
{
register int nline;
off_t tpos;
if ((nline = onscreen(pos)) >= 0) {
/*
* The line is currently displayed.
* Just scroll there.
*/
forw(nline, position(BOTTOM_PLUS_ONE), 0);
return;
}
/*
* Line is not on screen.
* Seek to the desired location.
*/
if (ch_seek(pos)) {
error("Cannot seek to that position");
return;
}
/*
* See if the desired line is BEFORE the currently displayed screen.
* If so, then move forward far enough so the line we're on will be
* at the bottom of the screen, in order to be able to call back()
* to make the screen scroll backwards & put the line at the top of
* the screen.
* {{ This seems inefficient, but it's not so bad,
* since we can never move forward more than a
* screenful before we stop to redraw the screen. }}
*/
tpos = position(TOP);
if (tpos != NULL_POSITION && pos < tpos) {
off_t npos = pos;
/*
* Note that we can't forw_line() past tpos here,
* so there should be no EOI at this stage.
*/
for (nline = 0; npos < tpos && nline < sc_height - 1; nline++)
npos = forw_line(npos);
if (npos < tpos) {
/*
* More than a screenful back.
*/
lastmark();
clear();
pos_clear();
add_back_pos(npos);
}
/*
* Note that back() will repaint() if nline > back_scroll.
*/
back(nline, npos, 0);
return;
}
/*
* Remember where we were; clear and paint the screen.
*/
lastmark();
prepaint(pos);
}
/*
* The table of marks.
* A mark is simply a position in the file.
*/
#define NMARKS (27) /* 26 for a-z plus one for quote */
#define LASTMARK (NMARKS-1) /* For quote */
static struct mark {
int horiz_off;
int wraplines;
off_t pos;
char *file;
} marks[NMARKS];
/*
* Initialize the mark table to show no marks are set.
*/
init_mark()
{
int i;
for (i = 0; i < NMARKS; i++)
marks[i].pos = NULL_POSITION, marks[i].file = NULL;
}
/*
* See if a mark letter is valid (between a and z).
*/
static int
badmark(c)
int c;
{
if (c < 'a' || c > 'z')
{
error("Choose a letter between 'a' and 'z'");
return (1);
}
return (0);
}
/*
* Set a mark.
*/
setmark(c)
int c;
{
extern char *current_file;
if (badmark(c))
return;
marks[c-'a'].pos = position(TOP);
marks[c-'a'].horiz_off = horiz_off;
marks[c-'a'].wraplines = wraplines;
if (marks[c-'a'].file) free(marks[c-'a'].file);
asprintf(&marks[c-'a'].file, "%s", current_file);
}
/*
* This function should be called whenever the file position we are viewing
* is changed by a significant amount. The function will record the current
* position and remember it for the user.
*/
lastmark()
{
extern char *current_file;
marks[LASTMARK].pos = position(TOP);
marks[LASTMARK].horiz_off = horiz_off;
marks[LASTMARK].wraplines = wraplines;
if (marks[LASTMARK].file) free(marks[LASTMARK].file);
asprintf(&marks[LASTMARK].file, "%s", current_file);
}
/*
* Go to a previously set mark.
*/
gomark(c)
int c;
{
off_t pos;
char *file;
int new_horiz_off, new_wraplines;
extern char *current_file;
if (c == '\'') {
pos = marks[LASTMARK].pos;
if (pos == NULL_POSITION)
pos = 0;
file = marks[LASTMARK].file;
new_horiz_off = marks[LASTMARK].horiz_off;
new_wraplines = marks[LASTMARK].wraplines;
} else {
if (badmark(c))
return;
pos = marks[c-'a'].pos;
if (pos == NULL_POSITION) {
error("mark not set");
return;
}
file = marks[c-'a'].file;
new_horiz_off = marks[c-'a'].horiz_off;
new_wraplines = marks[c-'a'].wraplines;
}
/*
* This can only fail if gomark('\'') is called before lastmark()
* is called, which is in turn impossible since both edit() and
* jump_back() call jump_loc() which calls lastmark() to start
* all files.
*/
assert (file);
/*
* This can only fail if gomark() is called before any file has
* been opened. Calling gomark() before any file has been
* opened would be non-sensical.
*/
assert (current_file);
/*
* XXX The edit() needs to return success or failure so that we
* can abort at this point if edit() fails.
*/
edit(file, NO_FORCE_OPEN);
/* Try to be nice about changing the horizontal scroll and wrapping */
if (new_horiz_off > horiz_off + sc_width / 3 ||
new_horiz_off < horiz_off - sc_width / 3 ||
wraplines != new_wraplines || strcmp(file, current_file)) {
/*
* We should change horiz_off: if we don't change horiz_off
* the bookmarked location won't be readily visible.
*/
/*
* A prepaint() doesn't call lastmark() (jump_loc() does),
* but we need to call repaint() somewhere since we've
* changed the horizontal offset. We don't want to call
* jump_loc() followed by repaint() since that represents
* more unnecessary screen redrawing than I'm comfortable
* with. Manually calling lastmark() here means, however,
* that lastmark() is always called even if we scroll only
* a few lines --- unlike letting jump_loc() call lastmark()
* where lastmark() is only called for jumps of more than
* a screenful. A better interface is needed.
*/
lastmark();
horiz_off = new_horiz_off;
wraplines = new_wraplines;
prepaint(pos);
} else {
/*
* We can honour the bookmark request without doing any
* horizontal scrolling.
*/
jump_loc(pos);
}
}
/*
* Get the backwards scroll limit.
* Must call this function instead of just using the value of
* back_scroll, because the default case depends on sc_height and
* top_scroll, as well as back_scroll.
*/
get_back_scroll()
{
if (back_scroll >= 0)
return (back_scroll);
if (top_scroll)
return (sc_height - 2);
return (sc_height - 1);
}
/*
* Search for the n-th occurence of a specified pattern,
* either forward or backward.
*/
search(search_forward, pattern, n, wantmatch)
register int search_forward;
register char *pattern;
register int n;
int wantmatch;
{
off_t pos, linepos;
register char *p;
register char *q;
int linenum;
int linematch;
static regex_t rx;
static int oncethru;
int regerr;
char errbuf[_POSIX2_LINE_MAX];
if (pattern && pattern[0]) {
if (oncethru) {
regfree(&rx);
}
regerr = regcomp(&rx, pattern, (REG_EXTENDED | REG_NOSUB
| (caseless ? REG_ICASE : 0)));
if (regerr) {
regerror(regerr, &rx, errbuf, sizeof errbuf);
error(errbuf);
oncethru = 0;
regfree(&rx);
return (0);
}
oncethru = 1;
} else if (!oncethru) {
error("No previous regular expression");
return (0);
}
/*
* Figure out where to start the search.
*
* XXX This should probably be adapted to handle horizontal
* scrolling. Consider a long line at the top of the screen
* that might be hiding more matches to its right (when doing
* successive searches).
*/
if (position(TOP) == NULL_POSITION) {
/*
* Nothing is currently displayed. Start at the beginning
* of the file. (This case is mainly for searches from the
* command line.
*/
pos = (off_t)0;
} else if (!search_forward) {
/*
* Backward search: start just before the top line
* displayed on the screen.
*/
pos = position(TOP);
} else {
/*
* Start at the second screen line displayed on the screen.
*/
pos = position(TOP_PLUS_ONE);
}
if (pos == NULL_POSITION)
{
/*
* Can't find anyplace to start searching from.
*/
error("Nothing to search");
return(0);
}
linenum = find_linenum(pos);
for (;;)
{
/*
* Get lines until we find a matching one or
* until we hit end-of-file (or beginning-of-file
* if we're going backwards).
*/
if (sigs)
/*
* A signal aborts the search.
*/
return(0);
if (search_forward)
{
/*
* Read the next line, and save the
* starting position of that line in linepos.
*/
linepos = pos;
pos = forw_raw_line(pos);
if (linenum != 0)
linenum++;
} else
{
/*
* Read the previous line and save the
* starting position of that line in linepos.
*/
pos = back_raw_line(pos);
linepos = pos;
if (linenum != 0)
linenum--;
}
if (pos == NULL_POSITION)
{
/*
* We hit EOF/BOF without a match.
*/
error("Pattern not found");
return(0);
}
/*
* If we're using line numbers, we might as well
* remember the information we have now (the position
* and line number of the current line).
*/
if (linenums)
add_lnum(linenum, pos);
/*
* Remove any backspaces along with the preceeding char.
* This allows us to match text which is underlined or
* overstruck.
*/
for (p = q = line; *p; p++, q++)
if (q > line && *p == '\b')
/* Delete BS and preceeding char. */
q -= 2;
else
/* Otherwise, just copy. */
*q = *p;
/*
* Test the next line to see if we have a match.
*/
linematch = !regexec(&rx, line, 0, 0, 0);
/*
* We are successful if wantmatch and linematch are
* both true (want a match and got it),
* or both false (want a non-match and got it).
*/
if (((wantmatch && linematch) || (!wantmatch && !linematch)) &&
--n <= 0)
/*
* Found the line.
*/
break;
}
jump_loc(linepos);
return(1);
}