shithub: purgatorio

ref: f5cc6fbe3a7bcf8bdb002c646ddd519014afafd2
dir: /appl/wm/telnet.b/

View raw version
implement WmTelnet;

include "sys.m";
	sys: Sys;
	Connection: import sys;

include "draw.m";
	draw: Draw;
	Context: import draw;

include "tk.m";
	tk: Tk;

include "tkclient.m";
	tkclient: Tkclient;

include "dialog.m";
	dialog: Dialog;

WmTelnet: module
{
	init:	fn(ctxt: ref Draw->Context, args: list of string);
};

Iob: adt
{
	fd:	ref Sys->FD;
	t:	ref Tk->Toplevel;
	out:	cyclic ref Iob;
	buf:	array of byte;
	ptr:	int;
	nbyte:	int;
};

BS:		con 8;		# ^h backspace character
BSW:		con 23;		# ^w bacspace word
BSL:		con 21;		# ^u backspace line
EOT:		con 4;		# ^d end of file
ESC:		con 27;		# hold mode

HIWAT:	con 2000;	# maximum number of lines in transcript
LOWAT:	con 1500;	# amount to reduce to after high water

Name:	con "Telnet";
ctxt:	ref Context;
cmds:	chan of string;
net:	Connection;
stderr: ref Sys->FD;
mcrlf:	int;
netinp:	ref Iob;

# control characters
Se:		con 240;	# end subnegotiation
NOP:		con 241;
Mark:		con 242;	# data mark
Break:		con 243;
Interrupt:	con 244;
Abort:		con 245;	# TENEX ^O
AreYouThere:	con 246;
Erasechar:	con 247;	# erase last character
Eraseline:	con 248;	# erase line
GoAhead:	con 249;	# half duplex clear to send
Sb:		con 250;	# start subnegotiation
Will:		con 251;
Wont:		con 252;
Do:		con 253;
Dont:		con 254;
Iac:		con 255;

# options
Binary,	Echo,	SGA,	Stat,	Timing,
Det,	Term,	EOR,	Uid,	Outmark,
Ttyloc,	M3270,	Padx3,	Window,	Speed,
Flow,	Line,	Xloc,	Extend: con iota;

Opt: adt
{
	name:	string;
	code:	int;
	noway:	int;	
	remote:	int;		# remote value
	local:	int;		# local value
};

opt := array[] of
{
	Binary	=> Opt("binary",			0,	0,	0, 	0),
	Echo		=> Opt("echo",				1,  	0, 	0,	0),
	SGA		=> Opt("suppress Go Ahead",	3,  	0, 	0,	0),
	Stat		=> Opt("status",			5,  	1, 	0,	0),
	Timing	=> Opt("timing",			6,  	1, 	0,	0),
	Det		=> Opt("det",				20, 	1, 	0,	0),
	Term	=> Opt("terminal",			24, 	0, 	0,	0),
	EOR		=> Opt("end of record",		25, 	1, 	0,	0),
	Uid		=> Opt("uid",				26, 	1, 	0,	0),
	Outmark	=> Opt("outmark",			27, 	1, 	0,	0),
	Ttyloc	=> Opt("ttyloc",				28, 	1, 	0,	0),
	M3270	=> Opt("3270 mode",		29, 	1, 	0,	0),
	Padx3	=> Opt("pad x.3",			30, 	1, 	0,	0),
	Window	=> Opt("window size",		31, 	1, 	0,	0),
	Speed	=> Opt("speed",			32, 	1, 	0,	0),
	Flow		=> Opt("flow control",		33, 	1, 	0,	0),
	Line		=> Opt("line mode",			34, 	0, 	0,	0),
	Xloc		=> Opt("X display loc",		35, 	1, 	0,	0),
	Extend	=> Opt("Extended",			255, 	1, 	0,	0),
};

