code: purgatorio

ref: a600cca5ff367ca7e7c64a14a8e87ba376780f6d
dir: /appl/charon/charon.b/

View raw version
implement Charon;

include "common.m";
include "debug.m";

sys: Sys;
CU: CharonUtils;
	ByteSource, MaskedImage, CImage, ImageCache, ReqInfo, Header, 
	ResourceState, config, max, min, X: import CU;

D: Draw;
	Point, Rect, Font, Image, Display, Screen: import D;

S: String;
U: Url;
	Parsedurl: import U;
L: Layout;
	Frame, Loc, Control: import L;
I: Img;
	ImageSource: import I;

B: Build;
	Item, Dimen: import B;

E: Events;
	Event: import E;

J: Script;

G: Gui;

C : Ctype;

include "sh.m"; 

# package up info related to a navigation command
GoSpec: adt {
	kind: int;				# GoNormal, etc.
	url: ref Parsedurl;		# destination (absolute)
	meth: int;				# HGet or HPost
	body: string;			# used if HPost
	target: string;			# name of target frame
	auth: string;			# optional auth info
	histnode: ref HistNode;	# if kind is GoHistnode

	newget: fn(kind: int, url: ref Parsedurl, target: string) : ref GoSpec;
	newpost: fn(url: ref Parsedurl, body, target: string) : ref GoSpec;
	newspecial: fn(kind: int, histnode: ref HistNode) : ref GoSpec;
	equal: fn(a: self ref GoSpec, b: ref GoSpec) : int;
};

GoNormal, GoReplace, GoLink, GoHistnode, GoSettext: con iota;

# Information about a set of frames making up the screen
DocConfig: adt {
	framename: string;		# nonempty, except possibly for topconfig
	title: string;
	initconfig: int;			# true unless this is a frameset and some subframe changed
	gospec: cyclic ref GoSpec;
	# TODO: add current y pos and form field values

	equal: fn(a: self ref DocConfig, b: ref DocConfig) : int;
	equalarray: fn(a1: array of ref DocConfig, a2: array of ref DocConfig) : int;
};

# Information about a particular screen configuration
HistNode: adt {
	topconfig: cyclic ref DocConfig;			# config of top (whole doc, or frameset root)
	kidconfigs: cyclic array of ref DocConfig;	# configs for kid frames (if a frameset)
	preds: cyclic list of ref HistNode;	# edges in (via normal navigation)
	succs: cyclic list of ref HistNode;	# edges out (via normal navigation)
	findid : int;
	findchain : cyclic list of ref HistNode;

	addedge: fn(a: self ref HistNode, b: ref HistNode, atob: int);
	copy: fn(a: self ref HistNode) : ref HistNode;
};

History: adt {
	h: array of ref HistNode;	# all visited HistNodes, in LRU order
	n: int;				# h[0:n] is valid part of h
	findid : int;

	add: fn(h: self ref History, f: ref Frame, g: ref GoSpec, navkind: int);
	update: fn(h: self ref History, f: ref Frame);
	find: fn(h: self ref History, k: int) : ref HistNode;
	print: fn(h: self ref History);
	histinfo: fn(h: self ref History) : (int, string, string, string);
	findurl: fn(h: self ref History, s: string) : ref HistNode;
};

# Authentication strings
AuthInfo: adt {
	realm: string;
	credentials: string;
};

auths: list of ref AuthInfo = nil;

history : ref History;
keyfocus: ref Control;
mouseover: ref B->Anchor;
mouseoverfr: ref Frame;
grabctl: ref Control;
popupctl: ref Control;

SP : con 8;			# a spacer for between controls
SP2 : con 4;			# half of SP
SP3 : con 2;
pgrp := 0;
gopgrp := 0;
dbg := 0;
warn := 0;
dbgres := 0;
doscripts := 0;

top, curframe: ref Frame;
mainwin: ref Image;
p0 := Point(0,0);

context: ref Draw->Context;
opener: chan of string;

sendopener(s: string)
{
	if(opener != nil){
		alt{
			opener <- = s =>
				;
			* =>
				;
		}
	}
}

hasopener(): int
{
	return opener != nil;
}

init(ctxt: ref Draw->Context, argl: list of string)
{
	chctxt := ref Context(ctxt, argl, nil, nil, nil);
	initc(chctxt);
}

initc(ctxt: ref Context)
{
	sys = load Sys Sys->PATH;
	if (ctxt == nil)
		fatalerror("bad args\n");
	opener = ctxt.c;
	argl := ctxt.args;
	context = ctxt.ctxt;

	(retval, nil) := sys->stat("/net/tcp");
	if(retval < 0)
		sys->bind("#I", "/net", sys->MREPL);
	(retval, nil) = sys->stat("/net/cs");
	if(retval < 0)
		startcs();

	pgrp = sys->pctl(sys->NEWPGRP, nil);
	CU = load CharonUtils CharonUtils->PATH;
	if(CU == nil)
		fatalerror(sys->sprint("Couldn't load %s\n", CharonUtils->PATH));

	ech := chan of ref Event;
	errpath := CU->init(load Charon SELF, CU, argl, ech, ctxt.cksrv, ctxt.ckclient);
	if(errpath != "")
		fatalerror(sys->sprint("Couldn't load %s\n", errpath));
	ctxt = nil;

	sys = load Sys Sys->PATH;
	D = load Draw Draw->PATH;
	S = load String String->PATH;
	U = load Url Url->PATH;
	if (U != nil)
		U->init();
	E = CU->E;
	L = CU->L;
	I = CU->I;
	B = CU->B;
	J = CU->J;
	G = CU->G;
	C = CU->C;

	dbg = int (CU->config).dbg['d'];
	warn = dbg ||  int (CU->config).dbg['w'];
	dbgres = int (CU->config).dbg['r'];
	doscripts = (CU->config).doscripts && J != nil;
	if(dbg && (CU->config).dbgfile != "") {
		dfile := sys->create((CU->config).dbgfile, sys->OWRITE, 8r666);
		if(dfile != nil) {
			sys->dup(dfile.fd, 1);
		}
	}
	curres := ResourceState.cur();
	newres: ResourceState;
	if(dbgres) {
		(CU->startres).print("starting resources");
		curres = ResourceState.cur();
	}

	context = G->init(context, CU);
	if(dbgres) {
		newres = ResourceState.cur();
		newres.since(curres).print("difference after G->init (made screen windows)");
		curres = newres;
	}
	mainwin = G->mainwin;

	# L->init() was deferred until after G was inited
	L->init(CU);
	if(dbgres) {
		newres = ResourceState.cur();
		newres.since(curres).print("difference after L->init (loaded Build, Lex)");
		curres = newres;
	}
	(CU->imcache).init();
	if(dbgres) {
		newres = ResourceState.cur();
		newres.since(curres).print("difference after (CU->imcache).init");
		curres = newres;
	}
	start();
	if(J != nil)
		J->frametreechanged(top);
	startpage := config.starturl;
	g := GoSpec.newget(GoNormal, CU->makeabsurl(startpage), "_top");
	if(dbgres) {
		newres = ResourceState.cur();
		newres.since(curres).print("difference after initial configure");
		curres = newres;
	}
	spawn plumbwatch();
	spawn go(g);

	sendopener("B");

Forloop:
	for(;;) {
		ev := <- ech;

		if(dbg > 1) {
			pick de := ev {
			Emouse =>
				if(dbg > 2 || de.mtype != E->Mmove)
					sys->print("%s\n", ev.tostring());
			* =>
				sys->print("%s\n", ev.tostring());
			}
		}
		pick  e := ev {
		Ekey =>
			g = nil;
			case e.keychar {
			E->Kdown =>
				curframe.yscroll(L->CAscrollpage, -1);
			E->Kup =>
				curframe.yscroll(L->CAscrollpage, 1);
			E->Khome =>
				curframe.yscroll(L->CAscrollpage, -10000);
			E->Kend => 
				curframe.yscroll(L->CAscrollpage, 10000);	
			E->Kaup =>
				curframe.yscroll(L->CAscrollline, -1);
			E->Kadown => 
				curframe.yscroll(L->CAscrollline, 1);	
			* =>
				handlekey(e);
			}
		Emouse =>
			g = handlemouse(e);
		Ereshape =>
			mainwin = G->mainwin;
			redraw(1);
			curframe = top;
			g = GoSpec.newspecial(GoHistnode, history.find(0));
		Equit =>
			break Forloop;
		Estop =>
			if(gopgrp != 0)
				stop();
			g = nil;
		Eback =>
			g = GoSpec.newspecial(GoHistnode, history.find(-1));
		Efwd =>
			g = GoSpec.newspecial(GoHistnode, history.find(1));
		Eform =>
			formaction(e.frameid, e.formid, e.ftype, 0);
			g = nil;
		Eformfield =>
			formfieldaction(e.frameid, e.formid, e.fieldid, e.fftype);
			g = nil;
		Ego =>
			case e.gtype {
			E->EGnormal =>
				url := CU->makeabsurl(e.url);
				if (url != nil)
					g = GoSpec.newget(GoNormal,url, e.target);
				else
					g = nil;
			E->EGreplace =>
				g = GoSpec.newget(GoReplace, U->parse(e.url), e.target);
			E->EGreload =>
				g = GoSpec.newspecial(GoHistnode, history.find(0));
			E->EGforward =>
				g = GoSpec.newspecial(GoHistnode, history.find(1));
			E->EGback =>
				g = GoSpec.newspecial(GoHistnode, history.find(-1));
			E->EGdelta =>
				g = GoSpec.newspecial(GoHistnode, history.find(e.delta));
			E->EGlocation =>
				g = GoSpec.newspecial(GoHistnode, history.findurl(e.url));
			}
		Esubmit =>
			if(e.subkind == CU->HGet)
				g = GoSpec.newget(GoNormal, e.action, e.target);
			else {
				g = GoSpec.newpost(e.action, e.data, e.target);
			}
		Escroll =>
			f := findframe(top, e.frameid);
			if (f != nil)
				f.scrollabs(e.pt);
			g = nil;
		Escrollr =>
			f := findframe(top, e.frameid);
			if (f != nil)
				f.scrollrel(e.pt);
			g = nil;
		Esettext =>
			f := findframe(top, e.frameid);
			if (f != nil)
				g = ref GoSpec (GoSettext, e.url, 0, e.text, f.name, "", nil);
		Elostfocus =>
			setfocus(nil);
			g = nil;
		Edismisspopup =>
			if (popupctl != nil)
				setfocus(popupctl.donepopup());
			popupctl = nil;
			grabctl = nil;
		}

		if (g == nil)
			continue;

		if (g.kind != GoSettext) {
			if (g.url != nil) {
				scheme := g.url.scheme;
				if (scheme == "javascript") {
					if (doscripts)
						spawn dojsurl(g);
					continue;
				}
				if (!CU->schemeok(scheme)) {
					url := g.url.tostring();
					if (plumbsend(url, "url") == -1)
						G->setstatus(X("bad URL", "gui")+": "+url);
					continue;
				}
			}
		}

		if(gopgrp != 0)
			stop();
		spawn go(g);
	}
	finish();
}

