summaryrefslogtreecommitdiff
path: root/appl/spree/clients
diff options
context:
space:
mode:
Diffstat (limited to 'appl/spree/clients')
-rw-r--r--appl/spree/clients/bounce.b958
-rw-r--r--appl/spree/clients/cards.b2220
-rw-r--r--appl/spree/clients/chat.b194
-rw-r--r--appl/spree/clients/gather.b178
-rw-r--r--appl/spree/clients/lobby.b562
-rw-r--r--appl/spree/clients/othello.b270
6 files changed, 4382 insertions, 0 deletions
diff --git a/appl/spree/clients/bounce.b b/appl/spree/clients/bounce.b
new file mode 100644
index 00000000..f1960582
--- /dev/null
+++ b/appl/spree/clients/bounce.b
@@ -0,0 +1,958 @@
+implement Clientmod;
+
+# bouncing balls demo. it uses tk and multiple processes to animate a
+# number of balls bouncing around the screen. each ball has its own
+# process; CPU time is doled out fairly to each process by using
+# a central monitor loop.
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+ draw: Draw;
+ Display, Point, Rect, Image: import draw;
+include "tk.m";
+ tk: Tk;
+include "tkclient.m";
+ tkclient: Tkclient;
+include "math.m";
+ math: Math;
+include "rand.m";
+include "../client.m";
+
+BALLSIZE: con 5;
+ZERO: con 1e-6;
+π: con Math->Pi;
+Maxδ: con π / 4.0; # max bat angle deflection
+
+Line: adt {
+ p, v: Realpoint;
+ s: real;
+ new: fn(p1, p2: Point): ref Line;
+ hittest: fn(l: self ref Line, p: Point): (Realpoint, real, real);
+ intersection: fn(b: self ref Line, p, v: Realpoint): (int, Realpoint, real, real);
+ point: fn(b: self ref Line, s: real): Point;
+};
+
+Realpoint: adt {
+ x, y: real;
+};
+
+cliquecmds := array[] of {
+"canvas .c -bg black",
+"bind .c <ButtonRelease-1> {send mouse 0 1 %x %y}",
+"bind .c <ButtonRelease-2> {send mouse 0 2 %x %y}",
+"bind .c <Button-1> {send mouse 1 1 %x %y}",
+"bind .c <Button-2> {send mouse 1 2 %x %y}",
+"bind . <Key-b> {send ucmd newball}",
+"bind . <ButtonRelease-1> {focus .}",
+"bind .Wm_t <ButtonRelease-1> +{focus .}",
+"focus .",
+"bind .c <Key-b> {send ucmd newball}",
+"bind .c <Key-u> {grab release .c}",
+"frame .f",
+"button .f.b -text {Start} -command {send ucmd start}",
+"button .f.n -text {New ball} -command {send ucmd newball}",
+"pack .f.b .f.n -side left",
+"pack .f -fill x",
+"pack .c -fill both -expand 1",
+"update",
+};
+
+Ballstate: adt {
+ owner: int; # index into member array
+ hitobs: ref Obstacle;
+ t0: int;
+ p, v: Realpoint;
+ speed: real;
+};
+
+Queue: adt {
+ h, t: list of T;
+ put: fn(q: self ref Queue, s: T);
+ get: fn(q: self ref Queue): T;
+};
+
+
+Obstacle: adt {
+ line: ref Line;
+ id: int;
+ isbat: int;
+ s1, s2: real;
+ srvid: int;
+ owner: int;
+ new: fn(id: int): ref Obstacle;
+ config: fn(b: self ref Obstacle);
+};
+
+Object: adt {
+ obstacle: ref Obstacle;
+ ballctl: chan of ref Ballstate;
+};
+
+
+Member: adt {
+ id: int;
+ colour: string;
+};
+
+win: ref Tk->Toplevel;
+
+lines: list of ref Obstacle;
+lineversion := 0;
+memberid: int;
+myturn: int;
+stderr: ref Sys->FD;
+timeoffset := 0;
+
+objects: array of ref Object;
+srvobjects: array of ref Obstacle; # all for lasthit...
+members: array of ref Member;
+
+CORNER: con 60;
+INSET: con 20;
+WIDTH: con 500;
+HEIGHT: con 500;
+
+bats: list of ref Obstacle;
+mkball: chan of (int, chan of chan of ref Ballstate);
+cliquefd: ref Sys->FD;
+currentlydragging := -1;
+Ballexit: ref Ballstate;
+Noobs: ref Obstacle;
+
+nomod(s: string)
+{
+ sys->fprint(stderr, "bounce: cannot load %s: %r\n", s);
+ sys->raise("fail:bad module");
+}
+
+client(ctxt: ref Draw->Context, argv: list of string, nil: int)
+{
+ sys = load Sys Sys->PATH;
+ stderr = sys->fildes(2);
+ draw = load Draw Draw->PATH;
+ math = load Math Math->PATH;
+ tk = load Tk Tk->PATH;
+ tkclient = load Tkclient Tkclient->PATH;
+ if (tkclient == nil)
+ nomod(Tkclient->PATH);
+ tkclient->init();
+ cliquefd = sys->fildes(0);
+ Ballexit = ref Ballstate;
+ Noobs = Obstacle.new(-1);
+ lines = tl lines; # XXX ahem.
+
+ if (len argv >= 3) # argv: modname mnt dir ...
+ membername = readfile(hd tl argv + "/name");
+
+ sys->pctl(Sys->NEWPGRP, nil);
+ wmctl: chan of string;
+ (win, wmctl) = tkclient->toplevel(ctxt.screen, nil, "Bounce", 0);
+ ucmd := chan of string;
+ tk->namechan(win, ucmd, "ucmd");
+ mouse := chan of string;
+ tk->namechan(win, mouse, "mouse");
+ for (i := 0; i < len cliquecmds; i++)
+ cmd(win, cliquecmds[i]);
+ cmd(win, ".c configure -width 500 -height 500");
+ cmd(win, ".c configure -width [.c cget -actwidth] -height [.c cget -actheight]");
+ imageinit();
+
+ mch := chan of (int, Point);
+
+ spawn mouseproc(mch);
+ mkball = chan of (int, chan of chan of ref Ballstate);
+ spawn monitor(mkball);
+ balls: list of chan of ref Ballstate;
+
+ spawn updateproc();
+ sys->sleep(500); # wait for things to calm down a little
+ cliquecmd("time " + string sys->millisec());
+
+ buts := 0;
+ for (;;) alt {
+ c := <-wmctl =>
+ if (c == "exit")
+ sys->write(cliquefd, array[0] of byte, 0);
+ tkclient->wmctl(win, c);
+ c := <-mouse =>
+ (nil, toks) := sys->tokenize(c, " ");
+ if ((hd toks)[0] == '1')
+ buts |= int hd tl toks;
+ else
+ buts &= ~int hd tl toks;
+ mch <-= (buts, Point(int hd tl tl toks, int hd tl tl tl toks));
+ c := <-ucmd =>
+ cliquecmd(c);
+ }
+}
+
+cliquecmd(s: string): int
+{
+ if (sys->fprint(cliquefd, "%s\n", s) == -1) {
+ err := sys->sprint("%r");
+ notify(err);
+ sys->print("bounce: cmd error on '%s': %s\n", s, err);
+ return 0;
+ }
+ return 1;
+}
+
+updateproc()
+{
+ wfd := sys->open("/prog/" + string sys->pctl(0, nil) + "/wait", Sys->OREAD);
+ spawn updateproc1();
+ buf := array[Sys->ATOMICIO] of byte;
+ n := sys->read(wfd, buf, len buf);
+ sys->print("updateproc process exited: %s\n", string buf[0:n]);
+}
+
+updateproc1()
+{
+ buf := array[Sys->ATOMICIO] of byte;
+ while ((n := sys->read(cliquefd, buf, len buf)) > 0) {
+ (nil, lines) := sys->tokenize(string buf[0:n], "\n");
+ for (; lines != nil; lines = tl lines)
+ applyupdate(hd lines);
+ cmd(win, "update");
+ }
+ if (n < 0)
+ sys->fprint(stderr, "bounce: error reading updates: %r\n");
+ sys->fprint(stderr, "bounce: updateproc exiting\n");
+}
+
+UNKNOWN, BALL, OBSTACLE: con iota;
+
+applyupdate(s: string)
+{
+# sys->print("bounce: got update %s\n", s);
+ (nt, toks) := sys->tokenize(s, " ");
+ case hd toks {
+ "create" =>
+ # create id parentid vis type
+ id := int hd tl toks;
+ if (id >= len objects) {
+ newobjects := array[id + 10] of ref Object;
+ newobjects[0:] = objects;
+ objects = newobjects;
+ }
+ objects[id] = ref Object;
+ "del" =>
+ # del parent start end objid...
+ for (toks = tl tl tl tl toks; toks != nil; toks = tl toks) {
+ id := int hd toks;
+ if (objects[id].obstacle != nil)
+ sys->fprint(stderr, "bounce: cannot delete obstructions yet\n");
+ else
+ objects[id].ballctl <-= Ballexit;
+ objects[id] = nil;
+ }
+ "set" =>
+ # set obj attr val
+ id := int hd tl toks;
+ attr := hd tl tl toks;
+ val := tl tl tl toks;
+ case attr {
+ "state" =>
+ # state lasthit owner p.x p.y v.x v.y s time
+ state := ref Ballstate;
+ (state.hitobs, val) = (srvobj(int hd val), tl val);
+ (state.owner, val) = (int hd val, tl val);
+ (state.p.x, val) = (real hd val, tl val);
+ (state.p.y, val) = (real hd val, tl val);
+ (state.v.x, val) = (real hd val, tl val);
+ (state.v.y, val) = (real hd val, tl val);
+ (state.speed, val) = (real hd val, tl val);
+ (state.t0, val) = (int hd val, tl val);
+ if (objects[id].ballctl == nil)
+ objects[id].ballctl = makeball(id, state);
+ else
+ objects[id].ballctl <-= state;
+ "pos" or "coords" or "owner" or "id" =>
+ if (objects[id].obstacle == nil)
+ objects[id].obstacle = Obstacle.new(id);
+ o := objects[id].obstacle;
+ case attr {
+ "pos" =>
+ (o.s1, val) = (real hd val, tl val);
+ (o.s2, val) = (real hd val, tl val);
+ o.isbat = 1;
+ "coords" =>
+ p1, p2: Point;
+ (p1.x, val) = (int hd val, tl val);
+ (p1.y, val) = (int hd val, tl val);
+ (p2.x, val) = (int hd val, tl val);
+ (p2.y, val) = (int hd val, tl val);
+ o.line = Line.new(p1, p2);
+ "owner" =>
+ o.owner = hd val;
+ if (o.owner == membername)
+ bats = o :: bats;
+ "id" =>
+ o.srvid = int hd val;
+ if (o.srvid >= len srvobjects) {
+ newobjects := array[id + 10] of ref Obstacle;
+ newobjects[0:] = srvobjects;
+ srvobjects = newobjects;
+ }
+ srvobjects[o.srvid] = o;
+ }
+ if (currentlydragging != id)
+ o.config();
+ "arenasize" =>
+ # arenasize w h
+ cmd(win, ".c configure -width " + hd val + " -height " + hd tl val);
+ * =>
+ if (len attr > 5 && attr[0:5] == "score") {
+ # scoreN val
+ n := int attr[5:];
+ w := ".f." + string n;
+ if (!tkexists(w)) {
+ cmd(win, "label " + w + "l -text '" + attr);
+ cmd(win, "label " + w + " -relief sunken -bd 5 -width 5w");
+ cmd(win, "pack " +w + "l " + w + " -side left");
+ }
+ cmd(win, w + " configure -text {" + hd val + "}");
+ } else if (len attr > 6 && attr[0:6] == "member") {
+ # memberN id colour
+ n := int attr[6:];
+ if (n >= len members) {
+ newmembers := array[n + 1] of ref Member;
+ newmembers[0:] = members;
+ members = newmembers;
+ }
+ p := members[n] = ref Member(int hd val, hd tl val);
+ cmd(win, ".c itemconfigure o" + string p.id + " -fill " + p.colour);
+ if (p.id == memberid)
+ myturn = n;
+ }
+ else
+ sys->fprint(stderr, "bounce: unknown attr '%s'\n", attr);
+ }
+ "time" =>
+ # time offset orig
+ now := sys->millisec();
+ time := int hd tl tl toks;
+ transit := now - time;
+ timeoffset = int hd tl toks - transit / 2;
+ sys->print("transit time %d, timeoffset: %d\n", transit, timeoffset);
+ * =>
+ sys->fprint(stderr, "chat: unknown update message '%s'\n", s);
+ }
+}
+
+tkexists(w: string): int
+{
+ return tk->cmd(win, w + " cget -bd")[0] != '!';
+}
+
+srvobj(id: int): ref Obstacle
+{
+ if (id < 0 || id >= len srvobjects || srvobjects[id] == nil)
+ return Noobs;
+ return srvobjects[id];
+}
+
+mouseproc(mch: chan of (int, Point))
+{
+ procname("mouse");
+ for (;;) {
+ hitbat: ref Obstacle = nil;
+ minperp, hitdist: real;
+ (buts, p) := <-mch;
+ for (bl := bats; bl != nil; bl = tl bl) {
+ b := hd bl;
+ (normal, perp, dist) := b.line.hittest(p);
+ perp = abs(perp);
+
+ if ((hitbat == nil || perp < minperp) && (dist >= b.s1 && dist <= b.s2))
+ (hitbat, minperp, hitdist) = (b, perp, dist);
+ }
+ if (hitbat == nil || minperp > 30.0) {
+ while ((<-mch).t0)
+ ;
+ continue;
+ }
+ offset := hitdist - hitbat.s1;
+ if (buts & 2)
+ (buts, p) = aim(mch, hitbat, p);
+ if (buts & 1)
+ drag(mch, hitbat, offset);
+ }
+}
+
+
+drag(mch: chan of (int, Point), hitbat: ref Obstacle, offset: real)
+{
+ realtosrv := chan of string;
+ dummytosrv := chan of string;
+ tosrv := dummytosrv;
+ currevent := "";
+
+ currentlydragging = hitbat.id;
+
+ line := hitbat.line;
+ batlen := hitbat.s2 - hitbat.s1;
+
+ cvsorigin := Point(int cmd(win, ".c cget -actx"), int cmd(win, ".c cget -acty"));
+ spawn sendproc(realtosrv);
+
+ cmd(win, "grab set .c");
+ cmd(win, "focus .");
+loop: for (;;) alt {
+ tosrv <-= currevent =>
+ tosrv = dummytosrv;
+
+ (buts, p) := <-mch =>
+ if (buts & 2)
+ (buts, p) = aim(mch, hitbat, p);
+ (v, perp, dist) := line.hittest(p);
+ dist -= offset;
+ # constrain bat and mouse positions
+ if (dist < 0.0 || dist + batlen > line.s) {
+ if (dist < 0.0) {
+ p = line.point(offset);
+ dist = 1.0;
+ } else {
+ p = line.point(line.s - batlen + offset);
+ dist = line.s - batlen;
+ }
+ p.x -= int (v.x * perp);
+ p.y -= int (v.y * perp);
+ win.image.display.cursorset(p.add(cvsorigin));
+ }
+ (hitbat.s1, hitbat.s2) = (dist, dist + batlen);
+ hitbat.config();
+ cmd(win, "update");
+ currevent = "bat " + string hitbat.s1;
+ tosrv = realtosrv;
+ if (!buts)
+ break loop;
+ }
+ cmd(win, "grab release .c");
+ realtosrv <-= nil;
+ currentlydragging = -1;
+}
+
+CHARGETIME: con 1000.0;
+MAXCHARGE: con 50.0;
+
+α: con 0.999; # decay in one millisecond
+D: con 5;
+aim(mch: chan of (int, Point), hitbat: ref Obstacle, p: Point): (int, Point)
+{
+ cvsorigin := Point(int cmd(win, ".c cget -actx"), int cmd(win, ".c cget -acty"));
+ startms := ms := sys->millisec();
+ δ := Realpoint(0.0, 0.0);
+ line := hitbat.line;
+ charge := 0.0;
+ pivot := line.point((hitbat.s1 + hitbat.s2) / 2.0);
+ s1 := p2s(line.point(hitbat.s1));
+ s2 := p2s(line.point(hitbat.s2));
+ cmd(win, ".c create line 0 0 0 0 -tags wire -fill yellow");
+ ballid := makeballitem(-1, myturn);
+ bp, p2: Point;
+ buts := 2;
+ for (;;) {
+ v := makeunit(δ);
+ bp = pivot.add((int (v.x * charge), int (v.y * charge)));
+ cmd(win, ".c coords wire "+s1+" "+p2s(bp)+" "+s2);
+ ballmove(ballid, bp);
+ cmd(win, "update");
+ if ((buts & 2) == 0)
+ break;
+ (buts, p2) = <-mch;
+ now := sys->millisec();
+ fade := math->pow(α, real (now - ms));
+ charge = real (now - startms) * (MAXCHARGE / CHARGETIME);
+ if (charge > MAXCHARGE)
+ charge = MAXCHARGE;
+ ms = now;
+ dp := p2.sub(p);
+ δ.x = δ.x * fade + real dp.x;
+ δ.y = δ.y * fade + real dp.y;
+ mag := δ.x * δ.x + δ.y * δ.y;
+ if (dp.x != 0 || dp.y != 0)
+ win.image.display.cursorset(p.add(cvsorigin));
+ }
+ cmd(win, ".c delete wire " + ballid);
+ cmd(win, "update");
+ (δ.x, δ.y) = (-δ.x, -δ.y);
+ cliquecmd("newball " + string hitbat.id + " " +
+ p2s(bp) + " " + rp2s(makeunit(δ)) + " " + string (charge / 100.0));
+ return (buts, p2);
+}
+
+makeunit(v: Realpoint): Realpoint
+{
+ mag := math->sqrt(v.x * v.x + v.y * v.y);
+ if (mag < ZERO)
+ return (1.0, 0.0);
+ return (v.x / mag, v.y / mag);
+}
+
+sendproc(tosrv: chan of string)
+{
+ procname("send");
+ while ((ev := <-tosrv) != nil)
+ cliquecmd(ev);
+}
+
+makeball(id: int, state: ref Ballstate): chan of ref Ballstate
+{
+ mkballreply := chan of chan of ref Ballstate;
+ mkball <-= (id, mkballreply);
+ ballctl := <-mkballreply;
+ ballctl <-= state;
+ return ballctl;
+}
+
+blankobstacle: Obstacle;
+Obstacle.new(id: int): ref Obstacle
+{
+ cmd(win, ".c create line 0 0 0 0 -width 3 -fill #aaaaaa" + " -tags l" + string id);
+ o := ref blankobstacle;
+ o.line = Line.new((0, 0), (0, 0));
+ o.id = id;
+ o.owner = -1;
+ o.srvid = -1;
+ lineversion++;
+ lines = o :: lines;
+ return o;
+}
+
+Obstacle.config(o: self ref Obstacle)
+{
+ if (o.isbat) {
+ cmd(win, ".c coords l" + string o.id + " " +
+ p2s(o.line.point(o.s1)) + " " + p2s(o.line.point(o.s2)));
+ if (o.owner == memberid)
+ cmd(win, ".c itemconfigure l" + string o.id + " -fill red");
+ else
+ cmd(win, ".c itemconfigure l" + string o.id + " -fill white");
+ } else {
+ cmd(win, ".c coords l" + string o.id + " " +
+ p2s(o.line.point(0.0)) + " " + p2s(o.line.point(o.line.s)));
+ }
+}
+
+# make sure cpu time is handed to all ball processes fairly
+# by passing a "token" around to each process in turn.
+# each process does its work when it *hasn't* got its
+# token but it can't go through two iterations without
+# waiting its turn.
+#
+# new processes are created by sending on mkball.
+# the channel sent back can be used to control the position
+# and velocity of the ball and to destroy it.
+monitor(mkball: chan of (int, chan of chan of ref Ballstate))
+{
+ procname("mon");
+ procl, proc: list of (chan of ref Ballstate, chan of int);
+ rc := dummyrc := chan of int;
+ for (;;) {
+ alt {
+ (id, ch) := <-mkball =>
+ (newc, newrc) := (chan of ref Ballstate, chan of int);
+ procl = (newc, newrc) :: procl;
+ spawn animproc(id, newc, newrc);
+ ch <-= newc;
+ if (tl procl == nil) { # first ball
+ newc <-= nil;
+ rc = newrc;
+ proc = procl;
+ }
+ alive := <-rc => # got token.
+ if (!alive) {
+ # ball has exited: remove from list
+ newprocl: list of (chan of ref Ballstate, chan of int);
+ for (; procl != nil; procl = tl procl)
+ if ((hd procl).t1 != rc)
+ newprocl = hd procl :: newprocl;
+ procl = newprocl;
+ }
+ if ((proc = tl proc) == nil)
+ proc = procl;
+ if (proc == nil) {
+ rc = dummyrc;
+ } else {
+ c: chan of ref Ballstate;
+ (c, rc) = hd proc;
+ c <-= nil; # hand token to next process.
+ }
+ }
+ }
+}
+
+# buffer ball state commands, so at least balls we handle
+# locally appear glitch free.
+bufferproc(cmdch: chan of string)
+{
+ procname("buffer");
+ buffer := ref Queue;
+ bufhd: string;
+ dummytosrv := chan of string;
+ realtosrv := chan of string;
+ spawn sendproc(realtosrv);
+ tosrv := dummytosrv;
+ for (;;) alt {
+ tosrv <-= bufhd =>
+ if ((bufhd = buffer.get()) == nil)
+ tosrv = dummytosrv;
+ s := <-cmdch =>
+ if (s == nil) {
+ # ignore other queued requests, as they're
+ # only state changes for a ball that's now been deleted.
+ realtosrv <-= nil;
+ exit;
+ }
+ buffer.put(s);
+ if (tosrv == dummytosrv) {
+ tosrv = realtosrv;
+ bufhd = buffer.get();
+ }
+ }
+}
+start: int;
+# animate one ball. initial position and unit-velocity are
+# given by p and v.
+animproc(id: int, c: chan of ref Ballstate, rc: chan of int)
+{
+ procname("anim");
+ while ((newstate := <-c) == nil)
+ rc <-= 1;
+ state := *newstate;
+ totaldist := 0.0; # distance ball has travelled from reference point to last intersection
+ ballid := makeballitem(id, state.owner);
+ smallcount := 0;
+ version := lineversion;
+ tosrv := chan of string;
+ start := sys->millisec();
+ spawn bufferproc(tosrv);
+loop: for (;;) {
+ hitp: Realpoint;
+
+ dist := 1000000.0;
+ oldobs := state.hitobs;
+ hitt: real;
+ for (l := lines; l != nil; l = tl l) {
+ obs := hd l;
+ (ok, hp, hdist, t) := obs.line.intersection(state.p, state.v);
+ if (ok && hdist < dist && obs != oldobs && (smallcount < 10 || hdist > 1.5)) {
+ (hitp, state.hitobs, dist, hitt) = (hp, obs, hdist, t);
+ }
+ }
+ if (dist > 10000.0) {
+ sys->print("no intersection!\n");
+ state = ballexit(1, ballid, tosrv, c, rc);
+ totaldist = 0.0;
+ continue loop;
+ }
+ if (dist < 0.0001)
+ smallcount++;
+ else
+ smallcount = 0;
+ t0 := int (totaldist / state.speed) + state.t0 - timeoffset;
+ et := t0 + int (dist / state.speed);
+ t := sys->millisec() - t0;
+ dt := et - t0;
+ do {
+ s := real t * state.speed;
+ currp := Realpoint(state.p.x + s * state.v.x, state.p.y + s * state.v.y);
+ ballmove(ballid, (int currp.x, int currp.y));
+ cmd(win, "update");
+ if (lineversion > version) {
+ (state.p, state.hitobs, version) = (currp, oldobs, lineversion);
+ totaldist += s;
+ continue loop;
+ }
+ if ((newstate := <-c) != nil) {
+ if (newstate == Ballexit)
+ ballexit(0, ballid, tosrv, c, rc);
+ state = *newstate;
+ totaldist = 0.0;
+ continue loop;
+ }
+ rc <-= 1;
+ t = sys->millisec() - t0;
+ } while (t < dt);
+ totaldist += dist;
+ state.p = hitp;
+ hitobs := state.hitobs;
+ if (hitobs.isbat) {
+ if (hitobs.owner == memberid) {
+ if (hitt >= hitobs.s1 && hitt <= hitobs.s2)
+ state.v = batboing(hitobs, hitt, state.v);
+ tosrv <-= "state " +
+ string id +
+ " " + string hitobs.srvid +
+ " " + string state.owner +
+ " " + rp2s(state.p) + " " + rp2s(state.v) +
+ " " + string state.speed +
+ " " + string (sys->millisec() + timeoffset);
+ } else {
+ # wait for enlightenment
+ while ((newstate := <-c) == nil)
+ rc <-= 1;
+ if (newstate == Ballexit)
+ ballexit(0, ballid, tosrv, c, rc);
+ state = *newstate;
+ totaldist = 0.0;
+ }
+ } else if (hitobs.owner == memberid) {
+ # if line has an owner but isn't a bat, then it's
+ # a terminating line, so we inform server.
+ cliquecmd("lost " + string id);
+ state = ballexit(1, ballid, tosrv, c, rc);
+ totaldist = 0.0;
+ } else
+ state.v = boing(state.v, hitobs.line);
+ }
+}
+
+#ballmask: ref Image;
+imageinit()
+{
+# displ := win.image.display;
+# ballmask = displ.newimage(((0, 0), (BALLSIZE+1, BALLSIZE+1)), 0, 0, Draw->White);
+# ballmask.draw(ballmask.r, displ.zeros, displ.ones, (0, 0));
+# ballmask.fillellipse((BALLSIZE/2, BALLSIZE/2), BALLSIZE/2, BALLSIZE/2, displ.ones, (0, 0));
+# End: con Draw->Endsquare;
+# n := 5;
+# θ := 0.0;
+# δ := (2.0 * π) / real n;
+# c := Point(BALLSIZE / 2, BALLSIZE / 2).sub((1, 1));
+# r := real (BALLSIZE / 2);
+# for (i := 0; i < n; i++) {
+# p2 := Point(int (r * math->cos(θ)), int (r * math->sin(θ)));
+# sys->print("drawing from %s to %s\n", p2s(c), p2s(p2.add(c)));
+# ballmask.line(c, c.add(p2), End, End, 1, displ.ones, (0, 0));
+# θ += δ;
+# }
+}
+
+makeballitem(id, owner: int): string
+{
+ displ := win.image.display;
+ return cmd(win, ".c create oval 0 0 1 1 -fill " + members[owner].colour +
+ " -tags o" + string owner);
+}
+
+ballmove(ballid: string, p: Point)
+{
+ cmd(win, ".c coords " + ballid +
+ " " + string (p.x - BALLSIZE) +
+ " " + string (p.y - BALLSIZE) +
+ " " + string (p.x + BALLSIZE) +
+ " " + string (p.y + BALLSIZE));
+}
+
+ballexit(wait: int, ballid: string, tosrv: chan of string, c: chan of ref Ballstate, rc: chan of int): Ballstate
+{
+ if (wait) {
+ while ((s := <-c) != Ballexit)
+ if (s == nil)
+ rc <-= 1;
+ else
+ return *s; # maybe we're not exiting, after all...
+ }
+ cmd(win, ".c delete " + ballid + ";update");
+# cmd(win, "image delete " + ballid);
+ tosrv <-= nil;
+ <-c;
+ rc <-= 0; # inform monitor that we've gone
+ exit;
+}
+
+# thread-safe access to the Rand module
+randgenproc(ch: chan of int)
+{
+ procname("rand");
+ rand := load Rand Rand->PATH;
+ for (;;)
+ ch <-= rand->rand(16r7fffffff);
+}
+
+abs(x: real): real
+{
+ if (x < 0.0)
+ return -x;
+ return x;
+}
+
+# bounce ball travelling in direction av off line b.
+# return the new unit vector.
+boing(av: Realpoint, b: ref Line): Realpoint
+{
+ d := math->atan2(b.v.y, b.v.x) * 2.0 - math->atan2(av.y, av.x);
+ return (math->cos(d), math->sin(d));
+}
+
+# calculate how a bounce vector should be modified when
+# hitting a bat. t gives the intersection point on the bat;
+# ballv is the ball's vector.
+batboing(bat: ref Obstacle, t: real, ballv: Realpoint): Realpoint
+{
+ ballθ := math->atan2(ballv.y, ballv.x);
+ batθ := math->atan2(bat.line.v.y, bat.line.v.x);
+ φ := ballθ - batθ;
+ δ: real;
+ t -= bat.s1;
+ batlen := bat.s2 - bat.s1;
+ if (math->sin(φ) > 0.0)
+ δ = (t / batlen) * Maxδ * 2.0 - Maxδ;
+ else
+ δ = (t / batlen) * -Maxδ * 2.0 + Maxδ;
+ θ := math->atan2(bat.line.v.y, bat.line.v.x) * 2.0 - ballθ; # boing
+ θ += δ;
+ return (math->cos(θ), math->sin(θ));
+}
+
+Line.new(p1, p2: Point): ref Line
+{
+ ln := ref Line;
+ ln.p = (real p1.x, real p1.y);
+ v := Realpoint(real (p2.x - p1.x), real (p2.y - p1.y));
+ ln.s = math->sqrt(v.x * v.x + v.y * v.y);
+ if (ln.s > ZERO)
+ ln.v = (v.x / ln.s, v.y / ln.s);
+ else
+ ln.v = (1.0, 0.0);
+ return ln;
+}
+
+# return normal from line, perpendicular distance from line and distance down line
+Line.hittest(l: self ref Line, ip: Point): (Realpoint, real, real)
+{
+ p := Realpoint(real ip.x, real ip.y);
+ v := Realpoint(-l.v.y, l.v.x);
+ (nil, nil, perp, ldist) := l.intersection(p, v);
+ return (v, perp, ldist);
+}
+
+Line.point(l: self ref Line, s: real): Point
+{
+ return (int (l.p.x + s * l.v.x), int (l.p.y + s * l.v.y));
+}
+
+# compute the intersection of lines a and b.
+# b is assumed to be fixed, and a is indefinitely long
+# but doesn't extend backwards from its starting point.
+# a is defined by the starting point p and the unit vector v.
+# return whether it hit, the point at which it hit if so,
+# the distance of the intersection point from p,
+# and the distance of the intersection point from b.p.
+Line.intersection(b: self ref Line, p, v: Realpoint): (int, Realpoint, real, real)
+{
+ det := b.v.x * v.y - v.x * b.v.y;
+ if (det > -ZERO && det < ZERO)
+ return (0, (0.0, 0.0), 0.0, 0.0);
+
+ y21 := b.p.y - p.y;
+ x21 := b.p.x - p.x;
+ s := (b.v.x * y21 - b.v.y * x21) / det;
+ t := (v.x * y21 - v.y * x21) / det;
+ if (s < 0.0)
+ return (0, (0.0, 0.0), s, t);
+ hit := t >= 0.0 && t <= b.s;
+ hp: Realpoint;
+ if (hit)
+ hp = (p.x+v.x*s, p.y+v.y*s);
+ return (hit, hp, s, t);
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+ e := tk->cmd(top, s);
+ if (e != nil && e[0] == '!')
+ sys->print("tk error %s on '%s'\n", e, s);
+ return e;
+}
+
+state2s(s: ref Ballstate): string
+{
+ return sys->sprint("[hitobs:%d(id %d), t0: %d, p: %g %g; v: %g %g; s: %g",
+ s.hitobs.srvid, s.hitobs.id, s.t0, s.p.x, s.p.y, s.v.x, s.v.y, s.speed);
+}
+
+l2s(l: ref Line): string
+{
+ return p2s(l.point(0.0)) + " " + p2s(l.point(l.s));
+}
+
+rp2s(rp: Realpoint): string
+{
+ return string rp.x + " " + string rp.y;
+}
+
+
+p2s(p: Point): string
+{
+ return string p.x + " " + string p.y;
+}
+
+notifypid := -1;
+notify(s: string)
+{
+ kill(notifypid);
+ sync := chan of int;
+ spawn notifyproc(s, sync);
+ notifypid = <-sync;
+}
+
+notifyproc(s: string, sync: chan of int)
+{
+ procname("notify");
+ sync <-= sys->pctl(0, nil);
+ cmd(win, ".c delete notify");
+ id := cmd(win, ".c create text 0 0 -anchor nw -fill red -tags notify -text '" + s);
+ bbox := cmd(win, ".c bbox " + id);
+ cmd(win, ".c create rectangle " + bbox + " -fill #ffffaa -tags notify");
+ cmd(win, ".c raise " + id);
+ cmd(win, "update");
+ sys->sleep(750);
+ cmd(win, ".c delete notify");
+ cmd(win, "update");
+ notifypid = -1;
+}
+
+kill(pid: int)
+{
+ if ((fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
+ sys->write(fd, array of byte "kill", 4);
+}
+
+T: type string;
+
+Queue.put(q: self ref Queue, s: T)
+{
+ q.t = s :: q.t;
+}
+
+Queue.get(q: self ref Queue): T
+{
+ s: T;
+ if(q.h == nil){
+ q.h = revlist(q.t);
+ q.t = nil;
+ }
+ if(q.h != nil){
+ s = hd q.h;
+ q.h = tl q.h;
+ }
+ return s;
+}
+
+revlist(ls: list of T) : list of T
+{
+ rs: list of T;
+ for (; ls != nil; ls = tl ls)
+ rs = hd ls :: rs;
+ return rs;
+}
+
+procname(s: string)
+{
+# sys->procname(sys->procname(nil) + " " + s);
+}
+
diff --git a/appl/spree/clients/cards.b b/appl/spree/clients/cards.b
new file mode 100644
index 00000000..17601bd7
--- /dev/null
+++ b/appl/spree/clients/cards.b
@@ -0,0 +1,2220 @@
+implement Cards;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+ draw: Draw;
+ Point, Rect, Display, Image, Font: import draw;
+include "tk.m";
+ tk: Tk;
+include "tkclient.m";
+ tkclient: Tkclient;
+include "math.m";
+ math: Math;
+
+Cards: module {
+ init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+# fairly general card clique client.
+# inherent restrictions:
+# no dragging of cards visible over the net; it's unclear how
+# to handle the coordinate spaces involved
+
+Object: adt {
+ id: int;
+ pick {
+ Card =>
+ parentid: int;
+ face: int; # 1 is face up
+ number: int;
+ rear: int;
+ Member =>
+ cid: int;
+ name: string;
+ Stack =>
+ o: ref Layobject.Stack;
+ Widget =>
+ o: ref Layobject.Widget;
+ Menuentry =>
+ parentid: int;
+ text: string;
+ Layoutframe =>
+ lay: ref Layout.Frame;
+ Layoutobj =>
+ lay: ref Layout.Obj;
+ Scoretable =>
+ scores: array of ref Object.Score;
+ Score =>
+ row: array of (int, string);
+ height: int;
+ Button =>
+ Other =>
+ }
+};
+
+# specify how an object is laid out.
+Layout: adt {
+ id: int;
+ parentid: int;
+ opts: string; # pack options
+ orientation: int;
+ pick {
+ Frame =>
+ lays: cyclic array of ref Layout;
+ Obj =>
+ layid: int; # reference to layid of laid-out object
+ }
+};
+
+# an object which can be laid out on the canvas
+Layobject: adt {
+ id: int;
+ parentid: int;
+ w: string;
+ size: Point;
+ needrepack: int;
+ orientation: int;
+ layid: int;
+ pick {
+ Stack =>
+ style: int;
+ cards: array of ref Object.Card; # fake objects when invisible
+ pos: Point; # top-left origin of first card in stack
+ delta: Point; # card offset delta.
+ animq: ref Queue; # queue of pending animations.
+ actions: int;
+ maxcards: int;
+ title: string;
+ visible: int;
+ n: int; # for concealed stacks, n cards in stack.
+ ownerid: int; # owner of selection
+ sel: ref Selection;
+ showsize,
+ hassize: int;
+ Widget =>
+ wtype: string;
+ entries: array of ref Object.Menuentry;
+ cmd: string; # only used for entry widgets
+ width: int;
+ }
+};
+
+Animation: adt {
+ tag: string; # canvas tag common to cards being moved.
+ srcpt: Point; # where cards are coming from.
+ cards: array of ref Object.Card; # objects being transferred.
+ dstid: int;
+ index: int;
+ waitch: chan of ref Animation; # notification comes on this chan when finished.
+};
+
+Selection: adt {
+ pick {
+ XRange =>
+ r: Range;
+ Indexes =>
+ idxl: list of int;
+ Empty =>
+ }
+};
+
+MAXPLAYERS: con 4;
+
+# layout actions
+lFRAME, lOBJECT: con iota;
+
+# possible actions on a card on a stack.
+aCLICK: con 1<<iota;
+
+# styles of stack display
+styDISPLAY, styPILE: con iota;
+
+# orientations
+oLEFT, oRIGHT, oUP, oDOWN: con iota;
+
+Range: adt {
+ start, end: int;
+};
+
+T: type ref Animation;
+Queue: adt {
+ h, t: list of T;
+ put: fn(q: self ref Queue, s: T);
+ get: fn(q: self ref Queue): T;
+ isempty: fn(q: self ref Queue): int;
+ peek: fn(q: self ref Queue): T;
+};
+
+configcmds := array[] of {
+"frame .buts",
+"frame .cf",
+"canvas .c -width 400 -height 450 -bg green",
+"label .status -text 0",
+"checkbutton .buts.scores -text {Show scores} -command {send cmd scores}",
+"button .buts.sizetofit -text {Fit} -command {send cmd sizetofit}",
+"checkbutton .buts.debug -text {Debug} -variable debug -command {send cmd debug}",
+"pack .buts.sizetofit .buts.debug .status -in .buts -side left",
+"pack .buts -side top -fill x",
+"pack .c -in .cf -side top -fill both -expand 1",
+"pack .cf -side top -fill both -expand 1",
+"bind .c <Button-1> {send cmd b1 %X %Y}",
+"bind .c <ButtonRelease-1} {send cmd b1r %X %Y}",
+"bind .c <Button-2> {send cmd b2 %X %Y}",
+"bind .c <ButtonRelease-2> {send cmd b2r %X %Y}",
+"bind .c <ButtonPress-3> {send cmd b3 %X %Y}",
+"bind .c <ButtonRelease-3> {send cmd b3r %X %Y}",
+"bind . <Configure> {send cmd config}",
+"pack propagate .buts 0",
+".status configure -text {}",
+"pack propagate . 0",
+};
+
+objects: array of ref Object;
+layobjects := array[20] of list of ref Layobject;
+members := array[8] of list of ref Object.Member;
+win: ref Tk->Toplevel;
+drawctxt: ref Draw->Context;
+me: ref Object.Member;
+layout: ref Layout;
+scoretable: ref Object.Scoretable;
+showingscores := 0;
+debugging := 0;
+
+stderr: ref Sys->FD;
+animfinishedch: chan of (ref Animation, chan of chan of ref Animation);
+yieldch: chan of int;
+cardlockch: chan of int;
+notifych: chan of string;
+tickregisterch, tickunregisterch: chan of chan of int;
+starttime := 0;
+cvsfont: ref Font;
+
+packwin: ref Tk->Toplevel; # invisible; used to steal tk's packing algorithms...
+packobjs: list of ref Layobject;
+repackobjs: list of ref Layobject;
+needresize := 0;
+needrepack := 0;
+
+animid := 0;
+fakeid := -2; # ids allocated to "fake" cards in private hands; descending
+nimages := 0;
+Hiddenpos := Point(5000, 5000);
+
+cliquefd: ref Sys->FD;
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+ sys = load Sys Sys->PATH;
+ stderr = sys->fildes(2);
+ draw = load Draw Draw->PATH;
+ tk = load Tk Tk->PATH;
+ math = load Math Math->PATH;
+ tkclient = load Tkclient Tkclient->PATH;
+ if (tkclient == nil) {
+ sys->fprint(stderr, "cards: cannot load %s: %r\n", Tkclient->PATH);
+ raise "fail:bad module";
+ }
+ tkclient->init();
+ drawctxt = ctxt;
+ client1();
+}
+
+# maximum number of rears (overridden by actual rear images)
+rearcolours := array[] of {
+ int 16r0000ccff,
+ int 16rff0000ff,
+ int 16rffff00ff,
+ int 16r008000ff,
+ int 16rffffffff,
+ int 16rffaa00ff,
+ int 16r00ffffff,
+ int 16r808080ff,
+ int 16r00ff00ff,
+ int 16r800000ff,
+ int 16r800080ff,
+};
+Rearborder := 3;
+Border := 6;
+Selectborder := 3;
+cardsize: Point;
+carddelta := Point(12, 15); # offset in order to see card number/suit
+Selectcolour := "red";
+Textfont := "/fonts/pelm/unicode.8.font";
+
+client1()
+{
+ cliquefd = sys->fildes(0);
+ if (readconfig() == -1)
+ raise "fail:error";
+
+ winctl: chan of string;
+ (win, winctl) = tkclient->toplevel(drawctxt, "-font " + Textfont,
+ "Cards", Tkclient->Appl);
+ cmd(win, ". unmap");
+ bcmd := chan of string;
+ tk->namechan(win, bcmd, "cmd");
+ srvcmd := chan of string;
+ tk->namechan(win, srvcmd, "srv");
+
+ if (readcardimages() == -1)
+ raise "fail:error";
+ for (i := 0; i < len configcmds; i++)
+ cmd(win, configcmds[i]);
+ tkclient->onscreen(win, nil);
+ tkclient->startinput(win, "kbd"::"ptr"::nil);
+
+ fontname := cmd(win, ".c cget -font");
+ cvsfont = Font.open(drawctxt.display, fontname);
+ if (cvsfont == nil) {
+ sys->fprint(stderr, "cards: cannot open font %s: %r\n", fontname);
+ raise "fail:error";
+ }
+ fontname = nil;
+
+ cardlockch = chan of int;
+ spawn lockproc();
+
+ yieldch = chan of int;
+ spawn yieldproc();
+
+ notifych = chan of string;
+ spawn notifierproc();
+
+ updatech := chan of array of byte;
+ spawn readproc(cliquefd, updatech);
+
+ spawn updateproc(updatech);
+ b1down := 0;
+
+ tickregisterch = chan of chan of int;
+ tickunregisterch = chan of chan of int;
+ spawn timeproc();
+ spawn eventproc(win);
+
+ for (;;) alt {
+ c := <-bcmd =>
+ (n, toks) := sys->tokenize(c, " ");
+ case hd toks {
+ "b3" =>
+ curp := Point(int cmd(win, ".c canvasx " + hd tl toks),
+ int cmd(win, ".c canvasy " + hd tl tl toks));
+ b3raise(bcmd, curp);
+ "b2" =>
+ curp := Point(int cmd(win, ".c canvasx " + hd tl toks),
+ int cmd(win, ".c canvasy " + hd tl tl toks));
+ dopan(bcmd, "b2", curp);
+ "b1" =>
+ if (!b1down) {
+ # b1 x y
+ # x and y in screen coords
+ curp := Point(int cmd(win, ".c canvasx " + hd tl toks),
+ int cmd(win, ".c canvasy " + hd tl tl toks));
+ b1down = b1action(bcmd, curp);
+ }
+ "b1r" =>
+ b1down = 0;
+ "entry" =>
+ id := int hd tl toks;
+ lock();
+ cc := "";
+ pick o := objects[id] {
+ Widget =>
+ cc = o.o.cmd;
+ * =>
+ sys->print("entry message from unknown obj: id %d\n", id);
+ }
+ unlock();
+ if (cc != nil) {
+ w := ".buts." + string id + ".b";
+ s := cmd(win, w + " get");
+ cardscmd(cc + " " + s);
+ cmd(win, w + " selection range 0 end");
+ cmd(win, "update");
+ }
+ "config" =>
+ lock();
+ needresize = 1;
+ updatearena();
+ unlock();
+ cmd(win, "update");
+ "scores" =>
+ if (scoretable == nil)
+ break;
+ if (!showingscores) {
+ cmd(win, ".c move score " + string -Hiddenpos.x + " " + string -Hiddenpos.y);
+ cmd(win, ".c raise score");
+ } else
+ cmd(win, ".c move score " + p2s(Hiddenpos));
+ cmd(win, "update");
+ showingscores = !showingscores;
+ "sizetofit" =>
+ lock();
+ sizetofit();
+ unlock();
+ cmd(win, "update");
+ "debug" =>
+ debugging = int cmd(win, "variable debug");
+ }
+ c := <-srvcmd => # from button or menu entry
+ cardscmd(c);
+ s := <-win.ctxt.ctl or
+ s = <-win.wreq or
+ s = <-winctl =>
+ if (s == "exit")
+ sys->write(cliquefd, array[0] of byte, 0);
+ tkclient->wmctl(win, s);
+ }
+}
+
+eventproc(win: ref Tk->Toplevel)
+{
+ for(;;)alt{
+ s := <-win.ctxt.kbd =>
+ tk->keyboard(win, s);
+ s := <-win.ctxt.ptr =>
+ tk->pointer(win, *s);
+ }
+}
+
+readproc(fd: ref Sys->FD, updatech: chan of array of byte)
+{
+ buf := rest := array[Sys->ATOMICIO * 2] of byte;
+ while ((n := sys->read(fd, rest, Sys->ATOMICIO)) > 0) {
+ updatech <-= rest[0:n];
+ rest = rest[n:];
+ if (len rest < Sys->ATOMICIO)
+ buf = rest = array[Sys->ATOMICIO * 2] of byte;
+ }
+ updatech <-= nil;
+}
+
+
+b1action(bcmd: chan of string, p: Point): int
+{
+ (hitsomething, id) := hitcard(p);
+ if (!hitsomething) {
+ dopan(bcmd, "b1", p);
+ return 0;
+ }
+ if (id < 0) { # either error, or someone else's private card
+ sys->print("no card hit (%d)\n", id);
+ return 1;
+ }
+ lock();
+ if (objects[id] == nil) {
+ notify("it's gone");
+ unlock();
+ return 1;
+ }
+ stack: ref Layobject.Stack;
+ index := -1;
+ pick o := objects[id] {
+ Card =>
+ card := o;
+ parentid := card.parentid;
+ stack = stackobj(parentid);
+ for (index = 0; index < len stack.cards; index++)
+ if (stack.cards[index] == card)
+ break;
+ if (index == len stack.cards)
+ index = -1;
+ Stack =>
+ stack = o.o;
+ * =>
+ unlock();
+ return 1;
+ }
+ actions := stack.actions;
+ stackid := stack.id;
+ unlock();
+ # XXX potential problems when object ids get reused.
+ # the object id that we saw before the unlock()
+ # might now refer to a different object, so the user
+ # might be performing a different action to the one intended.
+ # this should be changed throughout... hmm.
+ if (actions == 0) {
+ notify("no way josé");
+ sys->print("no way: stack %d, actions %d\n", stackid, actions);
+ return 1;
+ }
+ cardscmd("click " + string stackid + " " + string index);
+ return 1;
+}
+
+dopan(bcmd: chan of string, b: string, p: Point)
+{
+ r := b + "r";
+ for (;;) {
+ (n, toks) := sys->tokenize(<-bcmd, " ");
+ if (hd toks == b) {
+ pan(p, (int hd tl toks, int hd tl tl toks));
+ p = Point(int cmd(win, ".c canvasx " + hd tl toks),
+ int cmd(win, ".c canvasy " + hd tl tl toks));
+ cmd(win, "update");
+ } else if (hd toks == r)
+ return;
+ }
+}
+
+b3raise(bcmd: chan of string, p: Point)
+{
+ currcard := -1;
+ above := "";
+loop: for (;;) {
+ (nil, id) := hitcard(p);
+ if (id != currcard) {
+ if (currcard != -1 && above != nil)
+ cmd(win, ".c lower i" + string currcard + " " + above);
+ if (id == -1 || tagof(objects[id]) != tagof(Object.Card)) {
+ above = nil;
+ currcard = -1;
+ } else {
+ above = cmd(win, ".c find above i" + string id);
+ cmd(win, ".c raise i" + string id);
+ cmd(win, "update");
+ currcard = id;
+ }
+ }
+ (nil, toks) := sys->tokenize(<-bcmd, " ");
+ case hd toks {
+ "b3" =>
+ p = Point(int cmd(win, ".c canvasx " + hd tl toks),
+ int cmd(win, ".c canvasy " + hd tl tl toks));
+ "b3r" =>
+ break loop;
+ }
+ }
+ if (currcard != -1 && above != nil) {
+ cmd(win, ".c lower i" + string currcard + " " + above);
+ cmd(win, "update");
+ }
+}
+
+hitcard(p: Point): (int, int)
+{
+ (nil, hitids) := sys->tokenize(cmd(win, ".c find overlapping " + r2s((p, p))), " ");
+ if (hitids == nil)
+ return (0, -1);
+ ids: list of string;
+ for (; hitids != nil; hitids = tl hitids)
+ ids = hd hitids :: ids;
+ for (; ids != nil; ids = tl ids) {
+ (nil, tags) := sys->tokenize(cmd(win, ".c gettags " + hd ids), " ");
+ for (; tags != nil; tags = tl tags) {
+ tag := hd tags;
+ if (tag[0] == 'i' || tag[0] == 'r' || tag[0] == 'n' || tag[0] == 'N')
+ return (1, int (hd tags)[1:]);
+ if (tag[0] == 's') # ignore selection
+ break;
+ }
+ if (tags == nil)
+ break;
+ }
+ return (1, -1);
+}
+
+cardscmd(s: string): int
+{
+ if (debugging)
+ sys->print("cmd: %s\n", s);
+ if (sys->fprint(cliquefd, "%s", s) == -1) {
+ err := sys->sprint("%r");
+ notify(err);
+ sys->print("cmd error on '%s': %s\n", s, err);
+ return 0;
+ }
+ return 1;
+}
+
+updateproc(updatech: chan of array of byte)
+{
+ wfd := sys->open("/prog/" + string sys->pctl(0, nil) + "/wait", Sys->OREAD);
+ spawn updateproc1(updatech);
+ buf := array[Sys->ATOMICIO] of byte;
+ n := sys->read(wfd, buf, len buf);
+ sys->print("updateproc process exited: %s\n", string buf[0:n]);
+}
+
+updateproc1(updatech: chan of array of byte)
+{
+ animfinishedch = chan of (ref Animation, chan of chan of ref Animation);
+ first := 1;
+ for (;;) {
+ alt {
+ v := <-animfinishedch =>
+ lock();
+ animterminated(v);
+ updatearena();
+ cmd(win, "update");
+ unlock();
+ u := <-updatech =>
+ if (u == nil) {
+ # XXX notify user that clique has been hung up somehow
+ exit;
+ }
+ moretocome := 0;
+ if (len u > 2 && u[len u-1] == byte '*' && u[len u-2] == byte '\n') {
+ u = u[0:len u - 2];
+ moretocome = 1;
+ }
+ (nil, lines) := sys->tokenize(string u, "\n");
+ lock();
+ starttime = sys->millisec();
+ for (; lines != nil; lines = tl lines)
+ applyupdate(hd lines);
+ updatearena();
+ if (!moretocome) {
+ if (first) {
+ sizetofit();
+ first = 0;
+ }
+ cmd(win, "update");
+ }
+ unlock();
+ }
+ }
+}
+
+updatearena()
+{
+ if (needrepack)
+ repackall();
+ if (needresize)
+ resizeall();
+ for (pstk := repackobjs; pstk != nil; pstk = tl pstk)
+ repackobj(hd pstk);
+ repackobjs = nil;
+}
+
+applyupdate(s: string)
+{
+ if (debugging) {
+ sys->print("update: %s\n", s);
+# showtk = 1;
+ }
+ (nt, toks) := sys->tokenize(s, " ");
+ case hd toks {
+ "create" =>
+ # create id parentid vis type
+ id := int hd tl toks;
+ if (id >= len objects)
+ objects = (array[len objects + 10] of ref Object)[0:] = objects;
+ if (objects[id] != nil)
+ panic(sys->sprint("object %d already exists!", id));
+ parentid := int hd tl tl toks;
+ vis := int hd tl tl tl toks;
+ objtype := tl tl tl tl toks;
+ case hd objtype {
+ "stack" =>
+ objects[id] = makestack(id, parentid, vis);
+ needrepack = 1;
+ "card" =>
+ stk := stackobj(parentid);
+ completeanim(stk);
+ if (!stk.visible) {
+ # if creating in a private stack, we assume
+ # that the cards were there already, and
+ # just make them real again.
+
+ # first find a fake card.
+ for (i := 0; i < len stk.cards; i++)
+ if (stk.cards[i].id < 0)
+ break;
+ c: ref Object.Card;
+ if (i == len stk.cards) {
+ # no fake cards - we'll create one instead.
+ # this can happen if we've entered halfway through
+ # a clique, so don't know how many cards people
+ # are holding.
+ c = makecard(id, stk);
+ insertcards(stk, array[] of {c}, len stk.cards);
+ } else {
+ c = stk.cards[i];
+ changecardid(c, id);
+ }
+ objects[id] = c;
+ } else {
+ objects[id] = c := makecard(id, stk);
+ insertcards(stk, array[] of {c}, len stk.cards);
+ }
+ "widget" =>
+ objects[id] = makewidget(id, parentid, hd tl objtype);
+ "menuentry" =>
+ objects[id] = makemenuentry(id, parentid, tl objtype);
+ "member" =>
+ objects[id] = ref Object.Member(id, -1, "");
+ "layframe" =>
+ lay := ref Layout.Frame(id, parentid, "", -1, nil);
+ objects[id] = ref Object.Layoutframe(id, lay);
+ addlayout(lay);
+ "layobj" =>
+ lay := ref Layout.Obj(id, parentid, "", -1, -1);
+ objects[id] = ref Object.Layoutobj(id, lay);
+ addlayout(lay);
+ "scoretable" =>
+ if (scoretable != nil)
+ panic("cannot make two scoretables");
+ scoretable = objects[id] = ref Object.Scoretable(id, nil);
+ "score" =>
+ pick l := objects[parentid] {
+ Scoretable =>
+ nl := array[len l.scores + 1] of ref Object.Score;
+ nl[0:] = l.scores;
+ nl[len nl - 1] = objects[id] = ref Object.Score(id, nil, 0);
+ l.scores = nl;
+ cmd(win, "pack .buts.scores -side left");
+ * =>
+ panic("score created outside scoretable object");
+ }
+ "button" =>
+ objects[id] = ref Object.Button(id);
+ cmd(win, "button .buts." + string id);
+ cmd(win, "pack .buts." + string id + " -side left");
+ * =>
+ if (parentid != -1)
+ sys->print("cards: unknown objtype: '%s'\n", hd objtype);
+ objects[id] = ref Object.Other(id);
+ }
+
+ "tx" =>
+ # tx src dst start end dstindex
+ src, dst: ref Layobject.Stack;
+ index: int;
+ r: Range;
+ (src, toks) = (stackobj(int hd tl toks), tl tl toks);
+ (dst, toks) = (stackobj(int hd toks), tl toks);
+ (r.start, toks) = (int hd toks, tl toks);
+ (r.end, toks) = (int hd toks, tl toks);
+ (index, toks) = (int hd toks, tl toks);
+ transfer(src, r, dst, index);
+
+ "del" =>
+ # del parent start end objs...
+ oo := objects[int hd tl toks]; # parent
+ r := Range(int hd tl tl toks, int hd tl tl tl toks);
+ pick o := oo {
+ Stack => # deleting cards from a stack.
+ stk := o.o;
+ completeanim(stk);
+ if (!stk.visible) {
+ # if deleting from a private area, we assume the cards aren't
+ # actually being deleted at all, but merely becoming
+ # invisible, so turn them into fakes.
+ for (i := r.start; i < r.end; i++) {
+ card := stk.cards[i];
+ objects[card.id] = nil;
+ changecardid(card, --fakeid);
+ cardsetattr(card, "face", "0" :: nil);
+ }
+ } else {
+ cards := extractcards(stk, r);
+ for (i := 0; i < len cards; i++)
+ destroy(cards[i]);
+ }
+ Layoutframe => # deleting the layout specification.
+ lay := o.lay;
+ if (r.start != 0 || r.end != len lay.lays)
+ panic("cannot partially delete layouts");
+ for (i := r.start; i < r.end; i++)
+ destroy(objects[lay.lays[i].id]);
+ lay.lays = nil;
+ needrepack = 1;
+ Widget =>
+ # must be a menu widget
+ cmd(win, ".buts." + string o.id + ".m delete " +
+ string r.start + " " + string r.end);
+ * =>
+ for (objs := tl tl tl tl toks; objs != nil; objs = tl objs)
+ destroy(objects[int hd objs]);
+ }
+
+ "set" =>
+ # set obj attr val
+ id := int hd tl toks;
+ (attr, val) := (hd tl tl toks, tl tl tl toks);
+ pick o := objects[id] {
+ Card =>
+ cardsetattr(o, attr, val);
+ Widget =>
+ widgetsetattr(o.o, attr, val);
+ Stack =>
+ stacksetattr(o.o, attr, val);
+ Member =>
+ membersetattr(o, attr, val);
+ Layoutframe =>
+ laysetattr(o.lay, attr, val);
+ Layoutobj =>
+ laysetattr(o.lay, attr, val);
+ Score =>
+ scoresetattr(o, attr, val);
+ Button =>
+ buttonsetattr(o, attr, val);
+ Menuentry =>
+ menuentrysetattr(o, attr, val);
+ * =>
+ sys->fprint(stderr, "unknown attr set on object(tag %d), %s\n", tagof(objects[id]), s);
+ }
+
+ "say" or
+ "remark" =>
+ notify(join(tl toks));
+ * =>
+ sys->fprint(stderr, "cards: unknown update message '%s'\n", s);
+ }
+}
+
+addlayout(lay: ref Layout)
+{
+ pick lo := objects[lay.parentid] {
+ Layoutframe =>
+ l := lo.lay;
+ nl := array[len l.lays + 1] of ref Layout;
+ nl[0:] = l.lays;
+ nl[len nl - 1] = lay;
+ l.lays = nl;
+ * =>
+ if (layout == nil)
+ layout = lay;
+ else
+ panic("cannot make two layout objects");
+ }
+}
+
+makestack(id, parentid: int, vis: int): ref Object.Stack
+{
+ o := ref Object.Stack(
+ id,
+ ref Layobject.Stack(
+ id,
+ parentid,
+ "", # pack widget name
+ (0, 0), # size
+ 0, # needrepack
+ -1, # orientation
+ -1, # layid
+ -1, # style
+ nil, # cards
+ Hiddenpos, # pos
+ (0, 0), # delta
+ ref Queue,
+ 0, # actions
+ 0, # maxcards
+ "", # title
+ vis, # visible
+ 0, # n
+ -1, # ownerid
+ ref Selection.Empty, # sel
+ 1, # showsize
+ 0 # hassize
+ )
+ );
+ cmd(win, ".c create rectangle -10 -10 -10 -10 -width 3 -tags r" + string id);
+ return o;
+}
+
+makewidget(id, parentid: int, wtype: string): ref Object.Widget
+{
+ wctype := wtype;
+ if (wtype == "menu")
+ wctype = "menubutton";
+ # XXX the widget is put in a frame 'cos of bugs in the canvas
+ # to do with size propagation.
+ w := cmd(win, "frame .buts." + string id + " -bg transparent");
+ cmd(win, wctype + " " + w + ".b");
+ cmd(win, "pack " + w + ".b -fill both -expand 1");
+ case wtype {
+ "menu" =>
+ cmd(win, "menu " + w + ".m");
+ cmd(win, w + ".b configure -menu " + w + ".m" +
+ " -relief raised");
+ "entry" =>
+ cmd(win, "bind " + w + ".b <Key-\n> {send cmd entry " + string id + "}");
+ }
+ cmd(win, ".c create window -1000 -1000 -tags r" + string id +
+ " -window " + w + " -anchor nw");
+ o := ref Object.Widget(
+ id,
+ ref Layobject.Widget(
+ id,
+ parentid,
+ nil, # w
+ (0, 0), # size
+ 0, # needrepack
+ -1, # orientation
+ -1, # style
+
+ wtype,
+ nil, # entries
+ "", # cmd
+ 0 # width
+ )
+ );
+ return o;
+}
+
+menutitleid := 0; # hack to identify menu entries
+makemenuentry(id, parentid: int, nil: list of string): ref Object.Menuentry
+{
+ m := ".buts." + string parentid + ".m";
+ t := "@" + string menutitleid++;
+ cmd(win, m + " add command -text " + t);
+ return ref Object.Menuentry(id, parentid, t);
+}
+
+makecard(id: int, stack: ref Layobject.Stack): ref Object.Card
+{
+ cmd(win, ".c create image 5000 5000 -anchor nw -tags i" + string id);
+ return ref Object.Card(id, stack.id, -1, -1, 0);
+}
+
+buttonsetattr(b: ref Object.Button, attr: string, val: list of string)
+{
+ w := ".buts." + string b.id;
+ case attr {
+ "text" =>
+ cmd(win, w + " configure -text '" + join(val));
+ "command" =>
+ cmd(win, w + " configure -command 'send srv " + join(val));
+ * =>
+ sys->print("unknown attribute on button: %s\n", attr);
+ }
+}
+
+widgetsetattr(b: ref Layobject.Widget, attr: string, val: list of string)
+{
+ w := ".buts." + string b.id + ".b";
+ case attr {
+ "text" =>
+ t := join(val);
+ if (b.wtype == "entry") {
+ cmd(win, w + " delete 0 end");
+ cmd(win, w + " insert 0 '" + t);
+ cmd(win, w + " select 0 end"); # XXX ??
+ } else {
+ cmd(win, w + " configure -text '" + t);
+ needresize = 1;
+ }
+ "command" =>
+ case b.wtype {
+ "button" =>
+ cmd(win, w + " configure -command 'send srv " + join(val));
+ "entry" =>
+ b.cmd = join(val);
+ }
+ "width" => # width in characters
+ b.width = int hd val;
+ sys->print("configuring %s for width %s\n", w, hd val);
+ cmd(win, w + " configure -width " + hd val + "w");
+ needresize = 1;
+ "layid" =>
+ setlayid(b, int hd val);
+ * =>
+ sys->print("unknown attribute on button: %s\n", attr);
+ }
+}
+
+findmenuentry(m: string, title: string): int
+{
+ end := int cmd(win, m + " index end");
+ for (i := 0; i <= end; i++) {
+ t := cmd(win, m + " entrycget " + string i + " -text");
+ if (t == title)
+ return i;
+ }
+ return -1;
+}
+
+menuentrysetattr(e: ref Object.Menuentry, attr: string, val: list of string)
+{
+ m := ".buts." + string e.parentid + ".m";
+ idx := findmenuentry(m, e.text);
+ if (idx == -1) {
+ sys->print("couldn't find menu entry '%s'\n", e.text);
+ return;
+ }
+ case attr {
+ "text" =>
+ t := join(val);
+ cmd(win, m + " entryconfigure " + string idx +" -text '" + t);
+ e.text = t;
+ "command" =>
+ cmd(win, m + " entryconfigure " + string idx +
+ " -command 'send srv " + join(val));
+ * =>
+ sys->print("unknown attribute on menu entry: %s\n", attr);
+ }
+}
+
+stacksetattr(stack: ref Layobject.Stack, attr: string, val: list of string)
+{
+ id := string stack.id;
+ case attr {
+ "maxcards" =>
+ stack.maxcards = int hd val;
+ needresize = 1;
+ "layid" =>
+ setlayid(stack, int hd val);
+ "showsize" =>
+ stack.showsize = int hd val;
+ showsize(stack);
+ "title" =>
+ title := join(val);
+ if (title != stack.title) {
+ if (stack.title == nil) {
+ cmd(win, ".c create text 5000 6000 -anchor n -tags t" + string id +
+ " -fill #ffffaa");
+ needresize = 1;
+ } else if (title == nil) {
+ cmd(win, ".c delete t" + string id);
+ needresize = 1;
+ }
+ if (title != nil)
+ cmd(win, ".c itemconfigure t" + string id + " -text '" + title);
+ stack.title = title;
+ }
+ "n" =>
+ # there are "n" cards in this stack, honest guv.
+ n := int hd val;
+ if (!stack.visible) {
+ if (n > len stack.cards) {
+ a := array[n - len stack.cards] of ref Object.Card;
+ for (i := 0; i < len a; i++) {
+ a[i] = makecard(--fakeid, stack);
+ cardsetattr(a[i], "face", "0" :: nil);
+ }
+ insertcards(stack, a, len stack.cards);
+ } else if (n < len stack.cards) {
+ for (i := len stack.cards - 1; i >= n; i--)
+ if (stack.cards[i].id >= 0)
+ break;
+ cards := extractcards(stack, (i + 1, len stack.cards));
+ for (i = 0; i < len cards; i++)
+ destroy(cards[i]);
+ }
+ }
+ stack.n = n;
+ "style" =>
+ case hd val {
+ "pile" =>
+ stack.style = styPILE;
+ "display" =>
+ stack.style = styDISPLAY;
+ * =>
+ sys->print("unknown stack style '%s'\n", hd val);
+ }
+ needresize = 1;
+ "owner" =>
+ if (val != nil)
+ stack.ownerid = int hd val;
+ else
+ stack.ownerid = -1;
+ changesel(stack, stack.sel);
+ "sel" =>
+ sel: ref Selection;
+ if (val == nil)
+ sel = ref Selection.Empty;
+ else if (tl val != nil && hd tl val == "-")
+ sel = ref Selection.XRange((int hd val, int hd tl tl val));
+ else {
+ idxl: list of int;
+ for (; val != nil; val = tl val)
+ idxl = int hd val :: idxl;
+ sel = ref Selection.Indexes(idxl);
+ }
+ changesel(stack, sel);
+ * =>
+ if (len attr >= len "actions" && attr[0:len "actions"] == "actions") {
+ oldactions := stack.actions;
+ act := 0;
+ for (; val != nil; val = tl val) {
+ case hd val {
+ "click" =>
+ act |= aCLICK;
+ * =>
+ sys->print("unknown action '%s'\n", hd val);
+ }
+ }
+ stack.actions = act;
+ } else
+ sys->fprint(stderr, "bad stack attr '%s'\n", attr);
+ }
+}
+
+showsize(stack: ref Layobject.Stack)
+{
+ id := string stack.id;
+ needsize := stack.showsize && len stack.cards > 0 && stack.style == styPILE;
+ if (needsize != stack.hassize) {
+ if (stack.hassize)
+ cmd(win, ".c delete n" + id + " N" + id);
+ else {
+ cmd(win, ".c create rectangle -5000 0 0 0 -fill #ffffaa -tags n" + id);
+ cmd(win, ".c create text -5000 0 -anchor sw -fill red -tags N" + id);
+ }
+ stack.hassize = needsize;
+ }
+ if (needsize) {
+ cmd(win, ".c itemconfigure N" + id + " -text " + string len stack.cards);
+ sr := cardrect(stack, (len stack.cards - 1, len stack.cards));
+ cmd(win, ".c coords N" + id + " " + p2s((sr.min.x, sr.max.y)));
+ bbox := cmd(win, ".c bbox N" + id);
+ cmd(win, ".c coords n" + id + " " + bbox);
+ cmd(win, ".c raise n" + id + "; .c raise N" + id);
+ }
+}
+
+changesel(stack: ref Layobject.Stack, newsel: ref Selection)
+{
+ sid := "s" + string stack.id;
+ cmd(win, ".c delete " + sid);
+
+ if (me != nil && stack.ownerid == me.cid) {
+ pick sel := newsel {
+ Indexes =>
+ for (l := sel.idxl; l != nil; l = tl l) {
+ s := cmd(win, ".c create rectangle " +
+ r2s(cardrect(stack, (hd l, hd l + 1)).inset(-1)) +
+ " -width " + string Selectborder +
+ " -outline " + Selectcolour +
+ " -tags {" + sid + " " + sid + "." + string hd l + "}");
+ cmd(win, ".c lower " + s + " i" + string stack.cards[hd l].id);
+ }
+ XRange =>
+ cmd(win, ".c create rectangle " +
+ r2s(cardrect(stack, sel.r).inset(-1)) +
+ " -outline " + Selectcolour +
+ " -width " + string Selectborder +
+ " -tags " + sid);
+ }
+ }
+ stack.sel = newsel;
+}
+
+cardsetattr(card: ref Object.Card, attr: string, val: list of string)
+{
+ id := string card.id;
+ case attr {
+ "face" =>
+ card.face = int hd val;
+ if (card.face) {
+ if (card.number != -1)
+ cmd(win, ".c itemconfigure i" + id + " -image c" + string card.number );
+ } else
+ cmd(win, ".c itemconfigure i" + id + " -image rear" + string card.rear);
+ "number" =>
+ card.number = int hd val;
+ if (card.face)
+ cmd(win, ".c itemconfigure i" + id + " -image c" + string card.number );
+ "rear" =>
+ card.rear = int hd val;
+ if (card.face == 0)
+ cmd(win, ".c itemconfigure i" + id + " -image rear" + string card.rear);
+ * =>
+ sys->print("unknown attribute on card: %s\n", attr);
+ }
+}
+
+setlayid(layobj: ref Layobject, layid: int)
+{
+ if (layobj.layid != -1)
+ panic("obj already has a layout id (" + string layobj.layid + ")");
+ layobj.layid = layid;
+ x := layobj.layid % len layobjects;
+ layobjects[x] = layobj :: layobjects[x];
+ needrepack = 1;
+}
+
+membersetattr(p: ref Object.Member, attr: string, val: list of string)
+{
+ case attr {
+ "you" =>
+ me = p;
+ p.cid = int hd val;
+ for (i := 0; i < len objects; i++) {
+ if (objects[i] != nil) {
+ pick o := objects[i] {
+ Stack =>
+ if (o.o.ownerid == p.cid)
+ objneedsrepack(o.o);
+ }
+ }
+ }
+ "name" =>
+ p.name = hd val;
+ "id" =>
+ p.cid = int hd val;
+ "status" =>
+ if (p == me)
+ cmd(win, ".status configure -text '" + join(val));
+ "cliquetitle" =>
+ if (p == me)
+ tkclient->settitle(win, join(val));
+ * =>
+ sys->print("unknown attribute on member: %s\n", attr);
+ }
+}
+
+laysetattr(lay: ref Layout, attr: string, val: list of string)
+{
+ case attr {
+ "opts" =>
+ # orientation opts
+ case hd val {
+ "up" =>
+ lay.orientation = oUP;
+ "down" =>
+ lay.orientation = oDOWN;
+ "left" =>
+ lay.orientation = oLEFT;
+ "right" =>
+ lay.orientation = oRIGHT;
+ * =>
+ sys->print("unknown orientation '%s'\n", hd val);
+ }
+ lay.opts = join(tl val);
+ "layid" =>
+# sys->print("layout obj %d => layid %s\n", lay.id, hd val);
+ pick l := lay {
+ Obj =>
+ l.layid = int hd val;
+ needrepack = 1;
+ * =>
+ sys->print("cannot set layid on Layout.Frame!\n");
+ }
+ * =>
+ sys->print("unknown attribute on lay: %s\n", attr);
+ }
+ needrepack = 1;
+}
+
+scoresetattr(score: ref Object.Score, attr: string, val: list of string)
+{
+ if (attr != "score")
+ return;
+ cmd(win, ".c delete score");
+
+ Padx: con 10; # padding to the right of each item
+ Pady: con 6; # padding below each item.
+
+ n := len val;
+ row := score.row = array[n] of (int, string);
+ height := 0;
+
+ # calculate values for this row
+ for ((col, vl) := (0, val); vl != nil; (col, vl) = (col + 1, tl vl)) {
+ v := hd vl;
+ size := textsize(v);
+ size.y += Pady;
+ if (size.y > height)
+ height = size.y;
+ row[col] = (size.x + Padx, v);
+ }
+ score.height = height;
+ totheight := 0;
+ scores := scoretable.scores;
+
+ # calculate number of columns
+ ncols := 0;
+ for (i := 0; i < len scores; i++)
+ if (len scores[i].row > ncols)
+ ncols = len scores[i].row;
+
+ # calculate column widths
+ colwidths := array[ncols] of {* => 0};
+ for (i = 0; i < len scores; i++) {
+ r := scores[i].row;
+ for (j := 0; j < len r; j++) {
+ (w, nil) := r[j];
+ if (w > colwidths[j])
+ colwidths[j] = w;
+ }
+ totheight += scores[i].height;
+ }
+ # create all table items
+ p := Hiddenpos;
+ for (i = 0; i < len scores; i++) {
+ p.x = Hiddenpos.x;
+ r := scores[i].row;
+ for (j := 0; j < len r; j++) {
+ (w, text) := r[j];
+ cmd(win, ".c create text " + p2s(p) + " -anchor nw -tags {score scoreent}-text '" + text);
+ p.x += colwidths[j];
+ }
+ p.y += scores[i].height;
+ }
+ r := Rect(Hiddenpos, p);
+ r.min.x -= Padx;
+ r.max.y -= Pady / 2;
+
+ cmd(win, ".c create rectangle " + r2s(r) + " -fill #ffffaa -tags score");
+
+ # horizontal lines
+ y := 0;
+ for (i = 0; i < len scores - 1; i++) {
+ ly := y + scores[i].height - Pady / 2;
+ cmd(win, ".c create line " + r2s(((r.min.x, ly), (r.max.x, ly))) + " -fill gray -tags score");
+ y += scores[i].height;
+ }
+
+ cmd(win, ".c raise scoreent");
+ cmd(win, ".c move score " + p2s(Hiddenpos.sub(r.min)));
+}
+
+textsize(s: string): Point
+{
+ return (cvsfont.width(s), cvsfont.height);
+}
+
+changecardid(c: ref Object.Card, newid: int)
+{
+ (nil, tags) := sys->tokenize(cmd(win, ".c gettags i" + string c.id), " ");
+ for (; tags != nil; tags = tl tags) {
+ tag := hd tags;
+ if (tag[0] >= '0' && tag[0] <= '9')
+ break;
+ }
+ cvsid := hd tags;
+ cmd(win, ".c dtag " + cvsid + " i" + string c.id);
+ c.id = newid;
+ cmd(win, ".c addtag i" + string c.id + " withtag " + cvsid);
+}
+
+stackobj(id: int): ref Layobject.Stack
+{
+ obj := objects[id];
+ if (obj == nil)
+ panic("nil stack object");
+ pick o := obj {
+ Stack =>
+ return o.o;
+ * =>
+ panic("expected obj " + string id + " to be a stack");
+ }
+ return nil;
+}
+
+# if there are updates pending on the stack,
+# then wait for them all to finish before we can do
+# any operations on the stack (e.g. insert, delete, create, etc)
+completeanim(stk: ref Layobject.Stack)
+{
+ while (!stk.animq.isempty())
+ animterminated(<-animfinishedch);
+}
+
+transfer(src: ref Layobject.Stack, r: Range, dst: ref Layobject.Stack, index: int)
+{
+ # we don't bother animating movement within a stack; maybe later?
+ if (src == dst) {
+ transfercards(src, r, dst, index);
+ return;
+ }
+ completeanim(src);
+
+ if (!src.visible) {
+ # cards being transferred out of private area should
+ # have already been created, but check anyway.
+ if (r.start != 0)
+ panic("bad transfer out of private");
+ for (i := 0; i < r.end; i++)
+ if (src.cards[i].id < 0)
+ panic("cannot transfer fake card");
+ }
+
+ startanimating(newanimation(src, r), dst, index);
+}
+
+objneedsrepack(obj: ref Layobject)
+{
+ if (!obj.needrepack) {
+ obj.needrepack = 1;
+ repackobjs = obj :: repackobjs;
+ }
+}
+
+repackobj(obj: ref Layobject)
+{
+ pick o := obj {
+ Stack =>
+ cards := o.cards;
+ pos := o.pos;
+ delta := o.delta;
+ for (i := 0; i < len cards; i++) {
+ p := pos.add(delta.mul(i));
+ id := string cards[i].id;
+ cmd(win, ".c coords i" + id + " " + p2s(p));
+ cmd(win, ".c raise i" + id); # XXX could be more efficient.
+ cmd(win, ".c lower s" + string o.id + "." + string i + " i" + id);
+ }
+ changesel(o, o.sel);
+ showsize(o);
+ }
+ obj.needrepack = 0;
+}
+
+cardrect(stack: ref Layobject.Stack, r: Range): Rect
+{
+ if (r.start == r.end)
+ return ((-10, -10), (-10, -10));
+ cr := Rect((0, 0), cardsize).addpt(stack.pos);
+ delta := stack.delta;
+ return union(cr.addpt(delta.mul(r.start)), cr.addpt(delta.mul(r.end - 1)));
+}
+
+repackall()
+{
+ sys->print("repackall()\n");
+ needrepack = 0;
+ if (layout == nil) {
+ sys->print("no layout\n");
+ return;
+ }
+ if (packwin == nil) {
+ # use an unmapped tk window to do our packing arrangements
+ packwin = tk->toplevel(drawctxt.display, "-bd 0");
+ packwin.wreq = nil; # stop window requests piling up.
+ }
+ cmd(packwin, "destroy " + cmd(packwin, "pack slaves ."));
+ packobjs = nil;
+ packit(layout, ".0");
+ sys->print("%d packobjs\n", len packobjs);
+ needresize = 1;
+}
+
+# make the frames for the objects to be laid out, in the
+# offscreen window.
+packit(lay: ref Layout, f: string)
+{
+ cmd(packwin, "frame " + f);
+ cmd(packwin, "pack " + f + " " + lay.opts);
+ pick l := lay {
+ Frame =>
+ for (i := 0; i < len l.lays; i++)
+ packit(l.lays[i], f + "." + string i);
+ Obj =>
+ if ((obj := findlayobject(l.layid)) != nil) {
+ obj.w = f;
+ obj.orientation = l.orientation;
+ packobjs = obj :: packobjs;
+ } else
+ sys->print("cannot find layobject %d\n", l.layid);
+ }
+}
+
+sizetofit()
+{
+ if (packobjs == nil)
+ return;
+ cmd(packwin, "pack propagate . 1");
+ cmd(packwin, ". configure -width 0 -height 0"); # make sure propagation works.
+ csz := actsize(packwin, ".");
+ cmd(win, "bind . <Configure> {}");
+ cmd(win, "pack propagate . 1");
+ cmd(win, ". configure -width 0 -height 0");
+
+ cmd(win, ".c configure -width " + string csz.x + " -height " + string csz.y
+ + " -scrollregion {0 0 " + p2s(csz) + "}");
+ winr := actrect(win, ".");
+ screenr := win.image.screen.image.r;
+ if (!winr.inrect(screenr)) {
+ if (winr.dx() > screenr.dx())
+ (winr.min.x, winr.max.x) = (screenr.min.x, screenr.max.x);
+ if (winr.dy() > screenr.dy())
+ (winr.min.y, winr.max.y) = (screenr.min.y, screenr.max.y);
+ if (winr.max.x > screenr.max.x)
+ (winr.min.x, winr.max.x) = (screenr.max.x - winr.dx(), screenr.max.x);
+ if (winr.max.y > screenr.max.y)
+ (winr.min.y, winr.max.y) = (screenr.max.y - winr.dy(), screenr.max.y);
+ }
+ cmd(win, "pack propagate . 0");
+ cmd(win, ". configure " +
+ " -x " + string winr.min.x +
+ " -y " + string winr.min.y +
+ " -width " + string winr.dx() +
+ " -height " + string winr.dy());
+ needresize = 1;
+ updatearena();
+ cmd(win, "bind . <Configure> {send cmd config}");
+}
+
+setorigin(r: Rect, p: Point): Rect
+{
+ sz := Point(r.max.x - r.min.x, r.max.y - r.min.y);
+ return (p, p.add(sz));
+}
+
+resizeall()
+{
+ needresize = 0;
+ if (packobjs == nil)
+ return;
+ cmd(packwin, "pack propagate . 1");
+ cmd(packwin, ". configure -width 0 -height 0"); # make sure propagation works.
+ for (sl := packobjs; sl != nil; sl = tl sl) {
+ obj := hd sl;
+ sizeobj(obj);
+ cmd(packwin, obj.w + " configure -width " + string obj.size.x +
+ " -height " + string obj.size.y);
+ }
+ csz := actsize(packwin, ".");
+ sz := actsize(win, ".cf");
+ if (sz.x > csz.x || sz.y > csz.y) {
+ cmd(packwin, "pack propagate . 0");
+ if (sz.x > csz.x) {
+ cmd(packwin, ". configure -width " + string sz.x);
+ cmd(win, ".c xview moveto 0");
+ csz.x = sz.x;
+ }
+ if (sz.y > csz.y) {
+ cmd(packwin, ". configure -height " + string sz.y);
+ cmd(win, ".c yview moveto 0");
+ csz.y = sz.y;
+ }
+ }
+ cmd(win, ".c configure -width " + string csz.x + " -height " + string csz.y
+ + " -scrollregion {0 0 " + p2s(csz) + "}");
+ onscreen();
+ for (sl = packobjs; sl != nil; sl = tl sl) {
+ obj := hd sl;
+ r := actrect(packwin, obj.w);
+ positionobj(obj, r);
+ }
+}
+
+# make sure that there aren't any unnecessary blank
+# bits in the scroll area.
+onscreen()
+{
+ (n, toks) := sys->tokenize(cmd(win, ".c xview"), " ");
+ cmd(win, ".c xview moveto " + hd toks);
+ (n, toks) = sys->tokenize(cmd(win, ".c yview"), " ");
+ cmd(win, ".c yview moveto " + hd toks);
+}
+
+# work out the size of an object to be laid out.
+sizeobj(obj: ref Layobject)
+{
+ pick o := obj {
+ Stack =>
+ delta := Point(0, 0);
+ case o.style {
+ styDISPLAY =>
+ case o.orientation {
+ oRIGHT => delta.x = carddelta.x;
+ oLEFT => delta.x = -carddelta.x;
+ oDOWN => delta.y = carddelta.y;
+ oUP => delta.y = -carddelta.y;
+ }
+ styPILE =>
+ ; # no offset
+ }
+ o.delta = delta;
+ r := Rect((0, 0), size(cardrect(o, (0, max(len o.cards, o.maxcards)))));
+ if (o.title != nil) {
+ p := Point(r.min.x + r.dx() / 2, r.min.y);
+ tr := s2r(cmd(win, ".c bbox t" + string o.id));
+ tbox := Rect((p.x - tr.dx() / 2, p.y - tr.dy()), (p.x + tr.dx() / 2, p.y));
+ r = union(r, tbox);
+ }
+ o.size = r.max.sub(r.min).add((Border * 2, Border * 2));
+# sys->print("sized stack %d => %s\n", o.id, p2s(o.size));
+ Widget =>
+ w := ".buts." + string o.id;
+ o.size.x = int cmd(win, w + " cget -width");
+ o.size.y = int cmd(win, w + " cget -height");
+# sys->print("sized widget %d (%s) => %s\n", o.id,
+# cmd(win, "winfo class " + w + ".b"), p2s(o.size));
+ }
+}
+
+# set a laid-out object's position on the canvas, given
+# its allocated rectangle, r.
+positionobj(obj: ref Layobject, r: Rect)
+{
+ pick o := obj {
+ Stack =>
+# sys->print("positioning stack %d, r %s\n", o.id, r2s(r));
+ delta := o.delta;
+ sz := o.size.sub((Border * 2, Border * 2));
+ r.min.x += (r.dx() - sz.x) / 2;
+ r.min.y += (r.dy() - sz.y) / 2;
+ r.max = r.min.add(sz);
+ if (o.title != nil) {
+ cmd(win, ".c coords t" +string o.id + " " +
+ string (r.min.x + r.dx() / 2) + " " + string r.min.y);
+ tr := s2r(cmd(win, ".c bbox t" + string o.id));
+ r.min.y = tr.max.y;
+ sz = size(cardrect(o, (0, max(len o.cards, o.maxcards))));
+ r.min.x += (r.dx() - sz.x) / 2;
+ r.min.y += (r.dy() - sz.y) / 2;
+ r.max = r.min.add(sz);
+ }
+ o.pos = r.min;
+ if (delta.x < 0)
+ o.pos.x = r.max.x - cardsize.x;
+ if (delta.y < 0)
+ o.pos.y = r.max.y - cardsize.y;
+ cmd(win, ".c coords r" + string o.id + " " + r2s(r.inset(-(Border / 2))));
+ objneedsrepack(o);
+ Widget =>
+# sys->print("positioning widget %d, r %s\n", o.id, r2s(r));
+ cmd(win, ".c coords r" + string o.id + " " + p2s(r.min));
+ bd := int cmd(win, ".buts." + string o.id + " cget -bd");
+ cmd(win, ".c itemconfigure r" + string o.id +
+ " -width " + string (r.dx() - bd * 2) +
+ " -height " + string (r.dy() - bd * 2));
+ }
+}
+
+size(r: Rect): Point
+{
+ return r.max.sub(r.min);
+}
+
+transfercards(src: ref Layobject.Stack, r: Range, dst: ref Layobject.Stack, index: int)
+{
+ cards := extractcards(src, r);
+ n := r.end - r.start;
+ # if we've just removed some cards from the destination,
+ # then adjust the destination index accordingly.
+ if (src == dst && index > r.start) {
+ if (index < r.end)
+ index = r.start;
+ else
+ index -= n;
+ }
+ insertcards(dst, cards, index);
+}
+
+extractcards(src: ref Layobject.Stack, r: Range): array of ref Object.Card
+{
+ if (len src.cards > src.maxcards)
+ needresize = 1;
+ deltag(src.cards[r.start:r.end], "c" + string src.id);
+ n := r.end - r.start;
+ cards := src.cards[r.start:r.end];
+ newcards := array[len src.cards - n] of ref Object.Card;
+ newcards[0:] = src.cards[0:r.start];
+ newcards[r.start:] = src.cards[r.end:];
+ src.cards = newcards;
+ objneedsrepack(src); # XXX not necessary if moving from top?
+ return cards;
+}
+
+insertcards(dst: ref Layobject.Stack, cards: array of ref Object.Card, index: int)
+{
+ n := len cards;
+ newcards := array[len dst.cards + n] of ref Object.Card;
+ newcards[0:] = dst.cards[0:index];
+ newcards[index + n:] = dst.cards[index:];
+ newcards[index:] = cards;
+ dst.cards = newcards;
+
+ for (i := 0; i < len cards; i++)
+ cards[i].parentid = dst.id;
+ addtag(dst.cards[index:index + n], "c" + string dst.id);
+ objneedsrepack(dst); # XXX not necessary if adding to top?
+ if (len dst.cards > dst.maxcards)
+ needresize = 1;
+}
+
+destroy(obj: ref Object)
+{
+ if (obj.id >= 0)
+ objects[obj.id] = nil;
+ id := string obj.id;
+ pick o := obj {
+ Card =>
+ cmd(win, ".c delete i" + id); # XXX crashed here once...
+ Widget =>
+ cmd(win, ".c delete r" + id);
+ w := ".buts." + id;
+ cmd(win, "destroy " + w);
+ dellayobject(o.o);
+ Stack =>
+ completeanim(o.o);
+ cmd(win, ".c delete r" + id + " s" + id + " n" + id + " N" + id);
+ if (o.o.title != nil)
+ cmd(win, ".c delete t" + id);
+ cmd(win, ".c delete c" + id); # any remaining "fake" cards
+ needrepack = 1;
+ dellayobject(o.o);
+ Button =>
+ cmd(win, "destroy .buts." + string o.id);
+ Member =>
+ if (o.cid != -1) {
+ # XXX remove member from members hash.
+ }
+ Layoutobj =>
+ if ((l := findlayobject(o.lay.layid)) != nil) {
+ # XXX are we sure they're not off-screen anyway?
+ cmd(win, ".c move r" + string l.id + " 5000 5000");
+ cmd(win, ".c move c" + string l.id + " 5000 5000");
+ cmd(win, ".c move N" + string l.id + " 5000 5000");
+ cmd(win, ".c move n" + string l.id + " 5000 5000");
+ cmd(win, ".c move s" + string l.id + " 5000 5000");
+ }
+ if (layout == o.lay)
+ layout = nil;
+ Layoutframe =>
+ if (layout == o.lay)
+ layout = nil;
+ }
+}
+
+dellayobject(lay: ref Layobject)
+{
+ if (lay.layid == -1)
+ return;
+ x := lay.layid % len layobjects;
+ nl: list of ref Layobject;
+ for (ll := layobjects[x]; ll != nil; ll = tl ll)
+ if ((hd ll).layid != lay.layid)
+ nl = hd ll :: nl;
+ layobjects[x] = nl;
+}
+
+findlayobject(layid: int): ref Layobject
+{
+ if (layid == -1)
+ return nil;
+ for (ll := layobjects[layid % len layobjects]; ll != nil; ll = tl ll)
+ if ((hd ll).layid == layid)
+ return hd ll;
+ return nil;
+}
+
+deltag(cards: array of ref Object.Card, tag: string)
+{
+ for (i := 0; i < len cards; i++)
+ cmd(win, ".c dtag i" + string cards[i].id + " " + tag);
+}
+
+addtag(cards: array of ref Object.Card, tag: string)
+{
+ for (i := 0; i < len cards; i++)
+ cmd(win, ".c addtag " + tag + " withtag i" + string cards[i].id);
+}
+
+join(v: list of string): string
+{
+ if (v == nil)
+ return nil;
+ s := hd v;
+ for (v = tl v; v != nil; v = tl v)
+ s += " " + hd v;
+ return s;
+}
+
+notify(s: string)
+{
+ notifych <-= s;
+}
+
+notifierproc()
+{
+ notifypid := -1;
+ sync := chan of int;
+ for (;;) {
+ s := <-notifych;
+ kill(notifypid);
+ spawn notifyproc(s, sync);
+ notifypid = <-sync;
+ }
+}
+
+notifyproc(s: string, sync: chan of int)
+{
+ sync <-= sys->pctl(0, nil);
+ cmd(win, ".c delete notify");
+ id := cmd(win, ".c create text " + p2s(visibleorigin()) + " -anchor nw -fill red -tags notify -text '" + s);
+ bbox := cmd(win, ".c bbox " + id);
+ cmd(win, ".c create rectangle " + bbox + " -fill #ffffaa -tags notify");
+ cmd(win, ".c raise " + id);
+ cmd(win, "update");
+ sys->sleep(1500);
+ cmd(win, ".c delete notify");
+ cmd(win, "update");
+}
+
+# move canvas so that canvas point canvp lies under
+# screen point scrp.
+pan(canvp, scrp: Point)
+{
+ o := Point(int cmd(win, ".c cget -actx"), int cmd(win, ".c cget -acty"));
+ co := canvp.sub(scrp.sub(o));
+ sz := Point(int cmd(win, ".c cget -width"), int cmd(win, ".c cget -height"));
+
+ cmd(win, ".c xview moveto " + string (real co.x / real sz.x));
+ cmd(win, ".c yview moveto " + string (real co.y / real sz.y));
+}
+
+# return the top left point that's currently visible
+# in the canvas, taking into account scrolling.
+visibleorigin(): Point
+{
+ (scrx, scry) := (cmd(win, ".c cget -actx"), cmd(win, ".c cget -acty"));
+ return Point (int cmd(win, ".c canvasx " + scrx),
+ int cmd(win, ".c canvasy " + scry));
+}
+
+s2r(s: string): Rect
+{
+ r: Rect;
+ (n, toks) := sys->tokenize(s, " ");
+ if (n < 4)
+ panic("malformed rectangle " + s);
+ (r.min.x, toks) = (int hd toks, tl toks);
+ (r.min.y, toks) = (int hd toks, tl toks);
+ (r.max.x, toks) = (int hd toks, tl toks);
+ (r.max.y, toks) = (int hd toks, tl toks);
+ return r;
+}
+
+r2s(r: Rect): string
+{
+ return sys->sprint("%d %d %d %d", r.min.x, r.min.y, r.max.x, r.max.y);
+}
+
+p2s(p: Point): string
+{
+ return string p.x + " " + string p.y;
+}
+
+union(r1, r2: Rect): Rect
+{
+ if (r1.min.x > r2.min.x)
+ r1.min.x = r2.min.x;
+ if (r1.min.y > r2.min.y)
+ r1.min.y = r2.min.y;
+
+ if (r1.max.x < r2.max.x)
+ r1.max.x = r2.max.x;
+ if (r1.max.y < r2.max.y)
+ r1.max.y = r2.max.y;
+ return r1;
+}
+
+kill(pid: int)
+{
+ if ((fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
+ sys->write(fd, array of byte "kill", 4);
+}
+
+lockproc()
+{
+ for (;;) {
+ <-cardlockch;
+ cardlockch <-=1;
+ }
+}
+
+lock()
+{
+ cardlockch <-= 1;
+}
+
+unlock()
+{
+ <-cardlockch;
+}
+
+openimage(file: string, id: string): Point
+{
+ if (tk->cmd(win, "image create bitmap " + id + " -file " + file)[0] == '!')
+ return (0, 0);
+ return (int tk->cmd(win, "image width " + id),
+ int tk->cmd(win, "image height " + id));
+}
+
+# read images into tk.
+readimages(dir: string, prefix: string): (int, Point)
+{
+ displ := drawctxt.display;
+ if (cardsize.x > 0 && cardsize.y > 0 &&
+ (img := displ.open(dir + "/" + prefix + ".all.bit")) != nil) {
+ if (img.r.dx() % cardsize.x != 0 || img.r.dy() != cardsize.y)
+ sys->fprint(stderr, "cards: inconsistent complete image, ignoring\n");
+ else {
+ n := img.r.dx() / cardsize.x;
+ x := img.r.min.x;
+ sys->print("found %d cards in complete image\n", n);
+ for (i := 0; i < n; i++) {
+ c := displ.newimage(((0, 0), cardsize), img.chans, 0, 0);
+ c.draw(c.r, img, nil, (x, 0));
+ id := prefix + string i;
+ cmd(win, "image create bitmap " + id);
+ tk->putimage(win, id, c, nil);
+ x += cardsize.x;
+ }
+ return (n, cardsize);
+ }
+ }
+
+ size := openimage("@" + dir + "/" + prefix + "0.bit", prefix + "0");
+ if (size.x == 0) {
+ sys->print("no first image (filename: '%s')\n", dir + "/" + prefix + "0.bit");
+ return (0, (0, 0));
+ }
+ i := 1;
+ for (;;) {
+ nsize := openimage("@" + dir + "/" + prefix + string i + ".bit", prefix + string i);
+ if (nsize.x == 0)
+ break;
+ if (!nsize.eq(size))
+ sys->fprint(stderr, "warning: inconsistent image size in %s/%s%d.bit, " +
+ "[%d %d] vs [%d %d]\n", dir, prefix, i, size.x, size.y, nsize.x, nsize.y);
+ i++;
+ }
+ return (i, size);
+}
+
+newanimation(src: ref Layobject.Stack, r: Range): ref Animation
+{
+ a := ref Animation;
+ a.srcpt = src.pos.add(src.delta.mul(r.start));
+ cards := extractcards(src, r);
+ a.cards = cards;
+ a.waitch = chan of ref Animation;
+ return a;
+}
+
+startanimating(a: ref Animation, dst: ref Layobject.Stack, index: int)
+{
+ q := dst.animq;
+ if (q.isempty())
+ spawn animqueueproc(a.waitch);
+
+ a.tag = "a" + string animid++;
+ addtag(a.cards, a.tag);
+ q.put(a);
+ a.dstid = dst.id;
+ a.index = index;
+ spawn animproc(a);
+}
+
+SPEED: con 1.5; # animation speed in pixels/millisec
+
+animproc(a: ref Animation)
+{
+ tick := chan of int;
+ dst := stackobj(a.dstid);
+ if (dst == nil)
+ panic("animation destination has gone!");
+ dstpt := dst.pos.add(dst.delta.mul(a.index));
+ srcpt := a.srcpt;
+ d := dstpt.sub(srcpt);
+ # don't bother animating if moving to or from a hidden stack.
+ if (!srcpt.eq(Hiddenpos) && !dst.pos.eq(Hiddenpos) && !d.eq((0, 0))) {
+ mag := math->sqrt(real(d.x * d.x + d.y * d.y));
+ (vx, vy) := (real d.x / mag, real d.y / mag);
+ currpt := a.srcpt; # current position of cards
+ t0 := starttime;
+ dt := int (mag / SPEED);
+ t := 0;
+ tickregister(tick);
+ cmd(win, ".c raise " + a.tag);
+ while (t < dt) {
+ s := real t * SPEED;
+ p := Point(srcpt.x + int (s * vx), srcpt.y + int (s * vy));
+ dp := p.sub(currpt);
+ cmd(win, ".c move " + a.tag + " " + string dp.x + " " + string dp.y);
+ currpt = p;
+ t = <-tick - t0;
+ }
+ tickunregister(tick);
+ cmd(win, "update");
+ }
+ a.waitch <-= a;
+}
+
+tickregister(tick: chan of int)
+{
+ tickregisterch <-= tick;
+}
+
+tickunregister(tick: chan of int)
+{
+ tickunregisterch <-= tick;
+}
+
+tickproc(tick: chan of int)
+{
+ for (;;)
+ tick <-= 1;
+}
+
+timeproc()
+{
+ reg: list of chan of int;
+ dummytick := chan of int;
+ realtick := chan of int;
+ tick := dummytick;
+ spawn tickproc(realtick);
+ for (;;) {
+ alt {
+ c := <-tickregisterch =>
+ if (reg == nil)
+ tick = realtick;
+ reg = c :: reg;
+ c := <-tickunregisterch =>
+ r: list of chan of int;
+ for (; reg != nil; reg = tl reg)
+ if (hd reg != c)
+ r = hd reg :: r;
+ reg = r;
+ if (reg == nil)
+ tick = dummytick;
+ <-tick =>
+ t := sys->millisec();
+ for (r := reg; r != nil; r = tl r) {
+ alt {
+ hd r <-= t =>
+ ;
+ * =>
+ ;
+ }
+ }
+ cmd(win, "update");
+ }
+ }
+}
+
+yield()
+{
+ yieldch <-= 1;
+}
+
+yieldproc()
+{
+ for (;;)
+ <-yieldch;
+}
+
+
+# send completed animations down animfinishedch;
+# wait for a reply, which is either a new animation to wait
+# for (the next in the queue) or nil, telling us to exit
+animqueueproc(waitch: chan of ref Animation)
+{
+ rc := chan of chan of ref Animation;
+ while (waitch != nil) {
+ animfinishedch <-= (<-waitch, rc);
+ waitch = <-rc;
+ }
+}
+
+# an animation has finished.
+# move the cards into their final place in the stack,
+# remove the animation from the queue it's on,
+# and inform the mediating process of the next animation process in the queue.
+animterminated(v: (ref Animation, chan of chan of ref Animation))
+{
+ (a, rc) := v;
+ deltag(a.cards, a.tag);
+ dst := stackobj(a.dstid);
+ insertcards(dst, a.cards, a.index);
+ repackobj(dst);
+ cmd(win, "update");
+ q := dst.animq;
+ q.get();
+ if (q.isempty())
+ rc <-= nil;
+ else {
+ a = q.peek();
+ rc <-= a.waitch;
+ }
+}
+
+actrect(win: ref Tk->Toplevel, w: string): Rect
+{
+ r: Rect;
+ r.min.x = int cmd(win, w + " cget -actx") + int cmd(win, w + " cget -bd");
+ r.min.y = int cmd(win, w + " cget -acty") + int cmd(win, w + " cget -bd");
+ r.max.x = r.min.x + int cmd(win, w + " cget -actwidth");
+ r.max.y = r.min.y + int cmd(win, w + " cget -actheight");
+ return r;
+}
+
+actsize(win: ref Tk->Toplevel, w: string): Point
+{
+ return (int cmd(win, w + " cget -actwidth"), int cmd(win, w + " cget -actheight"));
+}
+
+Queue.put(q: self ref Queue, s: T)
+{
+ q.t = s :: q.t;
+}
+
+Queue.get(q: self ref Queue): T
+{
+ s: T;
+ if(q.h == nil){
+ q.h = revlist(q.t);
+ q.t = nil;
+ }
+ if(q.h != nil){
+ s = hd q.h;
+ q.h = tl q.h;
+ }
+ return s;
+}
+
+Queue.peek(q: self ref Queue): T
+{
+ s: T;
+ if (q.isempty())
+ return s;
+ s = q.get();
+ q.h = s :: q.h;
+ return s;
+}
+
+Queue.isempty(q: self ref Queue): int
+{
+ return q.h == nil && q.t == nil;
+}
+
+revlist(ls: list of T) : list of T
+{
+ rs: list of T;
+ for (; ls != nil; ls = tl ls)
+ rs = hd ls :: rs;
+ return rs;
+}
+
+readconfig(): int
+{
+ for (lines := readconfigfile("/icons/cards/config"); lines != nil; lines = tl lines) {
+ t := hd lines;
+ case hd t {
+ "rearborder" =>
+ Rearborder = int hd tl t;
+ "border" =>
+ Border = int hd tl t;
+ "selectborder" =>
+ Selectborder = int hd tl t;
+ "xdelta" =>
+ carddelta.x = int hd tl t;
+ "ydelta" =>
+ carddelta.y = int hd tl t;
+ "font" =>
+ Textfont = hd tl t;
+ "selectcolour" =>
+ Selectcolour = hd tl t;
+ "cardsize" =>
+ if (len t != 3)
+ sys->fprint(stderr, "cards: invalid value for cardsize attribute\n");
+ else
+ cardsize = (int hd tl t, int hd tl tl t);
+ * =>
+ sys->fprint(stderr, "cards: unknown config attribute: %s\n", hd t);
+ }
+ }
+ return 0;
+}
+
+readcardimages(): int
+{
+ (nimages, cardsize) = readimages("/icons/cards", "c");
+ if (nimages == 0) {
+ sys->fprint(stderr, "cards: no card images found\n");
+ return -1;
+ }
+ sys->print("%d card images found\n", nimages);
+
+ (nrears, rearsize) := readimages("/icons/cardrears", "rear");
+ if (nrears > 0 && !rearsize.eq(cardsize)) {
+ sys->fprint(stderr, "cards: card rear sizes don't match card sizes (%s vs %s)\n", p2s(rearsize), p2s(cardsize));
+ return -1;
+ }
+ sys->print("%d card rear images found\n", nrears);
+ cr := Rect((0, 0), cardsize);
+ for (i := nrears; i < len rearcolours; i++) {
+ cmd(win, "image create bitmap rear" + string i);
+ img := drawctxt.display.newimage(cr, Draw->XRGB32, 0, Draw->Black);
+ img.draw(cr.inset(Rearborder),
+ drawctxt.display.color(rearcolours[i] - nrears), nil, (0, 0));
+ tk->putimage(win, "rear" + string i, img, nil);
+ }
+ return 0;
+}
+
+readconfigfile(f: string): list of list of string
+{
+ sys->print("opening config file '%s'\n", f);
+ fd := sys->open(f, Sys->OREAD);
+ if (fd == nil)
+ return nil;
+ buf := array[Sys->ATOMICIO] of byte;
+ nb := sys->read(fd, buf, len buf);
+ if (nb <= 0)
+ return nil;
+ (nil, lines) := sys->tokenize(string buf[0:nb], "\r\n");
+ r: list of list of string;
+ for (; lines != nil; lines = tl lines) {
+ (n, toks) := sys->tokenize(hd lines, " \t");
+ if (n == 0)
+ continue;
+ if (n < 2)
+ sys->fprint(stderr, "cards: invalid config line: %s\n", hd lines);
+ else
+ r = toks :: r;
+ }
+ return r;
+}
+
+fittoscreen(win: ref Tk->Toplevel)
+{
+ Point: import draw;
+ if (win.image == nil || win.image.screen == nil)
+ return;
+ r := win.image.screen.image.r;
+ scrsize := Point((r.max.x - r.min.x), (r.max.y - r.min.y));
+ bd := int cmd(win, ". cget -bd");
+ winsize := Point(int cmd(win, ". cget -actwidth") + bd * 2, int cmd(win, ". cget -actheight") + bd * 2);
+ if (winsize.x > scrsize.x)
+ cmd(win, ". configure -width " + string (scrsize.x - bd * 2));
+ if (winsize.y > scrsize.y)
+ cmd(win, ". configure -height " + string (scrsize.y - bd * 2));
+ actr: Rect;
+ actr.min = Point(int cmd(win, ". cget -actx"), int cmd(win, ". cget -acty"));
+ actr.max = actr.min.add((int cmd(win, ". cget -actwidth") + bd*2,
+ int cmd(win, ". cget -actheight") + bd*2));
+ (dx, dy) := (actr.dx(), actr.dy());
+ if (actr.max.x > r.max.x)
+ (actr.min.x, actr.max.x) = (r.min.x - dx, r.max.x - dx);
+ if (actr.max.y > r.max.y)
+ (actr.min.y, actr.max.y) = (r.min.y - dy, r.max.y - dy);
+ if (actr.min.x < r.min.x)
+ (actr.min.x, actr.max.x) = (r.min.x, r.min.x + dx);
+ if (actr.min.y < r.min.y)
+ (actr.min.y, actr.max.y) = (r.min.y, r.min.y + dy);
+ cmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y);
+}
+
+panic(s: string)
+{
+ sys->fprint(stderr, "cards: panic: %s\n", s);
+ raise "panic";
+}
+
+showtk := 0;
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+ if (showtk)
+ sys->print("tk: %s\n", s);
+ e := tk->cmd(top, s);
+ if (e != nil && e[0] == '!') {
+ sys->fprint(stderr, "tk error %s on '%s'\n", e, s);
+ raise "panic";
+ }
+ return e;
+}
+
+max(a, b: int): int
+{
+ if (a > b)
+ return a;
+ return b;
+}
diff --git a/appl/spree/clients/chat.b b/appl/spree/clients/chat.b
new file mode 100644
index 00000000..d474ef8f
--- /dev/null
+++ b/appl/spree/clients/chat.b
@@ -0,0 +1,194 @@
+implement Clientmod;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+ draw: Draw;
+ Point, Rect, Display, Image: import draw;
+include "tk.m";
+ tk: Tk;
+include "tkclient.m";
+ tkclient: Tkclient;
+include "../client.m";
+include "commandline.m";
+ commandline: Commandline;
+ Cmdline: import commandline;
+
+stderr: ref Sys->FD;
+
+memberid := -1;
+win: ref Tk->Toplevel;
+
+client(ctxt: ref Draw->Context, argv: list of string, nil: int)
+{
+ sys = load Sys Sys->PATH;
+ stderr = sys->fildes(2);
+ draw = load Draw Draw->PATH;
+ tk = load Tk Tk->PATH;
+ tkclient = load Tkclient Tkclient->PATH;
+ if (tkclient == nil) {
+ sys->fprint(stderr, "chat: cannot load %s: %r\n", Tkclient->PATH);
+ sys->raise("fail:bad module");
+ }
+ commandline = load Commandline Commandline->PATH;
+ if (commandline == nil) {
+ sys->fprint(stderr, "chat: cannot load %s: %r\n", Commandline->PATH);
+ sys->raise("fail:bad module");
+ }
+ commandline->init();
+
+ tkclient->init();
+ client1(ctxt);
+}
+cmdlinech: chan of string;
+cmdline: ref Cmdline;
+
+client1(ctxt: ref Draw->Context)
+{
+ cliquefd := sys->fildes(0);
+
+ sys->pctl(Sys->NEWPGRP, nil);
+
+ winctl: chan of string;
+ (win, winctl) = tkclient->toplevel(ctxt.screen, nil,
+ "Cards", Tkclient->Appl);
+ cmdlinech = chan of string;
+
+ srvcmd := chan of string;
+ spawn updateproc(cliquefd, srvcmd);
+
+ for (;;) alt {
+ c := <-cmdlinech =>
+ for (cmds := cmdline.event(c); cmds != nil; cmds = tl cmds)
+ cliquecmd(cliquefd, "say " + quote(hd cmds));
+ c := <-srvcmd =>
+ applyupdate(c);
+ cmd(win, "update");
+ c := <-winctl =>
+ if (c == "exit")
+ sys->write(cliquefd, array[0] of byte, 0);
+ tkclient->wmctl(win, c);
+ }
+}
+
+quote(s: string): string
+{
+ for (i := 0; i < len s; i++)
+ if (s[i] == ' ')
+ s[i] = '_';
+ return s;
+}
+
+unquote(s: string): string
+{
+ for (i := 0; i < len s; i++)
+ if (s[i] == '_')
+ s[i] = ' ';
+ return s;
+}
+
+cliquecmd(fd: ref Sys->FD, s: string): int
+{
+ if (sys->fprint(fd, "%s\n", s) == -1) {
+ sys->print("chat: cmd error on '%s': %r\n", s);
+ return 0;
+ }
+ return 1;
+}
+
+
+updateproc(fd: ref Sys->FD, srvcmd: chan of string)
+{
+ wfd := sys->open("/prog/" + string sys->pctl(0, nil) + "/wait", Sys->OREAD);
+ spawn updateproc1(fd, srvcmd);
+ buf := array[Sys->ATOMICIO] of byte;
+ n := sys->read(wfd, buf, len buf);
+ sys->print("updateproc process exited: %s\n", string buf[0:n]);
+}
+
+updateproc1(fd: ref Sys->FD, srvcmd: chan of string)
+{
+ buf := array[Sys->ATOMICIO] of byte;
+ while ((n := sys->read(fd, buf, len buf)) > 0) {
+ (nil, lines) := sys->tokenize(string buf[0:n], "\n");
+ for (; lines != nil; lines = tl lines)
+ srvcmd <-= hd lines;
+ }
+ if (n < 0)
+ sys->fprint(stderr, "chat: error reading updates: %r\n");
+ sys->fprint(stderr, "chat: updateproc exiting\n");
+}
+
+
+applyupdate(s: string)
+{
+ (nt, toks) := sys->tokenize(s, " ");
+ case hd toks {
+ "memberid" =>
+ # memberid clientid memberid name
+ memberid = int hd tl tl toks;
+ cmd(win, "frame .me");
+ cmd(win, "label .me.l -text {Type here}");
+ (cmdline, cmdlinech) = Cmdline.new(win, ".me.f", nil);
+ cmd(win, "pack .me -side top -fill x");
+ cmd(win, "pack .me.l -side top");
+ cmd(win, "pack .me.f -side top -fill both -expand 1 -anchor w");
+
+ "joinclique" =>
+ # joinclique cliqueid clientid memberid name
+ id := int hd tl tl tl toks;
+ name := hd tl tl tl tl toks;
+ if (id == memberid)
+ break;
+ f := "." + string id;
+ cmd(win, "frame " + f);
+ cmd(win, "label " + f + ".l -text '" + name);
+ tf := f + ".tf";
+ cmd(win, "frame " + tf);
+ cmd(win, "scrollbar " + tf + ".s -orient vertical -command {" + tf + ".t yview}");
+ cmd(win, "text " + tf + ".t -height 5h");
+ cmd(win, "pack " + f + ".l -side top");
+ cmd(win, "pack " + tf + ".s -side left -fill y");
+ cmd(win, "pack " + tf + ".t -side top -fill both -expand 1");
+ cmd(win, "pack " + tf + " -side top -fill both -expand 1");
+ cmd(win, "pack " + f + " -side top -fill both -expand 1");
+
+ "say" =>
+ # say memberid text
+ id := int hd tl toks;
+ if (id == memberid)
+ break;
+ t := "." + string id + ".tf.t";
+ cmd(win, t + " insert end '" + unquote(hd tl tl toks) + "\n");
+ cmd(win, t + " see end");
+ * =>
+ sys->fprint(stderr, "chat: unknown update message '%s'\n", s);
+ }
+}
+
+concat(v: list of string): string
+{
+ if (v == nil)
+ return nil;
+ s := hd v;
+ for (v = tl v; v != nil; v = tl v)
+ s += " " + hd v;
+ return s;
+}
+
+kill(pid: int)
+{
+ if ((fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
+ sys->write(fd, array of byte "kill", 4);
+}
+
+showtk := 0;
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+ if (showtk)
+ sys->print("tk: %s\n", s);
+ e := tk->cmd(top, s);
+ if (e != nil && e[0] == '!')
+ sys->fprint(stderr, "tk error %s on '%s'\n", e, s);
+ return e;
+}
+
diff --git a/appl/spree/clients/gather.b b/appl/spree/clients/gather.b
new file mode 100644
index 00000000..113985b3
--- /dev/null
+++ b/appl/spree/clients/gather.b
@@ -0,0 +1,178 @@
+implement Gather;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+ draw: Draw;
+ Point, Rect, Display, Image, Font: import draw;
+include "tk.m";
+ tk: Tk;
+include "tkclient.m";
+ tkclient: Tkclient;
+include "commandline.m";
+ commandline: Commandline;
+ Cmdline: import commandline;
+include "sh.m";
+
+Gather: module {
+ init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+CLIENTDIR: con "/dis/spree/clients";
+
+drawctxt: ref Draw->Context;
+cliquefd: ref Sys->FD;
+stderr: ref Sys->FD;
+
+mnt, dir: string;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+ sys = load Sys Sys->PATH;
+ stderr = sys->fildes(2);
+ draw = load Draw Draw->PATH;
+ tk = load Tk Tk->PATH;
+ tkclient = load Tkclient Tkclient->PATH;
+ if (tkclient == nil) {
+ sys->fprint(stderr, "gather: cannot load %s: %r\n", Tkclient->PATH);
+ raise "fail:bad module";
+ }
+ tkclient->init();
+ commandline = load Commandline Commandline->PATH;
+ if(commandline == nil) {
+ sys->fprint(stderr, "gather: cannot load %s: %r\n", Commandline->PATH);
+ raise "fail:bad module";
+ }
+ commandline->init();
+ drawctxt = ctxt;
+ cliquefd = sys->fildes(0);
+
+ if (len argv >= 3) {
+ mnt = hd tl argv;
+ dir = hd tl tl argv;
+ } else
+ sys->fprint(stderr, "gather: expected mnt, dir args\n");
+ client1();
+}
+
+client1()
+{
+ (win, winctl) := tkclient->toplevel(drawctxt, nil, "Gathering", Tkclient->Appl);
+ ech := chan of string;
+ tk->namechan(win, ech, "e");
+ (chat, chatevent) := Cmdline.new(win, ".chat", nil);
+ updatech := chan of string;
+ spawn readproc(updatech);
+
+ cmd(win, "button .b -text Start -command {send e start}");
+ cmd(win, "pack .b -side top -anchor w");
+ cmd(win, "pack .chat -fill both -expand 1");
+ cmd(win, "pack propagate . 0");
+ cmd(win, "update");
+ tkclient->onscreen(win, nil);
+ tkclient->startinput(win, "kbd"::"ptr"::nil);
+ for (;;) alt {
+ s := <-win.ctxt.kbd =>
+ tk->keyboard(win, s);
+ s := <-win.ctxt.ptr =>
+ tk->pointer(win, *s);
+ s := <-win.ctxt.ctl or
+ s = <-win.wreq or
+ s = <-winctl =>
+ tkclient->wmctl(win, s);
+ line := <-updatech =>
+ (n, toks) := sys->tokenize(line, " ");
+ if (toks == nil)
+ continue;
+ case hd toks {
+ "clienttype" =>
+ chat.addtext("starting " + hd tl toks + " session...\n");
+ cmd(win, "update");
+ path := CLIENTDIR + "/" + hd tl toks + ".dis";
+ mod := load Command path;
+ if (mod == nil) {
+ chat.addtext(sys->sprint("could not load %s: %r\n", path));
+ chat.addtext("bye bye\n");
+ cliquefd = nil;
+ } else {
+ win = nil;
+ chat = nil;
+ startclient(mod, hd tl toks :: mnt :: dir :: tl tl toks);
+ exit;
+ }
+ "chat" =>
+ chat.addtext(hd tl toks + ": " + concat(tl tl toks) + "\n");
+ "title" =>
+ tkclient->settitle(win, "Gather " + concat(tl toks));
+ "join" or
+ "leave" or
+ "watch" or
+ "unwatch" =>
+ chat.addtext(line + "\n");
+ * =>
+ chat.addtext("unknown update: " + line + "\n");
+ }
+ cmd(win, "update");
+ c := <-chatevent =>
+ lines := chat.event(c);
+ for (; lines != nil; lines = tl lines)
+ cliquecmd("chat " + hd lines, chat);
+ c := <-ech =>
+ cliquecmd(c, chat);
+ }
+}
+
+cliquecmd(s: string, chat: ref Cmdline)
+{
+ if (sys->fprint(cliquefd, "%s", s) == -1) {
+ chat.addtext(sys->sprint("command failed: %r\n"));
+ cmd(chat.top, "update");
+ }
+}
+
+prefixed(s: string, prefix: string): int
+{
+ return len s >= len prefix && s[0:len prefix] == prefix;
+}
+
+readproc(updatech: chan of string)
+{
+ buf := array[Sys->ATOMICIO] of byte;
+ while ((n := sys->read(cliquefd, buf, Sys->ATOMICIO)) > 0) {
+ (nil, lines) := sys->tokenize(string buf[0:n], "\n");
+ for (; lines != nil; lines = tl lines) {
+ updatech <-= hd lines;
+ if (prefixed(hd lines, "clienttype"))
+ exit;
+ }
+ }
+ updatech <-= nil;
+}
+
+startclient(mod: Command, argv: list of string)
+{
+ {
+ mod->init(drawctxt, argv);
+ } exception e {
+ "*" =>
+ sys->print("client %s broken: %s\n", hd argv, e);
+ }
+}
+
+cmd(win: ref Tk->Toplevel, s: string): string
+{
+ r := tk->cmd(win, s);
+ if(len r > 0 && r[0] == '!')
+ sys->print("error executing '%s': %s\n", s, r[1:]);
+ return r;
+}
+
+concat(l: list of string): string
+{
+ if (l == nil)
+ return nil;
+ s := hd l;
+ for (l = tl l; l != nil; l = tl l)
+ s += " " + hd l;
+ return s;
+}
diff --git a/appl/spree/clients/lobby.b b/appl/spree/clients/lobby.b
new file mode 100644
index 00000000..1af52827
--- /dev/null
+++ b/appl/spree/clients/lobby.b
@@ -0,0 +1,562 @@
+implement Lobby;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+ draw: Draw;
+ Point, Rect, Display, Image, Font: import draw;
+include "tk.m";
+ tk: Tk;
+include "tkclient.m";
+ tkclient: Tkclient;
+include "../join.m";
+ join: Join;
+include "dividers.m";
+ dividers: Dividers;
+ Divider: import dividers;
+include "commandline.m";
+ commandline: Commandline;
+ Cmdline: import commandline;
+include "sh.m";
+
+Lobby: module {
+ init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+CLIENTDIR: con "/dis/spree/clients";
+NAMEFONT: con "/fonts/charon/plain.small.font";
+TITLEFONT: con "/fonts/charon/bold.normal.font";
+HEADERFONT: con "/fonts/charon/italic.normal.font";
+
+Object: adt {
+ id: int;
+ pick {
+ Session =>
+ filename: string;
+ owner: string;
+ invitations: list of string;
+ members: list of string;
+ invited: int;
+ Sessiontype =>
+ start: string;
+ name: string;
+ title: string;
+ clienttype: string;
+ Invite =>
+ session: ref Object.Session;
+ name: string;
+ Member =>
+ parentid: int;
+ name: string;
+ Archive =>
+ Other =>
+ }
+};
+
+drawctxt: ref Draw->Context;
+cliquefd: ref Sys->FD;
+objects: array of ref Object;
+myname: string;
+maxid := 0;
+
+badmodule(m: string)
+{
+ sys->fprint(sys->fildes(2), "lobby: cannot load %s: %r\n", m);
+ raise "fail:bad module";
+}
+
+init(ctxt: ref Draw->Context, nil: list of string)
+{
+ sys = load Sys Sys->PATH;
+ draw = load Draw Draw->PATH;
+ tk = load Tk Tk->PATH;
+
+ tkclient = load Tkclient Tkclient->PATH;
+ if (tkclient == nil)
+ badmodule(Tkclient->PATH);
+ tkclient->init();
+
+ commandline = load Commandline Commandline->PATH;
+ if(commandline == nil)
+ badmodule(Commandline->PATH);
+ commandline->init();
+
+ dividers = load Dividers Dividers->PATH;
+ if (dividers == nil)
+ badmodule(Dividers->PATH);
+ dividers->init();
+
+ join = load Join Join->PATH;
+ if (join == nil)
+ badmodule(Join->PATH);
+
+ drawctxt = ctxt;
+ cliquefd = sys->fildes(0);
+ sys->pctl(Sys->NEWPGRP, nil);
+ client1();
+}
+
+columns := array[] of {("name", ""), ("members", ""), ("watch", "Watch"), ("join", "Join"), ("invite", "Invite")};
+
+reqwidth(win: ref Tk->Toplevel, w: string): int
+{
+ return 2 * int cmd(win, w + " cget -bd") + int cmd(win, w + " cget -width");
+}
+
+client1()
+{
+ (win, winctl) := tkclient->toplevel(drawctxt, nil, "Lobby", Tkclient->Appl);
+ ech := chan of string;
+ tk->namechan(win, ech, "e");
+ (chat, chatevent) := Cmdline.new(win, ".d2", nil);
+ updatech := chan of list of string;
+ spawn readproc(updatech);
+
+ cmd(win, "frame .buts");
+ cmd(win, "menubutton .buts.start -text New -menu .buts.start.m");
+ cmd(win, "menu .buts.start.m");
+ cmd(win, "pack .buts.start -side left");
+ cmd(win, "button .buts.kick -text Kick -command {send e kick}");
+ cmd(win, "pack .buts.kick -side left");
+ cmd(win, "pack .buts -side top -fill x");
+
+ cmd(win, "frame .d1");
+
+ cmd(win, "scrollbar .d1.s -orient vertical -command {.d1.c yview}");
+ cmd(win, "canvas .d1.c -yscrollcommand {.d1.s set}");
+ cmd(win, "pack .d1.s -side left -fill y");
+ cmd(win, "pack .d1.c -side top -fill both -expand 1");
+ cmd(win, "frame .t");
+ cmd(win, ".d1.c create window 0 0 -anchor nw -window .t");
+ cmd(win, "frame .t.f1 -bd 2 -relief sunken");
+ cmd(win, "pack .t.f1 -side top -fill both -expand 1");
+
+ cmd(win, "label .t.f1.sessionlabel -text Sessions -font " + TITLEFONT);
+ cmd(win, "pack .t.f1.sessionlabel");
+ cmd(win, "frame .t.s");
+ cmd(win, "pack .t.s -in .t.f1 -side top -fill both -expand 1");
+
+ cmd(win, "frame .t.f2 -bd 2 -relief sunken");
+ cmd(win, "label .t.archiveslabel -text Archives -font " + TITLEFONT);
+ cmd(win, "pack .t.archiveslabel");
+ cmd(win, "frame .t.a");
+ cmd(win, "pack .t.a -in .t.f2 -side top -fill both -expand 1 -anchor w");
+ cmd(win, "pack .t.f2 -side top -fill both -expand 1");
+
+ cmd(win, "label .t.a.title0 -text Title -font " + HEADERFONT);
+ cmd(win, "label .t.a.title1 -text Members -font " + HEADERFONT);
+ cmd(win, "grid .t.a.title0 .t.a.title1 -sticky w");
+ cmd(win, "grid columnconfigure .t.a 1 -weight 1");
+
+ cmd(win, "bind .t <Configure> {.d1.c configure -scrollregion {0 0 [.t cget -width] [.t cget -height]}}");
+
+ cmd(win, "button .tmp");
+ for (i := 0; i < len columns; i++) {
+ (name, mintext) := columns[i];
+ cmd(win, ".tmp configure -text '" + mintext);
+ cmd(win, "grid columnconfigure .t.s " + string i +
+ " -name " + name +
+ " -minsize " + string reqwidth(win, ".tmp"));
+ }
+ cmd(win, "grid columnconfigure .t.s members -weight 1");
+ cmd(win, "destroy .tmp");
+ cmd(win, "menu .invite");
+
+ (divider, dividerevent) := Divider.new(win, ".d", ".d1" :: ".d2" :: nil, Dividers->NS);
+ cmd(win, "pack .d -side top -fill both");
+ cmd(win, "pack propagate . 0");
+ tkclient->onscreen(win, nil);
+ tkclient->startinput(win, "kbd"::"ptr"::nil);
+ for (;;) {
+ alt {
+ s := <-win.ctxt.kbd =>
+ tk->keyboard(win, s);
+ s := <-win.ctxt.ptr =>
+ tk->pointer(win, *s);
+ s := <-win.ctxt.ctl or
+ s = <-win.wreq or
+ s = <-winctl =>
+ tkclient->wmctl(win, s);
+ c := <-dividerevent =>
+ divider.event(c);
+ c := <-chatevent =>
+ lines := chat.event(c);
+ for (; lines != nil; lines = tl lines) {
+ line := hd lines;
+ if (len line > 0 && line[len line-1]=='\n')
+ line = line[0:len line-1];
+ cliquecmd("chat " + line);
+ }
+ lines := <-updatech =>
+#sys->print("++\n");
+ for (; lines != nil; lines = tl lines) {
+#sys->print("+%s\n", hd lines);
+ doupdate(win, chat, hd lines);
+ }
+ cmd(win, "update");
+ c := <-ech =>
+ (n, toks) := sys->tokenize(c, " ");
+ case hd toks {
+ "watch" =>
+ joinclique(win, chat, int hd tl toks, "watch");
+ "join" =>
+ joinclique(win, chat, int hd tl toks, "join");
+ "start" =>
+ start(win, chat, int hd tl toks);
+ "postinvite" =>
+ postinvite(win, int hd tl toks, hd tl tl toks);
+ "unarchive" =>
+ e := cliquecmd("unarchive " + hd tl toks);
+ if (e != nil)
+ chat.addtext("failed to unarchive: " + e + "\n");
+ "invite" =>
+ # invite sessionid name
+ (id, name) := (hd tl toks, hd tl tl toks);
+ vname := "inv." + name;
+ v := int cmd(win, "variable " + vname);
+ s := "invite";
+ if (!v)
+ s = "uninvite";
+ e := cliquecmd(s + " " + string id + " " + name);
+ if (e != nil) {
+ chat.addtext("invite failed: " + e + "\n");
+ cmd(win, "variable " + vname + " " + string !v);
+ }
+ "kick" =>
+ e := cliquecmd("kick");
+ if (e != nil)
+ chat.addtext("kick failed: " + e + "\n");
+ * =>
+ sys->print("unknown msg %s\n", c);
+ }
+ cmd(win, "update");
+ }
+ }
+}
+
+joinclique(nil: ref Tk->Toplevel, chat: ref Cmdline, id: int, how: string)
+{
+ pick o := objects[id] {
+ Session =>
+ e := join->join(drawctxt, "/n/remote", o.filename, how);
+ if (e != nil)
+ chat.addtext("couldn't join clique: " + e + "\n");
+ else
+ chat.addtext("joined clique ok\n");
+ * =>
+ sys->print("join bad id %d (type %d)\n", id, tagof objects[id]);
+ }
+}
+
+start(nil: ref Tk->Toplevel, chat: ref Cmdline, id: int)
+{
+ pick o := objects[id] {
+ Sessiontype =>
+ e := cliquecmd("start " + o.start);
+ if (e != nil)
+ chat.addtext("failed to start clique: " + e + "\n");
+ * =>
+ sys->print("start bad id %d (type %d)\n", id, tagof objects[id]);
+ }
+}
+
+postinvite(win: ref Tk->Toplevel, id: int, widget: string)
+{
+ pick o := objects[id] {
+ Session =>
+ cmd(win, ".invite delete 0 end");
+ cmd(win, ".invite add checkbutton -text All -variable inv.all -command {send e invite " + string id + " all}");
+ for (invites := o.invitations; invites != nil; invites = tl invites)
+ if (hd invites == "all")
+ break;
+ cmd(win, "variable inv.all " + string (invites != nil));
+
+ for (i := 0; i < len objects; i++) {
+ if (objects[i] == nil)
+ continue;
+ pick p := objects[i] {
+ Member =>
+ if (tagof(objects[p.parentid]) != tagof(Object.Session) && p.name != o.owner) {
+ for (invites = o.invitations; invites != nil; invites = tl invites)
+ if (hd invites == p.name)
+ break;
+ invited := invites != nil;
+ cmd(win, "variable inv." + p.name + " " + string invited);
+ cmd(win, ".invite add checkbutton -variable inv." + p.name +
+ " -command {send e invite " + string id + " " + p.name + "}" +
+ " -text '" + p.name);
+ }
+ }
+ }
+ x := int cmd(win, widget + " cget -actx");
+ y := int cmd(win, widget + " cget -acty");
+ h := 2 * int cmd(win, widget + " cget -bd") + int cmd(win, widget + " cget -actheight");
+ cmd(win, ".invite post " + string x + " " + string (y + h));
+ * =>
+ sys->print("bad invited id %d (type %d)\n", id, tagof objects[id]);
+ }
+}
+
+panic(s: string)
+{
+ sys->print("lobby panic: %s\n", s);
+ raise "panic";
+}
+
+doupdate(win: ref Tk->Toplevel, chat: ref Cmdline, line: string)
+{
+ (n, toks) := sys->tokenize(line, " ");
+ if (n == 0)
+ return;
+ case hd toks {
+ "chat" =>
+ chat.addtext(sys->sprint("%s: %s\n", hd tl toks, concat(tl tl toks)));
+ "create" =>
+ # create id parentid vis type
+ id := int hd tl toks;
+ if (id >= len objects)
+ objects = (array[len objects + 10] of ref Object)[0:] = objects;
+ if (objects[id] != nil)
+ panic(sys->sprint("object %d already exists!", id));
+ parentid := int hd tl tl toks;
+ objtype := tl tl tl tl toks;
+ o: ref Object;
+ case hd objtype {
+ "sessiontype" =>
+ o = ref Object.Sessiontype(id, nil, nil, nil, nil);
+ "session" =>
+ cmd(win, "grid rowinsert .t.s 0");
+ cmd(win, "grid rowconfigure .t.s 0 -name id" + string id);
+ f := ".t.s.f" + string id;
+ cmd(win, "frame " + f); # dummy, so we can destroy row easily
+ cmd(win, "label "+f+".name");
+ cmd(win, "grid "+f+".name -row id" + string id + " -column name -in .t.s");
+ cmd(win, "button "+f+".watch -text Watch -command {send e watch " + string id + "}");
+ cmd(win, "grid "+f+".watch -row id" + string id + " -column watch -in .t.s");
+ cmd(win, "label "+f+".members -font " + NAMEFONT);
+ cmd(win, "grid "+f+".members -row id" + string id + " -column members -in .t.s");
+ o = ref Object.Session(id, nil, nil, nil, nil, 0);
+ "member" =>
+ o = ref Object.Member(id, parentid, nil);
+ "invite" =>
+ pick parent := objects[parentid] {
+ Session =>
+ o = ref Object.Invite(id, parent, nil);
+ * =>
+ panic("invite not under session");
+ }
+ "archive" =>
+ cmd(win, "grid rowinsert .t.a 1");
+ cmd(win, "grid rowconfigure .t.a 1 -name id" + string id);
+ f := ".t.a.f" + string id;
+ cmd(win, "frame " + f);
+ cmd(win, "label "+f+".name");
+ cmd(win, "grid "+f+".name -row id" + string id + " -column 0 -in .t.a -sticky w");
+ cmd(win, "label "+f+".members -anchor w -font " + NAMEFONT);
+ cmd(win, "grid "+f+".members -row id" + string id + " -column 1 -in .t.a -sticky ew");
+ cmd(win, "button "+f+".unarchive -text Unarchive -command {send e unarchive " + string id + "}");
+ cmd(win, "grid "+f+".unarchive -row id" + string id + " -column 2 -in .t.a");
+ o = ref Object.Archive(id);
+ * =>
+ o = ref Object.Other(id);
+ }
+ objects[id] = o;
+
+ "del" =>
+ # del parent start end objs...
+ for (objs := tl tl tl tl toks; objs != nil; objs = tl objs) {
+ id := int hd objs;
+ pick o := objects[id] {
+ Session =>
+ cmd(win, "grid rowdelete .t.s id" + string id);
+ cmd(win, "destroy .t.s.f" + string id);
+ Archive =>
+ cmd(win, "grid rowdelete .t.a id" + string id);
+ cmd(win, "destroy .t.a.f" + string id);
+ Sessiontype =>
+ sys->print("cannot destroy sessiontypes yet\n");
+ Member =>
+ pick parent := objects[o.parentid] {
+ Session =>
+ parent.members = removeitem(parent.members, o.name);
+ cmd(win, sys->sprint(".t.s.f%d.members configure -text '%s", o.parentid, concat(parent.members)));
+ * =>
+ chat.addtext(o.name + " has left\n");
+ }
+ Invite =>
+ s := o.session;
+ invites := s.invitations;
+ invited := 0;
+ for (s.invitations = nil; invites != nil; invites = tl invites) {
+ inv := hd invites;
+ if (inv != o.name) {
+ s.invitations = inv :: s.invitations;
+ if (inv == "all" || inv == myname)
+ invited = 1;
+ }
+ }
+ if (!invited && s.invited) {
+ cmd(win, "destroy .t.s.f" + hd tl toks + ".join");
+ s.invited = 0;
+ }
+ }
+ objects[id] = nil;
+ }
+
+ "name" =>
+ myname = hd tl toks;
+ tkclient->settitle(win, "Lobby (" + myname + ")");
+
+ "set" =>
+ # set obj attr val
+ id := int hd tl toks;
+ (attr, val) := (hd tl tl toks, tl tl tl toks);
+ pick o := objects[id] {
+ Session =>
+ f := ".t.s.f" + string id;
+ case attr {
+ "filename" =>
+ o.filename = hd val;
+ "owner" =>
+ if (hd val == myname) {
+ cmd(win, "label "+f+".invite -text Invite -bd 2 -relief raised");
+ cmd(win, "bind "+f+".invite <Button-1> {send e postinvite " + string id + " %W}");
+ cmd(win, "grid "+f+".invite -row id" + string id + " -column invite -in .t.s");
+ }
+ o.owner = hd val;
+ "title" =>
+ cmd(win, f + ".name configure -text '" + concat(val));
+ }
+ Archive =>
+ f := ".t.a.f" + string id;
+ case attr {
+ "name" =>
+ cmd(win, f + ".name configure -text '" + concat(val));
+ "members" =>
+ cmd(win, f + ".members configure -text '" + concat(val));
+ }
+ Sessiontype =>
+ case attr {
+ "start" =>
+ o.start = concat(val);
+ "clienttype" =>
+ o.clienttype = hd val;
+ "title" =>
+ if (o.title != nil)
+ panic("can't change sessiontype name!");
+ else {
+ o.title = concat(val);
+ cmd(win, ".buts.start.m add command" +
+ " -command {send e start " + string id + "}" +
+ " -text '" + o.title);
+ }
+ "name" =>
+ o.name = hd val;
+ }
+ Member =>
+ case attr {
+ "name" =>
+ if (o.name != nil)
+ panic("cannot change member name!");
+ o.name = hd val;
+ pick parent := objects[o.parentid] {
+ Session =>
+ parent.members = o.name :: parent.members;
+ cmd(win, sys->sprint(".t.s.f%d.members configure -text '%s", o.parentid, concat(parent.members)));
+ * =>
+ chat.addtext(o.name + " has arrived\n");
+ }
+ }
+ Invite =>
+ case attr {
+ "name" =>
+ o.name = hd val;
+ s := o.session;
+ sid := string s.id;
+ f := ".t.s.f" + sid;
+ invited := o.name == myname || o.name == "all";
+ s.invitations = o.name :: s.invitations;
+ if (invited && !s.invited) {
+ cmd(win, "button "+f+".join -text Join -command {send e join " + sid + "}");
+ cmd(win, "grid "+f+".join -row id" + sid + " -column join -in .t.s");
+ s.invited = 1;
+ }
+ }
+ }
+ }
+}
+
+removeitem(l: list of string, i: string): list of string
+{
+ rl: list of string;
+ for (; l != nil; l = tl l)
+ if (hd l != i)
+ rl = hd l :: rl;
+ return rl;
+}
+
+numsplit(s: string): (string, int)
+{
+ for (i := len s - 1; i >= 0; i--)
+ if (s[i] < '0' || s[i] > '9')
+ break;
+ if (i == len s -1)
+ return (s, 0);
+ return (s[0:i+1], int s[i+1:]);
+}
+
+cliquecmd(s: string): string
+{
+ if (sys->fprint(cliquefd, "%s", s) == -1) {
+ e := sys->sprint("%r");
+ sys->print("error on '%s': %s\n", s, e);
+ return e;
+ }
+ return nil;
+}
+
+prefixed(s: string, prefix: string): int
+{
+ return len s >= len prefix && s[0:len prefix] == prefix;
+}
+
+readproc(updatech: chan of list of string)
+{
+ buf := array[Sys->ATOMICIO] of byte;
+ while ((n := sys->read(cliquefd, buf, Sys->ATOMICIO)) > 0) {
+ (nil, lines) := sys->tokenize(string buf[0:n], "\n");
+ if (lines != nil)
+ updatech <-= lines;
+ }
+ updatech <-= nil;
+}
+
+startclient(mod: Command, argv: list of string)
+{
+ {
+ mod->init(drawctxt, argv);
+ } exception e {
+ "*" =>
+ sys->print("client %s broken: %s\n", hd argv, e);
+ exit;
+ }
+ mod->init(drawctxt, argv);
+}
+
+cmd(win: ref Tk->Toplevel, s: string): string
+{
+ r := tk->cmd(win, s);
+ if(len r > 0 && r[0] == '!')
+ sys->print("error executing '%s': %s\n", s, r[1:]);
+ return r;
+}
+
+concat(l: list of string): string
+{
+ if (l == nil)
+ return nil;
+ s := hd l;
+ for (l = tl l; l != nil; l = tl l)
+ s += " " + hd l;
+ return s;
+}
diff --git a/appl/spree/clients/othello.b b/appl/spree/clients/othello.b
new file mode 100644
index 00000000..2a146b8e
--- /dev/null
+++ b/appl/spree/clients/othello.b
@@ -0,0 +1,270 @@
+implement Othello;
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+ draw: Draw;
+ Point, Rect: import draw;
+include "tk.m";
+ tk: Tk;
+include "tkclient.m";
+ tkclient: Tkclient;
+
+SQ: con 30; # Square size in pixels
+N: con 8;
+
+stderr: ref Sys->FD;
+
+Othello: module {
+ init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Black, White, Nocolour: con iota;
+colours := array[] of {White => "white", Black => "black"};
+
+win: ref Tk->Toplevel;
+board: array of array of int;
+notifypid := -1;
+membername: string;
+membernames := array[2] of string;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+ sys = load Sys Sys->PATH;
+ stderr = sys->fildes(2);
+ draw = load Draw Draw->PATH;
+ tk = load Tk Tk->PATH;
+ tkclient = load Tkclient Tkclient->PATH;
+ if (tkclient == nil) {
+ sys->fprint(stderr, "othello: cannot load %s: %r\n", Tkclient->PATH);
+ raise "fail:bad module";
+ }
+ tkclient->init();
+
+ if (len argv >= 3) { # argv: modname mnt dir ...
+ membername = readfile(hd tl argv + "/name");
+ sys->print("name is %s\n", membername);
+ }
+ client1(ctxt);
+}
+
+configcmds := array[] of {
+"canvas .c -height " + string (SQ * N) + " -width " + string (SQ * N) + " -bg green",
+"label .status -text {No clique in progress}",
+"frame .f",
+"label .f.l -text {watching} -bg white",
+"label .f.turn -text {}",
+"pack .f.l -side left -expand 1 -fill x",
+"pack .f.turn -side left -fill x -expand 1",
+"pack .c -side top",
+"pack .status .f -side top -fill x",
+"bind .c <ButtonRelease-1> {send cmd b1up %x %y}",
+};
+
+client1(ctxt: ref Draw->Context)
+{
+ cliquefd := sys->fildes(0);
+
+ sys->pctl(Sys->NEWPGRP, nil);
+
+ winctl: chan of string;
+ (win, winctl) = tkclient->toplevel(ctxt, nil,
+ "Othello", Tkclient->Appl);
+ bcmd := chan of string;
+ tk->namechan(win, bcmd, "cmd");
+ for (i := 0; i < len configcmds; i++)
+ cmd(win, configcmds[i]);
+
+ for (i = 0; i < N; i++)
+ for (j := 0; j < N; j++)
+ cmd(win, ".c create rectangle " + r2s(square(i, j)));
+ board = array[N] of {* => array[N] of {* => Nocolour}};
+ tkclient->onscreen(win, nil);
+ tkclient->startinput(win, "ptr"::"kbd"::nil);
+ spawn updateproc(cliquefd);
+
+ for (;;) alt {
+ c := <-bcmd =>
+ (n, toks) := sys->tokenize(c, " ");
+ case hd toks {
+ "b1up" =>
+ (inboard, x, y) := boardpos((int hd tl toks, int hd tl tl toks));
+ if (!inboard)
+ break;
+ othellocmd(cliquefd, "move " + string x + " " + string y);
+ cmd(win, "update");
+ }
+ s := <-win.ctxt.kbd =>
+ tk->keyboard(win, s);
+ s := <-win.ctxt.ptr =>
+ tk->pointer(win, *s);
+ s := <-win.ctxt.ctl or
+ s = <-win.wreq or
+ s = <-winctl =>
+ if (s == "exit")
+ sys->write(cliquefd, array[0] of byte, 0);
+ tkclient->wmctl(win, s);
+ }
+}
+
+othellocmd(fd: ref Sys->FD, s: string): int
+{
+ if (sys->fprint(fd, "%s\n", s) == -1) {
+ notify(sys->sprint("%r"));
+ return 0;
+ }
+ return 1;
+}
+
+updateproc(cliquefd: ref Sys->FD)
+{
+ buf := array[Sys->ATOMICIO] of byte;
+ while ((n := sys->read(cliquefd, buf, len buf)) > 0) {
+ (nil, lines) := sys->tokenize(string buf[0:n], "\n");
+ for (; lines != nil; lines = tl lines)
+ applyupdate(hd lines);
+ cmd(win, "update");
+ }
+ if (n < 0)
+ sys->fprint(stderr, "othello: error reading updates: %r\n");
+ sys->fprint(stderr, "othello: updateproc exiting\n");
+}
+
+applyupdate(s: string)
+{
+ (nt, toks) := sys->tokenize(s, " ");
+ case hd toks {
+ "create" =>
+ ; # ignore - there's only one object (the board)
+ "set" =>
+ # set objid attr val
+ toks = tl tl toks;
+ (attr, val) := (hd toks, hd tl toks);
+ case attr {
+ "members" =>
+ membernames[Black] = hd tl toks;
+ membernames[White] = hd tl tl toks;
+ status(membernames[Black]+ "(Black) vs. " + string membernames[White] + "(White)");
+ if (membername == membernames[Black])
+ cmd(win, ".f.l configure -text Black");
+ else if (membername == membernames[White])
+ cmd(win, ".f.l configure -text White");
+ "turn" =>
+ turn := int val;
+ if (turn != Nocolour) {
+ if (membername == membernames[turn])
+ cmd(win, ".f.turn configure -text {(Your turn)}");
+ else if (membername == membernames[!turn])
+ cmd(win, ".f.turn configure -text {}");
+ }
+ "winner" =>
+ text := "it was a draw";
+ winner := int val;
+ if (winner != Nocolour)
+ text = colours[int val] + " won.";
+ status("clique over. " + text);
+ cmd(win, ".f.l configure -text {watching}");
+ * =>
+ (x, y) := (attr[0] - 'a', attr[1] - 'a');
+ set(x, y, int val);
+ }
+ * =>
+ sys->fprint(stderr, "othello: unknown update message '%s'\n", s);
+ }
+}
+
+status(s: string)
+{
+ cmd(win, ".status configure -text '" + s);
+}
+
+itemopts(colour: int): string
+{
+ return "-fill " + colours[colour] +
+ " -outline " + colours[!colour];
+}
+
+set(x, y, colour: int)
+{
+ id := piece(x, y);
+ if (colour == Nocolour)
+ cmd(win, ".c delete " + id);
+ else if (board[x][y] != Nocolour)
+ cmd(win, ".c itemconfigure " + id + " " + itemopts(colour));
+ else
+ cmd(win, ".c create oval " + r2s(square(x, y)) + " " +
+ itemopts(colour) +
+ " -tags {piece " + id + "}");
+ board[x][y] = colour;
+}
+
+notify(s: string)
+{
+ kill(notifypid);
+ sync := chan of int;
+ spawn notifyproc(s, sync);
+ notifypid = <-sync;
+}
+
+notifyproc(s: string, sync: chan of int)
+{
+ sync <-= sys->pctl(0, nil);
+ cmd(win, ".c delete notify");
+ id := cmd(win, ".c create text 0 0 -anchor nw -fill red -tags notify -text '" + s);
+ bbox := cmd(win, ".c bbox " + id);
+ cmd(win, ".c create rectangle " + bbox + " -fill #ffffaa -tags notify");
+ cmd(win, ".c raise " + id);
+ cmd(win, "update");
+ sys->sleep(750);
+ cmd(win, ".c delete notify");
+ cmd(win, "update");
+ notifypid = -1;
+}
+
+boardpos(p: Point): (int, int, int)
+{
+ (x, y) := (p.x / SQ, p.y / SQ);
+ if (x < 0 || x > N - 1 || y < 0 || y > N - 1)
+ return (0, 0, 0);
+ return (1, x, y);
+}
+
+square(x, y: int): Rect
+{
+ return ((SQ*x, SQ*y), (SQ*(x + 1), SQ*(y + 1)));
+}
+
+piece(x, y: int): string
+{
+ return "p" + string x + "." + string y;
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+ e := tk->cmd(top, s);
+ if (e != nil && e[0] == '!')
+ sys->fprint(stderr, "tk error %s on '%s'\n", e, s);
+ return e;
+}
+
+r2s(r: Rect): string
+{
+ return sys->sprint("%d %d %d %d", r.min.x, r.min.y, r.max.x, r.max.y);
+}
+
+kill(pid: int)
+{
+ if ((fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
+ sys->write(fd, array of byte "kill", 4);
+}
+
+readfile(f: string): string
+{
+ if ((fd := sys->open(f, Sys->OREAD)) == nil)
+ return nil;
+ a := array[8192] of byte;
+ n := sys->read(fd, a, len a);
+ if (n <= 0)
+ return nil;
+ return string a[0:n];
+}
+