shwin_cfg := array[] of {
	"menu .m",
	".m add command -text Cut -command {send edit cut}",
	".m add command -text Paste -command {send edit paste}",
	".m add command -text Snarf -command {send edit snarf}",
	".m add command -text Send -command {send edit send}",
	"frame .ft",
	"scrollbar .ft.scroll -command {.ft.t yview}",
	"text .ft.t -width 70w -height 25h -yscrollcommand {.ft.scroll set}",
	"frame .mb",
	"menubutton .mb.c -text Connect -menu .mbc",
	"menubutton .mb.t -text Terminal -menu .mbt",
	"menu .mbc",
	".mbc add command -text {Remote System} -command {send cmd con}",
	".mbc add command -text {Disconnect} -state disabled -command {send cmd dis}",
	".mbc add command -text {Exit} -command {send cmd exit}",
	".mbc add separator",
	"menu .mbt",
	".mbt add checkbutton -text {Line Mode} -command {send cmd line}",
	".mbt add checkbutton -text {Map CR to LF} -command {send cmd crlf}",
	"pack .mb.c .mb.t -side left",
	"pack .ft.scroll -side left -fill y",
	"pack .ft.t -fill both -expand 1",
	"pack .mb -fill x",
	"pack .ft -fill both -expand 1",
	"pack propagate . 0",
	"focus .ft.t",
	"bind .ft.t <Key> {send keys {%A}}",
	"bind .ft.t <Control-d> {send keys {%A}}",
	"bind .ft.t <Control-h> {send keys {%A}}",
	"bind .ft.t <ButtonPress-3> {send but3 %X %Y}",
	"bind .ft.t <ButtonRelease-3> {}",
	"bind .ft.t <DoubleButton-3> {}",
	"bind .ft.t <Double-ButtonRelease-3> {}",
	"bind .ft.t <ButtonPress-2> {}",
	"bind .ft.t <ButtonRelease-2> {}",
	"update"
};

connect_cfg := array[] of {
	"frame .fl",
	"label .fl.h -text Host",
	"label .fl.p -text Port",
	"pack .fl.h .fl.p",
	"frame .el",
	"entry .el.h",
	"entry .el.p",
	".el.p insert end 'telnet",
	"pack .el.h .el.p",
	"pack .Wm_t -fill x",
	"pack .fl .el -side left",
	"focus .el.h",
	"bind .el.h <Key-\n> {send cmd ok}",
	"bind .el.p <key-\n> {send cmd ok}",
	"update"
};

connected_cfg := array[] of {
	"focus .ft.t",
	".mbc entryconfigure 0 -state disabled",
	".mbc entryconfigure 1 -state normal"
};

menuindex := "0";
holding := 0;

