ref: babf901b4a508c3ec5d1f89655f10377bbdf9637
dir: /appl/wm/man.b/
implement WmMan;
include "sys.m";
sys: Sys;
include "draw.m";
draw: Draw;
Font: import draw;
include "tk.m";
tk: Tk;
include "tkclient.m";
tkclient: Tkclient;
include "plumbmsg.m";
include "man.m";
man: Man;
WmMan: module {
init: fn (ctxt: ref Draw->Context, argv: list of string);
};
window: ref Tk->Toplevel;
W: adt {
textwidth: fn(nil: self ref W, text: Text): int;
};
ROMAN: con "/fonts/lucidasans/unicode.7.font";
BOLD: con "/fonts/lucidasans/typelatin1.7.font";
ITALIC: con "/fonts/lucidasans/italiclatin1.7.font";
HEADING1: con "/fonts/lucidasans/boldlatin1.7.font";
HEADING2: con "/fonts/lucidasans/italiclatin1.7.font";
rfont, bfont, ifont, h1font, h2font: ref Font;
GOATTR: con Parseman->ATTR_LAST << iota;
MANPATH: con "/man/1/man";
INDENT: con 40;
metrics: Parseman->Metrics;
parser: Parseman;
Text: import parser;
tkconfig := array [] of {
"frame .input",
"frame .view",
"text .view.t -state disabled -width 0 -height 0 -bg white -yscrollcommand {.view.yscroll set} -xscrollcommand {.view.xscroll set}",
"scrollbar .view.yscroll -orient vertical -command {.view.t yview}",
"scrollbar .view.xscroll -orient horizontal -command {.view.t xview}",
"entry .input.e -bg white",
"button .input.back -state disabled -bitmap small_color_left.bit -command {send nav b}",
"button .input.forward -state disabled -bitmap small_color_right.bit -command {send nav f}",
"pack .input.back .input.forward -side left -anchor w",
"pack .input.e -expand 1 -fill x",
"pack .view.yscroll -fill y -side left",
"pack .view.t -expand 1 -fill both",
"bind .input.e <Key-\n> {send nav e}",
"bind .input.e <Button-1> +{grab set .input.e}",
"bind .input.e <ButtonRelease-1> +{grab release .input.e}",
"bind .view.t <Button-1> +{grab set .view.t}",
"bind .view.t <ButtonRelease-1> +{grab release .view.t}",
"bind .view.t <ButtonRelease-3> {send plumb %x %y}",
"pack .input -fill x",
"pack .view -expand 1 -fill both",
"pack propagate . 0",
". configure -width 500 -height 500",
"focus .input.e",
};
History: adt {
prev: cyclic ref History;
next: cyclic ref History;
topline: string;
searchstart: string;
searchend: string;
pick {
Search =>
search: list of string;
Go =>
path: string;
}
};
history: ref History;
init(ctxt: ref Draw->Context, argv: list of string)
{
doplumb := 0;
sys = load Sys Sys->PATH;
if (ctxt == nil) {
sys->fprint(sys->fildes(2), "man: no window context\n");
raise "fail:bad context";
}
sys->pctl(Sys->NEWPGRP, nil);
draw = load Draw Draw->PATH;
if (draw == nil)
loaderr("Draw");
tk = load Tk Tk->PATH;
if (tk == nil)
loaderr(Tk->PATH);
man = load Man Man->PATH;
if (man == nil)
loaderr(Man->PATH);
tkclient = load Tkclient Tkclient->PATH;
if (tkclient == nil)
loaderr(Tkclient->PATH);
parser = load Parseman Parseman->PATH;
if (parser == nil)
loaderr(Parseman->PATH);
parser->init();
plumber := load Plumbmsg Plumbmsg->PATH;
if (plumber != nil) {
if (plumber->init(1, nil, 0) >= 0)
doplumb = 1;
}
argv = tl argv;
rfont = Font.open(ctxt.display, ROMAN);
bfont = Font.open(ctxt.display, BOLD);
ifont = Font.open(ctxt.display, ITALIC);
h1font = Font.open(ctxt.display, HEADING1);
h2font = Font.open(ctxt.display, HEADING2);
em := rfont.width("m");
en := rfont.width("n");
metrics = Parseman->Metrics(490, 80, em, en, 14, 40, 20);
tkclient->init();
buts := Tkclient->Resize | Tkclient->Hide;
winctl: chan of string;
(window, winctl) = tkclient->toplevel(ctxt, nil, "Man", buts);
nav := chan of string;
plumb := chan of string;
tk->namechan(window, nav, "nav");
tk->namechan(window, plumb, "plumb");
for(tc:=0; tc<len tkconfig; tc++)
tkcmd(window, tkconfig[tc]);
if ((err := tkcmd(window, "variable lasterror")) != nil) {
sys->fprint(sys->fildes(2), "man: tk initialization failed: %s\n", err);
raise "fail:tk";
}
fittoscreen(window);
tkcmd(window, "update");
mktags();
vw := int tkcmd(window, ".view.t cget -actwidth") - 10;
if (vw <= 0)
vw = 1;
metrics.pagew = vw;
linechan := chan of list of (int, Text);
man->loadsections(nil);
pidc := chan of int;
if (argv != nil) {
if (hd argv == "-f") {
first: ref History;
for (argv = tl argv; argv != nil; argv = tl argv) {
hnode := ref History.Go(history, nil, "", "", "", hd argv);
if (history != nil)
history.next = hnode;
history = hnode;
if (first == nil)
first = history;
}
history = first;
} else
history = ref History.Search(nil, nil, "", "", "", argv);
}
if (history == nil)
history = ref History.Go(nil, nil, "", "", "", MANPATH);
setbuttons();
spawn printman(pidc, linechan, history);
layoutpid := <- pidc;
tkclient->onscreen(window, nil);
tkclient->startinput(window, "kbd"::"ptr"::nil);
for (;;) alt {
s := <-window.ctxt.kbd =>
tk->keyboard(window, s);
s := <-window.ctxt.ptr =>
tk->pointer(window, *s);
s := <-window.ctxt.ctl or
s = <-window.wreq or
s = <-winctl =>
e := tkclient->wmctl(window, s);
if (e == nil && s[0] == '!') {
topline := tkcmd(window, ".view.t yview");
(nil, toptoks) := sys->tokenize(topline, " ");
if (toptoks != nil)
history.topline = hd toptoks;
vw = int tkcmd(window, ".view.t cget -actwidth") - 10;
if (vw <= 0)
vw = 1;
if (vw != metrics.pagew) {
if (layoutpid != -1)
kill(layoutpid);
metrics.pagew = vw;
tkcmd(window, ".view.t delete 1.0 end");
tkcmd(window, "update");
spawn printman(pidc, linechan, history);
layoutpid = <- pidc;
}
}
line := <- linechan =>
if (line == nil) {
# layout done
if (history.topline != "") {
topline := tkcmd(window, ".view.t yview");
(nil, toptoks) := sys->tokenize(topline, " ");
if (toptoks != nil)
if (hd toptoks == "0")
tkcmd(window, ".view.t yview moveto " + history.topline);
}
tkcmd(window, "update");
} else
setline(line);
go := <- nav =>
topline := tkcmd(window, ".view.t yview");
(nil, toptoks) := sys->tokenize(topline, " ");
if (toptoks != nil)
history.topline = hd toptoks;
case go[0] {
'f' =>
# forward
history = history.next;
setbuttons();
if (layoutpid != -1)
kill(layoutpid);
tkcmd(window, ".view.t delete 1.0 end");
tkcmd(window, "update");
spawn printman(pidc, linechan, history);
layoutpid = <- pidc;
'b' =>
# back
history = history.prev;
setbuttons();
if (layoutpid != -1)
kill(layoutpid);
tkcmd(window, ".view.t delete 1.0 end");
tkcmd(window, "update");
spawn printman(pidc, linechan, history);
layoutpid = <- pidc;
'e' or 'l' =>
t := "";
if (go[0] == 'l') {
# link
t = go[1:];
} else {
# entry
t = tkcmd(window, ".input.e get");
for (i := 0; i < len t; i++)
if (!(t[i] == ' ' || t[i] == '\t'))
break;
if (i == len t)
break;
t = t[i:];
if (t[0] == '/' || t[0] == '?') {
search(t);
break;
}
}
(n, toks) := sys->tokenize(t, " \t");
if (n == 0)
continue;
h := ref History.Search(history, nil, "", "", "", toks);
history.next = h;
history = h;
setbuttons();
if (layoutpid != -1)
kill(layoutpid);
tkcmd(window, ".view.t delete 1.0 end");
tkcmd(window, "update");
spawn printman(pidc, linechan, history);
layoutpid = <- pidc;
'g' =>
# goto file
h := ref History.Go(history, nil, "", "", "", go[1:]);
history.next = h;
history = h;
setbuttons();
if (layoutpid != 0)
kill(layoutpid);
tkcmd(window, ".view.t delete 1.0 end");
tkcmd(window, "update");
spawn printman(pidc, linechan, history);
layoutpid = <- pidc;
}
p := <- plumb =>
if (!doplumb)
break;
(nil, l) := sys->tokenize(p, " ");
x := int hd l;
y := int hd tl l;
index := tkcmd(window, ".view.t index @"+string x+","+string y);
selindex := tkcmd(window, ".view.t tag ranges sel");
insel := 0;
if(selindex != "")
insel = tkcmd(window, ".view.t compare sel.first <= "+index)=="1" &&
tkcmd(window, ".view.t compare sel.last >= "+index)=="1";
text := "";
attr := "";
if (insel)
text = tkcmd(window, ".view.t get sel.first sel.last");
else{
# have line with text in it
# now extract whitespace-bounded string around click
(nil, w) := sys->tokenize(index, ".");
charno := int hd tl w;
left := tkcmd(window, ".view.t index {"+index+" linestart}");
right := tkcmd(window, ".view.t index {"+index+" lineend}");
line := tkcmd(window, ".view.t get "+left+" "+right);
for(i:=charno; i>0; --i)
if(line[i-1]==' ' || line[i-1]=='\t')
break;
for(j:=charno; j<len line; j++)
if(line[j]==' ' || line[j]=='\t')
break;
text = line[i:j];
attr = "click="+string (charno-i);
}
msg := ref Plumbmsg->Msg(
"WmMan",
"",
"",
"text",
attr,
array of byte text);
plumber->msg.send();
layoutpid = <- pidc =>
;
}
}
search(pat: string)
{
dir: string;
start: string;
if (pat[0] == '/') {
dir = "-forwards";
start = history.searchend;
} else {
dir = "-backwards";
start = history.searchstart;
}
pat = pat[1:];
if (start == "")
start = "1.0";
r := tkcmd(window, ".view.t search " + dir + " -- " + tk->quote(pat) + " " + start);
if (r != nil) {
history.searchstart = r;
history.searchend = r + "+" + string len pat + "c";
tkcmd(window, ".view.t tag remove sel 1.0 end");
tkcmd(window, ".view.t tag add sel " + history.searchstart + " " + history.searchend);
tkcmd(window, ".view.t see " + r);
tkcmd(window, "update");
}
}
setbuttons()
{
if (history.prev == nil)
tkcmd(window, ".input.back configure -state disabled");
else
tkcmd(window, ".input.back configure -state normal");
if (history.next == nil)
tkcmd(window, ".input.forward configure -state disabled");
else
tkcmd(window, ".input.forward configure -state normal");
}
dolayout(linechan: chan of list of (int, Text), path: string)
{
fd := sys->open(path, Sys->OREAD);
if (fd == nil) {
layouterror(linechan, sys->sprint("cannot open file %s: %r", path));
return;
}
w: ref W;
parser->parseman(fd, metrics, 0, w, linechan);
}
printman(pidc: chan of int, linechan: chan of list of (int, Text), h: ref History)
{
pidc <-= sys->pctl(0, nil);
args: list of string;
pick hp := h {
Search =>
args = hp.search;
Go =>
dolayout(linechan, hp.path);
pidc <-= -1;
return;
}
sections: list of string;
argstext := "";
addsections := 1;
keywords: list of string;
for (; args != nil; args = tl args) {
arg := hd args;
if (arg == nil)
continue;
if (addsections && !isint(trimdot(arg))) {
addsections = 0;
keywords = args;
}
if (addsections)
sections = arg :: sections;
argstext = argstext + " " + arg;
}
manpages := man->getfiles(sections, keywords);
pagelist := sortpages(manpages);
if (len pagelist == 1) {
(nil, path, nil) := hd pagelist;
dolayout(linechan, path);
pidc <-= -1;
return;
}
tt := Text(Parseman->FONT_ROMAN, 0, "Search:", 1, nil);
at := Text(Parseman->FONT_BOLD, 0, argstext, 0, nil);
linechan <-= (0, tt)::(0, at)::nil;
tt.text = "";
linechan <-= (0, tt)::nil;
if (pagelist == nil) {
donet := Text(Parseman->FONT_ROMAN, 0, "No matches", 0, nil);
linechan <-= (INDENT, donet) :: nil;
linechan <-= nil;
pidc <-= -1;
return;
}
linelist: list of list of Text;
pathlist: list of Text;
maxkwlen := 0;
comma := Text(Parseman->FONT_ROMAN, 0, ", ", 0, "");
for (; pagelist != nil; pagelist = tl pagelist) {
(n, p, kwl) := hd pagelist;
l := 0;
keywords: list of Text = nil;
for (; kwl != nil; kwl = tl kwl) {
kw := hd kwl;
kwt := Text(Parseman->FONT_ITALIC, GOATTR, kw, 0, p);
nt := Text(Parseman->FONT_ROMAN, GOATTR, "(" + string n + ")", 0, p);
l += textwidth(kwt) + textwidth(nt);
if (keywords != nil) {
l += textwidth(comma);
keywords = nt :: kwt :: comma :: keywords;
} else
keywords = nt :: kwt :: nil;
}
if (l > maxkwlen)
maxkwlen = l;
linelist = keywords :: linelist;
ptext := Text(Parseman->FONT_ROMAN, GOATTR, p, 0, "");
pathlist = ptext :: pathlist;
}
for (; pathlist != nil; (pathlist, linelist) = (tl pathlist, tl linelist)) {
line := (10 + INDENT + maxkwlen, hd pathlist) :: nil;
for (ll := hd linelist; ll != nil; ll = tl ll) {
litem := hd ll;
if (tl ll == nil)
line = (INDENT, litem) :: line;
else
line = (0, litem) :: line;
}
linechan <-= line;
}
linechan <-= nil;
pidc <-= -1;
}
layouterror(linechan: chan of list of (int, Text), msg: string)
{
text := "ERROR: " + msg;
t := Text(Parseman->FONT_ROMAN, 0, text, 0, nil);
linechan <-= (0, t)::nil;
linechan <-= nil;
}
loaderr(modname: string)
{
sys->print("cannot load %s module: %r\n", modname);
raise "fail:init";
}
W.textwidth(nil: self ref W, text: Text): int
{
return textwidth(text);
}
textwidth(text: Text): int
{
f: ref Font;
if (text.heading == 1)
f = h1font;
else if (text.heading == 2)
f = h2font;
else {
case text.font {
Parseman->FONT_ROMAN =>
f = rfont;
Parseman->FONT_BOLD =>
f = bfont;
Parseman->FONT_ITALIC =>
f = ifont;
* =>
return 8 * len text.text;
}
}
return draw->f.width(text.text);
}
lnum := 0;
setline(line: list of (int, Text))
{
tabstr := "";
linestr := "";
lastoff := 0;
curfont := Parseman->FONT_ROMAN;
curlink := "";
curgtag := "";
curheading := 0;
fonttext := "";
for (l := line; l != nil; l = tl l) {
(offset, nil) := hd l;
if (offset != 0) {
lastoff = offset;
if (tabstr != "")
tabstr[len tabstr] = ' ';
tabstr = tabstr + string offset;
}
}
# fudge up tabs for rest of line
if (lastoff != 0)
tabstr = tabstr + " " + string lastoff + " " + string (lastoff + INDENT);
ttag := "";
gtag := "";
if (tabstr != nil)
ttag = tabtag(tabstr) + " ";
for (l = line; l != nil; l = tl l) {
(offset, text) := hd l;
gtag = "";
if (text.link != nil) {
if (text.attr & GOATTR)
gtag = gotag(text.link) + " ";
else {
gtag = linktag(text.link) + " ";
}
}
if (offset != 0)
fonttext[len fonttext] = '\t';
if (text.font != curfont || text.link != curlink || text.heading != curheading || gtag != curgtag) {
# need to change tags
linestr = linestr + " " + tk->quote(fonttext) + " {" + ttag + curgtag + fonttag(curfont, curheading) + "}";
ttag = "";
curgtag = gtag;
fonttext = "";
curfont = text.font;
curlink = text.link;
curheading = text.heading;
}
fonttext = fonttext + text.text;
}
if (fonttext != nil)
linestr = linestr + " " + tk->quote(fonttext) + " {" + ttag + curgtag + fonttag(curfont, curheading) + "}";
tkcmd(window, ".view.t insert end " + linestr);
tkcmd(window, ".view.t insert end {\n}");
# only update on every other line
if (lnum++ & 1)
tkcmd(window, "update");
}
mktags()
{
tkcmd(window, ".view.t tag configure ROMAN -font " + ROMAN);
tkcmd(window, ".view.t tag configure BOLD -font " + BOLD);
tkcmd(window, ".view.t tag configure ITALIC -font " + ITALIC);
tkcmd(window, ".view.t tag configure H1 -font " + HEADING1);
tkcmd(window, ".view.t tag configure H2 -font " + HEADING2);
}
fonttag(font, heading: int): string
{
if (heading == 1)
return "H1";
if (heading == 2)
return "H2";
case font {
Parseman->FONT_ROMAN =>
return "ROMAN";
Parseman->FONT_BOLD =>
return "BOLD";
Parseman->FONT_ITALIC =>
return "ITALIC";
}
return nil;
}
nexttag := 0;
lasttabstr := "";
lasttagname := "";
tabtag(tabstr: string): string
{
if (tabstr == lasttabstr)
return lasttagname;
lasttagname = "TAB" + string nexttag++;
lasttabstr = tabstr;
tkcmd(window, ".view.t tag configure " + lasttagname + " -tabs " + tk->quote(tabstr));
return lasttagname;
}
# optimise this!
gotag(path: string): string
{
cmd := "{send nav g" + path + "}";
name := "GO" + string nexttag++;
tkcmd(window, ".view.t tag bind " + name + " <ButtonRelease-1> +" + cmd);
tkcmd(window, ".view.t tag configure " + name + " -fg green");
return name;
}
# and this!
linktag(search: string): string
{
cmd := tk->quote("send nav l" + search);
name := "LN" + string nexttag++;
tkcmd(window, ".view.t tag bind " + name + " <ButtonRelease-1> +" + cmd);
tkcmd(window, ".view.t tag configure " + name + " -fg green");
return name;
}
isint(s: string): int
{
for (i := 0; i < len s; i++)
if (s[i] < '0' || s[i] > '9')
return 0;
return 1;
}
kill(pid: int)
{
fd := sys->open("/prog/" + string pid + "/ctl", Sys->OWRITE);
if (fd != nil)
sys->fprint(fd, "kill");
}
revsortuniq(strlist: list of string): list of string
{
strs := array [len strlist] of string;
for (i := 0; strlist != nil; (i, strlist) = (i+1, tl strlist))
strs[i] = hd strlist;
# simple sort (ascending)
for (i = 0; i < len strs - 1; i++) {
for (j := i+1; j < len strs; j++)
if (strs[i] < strs[j])
(strs[i], strs[j]) = (strs[j], strs[i]);
}
# construct list (result is descending)
r: list of string;
prev := "";
for (i = 0; i < len strs; i++) {
if (strs[i] != prev) {
r = strs[i] :: r;
prev = strs[i];
}
}
return r;
}
sortpages(pagelist: list of (int, string, string)): list of (int, string, list of string)
{
pages := array [len pagelist] of (int, string, string);
for (i := 0; pagelist != nil; (i, pagelist) = (i+1, tl pagelist))
pages[i] = hd pagelist;
for (i = 0; i < len pages - 1; i++) {
for (j := i+1; j < len pages; j++) {
(nil, nil, ipath) := pages[i];
(nil, nil, jpath) := pages[j];
if (ipath > jpath)
(pages[i], pages[j]) = (pages[j], pages[i]);
}
}
r: list of (int, string, list of string);
filecmds: list of string;
lastfile := "";
lastsect := 0;
for (i = 0; i < len pages; i++) {
(section, cmd, file) := pages[i];
if (lastfile == "") {
lastfile = file;
lastsect = section;
}
if (file != lastfile) {
r = (lastsect, lastfile, filecmds) :: r;
lastfile = file;
lastsect = section;
filecmds = nil;
}
filecmds = cmd :: filecmds;
}
if (filecmds != nil)
r = (lastsect, lastfile, revsortuniq(filecmds)) :: r;
return r;
}
fittoscreen(win: ref Tk->Toplevel)
{
Point, Rect: 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 tkcmd(win, ". cget -bd");
winsize := Point(int tkcmd(win, ". cget -actwidth") + bd * 2, int tkcmd(win, ". cget -actheight") + bd * 2);
if (winsize.x > scrsize.x)
tkcmd(win, ". configure -width " + string (scrsize.x - bd * 2));
if (winsize.y > scrsize.y)
tkcmd(win, ". configure -height " + string (scrsize.y - bd * 2));
actr: Rect;
actr.min = Point(int tkcmd(win, ". cget -actx"), int tkcmd(win, ". cget -acty"));
actr.max = actr.min.add((int tkcmd(win, ". cget -actwidth") + bd*2,
int tkcmd(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.max.x - dx, r.max.x);
if (actr.max.y > r.max.y)
(actr.min.y, actr.max.y) = (r.max.y - dy, r.max.y);
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);
tkcmd(win, ". configure -x " + string actr.min.x + " -y " + string actr.min.y);
}
tkcmd(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;
}
trimdot(s: string): string
{
for(i := 0; i < len s; i++)
if(s[i] == '.')
return s[0: i];
return s;
}