mkprog(c: Command, ctxt: ref Draw->Context, args: list of string)
{
	sys->pctl(Sys->NEWPGRP|Sys->NEWFD, list of {0, 1, 2});
	c->init(ctxt, args);
}

start()
{
	top = Frame.new();
	curframe = top;
	history = ref History(nil, 0, 0);
	
	keyfocus = nil;
	mouseover = nil;
	redraw(1);
}

redraw(resized: int)
{
	im := mainwin;
	if(resized) {
#		top.r = im.r.inset(2*L->ReliefBd);
		top.r = im.r;
		top.cim = mainwin;
		top.reset();
		(CU->imcache).resetlimits();
	}
	im.clipr = im.r;
#	L->drawrelief(im, top.r.inset(-L->ReliefBd), L->ReliefRaised);
#	L->drawrelief(im, top.r, L->ReliefSunk);
	L->drawfill(im, top.r, CU->White);
	G->flush(im.r);
#	im.clipr = top.r;
}

# Return a Loc representing a control in the frame f
frameloc(c: ref Control, f: ref Frame) : ref Loc
{
	loc := Loc.new();
	loc.add(L->LEframe, f.r.min);
	loc.le[loc.n-1].frame = f;
	if (c != nil) {
		loc.add(L->LEcontrol, c.r.min);
		loc.le[loc.n-1].control = c;
	}
	return loc;
}

resetkeyfocus(f: ref Frame)
{
	# determine if focus is in frame f or one of its sub-frames
	if (keyfocus == nil)
		return;

	for (focusf := keyfocus.f; focusf != nil; focusf = focusf.parent) {
		if (focusf == f) {
			keyfocus = nil;
			break;
		}
	}
	# current focus not in frameset being modified - leave as is
}

ctlmouse(e: ref Event.Emouse, ctl, grab: ref Control): ref Control
{
	ev := E->SEnone;
	(action, newgrab) := ctl.domouse(e.p, e.mtype, grab);
	case (action) {
	L->CAbuttonpush =>
		if(doscripts && ctl.ff != nil && ctl.ff.evmask)
			ev = E->SEonclick;
		else
			pushaction(ctl, e.p.sub(ctl.r.min));
	L->CAkeyfocus =>
		setfocus(ctl);
	L->CAchanged =>
		# Select Formfield - selection has changed
		ev = E->SEonchange;
	L->CAselected =>
		# text input Formfield - text selection has changed
		ev = E->SEonselect;
	L->CAdopopup =>
		popupctl = ctl.dopopup();
		if (popupctl != nil)
			setfocus(popupctl);
	L->CAdonepopup =>
		setfocus(ctl.donepopup());
		ev = E->SEonchange;
		popupctl = nil;
	}
	if (doscripts && ctl.ff != nil && (ctl.ff.evmask & ev)) {
		se := ref E->ScriptEvent(ev, ctl.f.id, ctl.ff.form.formid, ctl.ff.fieldid,
				-1, -1, e.p.x, e.p.y, 1, nil, nil, 0);
		J->jevchan <-= se;
	}
	return newgrab;
}

mainwinmouse(e: ref Event.Emouse) : (ref GoSpec, ref Control)
{
	p := e.p;
	g : ref GoSpec;
	ctl : ref Control;
	newgrab : ref Control;
	domouseout := 0;
	loc : ref Loc;
	if(mouseover != nil)
		domouseout = 1;

	loc = top.find(p, nil);
	if(loc != nil) {
		if(dbg > 1)
			loc.print("mouse loc");
		f := loc.lastframe();
		hasscripts := f.doc.hasscripts;
		if(e.mtype != E->Mmove)
			curframe = f;
		n1 := loc.n-1;
		case loc.le[n1].kind {
		L->LEitem =>
			it := loc.le[n1].item;
			if (it.anchorid < 0)
				break;

			a : ref Build->Anchor = nil;
			for(al := f.doc.anchors; al != nil; al = tl al) {
				a = hd al;
				if(a.index == it.anchorid)
					break;
			}
			if (al == nil)
				break;

			if(dbg > 1)
				sys->print("in anchor %d, href=%s\n", a.index, a.href.tostring());
			if(doscripts && a.evmask) {
				if(a == mouseover) {
					domouseout = 0;	# still over same anchor
				} else if(e.mtype == E->Mmove) {
					if(domouseout) {
						if(mouseover.evmask & E->SEonmouseout) {
							se := ref E->ScriptEvent(E->SEonmouseout, mouseoverfr.id, -1, -1, mouseover.index, -1, 0, 0, 0, nil, nil, 0);
							J->jevchan <-= se;
						}
						domouseout = 0;
					}
					mouseover = a;
					mouseoverfr = f;
					if(a.evmask & E->SEonmouseover) {
						se := ref E->ScriptEvent(E->SEonmouseover, f.id, -1, -1, a.index, -1, e.p.x, e.p.y, 0, nil, nil, 0);
						J->jevchan <-= se;
					}
				}
				if (e.mtype == E->Mlbuttonup || e.mtype == E->Mldrop) {
					if(a.evmask & E->SEonclick) {
						se := ref E->ScriptEvent(E->SEonclick, f.id, -1, -1, a.index, -1, 0, 0, 0, nil, nil, 0);
						J->jevchan <-= se;
						break;
					}
					ctl = nil;
				}
			}
			if(e.mtype == E->Mlbuttonup || e.mtype == E->Mldrop) {
				g = anchorgospec(it, a, loc.pos);
				if (g == nil)
					break;
			} else if(e.mtype == E->Mmbuttonup) {
				g = anchorgospec(it, a, loc.pos);
				if (g == nil)
					break;
				url := g.url.tostring();
				G->setstatus(url);
				G->snarfput(url);
				g = nil;
			}
		L->LEcontrol =>
			ctl = loc.le[n1].control;
		}
	}
	if (ctl != nil)
		newgrab = ctlmouse(e, ctl, nil);
	if(newgrab == nil && domouseout && doscripts) {
		if(mouseover.evmask & E->SEonmouseout) {
			se := ref E->ScriptEvent(E->SEonmouseout,
				mouseoverfr.id, -1, -1, mouseover.index, -1, 0, 0, 0, nil, nil, 0);
			J->jevchan <-= se;
		}
		mouseoverfr = nil;
		mouseover = nil;
	}
	return (g, newgrab);
}

