/*
 *  ek -- serial external keyboard :-) for FreeBSD/Linux
 *
 *  revision history
 *	0.0: Feb.  5, 2003 by Dai ISHIJIMA
 *	0.1: Feb.  6, 2003 (code conversion)
 *	0.2: Feb. 11, 2003 (IrKB101 support)
 *	0.3: Feb. 17, 2003 (HHC support)
 *
 *  usage: ek [serial device]
 *
 *  example: ek /dev/ttyS1
 *
 *  see also: source of ``script'' command
 */

#ifdef SLZAURUS
#ifndef LINUX
#define LINUX
#endif
#endif

#ifdef LINUX
#define NEED_PTY_H
#define NEED_UTMP_H
#define NEED_WAIT_H
#else
#define NEED_LIBUTIL_H
#endif

#include <stdio.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
#include <string.h>	/* bzero() <- FDZERO() */
#include <termios.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#ifdef NEED_LIBUTIL_H
#include <libutil.h>	/* openpty() */ /* -lutil */
#endif
#ifdef NEED_PTY_H
#include <pty.h>	/* openpty() */ /* -lutil */
#endif
#ifdef NEED_UTMP_H
#include <utmp.h>	/* login_tty() */ /* -lutil */
#endif
#ifdef NEED_WAIT_H
#include <wait.h>	/* wait3(), WNOHANG */
#endif
#include <signal.h>
#include <errno.h>
#include <fcntl.h>
#include "tty.h"
#include "codeconv.h"
#include "irkb101.h"
#include "ps2key.h"


#ifdef LINUX
#define DEFAULT_SHELL "/bin/sh"
#ifdef SLZAURUS
#define DEFAULT_DEVICE "/dev/ttyS1"	/* raw IrDA */
#else
#define DEFAULT_DEVICE "/dev/ttyS0"	/* COM1: */
#endif
#else
#define DEFAULT_SHELL "/bin/csh"
#define DEFAULT_DEVICE "/dev/cuaa1"	/* IrDA, COM2: */
#endif
#define DEFAULT_SPEED 9600
#define RAW_KEYBOARD 'r'
#define SJIS_RAW_KEYBOARD 's'
#define IRKB101_PS2KEYBOARD 'i'
#define HHC 'h'
#define HHC_BPS 4800

#define EK_ENVIRON "EXTERNAL_KEYBOARD"
#define EK_VALUE "SERIAL"
#define SHELL_ENV "SHELL"
#define ENV_OVERWRITE 1

/* 子プロセスの状態その他 */
#define RUNNING		0x01
#define ORG_KBD_EOF	0xc0
#define EXT_KBD_EOF	0xa0
#define CHILD_EOF	0x90
#define CHILD_EXIT	0x08
#define CHILD_CHANGED	0x04

#define YES 1
#define NO  0


char *prog;
#define shift --argc; ++argv

static int childstatus = RUNNING;
static pid_t child;


/*
 * 仮想端末関連
 */

/* 子プロセスとしてシェルを起動する */
void subshell(int master, int slave)
{
    char *shell;

    /* 起動するシェルを決める */
    shell = getenv(SHELL_ENV);
    if (shell == NULL) {
	shell = DEFAULT_SHELL;
    }
    close(master);
    login_tty(slave);
    execl(shell, "sh", "-i", NULL);	/* シェルを「対話モード」で起動 */
    /* ここから下は実行されないはず… */
    fprintf(stderr, "%s: can't start subshell (%s)\r\n", prog, shell);
    kill(0, SIGTERM);
    exit(1);
}


#ifdef LINUX
/* SIGCHILD で飛んでくる。exitした場合、電源OFF→ON */
void chk_child(int dummy)
{
    int status;
    pid_t pid;

    while ((pid = wait3(&status, WNOHANG, NULL)) > 0) {
	if (pid == child) {
	    /* 多分、exit した */
	    childstatus = CHILD_EXIT;
	}
    }
    /* 多分、電源OFF→ON */
    childstatus |= CHILD_CHANGED;
}
#endif


/* ウィンドウサイズ変更, 縦横切り替え...(?) */
void winch(int dummy)
{
    kill(child, SIGWINCH);
}


/* バイトの分解と合成 */
#define lobyte(x) (((unsigned short)(x)) & 0xff)
#define hibyte(x) ((((unsigned short)(x)) >> 8) & 0xff)
#define hilo(h,l) ((((unsigned char)(h)) << 8) | ((unsigned char)(l)))


