[comp.sources.misc] Font editor for VT220 terminals

allbery@ncoast.UUCP (Brandon S. Allbery) (05/30/87)

Here is a program which is used to create new character sets for VT220
terminals.  It was compiled on a System V machine.  There should be
nothing special to prevent it from compiling on any other UN*X though. 
To compile - just cc the .c file!

Happy chars to you!

#! /bin/sh
# This is a shell archive, meaning:
# 1. Remove everything above the #! /bin/sh line.
# 2. Save the resulting text in a file.
# 3. Execute the file with /bin/sh (not csh) to create:
#	fontedit.1
#	fontedit.c
# This archive created: Sat May 30 13:34:25 1987
export PATH; PATH=/bin:/usr/bin:$PATH
if test -f 'fontedit.1'
then
	echo shar: "will not over-write existing file 'fontedit.1'"
else
cat << \SHAR_EOF > 'fontedit.1'
.TH TOP 1 LOCAL
.SH NAME
fontedit \- Edit fonts.
.SH SYNOPSIS
.B fontedit file
.SH DESCRIPTION
.I Fontedit
is used to edit the down line reloadable character set (DRCS) of a VT220
terminal.  The editor has two display areas, one for displaying the
entry currently being manipulated, and one for displaying the complete
DRCS.  Commands to the editor take the form of function keys.
.PP
.I Fontedit 
takes one command line parameter, a file name.  This file is
used to save the character set.  If the file exists when \fIfontedit\fP
is invoked, it is read in to initialize the DRCS.  The file is written
to when \fIfontedit\fP exits.
.PP
Commands to fontedit take the form of function keys.  The current
definitions are:
.IP \fBHELP\fP
Display a help screen.
.IP \fBF6\fP
Turn the pixel under the cursor on.
.IP \fBF7\fP
Turn the pixel under the cursor off.
.IP \fBF13\fP
Clear the display area.
.IP \fBFind\fP
Save the current font in the font table.  Update the DRCS display.
.IP \fBSelect\fP
Extract the entry selected by the cursor in the DRCS display.
.IP \fBPrev\fP
Move the cursor to the previous entry in the DRCS display.
.IP \fBNext\fP
Move the cursor to the next entry in the DRCS display.
.IP \fBInsert\fP
Insert a blank line at the current cursor position.  The bottom row is lost.
.IP \fBRemove\fP
Remove the row at the current cursor position.  All rows below the
current one are shifted up.
.IP \fBCursors\fP
Move the cursor in the main display area.
.PP
If the screen gets garbled, press <control-L>.
.PP
To exit \fIfontedit\fP, press <control-D>.  The DRCS will be saved in
\fIfile\fP.  To exit without saving the DRCS, hit interrupt (usually
DEL). 
.SH DIAGNOSTICS
.I Fontedit
will issue a warning when the entry being worked on is not saved, and
some potentially destructive command, like \fBSelect\fP is used.  To
override the warning message, immediately reissue the command.
.SH AUTHOR
Greg Franks.


SHAR_EOF
fi
if test -f 'fontedit.c'
then
	echo shar: "will not over-write existing file 'fontedit.c'"
else
cat << \SHAR_EOF > 'fontedit.c'
/*
 * fontedit
 *	Fonteditor for VT220
 *
 *  BUGS:
 *	o Cursor motion is less than optimal (but who cares at 9600),
 *
 *  COMPILE:
 *	cc -O fontedit.c -o fontedit
 *
 *
 *	Copyright (c) 1987 by Greg Franks.
 *
 *	Permission is granted to do anything you want with this program
 *	except claim that you wrote it.
 */

#include <stdio.h>
#include <sys/termio.h>
#include <signal.h>

#define	MAX_ROWS	10
#define	MAX_COLS	8

typedef enum { false, true } bool;

#define	KEY_FIND 	0x0100
#define	KEY_INSERT 	0x0101
#define	KEY_REMOVE 	0x0102
#define	KEY_SELECT 	0x0103
#define	KEY_PREV 	0x0104
#define	KEY_NEXT 	0x0105
#define	KEY_F6		0X0106
#define	KEY_F7		0x0107
#define	KEY_F8		0x0108
#define	KEY_F9		0x0109
#define	KEY_F10		0x010a
#define	KEY_F11		0x010b
#define	KEY_F12		0x010c
#define	KEY_F13		0x010d
#define	KEY_F14		0x010e
#define	KEY_HELP	0x010f
#define	KEY_DO		0x0110
#define	KEY_F17		0x0111
#define	KEY_F18		0x0112
#define	KEY_F19		0x0113
#define	KEY_F20		0x0114
#define	KEY_UP 		0x0115
#define	KEY_DOWN 	0x0116
#define	KEY_RIGHT 	0x0117
#define	KEY_LEFT 	0x0118

