summaryrefslogtreecommitdiff
path: root/appl/wm/ftree
diff options
context:
space:
mode:
Diffstat (limited to 'appl/wm/ftree')
-rw-r--r--appl/wm/ftree/cptree.b136
-rw-r--r--appl/wm/ftree/cptree.m8
-rw-r--r--appl/wm/ftree/ftree.b873
-rw-r--r--appl/wm/ftree/items.b326
-rw-r--r--appl/wm/ftree/items.m30
-rw-r--r--appl/wm/ftree/mkfile36
-rw-r--r--appl/wm/ftree/wmsetup48
7 files changed, 1457 insertions, 0 deletions
diff --git a/appl/wm/ftree/cptree.b b/appl/wm/ftree/cptree.b
new file mode 100644
index 00000000..5af59fff
--- /dev/null
+++ b/appl/wm/ftree/cptree.b
@@ -0,0 +1,136 @@
+implement Cptree;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+include "readdir.m";
+ readdir: Readdir;
+include "cptree.m";
+
+init()
+{
+ sys = load Sys Sys->PATH;
+ readdir = load Readdir Readdir->PATH;
+}
+
+Context: adt {
+ progressch: chan of string;
+ warningch: chan of (string, chan of int);
+ finishedch: chan of string;
+};
+
+# recursively copy file/directory f into directory d;
+# the name remains the same.
+copyproc(f, d: string, progressch: chan of string,
+ warningch: chan of (string, chan of int),
+ finishedch: chan of string)
+{
+ ctxt := ref Context(progressch, warningch, finishedch);
+ (fok, fstat) := sys->stat(f);
+ if (fok == -1)
+ error(ctxt, sys->sprint("cannot stat '%s': %r", f));
+ (dok, dstat) := sys->stat(d);
+ if (dok == -1)
+ error(ctxt, sys->sprint("cannot stat '%s': %r", d));
+ if ((dstat.mode & Sys->DMDIR) == 0)
+ error(ctxt, sys->sprint("'%s' is not a directory", d));
+ if (fstat.qid.path == dstat.qid.path)
+ error(ctxt, sys->sprint("'%s' and '%s' are identical", f, d));
+
+ c := d + "/" + fname(f);
+ (cok, cstat) := sys->stat(c);
+ if (cok == 0)
+ error(ctxt, sys->sprint("'%s' already exists", c));
+ rcopy(ctxt, f, ref fstat, c);
+ finishedch <-= nil;
+}
+
+rcopy(ctxt: ref Context, src: string, srcstat: ref Sys->Dir, dst: string)
+{
+ omode := Sys->OWRITE;
+ perm := srcstat.mode;
+ if (perm & Sys->DMDIR) {
+ omode = Sys->OREAD;
+ perm |= 8r300;
+ }
+
+ dstfd := sys->create(dst, omode, perm);
+ if (dstfd == nil) {
+ warning(ctxt, sys->sprint("cannot create '%s': %r", dst));
+ return;
+ }
+ if (srcstat.mode & Sys->DMDIR) {
+ (entries, n) := readdir->init(src, Readdir->NAME | Readdir->COMPACT);
+ if (n == -1)
+ warning(ctxt, sys->sprint("cannot read dir '%s': %r", src));
+ for (i := 0; i < len entries; i++) {
+ e := entries[i];
+ rcopy(ctxt, src + "/" + e.name, e, dst + "/" + e.name);
+ }
+ if (perm != srcstat.mode) {
+ (ok, nil) := sys->fstat(dstfd);
+ if (ok != -1) {
+ dststat := sys->nulldir;
+ dststat.mode = srcstat.mode;
+ sys->fwstat(dstfd, dststat);
+ }
+ }
+ } else {
+ srcfd := sys->open(src, Sys->OREAD);
+ if (srcfd == nil) {
+ sys->remove(dst);
+ warning(ctxt, sys->sprint("cannot open '%s': %r", src));
+ return;
+ }
+ ctxt.progressch <-= "copying " + src;
+ buf := array[Sys->ATOMICIO] of byte;
+ while ((n := sys->read(srcfd, buf, len buf)) > 0) {
+ if (sys->write(dstfd, buf, n) != n) {
+ sys->remove(dst);
+ warning(ctxt, sys->sprint("error writing '%s': %r", dst));
+ return;
+ }
+ }
+ if (n == -1) {
+ sys->remove(dst);
+ warning(ctxt, sys->sprint("error reading '%s': %r", src));
+ return;
+ }
+ }
+}
+
+warning(ctxt: ref Context, msg: string)
+{
+ r := chan of int;
+ ctxt.warningch <-= (msg, r);
+ if (!<-r)
+ exit;
+}
+
+error(ctxt: ref Context, msg: string)
+{
+ ctxt.finishedch <-= msg;
+ exit;
+}
+
+fname(f: string): string
+{
+ f = cleanname(f);
+ for (i := len f - 1; i >= 0; i--)
+ if (f[i] == '/')
+ break;
+ return f[i+1:];
+}
+
+cleanname(s: string): string
+{
+ t := "";
+ i := 0;
+ while (i < len s)
+ if ((t[len t] = s[i++]) == '/')
+ while (i < len s && s[i] == '/')
+ i++;
+ if (len t > 1 && t[len t - 1] == '/')
+ t = t[0:len t - 1];
+ return t;
+}
diff --git a/appl/wm/ftree/cptree.m b/appl/wm/ftree/cptree.m
new file mode 100644
index 00000000..874a66a1
--- /dev/null
+++ b/appl/wm/ftree/cptree.m
@@ -0,0 +1,8 @@
+Cptree: module {
+ PATH: con "/dis/lib/ftree/cptree.dis";
+ init: fn();
+ copyproc: fn(f, d: string, progressch: chan of string,
+ warningch: chan of (string, chan of int),
+ finishedch: chan of string);
+};
+
diff --git a/appl/wm/ftree/ftree.b b/appl/wm/ftree/ftree.b
new file mode 100644
index 00000000..d70629d0
--- /dev/null
+++ b/appl/wm/ftree/ftree.b
@@ -0,0 +1,873 @@
+implement Ftree;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+ draw: Draw;
+ Point, Rect: import draw;
+include "tk.m";
+ tk: Tk;
+include "tkclient.m";
+ tkclient: Tkclient;
+include "readdir.m";
+ readdir: Readdir;
+include "items.m";
+ items: Items;
+ Item, Expander: import items;
+include "plumbmsg.m";
+ plumbmsg: Plumbmsg;
+ Msg: import plumbmsg;
+include "sh.m";
+ sh: Sh;
+include "popup.m";
+ popup: Popup;
+include "cptree.m";
+ cptree: Cptree;
+include "string.m";
+ str: String;
+include "arg.m";
+ arg: Arg;
+
+stderr: ref Sys->FD;
+
+Ftree: module {
+ init: fn(ctxt: ref Draw->Context, argv: list of string);
+};
+
+Tree: adt {
+ fname: string;
+ pick {
+ L =>
+ N =>
+ e: ref Expander;
+ sub: cyclic array of ref Tree;
+ }
+};
+
+tkcmds := array[] of {
+ "frame .top",
+ "label .top.l -text |",
+ "pack .top.l -side left -expand 1 -fill x",
+ "frame .f",
+ "canvas .c -yscrollcommand {.f.s set}",
+ "scrollbar .f.s -command {.c yview}",
+ "pack .f.s -side left -fill y",
+ "pack .c -side top -in .f -fill both -expand 1",
+ "pack .top -anchor w",
+ "pack .f -fill both -expand 1",
+ "pack propagate . 0",
+ ".top.l configure -text {}",
+};
+
+badmodule(p: string)
+{
+ sys->fprint(stderr, "ftree: cannot load %s: %r\n", p);
+ raise "fail:bad module";
+}
+
+tkwin: ref Tk->Toplevel;
+root := "/";
+
+cpfile := "";
+
+usage()
+{
+ sys->fprint(stderr, "usage: ftree [-e] [-E] [-p] [-d] [root]\n");
+ raise "fail:usage";
+}
+
+plumbinprogress := 0;
+disallow := 1;
+plumbed: chan of int;
+roottree: ref Tree.N;
+rootitem: Item;
+runplumb := 1;
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+ loadmods();
+ if (ctxt == nil) {
+ sys->fprint(sys->fildes(2), "ftree: no window context\n");
+ raise "fail:bad context";
+ }
+ draw = load Draw Draw->PATH;
+ noexit := 0;
+ winopts := Tkclient->Resize | Tkclient->Hide;
+ arg->init(argv);
+ while ((opt := arg->opt()) != 0) {
+ case opt {
+ 'e' =>
+ (noexit, winopts) = (1, Tkclient->Resize);
+ 'E' =>
+ (noexit, winopts) = (1, 0);
+ 'p' =>
+ (noexit, winopts) = (0, 0);
+ 'd' =>
+ disallow = 0;
+ 'P' =>
+ runplumb = 1;
+ * =>
+ usage();
+ }
+ }
+ argv = arg->argv();
+ if (argv != nil && tl argv != nil)
+ usage();
+ if (argv != nil) {
+ root = hd argv;
+ (ok, s) := sys->stat(root);
+ if (ok == -1) {
+ sys->fprint(stderr, "ftree: %s: %r\n", root);
+ raise "fail:bad root";
+ } else if ((s.mode & Sys->DMDIR) == 0) {
+ sys->fprint(stderr, "ftree: %s is not a directory\n", root);
+ raise "fail:bad root";
+ }
+ }
+
+ sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);
+
+ (win, wmctl) := tkclient->toplevel(ctxt, nil, "Ftree", winopts);
+ tkwin = win;
+ for (i := 0; i < len tkcmds; i++)
+ cmd(win, tkcmds[i]);
+ fittoscreen(win);
+ cmd(win, "update");
+
+ event := chan of string;
+ tk->namechan(win, event, "event");
+
+ clickfile := chan of string;
+ tk->namechan(win, clickfile, "clickfile");
+
+ sys->bind("#s", "/chan", Sys->MBEFORE);
+ fio := sys->file2chan("/chan", "plumbstart");
+ if (fio == nil) {
+ sys->fprint(stderr, "ftree: cannot make /chan/plumbstart: %r\n");
+ raise "fail:error";
+ }
+ nsfio := sys->file2chan("/chan", "nsupdate");
+ if (nsfio == nil) {
+ sys->fprint(stderr, "ftree: cannot make /chan/nsupdate: %r\n");
+ raise "fail:error";
+ }
+
+ if (runplumb){
+ if((err := sh->run(ctxt, "plumber" :: "-n" :: "-w" :: "-c/chan/plumbstart" :: nil)) != nil)
+ sys->fprint(stderr, "ftree: can't start plumber: %s\n", err);
+ }
+
+ plumbmsg = load Plumbmsg Plumbmsg->PATH;
+ if (plumbmsg != nil && plumbmsg->init(1, nil, 0) == -1) {
+ sys->fprint(stderr, "ftree: no plumber\n");
+ plumbmsg = nil;
+ }
+
+ nschanged := chan of string;
+ roottree = ref Tree.N("/", Expander.new(win, ".c"), nil);
+ rootitem = roottree.e.make(items->maketext(win, ".c", "/", "/"));
+ cmd(win, ".c configure -width " + string rootitem.r.dx() + " -height " + string rootitem.r.dy() +
+ " -scrollregion {" + r2s(rootitem.r) + "}");
+ sendevent("/", "expand");
+ tkclient->onscreen(win, nil);
+ tkclient->startinput(win, "ptr"::nil);
+ cmd(win, "update");
+
+ plumbed = chan of int;
+ for (;;) alt {
+ key := <-win.ctxt.kbd =>
+ tk->keyboard(win, key);
+ m := <-win.ctxt.ptr =>
+ tk->pointer(win, *m);
+ s := <-win.ctxt.ctl or
+ s = <-win.wreq or
+ s = <-wmctl =>
+ if (noexit && s == "exit")
+ s = "task";
+ tkclient->wmctl(win, s);
+ s := <-event =>
+ (target, ev) := eventtarget(s);
+ sendevent(target, ev);
+ m := <-clickfile =>
+ (n, toks) := sys->tokenize(m, " ");
+ (b, s) := (hd toks, hd tl toks);
+ if (b == "menu") {
+ c := chan of (ref Tree, Item, chan of Item);
+ nsu := chan of string;
+ spawn menuproc(c, nsu);
+ found := operate(s, c);
+ if (found) {
+ if ((upd := <-nsu) != nil)
+ updatens(upd);
+ }
+ } else if (b == "plumb")
+ plumbit(s);
+ ok := <-plumbed =>
+ colour := "#00ff00";
+ if (!ok)
+ colour = "red";
+ cmd(tkwin, ".c itemconfigure highlight -fill " + colour);
+ cmd(tkwin, "update");
+ plumbinprogress = 0;
+ s := <-nschanged =>
+ sys->print("got nschanged: %s\n", s);
+ updatens(s);
+ (nil, nil, nil, rc) := <-nsfio.read =>
+ if (rc != nil)
+ readreply(rc, nil, "permission denied");
+ (nil, data, nil, wc) := <-nsfio.write =>
+ if (wc == nil)
+ break;
+ s := cleanname(string data);
+ if (len s >= len root && s[0:len root] == root) {
+ s = s[len root:];
+ if (s == nil)
+ s = "/";
+ if (s[0] == '/')
+ updatens(s);
+ }
+ writereply(wc, len data, nil);
+ (nil, nil, nil, rc) := <-fio.read =>
+ if (rc != nil)
+ readreply(rc, nil, "permission denied");
+ (nil, data, nil, wc) := <-fio.write =>
+ if (wc == nil)
+ break;
+ s := string data;
+ if (len s == 0 || s[0] != 's')
+ writereply(wc, 0, "invalid write");
+ cmd := str->unquoted(s);
+ if (cmd == nil || tl cmd == nil || tl tl cmd == nil) {
+ writereply(wc, 0, "invalid write");
+ } else {
+ if (hd tl tl cmd == "+ftree")
+ runsubftree(ctxt, tl tl tl cmd);
+ else
+ sh->run(ctxt, "{$* &}" :: tl tl cmd);
+ writereply(wc, len data, nil);
+ }
+ }
+}
+
+runsubftree(ctxt: ref Draw->Context, c: list of string)
+{
+ if (len c < 2) {
+ return;
+ }
+ cmd(tkwin, ". unmap");
+ sh->run(ctxt, c);
+ cmd(tkwin, ". map");
+}
+
+sendevent(target, ev: string)
+{
+ c := chan of (ref Tree, Item, chan of Item);
+ spawn sendeventproc(ev, c);
+ operate(target, c);
+ cmd(tkwin, "update");
+}
+
+# non-blocking reply to read request, in case client has gone away.
+readreply(reply: Sys->Rread, data: array of byte, err: string)
+{
+ alt {
+ reply <-= (data, err) =>;
+ * =>;
+ }
+}
+
+# non-blocking reply to write request, in case client has gone away.
+writereply(reply: Sys->Rwrite, count: int, err: string)
+{
+ alt {
+ reply <-= (count, err) =>;
+ * =>;
+ }
+}
+
+plumbit(f: string)
+{
+ if (!plumbinprogress) {
+ highlight(f, "yellow", 2000);
+ spawn plumbproc(root + f, plumbed);
+ plumbinprogress = 1;
+ }
+}
+
+plumbproc(f: string, plumbed: chan of int)
+{
+ if (plumbmsg == nil || (ref Msg("browser", nil, nil, "text", nil, array of byte f)).send() == -1) {
+ sys->fprint(stderr, "ftree: cannot plumb %s\n", f);
+ plumbed <-= 0;
+ } else
+ plumbed <-= 1;
+}
+
+loadmods()
+{
+ 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)
+ badmodule(Tkclient->PATH);
+ tkclient->init();
+
+ readdir = load Readdir Readdir->PATH;
+ if (readdir == nil)
+ badmodule(Readdir->PATH);
+
+ str = load String String->PATH;
+ if (str == nil)
+ badmodule(String->PATH);
+
+ items = load Items Items->PATH;
+ if (items == nil)
+ badmodule(Items->PATH);
+ items->init();
+
+ sh = load Sh Sh->PATH;
+ if (sh == nil)
+ badmodule(Sh->PATH);
+
+ popup = load Popup Popup->PATH;
+ if (popup == nil)
+ badmodule(Popup->PATH);
+ popup->init();
+
+ cptree = load Cptree Cptree->PATH;
+ if (cptree == nil)
+ badmodule(Cptree->PATH);
+ cptree->init();
+
+ arg = load Arg Arg->PATH;
+ if (arg == nil)
+ badmodule(Arg->PATH);
+}
+
+updatens(s: string)
+{
+ sys->print("updatens(%s)\n", s);
+ (target, ev) := eventtarget(s);
+ spawn rereadproc(c := chan of (ref Tree, Item, chan of Item));
+ operate(target, c);
+ cmd(tkwin, "update");
+}
+
+nsupdatereaderproc(fd: ref Sys->FD, path: string, nschanged: chan of string)
+{
+ buf := array[Sys->ATOMICIO] of byte;
+ while ((n := sys->read(fd, buf, len buf)) > 0) {
+ s := string buf[0:n];
+ nschanged <-= path + string buf[0:n-1];
+ }
+ sys->print("nsupdate gave eof: (%r)\n");
+}
+
+sendeventproc(ev: string, c: chan of (ref Tree, Item, chan of Item))
+{
+ (tree, it, replyc) := <-c;
+ if (replyc == nil)
+ return;
+ pick t := tree {
+ N =>
+ if (ev == "expand")
+ expand(t, it);
+ else if (ev == "contract")
+ t.sub = nil;
+ it = t.e.event(it, ev);
+ }
+ replyc <-= it;
+}
+
+Open, Copy, Paste, Remove: con iota;
+
+menu := array[] of {
+Open => "Open",
+Copy => "Copy",
+Paste => "Paste into",
+Remove => "Remove",
+};
+
+screenx(cvs: string, x: int): int
+{
+ return x - int cmd(tkwin, cvs + " canvasx 0");
+}
+
+screeny(cvs: string, y: int): int
+{
+ return y - int cmd(tkwin, cvs + " canvasy 0");
+}
+
+menuproc(c: chan of (ref Tree, Item, chan of Item), nsu: chan of string)
+{
+ (tree, it, replyc) := <-c;
+ if (replyc == nil)
+ return;
+
+ p := Point(screenx(".c", it.r.min.x), screeny(".c", it.r.min.y));
+ m := array[len menu] of string;
+ for (i := 0; i < len m; i++)
+ m[i] = menu[i] + " " + tree.fname;
+ n := post(tkwin, p, m, 0);
+ upd: string;
+ if (n >= 0) {
+ case n {
+ Copy =>
+ cpfile = it.name;
+ Paste =>
+ if (cpfile == nil)
+ notice("no file in snarf buffer");
+ else {
+ cp(cpfile, it.name);
+ upd = it.name;
+ }
+ Remove =>
+ if ((e := rm(it.name)) != nil)
+ notice(e);
+ upd = parent(it.name);
+ Open =>
+ plumbit(it.name);
+ }
+ }
+
+# id := cmd(tkwin, ".c create rectangle " + r2s(it.r) + " -fill yellow");
+ replyc <-= it;
+ nsu <-= upd;
+}
+
+post(win: ref Tk->Toplevel, p: Point, a: array of string, n: int): int
+{
+ rc := popup->post(win, p, a, n);
+ for(;;)alt{
+ r := <-rc =>
+ return r;
+ key := <-win.ctxt.kbd =>
+ tk->keyboard(win, key);
+ m := <-win.ctxt.ptr =>
+ tk->pointer(win, *m);
+ s := <-win.ctxt.ctl or
+ s = <-win.wreq =>
+ tkclient->wmctl(win, s);
+ }
+}
+
+highlight(f: string, colour: string, time: int)
+{
+ spawn highlightproc(c := chan of (ref Tree, Item, chan of Item), colour, time);
+ operate(f, c);
+ tk->cmd(tkwin, "update");
+}
+
+unhighlight()
+{
+ cmd(tkwin, ".c delete highlight");
+ tk->cmd(tkwin, "update");
+}
+
+hpid := -1;
+highlightproc(c: chan of (ref Tree, Item, chan of Item), colour: string, time: int)
+{
+ (tree, it, replyc) := <-c;
+ if (replyc == nil)
+ return;
+ r: Rect;
+ pick t := tree {
+ N =>
+ r = t.e.titleitem.r.addpt(it.r.min);
+ L =>
+ r = it.r;
+ }
+ id := cmd(tkwin, ".c create rectangle " + r2s(r) + " -fill " + colour + " -tags highlight");
+ cmd(tkwin, ".c lower " + id);
+ kill(hpid);
+ sync := chan of int;
+ spawn highlightsleepproc(sync, time);
+ hpid = <-sync;
+ replyc <-= it;
+}
+
+highlightsleepproc(sync: chan of int, time: int)
+{
+ sync <-= sys->pctl(0, nil);
+ sys->sleep(time);
+ cmd(tkwin, ".c delete highlight");
+ cmd(tkwin, "update");
+}
+
+operate(towhom: string, c: chan of (ref Tree, Item, chan of Item)): int
+{
+ towhom = cleanname(towhom);
+ (ok, it) := operate1(roottree, rootitem, towhom, towhom, c);
+ if (!it.eq(rootitem)) {
+ cmd(tkwin, ".c configure -width " + string it.r.dx() + " -height " + string it.r.dy() +
+ " -scrollregion {" + r2s(it.r) + "}");
+ rootitem = it;
+ }
+ if (!ok)
+ c <-= (nil, it, nil);
+ return ok;
+}
+
+blankitem: Item;
+operate1(tree: ref Tree, it: Item, towhom, below: string,
+ c: chan of (ref Tree, Item, chan of Item)): (int, Item)
+{
+# sys->print("operate on %s, towhom: %s, below: %s\n", it.name, towhom, below);
+ n: ref Tree.N;
+ replyc := chan of Item;
+ if (it.name != towhom) {
+ pick t := tree {
+ L =>
+ return (0, it);
+ N =>
+ n = t;
+ }
+ below = dropelem(below);
+ if (below == nil)
+ return (0, it);
+ path := pathcat(it.name, below);
+ if (len n.e.children != len n.sub) {
+ sys->fprint(stderr, "inconsistent children in %s (%d vs sub %d)\n", it.name, len n.e.children, len n.sub);
+ return (0, it);
+ }
+ for (i := 0; i < len n.e.children; i++) {
+ f := n.e.children[i].name;
+# sys->print("checking %s against child %s\n", path, f);
+ if (len path >= len f && path[0:len f] == f &&
+ (len path == len f || path[len f] == '/')) {
+ break;
+ }
+ }
+ if (i == len n.e.children)
+ return (0, it);
+ oldit := n.e.children[i].addpt(it.r.min);
+ (ok, nit) := operate1(n.sub[i], oldit, towhom, below, c);
+ if (nit.eq(oldit))
+ return (ok, it);
+# sys->print("childchanged({%s, [%s]}, %d, {%s, [%s]})\n",
+# it.name, r2s(it.r), i, nit.name, r2s(nit.r));
+ n.e.children[i] = nit.subpt(it.r.min);
+ return (ok, n.e.childrenchanged(it));
+ }
+ c <-= (tree, it, replyc);
+ return (1, <-replyc);
+}
+
+
+dropelem(below: string): string
+{
+ if (below[0] == '/')
+ return below[1:];
+ for (i := 1; i < len below; i++)
+ if (below[i] == '/')
+ break;
+ if (i == len below)
+ return nil;
+ return below[i+1:];
+}
+
+cleanname(s: string): string
+{
+ t := "";
+ i := 0;
+ while (i < len s)
+ if ((t[len t] = s[i++]) == '/')
+ while (i < len s && s[i] == '/')
+ i++;
+ if (len t > 1 && t[len t - 1] == '/')
+ t = t[0:len t - 1];
+ return t;
+}
+
+pathcat(s1, s2: string): string
+{
+ if (s1 == nil || s2 == nil)
+ return s1 + s2;
+ if (s1[len s1 - 1] != '/' && s2[0] != '/')
+ return s1 + "/" + s2;
+ return s1 + s2;
+}
+
+# read the directory referred to by t.
+expand(t: ref Tree.N, it: Item)
+{
+ (d, n) := readdir->init(root + it.name, Readdir->NAME|Readdir->COMPACT);
+ if (d == nil) {
+ sys->print("readdir failed: %r\n");
+ d = array[0] of ref Sys->Dir;
+ }
+ sortit(d);
+ t.sub = array[len d] of ref Tree;
+ t.e.children = array[len d] of Item;
+ for (i := 0; i < len d; i++) {
+ tagname := pathcat(it.name, d[i].name);
+ (t.sub[i], t.e.children[i]) = makenode(d[i].mode & Sys->DMDIR, d[i].name, tagname);
+ # make coords relative to parent
+ t.e.children[i] = t.e.children[i].subpt(it.r.min);
+ }
+}
+
+makenode(isdir: int, title, tagname: string): (ref Tree, Item)
+{
+ tree: ref Tree;
+ it: Item;
+ if (isdir) {
+ e := Expander.new(tkwin, ".c");
+ tree = ref Tree.N(title, e, nil);
+ it = e.make(items->maketext(tkwin, ".c", tagname, title));
+ cmd(tkwin, ".c bind " + e.titleitem.name +
+ " <Button-1> {send clickfile menu " + tagname + "}");
+ } else {
+ tree = ref Tree.L(title);
+ it = items->maketext(tkwin, ".c", tagname, title);
+ cmd(tkwin, ".c bind " + tagname +
+ " <ButtonRelease-2> {send clickfile plumb " + tagname + "}");
+ cmd(tkwin, ".c bind " + tagname +
+ " <Button-1> {send clickfile menu " + tagname + "}");
+ }
+ return (tree, it);
+}
+
+rereadproc(c: chan of (ref Tree, Item, chan of Item))
+{
+ (tree, it, replyc) := <-c;
+ if (replyc == nil)
+ return;
+ pick t := tree {
+ L =>
+ replyc <-= it;
+ N =>
+ replyc <-= reread(t, it);
+ }
+}
+
+# re-read tree & update recursively as necessary.
+# _it_ is the tree's Item, in absolute coords.
+reread(tree: ref Tree.N, it: Item): Item
+{
+ (d, n) := readdir->init(root + it.name, Readdir->NAME|Readdir->COMPACT);
+ sortit(d);
+ sys->print("re-reading %s (was %d, now %d)\n", it.name, len tree.sub, len d);
+
+ sub := tree.sub;
+ newsub := array[len d] of ref Tree;
+ newchildren := array[len d] of Item;
+ i := j := 0;
+ while (i < len sub || j < len d) {
+ cmp: int;
+ if (i >= len sub)
+ cmp = 1;
+ else if (j >= len d)
+ cmp = -1;
+ else {
+ cmp = entrycmp(sub[i].fname, tagof(sub[i]) == tagof(Tree.N),
+ d[j].name, d[j].mode & Sys->DMDIR);
+ }
+ if (cmp == 0) {
+ # entry remains the same, but maybe it's changed type.
+ if ((tagof(sub[i]) == tagof(Tree.N)) != ((d[j].mode & Sys->DMDIR) != 0)) {
+ # delete old item and make new one...
+ tagname := tree.e.children[i].name;
+ cmd(tkwin, ".c delete " + tagname);
+ (newsub[j], newchildren[j]) =
+ makenode(d[j].mode & Sys->DMDIR, d[j].name, tagname);
+ newchildren[j] = newchildren[j].subpt(it.r.min);
+ } else {
+ nit := tree.e.children[i];
+ pick t := sub[i] {
+ N =>
+ if (t.e.expanded)
+ nit = reread(t, nit.addpt(it.r.min)).subpt(it.r.min);
+ }
+ (newsub[j], newchildren[j]) = (sub[i], nit);
+ }
+ i++;
+ j++;
+ } else if (cmp > 0) {
+ # new entry, d[j]
+ tagname := pathcat(it.name, d[j].name);
+ (newsub[j], newchildren[j]) =
+ makenode(d[j].mode & Sys->DMDIR, d[j].name, tagname);
+ newchildren[j] = newchildren[j].subpt(it.r.min);
+ j++;
+ } else {
+ # entry has been deleted, sub[i]
+ cmd(tkwin, ".c delete " + tree.e.children[i].name);
+ i++;
+ }
+ }
+ (tree.sub, tree.e.children) = (newsub, newchildren);
+ return tree.e.childrenchanged(it);
+}
+
+entrycmp(s1: string, isdir1: int, s2: string, isdir2: int): int
+{
+ if (!isdir1 == !isdir2) {
+ if (s1 > s2)
+ return 1;
+ else if (s1 < s2)
+ return -1;
+ else
+ return 0;
+ } else if (isdir1)
+ return -1;
+ else
+ return 1;
+}
+
+sortit(d: array of ref Sys->Dir)
+{
+ da := array[len d] of ref Sys->Dir;
+ fa := array[len d] of ref Sys->Dir;
+ nd := nf := 0;
+ for (i := 0; i < len d; i++) {
+ if (d[i].mode & Sys->DMDIR)
+ da[nd++] = d[i];
+ else
+ fa[nf++] = d[i];
+ }
+ d[0:] = da[0:nd];
+ d[nd:] = fa[0:nf];
+}
+
+eventtarget(s: string): (string, string)
+{
+ for (i := 0; i < len s; i++)
+ if (s[i] == ' ')
+ return (s[0:i], s[i+1:]);
+ return (s, nil);
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+ e := tk->cmd(top, s);
+ if (e != nil && e[0] == '!')
+ sys->fprint(sys->fildes(2), "ftree: tk error %s on '%s'\n", e, s);
+ return e;
+}
+
+r2s(r: Rect): string
+{
+ return string r.min.x + " " + string r.min.y + " " +
+ string r.max.x + " " + string r.max.y;
+}
+
+p2s(p: Point): string
+{
+ return string p.x + " " + string p.y;
+}
+
+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);
+}
+
+cp(src, dst: string)
+{
+ if(disallow){
+ notice("permission denied");
+ return;
+ }
+ progressch := chan of string;
+ warningch := chan of (string, chan of int);
+ finishedch := chan of string;
+ spawn cptree->copyproc(root + src, root + dst, progressch, warningch, finishedch);
+loop: for (;;) alt {
+ m := <-progressch =>
+ status(m);
+ (m, r) := <-warningch =>
+ notice("warning: " + m);
+ sys->sleep(1000);
+ r <-= 1;
+ m := <-finishedch =>
+ status(m);
+ break loop;
+ }
+}
+
+parent(f: string): string
+{
+ f = cleanname(f);
+ for (i := len f - 1; i >= 0; i--)
+ if (f[i] == '/')
+ break;
+ if (i > 0)
+ f = f[0:i];
+ return f;
+}
+
+notice(s: string)
+{
+ status(s);
+}
+
+status(s: string)
+{
+ cmd(tkwin, ".top.l configure -text '" + s);
+ cmd(tkwin, "update");
+}
+
+rm(name: string): string
+{
+ if(disallow)
+ return "permission denied";
+ name = root + name;
+ if(sys->remove(name) < 0) {
+ e := sys->sprint("%r");
+ (ok, d) := sys->stat(name);
+ if(ok >= 0 && (d.mode & Sys->DMDIR) != 0)
+ return rmdir(name);
+ return e;
+ }
+ return nil;
+}
+
+rmdir(name: string): string
+{
+ (d, n) := readdir->init(name, Readdir->NONE|Readdir->COMPACT);
+ for(i := 0; i < n; i++) {
+ path := name+"/"+d[i].name;
+ e: string;
+ if(d[i].mode & Sys->DMDIR)
+ e = rmdir(path);
+ else if (sys->remove(path) == -1)
+ e = sys->sprint("cannot remove %s: %r", path);
+ if (e != nil)
+ return e;
+ }
+ if (sys->remove(name) == -1)
+ return sys->sprint("cannot remove %s: %r", name);
+ return nil;
+}
+
+kill(pid: int)
+{
+ if ((fd := sys->open("/prog/"+string pid+"/ctl", Sys->OWRITE)) != nil)
+ sys->write(fd, array of byte "kill", 4);
+}
diff --git a/appl/wm/ftree/items.b b/appl/wm/ftree/items.b
new file mode 100644
index 00000000..023e3d33
--- /dev/null
+++ b/appl/wm/ftree/items.b
@@ -0,0 +1,326 @@
+implement Items;
+
+include "sys.m";
+ sys: Sys;
+include "draw.m";
+ draw: Draw;
+ Point, Rect: import draw;
+include "tk.m";
+ tk: Tk;
+include "items.m";
+
+Taglen: con 5;
+Titletaglen: con 10;
+Spotdiam: con 10;
+Lineopts: con " -width 1 -fill gray";
+Ovalopts: con " -outline gray";
+Crossopts: con " -fill red";
+
+init()
+{
+ sys = load Sys Sys->PATH;
+ draw = load Draw Draw->PATH;
+ tk = load Tk Tk->PATH;
+}
+
+blankexpander: Expander;
+Expander.new(win: ref Tk->Toplevel, cvs: string): ref Expander
+{
+ e := ref blankexpander;
+ e.win = win;
+ e.cvs = cvs;
+ return e;
+}
+
+moveto(win: ref Tk->Toplevel, cvs: string, tag: string, bbox: Rect, p: Point)
+{
+ if (!bbox.min.eq(p))
+ cmd(win, cvs + " move " + tag + " " + p2s(p.sub(bbox.min)));
+}
+
+bbox(win: ref Tk->Toplevel, cvs, w: string): Rect
+{
+ return s2r(cmd(win, cvs + " bbox " + w));
+}
+
+rename(win: ref Tk->Toplevel, it: Item, newname: string): Item
+{
+ (nil, itl) := sys->tokenize(cmd(win, ".c find withtag " + it.name), " ");
+ cmd(win, ".c dtag " + it.name + " " + it.name);
+ for (; itl != nil; itl = tl itl)
+ cmd(win, ".c addtag " + newname + " withtag " + hd itl);
+ it.name = newname;
+ return it;
+}
+
+Expander.make(e: self ref Expander, titleitem: Item): Item
+{
+ name := titleitem.name;
+ tag := " -tags " + name;
+
+ e.titleitem = rename(e.win, titleitem, "!!." + name);
+ cmd(e.win, e.cvs + " addtag " + name + " withtag !!." + name);
+ sc := spotcentre((0, 0), dxy(e.titleitem.r));
+ spotr := Rect(sc, sc).inset(-Spotdiam/2);
+
+ p := (spotr.max.x + Titletaglen, 0);
+ moveto(e.win, e.cvs, e.titleitem.name, e.titleitem.r, p);
+ e.titleitem.r = rmoveto(e.titleitem.r, p);
+ it := Item(name, ((0, 0), (spotr.max.x + Titletaglen + titleitem.r.dx(), titleitem.r.dy())), (0, 0));
+
+ # make line to the right of spot
+ cmd(e.win, e.cvs + " create line " +
+ p2s((spotr.max.x, sc.y)) + " " + p2s((spotr.max.x+Titletaglen, sc.y)) + tag + Lineopts);
+
+ # make spot
+ spotid := cmd(e.win, e.cvs + " create oval " +
+ r2s(spotr) + Ovalopts + tag);
+ if (e.expanded)
+ cmd(e.win, e.cvs + " bind " + spotid + " <ButtonRelease-1>"
+ + " {send event " + name + " contract}");
+ else
+ cmd(e.win, e.cvs + " bind " + spotid + " <ButtonRelease-1>"
+ + " {send event " + name + " expand}");
+
+ cmd(e.win, e.cvs + " raise " + spotid);
+ e.spotid = int spotid;
+
+ it.attach = (0, sc.y);
+ it.r.max = (e.titleitem.r.dx() + spotr.max.x + Titletaglen, e.titleitem.r.dy());
+
+ if (!e.expanded) {
+ addcross(e, it, name);
+ return it;
+ }
+
+ it.r = placechildren(e, it, name);
+ return it;
+}
+
+rmoveto(r: Rect, p: Point): Rect
+{
+ return r.addpt(p.sub(r.min));
+}
+
+# place all children of e appropriately.
+# assumes that the canvas items of all children are already made.
+# return bbox rectangle of whole thing.
+placechildren(e: ref Expander, it: Item, tags: string): Rect
+{
+ ltag := " -tags {"+ tags + " !." + it.name + "}";
+ titlesize := dxy(e.titleitem.r);
+ sc := spotcentre(it.r.min, titlesize);
+ maxwidth := 0;
+ y := it.r.min.y + titlesize.y;
+ lasty := 0;
+ for (i := 0; i < len e.children; i++) {
+ c := e.children[i];
+ if (c.r.dx() > maxwidth)
+ maxwidth = c.r.dx();
+ c.r = c.r.addpt(it.r.min);
+ r: Rect;
+ r.min = (sc.x + Taglen, y);
+ r.max = r.min.add(dxy(c.r));
+ moveto(e.win, e.cvs, c.name, c.r, r.min);
+
+ # make item coords relative to parent
+ e.children[i].r = r.subpt(it.r.min);
+ cmd(e.win, e.cvs + " addtag " + it.name + " withtag " + c.name);
+
+ # horizontal attachment
+ cmd(e.win, e.cvs + " create line " +
+ p2s((sc.x, y + c.attach.y)) + " " +
+ p2s((sc.x + Taglen + c.attach.x, y + c.attach.y)) +
+ ltag + Lineopts);
+ lasty = y + c.attach.y;
+ y += r.dy();
+ }
+
+ # vertical attachment (if there were any children)
+ if (i > 0) {
+ id := cmd(e.win, e.cvs + " create line " +
+ p2s((sc.x, sc.y + Spotdiam/2)) + " " + p2s((sc.x, lasty)) + ltag + Lineopts);
+ cmd(e.win, e.cvs + " bind " + id + " <Button-1>"+
+ " {send event " + it.name + " see}");
+ }
+ r := Rect(it.r.min,
+ (max(sc.x+Spotdiam/2+Titletaglen+titlesize.x, sc.x+Taglen+maxwidth),
+ y));
+ return r;
+}
+
+Expander.event(e: self ref Expander, it: Item, ev: string): Item
+{
+ case ev {
+ "expand" =>
+ if (e.expanded) {
+ sys->print("item %s is already expanded\n", it.name);
+ return it;
+ }
+ e.expanded = 1;
+ tags := gettags(e.win, e.cvs, string e.spotid);
+ cmd(e.win, e.cvs + " delete !." + it.name);
+ cmd(e.win, e.cvs + " bind " + string e.spotid + " <ButtonRelease-1>" +
+ + " {send event " + it.name + " contract}");
+ it.r = placechildren(e, it, tags);
+ "contract" =>
+ if (!e.expanded) {
+ sys->print("item %s is already contracted\n", it.name);
+ return it;
+ }
+ e.expanded = 0;
+ cmd(e.win, e.cvs + " delete !." + it.name);
+ for (i := 0; i < len e.children; i++)
+ cmd(e.win, e.cvs + " delete " + e.children[i].name);
+ cmd(e.win, e.cvs + " bind " + string e.spotid + " <ButtonRelease-1>" +
+ + " {send event " + it.name + " expand}");
+ tags := gettags(e.win, e.cvs, string e.spotid);
+ addcross(e, it, tags);
+ titlesize := dxy(e.titleitem.r);
+ it.r.max = it.r.min.add((Taglen * 2 + Spotdiam + titlesize.x, titlesize.y));
+ e.children = nil;
+ "see" =>
+ cmd(e.win, e.cvs + " see " + p2s(it.r.min));
+ * =>
+ sys->print("unknown event '%s' on item %s\n", ev, it.name);
+ }
+ return it;
+}
+
+Expander.childrenchanged(e: self ref Expander, it: Item): Item
+{
+ cmd(e.win, e.cvs + " delete !." + it.name);
+ tags := gettags(e.win, e.cvs, string e.spotid);
+ it.r = placechildren(e, it, tags);
+ return it;
+}
+
+gettags(win: ref Tk->Toplevel, cvs: string, name: string): string
+{
+ tags := cmd(win, cvs + " gettags " + name);
+ (n, tagl) := sys->tokenize(tags, " ");
+ ntags := "";
+ for (; tagl != nil; tagl = tl tagl) {
+ t := hd tagl;
+ if (t[0] != '!' && (t[0] < '0' || t[0] > '9'))
+ ntags += " " + t;
+ }
+ return ntags;
+}
+
+spotcentre(origin, titlesize: Point): Point
+{
+ return (origin.x + Spotdiam / 2, origin.y + titlesize.y / 2);
+}
+
+addcross(e: ref Expander, it: Item, tags: string)
+{
+ p := spotcentre(it.r.min, dxy(e.titleitem.r));
+ crosstags := " -tags {" + tags + " !." + it.name + "}";
+
+ id1 := cmd(e.win, e.cvs + " create line " +
+ p2s((p.x-Spotdiam/2, p.y)) + " " +
+ p2s((p.x+Spotdiam/2, p.y)) + crosstags + Crossopts);
+ id2 := cmd(e.win, e.cvs + " create line " +
+ p2s((p.x, p.y-Spotdiam/2)) + " " +
+ p2s((p.x, p.y+Spotdiam/2)) + crosstags + Crossopts);
+ cmd(e.win, e.cvs + " lower " + id1 + ";" + e.cvs + " lower " + id2);
+}
+
+knownfont: string;
+knownfontheight: int;
+fontheight(win: ref Tk->Toplevel, font: string): int
+{
+ Font: import draw;
+ if (font == knownfont)
+ return knownfontheight;
+ if (win.image == nil) # can happen if we run out of image memory
+ return -1;
+ f := Font.open(win.image.display, font);
+ if (f == nil)
+ return -1;
+ knownfont = font;
+ knownfontheight = f.height;
+ return f.height;
+}
+
+maketext(win: ref Tk->Toplevel, cvs: string, name: string, text: string): Item
+{
+ tag := " -tags " + name;
+ it := Item(name, ((0, 0), (0, 0)), (0, 0));
+ ttid := cmd(win, cvs + " create text 0 0 " +
+ " -anchor nw" + tag +
+ " -text '" + text);
+ it.r = bbox(win, cvs, ttid);
+ h := fontheight(win, cmd(win, cvs + " itemcget " + ttid + " -font"));
+ if (h != -1) {
+ dh := it.r.dy() - h;
+ it.r.min.y += dh / 2;
+ it.r.max.y -= dh / 2;
+ }
+ it.attach = (0, it.r.dy() / 2);
+ return it;
+}
+
+cmd(top: ref Tk->Toplevel, s: string): string
+{
+ e := tk->cmd(top, s);
+ if (e != nil && e[0] == '!')
+ sys->fprint(sys->fildes(2), "items: tk error %s on '%s'\n", e, s);
+ return e;
+}
+
+r2s(r: Rect): string
+{
+ return string r.min.x + " " + string r.min.y + " " +
+ string r.max.x + " " + string r.max.y;
+}
+
+s2r(s: string): Rect
+{
+ (n, toks) := sys->tokenize(s, " ");
+ if (n != 4) {
+ sys->print("'%s' is not a rectangle!\n", s);
+ raise "bad conversion";
+ }
+ r: Rect;
+ (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;
+}
+
+Item.eq(i: self Item, j: Item): int
+{
+ return i.r.eq(j.r) && i.attach.eq(j.attach) && i.name == j.name;
+}
+
+Item.addpt(i: self Item, p: Point): Item
+{
+ i.r = i.r.addpt(p);
+ return i;
+}
+
+Item.subpt(i: self Item, p: Point): Item
+{
+ i.r = i.r.subpt(p);
+ return i;
+}
+
+p2s(p: Point): string
+{
+ return string p.x + " " + string p.y;
+}
+
+dxy(r: Rect): Point
+{
+ return r.max.sub(r.min);
+}
+
+max(a, b: int): int
+{
+ if (a > b)
+ return a;
+ return b;
+}
diff --git a/appl/wm/ftree/items.m b/appl/wm/ftree/items.m
new file mode 100644
index 00000000..7af34d12
--- /dev/null
+++ b/appl/wm/ftree/items.m
@@ -0,0 +1,30 @@
+Items: module {
+ PATH: con "/dis/lib/ftree/items.dis";
+
+ Item: adt {
+ name: string; # tag held in common by all canvas items in this Item.
+ r: Rect; # relative to parent's Item when stored in children
+ attach: Point; # attachment point relative to r.min
+
+ eq: fn(i: self Item, j: Item): int;
+ addpt: fn(i: self Item, p: Point): Item;
+ subpt: fn(i: self Item, p: Point): Item;
+ };
+
+ Expander: adt {
+ titleitem: Item;
+ expanded: int;
+ children: array of Item;
+ win: ref Tk->Toplevel;
+ cvs: string;
+ spotid: int;
+
+ new: fn(win: ref Tk->Toplevel, cvs: string): ref Expander;
+ make: fn(e: self ref Expander, it: Item): Item;
+ event: fn(e: self ref Expander, it: Item, ev: string): Item;
+ childrenchanged: fn(e: self ref Expander, it: Item): Item;
+ };
+
+ init: fn();
+ maketext: fn(win: ref Tk->Toplevel, cvs: string, name: string, text: string): Item;
+};
diff --git a/appl/wm/ftree/mkfile b/appl/wm/ftree/mkfile
new file mode 100644
index 00000000..4f4c5f39
--- /dev/null
+++ b/appl/wm/ftree/mkfile
@@ -0,0 +1,36 @@
+<../../../mkconfig
+
+TARG=\
+ items.dis\
+ cptree.dis\
+ ftree.dis
+
+MODULES=\
+ items.m\
+ cptree.m\
+
+SYSMODULES=\
+ arg.m\
+ draw.m\
+ plumbmsg.m\
+ popup.m\
+ readdir.m\
+ sh.m\
+ string.m\
+ sys.m\
+ tk.m\
+ tkclient.m\
+
+DISBIN=$ROOT/dis/lib/ftree
+
+all:V: ftree.dis $TARG
+
+$ROOT/dis/wm/ftree.dis: ftree.dis
+ rm -f $ROOT/dis/wm/ftree.dis && cp ftree.dis $ROOT/dis/wm/ftree.dis
+
+<$ROOT/mkfiles/mkdis
+
+install:V: $ROOT/dis/wm/ftree.dis
+
+nuke:V: nuke-std
+ cd $ROOT/dis/wm; rm -f ftree.dis
diff --git a/appl/wm/ftree/wmsetup b/appl/wm/ftree/wmsetup
new file mode 100644
index 00000000..e229e63c
--- /dev/null
+++ b/appl/wm/ftree/wmsetup
@@ -0,0 +1,48 @@
+# /dis/sh script
+# wm defines "menu" and "delmenu" builtins
+load std
+prompt='% ' ''
+fn % {$*}
+autoload=std
+home=/usr/^"{cat /dev/user}
+
+if {! {~ wm ${loaded}}} {
+ echo wmsetup must run under wm >[1=2]
+ raise usage
+}
+
+fn wmrun {
+ args := $*
+ {
+ pctl newpgrp
+ fn wmrun
+ $args
+ } >[2] /chan/wmstderr &
+}
+
+fn cd {
+ builtin cd $*; echo cwd `{pwd} > /chan/shctl
+}
+
+menu Shell {wmrun wm/sh}
+menu Acme {wmrun acme}
+menu Edit {wmrun wm/edit}
+menu Charon {wmrun charon}
+menu Manual {wmrun wm/man}
+menu Files {if {ftest -d $home} {wmrun wm/dir $home} {wmrun wm/dir /}}
+menu '' ''
+menu System 'Debugger' {wmrun wm/deb}
+menu System 'Module manager' {wmrun wm/rt}
+menu System 'Task manager' {wmrun wm/task}
+menu System 'Memory monitor' {wmrun wm/memory}
+menu System 'About' {wmrun wm/about}
+menu Misc 'Tetris' {wmrun wm/tetris}
+menu Misc 'Coffee' {wmrun wm/coffee}
+menu Misc 'Colours' {wmrun wm/colors}
+menu Misc 'Winctl' {wmrun wm/winctl}
+menu Misc 'Clock' {wmrun wm/date}
+
+if {ftest -f $home/lib/wmsetup} {run $home/lib/wmsetup} {}
+
+builtin cd /usr/rog/limbo/browser
+wmrun ftree