init(C: ref Context, argv: list of string)
{
	sys = load Sys Sys->PATH;
	if (C == nil) {
		sys->fprint(sys->fildes(2), "telnet: no window context\n");
		raise "fail:bad context";
	}
	draw = load Draw Draw->PATH;
	tk = load Tk Tk->PATH;
	tkclient = load Tkclient Tkclient->PATH;
	dialog = load Dialog Dialog->PATH;

	ctxt = C;
	tkclient->init();
	dialog->init();

	sys->pctl(Sys->NEWPGRP, nil);
	stderr = sys->fildes(2);

	tkargs := "";
	argv = tl argv;
	if(argv != nil) {
		tkargs = hd argv;
		argv = tl argv;
	}
	(t, titlectl) := tkclient->toplevel(ctxt, tkargs, Name, Tkclient->Appl);

	edit := chan of string;
	tk->namechan(t, edit, "edit");
	for (cc:=0; cc<len shwin_cfg; cc++)
		tk->cmd(t, shwin_cfg[cc]);

	keys := chan of string;
	tk->namechan(t, keys, "keys");

	but3 := chan of string;
	tk->namechan(t, but3, "but3");

	cmds = chan of string;
	tk->namechan(t, cmds, "cmd");

	# outpoint is place in text to insert characters printed by programs
	tk->cmd(t, ".ft.t mark set outpoint end; .ft.t mark gravity outpoint left");
	tkclient->onscreen(t, nil);
	tkclient->startinput(t, "kbd"::"ptr"::nil);

	for(;;) alt {
	s := <-t.ctxt.kbd =>
		tk->keyboard(t, s);
	s := <-t.ctxt.ptr =>
		tk->pointer(t, *s);
	s := <-t.ctxt.ctl or
	s = <-t.wreq or
	s = <-titlectl =>
		if(s == "exit") {
			kill();
			return;
		}
		tkclient->wmctl(t, s);
	ecmd := <-edit =>
		editor(t, ecmd);
		sendinput(t);

	c := <-keys =>
		if(opt[Echo].local == 0) {
			sys->fprint(net.dfd, "%c", c[1]);
			break;
		}
		cut(t, 1);
		char := c[1];
		if(char == '\\')
			char = c[2];
		update := ";.ft.t see insert;update";
		case char{
		* =>
			tk->cmd(t, ".ft.t insert insert "+c+update);
		'\n' or EOT =>
			tk->cmd(t, ".ft.t insert insert "+c+update);
			sendinput(t);
		BS =>
			if(!insat(t, "outpoint"))
				tk->cmd(t, ".ft.t delete insert-1chars"+update);
		ESC =>
			holding ^= 1;
			color := "blue";
			if(!holding){
				color = "black";
				tkclient->settitle(t, Name);
				sendinput(t);
			}else
				tkclient->settitle(t, Name+" (holding)");
			tk->cmd(t, ".ft.t configure -foreground "+color+update);
		BSL =>
			if(insininput(t))
				tk->cmd(t, ".ft.t delete outpoint insert"+update);
			else
				tk->cmd(t, ".ft.t delete {insert linestart} insert"+update);
		BSW =>
			if(insat(t, "outpoint"))
				break;
			a0 := isalnum(tk->cmd(t, ".ft.t get insert-1chars"));
			a1 := isalnum(tk->cmd(t, ".ft.t get insert"));
			start: string;
			if(a0 && a1)	# middle of word
				start = "{insert wordstart}";
			else if(a0)		# end of word
				start = "{insert-1chars wordstart}";
			else{	# beginning or not in word; must search
				s: string;
				for(n:=1; ;){
					s = tk->cmd(t, ".ft.t get insert-"+ string n +"chars");
					if(s=="" || s=="\n"){
						start = "insert-"+ string n+"chars";
						break;
					}
					n++;
					if(isalnum(s)){
						start = "{insert-"+ string n+"chars wordstart}";
						break;
					}
				}
				
			}
			# don't ^w across outpoint
			if(tk->cmd(t, ".ft.t compare insert >= outpoint") == "1"
			&& tk->cmd(t, ".ft.t compare "+start+" < outpoint") == "1")
				start = "outpoint";
			tk->cmd(t, ".ft.t delete " + start + " insert"+update);
		}

	c := <-but3 =>
		(nil, l) := sys->tokenize(c, " ");
		x := int hd l - 50;
		y := int hd tl l - int tk->cmd(t, ".m yposition "+menuindex) - 10;
		tk->cmd(t, ".m activate "+menuindex+"; .m post "+string x+" "+string y+
			"; grab set .m; update");

	c := <-cmds =>
		case c {
		"con" =>
			tk->cmd(t, ".mb.c configure -state disabled");
			connect(t);
			tk->cmd(t, ".mb.c configure -state normal; update");
		"dis" =>
			tkclient->settitle(t, "Telnet");
			tk->cmd(t, ".mbc entryconfigure 0 -state normal");
			tk->cmd(t, ".mbc entryconfigure 1 -state disabled");
			net.cfd = nil;
			net.dfd = nil;
			kill();
		"exit" =>
			kill();
			return;
		"crlf" =>
			mcrlf = !mcrlf;
			break;
		"line" =>
			if(opt[Line].local == 0)
				send3(netinp, Iac, Will, opt[Line].code);
			else
				send3(netinp, Iac, Wont, opt[Line].code);
		}
	}
}

insat(t: ref Tk->Toplevel, mark: string): int
{
	return tk->cmd(t, ".ft.t compare insert == "+mark) == "1";
}

insininput(t: ref Tk->Toplevel): int
{
	if(tk->cmd(t, ".ft.t compare insert >= outpoint") != "1")
		return 0;
	return tk->cmd(t, ".ft.t compare {insert linestart} == {outpoint linestart}") == "1";
}

