shithub: purgatorio

ref: 76b565d8b219c8db94a882e93725b4ab735414de
dir: /appl/wm/brutus.b/

View raw version
implement Brutus;

include "sys.m";
	sys: Sys;

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

include "tk.m";
	tk: Tk;

include "tkclient.m";
	tkclient: Tkclient;

include "dialog.m";
	dialog: Dialog;

include "selectfile.m";
	selectfile: Selectfile;

include	"bufio.m";
	bufio: Bufio;
	Iobuf: import bufio;

include	"workdir.m";

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

include	"brutus.m";
include	"brutusext.m";

EXTDIR:	con "/dis/wm/brutus";
NEXTRA:	con NTAG-NFONTTAG;
DEFFONT:	con "/fonts/lucidasans/unicode.8.font";
DEFFONTNAME:	con "Roman";
DEFSIZE:	con 10;
DEFTAG:	con "Roman.10";
SETFONT:	con " -font "+DEFFONT+" ";
FOCUS:	con "focus .ft.t";
NOSEL:	con ".ft.t tag remove sel sel.first sel.last";
UPDATE:	con "update";

#
# Foreign keyboards and languages
#
Remaptab: adt
{
	in, out:	int;
};
include	"hebrew.m";

BS:		con 8;		# ^h backspace character
BSW:		con 23;		# ^w bacspace word
BSL:		con 21;		# ^u backspace line
ESC:		con 27;		# ^[ cut selection

Name:	con "Brutus";

# build menu
menu_cfg := array[] of {
	# menu
	"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 Look -command {send edit look}",
};

brutus_cfg := array[] of {
	# buttons
	"button .b.Tag -text Tag -command {send cmd tag} -state disabled",
	"menubutton .b.Font -text Roman -menu .b.Font.menu -underline -1 -state disabled",
	"menu .b.Font.menu",
	".b.Font.menu add command -label Roman -command {send cmd font Roman}",
	".b.Font.menu add command -label Italic -command {send cmd font Italic}",
	".b.Font.menu add command -label Bold -command {send cmd font Bold}",
	".b.Font.menu add command -label Type -command {send cmd font Type}",
	"checkbutton .b.Applyfont -variable Applyfont -command {send cmd applyfont}} -state disabled",
	"button .b.Applyfontnow -text Font -command {send cmd applyfontnow} -state disabled",
	"button .b.Applysizenow -text Size -command {send cmd applysizenow} -state disabled",
	"button .b.Applyfontsizenow -text F&S -command {send cmd applyfontsizenow} -state disabled",
	"menubutton .b.Size -text 10pt -menu .b.Size.menu -underline -1 -state disabled",
	"menu .b.Size.menu",
	".b.Size.menu add command -label 6pt -command {send cmd size 6}",
	".b.Size.menu add command -label 8pt -command {send cmd size 8}",
	".b.Size.menu add command -label 10pt -command {send cmd size 10}",
	".b.Size.menu add command -label 12pt -command {send cmd size 12}",
	".b.Size.menu add command -label 16pt -command {send cmd size 16}",
	"button .b.Put -text Put -command {send cmd put} -state disabled",

	# text
	"frame .ft",
	"scrollbar .ft.scroll -command {.ft.t yview}",
	"text .ft.t -height 7c -tabs {1c} -wrap word -yscrollcommand {.ft.scroll set}",
	FOCUS,

	# pack
	"pack .b.File .b.Ext .b.Tag .b.Applyfontnow .b.Applysizenow .b.Applyfontsizenow .b.Applyfont .b.Font .b.Size .b.Put -side left",
	"pack .b -anchor w",
	"pack .ft.scroll -side left -fill y",
	"pack .ft.t -fill both -expand 1",
	"pack .ft -fill both -expand 1",
	"pack propagate . 0",
};

control_cfg := array[] of {
	# text
	"frame .ft",
	"scrollbar .ft.scroll -command {.ft.t yview}",
	"text .ft.t -height 4c -wrap word -yscrollcommand {.ft.scroll set}",
	"pack .b.File",
	"pack .b -anchor w",
	"pack .ft.scroll -side left -fill y",
	"pack .ft.t -fill both -expand 1",
	"pack .ft -fill both -expand 1",
	"pack propagate . 0",
};

# bindings to build nice controls in text widget
input_cfg := array[] of {
	# input
	"bind .ft.t <Key> {send keys {%A}}",
	"bind .ft.t <Control-h> {send keys {%A}}",
	"bind .ft.t <Control-w> {send keys {%A}}",
	"bind .ft.t <Control-u> {send keys {%A}}",
	"bind .ft.t <Button-1> +{grab set .ft.t; send but1 pressed}",
	"bind .ft.t <Double-Button-1> +{grab set .ft.t; send but1 pressed}",
	"bind .ft.t <ButtonRelease-1> +{grab release .ft.t; send but1 released}",
	"bind .ft.t <Button-2> {send but2 %X %Y}",
	"bind .ft.t <Motion-Button-2-Button-1> {}",
	"bind .ft.t <Motion-Button-2> {}",
	"bind .ft.t <ButtonPress-3> {send but3 pressed}",
	"bind .ft.t <ButtonRelease-3> {send but3 released %x %y}",
	"bind .ft.t <Motion-Button-3> {}",
	"bind .ft.t <Motion-Button-3-Button-1> {}",
	"bind .ft.t <Double-Button-3> {}",
	"bind .ft.t <Double-ButtonRelease-3> {}",
	"bind .ft.t <FocusIn> +{send cmd focus}",
	UPDATE
};

fontbuts := array[] of {
	".b.Ext",
	".b.Tag",
	".b.Applyfontnow",
	".b.Applysizenow",
	".b.Applyfontsizenow",
	".b.Applyfont",
	".b.Font",
	".b.Size",
};

fontname = array[NFONT] of {
	"Roman",
	"Italic",
	"Bold",
	"Type",
};

sizename = array[NSIZE] of {
	"6",
	"8",
	"10",
	"12",
	"16",
};

tagname = array[NTAG] of {
	# first NFONT*NSIZE are font/size names
	"Roman.6",
	"Roman.8",
	"Roman.10",
	"Roman.12",
	"Roman.16",
	"Italic.6",
	"Italic.8",
	"Italic.10",
	"Italic.12",
	"Italic.16",
	"Bold.6",
	"Bold.8",
	"Bold.10",
	"Bold.12",
	"Bold.16",
	"Type.6",
	"Type.8",
	"Type.10",
	"Type.12",
	"Type.16",
	"Example",
	"Caption",
	"List",
	"List-elem",
	"Label",
	"Label-ref",
	"Exercise",
	"Heading",
	"No-fill",
	"Author",
	"Title",
	"Index",
	"Index-topic",
};

tagconfig = array[NTAG] of {
	"-font /fonts/lucidasans/unicode.6.font",
	"-font /fonts/lucidasans/unicode.7.font",
	"-font /fonts/lucidasans/unicode.8.font",
	"-font /fonts/lucidasans/unicode.10.font",
	"-font /fonts/lucidasans/unicode.13.font",
	"-font /fonts/lucidasans/italiclatin1.6.font",
	"-font /fonts/lucidasans/italiclatin1.7.font",
	"-font /fonts/lucidasans/italiclatin1.8.font",
	"-font /fonts/lucidasans/italiclatin1.10.font",
	"-font /fonts/lucidasans/italiclatin1.13.font",
	"-font /fonts/lucidasans/boldlatin1.6.font",
	"-font /fonts/lucidasans/boldlatin1.7.font",
	"-font /fonts/lucidasans/boldlatin1.8.font",
	"-font /fonts/lucidasans/boldlatin1.10.font",
	"-font /fonts/lucidasans/boldlatin1.13.font",
	"-font /fonts/lucidasans/typelatin1.6.font",
	"-font /fonts/lucidasans/typelatin1.7.font",
	"-font /fonts/pelm/latin1.9.font",
	"-font /fonts/pelm/ascii.12.font",
	"-font /fonts/pelm/ascii.16.font",
	"-foreground #444444 -lmargin1 1c -lmargin2 1c; .ft.t tag lower Example",
	"-foreground #444444; .ft.t tag lower Caption",
	"-foreground #444444 -lmargin1 1c -lmargin2 1c; .ft.t tag lower List",
	"-foreground #0000A0; .ft.t tag lower List-elem",
	"-foreground #444444; .ft.t tag lower Label",
	"-foreground #444444; .ft.t tag lower Label-ref",
	"-foreground #444444; .ft.t tag lower Exercise",
	"-foreground #444444; .ft.t tag lower Heading",
	"-foreground #444444; .ft.t tag lower No-fill",
	"-foreground #444444; .ft.t tag lower Author",
	"-foreground #444444; .ft.t tag lower Title",
	"-foreground #444444; .ft.t tag lower Index",
	"-foreground #444444; .ft.t tag lower Index-topic",
};