dojsurl(g : ref GoSpec)
{
	f := curframe;
	case g.target {
	"_top" =>
		f = top;
	"_self" =>
		; # curframe is already OK
	"_parent" =>
		if(f.parent != nil)
			f = f.parent;
	"_blank" =>
		f = top; # we don't create new browsers...
	* =>
		# this is recommended "current practice"
		f = findnamedframe(f, g.target);
		if(f == nil) {
			f = findnamedframe(top, g.target);
			if(f == nil)
				f = top;
		}
	}

	jev := ref E->ScriptEvent (E->SEscript, f.id, -1, -1, -1, -1, 0, 0, 0, g.url.path, chan of string, 0);
	J->jevchan <-= jev;
	v := <- jev.reply;
	if (v != nil) {
		ev := ref Event.Esettext(f.id, g.url, v);
		E->evchan <-= ev;
	}
}

# If mouse event results in command to navigate somewhere else,
# return a GoSpec ref, else nil.
handlemouse(e: ref Event.Emouse): ref GoSpec
{
	g: ref GoSpec;
	ctl := grabctl;
	if (popupctl != nil)
		ctl = popupctl;
	if (ctl != nil)
		grabctl = ctlmouse(e, ctl, grabctl);
	else if (e.p.in(mainwin.r))
		(g, grabctl) = mainwinmouse(e);
	return g;
}

setfocus(newc : ref Control)
{
	newf, oldf: ref Frame;
	if (newc != nil)
		newf = newc.f;

	oldc := keyfocus;
	if (oldc != nil)
		oldf = oldc.f;
	
	if (oldc != nil && oldc != newc)
		oldc.losefocus(1);
	if (oldf != nil && oldf != newf)
		oldf.focus(0, 1);
	if (newf != nil && newf != oldf)
		newf.focus(1,1);
	if (newc != nil && newc != oldc)
		newc.gainfocus(1);
	keyfocus = newc;
}

handlekey(e: ref Event.Ekey)
{
	c := keyfocus;
	if (c == nil)
		return;

	pick ce := c {
	Centry =>
		case c.dokey(e.keychar) {
		L->CAreturnkey =>
			if(c.ff != nil) {
				spawn form_submit(c.f, c.ff.form, p0, c, 1);
				return;
			}
		L->CAtabkey =>
			# if control in a form - move focus to next focus-able control
			if (c.ff != nil) {
				found := 0;
				form := c.ff.form;
				nextff : ref B->Formfield;
				for (ffl := form.fields; ffl != nil; ffl = tl ffl) {
					ff := hd ffl;
					if (ff == c.ff) {
						found = 1;
						continue;
					}
					if (ff.ftype == B->Ftext || ff.ftype == B->Fpassword) {
						if (nextff == nil || found)
							nextff = ff;
						if (found)
							break;
					}
				}
				if (nextff != nil)
					formfield_focus(c.f, nextff);
			}
		}
	}
	return;
}

fileexist(file: string) :int
{
		fd := sys->open(file, sys->OREAD);
		if (fd == nil)
			return 0;
		else
			return 1;
}

go(g: ref GoSpec)
{
	gopgrp = sys->pctl(sys->NEWPGRP, nil);
	spawn goproc(g);

	# got to make netget the thread with the gopgrp thread,
	# since it runs until killed, and killing a pgrp needs an active
	# thread
	CU->netget();
}

goproc(g: ref GoSpec)
{
	origkind := g.kind;
	hn : ref HistNode = nil;
	doctext := "";
	case origkind {
	GoNormal or
	GoReplace or
	GoSettext =>
		;
	GoHistnode =>
		hn = g.histnode;
		if(hn == nil)
			return;
		g = hn.topconfig.gospec;
	}
	case g.target {
	"_top" =>
		curframe = top;
	"_self" =>
		; # curframe is already OK
	"_parent" =>
		if(curframe.parent != nil)
			curframe = curframe.parent;
	"_blank" =>
		curframe = top; # we don't create new browsers...
	* =>
		# this is recommended "current practice"
		curframe = findnamedframe(curframe, g.target);
		if(curframe == nil) {
			curframe = findnamedframe(top, g.target);
			if(curframe == nil)
				curframe = top;
		}
	}

	f := curframe;
	if(dbg) {
		sys->print("\n\nGO TO %s\n", g.url.tostring());
		if(g.target != "_top")
			sys->print("target frame name=%s\n", f.name);
	}
	G->progress <-= (-1, G->Pstart, 0, "");
	err := "";
	status := "Done";

	if((origkind == GoNormal || origkind == GoReplace || origkind == GoLink) && g.url.frag != "" 
			&& f.doc != nil && f.doc.src != nil && CU->urlequal(g.url, f.doc.src))
		go_local(f, g.url.frag);
	else {
		if (g.kind == GoSettext)
			settext(g, f, g.body);
		else
			err = get(g, f, origkind, hn);

		if(doscripts && J->defaultStatus != "")
			status = J->defaultStatus;
	}
	if(err != nil) {
		status = err;
		G->progress <-= (-1, G->Perr, 100, err);
	} else 
		G->progress <-= (-1, G->Pdone, 0, nil);
		
	G->setstatus(status);
	checkrefresh(f);
}

settext(g : ref GoSpec, f : ref Frame, text : string) : string
{
	sdest := g.url.tostring();
	G->setstatus(X("Fetching", "gui") + " " + sdest);
	bs := CU->stringreq(text);
	G->seturl(sdest);
	history.add(f, g, GoNormal);
	resetkeyfocus(f);
	L->layout(f, bs, 0);
	if (J != nil)
		J->framedone(f, f.doc.hasscripts);
	history.update(f);
	error := "";
	if(f.kids != nil) {
		if(J != nil)
			J->frametreechanged(f);
		nkids := len f.kids;
		kdone := chan of (ref Frame, string);
		for(kl := f.kids; kl != nil; kl = tl kl) {
			k := hd kl;
			if(k.src != nil) {
				gs := GoSpec.newget(GoNormal, k.src, "_self");
				if(dbg)
					sys->print("get child frame %s\n", gs.url.tostring());
				spawn getproc(gs, k, GoNormal, nil, kdone);
			}
		}
		while (nkids--) {
			(k, e) := <- kdone;
			if (error != nil)
				error = e;
			checkrefresh(k);
		}
	}

	if (J != nil) {
#this code should be split off as it is duplicated from get()
		# at this point all sub-frames and images have been loaded
		# Optimise this! so as only do it if a doc in the frameset
		# has script/event code
		J->jevchan <-= ref E->ScriptEvent(E->SEonload, f.id, -1, -1, -1, -1, -1, -1, -1, nil, nil, 0);
		if (doscripts && f.doc.hasscripts) {
			for(itl := f.doc.images; itl != nil; itl = tl itl) {
				it := hd itl;
				if(it.genattr == nil || !it.genattr.evmask)
					continue;
				ev := E->SEnone;
				pick im := it {
				Iimage =>
					case im.ci.complete {
					# correct to equate these two ?
					Img->Mimnone or
					Img->Mimerror =>
						ev = E->SEonerror;
					Img->Mimdone =>
						ev = E->SEonload;
					}
					if(im.genattr.evmask & ev)
						J->jevchan <-= ref E->ScriptEvent(ev, f.id, -1, -1, -1, im.imageid, -1, -1, -1, nil, nil, 0);
				}
			}
		}
	}
	return error;
}