isalnum(s: string): int
{
	if(s == "")
		return 0;
	c := s[0];
	if('a' <= c && c <= 'z')
		return 1;
	if('A' <= c && c <= 'Z')
		return 1;
	if('0' <= c && c <= '9')
		return 1;
	if(c == '_')
		return 1;
	if(c > 16rA0)
		return 1;
	return 0;
}

editor(t: ref Tk->Toplevel, ecmd: string)
{
	s, snarf: string;

	case ecmd {
	"cut" =>
		menuindex = "0";
		cut(t, 1);
	
	"paste" =>
		menuindex = "1";
		snarf = tkclient->snarfget();
		if(snarf == "")
			break;
		cut(t, 0);
		tk->cmd(t, ".ft.t insert insert '"+snarf);
		sendinput(t);

	"snarf" =>
		menuindex = "2";
		if(tk->cmd(t, ".ft.t tag ranges sel") == "")
			break;
		snarf = tk->cmd(t, ".ft.t get sel.first sel.last");
		tkclient->snarfput(snarf);

	"send" =>
		menuindex = "3";
		if(tk->cmd(t, ".ft.t tag ranges sel") != ""){
			snarf = tk->cmd(t, ".ft.t get sel.first sel.last");
			tkclient->snarfput(snarf);
		}else
			snarf = tkclient->snarfget();
		if(snarf != "")
			s = snarf;
		else
			return;
		if(s[len s-1] != '\n' && s[len s-1] != EOT)
			s[len s] = '\n';
		tk->cmd(t, ".ft.t see end; .ft.t insert end '"+s);
		tk->cmd(t, ".ft.t mark set insert end");
		tk->cmd(t, ".ft.t tag remove sel sel.first sel.last");
	}
	tk->cmd(t, "update");
}

cut(t: ref Tk->Toplevel, snarfit: int)
{
	if(tk->cmd(t, ".ft.t tag ranges sel") == "")
		return;
	if(snarfit)
		tkclient->snarfput(tk->cmd(t, ".ft.t get sel.first sel.last"));
	tk->cmd(t, ".ft.t delete sel.first sel.last");
}

sendinput(t: ref Tk->Toplevel)
{
	if(holding)
		return;
	input := tk->cmd(t, ".ft.t get outpoint end");
	slen := len input;
	if(slen == 0)
		return;

	for(i := 0; i < slen; i++)
		if(input[i] == '\n' || input[i] == EOT)
			break;

	if(i >= slen)
		return;

	advance := string (i+1);
	if(input[i] == EOT)
		input = input[0:i];
	else
		input = input[0:i+1];

	sys->fprint(net.dfd, "%s", input);
	tk->cmd(t, ".ft.t mark set outpoint outpoint+" + advance + "chars");
}

kill()
{
	path := sys->sprint("#p/%d/ctl", sys->pctl(0, nil));
	fd := sys->open(path, sys->OWRITE);
	if(fd != nil)
		sys->fprint(fd, "killgrp");
}

connect(t: ref Tk->Toplevel)
{
	(b, titlectl) := tkclient->toplevel(ctxt, nil, "Connect", 0);
	for (c:=0; c<len connect_cfg; c++)
		tk->cmd(b, connect_cfg[c]);

	cmd := chan of string;
	tk->namechan(b, cmd, "cmd");
	tkclient->onscreen(b, nil);
	tkclient->startinput(b, "kbd"::"ptr"::nil);

loop:	for(;;) alt {
		s := <-b.ctxt.kbd =>
			tk->keyboard(b, s);
		s := <-b.ctxt.ptr =>
			tk->pointer(b, *s);
		s := <-b.ctxt.ctl or
		s = <-b.wreq or
		s = <-titlectl =>
			if(s == "exit")
				return;
			tkclient->wmctl(b, s);
	<-cmd =>
		break loop;		
	}

	addr := sys->sprint("tcp!%s!%s",
			tk->cmd(b, ".el.h get"),
			tk->cmd(b, ".el.p get"));

	tkclient->settitle(b, "Dialing");
	tk->cmd(b, "update");

	ok: int;
	(ok, net) = sys->dial(addr, nil);
	if(ok < 0) {
		dialog->prompt(ctxt, b.image, "error -fg red",
			"Connect", "Connection to host failed\n"+sys->sprint("%r"),
			0, "Stop connect" :: nil);
		return;
	}

	tkclient->settitle(t, "Telnet - "+addr);
	for (c=0; c<len connected_cfg; c++)
		tk->cmd(b, connected_cfg[c]);

	spawn fromnet(t);
}