enabled := array[] of {"disabled", "normal"};

File: adt
{
	tk:			ref Tk->Toplevel;
	isctl:			int;
	applyfont:		int;
	fontsused:	int;
	name:		string;
	dirty:		int;
	font:			string;	# set by the buttons, not nec. by the text
	size:			int;		# set by the buttons, not nec. by the text
	fonttag:		string;	# set by the buttons, not nec. by the text
	configed:		array of int;
	button1:		int;
	button3:		int;
	fontsok:		int;		# fonts and tags can be set
	extensions:	list of ref Ext;
};

Ext: adt
{
	tkname:		string;
	modname:	string;
	mod:		Brutusext;
	args:			string;
};

menuindex := "0";
snarftext := "";
snarfsgml := "";
central: chan of (ref File, string);
files:	array of ref File;	# global but modified only by control thread
plumbed := 0;
curdir := "";
lang := "";

init(c: ref Context, argv: list of string)
{
	ctxt = c;
	sys = load Sys Sys->PATH;
	if (ctxt == nil) {
		sys->fprint(sys->fildes(2), "brutus: 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;
	selectfile = load Selectfile Selectfile->PATH;
	bufio = load Bufio Bufio->PATH;
	plumbmsg = load Plumbmsg Plumbmsg->PATH;

	if(plumbmsg->init(1, "edit", 1000) >= 0){
		plumbed = 1;
		workdir := load Workdir Workdir->PATH;
		curdir = workdir->init();
		workdir = nil;
	}

	tkclient->init();
	dialog->init();
	selectfile->init();
	sys->pctl(Sys->NEWPGRP, nil);	# so we can pass "exit" command to tkclient

	file := "";
	if(argv != nil)
		argv = tl argv;
	if(argv != nil)
		file = hd argv;
	central = chan of (ref File, string);
	spawn control(ctxt);
	<-central;
	spawn brutus(ctxt, file);
}

# build menu button for dynamically generated menu
buttoncfg(label, enable: string): string
{
	return "label .b."+label+" -text "+label + " " + enable +
		";bind .b."+label+" <Button-1> {send cmd "+label+"}" +
		";bind .b."+label+" <ButtonRelease-1> {}" +
		";bind .b."+label+" <Motion-Button-1> {}" +
		";bind .b."+label+" <Double-Button-1> {}" +
		";bind .b."+label+" <Double-ButtonRelease-1> {}" +
		";bind .b."+label+" <Enter> {.b."+label+" configure -background #EEEEEE}" +
		";bind .b."+label+" <Leave> {.b."+label+" configure -background #DDDDDD}";
}

tkchans(t: ref Tk->Toplevel): (chan of string, chan of string, chan of string, chan of string, chan of string, chan of string, chan of string)
{
	keys := chan of string;
	tk->namechan(t, keys, "keys");
	edit := chan of string;
	tk->namechan(t, edit, "edit");
	cmd := chan of string;
	tk->namechan(t, cmd, "cmd");
	but1 := chan of string;
	tk->namechan(t, but1, "but1");
	but2 := chan of string;
	tk->namechan(t, but2, "but2");
	but3 := chan of string;
	tk->namechan(t, but3, "but3");
	drag := chan of string;
	tk->namechan(t, drag, "Wm_drag");
	return (keys, edit, cmd, but1, but2, but3, drag);
}

control(ctxt: ref Context)
{
	(t, titlectl) := tkclient->toplevel(ctxt, SETFONT, Name, Tkclient->Appl);

	# f is not used to store anything, just to simplify interfaces
	# shared by control and brutus
	f := ref File (t, 1, 0, 0, "", 0, DEFFONTNAME, DEFSIZE, DEFTAG, nil, 0, 0, 0, nil);

	tkcmds(t, menu_cfg);
	tkcmd(t, "frame .b");
	tkcmd(t, buttoncfg("File", ""));
	tkcmds(t, control_cfg);
	tkcmds(t, input_cfg);
	files = array[1] of ref File;
	files[0] = f;

	(keys, edit, cmd, but1, but2, but3, drag) := tkchans(t);

	tkcmd(t, ".ft.t mark set typingstart 1.0; .ft.t mark gravity typingstart left");
	central <-= (nil, "");	# signal readiness
#	spawn tkclient->wmctl(t, "task");
	curfile: ref File;

	plumbc := chan of (string, string);
	spawn plumbproc(plumbc);

	tkclient->startinput(t, "kbd"::"ptr"::nil);
	tkclient->onscreen(t, nil);
	tkclient->wmctl(t, "task");
	for(;;) alt {
	s := <-t.ctxt.kbd =>
		tk->keyboard(t, s);
	s := <-t.ctxt.ptr =>
		tk->pointer(t, *s);

	menu := <-t.ctxt.ctl or
	menu = <-t.wreq or
	menu = <-titlectl =>
		if(menu == "exit"){
			if(shutdown(ctxt, t)){
				killplumb();
				tkclient->wmctl(t, menu);
			}
			break;
		}
		# spawn tkclient->wmctl(t, menu);
		tkclient->wmctl(t, menu);

	ecmd := <-edit =>
		editor(f, ecmd);
		tkcmd(t, FOCUS);

	c := <-cmd =>
		(nil, s) := sys->tokenize(c, " ");
		case hd s {
		* =>
			sys->print("unknown control cmd %s\n",c );
		"File" =>
			filemenu(t, 0, 0);
		"new" =>
			(name, ok, nil) := getfilename(ctxt, t, "file for new window", f.name, 1, 0, 0);
			if(ok)
				spawn brutus(ctxt, name);
		"select" =>
			n := int hd tl s;
			if(n > len files)
				break;
			if(n > 0)
				curfile = files[n];
			tkcmd(files[n].tk, ". map; raise .; focus .ft.t");
		"focus" =>
			;
		}

	(file, action) := <-central =>
		(nil, s) := sys->tokenize(action, " ");
		case hd s {
		* =>
			sys->print("control unknown central command %s\n", action);
		"new" =>
			curfile = file;
			nfiles := array[len files+1] of ref File;
			nfiles[0:] = files;
			files = nfiles;
			nfiles = nil;	# make sure references don't linger
			files[len files-1] = file;
		"name" =>
			name := nameof(file);
			index := 0;
			for(i:=1; i<len files; i++)
				if(files[i] == file){
					index = i;
					break;
				}
			if(index == 0)
				sys->print("can't find file\n");
		"focus" =>
			if(file != f)
				curfile = file;
		"select" =>
			n := int hd tl s;
			if(n >= len files)
				break;
			if(n > 0)
				curfile = files[n];
			tkcmd(files[n].tk, ". map; raise .; focus .ft.t; update");
		"exiting" =>
			if(file == nil)
				break;
			if(file == curfile)
				curfile = nil;
			index := 0;
			for(i:=1; i<len files; i++)
				if(files[i] == file){
					index = i;
					break;
				}
			if(index == 0)
				sys->print("can't find file\n");
			else{
				# make a new one rather than slice, to clean up references
				nfiles := array[len files-1] of ref File;
				for(i=0; i<index; i++)
					nfiles[i] = files[i];
				for(; i<len nfiles; i++)
					nfiles[i] = files[i+1];
				files = nfiles;
			}
			file = nil;
		}
	c := <-keys =>
		char := typing(f, c);
		if(curfile!=nil && char=='\n' && insat(t, "end"))
			execute(t, curfile, tkcmd(t, ".ft.t get insert-1line insert"));

	c := <-but1 =>
		mousebut1(f, c);

	c := <-but2 =>
		mousebut2(f, c);

	c := <-but3 =>
		mousebut3(f, c);

	c := <-drag =>
		if(len c < 6 || c[0:5] != "path=")
			break;
		spawn brutus(ctxt, c[5:]);

	(fname, addr) := <-plumbc =>
		for(i:=1; i<len files; i++)
			if(files[i].name == fname){
				tkcmd(files[i].tk, ". map; raise .; focus .ft.t");
				showaddr(files[i], addr);
				break;
			}
		if(i == len files){
			if(addr != "")
				spawn brutus(ctxt, fname+":"+addr);
			else
				spawn brutus(ctxt, fname);
		}
	}
}

brutus(ctxt: ref Context, filename: string)
{
	addr := "";
	for(i:=len filename; --i>0; ){
		if(filename[i] == ':'){
			(ok, dir) := sys->stat(filename[0:i]);
			if(ok >= 0){
				addr = filename[i+1:];
				filename = filename[0:i];
				break;
			}
		}
	}

	(t, titlectl)  := tkclient->toplevel(ctxt, SETFONT, Name, Tkclient->Appl);

	f := ref File (t, 0, 0, 0, filename, 0, DEFFONTNAME, DEFSIZE, DEFTAG, nil, 0, 0, 0, nil);
	f.configed = array[NTAG] of {* => 0};

	tkcmds(t, menu_cfg);
	tkcmd(t, "frame .b");
	tkcmd(t, buttoncfg("File", ""));
	tkcmd(t, buttoncfg("Ext", "-state disabled"));

	tkcmds(t, brutus_cfg);
	tkcmds(t, input_cfg);

	# buttons work better when they grab the mouse
	a := array[] of {".b.Tag", ".b.Applyfontnow", ".b.Applysizenow", ".b.Applyfontsizenow"};
	for(i=0; i<len a; i++){
		tkcmd(t, "bind "+a[i]+" <Button-1> +{grab set "+a[i]+"}");
		tkcmd(t, "bind "+a[i]+" <ButtonRelease-1> +{grab release "+a[i]+"}");
	}

	(keys, edit, cmd, but1, but2, but3, drag) := tkchans(t);

	configfont(f, "Heading");
	configfont(f, "Title");
	configfont(f, f.fonttag);
	tkcmd(t, ".ft.t mark set typingstart 1.0; .ft.t mark gravity typingstart left");
	tkcmd(t, "image create bitmap waiting -file cursor.wait");

	central <-= (f, "new");
	setfilename(f, filename);

	if(filename != "")
		if(loadfile(f, filename) < 0)
			dialog->prompt(ctxt, t.image, "error -fg red",
				"Open file",
				sys->sprint("Can't read %s:\n%r", filename),
				0, "Continue" :: nil);
		else
			showaddr(f, addr);

	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);

	menu := <-t.ctxt.ctl or
	menu = <-t.wreq or
	menu = <-titlectl =>
		case menu {
		"exit" =>
			if(f.dirty){
				action := confirm(ctxt, t, nameof(f)+" is dirty", 1);
				case action {
				"cancel" =>
					continue;
				"exitclean" =>
					if(dumpfile(f, f.name, f.fontsused) < 0)
						continue;
					break;
				"exitdirty" =>
					break;
				}
			}
			central <-= (f, "exiting");
			# this one tears down temporaries holding references to f
			central <-= (nil, "exiting");
			return;
		"task" =>
			tkcmd(t, ". unmap");
		* =>
			tkclient->wmctl(t, menu);
		}

	ecmd := <-edit =>
		editor(f, ecmd);
		tkcmd(t, FOCUS);

	command := <-cmd =>
		(nil, c) := sys->tokenize(command, " ");
		case hd c {
		* =>
			sys->print("unknown command %s\n", command);
		"File" =>
			filemenu(t, 1, f.fontsok);
		"Ext" =>
			extmenu(t);
		"new" =>
			(name, ok, nil) := getfilename(ctxt, t, "file for new window", f.name, 1, 0, 0);
			if(ok)
				spawn brutus(ctxt, name);
		"open" =>
			if(f.dirty){
				action := confirm(ctxt, t, nameof(f)+" is dirty", 1);
				case action {
				"cancel" =>
					continue;
				"exitclean" =>
					if(dumpfile(f, f.name, f.fontsused) < 0)
						continue;
					break;
				"exitdirty" =>
					break;
				}
			}
			(name, ok, nil) := getfilename(ctxt, t, "file for this window", f.name, 1, 0, 0);
			if(ok && name!=""){
				setfilename(f, name);
				if(loadfile(f, name) < 0){
					tkcmd(t, ".ft.t delete 1.0 end");
					dialog->prompt(ctxt, t.image, "error -fg red",
						"Open file",
						sys->sprint("Can't open %s:\n%r", name),
						0, "Continue"::nil);
				}
			}
		"name" =>
			(name, ok, nil) := getfilename(ctxt, t, "remembered file name", f.name, 1, 0, 0);
			if(ok){
				if(name != f.name){
					setfilename(f, name);
					dirty(f, 1);
				}
			}
		"write" =>
			(name, ok, sgml) := getfilename(ctxt, t, "file to write", f.name, 1, 1, f.fontsused);
			if(ok && name!=""){
				if(f.name == ""){
					setfilename(f, name);
					dirty(f, 1);
				}
				dumpfile(f, name, sgml);
			}
		"fonts" =>
			if(f.fontsok==0 && f.fontsused==0){
				action := confirm(ctxt, t, "Converting "+nameof(f)+" to SGML", 0);
				case action {
				"cancel" =>
					continue;
				"exitdirty" =>
					usingfonts(f);
					dirty(f, 1);
				}
			}
			enablefonts(f, !f.fontsok);
		"language" =>
			if(lang == "")
				lang = "Hebrew";
			else
				lang = "";
		"addext" =>
			ext := hd tl c;
			(args, ok, nil) := getfilename(ctxt, t, "parameters for "+ext, "", 0, 0, 0);
			if(ok){
				tkcmd(t, "cursor -image waiting; update");
				addextension(f, ext+" "+args, nil);
				usingfonts(f);
				dirty(f, 1);
				tkcmd(t, "cursor -default; update");
			}
		"select" =>
			central <-= (f, command);
		"tag" =>
			tageditor(ctxt, f);
			tkcmd(t, FOCUS);
		"font" =>
			f.font = hd tl c;
			tkcmd(t, ".b.Font configure -text "+f.font+";"+UPDATE);
			f.fonttag = f.font+"."+string f.size;
			configfont(f, f.fonttag);
			if(changefont(f, f.font))
				dirty(f, 1);
		"size" =>
			sz := hd tl c;
			tkcmd(t, ".b.Size configure -text "+sz+"pt; update");
			f.size = int sz;
			f.fonttag = f.font+"."+string f.size;
			configfont(f, f.fonttag);
			if(changesize(f, string f.size))
				dirty(f, 1);
		"applyfont" =>
			f.applyfont = int tkcmd(t, "variable Applyfont");
			if(f.applyfont)
				configfont(f, f.fonttag);
		"applyfontnow" =>
			if(changefont(f, f.font))
				dirty(f, 1);
		"applysizenow" =>
			if(changesize(f, string f.size))
				dirty(f, 1);
		"applyfontsizenow" =>
			if(changefontsize(f, f.fonttag))
				dirty(f, 1);
		"put" =>
			dumpfile(f, f.name, f.fontsused);
		"focus" =>
			central <-= (f, "focus");
		}

	c := <-keys =>
		typing(f, c);

	c := <-but1 =>
		mousebut1(f, c);

	c := <-but2 =>
		mousebut2(f, c);

	c := <-but3 =>
		mousebut3(f, c);

	c := <-drag =>
		if(len c < 6 || c[0:5] != "path=")
			break;
		spawn brutus(ctxt, c[5:]);
	}
}

