/*
 *   Copyright (c) 1998 David Parsons. 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 David Parsons
 *   (orc@pell.chi.il.us)
 *
 *  4. My name may not be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *     
 *  THIS SOFTWARE IS PROVIDED BY DAVID PARSONS ``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 DAVID
 *  PARSONS 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.
 */
static const char rcsid[] = "$Id: s.tip.c 1.5 00/05/21 16:33:42-00:00 orc $";
/*
 * tip: terminal interaction program, sorta like what I remember
 * from using tip almost a decade ago.
 *
 * this implementation of tip uses flock() locking to reserve the
 * tty device for itself.
 */

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>
#include <ctype.h>
#include <errno.h>
#include <stdlib.h>

#include <basis/options.h>

struct termios tty, saved_tty, modem, saved_modem;
int modem_fd;

char homedir[512];

char linekill, charkill;

#define BACKSPACE	fwrite("\010 \010", 3, 1, stdout), fflush(stdout);

/*
 * bpstab[] maps permitted baudrates to what the termios flags for
 * those baudrates are.
 */
struct bpstab {
    int flag;	/* termios flag */
    int speed;	/* actual speed */
} bpstab[] = {
    { B50,	50	},
    { B75,	75	},
    { B110,	110	},
    { B134,	134	},
    { B150,	150	},
    { B200,	200	},
    { B300,	300	},
    { B600,	600	},
    { B1200,	1200	},
    { B1800,	1800	},
    { B2400,	2400	},
    { B4800,	4800	},
    { B9600,	9600	},
#ifdef B19200
    { B19200,	19200	},
#endif
#ifdef B38400
    { B38400,	38400	},
#endif
#ifdef B57600
    { B57600,	57600	},
#endif
#ifdef B115200
    { B115200,	115200	},
#endif
#ifdef B23040
    { B230400,	230400	},
#endif
} ;
#define NRBPS	(sizeof bpstab/sizeof bpstab[0])

int
bps(int suggested_speed)
{
    int x;

    for (x = 0; x < NRBPS; x++)
	if (bpstab[x].speed == suggested_speed)
	    return bpstab[x].flag;

    return 0;
}


void
console_mode()
{
    tcsetattr(0, TCSANOW, &saved_tty);
    /*fcntl(0, F_SETFL, fcntl(0, F_GETFL) & ~O_NONBLOCK);*/
}


void
modem_mode()
{
    /*fcntl(0, F_SETFL, fcntl(0, F_GETFL) | O_NONBLOCK);*/
    tcsetattr(0, TCSANOW, &tty);
}


void
ttyclose(int fd)
{
    close(fd);	/* implicitly drops file lock? */
} /* ttyclose */


int
recover_tty()
{
    console_mode();
    tcsetattr(modem_fd, TCSANOW, &saved_modem);
    ttyclose(modem_fd);
}


int
ttyopen(char *device)
{
    int fd;
    int rc;

    /* open the device */
    if ((fd = open(device, O_RDWR|O_NONBLOCK)) < 0)
	return EOF;

    /* try to lock it */
    if ((rc = flock(fd, LOCK_EX|LOCK_NB)) < 0) {
	/* nope */
	close(fd);
	return EOF;
    }
    return fd;
} /* ttyopen */


int
getline(char *string, int len)
{
    char *p;
    unsigned char bfr[1];
    unsigned char c;
    int touch;
    fd_set rdset;

    --len;
    p = string;
    while (1) {
	FD_ZERO(&rdset);
	FD_SET(0, &rdset);

	if (select(1+0, &rdset, 0, 0, 0) > 0) {
	    read(0, bfr, 1);

	    c = bfr[0];

	    touch = 0;
	    if (c == charkill) {
		if (p > string) {
		    BACKSPACE;
		    --p;
		    touch++;
		}
		else
		    return -1;
	    }
	    else if (c == linekill) {
		if (p > string) {
		    touch++;
		    while (p-- > string)
			BACKSPACE;
		}
	    }
	    else if (c == '\r' || c == '\n') {
		putchar('\r');
		putchar('\n');
		*p = 0;
		return p-string;
	    }
	    else if (p < string+len) {
		*p++ = c;
		if (c >= 32)
		    putchar(c);
		else
		    putchar('?');
		touch++;
	    }
	    if (!touch)
		putchar(7);
	    fflush(stdout);
	}
    }
} /* getline */


