[net.sources] vnews source

ka@spanky.UUCP (06/06/83)

# The vnews source has been posted in three pieces.  The one titled
# "vnews source -- visual.c" contains the file visual.c.  This file
# contains all the rest of the source files in shell archive format.
# (Simply delete the article header and feed it into the shell.)  The
# article entitled "Vnews User Manual" describes how to use vnews.

cat > README <<\!
1.  Terminal Handling

Dealing with various terminals is a major headache.  Vnews
will support any of three tarminal interfaces; you get your
choice.

  1.  You can use the terminal handle provided in
      virtterm.c.  The problem with this is that the code is
      written for a HP2621 terminal; if you have a different
      terminal you will have to hack up the code.  The file
      convert.term explains how to do this.  I have already
      done this for the IBM PC Asynchronous Communications
      Package; let me know if you are interested and I will
      mail you a copy.  If you have another terminal with
      destructive tabs, also ask me for this; you can lift
      some code from it.

  2.  You can use Mark Horton's version of curses.  There
      are several problems with this.  Mark's curses are not
      available outside Bell Labs.  You can't use curses on
      a PDP-11; it won't fit.  Commands typed during the
      middle of a screen update won't (normally) interrupt
      the update.  There are problems with handling certain
      terminals--the IBM PC termcap entry broke both Mark
      Horton's and Ken Arnold's versions of curses.

  3.  You can use Ken Arnold's version of curses.  This was
      posted to net.sources a few days ago so everybody
      should have it, but the other problems with Mark's
      curses apply here as well.  (Actually, you might be
      able to get it to fit on a PDP-11 if you trimmed
      readnews a bit; I haven't tried.)  In addition, Ken
      Arnold's version of curses doesn't use line insert and
      delete.


2.  Auxiliary files

If that hasn't scared you off, the first step is to install
the files vnews.help, reply.sh, and follow.sh.  Vnews.help
is the vnews help file.  It is normally installed as
"/usr/lib/news/vnews.help"; if you install it elsewhere then
definition of VHELP in visual.c.  Reply.sh and follow.sh are
shell procedures which handle replies and followups,
respectively.  In case you want to change these, the
arguments to reply are the name of a temp file containing
the header, the name of the file containing the article, and
the author of the article.  The arguments to follow are the
same except that the last argument contains the newsgroup(s)
the reply is to be posted to.  You will have to change reply
if the program recmail (delivered with 2.10) isn't in your
path.  These programs are normally installed as
"/usr/lib/news/reply" and "/usr/lib/news/follow"; if you
install them elsewhere change the definitions of REPLYPROG
and FOLLOWPROG in visual.c


3.  Mail format

In 2.10 Mark moved the routine save out of readr.c even
though it writes to the terminal, so I had to copy it.  If
you made any local changes to this routine (presumably to
support a different mail file format), you will have to
duplicate the change in the routine vsave.


4.  Compiling vnews

Vnews uses all the object modules used in readnews except
for readr.c, which is replaced with visual.c.  Therefore, if
you have write permission on the netnews source directory,
you may want to build vnews there.  Otherwise copy all of
the ".o" files out of the netnews directory into the vnews
directory.  The vnews makefile is intended to be
concatenated onto the end of the netnews makefile.  Once
this is done, "make hpvnews" will compile vnews using the
virtterm package, "make hvnews" will compile vnews using
Mark Horton's curses, and "make avnews" will compile vnews
using Ken Arnold's curses.  The shell procedure mkvnews will
compile vnews and install it in /usr/bin.  Modify the
assignment to "type" to have it compile the version of vnews
that you desire.
!

cat > convert.term <<\!
HOW TO CONVERT VIRTTERM TO RUN WITH A DIFFERENT TERMINAL TYPE:


I recomended that you make the minimum number of changes neces-
sary to make the code work, and worry about efficiency only after
you have something running.  I will describe the routines which
you will probably have to change, but first a few conventions
should be described.

All these routines should perform output by calling _putstr to
output a string or putch to output a single character.  The
current location of the cursor on the screen is always stored in
the variables _srow and _scol; any routine with moves the cursor
must update them.  Most of the terminal escape sequences are de-
fined at the top of the program.  You should change SCLEAR to a
sequence which will clear the screen and leave the cursor in the
upper left hand corner.  As a matter of style, you will probably
want to delete or modify the other sequences.


_amove moves the cursor to the specified row and column.  If your
terminal has an escape sequence to move to an arbitrary location
on the screen, just use that as a first crack.  Although this is
nominally an "internal" routine, visual cheats and calls it any-
way, so you must provide it.  The flag _curjunked indicates that
the values in _srow and _scol do not reflect the actual location
of the cursor; vnews doesn't use it.

In the HP version, the subroutines _relmove and _rmcost are used
by _amove to help find an optimum sequence to move the cursor.
_relmove moves the cursor between two points using only relative
motions, and _rmcost returns the cost of doing so.  You should be
able to convert this code with little effort.  Just delete the
code for sequences (like tab and backtab) which you don't have.

_aputc outputs a character at the current cursor position.  This
routine may have to be modified to update _srow and _scol
correctly when a character is written in the last column of a
line.  The HP code will allow a character to be written to the
bottom right hand corner of the screen, but if your terminal does
not make that easy then simply check for _srow==PAGWID-1 &&
_scol==PAGLEN-1 and return without writing the character.

_setul turns underlining mode on and off.  If your terminal
doesn't support underlining, make this routine a no-op.  Current-
ly, vnews only uses underlining to indicate that the article con-
tains overstrikes, so you may not want to bother with underlining
immediately.

dshift and ushift scroll the article.  If your terminal has line
insert/delete, simply update the definitions of LINSERT and LIN-
DEL the top of the program.  If these aren't defined at all, you
will get a rather shoddy version that will work on many termi-
nals.  If you convert ushift into a no-op, you should remove the
"#define HASSCROLL" line in visual.c.

botscreen moves to one line below the bottom of the screen.  The
version provided will work on most terminals.

_clrtoeol clears everything between the specified locaion and the
end of the line.  If your terminal has "clear to end of line,"
simply update the definition of SCLRLIN.  If not, use the version
which has been ifdeffed out.

_chkclrline is used on the HP to decide whether to output a clear
line sequence before updating a line.  For a first pass, simply
ifdef out the entire body of this routine.
!

cat > mkvnews <<\!
type=hpnews
make $type
echo cp $type /usr/bin/vnews
cp $type /usr/bin/vnews
!

cat > vnews.mk <<\!

# Vnews section of makefile
# This is set up to allow you to make multiple version of vnews.
# You can strip it down for your system.

RDOBJECTS = readnews.o rfuncs.o rfuncs2.o rextern.o process.o rpathinit.o $(OBJECTS)
VOBJECTS = $(RDOBJECTS) visual.o
HPOBJECTS = $(VOBJECTS) virtterm.o
PCOBJECTS = $(VOBJECTS) pcterm.o
HOBJECTS = $(RDOBJECTS) hvisual.o curterm.o
AOBJECTS = $(RDOBJECTS) avisual.o acurterm.o

# Normal vnews (with virtterm interface)
hpvnews:  $(HPOBJECTS)
	$(CC) $(LFLAGS) $(HPOBJECTS) -o $@

# Vnews with IBM PC terminal handler
# Contact spanky!ka if you want this.
pcnews:  $(PCOBJECTS)
	$(CC) $(LFLAGS) $(PCOBJECTS) -o $@

# Vnews with Mark Horton's curses
hvnews:  $(HOBJECTS)
	$(CC) $(LFLAGS) $(HOBJECTS) -lcurses -o $@

# Vnews with Ken Arnold's curses
avnews:  $(AOBJECTS)
	$(CC) $(LFLAGS) $(AOBJECTS) -lcurses -ltermcap -o $@


# dependencies

visual.o:  rparams.h defs.h params.h

hvisual.o: visual.c rparams.h defs.h params.h
	cp visual.c hvisual.c
	$(CC) -c $(CFLAGS) -DHCURSES hvisual.c
	rm hvisual.c

avisual.o: visual.c rparams.h defs.h params.h
	cp visual.c avisual.c
	$(CC) -c $(CFLAGS) -DACURSES avisual.c
	rm avisual.c

acurterm.o: curterm.c
	cp curterm.c acurterm.c
	$(CC) -c $(CFLAGS) -DACURSES acurterm.c
	rm acurterm.c

# Install.  You may have to modify this to work on your system.
vcp: all
	cp vnews $(BINDIR)/vnews
	chown $(NEWSUSER) $(BINDIR)/vnews
	chmod 755 $(BINDIR)/vnews
	cp vnews.help $(LIBDIR)
	cp reply.sh $(LIBDIR)/reply
	cp follow.sh $(LIBDIR)/follow

clean:
	-rm -f visual.o hvisual.o avisual.o virtterm.o curterm.o acurterm.o
!

cat > virtterm.c <<\!
/*
 *  Virtual terminal handler for the HP-2621 terminal.
 *  Written by Kenneth Almquist, AGS Computers  (HO 4C601, X7105).
 */

#include <stdio.h>


#define PAGLEN 24
#define BOTLINE (PAGLEN - 1)
#define PAGWID 80
#define DIRTY 01

/* terminal escape sequences */
#define SHOME "\033H"		/* move cursor to (0, 0) */
#define HOMELEN 2		/* length of SHOME */
#define SCLEAR "\033H\033J"	/* clear screen and move cursor to (0, 0) */
#define SCLRLINE "\033K"	/* clear to end of line */
#define CLRLINELEN 2		/* length of SCLRLINE */
#define SUP "\033A"		/* move cursor up one line */
#define SBKTAB "\033i"		/* back tab */
#define SLINSERT "\033L"	/* insert line */
#define SLINDEL "\033M"		/* delete line */
#define ULON "\033&dD"		/* turn underlining on */
#define ULOFF "\033&d@"		/* turn underlining off */

#define ULINE 0200
#define CURSEEN 1
#define putch(c) vputc(c)

/* Constants accessable by user */
int hasscroll = 1;			/* terminal has line insert/delete */
int LINES = PAGLEN;			/* number of lines on screen */
int COLS = PAGWID;			/* width of screen */

struct line {
	char len;
	char flags;
	char l[PAGWID];
};

int _row, _col;
int _srow, _scol;
struct line _virt[PAGLEN], _actual[PAGLEN];
int _uline = 0;
int _junked = 1;
int _curjunked;
int _dir = 1;



/*
 * Scrolling commands.  These have immediate effect on the screen.
 * It is up to the caller to decide whether scrolling will help.
 */

#ifdef SLINSERT		/* version using line insert/delete */

dshift(top, bot, count) {
	register i;

	if (count < 0) {
		ushift(top, bot, -count);
		return;
	}
	if (_junked || count >= bot - top)
		return;
	for (i = bot - count ; _actual[i].len == 0 ; i--)
		if (i == top)
			return;
	for (i = top ; i <= bot ; i++)
		_virt[i].flags |= DIRTY;
	for (i = bot ; i >= top + count ; i--)
		_actual[i] = _actual[i - count];
	for ( ; i >= top ; i--)
		_actual[i].len = 0;
	_amove(bot - count + 1, 0);
	for (i = count ; --i >= 0 ; )
		_putstr(SLINDEL);
	_amove(top, 0);
	for (i = count ; --i >= 0 ; )
		_putstr(SLINSERT);
	_dir = -1;
}


ushift(top, bot, count) {
	register i;

	if (count < 0) {
		dshift(top, bot, -count);
		return;
	}
	if (_junked || count >= bot - top)
		return;
	for (i = top + count ; _actual[i].len == 0 ; i++)
		if (i == bot)
			return;
	for (i = top ; i <= bot ; i++)
		_virt[i].flags |= DIRTY;
	for (i = top ; i <= bot - count ; i++)
		_actual[i] = _actual[i + count];
	for ( ; i <= bot ; i++)
		_actual[i].len = 0;
	_amove(top, 0);
	for (i = count ; --i >= 0 ; )
		_putstr(SLINDEL);
	_amove(bot - count + 1, 0);
	for (i = count ; --i >= 0 ; )
		_putstr(SLINSERT);
}

#else

dshift(top, bot, count) {
	if (count < 0) {
		ushift(top, bot, -count);
		return;
	}
	/* downward shift not implemented */
}


ushift(top, bot, count) {
	register i;

	if (count < 0) {
		dshift(top, bot, -count);
		return;
	}
	if (_junked || count >= bot - top)
		return;
	for (i = top + count ; _actual[i].len == 0 ; i++)
		if (i == bot)
			return;
	/* we cheat and shift the entire screen */
	/* be sure we are shifting more lines into than out of position */
	if ((bot - top + 1) - count <= PAGLEN - (bot - top + 1))
		return;
	for (i = 0 ; i <= BOTLINE ; i++)
		_virt[i].flags |= DIRTY;
	for (i = 0 ; i <= BOTLINE - count ; i++)
		_actual[i] = _actual[i + count];
	for ( ; i <= BOTLINE ; i++)
		_actual[i].len = 0;
	_amove(BOTLINE, 0);
	for (i = 0 ; i < count ; i++)
		putch('\n');
}

#endif LINSERT



/*
 * generate a beep on the terminal
 */

beep() {
	putch('\7');
}



/*
 * Move to one line below the bottom of the screen.
 */

botscreen() {
	_amove(BOTLINE, 0);
	putch('\n');
	vflush();
}



move(row, col) {
	if (row < 0 || row >= PAGLEN || col < 0 || col >= PAGWID)
		return;
	_row = row;
	_col = col;
}



/*
 * Output string at specified location.
 */

mvaddstr(row, col, str)
	char *str;
	{
	move(row, col);
	addstr(str);
}



addstr(s)
	char *s;
	{
	register char *p;
	register struct line *lp;
	register int col = _col;

	lp = &_virt[_row];
	if (lp->len < col) {
		p = &lp->l[lp->len];
		while (lp->len < col) {
			*p++ = ' ';
			lp->len++;
		}
	}
	for (p = s ; *p != '\0' ; p++) {
		if (*p == '\n') {
			lp->len = col;
			lp->flags |= DIRTY;
			col = 0;
			if (++_row >= PAGLEN)
				_row = 0;
			lp = &_virt[_row];
		} else {
			lp->l[col] = *p;
			lp->flags |= DIRTY;
			if (++col >= PAGWID) {
				lp->len = PAGWID;
				col = 0;
				if (++_row >= PAGLEN)
					_row = 0;
				lp = &_virt[_row];
			}
		}
	}
	if (lp->len <= col)
		lp->len = col;
	_col = col;
}



addch(c) {
	register struct line *lp;
	register char *p;

	lp = &_virt[_row];
	if (lp->len < _col) {
		p = &lp->l[lp->len];
		while (lp->len < _col) {
			*p++ = ' ';
			lp->len++;
		}
	}
	lp->l[_col] = c;
	if (lp->len == _col)
		lp->len++;
	if (++_col >= PAGWID) {
		_col = 0;
		if (++_row >= PAGLEN)
			_row = 0;
	}
	lp->flags |= DIRTY;
}



clrtoeol() {
	register struct line *lp;

	lp = &_virt[_row];
	if (lp->len > _col) {
		lp->len = _col;
		lp->flags |= DIRTY;
	}
}



/*
 * Clear an entire line.
 */

clrline(row) {
	register struct line *lp;

	lp = &_virt[row];
	if (lp->len > 0) {
		lp->len = 0;
		lp->flags |= DIRTY;
	}
}



clear() {
	erase();
	_junked++;
}



erase() {
	register i;

	for (i = 0 ; i < PAGLEN ; i++) {
		_virt[i].len = 0;
		_virt[i].flags |= DIRTY;
	}
}



refresh() {
	register i;
	int j, len;
	register char *p, *q;

	if (checkin())  return;
	if (_junked) {
		_sclear();
		_junked = 0;
	}
	_fixlines();
	for (i = _dir > 0? 0 : BOTLINE ; i >= 0 && i < PAGLEN ; i += _dir) {
		if ((_virt[i].flags & DIRTY) == 0)
			continue;
		_ckclrlin(i);		/* decide whether to do a clear line */
		len = _virt[i].len;
		if (_actual[i].len < len)  len = _actual[i].len;
		p = _virt[i].l;
		q = _actual[i].l;
		for (j = 0 ; j < len ; j++) {
			if (*p != *q) {
				_amove(i, j);
				_aputc(*p);
				*q = *p;
			}
			p++, q++;
		}
		len = _virt[i].len;
		if (_actual[i].len > len) {
			_clrtoeol(i, len);
		} else {
			for ( ; j < len ; j++) {
				if (*p != ' ') {
					_amove(i, j);
					_aputc(*p);
				}
				*q++ = *p++;
			}
			_actual[i].len = len;
		}
		if (checkin())  return;
	}
	_dir = 1;
	if (CURSEEN)
		_amove(_row, _col);
	vflush();		/* flush output buffer */
}


_sclear() {
	register struct line *lp;

	_putstr(SCLEAR);
	_srow = _scol = 0;
	for (lp = _actual ; lp < &_actual[PAGLEN] ; lp++) {
		lp->len = 0;
	}
	for (lp = _virt ; lp < &_virt[PAGLEN] ; lp++) {
		if (lp->len != 0)
			lp->flags |= DIRTY;
	}
}



#ifdef notdef		/* included to simplify conversion to new terminal */

_clrtoeol(row, col) {
	register struct line *lp = &_actual[row];
	register i;

	for (i = col ; i < lp->len ; i++) {
		if (lp->l[i] != ' ') {
			_amove(row, i);
			_aputc(' ');
		}
	}
	lp->len = col;
}

#else	/* HP version */

_clrtoeol(row, col) {
	_amove(row, col);
	if (_actual[row].len == col + 1)
		_aputc(' ');
	else
		_putstr(SCLRLINE);
	_actual[row].len = col;
}

#endif



_fixlines() {
	register struct line *lp;
	register char *p;
	register int i;

	for (i = 0 ; i < PAGLEN ; i++) {
		lp = &_virt[i];
		if (lp->flags & DIRTY) {
			lp = &_virt[i];
			for (p = &lp->l[lp->len] ; --p >= lp->l && *p == ' ' ; );
			lp->len = p + 1 - lp->l;
			if (lp->len == _actual[i].len && strncmp(lp->l, _actual[i].l, lp->len) == 0)
				lp->flags &=~ DIRTY;
		}
	}
}



_ckclrlin(i) {
	int eval;
	int len;
	int first;
	register struct line *vp, *ap;
	register int j;

	ap = &_actual[i];
	vp = &_virt[i];
	len = ap->len;
	eval = -2;
	if (len > vp->len) {
		len = vp->len;
		eval = 0;
	}
	for (j = 0 ; j < len && vp->l[j] == ap->l[j] ; j++);
	if (j == len)
		return;
	first = j;
	while (j < len) {
		if (vp->l[j] == ' ') {
			if (ap->l[j] != ' ') {
				while (++j < len
				    && vp->l[j] == ' ' && ap->l[j] != ' ') {
					eval++;
				}
				if (j == len)
					eval++;
				continue;
			}
		} else {
			if (vp->l[j] == ap->l[j]) {
				while (++j < len && vp->l[j] == ap->l[j]) {
					eval--;
				}
				continue;
			}
		}
		j++;
	}
	for (j = first ; --j >= 0 ; )
		if (vp->l[j] != ' ')
			break;
	if (j < 0)
		first = 0;
	if (eval > 0) {
		_amove(i, first);
		_putstr(SCLRLINE);
		_actual[i].len = first ;
	}
}



_amove(row, col) {
	char s[20];
	int cost, i;

	if (row == _srow && col == _scol && _curjunked == 0)
		return;
	_setul(0);
	/* Warning: omitting the row isn't documented; it seems to work */
	if (row == _srow)
		sprintf(s, "\033&a%dC", col);
	else if (row == _srow + 1)
		sprintf(s, "\n\033&a%dC", col);
	else
		sprintf(s, "\033&a%dy%dC", row, col);
	cost = strlen(s);
	if (_curjunked == 0 && (i = _rmcost(_srow, _scol, row, col)) < cost) {
		_relmove(s, _srow, _scol, row, col);
		cost = i;
	}
	if (_curjunked == 0 && col < _scol) {
		if (_scol < 72 || _srow <= row) {
			if ((i = _rmcost(_srow, 0, row, col) + 1) < cost) {
				s[0] = '\r';
				_relmove(s + 1, _srow, 0, row, col);
				cost = i;
			}
		} else {
			if ((i = _rmcost(_srow + 1, 0, row, col) + 1) < cost) {
				s[0] = '\t';
				_relmove(s + 1, _srow + 1, 0, row, col);
				cost = i;
			}
		}
	}
	if (row < cost && (i = _rmcost(0, 0, row, col) + HOMELEN) < cost) {
		strcpy(s, SHOME);
		_relmove(s + HOMELEN, 0, 0, row, col);
		cost = i;
	}
	_putstr(s);
	_srow = row, _scol = col;
	_curjunked = 0;
}



_rmcost(orow, ocol, nrow, ncol) {
	char s[200];

	_relmove(s, orow, ocol, nrow, ncol);
	return strlen(s);
}



_relmove(s, orow, ocol, nrow, ncol)
	char *s;
	{
	register char *p, *q;
	int tab;

	p = s;
	while (orow < nrow) {			/* cursor down */
		*p++ = '\n';
		orow++;
	}
	while (orow > nrow) {			/* cursor up */
		for (q = SUP ; *p = *q++ ; p++);
		orow--;
	}
	if (ocol < ncol) {			/* tab (nondestructive) */
		while ((tab = (ocol + 8) &~ 07) <= ncol) {
			*p++ = '\t';
			ocol = tab;
		}
		if (tab < PAGWID && tab - ncol < ncol - ocol) {
			*p++ = '\t';
			ocol = tab;
		}
	}
	else if (ocol > ncol) {			/* backwards tab */
		while ((tab = (ocol - 1) &~ 07) >= ncol) {
			if (tab == ocol - 1)
				*p++ = '\b';
			else
				for (q = SBKTAB ; *p = *q++ ; p++);
			ocol = tab;
		}
		if (ncol - tab + 1 < ocol - ncol) {
			for (q = SBKTAB ; *p = *q++ ; p++);
			ocol = tab;
		}
	}
	while (ocol > ncol) {			/* cursor left */
		*p++ = '\b';
		ocol--;
	}
	if (ocol < ncol) {			/* cursor right */
		register struct line *lp = &_actual[nrow];
		while (ocol < ncol) {
			if (ocol < lp->len)
				*p++ = lp->l[ocol];
			else
				*p++ = ' ';
			ocol++;
		}
	}
	*p++ = '\0';
}



_aputc(c) {
	_setul(c & ULINE);		
	putch(c &~ ULINE);
	if (++_scol >= PAGWID) {
		_scol = 0;
		if (++_srow >= PAGLEN) {
			_srow = 0;
			_putstr(SHOME);
		}
	}
}


_setul(on) {
	if (on) {
		if (_uline == 0) {
			_putstr(ULON);
			_uline = 1;
		}
	} else {
		if (_uline != 0) {
			_putstr(ULOFF);
			_uline = 0;
		}
	}
}


_putstr(s)
	char *s;
	{
	register char *p;

	for (p = s ; *p ; p++) {
		putch(*p);
	}
}
!

cat > reply.sh <<\!
trap : 2 3
/bin/cp $1 /tmp/mailr$$a
A=$2 ${EDITOR-ed} $1 $2
if /bin/cmp -s $1 /tmp/mailr$$a
then	rm -f $1 /tmp/mailr$$a
	exit 22
fi
(
	trap '' 1 2 3
	recmail <$1
	x=$?
	if test $x != 0
	then	echo 'Subject: reply command failed\n' | \
			cat - /tmp/mailr$$b | mail "${LOGNAME-$USER}"
	fi
	rm -f $1 /tmp/mailr$$a /tmp/mailr$$b
) > /tmp/mailr$$b 2>&1 &
exit 0
!

cat > follow.sh <<\!
trap : 2 3
/bin/cp $1 /tmp/fol$$a
A=$2 ${EDITOR-ed} $1 $2
if /bin/cmp -s $1 /tmp/fol$$a
then	/bin/rm -f $1 /tmp/fol$$a
	exit 22
fi
(
	trap '' 1 2 3
	inews -h < $1
	x=$?
	/bin/sleep 60	# wait for inews to finish
	if test -s /tmp/fol$$b -o $x != 0
	then	(
			/bin/echo 'Subject: followup failed\n'
			/bin/cat /tmp/fol$$b
			/bin/echo '\nYour article follows:'
			/bin/cat $1
		) | mail "${LOGNAME-$USER}"
	fi
	/bin/rm -f $1 /tmp/fol$$a /tmp/fol$$b

) > /tmp/fol$$b 2>&1 &
exit 0
!

cat > vnews.help <<\!
Vnews commands:    (each may be preceded by a non-negative )

CR  Next page or article                D   Decrypt a rot 13 joke
n   Go to next article                  A   Go to article numbered count
e   Mark current article as unread      <   Go to article with given ID
+   Go forwards count articles          p   Go to parent article
b   Go to previous article              ug  Unsubscribe to this group
^B  Go backwards count pages            ^L  Redraw screen
^N  Go forward count lines              v   Printf netnews version
^P  Go backwards count lines            q   Quit
^D  Go forward half a page              ^\  Quit without updating .newsrc
h   Display article header              ?   Display this message
!   Escape to shell
r   Reply to article
f   Post a followup article
l   Display article (use after !, r, f, or ?)
s   Save article in file
w   Save without header
c   Cancel the current article
N   Advance to newsgroup (next is default)

[Press l to see article again]
!