kbdremap(c: int) : (int, int)
{
	tab: array of Remaptab;

	dir := 1;
	case lang{
	"" =>
		return (c, dir);
	"Hebrew" =>
		tab = hebrewtab;
		dir = -1;
	* =>
		sys->print("unknown language %s\n", lang);
		return (c, dir);
	}
	for(i:=0; i<len tab; i++)
		if(c == tab[i].in)
			return (tab[i].out, dir);
	return (c, 1);
}

typing(f: ref File, c: string): int
{
	t := f.tk;
	char := c[1];
	if(char == '\\')
		char = c[2];
	update := ";.ft.t see insert;"+UPDATE;
	if(char != ESC)
		cut(f, 1);
	case char {
	* =>
		dir := 1;
		if(c[1] != '\\')	# safe character; remap it
			(c[1], dir) = kbdremap(char);
		s := ".ft.t insert insert "+c;
		if(dir < 0)
			s += ";.ft.t mark set insert insert-1c";
		if(f.applyfont){
			usingfonts(f);
			s += f.fonttag;
		}
		tkcmd(t, s+update);
		if(f.fontsused && f.applyfont==0){
			# nasty goo to make sure we don't insert text without a font tag;
			# must ask after the fact if default rules set a tag.
			names := tkcmd(t, ".ft.t tag names insert-1chars");
			if(!somefont(names))
				tkcmd(t, ".ft.t tag add "+DEFTAG+" insert-1chars");
		}
		dirty(f, 1);
	ESC =>
		if(nullsel(t))
			tkcmd(t, ".ft.t tag add sel typingstart insert;"+
					".ft.t mark set typingstart insert");
		else
			cut(f, 1);
		tkcmd(t, UPDATE);
	BS =>
		bs(f, "c");
	BSL =>
		bs(f, "l");
	BSW =>
		bs(f, "w");
	}
	return char;
}