int
run_command(char cmdchar, int input, int output)
{
    pid_t pid;
    char command[200];
    int rc;

    putchar(cmdchar);
    putchar(' ');
    fflush(stdout);

    if ((rc = getline(command, sizeof command)) > 0) {
	if ((pid = fork()) == 0) {
	    if (input != 0) 
		dup2(input, 0);
	    if (output != 1)
		dup2(output, 1);
	    console_mode();
	    system(command);
	    modem_mode();
	    exit(0);
	}
	else if (pid < 0) {
	    console_mode();
	    perror(command);
	    modem_mode();
	}
	else
	    waitpid(pid, 0, 0);
    }
    else if (rc < 0) {
	BACKSPACE;
	BACKSPACE;
	return 0;
    }

    return 1;
}


int
change_dir()
{
    char directory[512];
    int rc;

    putchar('c');
    putchar(' ');
    fflush(stdout);
    if ((rc = getline(directory, sizeof directory)) < 0) {
	BACKSPACE;	/* back over ' ' */
	BACKSPACE;	/* back over 'c' */
	return 0;
    }
    if (rc == 0)
	strncpy(directory, homedir, sizeof directory);
    if (chdir(directory) == -1) {
	console_mode();
	perror(directory);
	modem_mode();
    }
    return 1;
}


int
run_shell()
{
    char *p = getenv("SHELL");

    console_mode();
    puts("[shell]");
    system( p ? p : "/bin/sh");
    puts("[back in tip]");
    modem_mode();
    return 1;
}


int
tipcmd(char cmd)
{
    int rc;

    switch (cmd) {
    case '.':
	    putchar('\r');
	    exit(0);
    case '?':
	    console_mode();
	    printf("enter command mode with [enter]~\n"
		   "commands are:\n\n"
		   " .\t-- drop the connection and exit\n"
		   " ?\t-- this menu\n"
		   " !\t-- execute a command\n"
		   " C\t-- execute a command with stdin/stdout attached to the modem\n"
		   " |\t-- execute a command with stdin attached to the modem\n"
		   " $\t-- execute a command with stdout attached to the modem\n"
		   " c\t-- change directory\n"
		   " ~\t-- literal ~\n");
	    modem_mode();
	    rc = 1;
	    break;
    case '!':
	    rc = run_shell();
	    break;
    case 'C':
	    rc = run_command(cmd, modem_fd, modem_fd);
	    break;
    case '$':
	    rc = run_command(cmd, 0, modem_fd);
	    break;
    case '|':
	    rc = run_command(cmd, modem_fd, 1);
	    break;
    case '~':
	    BACKSPACE;
	    write(modem_fd, "~", 1);
	    return 2;
    case 'c':
	    rc = change_dir();
	    break;
    default:
	    printf("%c - ?\r\n", cmd);
	    fflush(stdout);
	    rc = 1;
	    break;
    }
    if (rc == 0)
	BACKSPACE;
    return 0;
}


enum { oSPEED=1, oHELP, oVERSION };

struct x_option options[] = {
    { oSPEED, 's', "speed", "RATE", "Terminal speed, in bps" },
    { oVERSION, 'v', "version", 0, "The current version" },
    { oHELP, 'h', "help", 0, "Print this help message" },
} ;
#define NROPTS	(sizeof options/sizeof options[0])

/*
 * tip, in mortal flesh
 */