getproc(g: ref GoSpec, f: ref Frame, origkind: int, hn: ref HistNode, done : chan of (ref Frame, string))
{
	done <-= (f, get(g, f, origkind, hn));
}

get(g: ref GoSpec, f: ref Frame, origkind: int, hn: ref HistNode) : string
{
	curres, newres: ResourceState;
	if(dbgres) {
		(CU->imcache).clear();
		curres = ResourceState.cur();
	}
	sdest := g.url.tostring();
        G->setstatus(X("Fetching", "gui") + " " + sdest);
	bsmain : ref ByteSource;
	hdr : ref Header;
	ri := ref ReqInfo(g.url, g.meth, array of byte g.body, g.auth, g.target);
	authtried := 0;
	realm := "";
	auth := "";
	error := "";
	for(nredirs := 0; ; nredirs++) {
		bsmain = CU->startreq(ri);
		error = bsmain.err;
		if(error != "") {
			CU->freebs(bsmain);
			return error;
		}
		CU->waitreq(bsmain::nil);
		error = bsmain.err;
		if(error != "") {
			CU->freebs(bsmain);
			return error;
		}
		hdr = bsmain.hdr;
		(use, e, challenge, newurl) := CU->hdraction(bsmain, 1, nredirs);
		error = e;
		if(challenge != nil) {
			if(authtried) {
				# we already tried once; give up
				error = "Need authorization";
				use = 1;
			}
			else {
				(realm, auth) = getauth(challenge);
				if(auth != "") {
					ri.auth = auth;
					authtried = 1;
					CU->freebs(bsmain);
					continue;
				}
				else {
					error = "Need authorization";
					use = 1;
				}
			}
		}
		if (error == nil) {
			if (hdr.code != CU->HCOk)
				error = CU->hcphrase(hdr.code);
			if(authtried) {
				# it succeeded; add to auths list so don't have to ask again
				auths = ref AuthInfo(realm, auth) :: auths;
			}
		}
		if(newurl != nil) {
			ri.url = newurl;
			# some sites (e.g., amazon.com) assume that POST turns into
			# GET on redirect (maybe this is just http 1.0?)
			ri.method = CU->HGet;
			CU->freebs(bsmain);
			continue;
		}
		if(use == 0) {
			CU->freebs(bsmain);
			return error;
		}
		break;
	}
	if(dbgres > 1) {
		newres = ResourceState.cur();
		newres.since(curres).print("resources to get header");
		curres = newres;
	}
	if(hdr.mtype == CU->TextHtml || hdr.mtype == CU->TextPlain ||
					I->supported(hdr.mtype)) {
		G->seturl(sdest);
		history.add(f, g, origkind);
		resetkeyfocus(f);
		srcdata := L->layout(f, bsmain, origkind == GoLink);
		if (J != nil)
			J->framedone(f, f.doc.hasscripts);
		history.update(f);
		if(dbgres > 1) {
			newres = ResourceState.cur();
			newres.since(curres).print("resources to get page and do layout");
			curres = newres;
		}
		if(f.kids != nil) {
			if(J != nil)
				J->frametreechanged(f);
			i := 0;
			nkids := len f.kids;
			kdone := chan of (ref Frame, string);
			for(kl := f.kids; kl != nil; kl = tl kl) {
				k := hd kl;
				if(k.src != nil) {
					if(hn != nil)
						gs := hn.kidconfigs[i].gospec;
					else
						gs = GoSpec.newget(GoNormal, k.src, "_self");
					if(dbg)
						sys->print("get child frame %s\n", gs.url.tostring());
					gokind := GoLink;
					if (origkind != GoLink)
						gokind = GoNormal;
					spawn getproc(gs, k, gokind, nil, kdone);
				}
				i++;
			}
			while (nkids--) {
				(k, err) := <- kdone;
				if (error == nil)
					# we currently only capture the first error
					# as we only have one palce to report it
					error = err;
				checkrefresh(k);
			}
		}

		if (J != nil) {
			# at this point all sub-frames and images have been loaded
			J->jevchan <-= ref E->ScriptEvent(E->SEonload, f.id, -1, -1, -1, -1, -1, -1, -1, nil, nil, 0);
			if (doscripts && f.doc.hasscripts) {
				for(itl := f.doc.images; itl != nil; itl = tl itl) {
					it := hd itl;
					if(it.genattr == nil || !it.genattr.evmask)
						continue;
					ev := E->SEnone;
					pick im := it {
					Iimage =>
						case im.ci.complete {
						# correct to equate these two ?
						Img->Mimnone or
						Img->Mimerror =>
							ev = E->SEonerror;
						Img->Mimdone =>
							ev = E->SEonload;
						}
						if(im.genattr.evmask & ev)
							J->jevchan <-= ref E->ScriptEvent(ev, f.id, -1, -1, -1, im.imageid, -1, -1, -1, nil, nil, 0);
					}
				}
			}
		}

		if(g.url.frag != "")
			go_local(f, g.url.frag);
	}
	else {
		error = X("Unsupported media type", "gui")+ " "+CU->mnames[hdr.mtype];
		# Optionally put a save-as dialog up here.
		if((CU->config).offersave)
			dosaveas(bsmain);
		CU->freebs(bsmain);
	}
	if(dbgres == 1) {
		newres = ResourceState.cur();
		newres.since(curres).print("resources to do page");
		curres = newres;
	}
	return error;
}

# Scroll frame f so that destination hyperlink loc is at top of view
go_local(f: ref Frame, loc: string)
{
	if(dbg)
		sys->print("go to local destination %s\n", loc);
	for(ld := f.doc.dests; ld != nil; ld = tl ld) {
		d := hd ld;
		if(d.name == loc) {
			dloc := f.find(p0, d.item);
			if(dloc == nil) {
				if(warn)
					sys->print("couldn't find item for destination anchor %s\n", loc);
				return;
			}
			p := f.sptolp(dloc.le[dloc.n-1].pos);
			f.yscroll(L->CAscrollabs, p.y);
			return;
		}
	}
	# special location names...
	l := S->tolower(loc);
	if(l == "top" || l == "home"){
		f.yscroll(L->CAscrollabs, 0);
		return;
	}
	if(l == "end" || l=="bottom"){
		f.yscroll(L->CAscrollabs, f.totalr.max.y);
		return;
	}
	if(warn)
		sys->print("couldn't find destination anchor %s\n", loc);
}

stripwhite(s: string) : string
{
	j := 0;
	n := len s;
	for(i := 0; i < n; i++) {
		c := s[i];
		if(c < C->NCTYPE && C->ctype[c]==C->W)
			continue;
		s[j++] = c;
	}
	if(j < n)
		s = s[0:j];
	return s;
}

# If refresh has been set in f (i.e., client pull),
# pause the appropriate amount of time and then go to new place
checkrefresh(f: ref Frame)
{
	if(f.doc != nil && f.doc.refresh != "") {
		seconds := 0;
		url : ref Parsedurl = nil;
		refresh := stripwhite(f.doc.refresh);
		(n, l) := sys->tokenize(refresh, ";");
		if(n > 0) {
			seconds = int hd l;
			if(n > 1) {
				s := hd tl l;
				if(len s > 4 && S->tolower(s[0:4]) == "url=") {
					url = U->mkabs(U->parse(s[4:]), f.doc.base);
				}
			}
		}
		spawn dorefresh(f, seconds, url);
	}
}

