xref: /inferno-os/appl/grid/lib/srvbrowse.b (revision e45fa0eb0763b57d6fb0649c064bc3b95ccdea6c)
1implement Srvbrowse;
2
3#
4# Copyright © 2003 Vita Nuova Holdings Limited.  All rights reserved.
5#
6
7
8include "sys.m";
9	sys : Sys;
10include "draw.m";
11	draw: Draw;
12	Rect: import draw;
13include "tk.m";
14	tk: Tk;
15include "tkclient.m";
16	tkclient: Tkclient;
17include "grid/srvbrowse.m";
18include "registries.m";
19	registries: Registries;
20	Registry, Attributes, Service: import registries;
21
22init()
23{
24	sys = load Sys Sys->PATH;
25	if (sys == nil)
26		badmod(Sys->PATH);
27	draw = load Draw Draw->PATH;
28	if (draw == nil)
29		badmod(Draw->PATH);
30	tk = load Tk Tk->PATH;
31	if (tk == nil)
32		badmod(Tk->PATH);
33	tkclient = load Tkclient Tkclient->PATH;
34	if (tkclient == nil)
35		badmod(Tkclient->PATH);
36	tkclient->init();
37	registries = load Registries Registries->PATH;
38	if (registries == nil)
39		badmod(Registries->PATH);
40	registries->init();
41	reg = Registry.new("/mnt/registry");
42	if (reg == nil) {
43		reg = Registry.connect(nil, nil, nil);
44		if (reg == nil)
45			error("Could not find registry");
46	}
47	qids = array[511] of { * => "" };
48}
49
50reg : ref Registry;
51qids : array of string;
52
53# Qid stuff is a bit rubbish at the mo but waiting for registries to change:
54#	currently address is unique but will not be in the future so waiting
55#	for another id to uniquely identify a resource
56
57addqid(srvc: ref Service): int
58{
59	addr := srvc.addr;
60	qid := addr2qid(addr);
61	for (;;) {
62		if (qids[qid] == nil)
63			break;
64		else if (qids[qid] == addr)
65			return qid;
66		qid++;
67		if (qid >= len qids)
68			qid = 0;
69	}
70	qids[qid] = addr;
71#	sys->print("adding %s (%s) to %d\n",srvc.attrs.get("resource"), addr, qid);
72	return qid;
73}
74
75getqid(srvc: ref Service): string
76{
77	addr := srvc.addr;
78	qid := addr2qid(addr);
79	startqid := qid;
80	for (;;) {
81		if (qids[qid] == addr)
82			return string qid;
83		qid++;
84		if (qid == startqid)
85			break;
86		if (qid >= len qids)
87			qid = 0;
88	}
89	return nil;
90}
91
92addr2qid(addr: string): int
93{
94	qid := 0;
95	# assume addr starts 'tcp!...'
96	for (i := 4; i < len addr; i++) {
97		qid += addr[i] * 2**(i%10);
98		qid = qid % len qids;
99	}
100	return qid;
101}
102
103addservice(srvc: ref Service)
104{
105	services = srvc :: services;
106	addqid(srvc);
107}
108
109find(filter: list of list of (string, string)): list of ref Service
110{
111	lsrv : list of ref Service = nil;
112	if (filter == nil)
113		(lsrv, nil) = reg.services();
114	else {
115		for (; filter != nil; filter = tl filter) {
116			attr := hd filter;
117			(s, nil) := reg.find(attr);
118			for (; s != nil; s = tl s)
119				lsrv = hd s :: lsrv;
120		}
121	}
122	return sortservices(lsrv);
123}
124
125refreshservices(filter: list of list of (string, string))
126{
127	services = find(filter);
128}
129
130servicepath2Service(path, qid: string): list of ref Service
131{
132	srvl : list of ref Service = nil;
133	(nil, lst) := sys->tokenize(path, "/");
134	pname: string;
135	l := len lst;
136	if (l < 2 || l > 3)
137		return nil;
138	presource := hd tl lst;
139	if (l == 3)
140		pname = hd tl tl lst;
141
142	for (tmpl := services; tmpl != nil; tmpl = tl tmpl) {
143		srvc := hd tmpl;
144		(resource, name) := getresname(srvc);
145		if (l == 2) {
146			if (resource == presource)
147				srvl = srvc :: srvl;
148		}
149		else if (l == 3) {
150			if (resource == presource) {
151				if (name == pname && qid == getqid(srvc)) {
152					srvl = srvc :: srvl;
153					break;
154				}
155			}
156		}
157	}
158	return srvl;
159}
160
161servicepath2Dir(path: string, qid: int): (array of ref sys->Dir, int)
162{
163	# sys->print("srvcPath2Dir: '%s' %d\n",path, qid);
164	res : list of (string, string) = nil;
165	(nil, lst) := sys->tokenize(path, "/");
166	presource, pname: string;
167	pattrib := 0;
168	l := len lst;
169	if (l > 1)
170		presource = hd tl lst;
171	if (l > 2)
172		pname = hd tl tl lst;
173	if (l == 4 && hd tl tl tl lst == "attributes")
174			pattrib = 1;
175	for (tmpl := services; tmpl != nil; tmpl = tl tmpl) {
176		srvc := hd tmpl;
177		(resource, name) := getresname(srvc);
178		if (l == 1) {
179			if (!isin(res, resource))
180				res = (resource, nil) :: res;
181		}
182		else if (l == 2) {
183			if (resource == presource)
184				res = (name, string getqid(srvc)) :: res;
185		}
186		else if (l == 3) {
187			if (resource == presource && name == pname) {
188				if (qid == int getqid(srvc)) {
189					if (srvc.addr[0] == '@')
190						res = (srvc.addr[1:], string getqid(srvc)) :: res;
191					else {
192						if (srvc.attrs != nil)
193							res = ("attributes", string getqid(srvc)) :: res;
194						res = ("address:\0"+srvc.addr+"}", string getqid(srvc)) :: res;
195					}
196					break;
197				}
198			}
199		}
200		else if (l == 4) {
201			if (resource == presource && name == pname && pattrib) {
202				if (qid == int getqid(srvc)) {
203					for (tmpl2 := srvc.attrs.attrs; tmpl2 != nil; tmpl2 = tl tmpl2) {
204						(attrib, val) := hd tmpl2;
205						if (attrib != "name" && attrib != "resource")
206							res = (attrib+":\0"+val, string getqid(srvc)) :: res;
207					}
208					break;
209				}
210			}
211		}
212	}
213	resa := array [len res] of ref sys->Dir;
214	i := len resa - 1;
215	for (; res != nil; res = tl res) {
216		dir : sys->Dir;
217		qid: string;
218		(dir.name, qid) = hd res;
219		if (l < 3 || dir.name == "attributes")
220			dir.mode = 8r777 | sys->DMDIR;
221		else
222			dir.mode = 8r777;
223		if (qid != nil)
224			dir.qid.path = big qid;
225		resa[i--] = ref dir;
226	}
227	dups := 0;
228	if (l >= 2)
229		dups = 1;
230	return (resa, dups);
231}
232
233isin(l: list of (string, string), s: string): int
234{
235	for (; l != nil; l = tl l)
236		if ((hd l).t0 == s)
237			return 1;
238	return 0;
239}
240
241getresname(srvc: ref Service): (string, string)
242{
243	resource := srvc.attrs.get("resource");
244	if (resource == nil)
245		resource = "Other";
246	name := srvc.attrs.get("name");
247	if (name == nil)
248		name = "?????";
249	return (resource,name);
250}
251
252badmod(path: string)
253{
254	sys->print("Srvbrowse: failed to load: %s\n",path);
255	exit;
256}
257
258sortservices(lsrv: list of ref Service): list of ref Service
259{
260	a := array[len lsrv] of ref Service;
261	i := 0;
262	for (; lsrv != nil; lsrv = tl lsrv) {
263		addqid(hd lsrv);
264		a[i++] = hd lsrv;
265	}
266	heapsort(a);
267	lsrvsorted: list of ref Service = nil;
268	for (i = len a - 1; i >= 0; i--)
269		lsrvsorted = a[i] :: lsrvsorted;
270	return lsrvsorted;
271}
272
273
274heapsort(a: array of ref Service)
275{
276	for (i := (len a / 2) - 1; i >= 0; i--)
277		movedownheap(a, i, len a - 1);
278
279	for (i = len a - 1; i > 0; i--) {
280		tmp := a[0];
281		a[0] = a[i];
282		a[i] = tmp;
283		movedownheap(a, 0, i - 1);
284	}
285}
286
287movedownheap(a: array of ref Service, root, end: int)
288{
289	max: int;
290	while (2*root <= end) {
291		r2 := root * 2;
292		if (2*root == end || comp(a[r2], a[r2+1]) == GT)
293			max = r2;
294		else
295			max = r2 + 1;
296
297		if (comp(a[root], a[max]) == LT) {
298			tmp := a[root];
299			a[root] = a[max];
300			a[max] = tmp;
301			root = max;
302		}
303		else
304			break;
305	}
306}
307
308LT: con -1;
309EQ: con 0;
310GT: con 1;
311
312comp(a1, a2: ref Service): int
313{
314	(resource1, name1) := getresname(a1);
315	(resource2, name2) := getresname(a2);
316	if (resource1 < resource2)
317		return LT;
318	if (resource1 > resource2)
319		return GT;
320	if (name1 < name2)
321		return LT;
322	if (name1 > name2)
323		return GT;
324	return EQ;
325}
326
327error(e: string)
328{
329	sys->fprint(sys->fildes(2), "Srvbrowse: %s\n", e);
330	raise "fail:error";
331}
332
333searchscr := array[] of {
334	"frame .f",
335	"scrollbar .f.sy -command {.f.c yview}",
336	"scrollbar .f.sx -command {.f.c xview} -orient horizontal",
337	"canvas .f.c -yscrollcommand {.f.sy set} -xscrollcommand {.f.sx set} -bg white -width 414 -borderwidth 2 -relief sunken -height 180 -xscrollincrement 10 -yscrollincrement 19",
338	"grid .f.sy -row 0 -column 0 -sticky ns -rowspan 2",
339	"grid .f.sx -row 1 -column 1 -sticky ew",
340	"grid .f.c -row 0 -column 1",
341	"pack .f -fill both -expand 1 ; pack propagate . 0; update",
342};
343
344SEARCH, RESULTS: con iota;
345
346searchwin(ctxt: ref Draw->Context, chanout: chan of string, filter: list of list of (string, string))
347{
348	(top, titlebar) := tkclient->toplevel(ctxt,"","Search", tkclient->Appl);
349	butchan := chan of string;
350	tk->namechan(top, butchan, "butchan");
351	tkcmds(top, searchscr);
352	makesearchframe(top);
353	flid := setframe(top, ".fsearch", nil);
354	selected := "";
355	lresults : list of ref Service = nil;
356	resultstart := 0;
357	resize(top, 368,220);
358	maxresults := getmaxresults(top);
359	currmode := SEARCH;
360	tkclient->onscreen(top, nil);
361	tkclient->startinput(top, "kbd"::"ptr"::nil);
362
363	main: for (;;) {
364		alt {
365		s := <-top.ctxt.kbd =>
366			tk->keyboard(top, s);
367		s := <-top.ctxt.ptr =>
368			tk->pointer(top, *s);
369		inp := <-butchan =>
370			(nil, lst) := sys->tokenize(inp, " ");
371			case hd lst {
372				"key" =>
373					s := " ";
374					id := hd tl lst;
375					nv := hd tl tl lst;
376					tkp : string;
377					if (id != "-1")
378						tkp = ".fsearch.ea"+nv+id;
379					else
380						tkp = ".fsearch.e"+nv;
381					char := int hd tl tl tl lst;
382					s[0] = char;
383					if (char == '\n' || char == '\t') {
384						newtkp := ".fsearch";
385						if (nv == "n")
386							newtkp += ".eav"+id;
387						else if (nv == "v") {
388							newid := string ((int id)+1);
389							e := tk->cmd(top, ".fsearch.ean"+newid+" cget -width");
390							if (e == "" || e[0] == '!') {
391								insertattribrow(top);
392								newtkp += ".ean"+newid;
393							}
394							else
395								newtkp += ".ean"+newid;
396						}
397						focus(top, newtkp);
398					}
399					else {
400						tkcmd(top, tkp+" insert insert {"+s+"}");
401						tkcmd(top, tkp+" see "+tkcmd(top, tkp+" index insert"));
402					}
403				"go" =>
404					lresults = search(top, filter);
405					resultstart = 0;
406					makeresultsframe(top, lresults, 0, maxresults);
407					selected = nil;
408					flid = setframe(top, ".fresults", flid);
409					currmode = RESULTS;
410					if (chanout != nil)
411						chanout <-= "search search";
412				"prev" =>
413					selected = nil;
414					resultstart -= maxresults;
415					if (resultstart < 0)
416						resultstart = 0;
417					makeresultsframe(top, lresults, resultstart, maxresults);
418					flid = setframe(top, ".fresults", flid);
419				"next" =>
420					selected = nil;
421					if (resultstart < 0)
422						resultstart = 0;
423					resultstart += maxresults;
424					if (resultstart >= len lresults)
425						resultstart -= maxresults;
426					makeresultsframe(top, lresults, resultstart, maxresults);
427					flid = setframe(top, ".fresults", flid);
428				"backto" =>
429					flid = setframe(top, ".fsearch", flid);
430					tkcmd(top, ".f.c see 0 "+tkcmd(top, ".fsearch cget -height"));
431					currmode = SEARCH;
432				"new" =>
433					resetsearchscr(top);
434					tkcmd(top, ".f.c see 0 0");
435					setscrollr(top, ".fsearch");
436				"select" =>
437					if (selected != nil)
438						tkcmd(top, selected+" configure -bg white");
439					if (selected == hd tl lst)
440						selected = nil;
441					else {
442						selected = hd tl lst;
443						tkcmd(top, hd tl lst+" configure -bg #5555FF");
444						if (chanout != nil)
445							chanout <-= "search select " +
446										tkcmd(top, selected+" cget -text") +													" " + hd tl tl lst;
447					}
448			}
449			tkcmd(top, "update");
450		title := <-top.ctxt.ctl or
451		title = <-top.wreq or
452		title = <-titlebar =>
453			if (title == "exit" || title == "ok")
454				break main;
455			e := tkclient->wmctl(top, title);
456			if (e == nil && title[0] == '!') {
457				(nil, lst) := sys->tokenize(title, " \t\n");
458				if (len lst >= 2 && hd lst == "!size" && hd tl lst == ".") {
459					resize(top, -1,-1);
460					maxresults = getmaxresults(top);
461					if (currmode == RESULTS) {
462						makeresultsframe(top, lresults, resultstart, maxresults);
463						flid = setframe(top, ".fresults", flid);
464						tkcmd(top, "update");
465					}
466				}
467			}
468		}
469	}
470
471}
472
473getmaxresults(top: ref Tk->Toplevel): int
474{
475	val := ((int tkcmd(top, ".f.c cget -height")) - 65)/17;
476	if (val < 1)
477		return 1;
478	return val;
479}
480
481setframe(top: ref Tk->Toplevel, f, oldflid: string): string
482{
483	if (oldflid != nil)
484		tkcmd(top, ".f.c delete " + oldflid);
485	newflid := tkcmd(top, ".f.c create window 0 0 -window "+f+" -anchor nw");
486	setscrollr(top, f);
487	return newflid;
488}
489
490setscrollr(top: ref Tk->Toplevel, f: string)
491{
492	h := tkcmd(top, f+" cget -height");
493	w := tkcmd(top, f+" cget -width");
494	tkcmd(top, ".f.c configure -scrollregion {0 0 "+w+" "+h+"}");
495}
496
497resize(top: ref Tk->Toplevel, width, height: int)
498{
499	if (width == -1) {
500		width = int tkcmd(top, ". cget -width");
501		height = int tkcmd(top, ". cget -height");
502	}
503	else
504		tkcmd(top, sys->sprint(". configure -width %d -height %d", width, height));
505	htitle := int tkcmd(top, ".f cget -acty") - int tkcmd(top, ". cget -acty");
506	height -= htitle;
507	ws := int tkcmd(top, ".f.sy cget -width");
508	hs := int tkcmd(top, ".f.sx cget -height");
509
510	tkcmd(top, ".f.c configure -width "+string (width - ws - 8)+
511			" -height "+string (height - hs - 8));
512
513	tkcmd(top, "update");
514}
515
516makesearchframe(top: ref Tk->Toplevel)
517{
518	font := " -font /fonts/charon/plain.normal.font";
519	fontb := " -font /fonts/charon/bold.normal.font";
520	f := ".fsearch";
521
522	tkcmd(top, "frame "+f+" -bg white");
523	tkcmd(top, "label "+f+".l -text {Search for Resource Attributes} -bg white" + fontb);
524	tkcmd(top, "grid "+f+".l -row 0 -column 0 -columnspan 3 -sticky nw");
525
526	tkcmd(top, "grid rowconfigure "+f+" 0 -minsize 30");
527	tkcmd(top, "frame "+f+".fgo -bg white");
528	tkcmd(top, "button "+f+".bs -text {Search} -command {send butchan go} "+font);
529	tkcmd(top, "button "+f+".bc -text {Clear} -command {send butchan new} "+font);
530	tkcmd(top, "grid "+f+".bs -row 3 -column 0 -sticky e -padx 2 -pady 5");
531	tkcmd(top, "grid "+f+".bc -row 3 -column 1 -sticky w -pady 5");
532
533	tkcmd(top, "label "+f+".la1 -text {name} -bg white "+fontb);
534	tkcmd(top, "label "+f+".la2 -text {value} -bg white "+fontb);
535
536	tkcmd(top, "grid "+f+".la1 "+f+".la2 -row 1");
537
538	insertattribrow(top);
539}
540
541insertattribrow(top: ref Tk->Toplevel)
542{
543	(n, nil) := sys->tokenize(tkcmd(top, "grid slaves .fsearch -column 1"), " \t\n");
544	row := string (n);
545	sn := string (n - 2);
546	fsn := ".fsearch.ean"+sn;
547	fsv := ".fsearch.eav"+sn;
548	font := " -font /fonts/charon/plain.normal.font";
549	tkcmd(top, "entry "+fsn+" -width 170 -borderwidth 0 "+font);
550	tkcmd(top, "bind "+fsn+" <Key> {send butchan key "+sn+" n %s}");
551	tkcmd(top, "entry "+fsv+" -width 170 -borderwidth 0 "+font);
552	tkcmd(top, "bind "+fsv+" <Key> {send butchan key "+sn+" v %s}");
553	tkcmd(top, "grid rowinsert .fsearch "+row);
554	tkcmd(top, "grid "+fsn+" -column 0 -row "+row+" -sticky w -pady 1 -padx 2");
555	tkcmd(top, "grid "+fsv+" -column 1 -row "+row+" -sticky w -pady 1");
556	setscrollr(top, ".fsearch");
557}
558
559min(a,b: int): int
560{
561	if (a < b)
562		return a;
563	return b;
564}
565
566max(a,b: int): int
567{
568	if (a > b)
569		return a;
570	return b;
571}
572
573makeresultsframe(top: ref Tk->Toplevel, lsrv: list of ref Service, resultstart, maxresults: int)
574{
575	font := " -font /fonts/charon/plain.normal.font";
576	fontb := " -font /fonts/charon/bold.normal.font";
577	f := ".fresults";
578	nresults := len lsrv;
579	row := 0;
580	n := 0;
581	tk->cmd(top, "destroy "+f);
582	tkcmd(top, "frame "+f+" -bg white");
583	title := "Search Results";
584	if (nresults > 0) {
585		from := resultstart+1;
586		too := min(resultstart+maxresults, nresults);
587		if (from == too)
588			title += sys->sprint(" (displaying match %d of %d)", from, nresults);
589		else
590			title += sys->sprint(" (displaying matches %d - %d of %d)", from, too, nresults);
591	}
592	tkcmd(top, "label "+f+".l -text {"+title+"} -bg white -anchor w" + fontb);
593	w1 := int tkcmd(top, f+".l cget -width");
594	w2 := int tkcmd(top, ".f.c cget -width");
595	tkcmd(top, f+".l configure -width "+string max(w1,w2));
596	tkcmd(top, "grid "+f+".l -row 0 -column 0 -columnspan 3 -sticky nw");
597
598	tkcmd(top, "grid rowconfigure "+f+" 0 -minsize 30");
599	tkcmd(top, "frame "+f+".f -bg white");
600	for (; lsrv != nil; lsrv = tl lsrv) {
601		if (n >= resultstart && n < resultstart + maxresults) {
602			srvc := hd lsrv;
603			(resource, name) := getresname(srvc);
604			qid := getqid(srvc);
605			if (qid == nil)
606				qid = string addqid(srvc);
607			label := f+".f.lQ"+qid;
608			tkcmd(top, "label "+label+" -bg white -text {services/"+
609				resource+"/"+name+"/}"+font);
610			tkcmd(top, "grid "+label+" -row "+string row+" -column 0 -sticky w");
611			tkcmd(top, "bind "+label+" <Button-1> {send butchan select "+label+" "+qid+"}");
612			row++;
613		}
614		n++;
615	}
616	if (nresults == 0) {
617		tkcmd(top, "label "+f+".f.l0 -bg white -text {No matches found}"+font);
618		tkcmd(top, "grid "+f+".f.l0 -row 0 -column 0 -columnspan 3 -sticky w");
619	}
620	else {
621		tkcmd(top, "button "+f+".bprev -text {<<} "+
622				"-command {send butchan prev}"+font);
623		if (resultstart == 0)
624			tkcmd(top, f+".bprev configure -state disabled");
625		tkcmd(top, "button "+f+".bnext -text {>>} "+
626				"-command {send butchan next}"+font);
627		if (resultstart + maxresults >= nresults)
628			tkcmd(top, f+".bnext configure -state disabled");
629		tkcmd(top, "grid "+f+".bprev -column 0 -row 2 -padx 5 -pady 5");
630		tkcmd(top, "grid "+f+".bnext -column 2 -row 2 -padx 5 -pady 5");
631	}
632	tkcmd(top, "grid "+f+".f -row 1 -column 0 -columnspan 3 -sticky nw");
633	tkcmd(top, "grid rowconfigure "+f+" 1 -minsize "+string (maxresults*17));
634	tkcmd(top, "button "+f+".bsearch -text {Back to Search} "+
635			"-command {send butchan backto}"+font);
636	tkcmd(top, "grid "+f+".bsearch -column 1 -row 2 -padx 5 -pady 5");
637}
638
639focus(top: ref Tk->Toplevel, newtkp: string)
640{
641	tkcmd(top, "focus "+newtkp);
642	x1 := int tkcmd(top, newtkp + " cget -actx")
643			- int tkcmd(top, ".fsearch cget -actx");
644	y1 := int tkcmd(top, newtkp + " cget -acty")
645			- int tkcmd(top, ".fsearch cget -acty");
646	x2 := x1 + int tkcmd(top, newtkp + " cget -width");
647	y2 := y1 + int tkcmd(top, newtkp + " cget -height") + 45;
648	tkcmd(top, sys->sprint(".f.c see %d %d %d %d", x1,y1-30,x2,y2));
649}
650
651search(top: ref Tk->Toplevel, filter: list of list of (string, string)): list of ref Service
652{
653	searchattrib: list of (string, string) = nil;
654	(n, nil) := sys->tokenize(tkcmd(top, "grid slaves .fsearch -column 0"), " \t\n");
655	for (i := 0; i < n - 3; i++) {
656		attrib := tkcmd(top, ".fsearch.ean"+string i+" get");
657		val := tkcmd(top, ".fsearch.eav"+string i+" get");
658		if (val == nil)
659			val = "*";
660		if (attrib != nil)
661			searchattrib = (attrib, val) :: searchattrib;
662	}
663	tmp : list of list of (string, string) = nil;
664	for (; filter != nil; filter = tl filter) {
665		l := hd filter;
666		for (tmp2 := searchattrib; tmp2 != nil; tmp2 = tl tmp2)
667			l = hd tmp2 :: l;
668		tmp = l :: tmp;
669	}
670	filter = tmp;
671	if (filter == nil)
672		filter = searchattrib :: nil;
673	return find(filter);
674}
675
676getitem(l : list of (string, ref Service), testid: string): ref Service
677{
678	for (; l != nil; l = tl l) {
679		(id, srvc) := hd l;
680		if (testid == id)
681			return srvc;
682	}
683	return nil;
684}
685
686delitem(l : list of (string, ref Service), testid: string): list of (string, ref Service)
687{
688	l2 : list of (string, ref Service) = nil;
689	for (; l != nil; l = tl l) {
690		(id, srvc) := hd l;
691		if (testid != id)
692			l2 = (id, srvc) :: l2;
693	}
694	return l2;
695}
696
697resetsearchscr(top: ref Tk->Toplevel)
698{
699	(n, nil) := sys->tokenize(tkcmd(top, "grid slaves .fsearch -column 1"), " \t\n");
700	for (i := 1; i < n - 2; i++)
701		tkcmd(top, "destroy .fsearch.ean"+string i+" .fsearch.eav"+string i);
702	s := " delete 0 end";
703	tkcmd(top, ".fsearch.ean0"+s);
704	tkcmd(top, ".fsearch.eav0"+s);
705}
706
707tkcmd(top: ref Tk->Toplevel, cmd: string): string
708{
709	e := tk->cmd(top, cmd);
710	if (e != "" && e[0] == '!')
711		sys->print("Tk error: '%s': %s\n",cmd,e);
712	return e;
713}
714
715tkcmds(top: ref Tk->Toplevel, a: array of string)
716{
717	for (j := 0; j < len a; j++)
718		tkcmd(top, a[j]);
719}
720