xref: /inferno-os/appl/wm/man.b (revision 7d5a2526f46cd3474fb96a684f7b87e9e78128b0)
1implement WmMan;
2
3include "sys.m";
4	sys: Sys;
5
6include "draw.m";
7	draw: Draw;
8	Font: import draw;
9
10include "tk.m";
11	tk: Tk;
12
13include "tkclient.m";
14	tkclient: Tkclient;
15
16include "plumbmsg.m";
17include "man.m";
18	man: Man;
19
20WmMan: module {
21	init: fn (ctxt: ref Draw->Context, argv: list of string);
22};
23
24window: ref Tk->Toplevel;
25
26W: adt {
27	textwidth: fn(nil: self ref W, text: Text): int;
28};
29
30ROMAN: con "/fonts/lucidasans/unicode.7.font";
31BOLD: con "/fonts/lucidasans/typelatin1.7.font";
32ITALIC: con "/fonts/lucidasans/italiclatin1.7.font";
33HEADING1: con "/fonts/lucidasans/boldlatin1.7.font";
34HEADING2: con "/fonts/lucidasans/italiclatin1.7.font";
35rfont, bfont, ifont, h1font, h2font: ref Font;
36
37GOATTR: con Parseman->ATTR_LAST << iota;
38MANPATH: con "/man/1/man";
39INDENT: con 40;
40
41metrics: Parseman->Metrics;
42parser: Parseman;
43Text: import parser;
44
45
46tkconfig := array [] of {
47	"frame .input",
48	"frame .view",
49	"text .view.t -state disabled -width 0 -height 0 -bg white -yscrollcommand {.view.yscroll set} -xscrollcommand {.view.xscroll set}",
50	"scrollbar .view.yscroll -orient vertical -command {.view.t yview}",
51	"scrollbar .view.xscroll -orient horizontal -command {.view.t xview}",
52	"entry .input.e -bg white",
53	"button .input.back -state disabled -bitmap small_color_left.bit -command {send nav b}",
54	"button .input.forward -state disabled -bitmap small_color_right.bit -command {send nav f}",
55
56	"pack .input.back .input.forward -side left -anchor w",
57	"pack .input.e -expand 1 -fill x",
58
59 	"pack .view.yscroll -fill y -side left",
60 	"pack .view.t -expand 1 -fill both",
61
62	"bind .input.e <Key-\n> {send nav e}",
63	"bind .input.e <Button-1> +{grab set .input.e}",
64	"bind .input.e <ButtonRelease-1> +{grab release .input.e}",
65	"bind .view.t <Button-1> +{grab set .view.t}",
66	"bind .view.t <ButtonRelease-1> +{grab release .view.t}",
67	"bind .view.t <ButtonRelease-3> {send plumb %x %y}",
68
69	"pack .input -fill x",
70	"pack .view -expand 1 -fill both",
71	"pack propagate . 0",
72	". configure -width 500 -height 500",
73	"focus .input.e",
74};
75
76History: adt {
77	prev: cyclic ref History;
78	next: cyclic ref History;
79	topline: string;
80	searchstart: string;
81	searchend: string;
82	pick {
83	Search =>
84		search: list of string;
85	Go =>
86		path: string;
87	}
88};
89
90history: ref History;
91
92
93init(ctxt: ref Draw->Context, argv: list of string)
94{
95	doplumb := 0;
96
97	sys = load Sys Sys->PATH;
98	if (ctxt == nil) {
99		sys->fprint(sys->fildes(2), "man: no window context\n");
100		raise "fail:bad context";
101	}
102	sys->pctl(Sys->NEWPGRP, nil);
103
104	draw = load Draw Draw->PATH;
105	if (draw == nil)
106		loaderr("Draw");
107
108	tk = load Tk Tk->PATH;
109	if (tk == nil)
110		loaderr(Tk->PATH);
111
112	man = load Man Man->PATH;
113	if (man == nil)
114		loaderr(Man->PATH);
115
116	tkclient = load Tkclient Tkclient->PATH;
117	if (tkclient == nil)
118		loaderr(Tkclient->PATH);
119
120	parser = load Parseman Parseman->PATH;
121	if (parser == nil)
122		loaderr(Parseman->PATH);
123	parser->init();
124
125	plumber := load Plumbmsg Plumbmsg->PATH;
126	if (plumber != nil) {
127		if (plumber->init(1, nil, 0) >= 0)
128			doplumb = 1;
129	}
130
131	argv = tl argv;
132
133	rfont = Font.open(ctxt.display, ROMAN);
134	bfont = Font.open(ctxt.display, BOLD);
135	ifont = Font.open(ctxt.display, ITALIC);
136	h1font = Font.open(ctxt.display, HEADING1);
137	h2font = Font.open(ctxt.display, HEADING2);
138
139	em := rfont.width("m");
140	en := rfont.width("n");
141	metrics = Parseman->Metrics(490, 80, em, en, 14, 40, 20);
142
143	tkclient->init();
144	buts := Tkclient->Resize | Tkclient->Hide;
145	winctl: chan of string;
146	(window, winctl) = tkclient->toplevel(ctxt, nil, "Man", buts);
147	nav := chan of string;
148	plumb := chan of string;
149	tk->namechan(window, nav, "nav");
150	tk->namechan(window, plumb, "plumb");
151	for(tc:=0; tc<len tkconfig; tc++)
152		tkcmd(window, tkconfig[tc]);
153	if ((err := tkcmd(window, "variable lasterror")) != nil) {
154		sys->fprint(sys->fildes(2), "man: tk initialization failed: %s\n", err);
155		raise "fail:tk";
156	}
157	fittoscreen(window);
158	tkcmd(window, "update");
159	mktags();
160
161	vw := int tkcmd(window, ".view.t cget -actwidth") - 10;
162	if (vw <= 0)
163		vw = 1;
164	metrics.pagew = vw;
165
166	linechan := chan of list of (int, Text);
167	man->loadsections(nil);
168
169	pidc := chan of int;
170
171	if (argv != nil) {
172		if (hd argv == "-f") {
173			first: ref History;
174			for (argv = tl argv; argv != nil; argv = tl argv) {
175				hnode := ref History.Go(history, nil, "", "", "", hd argv);
176				if (history != nil)
177					history.next = hnode;
178				history = hnode;
179				if (first == nil)
180					first = history;
181			}
182			history = first;
183		} else
184			history = ref History.Search(nil, nil, "", "", "", argv);
185	}
186
187	if (history == nil)
188		history = ref History.Go(nil, nil, "", "", "", MANPATH);
189
190	setbuttons();
191	spawn printman(pidc, linechan, history);
192	layoutpid := <- pidc;
193	tkclient->onscreen(window, nil);
194	tkclient->startinput(window, "kbd"::"ptr"::nil);
195	for (;;) alt {
196	s := <-window.ctxt.kbd =>
197		tk->keyboard(window, s);
198	s := <-window.ctxt.ptr =>
199		tk->pointer(window, *s);
200	s := <-window.ctxt.ctl or
201	s = <-window.wreq or
202	s = <-winctl =>
203		e := tkclient->wmctl(window, s);
204		if (e == nil && s[0] == '!') {
205			topline := tkcmd(window, ".view.t yview");
206			(nil, toptoks) := sys->tokenize(topline, " ");
207			if (toptoks != nil)
208				history.topline = hd toptoks;
209			vw = int tkcmd(window, ".view.t cget -actwidth") - 10;
210			if (vw <= 0)
211				vw = 1;
212			if (vw != metrics.pagew) {
213				if (layoutpid != -1)
214					kill(layoutpid);
215				metrics.pagew = vw;
216				tkcmd(window, ".view.t delete 1.0 end");
217				tkcmd(window, "update");
218				spawn printman(pidc, linechan, history);
219				layoutpid = <- pidc;
220			}
221		}
222	line := <- linechan =>
223		if (line == nil) {
224			# layout done
225			if (history.topline != "") {
226				topline := tkcmd(window, ".view.t yview");
227				(nil, toptoks) := sys->tokenize(topline, " ");
228				if (toptoks != nil)
229					if (hd toptoks == "0")
230						tkcmd(window, ".view.t yview moveto " + history.topline);
231			}
232			tkcmd(window, "update");
233		} else
234			setline(line);
235	go := <- nav =>
236		topline := tkcmd(window, ".view.t yview");
237		(nil, toptoks) := sys->tokenize(topline, " ");
238		if (toptoks != nil)
239			history.topline = hd toptoks;
240		case go[0] {
241		'f' =>
242			# forward
243			history = history.next;
244			setbuttons();
245			if (layoutpid != -1)
246				kill(layoutpid);
247			tkcmd(window, ".view.t delete 1.0 end");
248			tkcmd(window, "update");
249			spawn printman(pidc, linechan, history);
250			layoutpid = <- pidc;
251		'b' =>
252			# back
253			history = history.prev;
254			setbuttons();
255			if (layoutpid != -1)
256				kill(layoutpid);
257			tkcmd(window, ".view.t delete 1.0 end");
258			tkcmd(window, "update");
259			spawn printman(pidc, linechan, history);
260			layoutpid = <- pidc;
261		'e' or 'l' =>
262			t := "";
263			if (go[0] == 'l') {
264				# link
265				t = go[1:];
266			} else {
267				# entry
268				t = tkcmd(window, ".input.e get");
269				for (i := 0; i < len t; i++)
270					if (!(t[i] == ' ' || t[i] == '\t'))
271						break;
272				if (i == len t)
273					break;
274				t = t[i:];
275				if (t[0] == '/' || t[0] == '?') {
276					search(t);
277					break;
278				}
279			}
280			(n, toks) := sys->tokenize(t, " \t");
281			if (n == 0)
282				continue;
283			h := ref History.Search(history, nil, "", "", "", toks);
284			history.next = h;
285			history = h;
286			setbuttons();
287			if (layoutpid != -1)
288				kill(layoutpid);
289			tkcmd(window, ".view.t delete 1.0 end");
290			tkcmd(window, "update");
291			spawn printman(pidc, linechan, history);
292			layoutpid = <- pidc;
293		'g' =>
294			# goto file
295			h := ref History.Go(history, nil, "", "", "", go[1:]);
296			history.next = h;
297			history = h;
298			setbuttons();
299			if (layoutpid != 0)
300				kill(layoutpid);
301			tkcmd(window, ".view.t delete 1.0 end");
302			tkcmd(window, "update");
303			spawn printman(pidc, linechan, history);
304			layoutpid = <- pidc;
305		}
306	p := <- plumb =>
307		if (!doplumb)
308			break;
309		(nil, l) := sys->tokenize(p, " ");
310		x := int hd l;
311		y := int hd tl l;
312		index := tkcmd(window, ".view.t index @"+string x+","+string y);
313		selindex := tkcmd(window, ".view.t tag ranges sel");
314		insel := 0;
315		if(selindex != "")
316			insel = tkcmd(window, ".view.t compare sel.first <= "+index)=="1" &&
317				tkcmd(window, ".view.t compare sel.last >= "+index)=="1";
318		text := "";
319		attr := "";
320		if (insel)
321			text = tkcmd(window, ".view.t get sel.first sel.last");
322		else{
323			# have line with text in it
324			# now extract whitespace-bounded string around click
325			(nil, w) := sys->tokenize(index, ".");
326			charno := int hd tl w;
327			left := tkcmd(window, ".view.t index {"+index+" linestart}");
328			right := tkcmd(window, ".view.t index {"+index+" lineend}");
329			line := tkcmd(window, ".view.t get "+left+" "+right);
330			for(i:=charno; i>0; --i)
331				if(line[i-1]==' ' || line[i-1]=='\t')
332					break;
333			for(j:=charno; j<len line; j++)
334				if(line[j]==' ' || line[j]=='\t')
335					break;
336			text = line[i:j];
337			attr = "click="+string (charno-i);
338		}
339		msg := ref Plumbmsg->Msg(
340			"WmMan",
341			"",
342			"",
343			"text",
344			attr,
345			array of byte text);
346		plumber->msg.send();
347
348	layoutpid = <- pidc =>
349		;
350	}
351}
352
353search(pat: string)
354{
355	dir: string;
356	start: string;
357	if (pat[0] == '/') {
358		dir = "-forwards";
359		start = history.searchend;
360	} else {
361		dir = "-backwards";
362		start = history.searchstart;
363	}
364	pat = pat[1:];
365	if (start == "")
366		start = "1.0";
367	r := tkcmd(window, ".view.t search " + dir + " -- " + tk->quote(pat) + " " + start);
368	if (r != nil) {
369		history.searchstart = r;
370		history.searchend = r + "+" + string len pat + "c";
371		tkcmd(window, ".view.t tag remove sel 1.0 end");
372		tkcmd(window, ".view.t tag add sel " + history.searchstart + " " + history.searchend);
373		tkcmd(window, ".view.t see " + r);
374		tkcmd(window, "update");
375	}
376}
377
378setbuttons()
379{
380	if (history.prev == nil)
381		tkcmd(window, ".input.back configure -state disabled");
382	else
383		tkcmd(window, ".input.back configure -state normal");
384	if (history.next == nil)
385		tkcmd(window, ".input.forward configure -state disabled");
386	else
387		tkcmd(window, ".input.forward configure -state normal");
388}
389
390dolayout(linechan: chan of list of (int, Text), path: string)
391{
392	fd := sys->open(path, Sys->OREAD);
393	if (fd == nil) {
394		layouterror(linechan, sys->sprint("cannot open file %s: %r", path));
395		return;
396	}
397	w: ref W;
398	parser->parseman(fd, metrics, 0, w, linechan);
399}
400
401printman(pidc: chan of int, linechan: chan of list of (int, Text), h: ref History)
402{
403	pidc <-= sys->pctl(0, nil);
404	args: list of string;
405	pick hp := h {
406		Search =>
407			args = hp.search;
408		Go =>
409			dolayout(linechan, hp.path);
410			pidc <-= -1;
411			return;
412	}
413	sections: list of string;
414	argstext := "";
415	addsections := 1;
416	keywords: list of string;
417	for (; args != nil; args = tl args) {
418		arg := hd args;
419		if (arg == nil)
420			continue;
421		if (addsections && !isint(trimdot(arg))) {
422			addsections = 0;
423			keywords = args;
424		}
425		if (addsections)
426			sections = arg :: sections;
427		argstext = argstext + " " + arg;
428	}
429	manpages := man->getfiles(sections, keywords);
430	pagelist := sortpages(manpages);
431	if (len pagelist == 1) {
432		(nil, path, nil) := hd pagelist;
433		dolayout(linechan, path);
434		pidc <-= -1;
435		return;
436	}
437
438	tt := Text(Parseman->FONT_ROMAN, 0, "Search:", 1, nil);
439	at := Text(Parseman->FONT_BOLD, 0, argstext, 0, nil);
440	linechan <-= (0, tt)::(0, at)::nil;
441	tt.text = "";
442	linechan <-= (0, tt)::nil;
443
444	if (pagelist == nil) {
445		donet := Text(Parseman->FONT_ROMAN, 0, "No matches", 0, nil);
446		linechan <-= (INDENT, donet) :: nil;
447		linechan <-= nil;
448		pidc <-= -1;
449		return;
450	}
451
452	linelist: list of list of Text;
453	pathlist: list of Text;
454
455	maxkwlen := 0;
456	comma := Text(Parseman->FONT_ROMAN, 0, ", ", 0, "");
457	for (; pagelist != nil; pagelist = tl pagelist) {
458		(n, p, kwl) := hd pagelist;
459		l := 0;
460		keywords: list of Text = nil;
461		for (; kwl != nil; kwl = tl kwl) {
462			kw := hd kwl;
463			kwt := Text(Parseman->FONT_ITALIC, GOATTR, kw, 0, p);
464			nt := Text(Parseman->FONT_ROMAN, GOATTR, "(" + string n + ")", 0, p);
465			l += textwidth(kwt) + textwidth(nt);
466			if (keywords != nil) {
467				l += textwidth(comma);
468				keywords = nt :: kwt :: comma :: keywords;
469			} else
470				keywords = nt :: kwt :: nil;
471		}
472		if (l > maxkwlen)
473			maxkwlen = l;
474		linelist = keywords :: linelist;
475		ptext := Text(Parseman->FONT_ROMAN, GOATTR, p, 0, "");
476		pathlist = ptext :: pathlist;
477	}
478
479	for (; pathlist != nil; (pathlist, linelist) = (tl pathlist, tl linelist)) {
480		line := (10 + INDENT + maxkwlen, hd pathlist) :: nil;
481		for (ll := hd linelist; ll != nil; ll = tl ll) {
482			litem := hd ll;
483			if (tl ll == nil)
484				line = (INDENT, litem) :: line;
485			else
486				line = (0, litem) :: line;
487		}
488		linechan <-= line;
489	}
490	linechan <-= nil;
491	pidc <-= -1;
492}
493
494layouterror(linechan: chan of list of (int, Text), msg: string)
495{
496	text := "ERROR: " + msg;
497	t := Text(Parseman->FONT_ROMAN, 0, text, 0, nil);
498	linechan <-= (0, t)::nil;
499	linechan <-= nil;
500}
501
502loaderr(modname: string)
503{
504	sys->print("cannot load %s module: %r\n", modname);
505	raise "fail:init";
506}
507
508W.textwidth(nil: self ref W, text: Text): int
509{
510	return textwidth(text);
511}
512
513textwidth(text: Text): int
514{
515	f: ref Font;
516	if (text.heading == 1)
517		f = h1font;
518	else if (text.heading == 2)
519		f = h2font;
520	else {
521		case text.font {
522		Parseman->FONT_ROMAN =>
523			f = rfont;
524		Parseman->FONT_BOLD =>
525			f = bfont;
526		Parseman->FONT_ITALIC =>
527			f = ifont;
528		* =>
529			return 8 * len text.text;
530		}
531	}
532	return draw->f.width(text.text);
533}
534
535lnum := 0;
536
537setline(line: list of (int, Text))
538{
539	tabstr := "";
540	linestr := "";
541	lastoff := 0;
542	curfont := Parseman->FONT_ROMAN;
543	curlink := "";
544	curgtag := "";
545	curheading := 0;
546	fonttext := "";
547
548	for (l := line; l != nil; l = tl l) {
549		(offset, nil) := hd l;
550		if (offset != 0) {
551			lastoff = offset;
552			if (tabstr != "")
553				tabstr[len tabstr] = ' ';
554			tabstr = tabstr + string offset;
555		}
556	}
557	# fudge up tabs for rest of line
558	if (lastoff != 0)
559		tabstr = tabstr + " " + string lastoff + " " + string (lastoff + INDENT);
560	ttag := "";
561	gtag := "";
562	if (tabstr != nil)
563		ttag = tabtag(tabstr) + " ";
564
565	for (l = line; l != nil; l = tl l) {
566		(offset, text) := hd l;
567		gtag = "";
568		if (text.link != nil) {
569			if (text.attr & GOATTR)
570				gtag = gotag(text.link) + " ";
571			else {
572				gtag = linktag(text.link) + " ";
573			}
574		}
575		if (offset != 0)
576			fonttext[len fonttext] = '\t';
577		if (text.font != curfont || text.link != curlink || text.heading != curheading || gtag != curgtag) {
578			# need to change tags
579			linestr = linestr + " " + tk->quote(fonttext) + " {" + ttag + curgtag + fonttag(curfont, curheading) + "}";
580			ttag = "";
581			curgtag = gtag;
582			fonttext = "";
583			curfont = text.font;
584			curlink = text.link;
585			curheading = text.heading;
586		}
587		fonttext = fonttext + text.text;
588	}
589	if (fonttext != nil)
590		linestr = linestr + " " + tk->quote(fonttext) + " {" + ttag + curgtag + fonttag(curfont, curheading) + "}";
591	tkcmd(window, ".view.t insert end " + linestr);
592	tkcmd(window, ".view.t insert end {\n}");
593	# only update on every other line
594	if (lnum++ & 1)
595		tkcmd(window, "update");
596}
597
598mktags()
599{
600	tkcmd(window, ".view.t tag configure ROMAN -font " + ROMAN);
601	tkcmd(window, ".view.t tag configure BOLD -font " + BOLD);
602	tkcmd(window, ".view.t tag configure ITALIC -font " + ITALIC);
603	tkcmd(window, ".view.t tag configure H1 -font " + HEADING1);
604	tkcmd(window, ".view.t tag configure H2 -font " + HEADING2);
605}
606
607fonttag(font, heading: int): string
608{
609	if (heading == 1)
610		return "H1";
611	if (heading == 2)
612		return "H2";
613	case font {
614	Parseman->FONT_ROMAN =>
615		return "ROMAN";
616	Parseman->FONT_BOLD =>
617		return "BOLD";
618	Parseman->FONT_ITALIC =>
619		return "ITALIC";
620	}
621	return nil;
622}
623
624nexttag := 0;
625lasttabstr := "";
626lasttagname := "";
627
628tabtag(tabstr: string): string
629{
630	if (tabstr == lasttabstr)
631		return lasttagname;
632	lasttagname = "TAB" + string nexttag++;
633	lasttabstr = tabstr;
634	tkcmd(window, ".view.t tag configure " + lasttagname + " -tabs " + tk->quote(tabstr));
635	return lasttagname;
636}
637
638# optimise this!
639gotag(path: string): string
640{
641	cmd := "{send nav g" + path + "}";
642	name := "GO" + string nexttag++;
643	tkcmd(window, ".view.t tag bind " + name + " <ButtonRelease-1> +" + cmd);
644	tkcmd(window, ".view.t tag configure " + name + " -fg green");
645	return name;
646}
647
648# and this!
649linktag(search: string): string
650{
651	cmd := tk->quote("send nav l" + search);
652	name := "LN" + string nexttag++;
653	tkcmd(window, ".view.t tag bind " + name + " <ButtonRelease-1> +" + cmd);
654	tkcmd(window, ".view.t tag configure " + name + " -fg green");
655	return name;
656}
657
658isint(s: string): int
659{
660	for (i := 0; i < len s; i++)
661		if (s[i] < '0' || s[i] > '9')
662			return 0;
663	return 1;
664}
665
666kill(pid: int)
667{
668	fd := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE);
669	if (fd != nil)
670		sys->fprint(fd, "kill");
671}
672
673revsortuniq(strlist: list of string): list of string
674{
675	strs := array [len strlist] of string;
676	for (i := 0; strlist != nil; (i, strlist) = (i+1, tl strlist))
677		strs[i] = hd strlist;
678
679	# simple sort (ascending)
680	for (i = 0; i < len strs - 1; i++) {
681		for (j := i+1; j < len strs; j++)
682			if (strs[i] < strs[j])
683				(strs[i], strs[j]) = (strs[j], strs[i]);
684	}
685
686	# construct list (result is descending)
687	r: list of string;
688	prev := "";
689	for (i = 0; i < len strs; i++) {
690		if (strs[i] != prev) {
691			r = strs[i] :: r;
692			prev = strs[i];
693		}
694	}
695	return r;
696}
697
698sortpages(pagelist: list of (int, string, string)): list of (int, string, list of string)
699{
700	pages := array [len pagelist] of (int, string, string);
701	for (i := 0; pagelist != nil; (i, pagelist) = (i+1, tl pagelist))
702		pages[i] = hd pagelist;
703
704	for (i = 0; i < len pages - 1; i++) {
705		for (j := i+1; j < len pages; j++) {
706			(nil, nil, ipath) := pages[i];
707			(nil, nil, jpath) := pages[j];
708			if (ipath > jpath)
709				(pages[i], pages[j]) = (pages[j], pages[i]);
710		}
711	}
712
713	r: list of (int, string, list of string);
714	filecmds: list of string;
715	lastfile := "";
716	lastsect := 0;
717	for (i = 0; i < len pages; i++) {
718		(section, cmd, file) := pages[i];
719		if (lastfile == "") {
720			lastfile = file;
721			lastsect = section;
722		}
723
724		if (file != lastfile) {
725			r = (lastsect, lastfile, filecmds) :: r;
726			lastfile = file;
727			lastsect = section;
728			filecmds = nil;
729		}
730		filecmds = cmd :: filecmds;
731	}
732	if (filecmds != nil)
733		r = (lastsect, lastfile, revsortuniq(filecmds)) :: r;
734	return r;
735}
736
737fittoscreen(win: ref Tk->Toplevel)
738{
739	Point, Rect: import draw;
740	if (win.image == nil || win.image.screen == nil)
741		return;
742	r := win.image.screen.image.r;
743	scrsize := Point((r.max.x - r.min.x), (r.max.y - r.min.y));
744	bd := int tkcmd(win, ". cget -bd");
745	winsize := Point(int tkcmd(win, ". cget -actwidth") + bd * 2, int tkcmd(win, ". cget -actheight") + bd * 2);
746	if (winsize.x > scrsize.x)
747		tkcmd(win, ". configure -width " + string (scrsize.x - bd * 2));
748	if (winsize.y > scrsize.y)
749		tkcmd(win, ". configure -height " + string (scrsize.y - bd * 2));
750	actr: Rect;
751	actr.min = Point(int tkcmd(win, ". cget -actx"), int tkcmd(win, ". cget -acty"));
752	actr.max = actr.min.add((int tkcmd(win, ". cget -actwidth") + bd*2,
753				int tkcmd(win, ". cget -actheight") + bd*2));
754	(dx, dy) := (actr.dx(), actr.dy());
755	if (actr.max.x > r.max.x)
756		(actr.min.x, actr.max.x) = (r.max.x - dx, r.max.x);
757	if (actr.max.y > r.max.y)
758		(actr.min.y, actr.max.y) = (r.max.y - dy, r.max.y);
759	if (actr.min.x < r.min.x)
760		(actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx);
761	if (actr.min.y < r.min.y)
762		(actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy);
763	tkcmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y);
764}
765
766tkcmd(top: ref Tk->Toplevel, s: string): string
767{
768	e := tk->cmd(top, s);
769	if (e != nil && e[0] == '!')
770		sys->print("tk error %s on '%s'\n", e, s);
771	return e;
772}
773
774trimdot(s: string): string
775{
776	for(i := 0; i < len s; i++)
777		if(s[i] == '.')
778			return s[0: i];
779	return s;
780}
781