/*
 * Position of main drawing screen.
 */

#define	ROW_OFFSET	3
#define COL_OFFSET	10

/* 
 * Position of the DRCS table.
 */

#define	TABLE_ROW	4
#define	TABLE_COL	50

/*
 *
 */

#define	ERROR_ROW	20
#define ERROR_COL	40

bool	display_table[MAX_ROWS][MAX_COLS];

#define	TOTAL_ENTRIES	(128 - 32)
#define	SIXELS_PER_CHAR	16

char	font_table[TOTAL_ENTRIES][SIXELS_PER_CHAR];
unsigned int	current_entry;

struct termio old_stty, new_stty;

FILE * font_file = (FILE *)0;


/*
 * Interrupt
 *	Exit gracefully.
 */

interrupt()
{
	void clear_screen();

	ioctl( 0, TCSETA, &old_stty );
	clear_screen();
	exit( 0 );
}


/*
 * Main
 *	Grab input/output file and call main command processor.
 */
	
main( argc, argv )
int argc;
char *argv[];
{
	void command(), init_restore(), clear_screen();
	void save_table(), get_table(), extract_entry();

	if ( argc != 2 ) {
		fprintf( stderr, "useage: fontedit filename\n" );
		exit( 1 );
	}

	printf( "Press HELP for help\n" );
	printf( "\033P1;1;2{ @\033\\" );	/* Clear font buffer	*/
	fflush( stdout );
	sleep( 1 );			/* Let terminal catch up	*/
					/* otherwise we get frogs	*/

	if ( ( font_file = fopen( argv[1], "r" ) ) != (FILE *)0 ) {
		get_table( font_file );
		fclose( font_file );
	}

	if ( ( font_file = fopen( argv[1], "r+" ) ) == (FILE *)0 ) {
		fprintf( stderr, "Cannot open %s for writing\n", argv[1] );
		exit( 1 );
	}

	ioctl( 0, TCGETA, &old_stty );
	signal( SIGINT, interrupt );

	new_stty = old_stty;
	new_stty.c_lflag &= ~ICANON;
	new_stty.c_cc[VMIN] = 1;
	ioctl( 0, TCSETA, &new_stty );

	current_entry = 1;
	extract_entry( current_entry );
	init_restore();
	command();

	ioctl( 0, TCSETA, &old_stty );
	clear_screen();

	/* Overwrite the old file. */

	fseek( font_file, 0L, 0 );
	save_table( font_file );
	fclose( font_file );
}



/*
 * Command
 *	Process a function key. 
 *
 *	The user cannot fill in slots 0 or 95 (space and del respecitively).
 */