bs(f: ref File, c: string)
{
	if(!insat(f.tk, "1.0")){
		tkcmd(f.tk, ".ft.t tkTextDelIns -"+c+";.ft.t see insert;"+UPDATE);
		dirty(f, 1);
	}
}

mousebut1(f: ref File, c: string)
{
	f.button1 = (c == "pressed");
	f.button3 = 0;	# abort any pending button 3 action
	tkcmd(f.tk, ".ft.t mark set typingstart insert");
}

mousebut2(f: ref File, c: string)
{
	if(f.button1){
		cut(f, 1);
		tk->cmd(f.tk, UPDATE);
	}else{
		(nil, l) := sys->tokenize(c, " ");
		x := int hd l - 50;
		y := int hd tl l - int tk->cmd(f.tk, ".m yposition "+menuindex) - 10;
#		tkcmd(f.tk, "focus .ft.t");
		tkcmd(f.tk, ".m activate "+menuindex+"; .m post "+string x+" "+string y+
			"; update");
	}
}

mousebut3(f: ref File, c: string)
{
	t := f.tk;
	if(c == "pressed"){
		f.button3 = 1;
		if(f.button1){
			paste(f);
			tk->cmd(t, "update");
		}
		return;
	}
	if(!plumbed || f.button3==0 || f.button1!=0)
		return;
	f.button3 = 0;
	# Plumb message triggered by release of button 3
	(nil, l) := sys->tokenize(c, " ");
	x := int hd tl l;
	y := int hd tl tl l;
	index := tk->cmd(t, ".ft.t index @"+string x+","+string y);
	selindex := tk->cmd(t, ".ft.t tag ranges sel");
	if(selindex != "")
		insel := tk->cmd(t, ".ft.t compare sel.first <= "+index)=="1" &&
			tk->cmd(t, ".ft.t compare sel.last >= "+index)=="1";
	else
		insel = 0;
	attr := "";
	if(insel)
		text := tk->cmd(t, ".ft.t get sel.first sel.last");
	else{
		# have line with text in it
		# now extract whitespace-bounded string around click
		(nil, w) := sys->tokenize(index, ".");
		charno := int hd tl w;
		left := tk->cmd(t, ".ft.t index {"+index+" linestart}");
		right := tk->cmd(t, ".ft.t index {"+index+" lineend}");
		line := tk->cmd(t, ".ft.t get "+left+" "+right);
		for(i:=charno; i>0; --i)
			if(line[i-1]==' ' || line[i-1]=='\t')
				break;
		for(j:=charno; j<len line; j++)
			if(line[j]==' ' || line[j]=='\t')
				break;
		text = line[i:j];
		attr = "click="+string (charno-i);
	}
	msg := ref Msg(
		"Brutus",
		"",
		directory(f),
		"text",
		attr,
		array of byte text);
	if(msg.send() < 0)
		sys->fprint(sys->fildes(2), "brutus: plumbing write error: %r\n");
}

directory(f: ref File): string
{
	for(i:=len f.name; --i>=0;)
		if(f.name[i] == '/'){
			if(i == 0)
				i++;
			return f.name[0:i];
		}
	return curdir;
}

enablefonts(f: ref File, enable: int)
{
	for(i:=0; i<len fontbuts; i++)
		tkcmd(f.tk, fontbuts[i] + " configure -state "+enabled[enable]);
	tkcmd(f.tk, "update");
	f.fontsok = enable;
}

filemenu(t: ref tk->Toplevel, buttons, fontsok: int)
{
	tkcmd(t, "menu .b.Filemenu");
	tkcmd(t, ".b.Filemenu add command -label New -command {send cmd new}");
	if(buttons){
		tkcmd(t, ".b.Filemenu add command -label Open -command {send cmd open}");
		tkcmd(t, ".b.Filemenu add command -label Name -command {send cmd name}");
		tkcmd(t, ".b.Filemenu add command -label Write -command {send cmd write}");
		if(fontsok)
			pre := "Dis";
		else
			pre = "En";
		tkcmd(t, ".b.Filemenu add command -label {"
			+pre+"able Fonts} -command {send cmd fonts}");
		if(lang == "")
			pre = "En";
		else
			pre = "Dis";
		tkcmd(t, ".b.Filemenu add command -label {"
			+pre+"able Hebrew} -command {send cmd language}");
	}
	tkcmd(t, ".b.Filemenu add command -label {["+Name+"]} -command {send cmd select 0}");
	if(files != nil)
		for(i:=1; i<len files; i++){
			name := nameof(files[i]);
			if(files[i].dirty)
				name = "{' "+name+"}";
			else
				name = "{  "+name+"}";
			tkcmd(t, ".b.Filemenu add command -label "+name+
				" -command {send cmd select "+string i+"}");
		}
	tkcmd(t, "bind .b.Filemenu <Unmap> {destroy .b.Filemenu}");
	x := tk->cmd(t, ".ft.scroll cget actx");
	y := tk->cmd(t, ".ft.scroll cget acty");
	tkcmd(t, ".b.Filemenu post "+x+" "+y+"; grab set .b.Filemenu; update");
}

extmenu(t: ref tk->Toplevel)
{
	fd := sys->open(EXTDIR, Sys->OREAD);
	if(fd == nil || ((n,dir):=sys->dirread(fd)).t0<=0){
		sys->print("%s: can't find extension directory %s: %r\n", Name, EXTDIR);
		return;
	}

	tkcmd(t, "menu .b.Extmenu");
	for(i:=0; i<n; i++){
		name := dir[i].name;
		if(len name>4 && name[len name-4:]==".dis"){
			name = name[0:len name-4];
			tkcmd(t, ".b.Extmenu add command -label {Add "+name+
				"} -command {send cmd addext "+name+"}");
		}
	}

	tkcmd(t, "bind .b.Extmenu <Unmap> {destroy .b.Extmenu}");
	x := tk->cmd(t, ".ft.scroll cget actx");
	y := tk->cmd(t, ".ft.scroll cget acty");
	tkcmd(t, ".b.Extmenu post "+x+" "+y+"; grab set .b.Extmenu; update");
}