dorefresh(f: ref Frame, seconds: int, url: ref Parsedurl)
{
	sys->sleep(seconds * 1000);
	e : ref Event;
	if(url == nil)
		e = ref Event.Ego(nil, f.name, 0, E->EGreload);
	else
		e = ref Event.Ego(url.tostring(), f.name, 0, E->EGnormal);
	E->evchan <-= e;
}

# Do depth first search from f, looking for frame with given name.
findnamedframe(f: ref Frame, name: string) : ref Frame
{
	if(f.name == name)
		return f;
	for(l := f.kids; l != nil; l = tl l) {
		k := hd l;
		a := findnamedframe(k, name);
		if(a != nil)
			return a;
	}
	return nil;
}

# Similar, but look for frame id, starting from f
findframe(f: ref Frame, id: int) : ref Frame
{
	if(f.id == id)
		return f;
	for(l := f.kids; l != nil; l = tl l) {
		k := hd l;
		a := findframe(k, id);
		if(a != nil)
			return a;
	}
	return nil;
}

# Return Gospec resulting from button up in anchor a, at offset pos inside item it.
anchorgospec(it: ref Item, a: ref B->Anchor, p: Point) : ref GoSpec
{
	g : ref GoSpec;
	u := a.href;
	target := a.target;
	pick i := it {
	Iimage =>
		ci := i.ci;
		if(ci.mims != nil) {
			if(i.map != nil) {
				(u, target) = findhit(i.map, p, ci.width, ci.height);
			}
			else if(u != nil && u.scheme != "javascript" && (it.state&B->IFsmap)) {
				# copy u, add ?x,y
				x := min(max(p.x-(int i.hspace + int i.border),0),ci.width-1);
				y := min(max(p.y-(int i.vspace + int i.border),0),ci.height-1);
				u = ref *a.href;
				u.query = string x + "," + string y;
			}
		}
	Ifloat =>
		return anchorgospec(i.item, a, p);
	}

	if(u != nil)
		g = GoSpec.newget(GoLink, u, target);
	return g;
}

# Control c has been pushed.
# Find the form it is in and perform required action (reset, or submit).
pushaction(c: ref Control, pt: Point)
{
	pick b := c {
	Cbutton =>
		ff := b.ff;
		f := b.f;
		if(ff != nil) {
			case ff.ftype {
			B->Fsubmit or B->Fimage =>
				spawn form_submit(c.f, ff.form, pt, c, 1);
			B->Freset =>
				spawn form_reset(f, ff.form);
			}
		}
	}
}

# if onsubmit==1, then raise onsubmit event (if handler present)
form_submit(fr: ref Frame, frm: ref B->Form, p: Point, submitctl: ref Control, onsubmit: int)
{
	submitfield : ref B->Formfield;
	if (submitctl != nil)
		submitfield = submitctl.ff;

	if(submitctl != nil && tagof(submitctl) == tagof(Control.Centry)) {
		# Via CR, so only submit if there is a submit button (first one is the default)
		firstsubmit : ref B->Formfield;
		for(l := frm.fields; l != nil; l = tl l) {
			f := hd l;
			if (f.ftype == B->Fsubmit) {
				firstsubmit = f;
				break;
			}
		}
		if (firstsubmit == nil)
			return;
		submitfield = firstsubmit;
	}
	if(doscripts && fr.doc.hasscripts && onsubmit && (frm.evmask & E->SEonsubmit)) {
		c := chan of string;
		J->jevchan <-= ref E->ScriptEvent(E->SEonsubmit, fr.id, frm.formid, -1, -1, -1, -1, -1, -1, nil, c, 0);
		if(<-c == nil)
			return;
	}
	v := "";
	sep := "";
	radiodone : list of string = nil;
floop:
	for(l := frm.fields; l != nil; l = tl l) {
		f := hd l;
		if(f.name == "")
			continue;
		val := "";
		c: ref Control;
		if(f.ctlid >= 0)
			c = fr.controls[f.ctlid];
		case f.ftype {
			B->Ftext or B->Fpassword or B->Ftextarea =>
				if(c != nil)
					pick e := c {
					Centry =>
						val = e.s;
					}
				if(val != "" && f.name == "_ISINDEX_") {
					# just the index terms after the "?"
					if(sep != "")
						v = v + sep;
					sep = "&";
					v = v + ucvt(val);
					break floop;
				}
			B->Fcheckbox or B->Fradio =>
				if(f.ftype == B->Fradio) {
					# Need the following to catch case where there
					# is more than one radiobutton with the same name
					# and value.
					for(rl := radiodone; rl != nil; rl = tl rl)
						if(hd rl == f.name)
							continue floop;
				}
				checked := 0;
				if(c != nil)
					pick cb := c {
					Ccheckbox =>
						checked = cb.flags & L->CFactive;
					}
				if(checked) {
					val = f.value;
					if(f.ftype == B->Fradio)
						radiodone = f.name :: radiodone;
				}
				else
					continue;
			B->Fhidden =>
				val = f.value;
			B->Fsubmit =>
				if(submitctl != nil && f == submitctl.ff && f.name != "_no_name_submit_")
					val = f.value;
				else
					continue;
			B->Fselect =>
				if(c != nil)
					pick s := c {
					Cselect =>
						for(i := 0; i < len s.options; i++) {
							if(s.options[i].selected) {
								if(sep != "")
									v = v + sep;
								sep = "&";
								v = v + ucvt(f.name) + "=" + ucvt(s.options[i].value);
							}
						}
						continue;
					}
			B->Fimage =>
				if(submitctl != nil && f == submitctl.ff) {
					if(sep != "")
						v = v + sep;
					sep = "&";
					v = v + ucvt(f.name + ".x") + "=" + ucvt(string max(p.x,0))
						+ sep + ucvt(f.name + ".y") + "=" + ucvt(string max(p.y,0));
					continue;
				}
		}
#		if(val != "") {
			if(sep != "")
				v = v + sep;
			sep = "&";
			v = v + ucvt(f.name) + "=" + ucvt(val);
#		}
	}
	action := ref *frm.action;
	if (frm.method == CU->HGet) {
		if (action.query != "" && v != "")
			action.query += "&";
		action.query += v;
		v = "";
	}
#	action.query = v;
	E->evchan <-= ref Event.Esubmit(frm.method, action, v, frm.target);
}

hexdigit := "0123456789ABCDEF";
urlchars := array [128] of {
	'a' to 'z' => byte 1,
	'A' to 'Z' => byte 1,
	'0' to '9' => byte 1,
	'-' or '/' or '$' or '_' or '@' or '.' or '!' or '*' or '\'' or '(' or ')' => byte 1,
	* => byte 0
};

ucvt(s: string): string
{
	b := array of byte s;
	u := "";
	for(i := 0; i < len b; i++) {
		c := int b[i];
		if (c < len urlchars && int urlchars[c])
			u[len u] = c;
		else if(c == ' ')
			u[len u] = '+';
		else {
			u[len u] = '%';
			u[len u] = hexdigit[(c>>4)&15];
			u[len u] = hexdigit[c&15];
		}
	}
	return u;
}

form_reset(fr: ref Frame, frm: ref B->Form)
{
	if(doscripts && fr.doc.hasscripts && (frm.evmask & E->SEonreset)) {
		c := chan of string;
		J->jevchan <-= ref E->ScriptEvent(E->SEonreset, fr.id, frm.formid, -1, -1, -1, -1, -1, -1, nil, c, 0);
		if(<-c == nil)
			return;
	}
	for(fl := frm.fields; fl != nil; fl = tl fl) {
		a := hd fl;
		if(a.ctlid >= 0)
			fr.controls[a.ctlid].reset();
	}
#	fr.cim.flush(D->Flushnow);
}

formaction(frameid, formid, ftype, onsubmit: int)
{
	if(dbg > 1)
		sys->print("formaction %d %d %d %d\n", frameid, formid, ftype, onsubmit);
	f := findframe(top, frameid);
	if(f != nil) {
		d := f.doc;
		if(d != nil) {
			for(fl := d.forms; fl != nil; fl = tl fl) {
				frm := hd fl;
				if(frm.formid == formid) {
					if(ftype == E->EFsubmit)
						spawn form_submit(f, frm, Point(0,0), nil, onsubmit);
					else
						spawn form_reset(f, frm);
				}
			}
		}
	}
}