void
command()
{
	register int c;
	register int row, col;
	register int i, j;
	bool change, error, override;

	void build_entry(), extract_entry(), send_entry(), print_entry();
	void highlight(), draw_current(), init_restore(), help();
	void warning();

	change = false;
	error = false;
	override = false;
	row = 0; col = 0;
	highlight( row, col, true );

	for ( ;; ) {
		c = get_key();
		highlight( row, col, false );	/* turn cursor off	*/

		if ( error ) {
			move ( ERROR_ROW, ERROR_COL );
			printf( "\033[K" );		/* Clear error message	*/
			move ( ERROR_ROW+1, ERROR_COL );
			printf( "\033[K" );		/* Clear error message	*/
			error = false;
		} else {
			override = false;
		}

		switch ( c ) {

		case KEY_FIND:		/* update DRCS 	*/
			if ( !change && !override ) {
				warning( "No changes to save" );
				override = true;
				error = true;
			} else {
				build_entry( current_entry );
				send_entry( current_entry );
				print_entry( current_entry, true );
				change = false;
			}
			break;

		case KEY_F6:		/* Turn on pixel	*/
			change = true;
			display_table[row][col] = true;
			highlight( row, col, false );
			col = ( col + 1 ) % MAX_COLS;
			if ( col == 0 )
				row = ( row + 1 ) % MAX_ROWS;
			break;

		case KEY_F7:		/* Turn off pixel	*/
			change = true;
			display_table[row][col] = false;
			highlight( row, col, false );
			col = ( col + 1 ) % MAX_COLS;
			if ( col == 0 )
				row = ( row + 1 ) % MAX_ROWS;
			break;

		case KEY_INSERT:	/* Insert a blank row	*/
			change = true;
			for ( j = 0; j < MAX_COLS; ++j ) {
				for ( i = MAX_ROWS - 1; i > row; --i ) {
					display_table[i][j] = display_table[i-1][j];
				}
				display_table[row][j] = false;
			}
			draw_current();
			break;
	
		case KEY_REMOVE:	/* Remove a row	*/
			change = true;
			for ( j = 0; j < MAX_COLS; ++j ) {
				for ( i = row; i < MAX_ROWS - 1; ++i ) {
					display_table[i][j] = display_table[i+1][j];
				}
				display_table[MAX_ROWS-1][j] = false;
			}
			draw_current();
			break;

		case KEY_F13:		/* Clear buffer	*/
			if ( change && !override ) {
				warning( "Changes not saved" );
				error = true;
				override = true;
			} else {
				for ( j = 0; j < MAX_COLS; ++j ) {
					for ( i = 0; i < MAX_ROWS; ++i ) {
						display_table[i][j] = false;
					}
				}
				draw_current();
			}
			break;

		case KEY_SELECT:	/* Select font from DRCS	*/
			if ( change && !override ) {
				warning( "Changes not saved" );
				error = true;
				override = true;
			} else { 
				extract_entry( current_entry );
				draw_current();
			}
			break;

		case KEY_PREV:		/* Move to prev entry in DRCS	*/
			if ( change && !override ) {
				warning( "Changes not saved" );
				override = true;
				error = true;
			} else {
				print_entry( current_entry, false );
				current_entry = current_entry - 1;
				if ( current_entry == 0 ) 
					current_entry = TOTAL_ENTRIES - 2;
				print_entry( current_entry, true );
			}
			break;

		case KEY_NEXT:		/* Move to next entry in DRCS	*/
			if ( change && !override ) {
				warning( "Changes not saved" );
				override = true;
				error = true;
			} else {
				print_entry( current_entry, false );
				current_entry = current_entry + 1;
				if ( current_entry  == TOTAL_ENTRIES - 1 )
					current_entry = 1;
				print_entry( current_entry, true );
			}
			break;

		case KEY_UP:		/* UP one row.			*/
			if ( row == 0 )
				row = MAX_ROWS;
			row = row - 1;
			break;

		case KEY_DOWN:		/* Guess.			*/
			row = ( row + 1 ) % MAX_ROWS;
			break;

		case KEY_RIGHT:
			col = ( col + 1 ) % MAX_COLS;
			break;

		case KEY_LEFT:
			if ( col == 0 ) 
				col = MAX_COLS;
			col = col - 1;
			break;

		case KEY_HELP:		/* Display helpful info		*/
			clear_screen();
			help();
			c = getchar();
			init_restore();
			break;

		case '\004':		/* All done!			*/
			return;

		case '\f':		/* Redraw display		*/
			init_restore();
			break;

		default:		/* user is a klutzy  typist	*/
			move ( ERROR_ROW, ERROR_COL );
			printf( "Unknown key: " );
			if ( c < 0x20 ) {
				printf( "^%c", c );
			} else if ( c < 0x0100 ) {
				printf( "%c", c );
			} else {
				printf( "0x%04x", c );
			}
			fflush( stdout );
			error = true;
		}

		highlight( row, col, true );	/* turn cursor on	*/
	}
}



char *key_table[]	= {
	"\033[1~",		/* Find		*/
	"\033[2~",		/* Insert	*/
	"\033[3~",		/* Remove	*/
	"\033[4~",		/* Select	*/
	"\033[5~",		/* Prev		*/
	"\033[6~",		/* Next		*/
	"\033[17~",
	"\033[18~",
	"\033[19~",
	"\033[20~",
	"\033[21~",
	"\033[23~",
	"\033[24~",
	"\033[25~",
	"\033[26~",
	"\033[28~",
	"\033[29~",
	"\033[31~",
	"\033[32~",
	"\033[33~",
	"\033[34~",
	"\033[A",
	"\033[B",
	"\033[C",
	"\033[D",
	(char *)0 };