basepath(file: string): (string, string)
{
	for(i := len file-1; i >= 0; i--) {
		if(file[i] == '/')
			return (file[0:i], file[i+1:]);
	}
	return (".", file);
}

putbut(f: ref File)
{
	state := enabled[f.dirty];
	if(f.name != "")
		tkcmd(f.tk, ".b.Put configure -state "+state+"; update");
}

dirty(f: ref File, nowdirty: int)
{
	if(f.isctl)
		return;
	old := f.dirty;
	f.dirty = nowdirty;
	if(old != nowdirty){
		setfilename(f, f.name);
		putbut(f);
	}
}

setfilename(f: ref File, name: string)
{
	oldname := f.name;
	f.name = name;
	if(oldname=="" && name!="")
		putbut(f);
	name = Name + ": \"" +nameof(f)+ "\"";
	if(f.dirty)
		name += " (dirty)";
	tkclient->settitle(f.tk, name);
	tkcmd(f.tk, UPDATE);
	central <-= (f, "name");
}

configfont(f: ref File, tag: string)
{
	for(i:=0; i<NTAG; i++)
		if(tag == tagname[i]){
			if(f.configed[i] == 0){
				tkcmd(f.tk, ".ft.t tag configure "+tag+" "+tagconfig[i]);
				f.configed[i] = 1;
			}
			return;
		}
	sys->print("Brutus: can't configure font %s\n", tag);
}