main(int argc, char **argv)
{
    int state=0;
    int opt;
    int speed = 0;
    char c[10];
    char blk[512];
    char thetty[200];
    fd_set inputs;
    struct timeval timeout;
    int x;
    char *p;
    struct termios term[3];	/* 0: stdin, 1: stdout, 2: tty */

    if (!isatty(0)) {
	fprintf(stderr, "tip: sorry, but stdin must be from a terminal\n");
	exit(1);
    }

    if ((p = getenv("HOME")) == 0)
	getcwd(homedir, sizeof homedir);
    else
	strncpy(homedir, p, sizeof homedir);

    x_opterr = 1;
    while ((opt = x_getopt(argc, argv, NROPTS, options)) != EOF)
	switch (opt) {
	case oSPEED:	speed = atoi(x_optarg);
			if (bps(speed) == 0) {
			    int i, xp;

			    xp = fprintf(stderr, "tip: bad --speed setting.  You may use:");
			    xp += fprintf(stderr, " %d", bpstab[0].speed);
			    for (i=1; i < NRBPS; i++) {
				if (xp > 60) {
				    fprintf(stderr, ",\n\t");
				    xp = 8;
				}
				else
				    xp += fprintf(stderr, ", ");
				xp += fprintf(stderr, "%d", bpstab[i].speed);
			    }
			    fputc('\n', stderr);
			    exit(1);
			}
			break;
	case oVERSION:
			puts("tip $Revision: 1.5 $");
			exit(0);
	case oHELP:
	default:	fprintf(stderr, "\nusage: tip [options] tty\n\n");
			showopts(stderr, NROPTS, options);
			exit (opt == oHELP ? 0 : 1);
	}

    if (x_optind >= argc) {
	fprintf(stderr, "tip: you need to specify a tty device\n");
	exit(1);
    }
    if (strlen(argv[x_optind]) > 100) {
	fprintf(stderr, "tip: improbably long tty device\n");
	exit(1);
    }
    if (strchr(argv[x_optind], '/'))
	strcpy(thetty, argv[x_optind]);
    else
	sprintf(thetty, "/dev/%s", argv[x_optind]);

    if ((modem_fd = ttyopen(thetty)) == EOF) {
	if (errno == EWOULDBLOCK)
	    fprintf(stderr, "tip: %s is in use\n", thetty);
	else {
	    fprintf(stderr, "tip:");
	    perror(thetty);
	}
	exit(1);
    }

    printf("connected to %s at %d bps; [enter]~? for help.\n", thetty, speed ? speed : 9600);
    tcgetattr(0, &saved_tty);
    tcgetattr(0, &tty);

    tty.c_iflag &= ~(BRKINT|IGNPAR|ISTRIP|ICRNL|IXON);
    tty.c_oflag &= ~OPOST;
    tty.c_lflag &= ~(ICANON|ISIG|ECHO|ECHOE|ECHONL|ECHOCTL|ECHOPRT|ECHOKE);

    tcgetattr(modem_fd, &saved_modem);
    tcgetattr(modem_fd, &modem);

    modem.c_cc[VMIN] = 0;
    modem.c_cc[VTIME] = 0;

    if ((linekill = tty.c_cc[VKILL]) == 0)
	linekill = '@';	/* standard horrible Unix convention */
    if ((charkill = tty.c_cc[VERASE]) == 0)
	charkill = '#';

    atexit(recover_tty);

    cfsetispeed(&modem, speed ? bps(speed) : B9600);
    cfsetospeed(&modem, speed ? bps(speed) : B9600);

    tcsetattr(0, TCSANOW, &tty);
    tcsetattr(modem_fd, TCSANOW, &modem);
    /*fcntl(0, F_SETFL, fcntl(0, F_GETFL) | O_NONBLOCK);*/

    while (1) {
	int rc;
	FD_ZERO(&inputs);
	FD_SET(0, &inputs);
	FD_SET(modem_fd, &inputs);

	rc = select(1+modem_fd, &inputs, 0, 0, 0);

	if (FD_ISSET(0, &inputs)) {
	    if ((rc = read(0, c, 1)) > 0) {
		switch (state) {
		case 2: write(modem_fd, c, 1);
			if (c[0] == '\r') state = 0;
			break;
		case 1: state = tipcmd(c[0]);
			break;
		case 0: if (c[0] == '~') {
			    putchar('~');
			    fflush(stdout);
			    state = 1;
			}
			else {
			    state = 2;
			    write(modem_fd, c, 1);
			}
			break;
		}
	    }
	}
	if (FD_ISSET(modem_fd, &inputs)) {
	    if ((rc = read(modem_fd, blk, sizeof blk)) > 0)
		fwrite(blk, 1, rc, stdout);
	}
    }
    exit(0);
}