/*
 * get_key
 *	Convert VT220 escape sequence into something more reasonable.
 */

int
get_key()
{
	register char	*p;
	char	s[10];
	register int i, j;

	p = s;
	for ( i = 0; i < 10; ++i ) {
		*p = getchar();
		if ( i == 0 && *p != '\033' )
			return( (int)*p );	/* Not an escape sequence */
		if ( *p != '\033' && *p < 0x0020 )
			return( (int)*p );	/* Control character	*/
		*++p = '\0';			/* Null terminate	*/
		for ( j = 0; key_table[j]; ++j ) {
			if ( strcmp( s, key_table[j] ) == 0 )
				return( j | 0x0100 );
		}
	}
	return( -1 );
}



/*
 * pad
 *	Emit nulls so that the terminal can catch up.
 */

pad()
{
	int i;

	for ( i = 0; i < 20; ++i )
		putchar( '\000' );
	fflush( stdout );
}



/*
 * init_restore
 *	refresh the main display table.
 */

void
init_restore()
{
	register int row, col;
	register int i;
	
	void  draw_current(), clear_screen(), print_entry();

	clear_screen();

	for ( col = 0; col < MAX_COLS; ++col ) {
		move( ROW_OFFSET - 2, col * 3 + COL_OFFSET + 1 );
		printf( "%d", col );
	}
	move( ROW_OFFSET - 1, COL_OFFSET );
	printf( "+--+--+--+--+--+--+--+--+" );
	move( ROW_OFFSET + MAX_ROWS * 2, COL_OFFSET );
	printf( "+--+--+--+--+--+--+--+--+" );

	for ( row = 0; row < MAX_ROWS; ++row ) {
		if ( row != 0 && row != 7 )  {
			move( row * 2 + ROW_OFFSET, COL_OFFSET - 2 );
			printf( "%d|", row );
			move( row * 2 + ROW_OFFSET + 1, COL_OFFSET - 1 );
			printf( "|" );
			move( row * 2 + ROW_OFFSET, COL_OFFSET + MAX_COLS * 3 );
			printf( "|" );
			move( row * 2 + ROW_OFFSET + 1, COL_OFFSET + MAX_COLS * 3 );
			printf( "|" );
		} else {
			move( row * 2 + ROW_OFFSET, COL_OFFSET - 2 );
			printf( "%d*", row );
			move( row * 2 + ROW_OFFSET + 1, COL_OFFSET - 1 );
			printf( "*" );
			move( row * 2 + ROW_OFFSET, COL_OFFSET + MAX_COLS * 3 );
			printf( "*" );
			move( row * 2 + ROW_OFFSET + 1, COL_OFFSET + MAX_COLS * 3 );
			printf( "*" );
		}
	}
	draw_current();

	move( TABLE_ROW - 1, TABLE_COL - 1 );
	printf( "+-+-+-+-+-+-+-+-+-+-+-+-+" );
	move( TABLE_ROW + 8 * 2 - 1, TABLE_COL - 1 );
	printf( "+-+-+-+-+-+-+-+-+-+-+-+-+" );
	for ( i = 0; i < 8; ++i ) {
		move ( TABLE_ROW + i * 2, TABLE_COL - 1 );
		printf( "|" );
		move ( TABLE_ROW + i * 2 + 1, TABLE_COL - 1 );
		printf( "+" );
		move ( TABLE_ROW + i * 2, TABLE_COL + 12 * 2 - 1);
		printf( "|" );
		move ( TABLE_ROW + i * 2 + 1, TABLE_COL +12 * 2 - 1);
		printf( "+" );
	}	
	for ( i = 0; i < TOTAL_ENTRIES; ++i )
		print_entry( i, (i == current_entry) ? true : false );
}



/*
 * draw_current
 *	Draw the complete current entry.
 */

void
draw_current()
{
	register int row, col;

	printf( "\033)0" );		/* Special graphics in G1	*/
	printf( "\016" );		/* Lock in G1 (SO)		*/

	for ( row = 0; row < MAX_ROWS; ++row ) {
		for ( col = 0; col < MAX_COLS; ++col ) {
			if ( display_table[row][col] ) {
				move( row * 2 + ROW_OFFSET,     col * 3 + COL_OFFSET );
				printf( "\141\141\141" );
				move( row * 2 + ROW_OFFSET + 1, col * 3 + COL_OFFSET );
				printf( "\141\141\141" );
			} else {
				move( row * 2 + ROW_OFFSET,     col * 3 + COL_OFFSET );
				printf( "   " ); 	/* erase splat	*/
				move( row * 2 + ROW_OFFSET + 1, col * 3 + COL_OFFSET );
				printf( "   " ); 	/* erase splat	*/
			}
		}
		pad();
	}
	printf( "\017" );		/* Lock in G0 (SI)	*/
	fflush( stdout );
}