insat(t: ref Tk->Toplevel, mark: string): int
{
	return tkcmd(t, ".ft.t compare insert == "+mark) == "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(f: ref File, ecmd: string)
{

	case ecmd {
	"cut" =>
		menuindex = "0";
		cut(f, 1);
	
	"paste" =>
		menuindex = "1";
		paste(f);

	"snarf" =>
		menuindex = "2";
		if(nullsel(f.tk))
			return;
		snarf(f);

	"look" =>
		menuindex = "3";
		look(f);
	}
	tkcmd(f.tk, UPDATE);
}

nullsel(t: ref Tk->Toplevel): int
{
	return tkcmd(t, ".ft.t tag ranges sel") == "";
}

cut(f: ref File, snarfit: int)
{
	if(nullsel(f.tk))
		return;
	dirty(f, 1);
	if(snarfit)
		snarf(f);
	# sometimes when clicking fast, selection and insert point can
	# separate.  the only time this really matters is when typing into
	# a double-clicked selection.  it's easy to fix here.
	tkcmd(f.tk, ".ft.t mark set insert sel.first;.ft.t delete sel.first sel.last");
}

snarf(f: ref File)
{
	# convert sel.first and sel.last to numeric forms because sgml()
	# must clear selection to avoid <sel> tags in result.
	(nil, sel) := sys->tokenize(tkcmd(f.tk, ".ft.t tag ranges sel"), " ");
	snarftext = tkcmd(f.tk, ".ft.t get "+hd sel+" "+hd tl sel);
	snarfsgml = sgml(f.tk, "-sgml", hd sel, hd tl sel);
	tkclient->snarfput(snarftext);
}

paste(f: ref File)
{
#	good question
	snarftext = tkclient->snarfget();
	if(snarftext == "" && (f.fontsused == 0 || snarfsgml == nil))
		return;
	cut(f, 0);
	dirty(f, 1);

	t := f.tk;
	start := tkcmd(t, ".ft.t index insert");
	if(f.fontsused == 0)
		tkcmd(t, ".ft.t insert insert '"+snarftext);
	else if(f.applyfont)
		tkcmd(t, ".ft.t insert insert "+tk->quote(snarftext)+" "+f.fonttag);
	else
		insert(f, snarfsgml);
	tkcmd(t, ".ft.t tag add sel "+start+" insert");
}

look(f: ref File)
{
	t := f.tk;
	(sel0, sel1) := word(t);
	if(sel0 == nil)
		return;
	text := tkcmd(t, ".ft.t get "+sel0+" "+sel1);
	if(text == nil)
		return;
	tkcmd(t, "cursor -image waiting; update");
	search(nil, f, text, 0, 0);
	tkcmd(t, "cursor -default; update");
}

# First time fonts are used explicitly, establish font tags for all extant text.
usingfonts(f: ref File)
{
	if(f.fontsused)
		return;
	tkcmd(f.tk, ".ft.t tag add "+DEFTAG+" 1.0 end");
	f.fontsused = 1;
}

word(t: ref Tk->Toplevel): (string, string)
{
	start := "sel.first";
	end := "sel.last";
	if(nullsel(t)){
		insert := tkcmd(t, ".ft.t index insert");
		start = tkcmd(t, ".ft.t index {insert wordstart}");
		if(insert == start){	# tk's definition of 'wordstart' is bogus
			# if at beginning, tk->cmd will return !error and a0 will be false.
			a0 := isalnum(tk->cmd(t, ".ft.t get insert-1chars"));
			a1 := isalnum(tk->cmd(t, ".ft.t get insert"));
			if(a0==0 && a1==0)
				return (nil, nil);
			if(a1 == 0)
				start = tkcmd(t, ".ft.t index {insert-1chars wordstart}");
		}
		end = tkcmd(t, ".ft.t index {"+start+" wordend}");
		if(start == end)
			return (nil, nil);
	}
	return (start, end);
}

# Change the font associated with the selection
changefont(f: ref File, font: string): int
{
	t := f.tk;
	(sel0, sel1) := word(f.tk);
	mod := 0;
	if(sel0 == nil)
		return mod;
	usingfonts(f);
	for(i:=0; i<NFONT; i++){
		if(fontname[i] == font)
			continue;
		for(j:=0; j<NSIZE; j++){
			tag := fontname[i]+"."+sizename[j];
			start := sel0;
			for(;;){
				range := tkcmd(t, ".ft.t tag nextrange "+tag+" "+start+" "+sel1);
				if(len range > 0 && range[0] == '!')
					break;
				(nil, tt) := sys->tokenize(range, " ");
				if(tt == nil)
					break;
				tkcmd(t, ".ft.t tag remove "+tag+" "+hd tt+" "+hd tl tt);
				fs := font+"."+sizename[j];
				tkcmd(t, ".ft.t tag add "+fs+" "+hd tt+" "+hd tl tt);
				configfont(f, fs);
				start = hd tl tt;
				mod = 1;
			}
		}
	}
	tkcmd(t, UPDATE);
	return mod;
}

# See if tag list includes a font name
somefont(tag: string): int
{
	(nil, tt) := sys->tokenize(tag, " ");
	for(; tt!=nil; tt=tl tt)
		for(i:=0; i<NFONT*NSIZE; i++){
			if(tagname[i] == hd tt)
				return 1;
		}
	return 0;
}

# Change the size associated with the selection
changesize(f: ref File, size: string): int
{
	t := f.tk;
	(sel0, sel1) := word(f.tk);
	mod := 0;
	if(sel0 == nil)
		return mod;
	usingfonts(f);
	for(i:=0; i<NFONT; i++){
		for(j:=0; j<NSIZE; j++){
			if(sizename[j] == size)
				continue;
			tag := fontname[i]+"."+sizename[j];
			start := sel0;
			for(;;){
				range := tkcmd(t, ".ft.t tag nextrange "+tag+" "+start+" "+sel1);
				if(len range > 0 && range[0] == '!')
					break;
				(nil, tt) := sys->tokenize(range, " ");
				if(tt == nil)
					break;
				tkcmd(t, ".ft.t tag remove "+tag+" "+hd tt+" "+hd tl tt);
				fs := fontname[i]+"."+size;
				tkcmd(t, ".ft.t tag add "+fs+" "+hd tt+" "+hd tl tt);
				configfont(f, fs);
				start = hd tl tt;
				mod = 1;
			}
		}
	}
	tkcmd(t, UPDATE);
	return mod;
}

# Change the font and size associated with the selection
changefontsize(f: ref File, newfontsize: string): int
{
	t := f.tk;
	(sel0, sel1) := word(f.tk);
	if(sel0 == nil)
		return 0;
	usingfonts(f);
	(nil, names) := sys->tokenize(tkcmd(t, ".ft.t tag names"), " ");
	# clear old tags
	tags := tagname[0:NFONT*NSIZE];
	for(l:=names; l!=nil; l=tl l)
		for(i:=0; i<len tags; i++)
			if(tags[i] == hd l)
				tkcmd(t, ".ft.t tag remove "+hd l+" "+sel0+" "+sel1);
	tkcmd(t, ".ft.t tag add "+newfontsize+" "+sel0+" "+sel1+"; update");
	return 1;
}

listtostring(l: list of string): string
{
	s := "{";
	while(l != nil){
		if(len s == 1)
			s += hd l;
		else
			s += " " + hd l;
		l = tl l;
	}
	s += "}";
	return s;
}

# splitl based on indices rather than slices.  this version returns char
# position of the matching character.
splitl(str: string, i, j: int, pat: string): int
{
	while(i < j){
		c := str[i];
		for(k:=len pat-1; k>=0; k--)
			if(c == pat[k])
				return i;
		i++;
	}
	return i;
}

# splitstrl based on indices rather than slices. this version returns char
# position of the beginning of the matching string.
splitstrl(str: string, i, j: int, pat: string): int
{
	l := len pat;
	if(l == 0)	# shouldn't happen, but be safe
		return j;
	first := pat[0];
	while(i <= j-l){
		# check first char for speed
		if(str[i] == first){
			for(k:=1; k<l && str[i+k]==pat[k]; k++)
				;
			if(k == l)
				return i;
		}
		i++;
	}
	return j;
}

# place the text, as annotated by SGML tags, into document
# where indicated by insert mark
insert(f: ref File, sgml: string)
{
	taglist: list of string;

	t := f.tk;
	usingfonts(f);
	if(f.applyfont)
		taglist = f.fonttag :: taglist;
	tag := listtostring(taglist);
	end := len sgml;
	j: int;
	for(i:=0; i<end; i=j){
		j = splitl(sgml, i, end, "<&");
		tt := tag;
		if(tt=="" || tt=="{}")
			tt = DEFTAG;	# can happen e.g. when pasting plain text
		if(j > i)
			tkcmd(t, ".ft.t insert insert "+tk->quote(sgml[i:j])+" "+tt);
		if(j < end)
			case sgml[j] {
			'&' =>
				if(j+4<=end && sgml[j:j+4]=="&lt;"){
					tkcmd(t, ".ft.t insert insert "+"{<} "+tt);
					j += 4;
				}else{
					tkcmd(t, ".ft.t insert insert {&} "+tt);
					j += 1;
				}
			'<' =>
				(nc, newtag, on) := tagstring(sgml, j, end);
				if(nc < 0){
					tkcmd(t, ".ft.t insert insert "+"{<} "+tt);
					j += 1;
				}else if(len newtag>9 && newtag[0:10]=="Extension "){
					addextension(f, newtag[10:], taglist);
					j += nc;
				}else if(len newtag>9 && newtag[0:7]=="Window "){
					repostextension(f, newtag[7:], taglist);
					j += nc;
				}else{
					if(on){
						taglist = newtag :: taglist;
						configfont(f, newtag);
					}else{
						taglist = drop(taglist, newtag);
						if(f.applyfont && hasfonts(taglist)==0)
							taglist = f.fonttag :: taglist;
					}
					j += nc;
					tag = listtostring(taglist);
				}
			}
	}
}

drop(l: list of string, s: string): list of string
{
	n: list of string;
	while(l != nil){
		if(s != hd l)
			n = hd l :: n;
		l = tl l;
	}
	return n;
}

extid := 0;
addextension(f: ref File, s: string, taglist: list of string)
{
	for(i:=0; i<len s; i++)
		if(s[i] == ' ')
			break;
	if(i == 0 || i == len s){
		sys->print("Brutus: badly formed extension %s\n", s);
		return;
	}
	modname := s[0:i];
	s = s[i+1:];

	mod: Brutusext;
	for(el:=f.extensions; el!=nil; el=tl el)
		if(modname == (hd el).modname){
			mod = (hd el).mod;
			break;
		}

	if(mod == nil){
		file := modname;
		if(i < 4 || file[i-4:i] != ".dis")
			file += ".dis";
		if(file[0] != '/')
			file = "/dis/wm/brutus/" + file;
		mod = load Brutusext file;
		if(mod == nil){
			sys->print("%s: can't load module %s: %r\n", Name, file);
			return;
		}
	}
	mkextension(f, mod, modname, s, taglist);
}

repostextension(f: ref File, tkname: string, taglist: list of string)
{
	mod: Brutusext;
	for(el:=f.extensions; el!=nil; el=tl el)
		if(tkname == (hd el).tkname){
			mod = (hd el).mod;
			break;
		}
	if(mod == nil){
		sys->print("Brutus: can't find extension widget %s: %r\n", tkname);
		return;
	}

	mkextension(f, mod, (hd el).modname, (hd el).args, taglist);
}

mkextension(f: ref File, mod: Brutusext, modname, args: string, taglist: list of string)
{
	t := f.tk;

	name := ".ext"+string extid++;
	mod->init(sys, draw, bufio, tk, tkclient);
	err := mod->create(f.name, t, name, args);
	if(err != ""){
		sys->print("%s: can't create extension widget %s: %s\n", Name, modname, err);
		return;
	}
	tkcmd(t, ".ft.t window create insert -window "+name);
	while(taglist != nil){
		tkcmd(t, ".ft.t tag add "+hd taglist+" "+name);
		taglist = tl taglist;
	}
	f.extensions = ref Ext(name, modname, mod, args) :: f.extensions;
}

# rewrite <window .ext1> tags into <Extension module args>
extrewrite(f: ref File, sgml: string): string
{
	if(f.extensions == nil)
		return sgml;

	new := "";

	end := len sgml;
	j: int;
	for(i:=0; i<end; i=j){
		j = splitstrl(sgml, i, end, "<Window ");
		if(j > i)
			new += sgml[i:j];
		if(j < end){
			j += 8;
			for(k:=j; sgml[k]!='>' && k<end; k++)
				;
			tkname := sgml[j:k];
			for(el:=f.extensions; el!=nil; el=tl el)
				if((hd el).tkname == tkname)
					break;
			if(el == nil)
				sys->print("%s: unrecognized extension %s\n", Name, tkname);
			else{
				e := hd el;
				new += "<Extension "+e.modname+" "+e.args+">";
			}
			j = k+1;	# skip '>'
		}
	}
	return new;
}

hasfonts(l: list of string): int
{
	for(i:=0; i<NFONT*NSIZE; i++)
		for(ll:=l; ll!=nil; ll=tl ll)
			if(hd ll == tagname[i])
				return 1;
	return 0;
}

# s[i] is known to be a less-than sign
tagstring(s: string, i, end: int): (int, string, int)
{
	tag: string;

	j := splitl(s, i+1, end, ">");
	if(j==end || s[j]!='>')
		return (-1, "", 0);
	nc := (j-i)+1;
	on := 1;
	if(s[i+1] == '/'){
		on = 0;
		i++;
	}
	tag = s[i+1:j];
# NEED TO CHECK VALIDITY OF TAG
	return (nc, tag, on);
}

sgml(t: ref Tk->Toplevel, flag, start, end: string): string
{
	# turn off selection, to avoid getting that in output
	sel := tkcmd(t, ".ft.t tag ranges sel");
	if(sel != "")
		tkcmd(t, ".ft.t tag remove sel "+sel);
	s := tkcmd(t, ".ft.t dump "+flag+" "+start+" "+end);
	if(sel != "")
		tkcmd(t, ".ft.t tag add sel "+sel);
	return s;
}

loadfile(f: ref File, file: string): int
{
	f.size = DEFSIZE;
	f.font = DEFFONTNAME;
	f.fonttag = DEFTAG;
	f.fontsused = 0;
	enablefonts(f, 0);
	t := f.tk;
	tkcmd(t, ".b.Font configure -text "+f.font);
	tkcmd(t, ".b.Size configure -text "+string f.size+"pt");
	tkcmd(t, "cursor -image waiting; update");
	r := loadfile1(f, file);
	tkcmd(t, "cursor -default");
	return r;
}

loadfile1(f: ref File, file: string): int
{
	fd := bufio->open(file, Sys->OREAD);
	if(fd == nil)
		return -1;
	(ok, dir) := sys->fstat(fd.fd);
	if(ok < 0){
		fd.close();
		return -1;
	}
	l := int dir.length;
	a := array[l] of byte;
	n := fd.read(a, len a);
	fd.close();
	if(n != len a)
		return -1;
	t := f.tk;
	tkcmd(t, ".ft.t delete 1.0 end");
	if(len a>=7 && string a[0:7]=="<SGML>\n")
		insert(f, string a[7:n]);
	else
		tkcmd(t, ".ft.t insert 1.0 '"+string a[0:n]);
	dirty(f, 0);
	tkcmd(t, ".ft.t mark set insert 1.0; update");
	return 1;
}

dumpfile(f: ref File, file: string, sgml: int): int
{
	tkcmd(f.tk, "cursor -image waiting");
	r := dumpfile1(f, file, sgml);
	tkcmd(f.tk, "cursor -default");
	return r;
}

dumpfile1(f: ref File, file: string, sgml: int): int
{
	if(writefile(f, file, sgml) < 0){
		dialog->prompt(ctxt, f.tk.image, "error -fg red",
			"Write file",
			sys->sprint("Can't write %s:\n%r", file),
			0, "Continue"::nil);
		tkcmd(f.tk, FOCUS);
		return -1;
	}
	return 1;
}

writefile(f: ref File, file: string, sgmlfmt: int): int
{
	if(file == "")
		return -1;
	fd := bufio->create(file, Sys->OWRITE, 8r666);
	if(fd == nil)
		return -1;

	t := f.tk;
	flag := "";
	if(sgmlfmt){
		flag = "-sgml";
		prefix := "<SGML>\n";
		if(f.fontsused == 0)
			prefix += "<"+DEFTAG+">";
		x := array of byte prefix;
		if(fd.write(x, len x) != len x){
			fd.close();
			return -1;
		}
	}
	sgmltext := sgml(t, flag, "1.0", "end");
	if(sgmlfmt)
		sgmltext = extrewrite(f, sgmltext);
	a := array of byte sgmltext;
	if(fd.write(a, len a) != len a){
		fd.close();
		return -1;
	}
	if(sgmlfmt && f.fontsused==0){
		suffix := array of byte ("</"+DEFTAG+">");
		if(fd.write(suffix, len suffix) != len suffix){
			fd.close();
			return -1;
		}
	}
	if(fd.flush() < 0){
		fd.close();
		return -1;
	}
	fd.close();
	if(file == f.name){
		dirty(f, sgmlfmt!=f.fontsused);
		tkcmd(t, UPDATE);
	}
	return 1;
}

shutdown(s: ref Draw->Context, t: ref Tk->Toplevel): int
{
	for(i:=1; i<len files; i++){
		f := files[i];
		if(f.dirty){
			action := confirm(s, t, "file "+nameof(f)+" is dirty", 1);
			case action {
			"cancel" =>
				return 0;
			"exitclean" =>
				if(dumpfile(f, f.name, f.fontsused) < 0)
					return 0;
			"exitdirty" =>
				break;
			}
		}
	}
	return 1;
}

nameof(f: ref File): string
{
	s := f.name;
	if(s == "")
		s = "(unnamed)";
	return s;
}

tkcmd(t: ref Tk->Toplevel, s: string): string
{
	res := tk->cmd(t, s);
	if(len res > 0 && res[0] == '!')
		sys->print("%s: tk error executing '%s': %s\n", Name, s, res);
	return res;
}

confirm_cfg := array[] of {
	"frame .f -borderwidth 2 -relief groove -padx 3 -pady 3",
	"frame .f.f",
#	"label .f.f.l -bitmap error -foreground red",
	"label .f.f.l -text Warning:",
	"label .f.f.m",
	"button .f.exitclean -text {  Write and Proceed  } -width 17w -command {send cmd exitclean}",
	"button .f.exitdirty -text {  Proceed  } -width 17w -command {send cmd exitdirty}",
	"button .f.cancel -text {  Cancel  } -width 17w -command {send cmd cancel}",
	"pack .f.f.l .f.f.m -side left",
	"pack .f.f .f.exitclean .f.exitdirty .f.cancel -padx 10 -pady 10",
	"pack .f",
};

widget(parent: ref Tk->Toplevel, ctxt: ref Draw->Context, cfg: array of string): ref Tk->Toplevel
{
	x := int tk->cmd(parent, ". cget -x");
	y := int tk->cmd(parent, ". cget -y");
	where := sys->sprint("-x %d -y %d ", x+45, y+25);
	(t,nil) := tkclient->toplevel(ctxt, where+SETFONT+" -borderwidth 2 -relief raised", "", tkclient->Plain);
	tkcmds(t, cfg);
	return t;
}

tkcmds(top: ref Tk->Toplevel, a: array of string)
{
	for(i := 0; i < len a; i++)
		v := tkcmd(top, a[i]);
}

confirm(ctxt: ref Draw->Context, parent: ref Tk->Toplevel, message: string, write: int): string
{
	s := confirm1(ctxt, parent, message, write);
	tkcmd(parent, FOCUS);
	return s;
}

confirm1(ctxt: ref Draw->Context, parent: ref Tk->Toplevel, message: string, write: int): string
{
	t := widget(parent, ctxt, confirm_cfg);
	tkcmd(t, ".f.f.m configure -text '"+message);
	if(write == 0)
		tkcmd(t, "destroy .f.exitclean");
	tkcmd(t, UPDATE);
	tkclient->onscreen(t, "onscreen");
	cmd := chan of string;
	tk->namechan(t, cmd, "cmd");
	tkclient->onscreen(t, "exact");
	tkclient->startinput(t, "ptr"::nil);
	for(;;) alt {
		s := <-t.ctxt.ptr =>
			tk->pointer(t, *s);
		c := <-cmd =>
			return c;
	}
	return <-cmd;
}

getfilename_cfg := array[] of {
	"frame .f",
	"label .f.Message",
	"entry .f.Name -width 25w",
	"checkbutton .f.SGML -text { Write SGML } -variable SGML",
	"button .f.Ok -text {  OK  } -width 14w -command {send cmd ok}",
	"button .f.Browse -text {  Browse  } -width 14w -command {send cmd browse}",
	"button .f.Cancel -text {  Cancel  } -width 14w -command {send cmd cancel}",
	"bind .f.Name <Control-j> {send cmd ok}",
	"pack .f.Message .f.Name .f.SGML .f.Ok .f.Browse .f.Cancel -padx 10 -pady 10",
	"pack .f",
	"focus .f.Name",
};

getfilename(ctxt: ref Draw->Context, parent: ref Tk->Toplevel, message, name: string, browse, sgml, nowsgml: int): (string, int, int)
{
	(s, i, issgml) := getfilename1(ctxt, parent, message, name, browse, sgml, nowsgml);
	tkcmd(parent, FOCUS);
	return (s, i, issgml);
}

getfilename1(ctxt: ref Draw->Context, parent: ref Tk->Toplevel, message, name: string, browse, sgml, nowsgml: int): (string, int, int)
{
	t := widget(parent, ctxt, getfilename_cfg);
	tkcmds(t, getfilename_cfg);

	tkcmd(t, ".f.Message configure -text '"+message);
	tk->cmd(t, ".f.Name insert 0 "+name);
	if(browse == 0)
		tkcmd(t, "destroy .f.Browse");
	if(sgml == 0)
		tkcmd(t, "destroy .f.SGML");
	else if(nowsgml)
		tkcmd(t, ".f.SGML select");
	tkcmd(t, UPDATE);
	cmd := chan of string;
	tk->namechan(t, cmd, "cmd");
	tkclient->onscreen(t, "exact");
	tkclient->startinput(t, "kbd"::"ptr"::nil);
	for(;;) alt {
		s := <-t.ctxt.kbd =>
			tk->keyboard(t, s);
		s := <-t.ctxt.ptr =>
			tk->pointer(t, *s);
		c := <-cmd =>
			case c {
			"ok" =>
				return (tkcmd(t, ".f.Name get"), 1, int tkcmd(t, "variable SGML"));
			"cancel" =>
				return ("", 0, 0);
			"browse" =>
				name = tkcmd(t, ".f.Name get");
				(dir, path) := basepath(name);
	
				pat := list of {
					"* (All files)",
					"*.sgml (SGML dump files)",
					"*.html (Web source files)",
					"*.tex (Latex source files)",
					"*.[bm] (Limbo source files)"
				};
	
				path = selectfile->filename(ctxt, parent.image, message, pat, dir);
				if(path != "")
					name = path;
				tk->cmd(t, ".f.Name delete 0 end; .f.Name insert 0 "+name+";focus .f.Name; update");
				if(path != "")
					return (name, 1, int tkcmd(t, "variable SGML"));
		}
	}
}

tageditor(ctxt: ref Draw->Context, f: ref File)
{
	(start, end) := word(f.tk);
	if(start == nil)
		return;
	cfg := array[100] of string;
	i := 0;
	cfg[i++] = "frame .f";
	(nil, names) := sys->tokenize(tkcmd(f.tk, ".ft.t tag names "+start), " ");
	pack := "pack";
	set := array[NEXTRA] of int;
	for(j:=0; j<NEXTRA; j++){
		n := tagname[j+NFONT*NSIZE];
		cfg[i++] = "checkbutton .f.c"+string j+" -variable c"+string j+
			" -text {"+n+"} -command {send cmd "+string j+"} -anchor w";
		pack += " .f.c"+string j;
		set[j] = 0;
		for(l:=names; l!=nil; l=tl l)
			if(hd l == n){
				cfg[i++] = ".f.c"+string j+" select";
				set[j] = 1;
			}
	}
	cfg[i++] = "button .f.Ok -text {  OK  } -width 6w -command {send cmd ok}";
	cfg[i++] = "button .f.Cancel -text {  Cancel  } -width 6w -command {send cmd cancel}";
	cfg[i++] = pack + " -padx 3 -pady 0 -fill x";
	cfg[i++] = "pack .f.Ok .f.Cancel -padx 2 -pady 2 -side left";
	cfg[i++] = "pack .f; grab set .f; update";
	t := widget(f.tk, ctxt, cfg[0:i]);
	cmd := chan of string;
	tk->namechan(t, cmd, "cmd");
	tkclient->onscreen(t, "exact");
	tkclient->startinput(t, "kbd"::"ptr"::nil);

    loop:
	for(;;){
		alt{
		s := <-t.ctxt.kbd =>
			tk->keyboard(t, s);
		s := <-t.ctxt.ptr =>
			tk->pointer(t, *s);
		c := <-cmd =>
			case c {
			"ok" =>
				break loop;
			"cancel" =>
				return;
			* =>
				j = int c;
				set[j] = (tkcmd(t, "variable c"+c) == "1");
			}
		}
	}
	for(j=0; j<NEXTRA; j++){
		s := tagname[j+NFONT*NSIZE];
		if(set[j]){
			configfont(f, s);
			tkcmd(f.tk, ".ft.t tag add "+s+" "+start+" "+end);
		}else
			tkcmd(f.tk, ".ft.t tag remove "+s+" "+start+" "+end);
	}
	dirty(f, 1);
	usingfonts(f);
	tkcmd(f.tk, UPDATE);
}

plumbpid: int;
plumbproc(plumbc: chan of (string, string))
{
	plumbpid = sys->pctl(0, nil);

	for(;;){
		msg := Msg.recv();
		if(msg == nil){
			sys->print("Brutus: can't read /chan/plumb.edit: %r\n");
			plumbpid = 0;
			return;
		}
		if(msg.kind != "text"){
			sys->print("Brutus: can't interpret '%s' kind of message\n", msg.kind);
			continue;
		}
		text := string msg.data;
		n := len text;
		addr := "";
		for(j:=0; j<n; j++)
			if(text[j] == ':'){
				addr = text[j+1:];
				break;
			}
		file := text[0:j];
		if(len file>0 && file[0]!='/' && len msg.dir>0){
			if(msg.dir[len msg.dir-1] == '/')
				file = msg.dir+file;
			else
				file = msg.dir+"/"+file;
		}
		plumbc <-= (file, addr);
	}
}

killplumb()
{
	if(plumbed == 0)
		return;
	plumbmsg->shutdown();
	if(plumbpid <= 0)
		return;
	fname := sys->sprint("#p/%d/ctl", plumbpid);
	fd := sys->open(fname, sys->OWRITE);
	if(fd != nil)
		sys->write(fd, array of byte "kill\n", 8);
}

lastpat: string;

execute(cmdwin: ref Tk->Toplevel, f: ref File, cmd: string)
{
	if(len cmd>1 && cmd[len cmd-1]=='\n')
		cmd = cmd[0:len cmd-1];
	if(cmd == "")
		return;
	if(cmd[0] == '/' || cmd[0]=='?'){
		search(cmdwin, f, cmd[1:], cmd[0]=='?', 1);
		return;
	}
	for(i:=0; i<len cmd; i++)
		if(cmd[i]<'0' || '9'<cmd[i]){
			sys->print("bad command %s\n", cmd);
			return;
		}
	t := f.tk;
	line := int cmd;
	if(!nullsel(t))
		tkcmd(t, NOSEL);
	tkcmd(t, ".ft.t tag add sel "+string line+".0 {"+string line+".0 lineend+1char}");
	tkcmd(t, ".ft.t mark set insert "+string line+".0; .ft.t see insert;update");
}

search(cmdwin: ref Tk->Toplevel, f: ref File, pat: string, backwards, uselast: int)
{
	t := f.tk;
	if(pat == nil)
		pat = lastpat;
	else if(uselast)
		lastpat = pat;
	if(pat == nil){
		error(cmdwin, "no pattern");
		return;
	}
	cmd := ".ft.t search ";
	if(backwards)
		cmd += "-backwards ";
	p := "";
	for(i:=0; i<len pat; i++){
		if(pat[i]== '\\' || pat[i]=='{')
			p[len p] = '\\';
		p[len p] = pat[i];
	}
	cmd += "{"+p+"} ";
	null := nullsel(t);
	if(null)
		cmd += "insert";
	else if(backwards)
		cmd += "sel.first";
	else
		cmd += "sel.last";
	s := tk->cmd(t, cmd);
	if(s == "")
		error(cmdwin, "not found");
	else{
		if(!null)
			tkcmd(t, NOSEL);
		tkcmd(t, ".ft.t tag add sel "+s+" "+s+"+"+string len pat+"chars");
		tkcmd(t, ".ft.t mark set insert "+s+";.ft.t see insert; update");
	}
}

showaddr(f: ref File, addr: string)
{
	 if(addr=="")
		return;
	t := f.tk;
	if(addr[0]=='#' || ('0'<=addr[0] && addr[0]<='9')){
		# UGLY! just do line and character numbers until we get a
		# decent command/address interface set up.
		if(!nullsel(t))
			tkcmd(t, NOSEL);
		if(addr[0] == '#'){
			addr = addr[1:];
			tkcmd(t, ".ft.t mark set insert {1.0+"+addr+"char}; .ft.t see insert;update");
		}else{
			tkcmd(t, ".ft.t tag add sel "+addr+".0 {"+addr+".0 lineend+1char}");
			tkcmd(t, ".ft.t mark set insert "+addr+".0; .ft.t see insert;update");
		}
	}
}

error(cmdwin: ref Tk->Toplevel, err: string)
{
	if(cmdwin == nil)
		return;
	tkcmd(cmdwin, ".ft.t insert end '?"+err+"\n");
	if(!nullsel(cmdwin))
		tkcmd(cmdwin, NOSEL);
	tkcmd(cmdwin, ".ft.t mark set insert end");
	tkcmd(cmdwin, ".ft.t mark set typingstart end; update");
}