shithub: purgatorio

ref: f8935b5778397074d41a48205e5c7f87d7b531fe
dir: /appl/wm/calendar.b/

View raw version
implement Calendar;

#
# Copyright © 2000 Vita Nuova Limited. All rights reserved.
#

include "sys.m";
	sys: Sys;
include "draw.m";
	draw: Draw;
	Font, Point, Rect: import draw;
include "daytime.m";
	daytime: Daytime;
	Tm: import Daytime;
include "tk.m";
	tk: Tk;
include "tkclient.m";
	tkclient: Tkclient;
include "dialog.m";
	dialog: Dialog;
include "readdir.m";
include "translate.m";
	translate: Translate;
	Dict: import translate;
include "arg.m";
	arg: Arg;
include "sh.m";

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

Cal: adt {
	w: string;
	dx, dy: int;
	onepos: int;
	top: ref Tk->Toplevel;
	sched: ref Schedule;
	date: int;
	marked: array of int;
	make: fn(top: ref Tk->Toplevel, sched: ref Schedule, w: string): (ref Cal, chan of string);
	show: fn(cal: self ref Cal, date: int);
	mark: fn(cal: self ref Cal, ent: Entry);
};

Entry: adt {
	date: int;		# YYYYMMDD
	mark: int;
};

Sentry: adt {
	ent: Entry;
	file: int;
};

Schedule: adt {
	dir: string;
	entries: array of Sentry;
	new: fn(dir: string): (ref Schedule, string);
	getentry: fn(sched: self ref Schedule, date: int): (int, Entry);
	readentry: fn(sched: self ref Schedule, date: int): (Entry, string);
	setentry: fn(sched: self ref Schedule, ent: Entry, data: string): (int, string);
};

Markset: adt {
	new: fn(top: ref Tk->Toplevel, cal: ref Cal, w: string): (ref Markset, chan of string);
	set: fn(m: self ref Markset, kind: int);
	get: fn(m: self ref Markset): int;
	ctl: fn(m: self ref Markset, c: string);

	top: ref Tk->Toplevel;
	cal: ref Cal;
	w: string;
	curr: int;
};

DBFSPATH: con "/dis/rawdbfs.dis";
SCHEDDIR: con "/mnt/schedule";

stderr: ref Sys->FD;
dict: ref Dict;
font := "/fonts/lucidasans/unicode.7.font";
days, months: array of string;

packcmds := array[] of {
"pack .ctf.show .ctf.set .ctf.date -side right",
"pack .ctf -side top -fill x",

"pack .cf.head.fwd .cf.head.bwd .cf.head.date -side right",
"pack .cf.head -side top -fill x",
"pack .cf.cal -side top",
"pack .cf -side top",

"pack .schedf.head.fwd .schedf.head.bwd .schedf.head.date .schedf.head.markset"
	+ " .schedf.head.save .schedf.head.del -side right",
"pack .schedf.head -side top -fill x",
"pack .schedf.tf.scroll -side left -fill y",
"pack .schedf.tf.t -side top -fill both -expand 1",
"pack .schedf.tf -side top -fill both -expand 1",
"pack .schedf -side top -fill both -expand 1",
};

Savebut: con ".schedf.head.save";
Delbut: con ".schedf.head.del";

usage()
{
	sys->fprint(stderr, "usage: calendar [-f font] [/mnt/schedule | schedfile]\n");
	raise "fail:usage";
}