flush(t: ref Tk->Toplevel, data: array of byte)
{
	cdata := string data;
	ncdata := string len cdata + "chars;";
	moveins := insat(t, "outpoint");
	tk->cmd(t, ".ft.t insert outpoint '"+ cdata);
	s := ".ft.t mark set outpoint outpoint+" + ncdata;
	s += ".ft.t see outpoint;";
	if(moveins)
		s += ".ft.t mark set insert insert+" + ncdata;
	s += "update";
	tk->cmd(t, s);
	nlines := int tk->cmd(t, ".ft.t index end");
	if(nlines > HIWAT){
		s = ".ft.t delete 1.0 "+ string (nlines-LOWAT) +".0;update";
		tk->cmd(t, s);
	}
}

iobnew(fd: ref Sys->FD, t: ref Tk->Toplevel, out: ref Iob, size: int): ref Iob
{
	iob := ref Iob;
	iob.fd = fd;
	iob.t = t;
	iob.out = out;
	iob.buf = array[size] of byte;
	iob.nbyte = 0;
	iob.ptr = 0;
	return iob;
}

iobget(iob: ref Iob): int
{
	if(iob.nbyte == 0) {
		if(iob.out != nil)
			iobflush(iob.out);
		iob.nbyte = sys->read(iob.fd, iob.buf, len iob.buf);
		if(iob.nbyte <= 0)
			return iob.nbyte;
		iob.ptr = 0;
	}
	iob.nbyte--;
	return int iob.buf[iob.ptr++];
}

iobput(iob: ref Iob, c: int)
{
	iob.buf[iob.ptr++] = byte c;
	if(iob.ptr == len iob.buf)
		iobflush(iob);
}

iobflush(iob: ref Iob)
{
	if(iob.fd == nil) {
		flush(iob.t, iob.buf[0:iob.ptr]);
		iob.ptr = 0;
	}
}

fromnet(t: ref Tk->Toplevel)
{
	conout := iobnew(nil, t, nil, 2048);
	netinp = iobnew(net.dfd, nil, conout, 2048);

	crnls := 0;
	freenl := 0;

loop:	for(;;) {
		c := iobget(netinp);
		case c {
		-1 =>
			cmds <-= "dis";
			return;
		'\n' =>				# skip nl after string of cr's */
			if(!opt[Binary].local && !mcrlf) {
				crnls++;
				if(freenl == 0)
					break;
				freenl = 0;
				continue loop;
			}
		'\r' =>
			if(!opt[Binary].local && !mcrlf) {
				if(crnls++ == 0){
					freenl = 1;
					c = '\n';
					break;
				}
				continue loop;
			}
		Iac  =>
			c = iobget(netinp);
			if(c == Iac)
				break;
			iobflush(conout);
			if(control(netinp, c) < 0)
				return;

			continue loop;	
		}
		iobput(conout, c);
	}
}

control(bp: ref Iob, c: int): int
{
	case c {
	AreYouThere =>
		sys->fprint(net.dfd, "Inferno telnet V1.0\r\n");
	Sb =>
		return sub(bp);
	Will =>
		return will(bp);
	Wont =>
		return wont(bp);
	Do =>
		return doit(bp);
	Dont =>
		return dont(bp);
	Se =>
		sys->fprint(stderr, "telnet: SE without an SB\n");
	-1 =>
		return -1;
	*  =>
		break;
	}
	return 0;
}

