freebsd-dev/usr.bin/more/command.c
Tim Vanderhoek 20d6e5fe6e Thorough revamp of how input commands are processed. This allows customization
of user keys (documentation pending).  The only key whose semantics have
changed is the capital 'N' key, which now performs a repeat-search in the
opposite direction (just like in vi).

This commit is a little bulkier than what I had originally planned.  I'm not
completely happy with the direction it went, but it's definately an
improvement, and the alternative is to continue becoming irrelevant compared
to GNU less.  (Does anyone even _use_ /usr/bin/more these days?)
1999-09-03 22:31:21 +00:00

735 lines
18 KiB
C

/*
* Copyright (c) 1988 Mark Nudleman
* Portions copyright (c) 1999 T. Michael Vanderhoek
* 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[] = "@(#)command.c 8.1 (Berkeley) 6/6/93";
#endif /* not lint */
#ifndef lint
static const char rcsid[] =
"$FreeBSD$";
#endif /* not lint */
/*
* Functions for interacting with the user directly printing hello
* messages or reading from the terminal. All of these functions deal
* specifically with the prompt line, and only the prompt line.
*/
#include <sys/param.h>
#include <assert.h>
#include <ctype.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include "less.h"
#include "pathnames.h"
extern int erase_char, kill_char, werase_char;
extern int sigs;
extern int quit_at_eof;
extern int hit_eof;
extern int horiz_off;
extern int sc_width;
extern int bo_width;
extern int be_width;
extern int so_width;
extern int se_width;
extern int curr_ac;
extern int ac;
extern char **av;
extern int screen_trashed; /* The screen has been overwritten */
static int cmd_col; /* Current screen column when accepting input */
static cmd_char(), cmd_erase(), getcc();
/*****************************************************************************
*
* Functions for reading-in user input.
*
*/
static int biggetinputhack_f;
/* biggetinputhack()
*
* Performs as advertised.
*/
biggetinputhack()
{
biggetinputhack_f = 1;
}
/*
* Read a line of input from the terminal. Reads at most bufsiz - 1 characters
* and places them in buffer buf. They are NUL-terminated. Prints the
* temporary prompt prompt.
*/
getinput(prompt, buf, bufsiz)
const char *prompt;
char *buf;
int bufsiz;
{
extern bo_width, be_width;
char *bufcur;
int c;
prmpt(prompt);
bufcur = buf;
for (;;) {
c = getcc();
if (c == '\n') {
*bufcur = '\0';
return;
}
if (c == READ_INTR ||
cmd_char(c, buf, &bufcur, buf + bufsiz - 1)) {
/* input cancelled */
if (bufsiz) *buf = '\0';
return;
}
if (biggetinputhack_f) {
biggetinputhack_f = 0;
*bufcur = '\0';
return;
}
}
}
/*
* Process a single character of a multi-character input, such as
* a number, or the pattern of a search command. Returns true if the user
* has cancelled the multi-character input, false otherwise and attempts
* to add it to buf (not exceeding bufsize). Prints the character on the
* terminal output. The bufcur should initially equal bufbeg. After that
* it does not need to be touched or modified by the user, but may be expected
* to point at the future position of the next character.
*/
static int
cmd_char(c, bufbeg, bufcur, bufend)
int c; /* The character to process */
char *bufbeg; /* The buffer to add the character to */
char **bufcur; /* The position at which to add the character */
char *bufend; /* The last spot available in the buffer --- remember
* to leave one after bufend for the '\0'! (You must
* add the '\0' yourself!!) */
{
if (c == erase_char)
return(cmd_erase(bufbeg, bufcur));
/* in this order, in case werase == erase_char */
if (c == werase_char) {
if (*bufcur > bufbeg) {
while (isspace((*bufcur)[-1]) &&
!cmd_erase(bufbeg, bufcur)) ;
while (!isspace((*bufcur)[-1]) &&
!cmd_erase(bufbeg, bufcur)) ;
while (isspace((*bufcur)[-1]) &&
!cmd_erase(bufbeg, bufcur)) ;
}
return *bufcur == bufbeg;
}
if (c == kill_char) {
while (!cmd_erase(bufbeg, bufcur));
return 1;
}
/*
* No room in the command buffer, or no room on the screen;
* XXX If there is no room on the screen, we should just let the
* screen scroll down and set screen_trashed=1 appropriately, or
* alternatively, scroll the prompt line horizontally.
*/
assert (*bufcur <= bufend);
if (*bufcur == bufend || cmd_col >= sc_width - 3)
bell();
else {
*(*bufcur)++ = c;
if (CONTROL_CHAR(c)) {
putchr('^');
cmd_col++;
c &= ~0200;
c = CARAT_CHAR(c);
}
putchr(c);
cmd_col++;
}
return 0;
}
/*
* Helper function to cmd_char(). Backs-up one character from bufcur in the
* buffer passed, and prints a backspace on the screen. Returns true if the
* we backspaced past bufbegin (ie. the input is being aborted), and false
* otherwise. The bufcur is expected to point to the future location of the
* next character in the buffer, and is modified appropriately.
*/
static
cmd_erase(bufbegin, bufcur)
char *bufbegin;
char **bufcur;
{
int c;
/*
* XXX Could add code to detect a backspace that is backing us over
* the beginning of a line and onto the previous line. The backspace
* would not be printed for some terminals (eg. hardcopy) in that
* case.
*/
/*
* backspace past beginning of the string: this usually means
* abort the input.
*/
if (*bufcur == bufbegin)
return 1;
(*bufcur)--;
/* If erasing a control-char, erase an extra character for the carat. */
c = **bufcur;
if (CONTROL_CHAR(c)) {
backspace();
cmd_col--;
}
backspace();
cmd_col--;
return 0;
}
static int ungotcc;
/*
* Get command character from the terminal.
*/
static
getcc()
{
int ch;
off_t position();
/* left over from error() routine. */
if (ungotcc) {
ch = ungotcc;
ungotcc = 0;
return(ch);
}
return(getchr());
}
/*
* Same as ungetc(), but works for people who don't like to use streams.
*/
ungetcc(c)
int c;
{
ungotcc = c;
}
/*****************************************************************************
*
* prompts
*
*/
static int longprompt;
/*
* Prints prmpt where the prompt would normally appear. This is different
* from changing the current prompt --- this is more like printing a
* unimportant notice or error. The prmpt line will be printed in bold (if
* possible). Will in the future print only the last sc_width - 1 - bo_width
* characters (to prevent newline).
*/
prmpt(prmpt)
const char *prmpt;
{
lower_left();
clear_eol();
bo_enter();
putxstr(prmpt);
bo_exit();
flush();
cmd_col = strlen(prmpt) + bo_width + be_width;
}
/*
* Print the main prompt that signals we are ready for user commands. This
* also magically positions the current file where it should be (either by
* calling repaint() if screen_trashed or by searching for a search
* string that was specified through option.c on the more(1) command line).
* Additional magic will randomly call the quit() function.
*
* This is really intended to do a lot of the work of commands(). It has
* little purpose outside of commands().
*/
prompt()
{
extern int linenums, short_file, ispipe;
extern char *current_name, *firstsearch, *next_name;
off_t len, pos, ch_length(), position(), forw_line();
char pbuf[40];
/*
* if nothing is displayed yet, display starting from line 1;
* if search string provided, go there instead.
*/
if (position(TOP) == NULL_POSITION) {
#if 0
/* This code causes "more zero-byte-file /etc/termcap" to skip straight
* to the /etc/termcap file ... that is undesireable. There are only a few
* instances where these two lines perform something useful. */
if (forw_line((off_t)0) == NULL_POSITION)
return 0 ;
#endif
if (!firstsearch || !search(1, firstsearch, 1, 1))
jump_back(1);
}
else if (screen_trashed)
repaint();
/* if no -e flag and we've hit EOF on the last file, quit. */
if (!quit_at_eof && hit_eof && curr_ac + 1 >= ac)
quit();
/* select the proper prompt and display it. */
lower_left();
clear_eol();
pbuf[sizeof(pbuf) - 1] = '\0';
if (longprompt) {
/*
* Get the current line/pos from the BOTTOM of the screen
* even though that's potentially confusing for the user
* when switching between wraplines=true and a valid horiz_off
* (with wraplines=false). In exchange, it is sometimes
* easier for the user to tell when a file is relatively
* short vs. long.
*/
so_enter();
putstr(current_name);
putstr(":");
if (!ispipe) {
(void)snprintf(pbuf, sizeof(pbuf) - 1,
" file %d/%d", curr_ac + 1, ac);
putstr(pbuf);
}
if (linenums) {
(void)snprintf(pbuf, sizeof(pbuf) - 1,
" line %d", currline(BOTTOM));
putstr(pbuf);
}
(void)snprintf(pbuf, sizeof(pbuf) - 1, " col %d", horiz_off);
putstr(pbuf);
if ((pos = position(BOTTOM)) != NULL_POSITION) {
(void)snprintf(pbuf, sizeof(pbuf) - 1,
" byte %qd", pos);
putstr(pbuf);
if (!ispipe && (len = ch_length())) {
(void)snprintf(pbuf, sizeof(pbuf) - 1,
"/%qd pct %qd%%", len, ((100 * pos) / len));
putstr(pbuf);
}
}
so_exit();
}
else {
so_enter();
putstr(current_name);
if (hit_eof)
if (next_name) {
putstr(": END (next file: ");
putstr(next_name);
putstr(")");
}
else
putstr(": END");
else if (!ispipe &&
(pos = position(BOTTOM)) != NULL_POSITION &&
(len = ch_length())) {
(void)snprintf(pbuf, sizeof(pbuf) - 1,
" (%qd%%)", ((100 * pos) / len));
putstr(pbuf);
}
so_exit();
}
return 1;
}
/*
* Sets the current prompt. Currently it sets the current prompt to the
* long prompt.
*/
statprompt(nostatprompt)
int nostatprompt; /* Turn off the stat prompt? (off by default...) */
{
if (nostatprompt)
longprompt = 0;
else
longprompt = 1;
}
/*****************************************************************************
*
* Errors, next-of-kin to prompts.
*
*/
/*
* Shortcut function that may be used when setting the current erreur
* and erreur string at the same time. The function name is chosen to be
* symetric with the SETERR() macro in less.h. This could be written as
* macro, too, but we'd need to use a GNU C extension.
*/
SETERRSTR(enum error e, const char *s, ...)
{
va_list args;
erreur = e;
if (errstr) free(errstr);
errstr = NULL;
va_start(args, s);
vasprintf(&errstr, s, args);
va_end(args);
}
/*
* Prints an error message and clears the current error.
*/
handle_error()
{
if (erreur == E_OK)
return;
bell();
if (errstr)
error(errstr);
else
error(deferr[erreur]);
erreur = E_OK;
errstr = NULL;
}
/*
* Clears any error messages and pretends they never occurred.
*/
clear_error()
{
erreur = E_OK;
if (errstr) free(errstr);
errstr = NULL;
}
int errmsgs;
static char return_to_continue[] = "(press RETURN)";
/*
* Output a message in the lower left corner of the screen
* and wait for carriage return.
*/
/* static */
error(s)
char *s;
{
extern int any_display;
int ch;
errmsgs++;
if (!any_display) {
/*
* Nothing has been displayed yet. Output this message on
* error output (file descriptor 2) and don't wait for a
* keystroke to continue.
*
* This has the desirable effect of producing all error
* messages on error output if standard output is directed
* to a file. It also does the same if we never produce
* any real output; for example, if the input file(s) cannot
* be opened. If we do eventually produce output, code in
* edit() makes sure these messages can be seen before they
* are overwritten or scrolled away.
*/
(void)write(2, s, strlen(s));
(void)write(2, "\n", 1);
return;
}
lower_left();
clear_eol();
so_enter();
if (s) {
putstr(s);
putstr(" ");
}
putstr(return_to_continue);
so_exit();
if ((ch = getchr()) != '\n') {
/* XXX hardcoded */
if (ch == 'q')
quit();
ungotcc = ch;
}
lower_left();
if ((s==NULL)?0:(strlen(s)) + sizeof(return_to_continue) +
so_width + se_width + 1 > sc_width) {
/*
* Printing the message has probably scrolled the screen.
* {{ Unless the terminal doesn't have auto margins,
* in which case we just hammered on the right margin. }}
*/
/* XXX Should probably just set screen_trashed=1, but I'm
* not going to touch that until all the places that call
* error() have been checked, or until error() is staticized. */
repaint();
}
flush();
}
/****************************************************************************
*
* The main command processor.
*
* (Well, it deals with things on the prompt line, doesn't it?)
*
*/
/*
* Main command processor.
*
* Accept and execute commands until a quit command, then return.
*/
commands()
{
enum runmacro runmacro();
enum runmacro rmret;
long numberN;
enum { NOTGOTTEN=0, GOTTEN=1, GETTING } Nstate; /* ie. numberNstate */
int c;
char inbuf[20], *incur = inbuf;
*inbuf = '\0';
Nstate = GETTING;
for (;;) {
/*
* See if any signals need processing.
*/
if (sigs)
psignals();
/*
* Display prompt and generally get setup. Don't display the
* prompt if we are already in the middle of accepting a
* set of characters.
*/
if (!*inbuf && !prompt()) {
next_file(1);
continue;
}
c = getcc();
/* Check sigs here --- getcc() may have given us READ_INTR */
if (sigs) {
/* terminate any current macro */
*inbuf = '\0';
incur = inbuf;
continue; /* process the sigs */
}
if (Nstate == GETTING && !isdigit(c)) {
/* mark the end of an input number N, if any */
if (!*inbuf) {
/* We never actually got an input number */
Nstate = NOTGOTTEN;
} else {
numberN = atol(inbuf);
Nstate = GOTTEN;
}
*inbuf = '\0';
incur = inbuf;
}
cmd_char(c, inbuf, &incur, inbuf + sizeof(inbuf) - 1);
*incur = '\0';
if (*inbuf) prmpt(inbuf);
if (Nstate == GETTING) {
/* Still reading in the number N ... don't want to
* try running the macro expander. */
continue;
} else {
/* Try expanding the macro */
switch (runmacro(inbuf, numberN, Nstate)) {
case TOOMACRO:
break;
case BADMACRO: case NOMACRO: case BADCOMMAND:
handle_error();
/* fallthrough */
case OK:
/* recock */
*inbuf = '\0';
incur = inbuf;
Nstate = GETTING;
break;
}
}
} /* for (;;) */
}
/*****************************************************************************
*
* Misc functions that belong in ncommand.c but are here for historical
* and for copyright reasons.
*
*/
editfile()
{
off_t position();
extern char *current_file;
static int dolinenumber;
static char *editor;
char *base;
int linenumber;
char buf[MAXPATHLEN * 2 + 20], *getenv();
if (editor == NULL) {
editor = getenv("EDITOR");
/* default editor is vi */
if (editor == NULL || *editor == '\0')
editor = _PATH_VI;
/* check last component in case of full path */
base = strrchr(editor, '/');
if (!base)
base = editor;
else
base++;
/* emacs also accepts vi-style +nnnn */
if (strncmp(base, "vi", 2) == 0 || strcmp(base, "emacs") == 0)
dolinenumber = 1;
else
dolinenumber = 0;
}
/*
* XXX Can't just use currline(MIDDLE) since that might be NULL_POSITION
* if we are editting a short file or some kind of search positioned
* us near the last line. It's not clear what currline() should do
* in those circumstances, but as of this writing, it doesn't do
* anything reasonable from our perspective. The currline(MIDDLE)
* never had the desired results for an editfile() after a search()
* anyways. Note, though, that when vi(1) starts its editting, it
* positions the focus line in the middle of the screen, not the top.
*
* I think what is needed is some kind of setfocus() and getfocus()
* function. This could put the focussed line in the middle, top,
* or wherever as per the user's wishes, and allow things like us
* to getfocus() the correct file-position/line-number. A search would
* then search forward (or backward) from the current focus position,
* etc.
*
* currline() doesn't belong.
*/
if (position(MIDDLE) == NULL_POSITION)
linenumber = currline(TOP);
else
linenumber = currline(MIDDLE);
if (dolinenumber && linenumber)
(void)snprintf(buf, sizeof(buf),
"%s +%d %s", editor, linenumber, current_file);
else
(void)snprintf(buf, sizeof(buf), "%s %s", editor, current_file);
lsystem(buf);
}
showlist()
{
extern int sc_width;
register int indx, width;
int len;
char *p;
if (ac <= 0) {
error("No files provided as arguments.");
return;
}
for (width = indx = 0; indx < ac;) {
p = strcmp(av[indx], "-") ? av[indx] : "stdin";
len = strlen(p) + 1;
if (curr_ac == indx)
len += 2;
if (width + len + 1 >= sc_width) {
if (!width) {
if (curr_ac == indx)
putchr('[');
putstr(p);
if (curr_ac == indx)
putchr(']');
++indx;
}
width = 0;
putchr('\n');
continue;
}
if (width)
putchr(' ');
if (curr_ac == indx)
putchr('[');
putstr(p);
if (curr_ac == indx)
putchr(']');
width += len;
++indx;
}
putchr('\n');
error((char *)NULL);
}