/*
 * highlight
 *	Draw the cursor in the main display area.
 */
 
void
highlight( row, col, on )
unsigned int row, col;
bool on;
{

	printf( "\033)0" );		/* Special graphics in G1	*/
	printf( "\016" );		/* Lock in G1 (SO)		*/
	if ( on ) {
		printf( "\033[7m" );	/* Reverse video cursor		*/
	}

	if ( display_table[row][col] ) {
		move( row * 2 + ROW_OFFSET,     col * 3 + COL_OFFSET );
		printf( "\141\141\141" );
		move( row * 2 + ROW_OFFSET + 1, col * 3 + COL_OFFSET );
		printf( "\141\141\141" );
	} else {
		move( row * 2 + ROW_OFFSET,     col * 3 + COL_OFFSET );
		printf( "   " ); 	/* erase splat	*/
		move( row * 2 + ROW_OFFSET + 1, col * 3 + COL_OFFSET );
		printf( "   " ); 	/* erase splat	*/
	}
	pad();
	printf( "\017" );		/* Lock in G0 (SI)	*/
	printf( "\033[0m" );		/* normal video		*/
	printf( "\b" );			/* Back up one spot	*/
	fflush( stdout );
}



/*
 * Clear_screen
 */
 
void
clear_screen()
{
	printf( "\033[H\033[J" );		/* Clear screen.	*/
	fflush( stdout );
}



/*
 * move
 */

move( y, x )
int y, x;
{
	printf( "\033[%d;%df", y, x );
}



/*
 * Build_entry
 *	Convert the bit pattern used in the main display area into something
 *	that the vt220 can digest - namely sixels...
 */
 
void
build_entry( entry_no )
unsigned int entry_no;
{
	register int row, col;
	register unsigned int mask;

	for ( col = 0; col < 8; ++col ) {

		/* Top set of sixels	*/

		mask = 0;
		for ( row = 5; row >= 0; --row ) {
			mask = mask << 1;
			if ( display_table[row][col] )
				mask |= 1;
		}
		font_table[entry_no][col] = mask + 077;

		/*  Bottom set of sixels	*/

		mask = 0;
		for ( row = 9; row >= 6; --row ) {
			mask = mask << 1;
			if ( display_table[row][col] )
				mask |= 1;
		}
		font_table[entry_no][col+8] = mask + 077;
	}
		
}



/*
 * Extract_engry
 *	convert sixel representation into an array of bits.
 */

void
extract_entry( entry_no )
unsigned int entry_no;
{
	register int row, col;
	register unsigned int mask;

	for ( col = 0; col < 8; ++col ) {

		/* Top set of sixels	*/

		mask = font_table[entry_no][col];
		if ( mask >= 077 ) 
			mask -= 077;
		else
			mask = 0;		/* Bogus entry	*/

		for ( row = 0; row <= 5; ++row ) {
			display_table[row][col] = (bool)(mask & 0x0001);
			mask = mask >> 1;
		}

		/*  Bottom set of sixels	*/

		mask = font_table[entry_no][col+8];
		if ( mask >= 077 )
			mask -= 077;
		else
			mask = 0;

		for ( row = 6; row <= 9; ++row ) {
			display_table[row][col] = (bool)(mask & 0x0001);
			mask = mask >> 1;
		}
	}
		
}



/*
 * Send_entry
 *	Emit the stuff used by the VT220 to load a character into the
 *	DRCS.  We could, of course, send more than one entry at a time...
 */

void
send_entry( entry_no )
int entry_no;
{
	register char *fp	= font_table[entry_no];

	printf( "\033P1;%d;1;0;0;0{ @%c%c%c%c%c%c%c%c/%c%c%c%c%c%c%c%c\033\\", 
		entry_no,
		fp[ 0], fp[ 1], fp[ 2], fp[ 3], fp[ 4], fp[ 5], fp[ 6], fp[ 7],
		fp[ 8], fp[ 9], fp[10], fp[11], fp[12], fp[13], fp[14], fp[15] );
}