sub(bp: ref Iob): int
{
	subneg: string;
	i := 0;
	for(;;){
		c := iobget(bp);
		if(c == Iac) {
			c = iobget(bp);
			if(c == Se)
				break;
			subneg[i++] = Iac;
		}
		if(c < 0)
			return -1;
		subneg[i++] = c;
	}
	if(i == 0)
		return 0;

	sys->fprint(stderr, "sub %d %d n = %d\n", subneg[0], subneg[1], i);

	for(i = 0; i < len opt; i++)
		if(opt[i].code == subneg[0])
			break;

	if(i >= len opt)
		return 0;

	case i {
	Term =>
		sbsend(opt[Term].code, array of byte "dumb");	
	}

	return 0;
}

sbsend(code: int, data: array of byte): int
{
	buf := array[4+len data+2] of byte;
	o := 4+len data;

	buf[0] = byte Iac;
	buf[1] = byte Sb;
	buf[2] = byte code;
	buf[3] = byte 0;
	buf[4:] = data;
	buf[o] = byte Iac;
	o++;
	buf[o] = byte Se;

	return sys->write(net.dfd, buf, len buf);
}

will(bp: ref Iob): int
{
	c := iobget(bp);
	if(c < 0)
		return -1;

	sys->fprint(stderr, "will %d\n", c);

	for(i := 0; i < len opt; i++)
		if(opt[i].code == c)
			break;

	if(i >= len opt) {
		send3(bp, Iac, Dont, c);
		return 0;
	}

	rv := 0;
	if(opt[i].noway)
		send3(bp, Iac, Dont, c);
	else
	if(opt[i].remote == 0)
		rv |= send3(bp, Iac, Do, c);

	if(opt[i].remote == 0)
		rv |= change(bp, i, Will);
	opt[i].remote = 1;
	return rv;
}

wont(bp: ref Iob): int
{
	c := iobget(bp);
	if(c < 0)
		return -1;

	sys->fprint(stderr, "wont %d\n", c);

	for(i := 0; i < len opt; i++)
		if(opt[i].code == c)
			break;

	if(i >= len opt)
		return 0;

	rv := 0;
	if(opt[i].remote) {
		rv |= change(bp, i, Wont);
		rv |= send3(bp, Iac, Dont, c);
	}
	opt[i].remote = 0;
	return rv;
}

doit(bp: ref Iob): int
{
	c := iobget(bp);
	if(c < 0)
		return -1;

	sys->fprint(stderr, "do %d\n", c);

	for(i := 0; i < len opt; i++)
		if(opt[i].code == c)
			break;

	if(i >= len opt || opt[i].noway) {
		send3(bp, Iac, Wont, c);
		return 0;
	}
	rv := 0;
	if(opt[i].local == 0) {
		rv |= change(bp, i, Do);
		rv |= send3(bp, Iac, Will, c);
	}
	opt[i].local = 1;
	return rv;
}

dont(bp: ref Iob): int
{
	c := iobget(bp);
	if(c < 0)
		return -1;

	sys->fprint(stderr, "dont %d\n", c);

	for(i := 0; i < len opt; i++)
		if(opt[i].code == c)
			break;

	if(i >= len opt || opt[i].noway)
		return 0;

	rv := 0;
	if(opt[i].local){
		opt[i].local = 0;
		rv |= change(bp, i, Dont);
		rv |= send3(bp, Iac, Wont, c);
	}
	opt[i].local = 0;
	return rv;
}

change(nil: ref Iob, nil: int, nil: int): int
{
	return 0;
}

send3(bp: ref Iob, c0: int, c1: int, c2: int): int
{
	buf := array[3] of byte;

	buf[0] = byte c0;
	buf[1] = byte c1;
	buf[2] = byte c2;

	t: string;
	case c0 {
	Will => t = "Will";
	Wont => t = "Wont";
	Do =>	t = "Do";
	Dont => t = "Dont";
	}
	if(t != nil)
		sys->fprint(stderr, "r %s %d\n", t, c1);

	r := sys->write(bp.fd, buf, 3);
	if(r != 3)
		return -1;
	return 0;
}