(* Last modified on Fri Apr 12 09:36:41 PDT 1996 by heydon *) <* PRAGMA LL *> MODULE JunoConfig; IMPORT JunoRsrc; IMPORT MultiSplit, MultiClass, TextEditVBT, FileBrowserVBT; IMPORT VBT, Split, Font, TextVBT, TextPort, ListVBT, Region, Rect, Point; IMPORT OSError, Pathname, Process, Fmt, Rd, FileRd, Env, Thread, Lex; IMPORT Sx, RefSeq, RefList, Atom, TextRd, TextSeq, Text, Wr, Rsrc; FROM Stdio IMPORT stderr; TYPE FontWeight = { Any, Medium, Bold }; CONST DefaultConfigName = "DefaultConfig.sx"; (* Name of default configuration file looked up on "JunoRsrc.Path" *) PROCEDURE SetDefaults() = <* FATAL Wr.Failure, Thread.Alerted *> VAR rd: Rd.T; BEGIN TRY rd := Rsrc.Open(DefaultConfigName, JunoRsrc.Path); ParseConfigFile(rd) EXCEPT Rsrc.NotFound => Wr.PutText(stderr, "Error: unable to open default " & "configuration file \"" & DefaultConfigName & "\"\n"); Process.Exit(1); | Error (msg) => Wr.PutText(stderr, "Error reading default configuration file:\n"); Wr.PutText(stderr, " " & msg & "\n"); Process.Exit(1); END END SetDefaults; PROCEDURE TryOpening(nm: Pathname.T; VAR (*INOUT*) rd: Rd.T): BOOLEAN = (* Try opening the file named "nm" as a regular file. If it exists and is a regular file, then set "rd" to be a reader on the file and return TRUE. Otherwise, leave "rd" unchanged and return FALSE. *) BEGIN TRY rd := FileRd.Open(nm) EXCEPT OSError.E => RETURN FALSE END; RETURN TRUE END TryOpening; PROCEDURE Init(filename: Pathname.T := NIL): Pathname.T RAISES {OSError.E, Error} = CONST DefaultName = ".juno-config.sx"; VAR rd: Rd.T := NIL; res: Pathname.T := NIL; BEGIN SetDefaults(); IF filename = NIL THEN IF TryOpening(DefaultName, (*OUT*) rd) THEN res := DefaultName ELSE (* try looking in the home directory *) VAR homeDir := Env.Get("HOME"); BEGIN IF homeDir # NIL THEN VAR fname := homeDir & "/" & DefaultName; BEGIN IF TryOpening(fname, (*OUT*) rd) THEN res := fname END END END END END ELSE rd := FileRd.Open(filename); res := filename; END; IF rd # NIL THEN ParseConfigFile(rd) END; RETURN res END Init; PROCEDURE ParseConfigFile(rd: Rd.T) RAISES {Error} = BEGIN TRY ParseSx(ReadLists(rd)) EXCEPT Sx.ReadError (msg) => RAISE Error(msg) | Rd.EndOfFile => RAISE Error("encountered premature end-of-file") | Rd.Failure => RAISE Error("failure reading file") END END ParseConfigFile; PROCEDURE ReadLists(rd: Rd.T): RefSeq.T RAISES {Sx.ReadError, Rd.EndOfFile, Rd.Failure} = (* The file "rd" should contain a sequence of symbolic expressions. This returns a sequence of "Sx.T" values in read from "rd". *) <* FATAL Thread.Alerted *> VAR res := NEW(RefSeq.T).init(); BEGIN LOOP Lex.Skip(rd); IF Rd.EOF(rd) THEN EXIT END; res.addhi(Sx.Read(rd)) END; RETURN res END ReadLists; PROCEDURE ParseSx(refs: RefSeq.T) RAISES {Error} = BEGIN WHILE refs.size() > 0 DO TYPECASE refs.remlo() OF RefList.T (l) => ParseList(l) ELSE RAISE Error("top-level element is not a list") END END END ParseSx; VAR (*CONST*) CodeFontSym := Atom.FromText("CodeFont"); TextFontSym := Atom.FromText("TextFont"); LabelFontSym := Atom.FromText("LabelFont"); DotSizeSym := Atom.FromText("DotSize"); CrossSizeSym := Atom.FromText("CrossSize"); ChkptIntvSym := Atom.FromText("ChkptIntv"); RealPrecSym := Atom.FromText("RealPrec"); PreviewCmdSym := Atom.FromText("PreviewCmd"); PrintCmdSym := Atom.FromText("PrintCmd"); OriginSym := Atom.FromText("Origin"); OrientSym := Atom.FromText("Orientation"); PROCEDURE ParseList(l: RefList.T) RAISES {Error} = BEGIN IF l = NIL THEN RAISE Error("top-level list is empty") END; TYPECASE l.head OF Atom.T (cmd) => IF cmd = CodeFontSym THEN codeFont := ParseFont(l.tail, cmd) ELSIF cmd = TextFontSym THEN textFont := ParseFont(l.tail, cmd) ELSIF cmd = LabelFontSym THEN labelFont := ParseFont(l.tail, cmd) ELSIF cmd = DotSizeSym THEN SetDot(ParseReal(l.tail, cmd)) ELSIF cmd = CrossSizeSym THEN SetCross(ParseCard(l.tail, cmd)) ELSIF cmd = ChkptIntvSym THEN chkptIntv := ParseCard(l.tail, cmd) ELSIF cmd = RealPrecSym THEN realPrec := ParseCard(l.tail, cmd) - 1 ELSIF cmd = PreviewCmdSym THEN previewCmd := ParseText(l.tail, cmd) ELSIF cmd = PrintCmdSym THEN printCmd := ParseText(l.tail, cmd) ELSIF cmd = OriginSym THEN origin := ParseOrig(l.tail, cmd) ELSIF cmd = OrientSym THEN orientation := ParseOrient(l.tail, cmd) END ELSE RAISE Error("first element of list is not an identifier") END END ParseList; PROCEDURE Circle(r: REAL): Region.T = VAR res := Region.Empty; lo := FLOOR(-r); hi := CEILING(r); BEGIN FOR h := lo TO hi DO FOR v := lo TO hi DO IF h * h + v * v <= FLOOR(r * r) THEN WITH rect = Rect.FromPoint(Point.T{h, v}) DO res := Region.JoinRect(rect, res) END END END END; RETURN res END Circle; PROCEDURE SetDot(sz: REAL) = BEGIN dot := Circle(sz) END SetDot; PROCEDURE Cross(rad: CARDINAL): Region.T = VAR width := MAX(1, ROUND(FLOAT(rad) / 4.0)); lower := - FLOOR(FLOAT(width) / 2.0); upper := CEILING(FLOAT(width) / 2.0); extra := width MOD 2; (* make cross symmetric for odd widths *) BEGIN RETURN Region.Join( Region.FromRect( Rect.T{west := -rad, east := rad + extra, north := lower, south := upper}), Region.FromRect( Rect.T{west := lower, east := upper, north := -rad, south := rad + extra})) END Cross; PROCEDURE SetCross(sz: CARDINAL) = BEGIN cross := Cross(sz); crossBdry := Region.Inset(cross, -1) END SetCross; PROCEDURE CheckLen(l: RefList.T; nm: Atom.T; cnt: CARDINAL) RAISES {Error} = (* Check that the list "l" has length "cnt"; if not, raise "Error" with an error message that refers to "nm" *) BEGIN IF RefList.Length(l) # cnt THEN RAISE Error("\"" & Atom.ToText(nm) & "\" decl has wrong number of args") END END CheckLen; PROCEDURE ParseText(l: RefList.T; nm: Atom.T): TEXT RAISES {Error} = BEGIN CheckLen(l, nm, 1); TYPECASE l.head OF TEXT (t) => RETURN t | Atom.T (a) => RETURN Atom.ToText(a) ELSE RAISE Error("expected arg to \"" & Atom.ToText(nm) & "\" to be a text or identifier") END END ParseText; PROCEDURE ParseCard(l: RefList.T; nm: Atom.T): CARDINAL RAISES {Error} = BEGIN CheckLen(l, nm, 1); TYPECASE l.head OF REF INTEGER (ri) => IF ri^ >= 0 THEN RETURN ri^ END ELSE (* SKIP *) END; RAISE Error("expected arg to \"" & Atom.ToText(nm) & "\" to be a cardinal") END ParseCard; PROCEDURE ParseReal(l: RefList.T; nm: Atom.T): REAL RAISES {Error} = BEGIN CheckLen(l, nm, 1); TYPECASE l.head OF REF REAL (rr) => RETURN rr^ ELSE RAISE Error("expected arg to \"" & Atom.ToText(nm) & "\" to be a real") END END ParseReal; PROCEDURE ParseAtom(l: RefList.T; nm: Atom.T): Atom.T RAISES {Error} = BEGIN CheckLen(l, nm, 1); TYPECASE l.head OF Atom.T (a) => RETURN a ELSE RAISE Error("expected arg to \"" & Atom.ToText(nm) & "\" to be an identifier") END END ParseAtom; VAR (*CONST*) CenterSym := Atom.FromText("center"); SouthWestSym := Atom.FromText("southwest"); PROCEDURE ParseOrig(l: RefList.T; nm: Atom.T): Origin RAISES {Error} = VAR val := ParseAtom(l, nm); BEGIN IF val = CenterSym THEN RETURN Origin.Center ELSIF val = SouthWestSym THEN RETURN Origin.SW ELSE RAISE Error("\"" & Atom.ToText(val) & "\" is not a legal value for \"" & Atom.ToText(nm) & "\"") END END ParseOrig; VAR (*CONST*) PortraitSym := Atom.FromText("portrait"); LandscapeSym := Atom.FromText("landscape"); PROCEDURE ParseOrient(l: RefList.T; nm: Atom.T): Orientation RAISES {Error} = VAR val := ParseAtom(l, nm); BEGIN IF val = PortraitSym THEN RETURN Orientation.Portrait ELSIF val = LandscapeSym THEN RETURN Orientation.Landscape ELSE RAISE Error("\"" & Atom.ToText(val) & "\" is not a legal value for \"" & Atom.ToText(nm) & "\"") END END ParseOrient; VAR (*CONST*) MediumSym := Atom.FromText("medium"); BoldSym := Atom.FromText("bold"); PROCEDURE ParseFont(l: RefList.T; nm: Atom.T): Font.T RAISES {Error} = VAR fonts := NEW(REF ARRAY OF TEXT, RefList.Length(l)); i := 0; BEGIN WHILE l # NIL DO TYPECASE l.head OF TEXT (xfont) => fonts[i] := xfont | RefList.T (f) => fonts[i] := ParseLFontSpec(f, nm); ELSE RAISE Error("invalid font specification for " & Atom.ToText(nm)) END; l := l.tail; INC(i) END; RETURN Font.FromName(fonts^) END ParseFont; PROCEDURE XFontName(family: TEXT; weight: FontWeight; sz: CARDINAL): TEXT = CONST Pre = "-*-"; Mid = "-r-*--*-"; Post = "0-*-*-*-*-*-*"; CONST Wt = ARRAY FontWeight OF TEXT{"*", "medium", "bold"}; BEGIN RETURN Pre & family & "-" & Wt[weight] & Mid & Fmt.Int(sz) & Post END XFontName; PROCEDURE ParseLFontSpec(l: RefList.T; nm: Atom.T): TEXT RAISES {Error} = (* Lookup the font specified by "l", which should be a list of the form "( FontName FontWeight FontSize )". Return the X font name corresponding to the font. *) VAR fontNm: TEXT; fontWt: FontWeight; sz: CARDINAL; BEGIN CheckLen(l, nm, 3); TYPECASE l.head OF Atom.T (a) => fontNm := Atom.ToText(a) | TEXT (t) => fontNm := t ELSE RAISE Error("font name for \"" & Atom.ToText(nm) & "\" not a text or identifier"); END; l := l.tail; TYPECASE l.head OF Atom.T (a) => IF a = MediumSym THEN fontWt := FontWeight.Medium ELSIF a = BoldSym THEN fontWt := FontWeight.Bold ELSE RAISE Error("font weight \"" & Atom.ToText(a) & "\" for \"" & Atom.ToText(nm) & "\" is not a legal value") END ELSE RAISE Error("font weight for \"" & Atom.ToText(nm) & "\" not an identifier"); END; sz := ParseCard(l.tail, nm); RETURN XFontName(fontNm, fontWt, sz) END ParseLFontSpec; PROCEDURE FindString(t, s: TEXT): INTEGER = (* Return the index of the first occurrence in "t" of "s", or -1 if "s" does not occur within "t". *) VAR startChar := Text.GetChar(s, 0); sLen := Text.Length(s); i := 0; BEGIN LOOP i := Text.FindChar(t, startChar, i); IF i < 0 OR Text.Equal(s, Text.Sub(t, i, sLen)) THEN EXIT END; INC(i); (* otherwise increment for next time 'round *) END; RETURN i END FindString; PROCEDURE SubstForVar(VAR (*INOUT*) arg: TEXT; varName, val: TEXT): TEXT RAISES {Error} = (* Return the result of replacing the (first) occurence (if any) of "varName" in "arg" by "val". If such an occurrence is found but "val" is NIL, raise "Error". *) VAR i := FindString(arg, varName); res := arg; BEGIN IF i >= 0 THEN IF val # NIL THEN res := Text.Sub(arg, 0, i) & val & Text.Sub(arg, i + Text.Length(varName)) ELSE RAISE Error("variable \"" & varName & "\" not allowed in this command") END END; RETURN res END SubstForVar; PROCEDURE ParseCmd(cmdLine: TEXT; VAR (*OUT*) cmd: TEXT; VAR (*OUT*) args: REF ARRAY OF TEXT; titleVal, displayVal, filenameVal: TEXT := NIL) RAISES {Error} = <* FATAL Rd.Failure, Thread.Alerted *> (* TextRd.T's cannot fail *) VAR rd := TextRd.New(cmdLine); seq := NEW(TextSeq.T).init(); BEGIN IF Rd.EOF(rd) THEN RAISE Error("no command specified") END; cmd := Lex.Scan(rd); Lex.Skip(rd); WHILE NOT Rd.EOF(rd) DO VAR arg := Lex.Scan(rd); BEGIN arg := SubstForVar(arg, "$Title", titleVal); arg := SubstForVar(arg, "$Display", displayVal); arg := SubstForVar(arg, "$Filename", filenameVal); seq.addhi(arg); END; Lex.Skip(rd) END; args := NEW(REF ARRAY OF TEXT, seq.size()); FOR i := 0 TO LAST(args^) DO args[i] := seq.remlo() END END ParseCmd; PROCEDURE SetFonts(v: VBT.T) = <* LL.sup = VBT.mu *> BEGIN TYPECASE v OF TextEditVBT.T (t) => t.tp.setFont(codeFont) | TextVBT.T (t) => TextVBT.SetFont(t, textFont, TextVBT.GetQuad(t)) | TextPort.T (t) => t.setFont(textFont) | FileBrowserVBT.DirMenu (t) => t.setFont(textFont) | ListVBT.T (t) => SetSplitFonts(t); TYPECASE t.painter OF NULL => (* SKIP *) | ListVBT.TextPainter (p) => p.setFont(t, textFont) ELSE (* SKIP *) END | Split.T (t) => SetSplitFonts(t) ELSE IF MultiClass.Resolve(v) # NIL THEN SetSplitFonts(v) END END END SetFonts; PROCEDURE SetSplitFonts(v: VBT.T) = <* LL.sup = VBT.mu *> <* FATAL MultiSplit.NotAChild *> VAR curr := MultiSplit.Succ(v, NIL); BEGIN WHILE curr # NIL DO SetFonts(curr); curr := MultiSplit.Succ(v, curr) END END SetSplitFonts; BEGIN END JunoConfig.