init(ctxt: ref Draw->Context, argv: list of string)
{
	loadmods();
	if (ctxt == nil) {
		sys->fprint(sys->fildes(2), "calendar: no window context\n");
		raise "fail:bad context";
	}
	days = Xa(array[] of {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri",  "Sat"});
	months = Xa(array[] of {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"});
	arg->init(argv);
	while ((opt := arg->opt()) != 0) {
		case opt {
		'f' =>
			if ((font = arg->arg()) == nil)
				usage();
		* =>
			usage();
		}
	}
	argv = arg->argv();
	scheddir := SCHEDDIR;
	if (argv != nil)
		scheddir = hd argv;
	(top, wmctl) := tkclient->toplevel(ctxt, "", X("Calendar"), Tkclient->Appl);
	if (top == nil) {
		sys->fprint(stderr, "cal: cannot make window: %r\n");
		raise "fail:cannot make window";
	}
	(sched, err) := Schedule.new(scheddir);
	if (sched == nil)
		sys->fprint(stderr, "cal: cannot load schedule: %s\n", err);
	currtime := daytime->local(daytime->now());
	if (currtime == nil) {
		sys->fprint(stderr, "cannot get local time: %r\n");
		raise "fail:failed to get local time";
	}
	date := tm2date(currtime);
	sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);

	cmdch := chan of string;
	tk->namechan(top, cmdch, "cmd");
	wincmds := array[] of {
	"frame .ctf",
	"button .ctf.set -text {"+X("Set")+"} -command {send cmd settime}",
	"button .ctf.show -text {"+X("Show")+"} -command {send cmd showtime}",
	
	"frame .cf -bd 2 -relief raised",
	"frame .cf.head",
	"button .cf.head.bwd -text {<<} -command {send cmd bwdmonth}",
	"button .cf.head.fwd -text {>>} -command {send cmd fwdmonth}",
	"label .cf.head.date -text {XXX 0000}",
	
	"frame .schedf -bd 2 -relief raised",
	"frame .schedf.head",
	"button .schedf.head.save -text {"+X("Save")+"} -command {send cmd save}",
	"button .schedf.head.del -text {"+X("Del")+"} -command {send cmd del}",
	"label .schedf.head.date -text {0000/00/00}",
	"canvas .schedf.head.markset",
	"button .schedf.head.bwd -text {<<} -command {send cmd bwdday}",
	"button .schedf.head.fwd -text {>>} -command {send cmd fwdday}",
	"frame .schedf.tf",
	"scrollbar .schedf.tf.scroll -command {.schedf.tf.t yview}",
	"text .schedf.tf.t -wrap word -yscrollcommand {.schedf.tf.scroll set} -height 7h -width 20w",
	"bind .schedf.tf.t <Key> +{send cmd dirty}",
	};
	tkcmds(top, wincmds);
	(cal, calch) := Cal.make(top, sched, ".cf.cal");
	sync := chan of int;
	spawn clock(top, ".ctf.date", sync);
	clockpid := <-sync;
	(ms, msch) := Markset.new(top, cal, ".schedf.head.markset");
	tkcmds(top, packcmds);
	if (sched == nil)
		cmd(top, "pack forget .schedf");

	showdate(top, cal, ms, date);
	cmd(top, "pack propagate . 0");
	cmd(top, "update");
	if (date < 19700002)
		raisesettime(ctxt, top);

	setting := 0;
	dirty := 0;
	empty := scheduleempty(top);
	currsched := 0;

	tkclient->onscreen(top, nil);
	tkclient->startinput(top, "kbd"::"ptr"::nil);

	for (;;) {
		enable(top, Savebut, dirty);
		enable(top, Delbut, !empty);
		cmd(top, "update");
		ndate := date;
		alt {
		c := <-calch =>
			(y,m,d) := date2ymd(date);
			d = int c;
			ndate = ymd2date(y,m,d);
		c := <-msch =>
			ms.ctl(c);
			cal.mark(Entry(date, ms.get()));
			dirty = 1;
		c := <-cmdch =>
			case c {
			"dirty" =>
				dirty = 1;
				nowempty := scheduleempty(top);
				if (nowempty != empty) {
					if (nowempty) {
						ms.set(0);
						cal.mark(Entry(date, 0));
					} else {
						ms.set(1);
						cal.mark(Entry(date, ms.get()));
					}
					empty = nowempty;
				}
			"bwdmonth" =>
				ndate = decmonth(date);
			"fwdmonth" =>
				ndate = incmonth(date);
			"bwdday" =>
				ndate = adddays(date, -1);
			"fwdday" =>
				ndate = adddays(date, 1);
			"del" =>
				if (!empty) {
					cmd(top, ".schedf.tf.t delete 1.0 end");
					empty = 1;
					dirty = 1;
					cal.mark(Entry(date, 0));
				}
			"save" =>
				if (dirty && save(ctxt, top, cal, ms, date) != -1)
					dirty = 0;
			"settime" =>
				raisesettime(ctxt, top);
			"showtime" =>
				ndate = tm2date(daytime->local(daytime->now()));
			* =>
				sys->fprint(stderr, "cal: unknown command '%s'\n", c);
			}
		s := <-top.ctxt.kbd =>
			tk->keyboard(top, s);
		s := <-top.ctxt.ptr =>
			tk->pointer(top, *s);
		c := <-top.ctxt.ctl or
		c = <-top.wreq or
		c = <-wmctl =>
			if (c == "exit" && dirty)
				save(ctxt, top, cal, ms, date);
			tkclient->wmctl(top, c);
		}
		if (ndate != date) {
			e := 0;
			if (dirty)
				e = save(ctxt, top, cal, ms, date);
			if (e != -1) {
				dirty = 0;
				showdate(top, cal, ms, ndate);
				empty = scheduleempty(top);
				date = ndate;
				cmd(top, "update");
			}
		}
	}
}

Markset.new(top: ref Tk->Toplevel, cal: ref Cal, w: string): (ref Markset, chan of string)
{
	cmd(top, w+" configure -width "+string (cal.dx * 2 + 6) +
				" -height "+string (cal.dy + 4));
	ch := chan of string;
	tk->namechan(top, ch, "markcmd");
	return (ref Markset(top, cal, w, 0), ch);
}

Markset.set(m: self ref Markset, kind: int)
{
	cmd(m.top, m.w + " delete x");
	if (kind > 0) {
		(shape, col) := kind2shapecol(kind);
		id := cmd(m.top, m.w + " create " +
			shapestr(m.cal, (m.cal.dx/2+2, m.cal.dy/2+2), Square) +
			" -fill " + colours[col] + " -tags x");
		cmd(m.top, m.w + " bind " + id + " <ButtonRelease-1> {send markcmd col}");
		id = cmd(m.top, m.w + " create " +
			shapestr(m.cal, (m.cal.dx * 3 / 2+4, m.cal.dy/2+2), shape) +
			" -tags x -width 2");
		cmd(m.top, m.w + " bind " + id + " <ButtonRelease-1> {send markcmd shape}");
	}
	m.curr = kind;
}

Markset.get(m: self ref Markset): int
{
	return m.curr;
}

Markset.ctl(m: self ref Markset, c: string)
{
	(shape, col) := kind2shapecol(m.curr);
	case c {
	"col" => col = (col + 1) % len colours;
	"shape" => shape = (shape + 1) % Numshapes;
	}
	m.set(shapecol2kind((shape, col)));
}

scheduleempty(top: ref Tk->Toplevel): int
{
	return int cmd(top, ".schedf.tf.t compare 1.0 == end");
}

enable(top: ref Tk->Toplevel, but: string, enable: int)
{
	cmd(top, but + " configure -state " +
		(array[] of {"disabled", "normal"})[!!enable]);
}

save(ctxt: ref Draw->Context, top: ref Tk->Toplevel, cal: ref Cal, ms: ref Markset, date: int): int
{
	s := cmd(top, ".schedf.tf.t get 1.0 end");
	empty := scheduleempty(top);
	mark := ms.get();
	if (empty)
		mark = 0;
	ent := Entry(date, mark);
	cal.mark(ent);
	(ok, err) := cal.sched.setentry(ent, s);
	if (ok == -1) {
		notice(ctxt, top, "Cannot save entry: " + err);
		return -1;
	}
	return 0;
}

notice(ctxt: ref Draw->Context, top: ref Tk->Toplevel, s: string)
{
	dialog->prompt(ctxt, top.image, nil, "Notice", s, 0, "OK"::nil);
}

showdate(top: ref Tk->Toplevel, cal: ref Cal, ms: ref Markset, date: int)
{
	(y,m,d) := date2ymd(date);
 	cal.show(date);
	cmd(top, ".cf.head.date configure -text {" + sys->sprint("%.4d/%.2d", y, m+1) + "}");
	cmd(top, ".schedf.head.date configure -text {" + sys->sprint("%.4d/%.2d/%.2d", y, m+1, d) + "}");
	(ent, s) := cal.sched.readentry(date);
	ms.set(ent.mark);
	cmd(top, ".schedf.tf.t delete 1.0 end; .schedf.tf.t insert 1.0 '" + s);
}

nomod(s: string)
{
	sys->fprint(stderr, "cal: cannot load %s: %r\n", s);
	raise "fail:bad module";
}

loadmods()
{
	sys = load Sys Sys->PATH;
	stderr = sys->fildes(2);
	draw = load Draw Draw->PATH;
	tk = load Tk Tk->PATH;
	daytime = load Daytime Daytime->PATH;
	if (daytime == nil)
		nomod(Daytime->PATH);
	tkclient = load Tkclient Tkclient->PATH;
	if (tkclient == nil)
		nomod(Tkclient->PATH);
	translate = load Translate Translate->PATH;
	if(translate != nil){
		translate->init();
		(dict, nil) = translate->opendict(translate->mkdictname("", "calendar"));
	}
	tkclient->init();
	arg = load Arg Arg->PATH;
	if (arg == nil)
		nomod(Arg->PATH);
	dialog = load Dialog Dialog->PATH;
	if(dialog == nil)
		nomod(Dialog->PATH);
	dialog->init();
}

s2a(s: string, min, max: int, sep: string): array of int
{
	(ntoks, toks) := sys->tokenize(s, sep);
	if (ntoks < min || ntoks > max)
		return nil;
	a := array[max] of int;
	for (i := 0; toks != nil; toks = tl toks) {
		if (!isnum(hd toks))
			return nil;
		a[i++] = int hd toks;
	}
	return a[0:i];
}

validtm(t: ref Daytime->Tm): int
{
	if (t.hour < 0 || t.hour > 23
			|| t.min < 0 || t.min > 59
			|| t.sec < 0 || t.sec > 59
			|| t.mday < 1 || t.mday > 31
			|| t.mon < 0 || t.mon > 11
			|| t.year < 70 || t.year > 137)
		return 0;
	if (t.mon == 1 && dysize(t.year+1900) > 365)
		return t.mday <= 29;
	return t.mday <= dmsize[t.mon];
}

clock(top: ref Tk->Toplevel, w: string, sync: chan of int)
{
	cmd(top, "label " + w);	
	fd := sys->open("/dev/time", Sys->OREAD);
	if (fd == nil) {
		sync <-= -1;
		return;
	}
	buf := array[128] of byte;
	for (;;) {
		sys->seek(fd, big 0, Sys->SEEKSTART);
		n := sys->read(fd, buf, len buf);
		if (n < 0) {
			sys->fprint(stderr, "cal: could not read time: %r\n");
			if (sync != nil)
				sync <-= -1;
			break;
		}
		ms := big string buf[0:n] / big 1000;
		ct := ms / big 1000;
		t := daytime->local(int ct);

		s := sys->sprint("%s %s %d %.2d:%.2d.%.2d",
			days[t.wday], months[t.mon], t.mday, t.hour, t.min, t.sec);
		cmd(top, w + " configure -text {" + s + "}");
		cmd(top, "update");
		if (sync != nil) {
			sync <-= sys->pctl(0, nil);
			sync = nil;
		}
		sys->sleep(int ((ct + big 1) * big 1000 - ms));
	}
}

# "the world is the lord's and all it contains,
# save the highlands and islands, which belong to macbraynes"
Cal.make(top: ref Tk->Toplevel, sched: ref Schedule, w: string): (ref Cal, chan of string)
{
	f := Font.open(top.display, font);
	if (f == nil) {
		sys->fprint(stderr, "cal: could not open font %s: %r\n", font);
		font = cmd(top, ". cget -font");
		f = Font.open(top.display, font);
	}
	if (f == nil)
		return (nil, nil);
	maxw := 0;
	for (i := 0; i < 7; i++) {
		if ((dw := f.width(days[i] + " ")) > maxw)
			maxw = dw;
	}
	for (i = 10; i < 32; i++) {
		if ((dw := f.width(string i + " ")) > maxw)
			maxw = dw;
	}
	cal := ref Cal;
	cal.w = w;
	cal.dx = maxw;
	cal.dy = f.height;
	cal.onepos = 0;
	cal.top = top;
	cal.sched = sched;
	cal.marked = array[31] of {* => 0};
	cmd(top, "canvas " + w + " -width " + string (cal.dx * 7) + " -height " + string (cal.dy * 7));
	for (i = 0; i < 7; i++)
		cmd(top, w + " create text " + posstr(daypos(cal, i, 0))
				+ " -text " + days[i] + " -font " + font);
	ch := chan of string;
	tk->namechan(top, ch, "ch" + w);
	return (cal, ch);
}

Cal.show(cal: self ref Cal, date: int)
{
	if (date == cal.date)
		return;
	mon := (date / 100) % 100;
	year := date / 10000;
	cmd(cal.top, cal.w + " delete curr");
	if (cal.date / 100 != date / 100) {
		cmd(cal.top, cal.w + " delete date");
		cmd(cal.top, cal.w + " delete mark");
		for (i := 0; i < len cal.marked; i++)
			cal.marked[i] = 0;
		(md, wd) := monthinfo(mon, year);
		base := year * 10000 + mon * 100;
		cal.onepos = wd;
		for (i = 0; i < 6; i++) {
			for (j := 0; j < 7; j++) {
				d := i * 7 + j - wd;
				if (d >= 0 && d < md) {
					id := cmd(cal.top, cal.w + " create text " + posstr(daypos(cal, j, i+1))
						+ " -tags date -text " + string (d+1)
						+ " -font " + font);
					cmd(cal.top, cal.w + " bind " + id +
						" <ButtonRelease-1> {send ch" + cal.w + " " + string (d+1) + "}");
					(ok, ent) := cal.sched.getentry(base + d + 1);
					if (ok != -1)
						cal.mark(ent);
				}
			}
		}
	}
	if (cal.sched != nil) {
		e := date % 100 - 1 + cal.onepos;
		p := daypos(cal, e % 7, e / 7 + 1);
		cmd(cal.top, cal.w + " create " + shapestr(cal, p, Square) +
				" -tags curr -width 3");
	}
	cal.date = date;
}

Cal.mark(cal: self ref Cal, ent: Entry)
{
	if (ent.date / 100 != ent.date / 100)
		return;
	(nil, nil, d) := date2ymd(ent.date);
	d--;
	cmd(cal.top, cal.w + " delete m" + string d);
	if (ent.mark) {
		e := d + cal.onepos;
		p := daypos(cal, e % 7, e / 7 + 1);
		id := cmd(cal.top, cal.w + " create " + itemshape(cal, p, ent.mark) +
				" -tags {mark m"+string d + "}");
		cmd(cal.top, cal.w + " bind " + id +
				" <ButtonRelease-1> {send ch" + cal.w + " " + string (d+1) + "}");
		cmd(cal.top, cal.w + " lower " + id);
	}
	cal.marked[d] = ent.mark;
}

Oval, Diamond, Square, Numshapes: con iota;

colours := array[] of {
	"red",
	"yellow",
	"#00eeee",
	"white"
};

kind2shapecol(kind: int): (int, int)
{
	kind = (kind - 1) & 16rffff;
	return ((kind & 16rff) % Numshapes, (kind >> 8) % len colours);
}

shapecol2kind(shapecol: (int, int)): int
{
	(shape, colour) := shapecol;
	return (shape + (colour << 8)) + 1;
}

itemshape(cal: ref Cal, centre: Point, kind: int): string
{
	(shape, colour) := kind2shapecol(kind);
	return shapestr(cal, centre, shape) + " -fill " + colours[colour];
}

shapestr(cal: ref Cal, p: Point, kind: int): string
{
	(hdx, hdy) := (cal.dx / 2, cal.dy / 2);
	case kind {
	Oval =>
		r := Rect((p.x - hdx, p.y - hdy), (p.x + hdx, p.y + hdy));
		return "oval " + rectstr(r);
	Diamond =>
		return "polygon " + string (p.x - hdx) + " " + string p.y + " " +
					string p.x + " " + string (p.y - hdy) + " " +
					string (p.x + hdx) + " " + string p.y + " " +
					string p.x + " " + string (p.y + hdy) +
				" -outline black";
	Square =>
		r := Rect((p.x - hdx, p.y - hdy), (p.x + hdx, p.y + hdy));
		return "rectangle " + rectstr(r);
	* =>
		sys->fprint(stderr, "cal: unknown shape %d\n", kind);
		return nil;
	}
}
		
rectstr(r: Rect): string
{
	return string r.min.x + " " + string r.min.y + " " +
			string r.max.x + " " + string r.max.y;
}

posstr(p: Point): string
{
	return string p.x + " " + string p.y;
}

# return centre point of position for day.
daypos(cal: ref Cal, d, w: int): Point
{
	return Point(d * cal.dx + cal.dx / 2, w * cal.dy + cal.dy / 2);
}

body2entry(body: string): (int, Entry, string)
{
	for (i := 0; i < len body; i++)
		if (body[i] == '\n')
			break;
	if (i == len body)
		return (-1, (-1, -1), "invalid schedule header (no newline)");
	(n, toks) := sys->tokenize(body[0:i], " \t\n");
	if (n < 2)
		return (-1, (-1, -1), "invalid schedule header (too few fields)");
	date := int hd toks;
	(y, m, d) := (date / 10000, (date / 100) % 100, date%100);
	if (y < 1970 || y > 2037 || m > 12 || m < 1 || d > 31 || d < 1)
		return (-1, (-1,-1), sys->sprint("invalid date (%.8d) in schedule header", date));
	e := Entry(ymd2date(y, m-1, d), int hd tl toks);
	return (0, e, body[i+1:]);
}

startdbfs(f: string): (string, string)
{
	dbfs := load Command DBFSPATH;
	if (dbfs == nil)
		return (nil, sys->sprint("cannot load %s: %r", DBFSPATH));
	sync := chan of string;
	spawn rundbfs(sync, dbfs, f, SCHEDDIR);
	e := <-sync;
	if (e != nil)
		return (nil, e);
	return (SCHEDDIR, nil);
}

rundbfs(sync: chan of string, dbfs: Command, f, d: string)
{
	sys->pctl(Sys->FORKFD, nil);
	{
		dbfs->init(nil, "dbfs" :: "-r" :: f :: d :: nil);
		sync <-= nil;
	}exception e{
	"fail:*" =>
		sync <-= "dbfs failed: " + e[5:];
		exit;
	}
}

Schedule.new(d: string): (ref Schedule, string)
{
	(rc, info) := sys->stat(d);
	if (rc == -1)
		return (nil, sys->sprint("cannot find %s: %r", d));
	if ((info.mode & Sys->DMDIR) == 0) {
		err: string;
		(d, err) = startdbfs(d);
		if (d == nil)
			return (nil, err);
	}
	(rc, nil) = sys->stat(d + "/new");
	if (rc == -1)
		return (nil, "no dbfs mounted on " + d);
		
	readdir := load Readdir Readdir->PATH;
	if (readdir == nil)
		return (nil, sys->sprint("cannot load %s: %r", Readdir->PATH));
	sched := ref Schedule;
	sched.dir = d;
	(de, nil) := readdir->init(d, Readdir->NONE);
	if (de == nil)
		return (nil, "could not read schedule directory");
	buf := array[Sys->ATOMICIO] of byte;
	sched.entries = array[len de] of Sentry;
	ne := 0;
	for (i := 0; i < len de; i++) {
		if (!isnum(de[i].name))
			continue;
		f := d + "/" + de[i].name;
		fd := sys->open(f, Sys->OREAD);
		if (fd == nil) {
			sys->fprint(stderr, "cal: cannot open %s: %r\n", f);
		} else {
			n := sys->read(fd, buf, len buf);
			if (n == -1) {
				sys->fprint(stderr, "cal: error reading %s: %r\n", f);
			} else {
				(ok, e, err) := body2entry(string buf[0:n]);
				if (ok == -1)
					sys->fprint(stderr, "cal: error on entry %s: %s\n", f, err);
				else
					sched.entries[ne++] = (e, int de[i].name);
				err = nil;
			}
		}
	}
	sched.entries = sched.entries[0:ne];
	sortentries(sched.entries);
	return (sched, nil);
}

Schedule.getentry(sched: self ref Schedule, date: int): (int, Entry)
{
	if (sched == nil)
		return (-1, (-1, -1));
	ent := search(sched, date);
	if (ent == -1)
		return (-1, (-1,-1));
	return (0, sched.entries[ent].ent);
}

Schedule.readentry(sched: self ref Schedule, date: int): (Entry, string)
{
	if (sched == nil)
		return ((-1, -1), nil);
	ent := search(sched, date);
	if (ent == -1)
		return ((-1, -1), nil);
	(nil, fno) := sched.entries[ent];

	f := sched.dir + "/" + string fno;
	fd := sys->open(f, Sys->OREAD);
	if (fd == nil) {
		sys->fprint(stderr, "cal: cannot open %s: %r", f);
		return ((-1, -1), nil);
	}
	buf := array[Sys->ATOMICIO] of byte;
	n := sys->read(fd, buf, len buf);
	if (n == -1) {
		sys->fprint(stderr, "cal: cannot read %s: %r", f);
		return ((-1, -1), nil);
	}
	(ok, e, body) := body2entry(string buf[0:n]);
	if (ok == -1) {
		sys->fprint(stderr, "cal: couldn't get body in file %s: %s\n", f, body);
		return ((-1, -1), nil);
	}
	return (e, body);
}	

writeentry(fd: ref Sys->FD, ent: Entry, data: string): (int, string)
{
	ent.date += 100;
	b := array of byte (sys->sprint("%d %d\n", ent.date, ent.mark) + data);
	if (len b > Sys->ATOMICIO)
		return (-1, "entry is too long");
	if (sys->write(fd, b, len b) != len b)
		return (-1, sys->sprint("cannot write entry: %r"));
	return (0, nil);
}
	
Schedule.setentry(sched: self ref Schedule, ent: Entry, data: string): (int, string)
{
	if (sched == nil)
		return (-1, "no schedule");
	idx := search(sched, ent.date);
	if (idx == -1) {
		if (data == nil)
			return (0, nil);
		fd := sys->open(sched.dir + "/new", Sys->OWRITE);
		if (fd == nil)
			return (-1, sys->sprint("cannot open new: %r"));
		(ok, info) := sys->fstat(fd);
		if (ok == -1)
			return (-1, sys->sprint("cannot stat new: %r"));
		if (!isnum(info.name))
			return (-1, "new dbfs entry is not numeric");
		err: string;
		(ok, err) = writeentry(fd, ent, data);
		if (ok == -1)
			return (ok, err);
		(fd, data) = (nil, nil);
		e := sched.entries;
		for (i := 0; i < len e; i++)
			if (ent.date < e[i].ent.date)
				break;
		ne := array[len e + 1] of Sentry;
		(ne[0:],  ne[i], ne[i+1:]) = (e[0:i], (ent, int info.name), e[i:]);
		sched.entries = ne;
		return (0, nil);
	} else {
		fno := sched.entries[idx].file;
		f := sched.dir + "/" + string fno;
		if (data == nil) {
			sys->remove(f);
			sched.entries[idx:] = sched.entries[idx+1:];
			sched.entries = sched.entries[0:len sched.entries - 1];
			return (0, nil);
		} else {
			sched.entries[idx] = (ent, fno);
			fd := sys->open(f, Sys->OWRITE);
			if (fd == nil)
				return (-1, sys->sprint("cannot open %s: %r", sched.dir + "/" + string fno));
			return writeentry(fd, ent, data);
		}
	}
}

search(sched: ref Schedule, date: int): int
{
	e := sched.entries;
	lo := 0;
	hi := len e - 1;
	while (lo <= hi) {
		mid := (lo + hi) / 2;
		if (date < e[mid].ent.date)
			hi = mid - 1;
		else if (date > e[mid].ent.date)
			lo = mid + 1;
		else
			return mid;
	}
	return -1;
}

sortentries(a: array of Sentry)
{
	m: int;
	n := len a;
	for(m = n; m > 1; ) {
		if(m < 5)
			m = 1;
		else
			m = (5*m-1)/11;
		for(i := n-m-1; i >= 0; i--) {
			tmp := a[i];
			for(j := i+m; j <= n-1 && tmp.ent.date > a[j].ent.date; j += m)
				a[j-m] = a[j];
			a[j-m] = tmp;
		}
	}
}

raisesettime(ctxt: ref Draw->Context, top: ref Tk->Toplevel)
{
	panelcmds := array[] of {
	"frame .d",
	"label .d.title -text {"+X("Date (YYYY/MM/DD):")+"}",
	"entry .d.de -width 11w}",
	"frame .t",
	"label .t.title -text {"+X("Time (HH:MM.SS):")+"}",
	"entry .t.te -width 11w}",
	"frame .b",
	"button .b.set -text Set -command {send cmd set}",
	"button .b.cancel -text Cancel -command {send cmd cancel}",
	"pack .d .t .b -side top -fill x",
	"pack .d.de .d.title -side right",
	"pack .t.te .t.title -side right",
	"pack .b.set .b.cancel -side right",
	};
	fd := sys->open("/dev/time", Sys->OWRITE);
	if (fd == nil) {
		notice(ctxt, top, X("Cannot set time: ") + sys->sprint("%r"));
		return;
	}
	(panel, wmctl) := tkclient->toplevel(ctxt, "",	X("Set Time"), 0);
	tkcmds(panel, panelcmds);
	cmdch := chan of string;
	tk->namechan(panel, cmdch, "cmd");
	t := daytime->local(daytime->now());
	if (t.year < 71)
		(t.year, t.mon, t.mday) = (100, 0, 1);
	cmd(panel, ".d.de insert 0 " + sys->sprint("%.4d/%.2d/%.2d",
				t.year+1900, t.mon+1, t.mday));
	cmd(panel, ".t.te insert 0 " + sys->sprint("%.2d:%.2d.%.2d", t.hour, t.min, t.sec));
	#cmd(panel, "grab set ."); XXX should, but not a good idea with global tk.
	# wouldn't work with current dialog->prompt() either...
	cmd(panel, "update");
	tkclient->onscreen(panel, nil);
	tkclient->startinput(panel, "kbd"::"ptr"::nil);

loop: for (;;) alt {
	s := <-panel.ctxt.kbd =>
		tk->keyboard(panel, s);
	s := <-panel.ctxt.ptr =>
		tk->pointer(panel, *s);
	c := <-cmdch =>
		case c {
		"set" =>
			err := settime(fd, cmd(panel, ".d.de get"), cmd(panel, ".t.te get"));
			if (err == nil)
				break loop;
			notice(ctxt, panel, X("Cannot set time: ") + err);
		"cancel" =>
			break loop;
		* =>;
		}
	c := <-wmctl =>
		case c {
		"exit" =>
			break loop;
		* =>
			tkclient->wmctl(panel, c);
		}
	}
}

settime(tfd: ref Sys->FD, date, time: string): string
{
	da := s2a(date, 3, 3, "/");
	if (da == nil)
		return X("Invalid date syntax");
	ta := s2a(time, 2, 3, ":.");
	if (ta == nil)
		return X("Invalid time syntax");
	t := ref blanktm;
	if (da[2] > 1000)
		(da[0], da[1], da[2]) = (da[2], da[1], da[0]);
	(t.year, t.mon, t.mday) = (da[0]-1900, da[1]-1, da[2]);
	if (len ta == 3)
		(t.hour, t.min, t.sec) = (ta[0], ta[1], ta[2]);
	else
		(t.hour, t.min, t.sec) = (ta[0], ta[1], 0);
	if (!validtm(t))
		return X("Invalid time or date given");
	s := string daytime->tm2epoch(t) + "000000";
	if (sys->fprint(tfd, "%s", s) == -1)
		return X("write failed:") + sys->sprint(" %r");
	return nil;
}
	

cmd(top: ref Tk->Toplevel, cmd: string): string
{
	e := tk->cmd(top, cmd);
	if (e != nil && e[0] == '!')
		sys->fprint(stderr, "cal: tk error on '%s': %s\n", cmd, e);
	return e;
}

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

isnum(s: string): int
{
	for (i := 0; i < len s; i++)
		if (s[i] < '0' || s[i] > '9')
			return 0;
	return 1;
}

tm2date(t: ref Tm): int
{
	if (t == nil)
		return 19700001;
	return ymd2date(t.year+1900, t.mon, t.mday);
}

date2ymd(date: int): (int, int, int)
{
	return (date / 10000, (date / 100) % 100, date%100);
}

ymd2date(y, m, d: int): int
{
	return d + m* 100 + y * 10000;
}

adddays(date, delta: int): int
{
	t := ref blanktm;
	t.mday = date % 100;
	t.mon = (date / 100) % 100;
	t.year = (date / 10000) - 1900;
	t.hour = 12;
	e := daytime->tm2epoch(t);
	e += delta * 24 * 60 * 60;
	t = daytime->gmt(e);
	if (!validtm(t))
		return date;
	return tm2date(t);
}

incmonth(date: int): int
{
	(y,m,d) := date2ymd(date);
	if (m < 11)
		m++;
	else if (y < 2037)
		(y, m) = (y+1, 0);
	(n, nil) := monthinfo(m, y);
	if (d > n)
		d = n;
	return ymd2date(y,m,d);
}

decmonth(date: int): int
{
	(y,m,d) := date2ymd(date);
	if (m > 0)
		m--;
	else if (y > 1970)
		(y, m) = (y-1, 11);
	(n, nil) := monthinfo(m, y);
	if (d > n)
		d = n;
	return ymd2date(y,m,d);
}

dmsize := array[] of {
	31, 28, 31, 30, 31, 30,
	31, 31, 30, 31, 30, 31
};

dysize(y: int): int
{
	if( (y%4) == 0 && (y % 100 != 0 || y % 400 == 0) )
		return 366;
	return 365;
}

blanktm: Tm;

# return number of days in month and
# starting day of month/year.
monthinfo(mon, year: int): (int, int)
{
	t  := ref blanktm;
	t.mday = 1;
	t.mon = mon;
	t.year = year - 1900;
	t = daytime->gmt(daytime->tm2epoch(t));
	md := dmsize[mon];
	if (dysize(year) == 366 && t.mon == 1)
		md++;
	return (md, t.wday);
}

X(s: string): string
{
	#sys->print("\"%s\"\n", s);
	if (dict == nil)
		return s;
	return dict.xlate(s);
}

Xa(a: array of string): array of string
{
	for (i := 0; i < len a; i++)
		a[i] = X(a[i]);
	return a;
}