int sjisconv(int n, char *buf)
{
    static unsigned char mybuf[BUFSIZ];
    static int p = 0;
    int i, j;
    int code;

    j = p;
    for (i = 0; (j < BUFSIZ) && (i < n); i++, j++) {
	mybuf[j] = buf[i];
    }
    n = 0;
    for (i = 0; (i < j) && (n < BUFSIZ); i++, n++) {
	if (mybuf[i] < 0x80) {
	    buf[n] = mybuf[i];
	}
	else if (iskana(mybuf[i])) {
	    code = jtoe(kanakana(mybuf[i]));
	    buf[n] = hibyte(code);
	    buf[n + 1] = lobyte(code);
	    ++n;
	}
	else if ((i + 1 < j) && (n - 1 < BUFSIZ)) {
	    code = jtoe(stoj(hilo(mybuf[i], mybuf[i + 1])));
	    ++i;
	    buf[n] = hibyte(code);
	    buf[n + 1] = lobyte(code);
	    ++n;
	}
	else {
	    break;
	}
    }
    for (p = 0; i < j; p++, i++) {
	mybuf[p] = mybuf[i];
    }
    return(n);
}


/* 外部キーボードを使う */
int keyboard(int extkey, int master, int mode)
{
    int nfds;
    fd_set readfds;
    struct timeval timeout;
    int n;	/* 読み込んだ文字の数 */
    char buf[BUFSIZ];

    childstatus = RUNNING;
    while (ttygetc(extkey) != EOF) {
	; /* 起動時のごみキャラクタを読み飛ばす */
    }
    while (childstatus & RUNNING) {
	FD_ZERO(&readfds);
	FD_SET(master, &readfds); /* 子プロセスの出力を調べる */
	FD_SET(fileno(stdin), &readfds); /* 標準入力 (本体キー) を調べる */
	FD_SET(extkey, &readfds); /* 外部キーボードを調べる */
	timeout.tv_sec = 0;
	timeout.tv_usec = 500000;
	nfds = select(master + 1, &readfds, NULL, NULL, &timeout);
	if ((nfds < 0) && (errno != EINTR)) {
	    fprintf(stderr, ">>>if ((nfds < 0) && (errno != EINTR)) {\n");
	    break;
	}
	if ((nfds > 0) && (FD_ISSET(fileno(stdin), &readfds))) {
	    /* 標準入力 (本体キーボード) からのデータを */
	    /* 子プロセスに書き込む */
	    n = read(fileno(stdin), buf, BUFSIZ);
	    if (n <= 0) {
		childstatus = ORG_KBD_EOF;
	    }
	    if (n > 0) {
		(void)write(master, buf, n);
	    }
	}
	if ((nfds > 0) && (FD_ISSET(extkey, &readfds))) {
	    /* 外部キーボードからのデータを子プロセスに書き込む */
	    if (childstatus & CHILD_CHANGED) {
		while (ttygetc(extkey) != EOF) {
		    ; /* 再起動時のごみキャラクタを読み飛ばす */
		}
		childstatus &= ~CHILD_CHANGED;
	    }
	    else {
		n = read(extkey, buf, BUFSIZ);
		if (n <= 0) {
		    childstatus = EXT_KBD_EOF;
		}
		if (n > 0) {
		    if (mode == SJIS_RAW_KEYBOARD) {
			n = sjisconv(n, buf);
		    }
		    else if (mode == IRKB101_PS2KEYBOARD) {
			n = irkb101conv(n, buf);
			n = ps2conv(n, buf);
		    }
		    else if (mode == HHC) {
			n = ps2conv(n, buf);
		    }
		    (void)write(master, buf, n);
		}
	    }
	}
	if (nfds > 0 && FD_ISSET(master, &readfds)) {
	    /* 子プロセスからのデータを標準出力へ書き出す */
	    n = read(master, buf, BUFSIZ);
	    if (n <= 0) {
		childstatus = CHILD_EOF;
		break;
	    }
	    write(fileno(stdout), buf, n);
	}
    } /* while */
    if (childstatus == CHILD_EOF) {
	fprintf(stderr, "%s: EOF encountered, child process finished\r\n",
		prog);
    }
    else if (childstatus & CHILD_EXIT) {
	fprintf(stderr, "%s: caught SIGCHLD, child process finished\r\n",
		prog);
    }
    else if (childstatus == ORG_KBD_EOF) {
	fprintf(stderr, "%s: EOF encountered on keyboard\r\n",
		prog);
    }
    else if (childstatus == EXT_KBD_EOF) {
	fprintf(stderr, "%s: EOF encountered on external keyboard\r\n",
		prog);
    }
    else {
	fprintf(stderr, "%s: child process finished (may be)\r\n", prog);
    }
    return(childstatus);
}