formfield_blur(f: ref Frame, ff: ref B->Formfield)
{
	if(ff.ftype != B->Fhidden) {
		c := f.controls[ff.ctlid];
		if(!(c.flags & L->CFhasfocus))
			return;
		# lose focus quietly - don't raise "onblur" event for the given control
		c.losefocus(0);
		setfocus(nil);
	}
}

formfield_focus(f: ref Frame, ff: ref B->Formfield)
{
	if(ff.ftype != B->Fhidden) {
		c := f.controls[ff.ctlid];
		if(c.flags & L->CFhasfocus)
			return;
		# gain focus quietly - don't raise "onfocus" event for the given control
		c.gainfocus(0);
		setfocus(c);
	}
}

# simulate a mouse click, but don't trigger onclick event
formfield_click(f: ref Frame, frm: ref B->Form, ff: ref B->Formfield)
{
	c := f.controls[ff.ctlid];
	case ff.ftype {
	B->Fcheckbox or
	B->Fradio or
	B->Fbutton =>
		c.domouse(p0, E->Mlbuttonup, nil);
	B->Fsubmit =>
		spawn form_submit(f, frm, p0, c, 1);
	B->Freset =>
		spawn form_reset(f, frm);
	}
}

formfield_select(f: ref Frame, ff: ref B->Formfield)
{
	case ff.ftype {
	B->Ftext or
	B->Fselect or
	B->Ftextarea =>
		ctl := f.controls[ff.ctlid];
		pick c := ctl {
		Centry =>
			c.sel = (0, len c.s);
			ctl.draw(1);
		}
	}
}

formfieldaction(frameid, formid, fieldid, fftype: int)
{
	if(dbg > 1)
		sys->print("formfieldaction %d %d %d %d\n", frameid, formid, fieldid, fftype);
	f := findframe(top, frameid);
	if(f == nil || f.doc == nil)
		return;

	# find form in frame
	frm : ref B->Form;
	for(fl := f.doc.forms; fl != nil; fl = tl fl) {
		if((hd fl).formid == formid) {
			frm = hd fl;
			break;
		}
	}
	if(frm == nil)
		return;

	# find formfield in form
	ff : ref B->Formfield;
	for(ffl := frm.fields; ffl != nil; ffl = tl ffl) {
		if((hd ffl).fieldid == fieldid) {
			ff = hd ffl;
			break;
		}
	}
	if(ff == nil || ff.ctlid < 0)
		return;

	# perform action
	case fftype {
	E->EFFblur =>
		formfield_blur(f, ff);
	E->EFFfocus =>
		formfield_focus(f, ff);
	E->EFFclick =>
		formfield_click(f, frm, ff);
	E->EFFselect =>
		formfield_select(f, ff);
	E->EFFredraw =>
		c := f.controls[ff.ctlid];
		pick ctl := c {
		Cselect =>
			sel := 0;
			for (i := 0; i < len ctl.options; i++) {
				if (ctl.options[i].selected) {
					sel = i;
					break;
				}
			}
			if (sel > len ctl.options - ctl.nvis)
				sel = len ctl.options - ctl.nvis;
			ctl.first = sel;
		}
		c.draw(1);
	}
}

# Find hit in a local map
findhit(map: ref B->Map, p: Point, w, h: int) : (ref Parsedurl, string)
{
	x := p.x;
	y := p.y;
	dflt : ref Parsedurl = nil;
	dflttarg := "";
	for(al := map.areas; al != nil; al = tl al) {
		a := hd al;
		c := a.coords;
		nc := len c;
		x1 := 0;
		y1 := 0;
		x2 := 0;
		y2 := 0;
		if(nc >= 2) {
			x1 = d2pix(c[0], w);
			y1= d2pix(c[1], h);
			if(nc > 2) {
				x2 = d2pix(c[2], w);
				if(nc > 3)
					y2 = d2pix(c[3], h);
			}
		}
		hit := 0;
		case a.shape {
		"rect" or "rectangle" =>
			if(nc == 4)
				hit = x1 <= x && x <= x2 &&
					y1 <= y && y <= y2;
		"circ" or "circle" =>
			if(nc == 3) {
				xd := x - x1;
				yd := y - y1;
				hit = xd*xd + yd*yd <= x2*x2;
			}
		"poly" or "polygon" =>
			np := nc / 2;
			hit = 0;
			xr := real x;
			yr := real y;
			j := np - 1;
			for(i := 0; i < np; j = i++) {
				xi := real d2pix(c[2*i], w);
				yi := real d2pix(c[2*i+1], h);
				xj := real d2pix(c[2*j], w);
				yj := real d2pix(c[2*j+1], h);
				if ((((yi<=yr) && (yr<yj)) ||
				     ((yj<=yr) && (yr<yi))) &&
				    (xr < (xj - xi) * (yr - yi) / (yj - yi) + xi))
					hit = !hit;
			}
		"def" or "default" =>
			dflt = a.href;
			dflttarg = a.target;
		}
		if(hit)
			return (a.href, a.target);
	}
	return (dflt, dflttarg);
}

d2pix(d: B->Dimen, tot: int) : int
{
	ans := d.spec();
	if(d.kind() == B->Dpercent)
		ans = (ans * tot) / 100;
	return ans;
}
GoSpec.newget(kind: int, url: ref Parsedurl, target: string) : ref GoSpec
{
	return ref GoSpec(kind, url, CU->HGet, "", target, "", nil);
}

GoSpec.newpost(url: ref Parsedurl, body, target: string) : ref GoSpec
{
	return ref GoSpec(GoNormal, url, CU->HPost, body, target, "", nil);
}

GoSpec.newspecial(kind: int, hn: ref HistNode) : ref GoSpec
{
	return ref GoSpec(kind, nil, 0, "", "", "", hn);
}

GoSpec.equal(a: self ref GoSpec, b: ref GoSpec) : int
{
	if(a.url == nil || b.url == nil)
		return 0;
	return CU->urlequal(a.url, b.url) && a.meth == b.meth && a.body == b.body;
}

DocConfig.equal(a: self ref DocConfig, b: ref DocConfig) : int
{
	return a.framename == b.framename && a.gospec.equal(b.gospec);
}

DocConfig.equalarray(a1: array of ref DocConfig, a2: array of ref DocConfig) : int
{
	n := len a1;
	if(n != len a2)
		return 0;
	for(i := 0; i < n; i++) {
		if(a1[i] == nil || a2[i] == nil)
			continue;
		if(!(a1[i]).equal(a2[i]))
			return 0;
	}
	return 1;
}

# Put b in a.succs (if atob is true) or a.preds (if atob is false)
# at front of list.
# If it is already in the list, move it to the front.
HistNode.addedge(a: self ref HistNode, b: ref HistNode, atob: int)
{
	if(atob)
		oldl := a.succs;
	else
		oldl = a.preds;
	there := 0;
	for(l := oldl; l != nil; l = tl l)
		if(hd l == b) {
			there = 1;
			break;
		}
	if(there)
		newl := b :: remhnode(oldl, b);
	else
		newl = b :: oldl;
	if(atob)
		a.succs = newl;
	else
		a.preds = newl;
}

# return copy of l with hn removed (known that hn
# occurs at most once)
remhnode(l: list of ref HistNode, hn: ref HistNode) : list of ref HistNode
{
	if(l == nil)
		return nil;
	hdl := hd l;
	if(hdl == hn)
		return tl l;
	return hdl :: remhnode(tl l, hn);
}

# Copy of a, with new kidconfigs array (so that it can be changed independent
# of a), and clear the preds and succs.
HistNode.copy(a: self ref HistNode) : ref HistNode
{
	n := len a.kidconfigs;
	kc : array of ref DocConfig = nil;
	if(n > 0) {
		kc = array[n] of ref DocConfig;
		for(i := 0; i < n; i++)
			kc[i] = a.kidconfigs[i];
	}
	return ref HistNode(a.topconfig, kc, nil, nil, -1, nil);
}