/*
 * Print_entry
 *	The terminal normally has G0 in GL.  We don't want to change
 *	this, nor do we want to use GR.  Sooooo send out the necessary
 *	magic for shifting in G2 temporarily for the character that we
 *	want to display.
 */

void
print_entry( entry_no, highlight )
register unsigned int entry_no;
bool highlight;
{

	register int y, x;

	y = entry_no & 0x07;
	x = entry_no >> 3 & 0x1f;
	entry_no += 32;			/* Map up to G set	*/

	move( y * 2 + TABLE_ROW, x * 2 + TABLE_COL );

	if ( highlight )
		printf( "\033[7m" );

	printf( "\033* @" );		/* select DRCS into G2	*/
	printf( "\033N" );		/* select single shift	*/
	printf( "%c", entry_no );	/* Draw the character	*/

	if ( highlight )
		printf( "\033[0m" );
}



/*
 * Save_table
 *	Save a font table
 */

void
save_table( font_file )
FILE *font_file;
{
	register char *fp;
	register int i;

	for ( i = 0; i < TOTAL_ENTRIES; ++i ) {
		fp = font_table[i];
		fprintf( font_file, "\033P1;%d;1;0;0;0{ @%c%c%c%c%c%c%c%c/%c%c%c%c%c%c%c%c\033\\\n", 
			i, 
			fp[ 0], fp[ 1], fp[ 2], fp[ 3], fp[ 4], fp[ 5], fp[ 6], fp[ 7],
			fp[ 8], fp[ 9], fp[10], fp[11], fp[12], fp[13], fp[14], fp[15] );
	}
}



/*
 * Get_table
 *	Extract font table entries from a file
 */

void
get_table( font_file )
FILE *font_file;
{
	char	s[256];
	register char	*p;
	char	*fp;
	int i;
	register int j;

	while( fgets( s, 255, font_file ) ) {
		if ( strncmp( s, "\033P1;", 4 ) !=  0 ) 
			continue;	/* Bogus line	*/
		p = &s[4];
		if ( sscanf( p, "%d", &i ) != 1 ) 
			continue;	/* Illegal entry number	*/

		if ( i <= 0 || TOTAL_ENTRIES <= i )
			continue;	/* Bogues entry	*/

		fp = font_table[i];

		while ( *p && *p != '@' )
			++p;		/* Skip to font definition */
		if ( ! *p++ ) 
			continue;	/* Skip @	*/

		for ( j = 0; *p && *p != '\033' && j < 16; ++j, ++p ) {
			if ( *p == '/' ) {
				j = 8; 
				++p;
			}
			fp[j] = *p;
		}
		send_entry( i );
	}
}



/*
 * Help
 *	Print out help information.
 */
 
void
help()
{
	printf( "Font editor\n\n" );
	printf( "F6     - Pixel on\n" );
	printf( "F7     - Pixel off\n" );
	printf( "F13    - Clear display area\n" );
	printf( "HELP   - This screen\n" );
	printf( "FIND   - Update font table\n" );
	printf( "INSERT - Insert a blank row\n" );
	printf( "REMOVE - Remove a row\n" );
	printf( "SELECT - Select current font table entry\n" );
	printf( "PREV   - Move to previous font table entry\n" );
	printf( "NEXT   - Move to next font table entry\n" );
	printf( "^D     - Exit\n" );
	printf( "\n\n\n\nPress any key to continue\n" );
}



/*
 * Warning
 *	Issue a warning to the regarding the current status.
 */

void
warning( s )
char *s;
{
	move( ERROR_ROW, ERROR_COL );
	printf( "Warning: %s!\n", s );
	move( ERROR_ROW+1, ERROR_COL );
	printf( "         Reissue command to override\n" );
}
SHAR_EOF
fi
exit 0
#	End of shell archive
-- 
Greg Franks     (613) 725-5411          "Vermont ain't flat"
{net-land}!utzoo!dciem!nrcaer!xios!greg
(Other paths will undoubtably work too - your mileage will vary)

-- 
Brandon S. Allbery	{decvax,cbatt,cbosgd}!cwruecmp!ncoast!allbery
Tridelta Industries	{ames,mit-eddie,talcott}!necntc!ncoast!allbery
7350 Corporate Blvd.	necntc!ncoast!allbery@harvard.HARVARD.EDU
Mentor, OH 44060	+01 216 255 1080	(also eddie.MIT.EDU)