/* 仮想端末を開く */
void pseudoterm(int extkey, int mode)
{
    struct termios tsave;	/* 端末の設定 保存版 */
    struct termios t;		/* 端末の設定 */
    struct winsize w;		/* 端末の画面サイズ */
    int master, slave;
    int status;
    
    tcgetattr(fileno(stdin), &t);
    tcgetattr(fileno(stdin), &tsave);
    ioctl(fileno(stdin), TIOCGWINSZ, &w);
    if (openpty(&master, &slave, NULL, &t, &w) != 0) {
	fprintf(stderr, "%s: can't open pty\n", prog);
	exit(1);
    }
    cfmakeraw(&t);
    t.c_lflag &= ~ECHO;
    (void)tcsetattr(fileno(stdin), TCSAFLUSH, &t);
    child = fork();
    if (child < 0) {			/* fork失敗 */
	fprintf(stderr, "%s: can't fork\n", prog);
	exit(1);
    }
    else if (child == 0) {		/* 子プロセス */
	subshell(master, slave);
    }
    else {				/* 元々のプロセス */
#ifdef LINUX
	signal(SIGCHLD, chk_child);	/* 子プロセスが死んだかチェック */
#endif
	signal(SIGWINCH, winch);	/* ウィンドウサイズ変更 */
	/* 外部キーボードを使う */
	fprintf(stderr, "%s: external keyboard now enabled\r\n", prog);
	status = keyboard(extkey, master, mode);
	fprintf(stderr, "%s: external keyboard now disabled\r\n", prog);
    }
    (void)tcsetattr(fileno(stdin), TCSAFLUSH, &tsave);
}


void usage()
{
    fprintf(stderr, "Usage: %s [option]... [device]\n", prog);
    fputs("    options:\n", stderr);
    fputs("\t-raw: RAW keyboard (no conversion) mode\n", stderr);
    fputs("\t-sjis: SJIS conversion mode\n", stderr);
    fputs("\t-irkb101: IrKB101+PS/2 mode\n", stderr);
    fputs("\t-IrKB101: IrKB101+PS/2 mode without initialize\n", stderr);
    fputs("\t-cradle: Happy Hacking Cradle+PS/2 mode\n", stderr);
    fputs("\t-#: set serial speed [bps]\n", stderr);
    fputs("    default:\n", stderr);
    fprintf(stderr, "\t%s -%d -sjis %s\n",
	    prog, DEFAULT_SPEED, DEFAULT_DEVICE);
}


int main(int argc, char *argv[])
{
    int mode;
    char *keydev;
    char *keyenv;
    int fd;
    int speed;
    int n;
    int init101;

    prog = *argv;
    shift;
    speed = DEFAULT_SPEED;
    mode = SJIS_RAW_KEYBOARD;
    init101 = YES;
    while ((argc > 0) && (argv[0][0] == '-')) {
	if (argv[0][1] == 'r') {
	    mode = RAW_KEYBOARD;
	}
	else if (argv[0][1] == 's') {
	    mode = SJIS_RAW_KEYBOARD;
	}
	else if (argv[0][1] == 'i') {
	    mode = IRKB101_PS2KEYBOARD;
	}
	else if (argv[0][1] == 'I') {
	    mode = IRKB101_PS2KEYBOARD;
	    init101 = NO;
	}
	else if (argv[0][1] == 'c') {
	    mode = HHC;
	    speed = HHC_BPS;
	}
	else if (('0' <= argv[0][1]) && (argv[0][1] <= '9')) {
	    speed = -atoi(*argv);
	}
	else {
	    usage();
	    exit(1);
	}
	shift;
    }
    if (argc > 0) {
	keydev = *argv;
    }
    else {
	keydev = DEFAULT_DEVICE;
    }
    /* すでに外部キーボードが有効になっているか? */
    keyenv = getenv(EK_ENVIRON);
    if (keyenv != NULL) {
	fprintf(stderr, "%s: may already running %s, ", prog, prog);
	fprintf(stderr, "environ $%s set, value = %s\r\n", EK_ENVIRON, keyenv);
	/* IrKB101の場合は初期化しない */
	init101 = NO;
    }
    setenv(EK_ENVIRON, EK_VALUE, ENV_OVERWRITE);
    /* */
    fd = opentty(keydev);
    ttyinit(fd, speed);
    if ((mode == IRKB101_PS2KEYBOARD) && (init101 == YES)) {
	if ((n = init_irkb101(fd)) != 0) {
	    fprintf(stderr,
		    "%s: can't initialize IrKB101+PS/2 keyboard (%d)\n",
		    prog, n);
	    closetty(fd);
	    exit(1);
	}
	fprintf(stderr, "%s: IrKB101+PS/2 initialized\n", prog);
    }
    pseudoterm(fd, mode);
    closetty(fd);
    exit(0);
}

/* Local Variables: */
/* compile-command:"gcc -g -Wall -o ek2 ek2.c tty.c codeconv.c irkb101.c ps2key.c -lutil" */
/* End: */