# This is called just before layout of f with result of getting g.
# (we don't yet know doctitle and whether this is a frameset).
# If navkind is not GoHistnode, update the history graph; but if
# navkind is GoReplace, replace oldcur with the new HistNode.
# In any case reorder the history array to put latest last in array.
History.add(h: self ref History, f: ref Frame, g: ref GoSpec, navkind: int)
{
	if(len h.h <= h.n) {
		newh := array[len h.h + 20] of ref HistNode;
		newh[0:] = h.h;
		h.h = newh;
	}
	oldcur : ref HistNode;
	if(h.n > 0)
		oldcur = h.h[h.n-1];
	dc := ref DocConfig(f.name, g.url.tostring(), navkind != GoHistnode, g);
	hnode := ref HistNode(dc, nil, nil, nil, -1, nil);
	if(f == top) {
		g.target = "_top";
	}
	else if(oldcur != nil) {
		# oldcur should be a frameset and f should be a kid in it
		kidpos := -1;
		for(i := 0; i < len oldcur.kidconfigs; i++) {
			kc := oldcur.kidconfigs[i];
			if(kc != nil && kc.framename == f.name) {
				kidpos = i;
				break;
			}
		}
		if(kidpos == -1) {
			if(dbg)
				sys->print("history botch\n");
		}
		else {
			hnode = oldcur.copy();
			hnode.kidconfigs[kidpos] = dc;
		}
	}
	# see if equivalent node to hnode is already in history
	hnodepos := -1;
	for(i := 0; i < h.n; i++) {
		if(hnode.topconfig.equal(h.h[i].topconfig)) {
			if((hnode.kidconfigs==nil && h.h[i].topconfig.initconfig) ||
			   DocConfig.equalarray(hnode.kidconfigs, h.h[i].kidconfigs)) {
				hnodepos = i;
				hnode = h.h[i];
				break;
			}
		}
	}
	if(hnodepos == -1) {
		if(navkind == GoReplace && h.n > 0)
			h.n--;
		hnodepos = h.n;
		h.h[h.n++] = hnode;
	}
	if(oldcur != nil && hnode != oldcur && navkind != GoHistnode) {
		oldcur.addedge(hnode, 1);
		if(navkind != GoReplace)
			hnode.addedge(oldcur, 0);
		else if(oldcur.preds != nil)
			hnode.addedge(hd oldcur.preds, 0);
	}
	if(hnodepos != h.n-1) {
		# move hnode to h.n-1, and shift rest back
		for(k := hnodepos; k < h.n-1; k++)
			h.h[k] = h.h[k+1];
		h.h[h.n-1] = hnode;
	}
	G->backbutton(hnode.preds != nil);
	G->fwdbutton(hnode.succs != nil);
}

# This is called just after layout of f.
# Now we can put in correct doctitle, and make kids array if necessary.
History.update(h: self ref History, f: ref Frame)
{
	hnode := h.h[h.n-1];
	if(f == top) {
		hnode.topconfig.title = f.doc.doctitle;
		if(f.kids != nil && hnode.kidconfigs == nil) {
			kc := array[len f.kids] of ref DocConfig;
			i := 0;
			for(l := f.kids; l != nil; l = tl l) {
				kf := hd l;
				if(kf.src != nil)
					kc[i] = ref DocConfig(kf.name, kf.src.tostring(), 1,  GoSpec.newget(GoNormal, kf.src, "_self"));
				i++;
			}
			hnode.kidconfigs = kc;
		}
	}
	else {
		# hnode should be a frameset and f should be a kid in it
		for(i := 0; i < len hnode.kidconfigs; i++) {
			kc := hnode.kidconfigs[i];
			if(kc != nil && kc.framename == f.name) {
				hnode.kidconfigs[i].title = f.doc.doctitle;
				return;
			}
		}
		if(dbg)
			sys->print("history update botch\n");
	}
}

# Find the gokind node (-1==Back, 0==Same, +1==Forward)
# other gokind values come from JavaScript's History.go(delta)
History.find(h: self ref History, gokind: int) : ref HistNode
{
	if(h.n > 0) {
		cur := h.h[h.n-1];
		case gokind {
		1 =>
			if(cur.succs != nil)
				return hd cur.succs;
		-1 =>
			if(cur.preds != nil)
				return hd cur.preds;
		0 =>
			return cur;
		* =>
# BUG: follows circularities: gives rise to different behaviour to other
# browsers but maintains the property of find(n) being equivalent to
# the user pressing the (forward/back) button n times

			h.findid++;
			while (gokind != 0 && cur != nil) {
				hn : list of ref HistNode;
				if (gokind > 0) {
					gokind--;
					hn = cur.succs;
				} else {
					gokind++;
					hn = cur.preds;
				}
				if (cur.findid == h.findid)
					hn = cur.findchain;
				else
					cur.findid = h.findid;
				if (hn != nil) {
					cur.findchain = tl hn;
					cur = hd hn;
				} else
					cur = nil;
			}
			return cur;
		}
	}
	return nil;
}

# for debugging
History.print(h: self ref History)
{
	sys->print("History\n");
	for(i := 0; i < h.n; i++) {
		hn := history.h[i];
		sys->print("Node %d:\n", i);
		dc := hn.topconfig;
		sys->print("\tframe=%s, target=%s, url=%s\n", dc.framename, dc.gospec.target, dc.gospec.url.tostring());
		if(hn.kidconfigs != nil) {
			for(j := 0; j < len hn.kidconfigs; j++) {
				dc = hn.kidconfigs[j];
				if(dc != nil)
					sys->print("\t\t%d: frame=%s, target=%s, url=%s\n",
							j, dc.framename, dc.gospec.target, dc.gospec.url.tostring());
			}
		}
		if(hn.preds != nil)
			printhnodeindices(h, "Preds", hn.preds);
		if(hn.succs != nil)
			printhnodeindices(h, "Succs", hn.succs);
	}
	sys->print("\n");
}

# helpers for JavaScript's History object
History.histinfo(h: self ref History) : (int, string, string, string)
{
	length := 0;
	current, next, previous : string;

	if(h.n > 0) {
		hn := h.h[h.n-1];
		length = len hn.succs + len hn.preds + 1;
		current = hn.topconfig.gospec.url.tostring();
		if(hn.succs != nil) {
			fwd := hd hn.succs;
			next = fwd.topconfig.gospec.url.tostring();
		}
		if(hn.preds != nil) {
			back := hd hn.preds;
			previous = back.topconfig.gospec.url.tostring();
		}
	}
	return (length, current, next, previous);
}

histinfo() : (int, string, string, string)
{
	return history.histinfo();
}

# does URL in hn contain s as a substring?
isurlsubstring(hn: ref HistNode, s: string) : int
{
	url := hn.topconfig.gospec.url.tostring();
	(l, r) := S->splitstrl(url, s);
	if(r != nil)
		return 1;
	return 0;
}

# for JavaScript's History.go(location)
# find nearest history entry whose URL contains s as a substring
# (search forward and backward from current "in parallel"?)
History.findurl(h: self ref History, s: string) : ref HistNode
{
	if(h.n > 0) {
		hn := h.h[h.n-1];
		if(isurlsubstring(hn, s))
			return hn;
		fwd := hn.succs;
		back := hn.preds;
		while(fwd != nil && back != nil) {
			if(fwd != nil) {
				if(isurlsubstring(hd fwd, s))
					return hd fwd;
				fwd = tl fwd;
			}
			if(back != nil) {
				if(isurlsubstring(hd back, s))
					return hd back;
				back = tl back;
			}
		}
	}
	return nil;
}

printhnodeindices(h: ref History, label: string, l: list of ref HistNode)
{
	sys->print("\t%s:", label);
	for( ; l != nil; l = tl l) {
		hn := hd l;
		for(i := 0; i < h.n; i++) {
			if(hn == h.h[i]) {
				sys->print(" %d", i);
				break;
			}
		}
		if(i == h.n)
			sys->print(" ?");
	}
	sys->print("\n");
}

dumphistory()
{
	fname := config.userdir + "/history.html";
	fd := sys->create(fname, sys->OWRITE, 8r600);
	if(fd == nil) {
		if(warn)
			sys->print("can't create history file\n");
		return;
	}
	line := "<HEAD><TITLE>History</TITLE>\n<META HTTP-EQUIV=\"content-type\" CONTENT=\"text/html; charset=utf8\">\n</HEAD>\n<BODY>\n";
	buf := array[Sys->ATOMICIO] of byte;
	aline := array of byte line;
	buf[0:] = aline;
	bufpos := len aline;
	for(i := history.n-1; i >= 0; i--) {
		hn := history.h[i];
		dc := hn.topconfig;
		line = "<A HREF=" + dc.gospec.url.tostring() + " TARGET=\"_top\">" + dc.title + "</A><BR>\n";
		if(hn.kidconfigs != nil) {
			line += "<UL>";
			for(j := 0; j < len hn.kidconfigs; j++) {
				dc = hn.kidconfigs[j];
				if(dc != nil) {
					line += "<LI><A HREF=" + dc.gospec.url.tostring() +
						" TARGET=\"" + dc.framename + "\">" +
						dc.title + "</A>\n";
				}
			}
			line += "</UL>";
		}
		aline = array of byte line;
		if(bufpos + len aline > Sys->ATOMICIO) {
			sys->write(fd, buf, bufpos);
			bufpos = 0;
		}
		buf[bufpos:] = aline;
		bufpos += len aline;
	}
	if(bufpos > 0)
		sys->write(fd, buf, bufpos);
}

# getauth returns the (realm, credentials), with "" for the credentials
# if we fail in getting authorization for some reason
getauth(chal: string) : (string, string)
{
	if(len chal < 12 || S->tolower(chal[0:12]) != "basic realm=") {
		if(dbg || warn)
			sys->print("unrecognized authorization challenge: %s\n", chal);
		return ("", "");
	}
	realm := chal[12:];
	if(realm[0] == '"')
		realm = realm[1:len realm - 1];
	for(al := auths; al != nil; al = tl al) {
		a := hd al;
		if(realm == a.realm)
			return (realm, a.credentials);
	}
	
	c := chan of (int, string);
	(code, uname, pword) := G->auth(realm);
	if(code != 1)
		return (nil, nil);
	cred := uname + ":" + pword;
	cred = tobase64(cred);
	return (realm, cred);
}

# Convert string to the base64 encoding
tobase64(a: string) : string
{
	n := len a;
	if(n == 0)
		return "";
	out := "";
	j := 0;
	i := 0;
	while(i < n) {
		x := a[i++] << 16;
		if(i < n)
			x |= (a[i++]&255) << 8;
		if(i < n)
			x |= (a[i++]&255);
		out[j++] = c64(x>>18);
		out[j++] = c64(x>>12);
		out[j++] = c64(x>> 6);
		out[j++] = c64(x);
	}
	nmod3 := n % 3;
	if(nmod3 != 0) {
		out[j-1] = '=';
		if(nmod3 == 1)
			out[j-2] = '=';
	}
	return out;
}

c64(c: int) : int
{
	v : con "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
	return v[c&63];
}

dosaveas(bsmain: ref ByteSource)
{
	(code, ans) := G->prompt("Save as", nil);
	if (code == -1)
		return;
	if(code == 1 && ans != "") {
		if(ans[0] != '/')
			ans = config.userdir + "/" + ans;
		fd := sys->create(ans, sys->OWRITE, 8r644);
		if(fd == nil) {
			G->alert(X("Couldn't create", "gui") + " " + ans);
			return;
		}
		G->setstatus(X("Saving", "gui") + " " + bsmain.hdr.actual.tostring());
		# TODO: should really use a different protocol that
		# doesn't require getting whole file before proceeding
		s := "";
		while(!bsmain.eof) {
			CU->waitreq(bsmain::nil);
			if(bsmain.err != "") {
				s = bsmain.err;
				break;
			}
		}
		if(s == "") {
			flen := bsmain.edata;
			for(i := 0; i < bsmain.edata; ) {
				n := sys->write(fd, bsmain.data[i:flen], flen-i);
				if(n <= 0)
					break;
				i += n;
			}
			if(i != flen)
				s = "whole file not written";
		}
		if(s == "")
			s = X("Created", "gui") + " " + ans;
		G->setstatus(X("Created", "gui") + " " + ans);
		# G->alert(s);
	}
	CU->freebs(bsmain);
}

fatalerror(msg: string)
{
	sys->print("Fatal error: %s\n", msg);
	finish();
}

pctoloc(mod: string, pc: int) : string
{
	ans := sys->sprint("pc=%d", pc);
	db := load Debug Debug->PATH;
	if(db == nil)
		return ans;
	Sym : import db;
	db->init();
	modname := mod;
	for(i := 0; i < len mod; i++)
		if(mod[i] == '[') {
			modname = mod[0:i];
			break;
		}
	sblname := "";
	case modname {
	"Build" =>
		sblname = "build.sbl";
	"CharonUtils" =>
		sblname = "chutils.sbl";
	"Gui" =>
		sblname = "gui.sbl";
	"Img" =>
		sblname = "img.sbl";
	"Layout" =>
		sblname = "layout.sbl";
	"Lex" =>
		sblname = "lex.sbl";
	"Test" =>
		sblname = "test.sbl";
	}
	if(sblname == "")
		return ans;
	(sym, nil) := db->sym(sblname);
	if(sym == nil)
		return ans;
	src := sym.pctosrc(pc);
	if(src == nil)
		return ans;
	return sys->sprint("%s:%d", src.start.file, src.start.line);
}

startcs()
{
	cs := load Command "/dis/ndb/cs.dis";
	if (cs == nil) {
		sys->print("failed to start cs\n");
		return;
	}
	spawn cs->init(nil, nil);
	sys->sleep(1000);
}

startcharon(url: string, c: chan of string)
{
	ctxt := ref Context;
	ctxt.ctxt = context;
	ctxt.args = "charon" :: url :: nil;
	ctxt.c = c;
	ctxt.cksrv = CU->CK;
	ctxt.ckclient = CU->ckclient;
	ch := load Charon "/dis/charon.dis";
	fdl := list of {0, 1, 2};
	if (CU->ckclient != nil)
		fdl = (CU->ckclient).fd.fd :: fdl;
	if(ch != nil){
		sys->pctl(Sys->NEWPGRP|Sys->NEWFD, fdl);
		ch->initc(ctxt);
	}
}

# Kill all processes spawned by us, and exit
finish()
{
	if (CU != nil) {
		CU->kill(pgrp, 1);
		if(gopgrp != 0)
			CU->kill(gopgrp, 1);
	}
	if(plumb != nil)
		plumb->shutdown();
	sendopener("E");
	exit;
}

include "plumbmsg.m";
	plumb: Plumbmsg;
	Msg: import plumb;

plumbwatch()
{
	plumb = load Plumbmsg Plumbmsg->PATH;
	if (plumb == nil)
		return;
	if (plumb->init(1, (CU->config).plumbport, 0) == -1) {
		# try to set up plumbing for sending only
		if (plumb->init(1, nil, 0) == -1)
			plumb = nil;
		return;
	}
	while ((m := Msg.recv()) != nil) {
		if (m.kind == "text") {
			u := CU->makeabsurl(string m.data);
			if (u != nil)
				E->evchan <-= ref Event.Ego(u.tostring(), "_top", 0, E->EGnormal);
		}
	}
}

plumbsend(s, dest: string): int
{
	if (plumb == nil)
		return -1;
	if (dest != nil)
		dest = "type="+dest;
	msg := ref Msg((CU->config).plumbport, nil, "", "text", dest, array of byte s);
	if (msg.send() < 0)
		return -1;
	return 0;
}

stop()
{
	stopped := X("Stopped", "gui");
	G->progress <-= (-1, G->Paborted, 0, stopped);
	G->setstatus(stopped);
	CU->abortgo(gopgrp);
}

gettop(): ref Layout->Frame
{
	return top;
}