Selaa lähdekoodia

Add tvision-m2: a TVision-style terminal UI toolkit in GNU Modula-2

New self-contained project (tvision-m2/) implementing a Turbo-Vision-like
character-cell interface in ISO GNU Modula-2 for the terminal.

- tvtty.def: the only non-M2 part - a libc binding for raw mode, winsize
  and read/write.
- tv.def / tv.mod: diffed ANSI cell screen with 16 colours, explicit blink
  handling and UTF-8 box drawing; a retained view tree (groups/windows) with
  clipping and z-order; widgets (text, frame, input line, button, check box,
  radio buttons, list box, scroll bar); focus chain; keyboard and xterm SGR
  mouse input (click, drag titles, wheel scroll); modal Message dialog.
- demo.mod: two windows exercising the widget set and mouse interaction.
- ttysize.mod, Makefile, README.md and SUMMARY.md.

Verified by driving a pty and reconstructing the frames: layout renders,
mouse click opens the modal dialog, and window titles drag in every
direction (clamped so the title stays on screen).
Eric Streit 3 viikkoa sitten
vanhempi
commit
900e85cc21
10 muutettua tiedostoa jossa 1896 lisäystä ja 0 poistoa
  1. 18 0
      tvision-m2/Makefile
  2. 72 0
      tvision-m2/README.md
  3. 69 0
      tvision-m2/SUMMARY.md
  4. BIN
      tvision-m2/demo
  5. 113 0
      tvision-m2/demo.mod
  6. 96 0
      tvision-m2/ttysize.mod
  7. 149 0
      tvision-m2/tv.def
  8. 1360 0
      tvision-m2/tv.mod
  9. BIN
      tvision-m2/tv.o
  10. 19 0
      tvision-m2/tvtty.def

+ 18 - 0
tvision-m2/Makefile

@@ -0,0 +1,18 @@
+GM2 = gm2
+FLAGS = -fiso -I.
+
+all: demo
+
+tv.o: tv.mod tv.def tvtty.def
+	$(GM2) $(FLAGS) -c tv.mod
+
+demo: tv.o demo.mod
+	$(GM2) $(FLAGS) tv.o demo.mod -o demo
+
+ttysize: ttysize.mod tvtty.def
+	$(GM2) $(FLAGS) ttysize.mod -o ttysize
+
+clean:
+	rm -f *.o demo ttysize
+
+.PHONY: all clean

+ 72 - 0
tvision-m2/README.md

@@ -0,0 +1,72 @@
+# tvision-m2 - a TVision/Turbo-Vision style toolkit in GNU Modula-2
+
+A character-cell UI toolkit written in ISO GNU Modula-2 (`gm2 -fiso`) that
+runs in a terminal (xterm and friends).  It is a small, self-contained
+Turbo-Vision-like library: a retained view tree with clipping and z-order,
+a diffed ANSI cell screen, keyboard and mouse decoding (xterm SGR mouse
+reports), widgets, and modal dialogs.
+
+Everything is Modula-2 except `tvtty.def`, the tiny libc binding used to put
+the terminal in raw mode and to query its size (`termios`, `ioctl`, `read`,
+`write`).  There is no C source and no graphics dependency.
+
+## Files
+
+| file | role |
+|---|---|
+| `tvtty.def` | libc binding (raw mode, winsize, read/write) |
+| `tv.def` / `tv.mod` | the toolkit: screen, view tree, widgets, events |
+| `demo.mod` | demo application |
+| `ttysize.mod` | diagnostic: checks the tty binding |
+
+## Build & run
+
+    gm2 -fiso -c tvtty.def
+    gm2 -fiso -I. -c tv.mod
+    gm2 -fiso -I. tv.o demo.mod -o demo
+    ./demo
+
+or simply `make`.  Run it from a real terminal; `Esc` exits.
+
+## What works
+
+- Alternate screen, hidden/show cursor, terminal restored on exit.
+- 16-colour foreground/background with blink; UTF-8 output (box-drawing).
+- Perspective view tree: groups and windows contain children; drawing is
+  clipped to each window; overlapping windows are ordered (z-order) and the
+  one you click comes to the front.
+- Windows: double-line frame, title, ` X ` close box, draggable by the title
+  bar with the mouse.
+- Widgets: static text, single/double frame, input line (insert, backspace,
+  delete, Home/End/arrows), push button, check box, radio buttons, list box
+  with selection and wheel scrolling, vertical scroll bar.
+- Focus chain: `Tab`/`Shift-Tab` and up/down cycle widgets; the focused
+  input gets the hardware cursor.
+- Keyboard decoding: printable UTF-8, arrows, Home/End, PgUp/PgDn, Ins/Del,
+  Tab/Shift-Tab, F1-F12, Enter, Space, Backspace, Esc, Ctrl-C.
+- Mouse: xterm SGR reporting - click to focus/press, drag window titles,
+  click list rows, wheel-scroll lists.
+- Diffed redraw (only changed cells are written).
+- Modal dialog helper (`Message`), used by the demo's OK button.
+
+## Roadmap ideas
+
+- Drop-down menus and a menu-bar widget.
+- Modal `Dialog` with arbitrary controls; form validation.
+- Nested clipping groups and a desktop with many windows / tiling.
+- More widgets: multi-line editor, tabs, combo box, progress bar.
+- `SIGWINCH`-driven relayout (today the app polls the size).
+- Mouse hover highlight and resizable windows.
+
+## GNU Modula-2 notes
+
+- Value-returning procedures with no parameters need explicit `()`
+  (`PROCEDURE Cols() : INTEGER;`), otherwise the `.def` fails to parse when
+  compiled through the `.mod`.
+- Pointer fields require `^` (`v^.rect`); there is no implicit dereference.
+- A compound `BEGIN ... END` block is not a statement, so it cannot appear
+  directly in a `CASE` alternative - use a helper procedure.
+- An `INTEGER` index compared with `HIGH(array)` (a `CARDINAL`) must be
+  converted: `i <= VAL(INTEGER, HIGH(a))`.
+- The libc `struct termios`/`struct winsize` are accessed through an opaque
+  byte buffer with fixed Linux offsets (see `tv.mod`).

+ 69 - 0
tvision-m2/SUMMARY.md

@@ -0,0 +1,69 @@
+# tvision-m2 - milestone summary (2026-09-16)
+
+A TVision/Turbo-Vision style character-cell UI toolkit written in ISO GNU
+Modula-2 (`gm2 -fiso`) for the terminal, developed alongside the tigr and
+microui bindings in this repository.  No C source and no graphics
+dependency; the only non-Modula-2 part is the libc binding `tvtty.def`.
+
+## Status
+
+Working milestone: a retained view tree with clipping and z-order, a diffed
+ANSI screen, keyboard + mouse input, widgets and modal dialogs.  Builds
+warning-free and was exercised by driving a real pty (frames reconstructed
+from the captured terminal stream).
+
+## Files
+
+| file | role |
+|---|---|
+| `tvtty.def` | libc binding: raw mode, winsize, read/write |
+| `tv.def` / `tv.mod` | screen, view tree, widgets, events, modal dialog |
+| `demo.mod` | demo application (two windows, all widgets, mouse) |
+| `ttysize.mod` | diagnostic for the tty binding |
+| `Makefile` | build |
+| `README.md` | build/run + API + GM2 notes |
+
+## Implemented
+
+- Alternate screen, hidden/show cursor, terminal restore on exit.
+- 16-colour fg/bg, explicit blink on/off, UTF-8 box drawing, clipping stack.
+- Diffed redraw: only changed cells are written.
+- View tree: groups and windows own children; per-window clipping; z-order
+  with click-to-front; windows draggable by the title row (any direction) and
+  clamped so the title stays on screen; ` X ` close box.
+- Widgets: static text, frame, input line (insert/backspace/delete/Home/End/
+  arrows), push button, check box, radio buttons, list box (selection, wheel
+  scroll) and vertical scroll bar.
+- Focus chain (Tab / Shift-Tab / up-down); hardware cursor in the focused
+  input; Enter/Space activate.
+- Keyboard decoding: printable UTF-8, arrows, Home/End, PgUp/PgDn, Ins/Del,
+  F1-F12, Enter, Space, Backspace, Esc, Ctrl-C.
+- Mouse: xterm SGR reporting (mode 1002/1006) - click to focus/press, drag
+  titles, click list rows, wheel scroll.
+- Modal `Message` dialog with its own event loop.
+
+## Verified
+
+Driving the demo through a pty and reconstructing the screen:
+- initial layout (Editor + About windows, list, radios, check box, buttons,
+  status bar) renders correctly;
+- clicking `OK` with the mouse opens the modal Info box;
+- dragging the title moves the window in every direction and clamps at the
+  top-left so the title stays visible.
+
+## Known limitations / next steps
+
+- No drop-down menus / menu-bar yet.
+- `Message` is the only dialog helper; no generic form dialog.
+- Windows are not resizable; no hover highlight.
+- Terminal size is polled (no `SIGWINCH`-driven relayout).
+- Nested clipping groups beyond windows are untested.
+
+## GNU Modula-2 notes
+
+See `README.md`: value-returning no-arg procedures need `()` in the `.def`;
+pointer fields need `^`; a `BEGIN...END` block cannot be a `CASE` branch;
+`INTEGER` vs `HIGH()` comparisons need `VAL`; termios/winsize use fixed Linux
+offsets.  A subtle runtime bug was found and fixed in the ANSI output: SGR
+attributes accumulate, so blink must be emitted explicitly as `;5`/`;25` or
+it leaks into every following cell.

BIN
tvision-m2/demo


+ 113 - 0
tvision-m2/demo.mod

@@ -0,0 +1,113 @@
+MODULE demo;
+
+(* Demo of the terminal TVision-style toolkit (view tree, widgets, mouse).
+
+   Build (from this directory):
+       gm2 -fiso -I. tv.o demo.mod -o demo
+   Run from a normal terminal.  Tab cycles widgets, arrow keys navigate,
+   Enter/Space activate, the mouse drags windows and clicks controls,
+   Esc exits. *)
+
+FROM InOut IMPORT WriteString, WriteLn;
+FROM SYSTEM IMPORT ADR, CAST;
+IMPORT tv;
+
+VAR
+    desk, w1, w2, status : tv.ViewPtr;
+    input, check, r1, r2, listv, okb, closeb : tv.ViewPtr;
+    t : tv.ViewPtr;
+    listData : tv.ListData;
+    e : tv.Event;
+    quit : BOOLEAN;
+    cmd : INTEGER;
+    sender : tv.ViewPtr;
+
+BEGIN
+    IF NOT tv.Init() THEN
+        WriteString("cannot initialise the terminal (not a tty?)"); WriteLn;
+        HALT
+    END;
+    desk := tv.Desk();
+
+    (* status bar (bottom line) *)
+    status := tv.NewView(tv.VText, 0, tv.Rows() - 1, tv.Cols(), 1);
+    tv.SetText(status, " Tab: next   arrows: move   Enter: press   mouse: click/drag   Esc: quit");
+    tv.AddView(desk, status);
+
+    (* ---- Editor window ---- *)
+    w1 := tv.NewView(tv.VWindow, 3, 2, 56, 16);
+    tv.SetText(w1, "Editor");
+
+    t := tv.NewView(tv.VText, 5, 4, 24, 1);
+    tv.SetText(t, "Name:"); tv.AddView(w1, t);
+
+    input := tv.NewView(tv.VInput, 12, 4, 32, 1);
+    tv.SetText(input, "Modula-2"); tv.AddView(w1, input);
+
+    check := tv.NewView(tv.VCheck, 12, 6, 24, 1);
+    tv.SetText(check, "Enabled"); check^.flags := tv.vfSelected; tv.AddView(w1, check);
+
+    t := tv.NewView(tv.VText, 5, 8, 6, 1);
+    tv.SetText(t, "Mode:"); tv.AddView(w1, t);
+
+    r1 := tv.NewView(tv.VRadio, 12, 8, 12, 1);
+    tv.SetText(r1, "Fast"); r1^.flags := tv.vfSelected; tv.AddView(w1, r1);
+    r2 := tv.NewView(tv.VRadio, 26, 8, 12, 1);
+    tv.SetText(r2, "Safe"); tv.AddView(w1, r2);
+
+    listData.items[0] := "Alpha";   listData.items[1] := "Bravo";
+    listData.items[2] := "Charlie"; listData.items[3] := "Delta";
+    listData.items[4] := "Echo";    listData.items[5] := "Foxtrot";
+    listData.count := 6;
+    listv := tv.NewView(tv.VList, 5, 10, 22, 6);
+    listv^.list := CAST(tv.ListDataPtr, ADR(listData));
+    tv.AddView(w1, listv);
+
+    okb := tv.NewView(tv.VButton, 30, 15, 8, 1);
+    tv.SetText(okb, "OK"); tv.SetTag(okb, tv.cmOK); tv.AddView(w1, okb);
+    closeb := tv.NewView(tv.VButton, 41, 15, 10, 1);
+    tv.SetText(closeb, "Close"); tv.SetTag(closeb, tv.cmCancel); tv.AddView(w1, closeb);
+
+    tv.AddView(desk, w1);
+
+    (* ---- About window ---- *)
+    w2 := tv.NewView(tv.VWindow, 38, 7, 38, 8);
+    tv.SetText(w2, "About");
+    t := tv.NewView(tv.VText, 40, 9, 34, 1);
+    tv.SetText(t, "tvision-m2"); tv.AddView(w2, t);
+    t := tv.NewView(tv.VText, 40, 10, 34, 1);
+    tv.SetText(t, "A TVision-style toolkit in GNU M2."); tv.AddView(w2, t);
+    t := tv.NewView(tv.VText, 40, 11, 34, 1);
+    tv.SetText(t, "Drag window titles with the mouse."); tv.AddView(w2, t);
+    tv.AddView(desk, w2);
+
+    tv.BringToFront(w1);
+    tv.FocusView(input);
+    tv.Finish;
+
+    quit := FALSE;
+    WHILE NOT quit DO
+        IF tv.Resized() THEN tv.Finish END;
+        IF tv.ReadEvent(e) THEN
+            IF (e.kind = tv.evKey) AND (e.key = tv.kEsc) THEN
+                quit := TRUE
+            ELSE
+                cmd := tv.HandleEvent(e);
+                sender := tv.Sender();
+                IF cmd = tv.cmClose THEN
+                    IF sender # NIL THEN tv.DelView(sender) END
+                ELSIF cmd = tv.cmOK THEN
+                    tv.Message("Info", "You pressed OK.")
+                ELSIF cmd = tv.cmCancel THEN
+                    tv.DelView(w1)
+                ELSIF (sender = r1) OR (sender = r2) THEN
+                    r1^.flags := 0; r2^.flags := 0;
+                    IF sender # NIL THEN sender^.flags := tv.vfSelected END
+                END
+            END;
+            IF NOT quit THEN tv.Finish END
+        END
+    END;
+
+    tv.Done
+END demo.

+ 96 - 0
tvision-m2/ttysize.mod

@@ -0,0 +1,96 @@
+MODULE ttysize;
+
+(* Quick check of the tvtty libc binding: raw mode, terminal size, timed read. *)
+
+FROM SYSTEM IMPORT ADDRESS, ADR, ADDADR, CAST, BYTE;
+FROM tvtty IMPORT tcgetattr, tcsetattr, ioctl, read, write;
+FROM InOut IMPORT WriteString, WriteLn, WriteInt;
+FROM Delay IMPORT Delay;
+
+TYPE BytePtr = POINTER TO BYTE;
+
+CONST
+    ICANON = 2; ECHO = 8; ISIG = 1; IEXTEN = 32768;
+    ICRNL = 256; IXON = 1024;
+    TCSAFLUSH = 2; TIOCGWINSZ = 5413H;
+
+VAR
+    t  : ARRAY [0..63] OF BYTE;
+    ws : ARRAY [0..7] OF BYTE;
+    rc : INTEGER;
+
+PROCEDURE GetDWord(p : ADDRESS; off : INTEGER) : CARDINAL;
+    VAR q : BytePtr; v : CARDINAL;
+BEGIN
+    q := CAST(BytePtr, ADDADR(p, off));
+    v := VAL(CARDINAL, q^);
+    q := CAST(BytePtr, ADDADR(q, 1)); v := v + VAL(CARDINAL, q^) * 256;
+    q := CAST(BytePtr, ADDADR(q, 1)); v := v + VAL(CARDINAL, q^) * 65536;
+    q := CAST(BytePtr, ADDADR(q, 1)); v := v + VAL(CARDINAL, q^) * 16777216;
+    RETURN v
+END GetDWord;
+
+PROCEDURE PutDWord(p : ADDRESS; off : INTEGER; v : CARDINAL);
+    VAR q : BytePtr; x : CARDINAL;
+BEGIN
+    x := v;
+    q := CAST(BytePtr, ADDADR(p, off)); q^ := VAL(BYTE, x MOD 256); x := x DIV 256;
+    q := CAST(BytePtr, ADDADR(q, 1));  q^ := VAL(BYTE, x MOD 256); x := x DIV 256;
+    q := CAST(BytePtr, ADDADR(q, 1));  q^ := VAL(BYTE, x MOD 256); x := x DIV 256;
+    q := CAST(BytePtr, ADDADR(q, 1));  q^ := VAL(BYTE, x MOD 256)
+END PutDWord;
+
+PROCEDURE Raw;
+    VAR lf, iff : CARDINAL;
+BEGIN
+    rc := tcgetattr(0, ADR(t));
+    lf := GetDWord(ADR(t), 12);
+    lf := VAL(CARDINAL, CAST(BITSET, lf) * CAST(BITSET, MAX(CARDINAL) - (ICANON + ECHO + ISIG + IEXTEN)));
+    PutDWord(ADR(t), 12, lf);
+    iff := GetDWord(ADR(t), 0);
+    iff := VAL(CARDINAL, CAST(BITSET, iff) * CAST(BITSET, MAX(CARDINAL) - (ICRNL + IXON)));
+    PutDWord(ADR(t), 0, iff);
+    t[17 + 6] := 0;   (* VMIN = 0 *)
+    t[17 + 5] := 1;   (* VTIME = 1 (0.1 s) *)
+    rc := tcsetattr(0, TCSAFLUSH, ADR(t))
+END Raw;
+
+PROCEDURE Restore;
+BEGIN
+    rc := tcsetattr(0, TCSAFLUSH, ADR(t))
+END Restore;
+
+VAR
+    rows, cols, tries : INTEGER;
+    buf : ARRAY [0..31] OF CHAR;
+    b   : CARDINAL;
+    found : BOOLEAN;
+
+BEGIN
+    WriteString("tcgetattr rc="); WriteInt(rc, 0); WriteLn;
+    Raw;
+    IF ioctl(1, TIOCGWINSZ, ADR(ws)) = 0 THEN
+        rows := VAL(INTEGER, ws[0]) + VAL(INTEGER, ws[1]) * 256;
+        cols := VAL(INTEGER, ws[2]) + VAL(INTEGER, ws[3]) * 256
+    ELSE
+        rows := 24; cols := 80
+    END;
+    WriteString("size="); WriteInt(rows, 0); WriteString("x"); WriteInt(cols, 0); WriteLn;
+    WriteString("waiting 2s for a key (press one): ");
+    found := FALSE;
+    tries := 0;
+    WHILE (NOT found) AND (tries < 20) DO
+        rc := read(0, ADR(buf), 1);
+        IF rc > 0 THEN
+            b := VAL(CARDINAL, ORD(buf[0]));
+            WriteString("got byte "); WriteInt(VAL(INTEGER, b), 0); WriteLn;
+            found := TRUE
+        ELSE
+            tries := tries + 1;
+            Delay(1)
+        END
+    END;
+    IF NOT found THEN WriteString("no key"); WriteLn END;
+    Restore;
+    WriteString("restored"); WriteLn
+END ttysize.

+ 149 - 0
tvision-m2/tv.def

@@ -0,0 +1,149 @@
+DEFINITION MODULE tv;
+
+(* A small TVision/Turbo-Vision style character-cell UI toolkit in ISO
+   GNU Modula-2 (gm2 -fiso), terminal backend.
+
+   It provides a tiny retained view tree (groups/windows containing widgets)
+   with clipping and z-order, a diffed ANSI cell screen, keyboard and mouse
+   decoding (xterm SGR mouse reports), and widgets: static text, frame,
+   input line, button, check box, radio buttons, list box and scroll bar.
+
+   Compile (from this directory):
+       gm2 -fiso -c tvtty.def
+       gm2 -fiso -I. -c tv.mod
+       gm2 -fiso -I. tv.o demo.mod -o demo
+
+   The only non-Modula-2 part is tvtty.def (libc termios/ioctl/read/write). *)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+CONST
+    (* 16 terminal colours *)
+    Black = 0;  Blue = 1;  Green = 2;  Cyan = 3;
+    Red = 4;  Magenta = 5;  Brown = 6;  LightGray = 7;
+    DarkGray = 8;  LightBlue = 9;  LightGreen = 10;  LightCyan = 11;
+    LightRed = 12;  LightMagenta = 13;  Yellow = 14;  White = 15;
+
+    SingleFrame = 1;
+    DoubleFrame = 2;
+
+    (* key codes *)
+    kNone  = 0;  kChar  = 1;  kUp    = 2;  kDown  = 3;
+    kLeft  = 4;  kRight = 5;  kHome  = 6;  kEnd   = 7;
+    kPgUp  = 8;  kPgDn  = 9;  kEnter = 10; kEsc   = 11;
+    kTab   = 12; kBack  = 13; kDel   = 14; kIns   = 15;
+    kF1    = 20; kF2    = 21; kF3    = 22; kF4    = 23; kF5  = 24;
+    kF6    = 25; kF7    = 26; kF8    = 27; kF9    = 28; kF10 = 29;
+    kCtrlC = 30; kF11   = 31; kF12   = 32; kSpace = 33;
+
+    (* built-in commands (button tags) *)
+    cmNone   = 0;
+    cmClose  = 1;
+    cmOK     = 2;
+    cmCancel = 3;
+    cmYes    = 4;
+    cmNo     = 5;
+
+    (* view flags *)
+    vfDisabled = 1;
+    vfSelected = 2;
+    vfFramed   = 4;
+
+    MAXLINE  = 128;
+    MAXVIEWS = 128;
+
+TYPE
+    Attr  = CARDINAL;
+    Point = RECORD x, y : INTEGER; END;
+    Rect  = RECORD x, y, w, h : INTEGER; END;
+
+    EventKind = (evNone, evKey, evMouse);
+
+    Event = RECORD
+                kind  : EventKind;
+                key   : INTEGER;
+                ch    : CARDINAL;
+                ctrl  : BOOLEAN;
+                mx, my : INTEGER;
+                mbtn  : INTEGER;     (* 1 left, 2 middle, 3 right *)
+                mpressed, mreleased : BOOLEAN;
+                mwheel : INTEGER;    (* -1 up, +1 down *)
+            END;
+
+(* ---- screen ------------------------------------------------------------ *)
+
+PROCEDURE Init() : BOOLEAN;
+PROCEDURE Done;
+PROCEDURE Cols() : INTEGER;
+PROCEDURE Rows() : INTEGER;
+PROCEDURE Resized() : BOOLEAN;
+
+PROCEDURE A (fg, bg : INTEGER; blink : BOOLEAN) : Attr;
+PROCEDURE Clear (attr : Attr);
+PROCEDURE Fill (x, y, w, h : INTEGER; cp : CARDINAL; attr : Attr);
+PROCEDURE DrawCh (x, y : INTEGER; cp : CARDINAL; attr : Attr);
+PROCEDURE DrawText (x, y : INTEGER; s : ARRAY OF CHAR; attr : Attr);
+PROCEDURE DrawBox (r : Rect; style : INTEGER; attr : Attr);
+PROCEDURE DrawShadow (r : Rect; attr : Attr);
+PROCEDURE SetCursor (x, y : INTEGER);
+PROCEDURE HideCursor;
+PROCEDURE Present;
+
+PROCEDURE ReadEvent (VAR e : Event) : BOOLEAN;
+
+(* ---- text helpers ------------------------------------------------------ *)
+
+PROCEDURE TextLen (s : ARRAY OF CHAR) : INTEGER;
+PROCEDURE CopyText (VAR dest : ARRAY OF CHAR; s : ARRAY OF CHAR);
+PROCEDURE AppendText (VAR dest : ARRAY OF CHAR; s : ARRAY OF CHAR);
+PROCEDURE PadText (VAR dest : ARRAY OF CHAR; s : ARRAY OF CHAR; width : INTEGER);
+PROCEDURE IntToText (VAR dest : ARRAY OF CHAR; n : INTEGER; width : INTEGER);
+
+(* ---- view tree --------------------------------------------------------- *)
+
+TYPE
+    VKind = (VGroup, VWindow, VText, VFrame, VInput, VButton,
+             VCheck, VRadio, VList, VScroll);
+
+    ListData = RECORD
+                   items : ARRAY [0..255] OF ARRAY [0..63] OF CHAR;
+                   count : INTEGER;
+               END;
+    ListDataPtr = POINTER TO ListData;
+
+    ViewPtr = POINTER TO View;
+    View = RECORD
+               kind   : VKind;
+               rect   : Rect;
+               parent, next, prev, first, last, focus : ViewPtr;
+               clipKids : BOOLEAN;
+               text   : ARRAY [0..MAXLINE - 1] OF CHAR;
+               len, pos : INTEGER;
+               value, minv, maxv, tag : INTEGER;
+               flags  : CARDINAL;
+               list   : ListDataPtr;
+           END;
+
+PROCEDURE NewView (kind : VKind; x, y, w, h : INTEGER) : ViewPtr;
+PROCEDURE FreeView (v : ViewPtr);
+PROCEDURE AddView (parent, child : ViewPtr);
+PROCEDURE DelView (v : ViewPtr);
+PROCEDURE SetText (v : ViewPtr; s : ARRAY OF CHAR);
+PROCEDURE SetTag (v : ViewPtr; t : INTEGER);
+PROCEDURE Desk () : ViewPtr;
+
+PROCEDURE DrawTree;                   (* draw desktop + all views *)
+PROCEDURE Finish;                     (* DrawTree + Present *)
+
+PROCEDURE HandleEvent (e : Event) : INTEGER;   (* returns a command *)
+PROCEDURE Sender () : ViewPtr;                (* view that produced it *)
+
+PROCEDURE BringToFront (v : ViewPtr);
+PROCEDURE FocusView (v : ViewPtr);
+PROCEDURE FocusNext (backwards : BOOLEAN);
+PROCEDURE MoveViewBy (v : ViewPtr; dx, dy : INTEGER);
+
+(* Modal helper: shows a small centered box with the text and an OK button. *)
+PROCEDURE Message (title : ARRAY OF CHAR; text : ARRAY OF CHAR);
+
+END tv.

+ 1360 - 0
tvision-m2/tv.mod

@@ -0,0 +1,1360 @@
+IMPLEMENTATION MODULE tv;
+
+(* Terminal (ANSI + termios) backend and a small retained view-tree toolkit.
+   See tv.def.  Pure ISO GNU Modula-2 apart from the libc binding tvtty.def. *)
+
+FROM SYSTEM IMPORT ADDRESS, ADR, ADDADR, CAST, BYTE;
+FROM tvtty IMPORT tcgetattr, tcsetattr, ioctl, read, write;
+
+CONST
+    MAXROWS  = 120;
+    MAXCOLS  = 300;
+    MAXCELLS = MAXROWS * MAXCOLS;
+    OUTCAP   = 262144;
+
+    ESCc = CHR(27);
+
+    TCSAFLUSH = 2;  TIOCGWINSZ = 5413H;
+    ICANON = 2; ECHO = 8; ISIG = 1; IEXTEN = 32768;
+    ICRNL = 256; IXON = 1024;
+    CC_OFF = 17; VMIN = 6; VTIME = 5;
+
+    H_SINGLE = 2500H; V_SINGLE = 2502H;
+    TL_SINGLE = 250CH; TR_SINGLE = 2510H; BL_SINGLE = 2514H; BR_SINGLE = 2518H;
+    H_DOUBLE = 2550H; V_DOUBLE = 2551H;
+    TL_DOUBLE = 2554H; TR_DOUBLE = 2557H; BL_DOUBLE = 255AH; BR_DOUBLE = 255DH;
+
+    vfPressed = 8;   (* internal, transient *)
+
+TYPE
+    Cell = RECORD cp : CARDINAL; attr : CARDINAL; END;
+    BytePtr = POINTER TO BYTE;
+
+VAR
+    cols, rows       : INTEGER;
+    cells            : ARRAY [0..MAXCELLS - 1] OF Cell;
+    prev             : ARRAY [0..MAXCELLS - 1] OF Cell;
+    prevValid        : BOOLEAN;
+    outBuf           : ARRAY [0..OUTCAP - 1] OF CHAR;
+    outLen           : CARDINAL;
+    inBuf            : ARRAY [0..255] OF BYTE;
+    inLen, inPos     : INTEGER;
+    term             : ARRAY [0..63] OF BYTE;
+    cursorX, cursorY : INTEGER;
+    cursorShown      : BOOLEAN;
+
+    clipStack : ARRAY [0..15] OF Rect;
+    clipN     : INTEGER;
+
+    pool   : ARRAY [0..MAXVIEWS - 1] OF View;
+    used   : ARRAY [0..MAXVIEWS - 1] OF BOOLEAN;
+    desk   : ViewPtr;
+    modal  : ViewPtr;
+
+    dragWin : ViewPtr;
+    lastSender : ViewPtr;
+    dragOX, dragOY : INTEGER;
+
+    focusList  : ARRAY [0..MAXVIEWS - 1] OF ViewPtr;
+    focusCount : INTEGER;
+
+(*========================================================================*)
+(*  libc struct helpers                                                    *)
+(*========================================================================*)
+
+PROCEDURE GetDWord(p : ADDRESS; off : INTEGER) : CARDINAL;
+    VAR q : BytePtr; v : CARDINAL;
+BEGIN
+    q := CAST(BytePtr, ADDADR(p, off));
+    v := VAL(CARDINAL, q^);
+    q := CAST(BytePtr, ADDADR(q, 1)); v := v + VAL(CARDINAL, q^) * 256;
+    q := CAST(BytePtr, ADDADR(q, 1)); v := v + VAL(CARDINAL, q^) * 65536;
+    q := CAST(BytePtr, ADDADR(q, 1)); v := v + VAL(CARDINAL, q^) * 16777216;
+    RETURN v
+END GetDWord;
+
+PROCEDURE PutDWord(p : ADDRESS; off : INTEGER; v : CARDINAL);
+    VAR q : BytePtr; x : CARDINAL;
+BEGIN
+    x := v;
+    q := CAST(BytePtr, ADDADR(p, off)); q^ := VAL(BYTE, x MOD 256); x := x DIV 256;
+    q := CAST(BytePtr, ADDADR(q, 1));  q^ := VAL(BYTE, x MOD 256); x := x DIV 256;
+    q := CAST(BytePtr, ADDADR(q, 1));  q^ := VAL(BYTE, x MOD 256); x := x DIV 256;
+    q := CAST(BytePtr, ADDADR(q, 1));  q^ := VAL(BYTE, x MOD 256)
+END PutDWord;
+
+PROCEDURE GetWinSize(VAR r, c : INTEGER);
+    VAR ws : ARRAY [0..7] OF BYTE;
+BEGIN
+    IF ioctl(1, TIOCGWINSZ, ADR(ws)) = 0 THEN
+        r := VAL(INTEGER, ws[0]) + VAL(INTEGER, ws[1]) * 256;
+        c := VAL(INTEGER, ws[2]) + VAL(INTEGER, ws[3]) * 256
+    ELSE
+        r := 24; c := 80
+    END;
+    IF r < 3 THEN r := 24 END;
+    IF c < 20 THEN c := 80 END;
+    IF r > MAXROWS THEN r := MAXROWS END;
+    IF c > MAXCOLS THEN c := MAXCOLS END
+END GetWinSize;
+
+(*========================================================================*)
+(*  Output buffer + escape helpers                                         *)
+(*========================================================================*)
+
+PROCEDURE OutCh(c : CHAR);
+BEGIN
+    IF outLen < OUTCAP THEN
+        outBuf[outLen] := c;
+        outLen := outLen + 1
+    END
+END OutCh;
+
+PROCEDURE OutStr(s : ARRAY OF CHAR);
+    VAR i : INTEGER;
+BEGIN
+    i := 0;
+    WHILE (i <= VAL(INTEGER, HIGH(s))) AND (s[i] # 0C) DO
+        OutCh(s[i]);
+        i := i + 1
+    END
+END OutStr;
+
+PROCEDURE OutInt(n : INTEGER);
+    VAR buf : ARRAY [0..15] OF CHAR;
+BEGIN
+    IntToText(buf, n, 0);
+    OutStr(buf)
+END OutInt;
+
+PROCEDURE OutCP(cp : CARDINAL);
+BEGIN
+    IF cp < 80H THEN
+        OutCh(CHR(cp))
+    ELSIF cp < 800H THEN
+        OutCh(CHR(0C0H + cp DIV 64));
+        OutCh(CHR(80H + cp MOD 64))
+    ELSIF cp < 10000H THEN
+        OutCh(CHR(0E0H + cp DIV 4096));
+        OutCh(CHR(80H + (cp DIV 64) MOD 64));
+        OutCh(CHR(80H + cp MOD 64))
+    ELSE
+        OutCh(CHR(0F0H + cp DIV 262144));
+        OutCh(CHR(80H + (cp DIV 4096) MOD 64));
+        OutCh(CHR(80H + (cp DIV 64) MOD 64));
+        OutCh(CHR(80H + cp MOD 64))
+    END
+END OutCP;
+
+PROCEDURE EmitSGR(attr : Attr);
+    VAR fg, bg, bl : INTEGER;
+BEGIN
+    fg := VAL(INTEGER, attr MOD 16);
+    bg := VAL(INTEGER, (attr DIV 16) MOD 16);
+    bl := VAL(INTEGER, (attr DIV 256) MOD 2);
+    OutCh(ESCc); OutCh('[');
+    IF fg < 8 THEN OutInt(30 + fg) ELSE OutInt(90 + fg - 8) END;
+    OutCh(';');
+    IF bg < 8 THEN OutInt(40 + bg) ELSE OutInt(100 + bg - 8) END;
+    (* blink is a mode: always emit it explicitly (5 = on, 25 = off) so that
+       it does not leak into subsequent cells *)
+    IF bl # 0 THEN OutStr(";5") ELSE OutStr(";25") END;
+    OutCh('m')
+END EmitSGR;
+
+PROCEDURE EmitMove(x, y : INTEGER);
+BEGIN
+    OutCh(ESCc); OutCh('[');
+    OutInt(y + 1); OutCh(';'); OutInt(x + 1); OutCh('H')
+END EmitMove;
+
+PROCEDURE FlushOut;
+BEGIN
+    IF outLen > 0 THEN
+        IF write(1, ADR(outBuf), outLen) < 0 THEN END;
+        outLen := 0
+    END
+END FlushOut;
+
+(*========================================================================*)
+(*  Terminal setup                                                         *)
+(*========================================================================*)
+
+PROCEDURE Init() : BOOLEAN;
+    VAR lf, iff : CARDINAL; i : INTEGER;
+BEGIN
+    IF tcgetattr(0, ADR(term)) # 0 THEN RETURN FALSE END;
+    lf := GetDWord(ADR(term), 12);
+    lf := VAL(CARDINAL, CAST(BITSET, lf) *
+              CAST(BITSET, MAX(CARDINAL) - (ICANON + ECHO + ISIG + IEXTEN)));
+    PutDWord(ADR(term), 12, lf);
+    iff := GetDWord(ADR(term), 0);
+    iff := VAL(CARDINAL, CAST(BITSET, iff) *
+               CAST(BITSET, MAX(CARDINAL) - (ICRNL + IXON)));
+    PutDWord(ADR(term), 0, iff);
+    term[CC_OFF + VMIN] := VAL(BYTE, 0);
+    term[CC_OFF + VTIME] := VAL(BYTE, 1);
+    IF tcsetattr(0, TCSAFLUSH, ADR(term)) # 0 THEN RETURN FALSE END;
+    GetWinSize(rows, cols);
+    prevValid := FALSE;
+    inLen := 0; inPos := 0;
+    cursorX := 0; cursorY := 0; cursorShown := FALSE;
+    clipN := 0;
+    modal := NIL; dragWin := NIL; lastSender := NIL;
+    (* reset view pool *)
+    FOR i := 0 TO MAXVIEWS - 1 DO used[i] := FALSE END;
+    desk := NIL;
+    OutCh(ESCc); OutStr("[?1049h");
+    OutCh(ESCc); OutStr("[2J");
+    OutCh(ESCc); OutStr("[?1002h");   (* button/mouse-drag reporting *)
+    OutCh(ESCc); OutStr("[?1006h");   (* SGR mouse encoding *)
+    OutCh(ESCc); OutStr("[?25l");
+    FlushOut;
+    Clear(A(LightGray, Blue, FALSE));
+    Present;
+    desk := NewView(VGroup, 0, 0, cols, rows);
+    RETURN TRUE
+END Init;
+
+PROCEDURE Done;
+BEGIN
+    OutCh(ESCc); OutStr("[?1006l");
+    OutCh(ESCc); OutStr("[?1002l");
+    OutCh(ESCc); OutStr("[0m");
+    OutCh(ESCc); OutStr("[?25h");
+    OutCh(ESCc); OutStr("[?1049l");
+    FlushOut;
+    IF tcsetattr(0, TCSAFLUSH, ADR(term)) # 0 THEN END
+END Done;
+
+PROCEDURE Cols() : INTEGER;
+BEGIN RETURN cols END Cols;
+
+PROCEDURE Rows() : INTEGER;
+BEGIN RETURN rows END Rows;
+
+PROCEDURE Resized() : BOOLEAN;
+    VAR r, c : INTEGER;
+BEGIN
+    GetWinSize(r, c);
+    IF (r # rows) OR (c # cols) THEN
+        rows := r; cols := c;
+        prevValid := FALSE;
+        Clear(A(LightGray, Blue, FALSE));
+        RETURN TRUE
+    END;
+    RETURN FALSE
+END Resized;
+
+(*========================================================================*)
+(*  Drawing primitives (with a clip stack)                                 *)
+(*========================================================================*)
+
+PROCEDURE A (fg, bg : INTEGER; blink : BOOLEAN) : Attr;
+BEGIN
+    RETURN VAL(CARDINAL, fg) + VAL(CARDINAL, bg) * 16 +
+           VAL(CARDINAL, blink) * 256
+END A;
+
+PROCEDURE Idx(x, y : INTEGER) : INTEGER;
+BEGIN RETURN y * cols + x END Idx;
+
+PROCEDURE InClip(x, y : INTEGER) : BOOLEAN;
+BEGIN
+    IF (x < 0) OR (x >= cols) OR (y < 0) OR (y >= rows) THEN RETURN FALSE END;
+    IF clipN = 0 THEN RETURN TRUE END;
+    RETURN (x >= clipStack[clipN - 1].x) AND
+           (x < clipStack[clipN - 1].x + clipStack[clipN - 1].w) AND
+           (y >= clipStack[clipN - 1].y) AND
+           (y < clipStack[clipN - 1].y + clipStack[clipN - 1].h)
+END InClip;
+
+PROCEDURE PushClipRect(r : Rect);
+    VAR cur : Rect; i1, j1, i2, j2 : INTEGER;
+BEGIN
+    IF clipN = 0 THEN
+        cur.x := 0; cur.y := 0; cur.w := cols; cur.h := rows
+    ELSE
+        cur := clipStack[clipN - 1]
+    END;
+    i1 := r.x; IF i1 < cur.x THEN i1 := cur.x END;
+    j1 := r.y; IF j1 < cur.y THEN j1 := cur.y END;
+    i2 := r.x + r.w; IF i2 > cur.x + cur.w THEN i2 := cur.x + cur.w END;
+    j2 := r.y + r.h; IF j2 > cur.y + cur.h THEN j2 := cur.y + cur.h END;
+    IF i2 < i1 THEN i2 := i1 END;
+    IF j2 < j1 THEN j2 := j1 END;
+    IF clipN < 16 THEN
+        clipStack[clipN].x := i1; clipStack[clipN].y := j1;
+        clipStack[clipN].w := i2 - i1; clipStack[clipN].h := j2 - j1;
+        clipN := clipN + 1
+    END
+END PushClipRect;
+
+PROCEDURE PopClip;
+BEGIN
+    IF clipN > 0 THEN clipN := clipN - 1 END
+END PopClip;
+
+PROCEDURE DrawCh(x, y : INTEGER; cp : CARDINAL; attr : Attr);
+BEGIN
+    IF InClip(x, y) THEN
+        cells[Idx(x, y)].cp := cp;
+        cells[Idx(x, y)].attr := attr
+    END
+END DrawCh;
+
+PROCEDURE Fill(x, y, w, h : INTEGER; cp : CARDINAL; attr : Attr);
+    VAR i, j : INTEGER;
+BEGIN
+    FOR j := y TO y + h - 1 DO
+        FOR i := x TO x + w - 1 DO
+            DrawCh(i, j, cp, attr)
+        END
+    END
+END Fill;
+
+PROCEDURE Clear(attr : Attr);
+BEGIN
+    clipN := 0;
+    Fill(0, 0, cols, rows, VAL(CARDINAL, ORD(' ')), attr)
+END Clear;
+
+PROCEDURE DrawText(x, y : INTEGER; s : ARRAY OF CHAR; attr : Attr);
+    VAR i : INTEGER;
+BEGIN
+    i := 0;
+    WHILE (i <= VAL(INTEGER, HIGH(s))) AND (s[i] # 0C) DO
+        DrawCh(x + i, y, VAL(CARDINAL, ORD(s[i])), attr);
+        i := i + 1
+    END
+END DrawText;
+
+PROCEDURE DrawHLine(x, y, w : INTEGER; cp : CARDINAL; attr : Attr);
+    VAR i : INTEGER;
+BEGIN
+    FOR i := x TO x + w - 1 DO DrawCh(i, y, cp, attr) END
+END DrawHLine;
+
+PROCEDURE DrawVLine(x, y, h : INTEGER; cp : CARDINAL; attr : Attr);
+    VAR i : INTEGER;
+BEGIN
+    FOR i := y TO y + h - 1 DO DrawCh(x, i, cp, attr) END
+END DrawVLine;
+
+PROCEDURE DrawBox(r : Rect; style : INTEGER; attr : Attr);
+    VAR hz, vt, tl, tr, bl, br : CARDINAL;
+        x2, y2 : INTEGER;
+BEGIN
+    IF (r.w < 2) OR (r.h < 2) THEN RETURN END;
+    IF style = DoubleFrame THEN
+        hz := H_DOUBLE; vt := V_DOUBLE;
+        tl := TL_DOUBLE; tr := TR_DOUBLE; bl := BL_DOUBLE; br := BR_DOUBLE
+    ELSE
+        hz := H_SINGLE; vt := V_SINGLE;
+        tl := TL_SINGLE; tr := TR_SINGLE; bl := BL_SINGLE; br := BR_SINGLE
+    END;
+    x2 := r.x + r.w - 1;
+    y2 := r.y + r.h - 1;
+    DrawHLine(r.x + 1, r.y, r.w - 2, hz, attr);
+    DrawHLine(r.x + 1, y2, r.w - 2, hz, attr);
+    DrawVLine(r.x, r.y + 1, r.h - 2, vt, attr);
+    DrawVLine(x2, r.y + 1, r.h - 2, vt, attr);
+    DrawCh(r.x, r.y, tl, attr);
+    DrawCh(x2, r.y, tr, attr);
+    DrawCh(r.x, y2, bl, attr);
+    DrawCh(x2, y2, br, attr)
+END DrawBox;
+
+PROCEDURE DrawShadow(r : Rect; attr : Attr);
+BEGIN
+    Fill(r.x + 2, r.y + r.h, r.w - 1, 1, VAL(CARDINAL, ORD(' ')), attr);
+    Fill(r.x + r.w, r.y + 1, 1, r.h, VAL(CARDINAL, ORD(' ')), attr)
+END DrawShadow;
+
+PROCEDURE SetCursor(x, y : INTEGER);
+BEGIN cursorX := x; cursorY := y; cursorShown := TRUE END SetCursor;
+
+PROCEDURE HideCursor;
+BEGIN cursorShown := FALSE END HideCursor;
+
+PROCEDURE Present;
+    VAR x, y, i, outX, outY : INTEGER;
+        outAttr : CARDINAL;
+        c : Cell;
+BEGIN
+    outLen := 0;
+    OutCh(ESCc); OutStr("[?25l");
+    outX := -1; outY := -1; outAttr := MAX(CARDINAL);
+    FOR y := 0 TO rows - 1 DO
+        FOR x := 0 TO cols - 1 DO
+            i := Idx(x, y);
+            c := cells[i];
+            IF (NOT prevValid) OR (c.cp # prev[i].cp) OR (c.attr # prev[i].attr) THEN
+                IF (x # outX) OR (y # outY) THEN
+                    EmitMove(x, y);
+                    outX := x; outY := y
+                END;
+                IF c.attr # outAttr THEN
+                    EmitSGR(c.attr);
+                    outAttr := c.attr
+                END;
+                OutCP(c.cp);
+                outX := outX + 1
+            END;
+            prev[i] := c
+        END
+    END;
+    prevValid := TRUE;
+    OutCh(ESCc); OutStr("[0m");
+    IF cursorShown AND (cursorX >= 0) AND (cursorX < cols) AND
+       (cursorY >= 0) AND (cursorY < rows) THEN
+        OutCh(ESCc); OutStr("[?25h");
+        EmitMove(cursorX, cursorY)
+    ELSE
+        OutCh(ESCc); OutStr("[?25l")
+    END;
+    FlushOut
+END Present;
+
+(*========================================================================*)
+(*  Text helpers                                                           *)
+(*========================================================================*)
+
+PROCEDURE TextLen (s : ARRAY OF CHAR) : INTEGER;
+    VAR i : INTEGER;
+BEGIN
+    i := 0;
+    WHILE (i <= VAL(INTEGER, HIGH(s))) AND (s[i] # 0C) DO i := i + 1 END;
+    RETURN i
+END TextLen;
+
+PROCEDURE CopyText (VAR dest : ARRAY OF CHAR; s : ARRAY OF CHAR);
+    VAR i : INTEGER;
+BEGIN
+    i := 0;
+    WHILE (i <= VAL(INTEGER, HIGH(s))) AND (i < VAL(INTEGER, HIGH(dest))) AND (s[i] # 0C) DO
+        dest[i] := s[i];
+        i := i + 1
+    END;
+    dest[i] := 0C
+END CopyText;
+
+PROCEDURE AppendText (VAR dest : ARRAY OF CHAR; s : ARRAY OF CHAR);
+    VAR i, j : INTEGER;
+BEGIN
+    i := TextLen(dest);
+    j := 0;
+    WHILE (j <= VAL(INTEGER, HIGH(s))) AND (i < VAL(INTEGER, HIGH(dest))) AND (s[j] # 0C) DO
+        dest[i] := s[j];
+        i := i + 1;
+        j := j + 1
+    END;
+    dest[i] := 0C
+END AppendText;
+
+PROCEDURE PadText (VAR dest : ARRAY OF CHAR; s : ARRAY OF CHAR; width : INTEGER);
+    VAR i, n : INTEGER;
+BEGIN
+    n := TextLen(s);
+    IF n > width THEN n := width END;
+    FOR i := 0 TO width - 1 DO
+        IF i < n THEN dest[i] := s[i] ELSE dest[i] := ' ' END
+    END;
+    dest[width] := 0C
+END PadText;
+
+PROCEDURE IntToText (VAR dest : ARRAY OF CHAR; n : INTEGER; width : INTEGER);
+    VAR tmp : ARRAY [0..15] OF CHAR;
+        k, i, p, v : INTEGER;
+        neg : BOOLEAN;
+BEGIN
+    k := 0; neg := FALSE; v := n;
+    IF v < 0 THEN neg := TRUE; v := 0 - v END;
+    IF v = 0 THEN
+        tmp[0] := '0'; k := 1
+    ELSE
+        WHILE v > 0 DO
+            tmp[k] := CHR(ORD('0') + VAL(CARDINAL, v MOD 10));
+            k := k + 1;
+            v := v DIV 10
+        END
+    END;
+    p := 0;
+    IF neg THEN dest[p] := '-'; p := p + 1 END;
+    i := k;
+    WHILE i > 0 DO
+        i := i - 1;
+        dest[p] := tmp[i];
+        p := p + 1
+    END;
+    WHILE (width > 0) AND (p < width) DO
+        dest[p] := ' ';
+        p := p + 1
+    END;
+    dest[p] := 0C
+END IntToText;
+
+(*========================================================================*)
+(*  Input                                                                  *)
+(*========================================================================*)
+
+PROCEDURE FillIn;
+BEGIN
+    IF inPos >= inLen THEN
+        inLen := read(0, ADR(inBuf), 64);
+        IF inLen < 0 THEN inLen := 0 END;
+        inPos := 0
+    END
+END FillIn;
+
+PROCEDURE PeekByte(VAR ok : BOOLEAN) : CARDINAL;
+BEGIN
+    FillIn;
+    IF inPos >= inLen THEN ok := FALSE; RETURN 0 END;
+    ok := TRUE;
+    RETURN VAL(CARDINAL, inBuf[inPos])
+END PeekByte;
+
+PROCEDURE TakeByte;
+BEGIN
+    IF inPos < inLen THEN inPos := inPos + 1 END
+END TakeByte;
+
+PROCEDURE ReadEvent (VAR e : Event) : BOOLEAN;
+    VAR b, b2, c1, c2, c3, cp, num : CARDINAL;
+        ok : BOOLEAN;
+BEGIN
+    e.kind := evNone; e.key := kNone; e.ch := 0; e.ctrl := FALSE;
+    e.mpressed := FALSE; e.mreleased := FALSE; e.mwheel := 0;
+    FillIn;
+    IF inPos >= inLen THEN RETURN FALSE END;
+    b := PeekByte(ok); TakeByte;
+    IF b = 1BH THEN
+        b2 := PeekByte(ok);
+        IF NOT ok THEN e.kind := evKey; e.key := kEsc; RETURN TRUE END;
+        IF b2 = VAL(CARDINAL, ORD('[')) THEN
+            TakeByte;
+            b2 := PeekByte(ok);
+            IF NOT ok THEN e.kind := evKey; e.key := kEsc; RETURN TRUE END;
+            IF b2 = VAL(CARDINAL, ORD('<')) THEN
+                (* SGR mouse: ESC [ < b ; x ; y M/m *)
+                TakeByte;
+                num := 0;
+                b2 := PeekByte(ok);
+                WHILE ok AND (b2 >= VAL(CARDINAL, ORD('0'))) AND
+                      (b2 <= VAL(CARDINAL, ORD('9'))) DO
+                    num := num * 10 + (b2 - VAL(CARDINAL, ORD('0')));
+                    TakeByte; b2 := PeekByte(ok)
+                END;
+                IF ok THEN TakeByte END;   (* ; *)
+                e.mx := 0;
+                b2 := PeekByte(ok);
+                WHILE ok AND (b2 >= VAL(CARDINAL, ORD('0'))) AND
+                      (b2 <= VAL(CARDINAL, ORD('9'))) DO
+                    e.mx := e.mx * 10 + VAL(INTEGER, b2 - VAL(CARDINAL, ORD('0')));
+                    TakeByte; b2 := PeekByte(ok)
+                END;
+                IF ok THEN TakeByte END;   (* ; *)
+                e.my := 0;
+                b2 := PeekByte(ok);
+                WHILE ok AND (b2 >= VAL(CARDINAL, ORD('0'))) AND
+                      (b2 <= VAL(CARDINAL, ORD('9'))) DO
+                    e.my := e.my * 10 + VAL(INTEGER, b2 - VAL(CARDINAL, ORD('0')));
+                    TakeByte; b2 := PeekByte(ok)
+                END;
+                IF ok THEN
+                    IF b2 = VAL(CARDINAL, ORD('M')) THEN
+                        IF (num >= 64) AND (num <= 65) THEN
+                            e.mwheel := 1
+                        ELSIF (num >= 128) AND (num <= 129) THEN
+                            e.mwheel := -1
+                        ELSIF num >= 32 THEN
+                            e.mbtn := VAL(INTEGER, num - 32) + 1
+                        ELSE
+                            e.mbtn := VAL(INTEGER, num) + 1;
+                            e.mpressed := TRUE
+                        END
+                    ELSE
+                        e.mbtn := VAL(INTEGER, num) + 1;
+                        e.mreleased := TRUE
+                    END;
+                    TakeByte
+                END;
+                e.mx := e.mx - 1; e.my := e.my - 1;
+                e.kind := evMouse;
+                RETURN TRUE
+            ELSIF (b2 >= VAL(CARDINAL, ORD('0'))) AND (b2 <= VAL(CARDINAL, ORD('9'))) THEN
+                num := 0;
+                WHILE ok AND (b2 >= VAL(CARDINAL, ORD('0'))) AND
+                      (b2 <= VAL(CARDINAL, ORD('9'))) DO
+                    num := num * 10 + (b2 - VAL(CARDINAL, ORD('0')));
+                    TakeByte; b2 := PeekByte(ok)
+                END;
+                IF NOT ok THEN e.kind := evKey; e.key := kEsc; RETURN TRUE END;
+                WHILE ok AND (b2 = VAL(CARDINAL, ORD(';'))) DO
+                    TakeByte; b2 := PeekByte(ok);
+                    WHILE ok AND (b2 >= VAL(CARDINAL, ORD('0'))) AND
+                          (b2 <= VAL(CARDINAL, ORD('9'))) DO
+                        TakeByte; b2 := PeekByte(ok)
+                    END
+                END;
+                IF ok THEN TakeByte END;
+                e.kind := evKey;
+                IF b2 = VAL(CARDINAL, ORD('~')) THEN
+                    IF num = 1 THEN e.key := kHome
+                    ELSIF num = 2 THEN e.key := kIns
+                    ELSIF num = 3 THEN e.key := kDel
+                    ELSIF num = 4 THEN e.key := kEnd
+                    ELSIF num = 5 THEN e.key := kPgUp
+                    ELSIF num = 6 THEN e.key := kPgDn
+                    ELSIF num = 15 THEN e.key := kF5
+                    ELSIF num = 17 THEN e.key := kF6
+                    ELSIF num = 18 THEN e.key := kF7
+                    ELSIF num = 19 THEN e.key := kF8
+                    ELSIF num = 20 THEN e.key := kF9
+                    ELSIF num = 21 THEN e.key := kF10
+                    ELSIF num = 23 THEN e.key := kF11
+                    ELSIF num = 24 THEN e.key := kF12
+                    ELSE e.key := kNone
+                    END
+                ELSIF b2 = VAL(CARDINAL, ORD('A')) THEN e.key := kUp
+                ELSIF b2 = VAL(CARDINAL, ORD('B')) THEN e.key := kDown
+                ELSIF b2 = VAL(CARDINAL, ORD('C')) THEN e.key := kRight
+                ELSIF b2 = VAL(CARDINAL, ORD('D')) THEN e.key := kLeft
+                ELSIF b2 = VAL(CARDINAL, ORD('H')) THEN e.key := kHome
+                ELSIF b2 = VAL(CARDINAL, ORD('F')) THEN e.key := kEnd
+                ELSIF b2 = VAL(CARDINAL, ORD('Z')) THEN e.key := kTab; e.ctrl := TRUE
+                ELSIF b2 = VAL(CARDINAL, ORD('P')) THEN e.key := kF1
+                ELSIF b2 = VAL(CARDINAL, ORD('Q')) THEN e.key := kF2
+                ELSIF b2 = VAL(CARDINAL, ORD('R')) THEN e.key := kF3
+                ELSIF b2 = VAL(CARDINAL, ORD('S')) THEN e.key := kF4
+                ELSE e.key := kNone
+                END;
+                RETURN TRUE
+            ELSE
+                TakeByte;
+                e.kind := evKey;
+                IF b2 = VAL(CARDINAL, ORD('A')) THEN e.key := kUp
+                ELSIF b2 = VAL(CARDINAL, ORD('B')) THEN e.key := kDown
+                ELSIF b2 = VAL(CARDINAL, ORD('C')) THEN e.key := kRight
+                ELSIF b2 = VAL(CARDINAL, ORD('D')) THEN e.key := kLeft
+                ELSIF b2 = VAL(CARDINAL, ORD('H')) THEN e.key := kHome
+                ELSIF b2 = VAL(CARDINAL, ORD('F')) THEN e.key := kEnd
+                ELSIF b2 = VAL(CARDINAL, ORD('Z')) THEN e.key := kTab; e.ctrl := TRUE
+                ELSE e.key := kNone
+                END;
+                RETURN TRUE
+            END
+        ELSIF b2 = VAL(CARDINAL, ORD('O')) THEN
+            TakeByte;
+            b2 := PeekByte(ok);
+            IF ok THEN TakeByte END;
+            e.kind := evKey;
+            IF b2 = VAL(CARDINAL, ORD('P')) THEN e.key := kF1
+            ELSIF b2 = VAL(CARDINAL, ORD('Q')) THEN e.key := kF2
+            ELSIF b2 = VAL(CARDINAL, ORD('R')) THEN e.key := kF3
+            ELSIF b2 = VAL(CARDINAL, ORD('S')) THEN e.key := kF4
+            ELSIF b2 = VAL(CARDINAL, ORD('A')) THEN e.key := kUp
+            ELSIF b2 = VAL(CARDINAL, ORD('B')) THEN e.key := kDown
+            ELSIF b2 = VAL(CARDINAL, ORD('C')) THEN e.key := kRight
+            ELSIF b2 = VAL(CARDINAL, ORD('D')) THEN e.key := kLeft
+            ELSIF b2 = VAL(CARDINAL, ORD('H')) THEN e.key := kHome
+            ELSIF b2 = VAL(CARDINAL, ORD('F')) THEN e.key := kEnd
+            END;
+            RETURN TRUE
+        ELSE
+            e.kind := evKey; e.key := kEsc;
+            RETURN TRUE
+        END
+    ELSIF (b = 0DH) OR (b = 0AH) THEN e.kind := evKey; e.key := kEnter; RETURN TRUE
+    ELSIF b = 09H THEN e.kind := evKey; e.key := kTab; RETURN TRUE
+    ELSIF b = 20H THEN e.kind := evKey; e.key := kSpace; RETURN TRUE
+    ELSIF (b = 7FH) OR (b = 08H) THEN e.kind := evKey; e.key := kBack; RETURN TRUE
+    ELSIF b = 03H THEN e.kind := evKey; e.key := kCtrlC; RETURN TRUE
+    ELSIF b < 20H THEN e.kind := evKey; e.key := kNone; RETURN TRUE
+    ELSIF b < 80H THEN e.kind := evKey; e.key := kChar; e.ch := b; RETURN TRUE
+    ELSE
+        IF b < 0E0H THEN
+            c1 := PeekByte(ok); IF ok THEN TakeByte ELSE c1 := 80H END;
+            cp := (b - 0C0H) * 64 + (c1 - 80H)
+        ELSIF b < 0F0H THEN
+            c1 := PeekByte(ok); IF ok THEN TakeByte ELSE c1 := 80H END;
+            c2 := PeekByte(ok); IF ok THEN TakeByte ELSE c2 := 80H END;
+            cp := (b - 0E0H) * 4096 + (c1 - 80H) * 64 + (c2 - 80H)
+        ELSE
+            c1 := PeekByte(ok); IF ok THEN TakeByte ELSE c1 := 80H END;
+            c2 := PeekByte(ok); IF ok THEN TakeByte ELSE c2 := 80H END;
+            c3 := PeekByte(ok); IF ok THEN TakeByte ELSE c3 := 80H END;
+            cp := (b - 0F0H) * 262144 + (c1 - 80H) * 4096 +
+                  (c2 - 80H) * 64 + (c3 - 80H)
+        END;
+        e.kind := evKey; e.key := kChar; e.ch := cp;
+        RETURN TRUE
+    END
+END ReadEvent;
+
+(*========================================================================*)
+(*  View tree                                                              *)
+(*========================================================================*)
+
+PROCEDURE NewView (k : VKind; x, y, w, h : INTEGER) : ViewPtr;
+    VAR i : INTEGER;
+BEGIN
+    i := 0;
+    WHILE (i < MAXVIEWS) AND used[i] DO i := i + 1 END;
+    IF i >= MAXVIEWS THEN RETURN NIL END;
+    used[i] := TRUE;
+    pool[i].kind := k;
+    pool[i].rect.x := x; pool[i].rect.y := y;
+    pool[i].rect.w := w; pool[i].rect.h := h;
+    pool[i].parent := NIL; pool[i].next := NIL; pool[i].prev := NIL;
+    pool[i].first := NIL; pool[i].last := NIL; pool[i].focus := NIL;
+    pool[i].clipKids := FALSE;
+    pool[i].text[0] := 0C; pool[i].len := 0; pool[i].pos := 0;
+    pool[i].value := 0; pool[i].minv := 0; pool[i].maxv := 0; pool[i].tag := 0;
+    pool[i].flags := 0; pool[i].list := NIL;
+    RETURN ADR(pool[i])
+END NewView;
+
+PROCEDURE FreeView (v : ViewPtr);
+    VAR i : INTEGER;
+BEGIN
+    IF v = NIL THEN RETURN END;
+    i := 0;
+    WHILE (i < MAXVIEWS) AND (ADR(pool[i]) # v) DO i := i + 1 END;
+    IF i < MAXVIEWS THEN used[i] := FALSE END
+END FreeView;
+
+PROCEDURE AddView (parent, child : ViewPtr);
+BEGIN
+    IF (parent = NIL) OR (child = NIL) THEN RETURN END;
+    child^.parent := parent;
+    child^.next := NIL;
+    child^.prev := parent^.last;
+    IF parent^.last # NIL THEN parent^.last^.next := child END;
+    parent^.last := child;
+    IF parent^.first = NIL THEN parent^.first := child END;
+    IF parent^.kind = VWindow THEN child^.clipKids := FALSE END
+END AddView;
+
+PROCEDURE DelView (v : ViewPtr);
+    VAR c, n : ViewPtr;
+BEGIN
+    IF v = NIL THEN RETURN END;
+    c := v^.first;
+    WHILE c # NIL DO
+        n := c^.next;
+        DelView(c);
+        c := n
+    END;
+    IF v^.prev # NIL THEN v^.prev^.next := v^.next END;
+    IF v^.next # NIL THEN v^.next^.prev := v^.prev END;
+    IF v^.parent # NIL THEN
+        IF v^.parent^.first = v THEN v^.parent^.first := v^.next END;
+        IF v^.parent^.last = v THEN v^.parent^.last := v^.prev END
+    END;
+    FreeView(v)
+END DelView;
+
+PROCEDURE SetText (v : ViewPtr; s : ARRAY OF CHAR);
+BEGIN
+    IF v # NIL THEN CopyText(v^.text, s); v^.len := TextLen(v^.text) END
+END SetText;
+
+PROCEDURE SetTag (v : ViewPtr; t : INTEGER);
+BEGIN
+    IF v # NIL THEN v^.tag := t END
+END SetTag;
+
+PROCEDURE Desk () : ViewPtr;
+BEGIN RETURN desk END Desk;
+
+PROCEDURE BringToFront (v : ViewPtr);
+    VAR p : ViewPtr;
+BEGIN
+    IF (v = NIL) OR (v^.parent = NIL) THEN RETURN END;
+    IF v^.parent^.last = v THEN RETURN END;
+    p := v^.parent;
+    (* unlink *)
+    IF v^.prev # NIL THEN v^.prev^.next := v^.next END;
+    IF v^.next # NIL THEN v^.next^.prev := v^.prev END;
+    IF p^.first = v THEN p^.first := v^.next END;
+    (* append *)
+    v^.prev := p^.last; v^.next := NIL;
+    IF p^.last # NIL THEN p^.last^.next := v END;
+    p^.last := v
+END BringToFront;
+
+PROCEDURE FocusView (v : ViewPtr);
+    VAR p : ViewPtr;
+BEGIN
+    p := v;
+    WHILE (p # NIL) AND (p^.parent # NIL) DO
+        p^.parent^.focus := p;
+        p := p^.parent
+    END
+END FocusView;
+
+PROCEDURE IsFocusable(v : ViewPtr) : BOOLEAN;
+BEGIN
+    RETURN (v # NIL) AND ((v^.flags DIV vfDisabled) MOD 2 = 0) AND
+           ((v^.kind = VInput) OR (v^.kind = VButton) OR (v^.kind = VCheck) OR
+            (v^.kind = VRadio) OR (v^.kind = VList) OR (v^.kind = VScroll))
+END IsFocusable;
+
+PROCEDURE CollectFocus(v : ViewPtr);
+    VAR c : ViewPtr;
+BEGIN
+    IF v = NIL THEN RETURN END;
+    IF IsFocusable(v) THEN
+        IF focusCount < MAXVIEWS THEN
+            focusList[focusCount] := v;
+            focusCount := focusCount + 1
+        END
+    END;
+    c := v^.first;
+    WHILE c # NIL DO
+        CollectFocus(c);
+        c := c^.next
+    END
+END CollectFocus;
+
+PROCEDURE CurrentFocus() : ViewPtr;
+    VAR v : ViewPtr;
+BEGIN
+    v := desk;
+    WHILE (v # NIL) AND (v^.focus # NIL) DO v := v^.focus END;
+    RETURN v
+END CurrentFocus;
+
+PROCEDURE FocusNext (backwards : BOOLEAN);
+    VAR cur, nxt : ViewPtr; i, idx : INTEGER;
+BEGIN
+    focusCount := 0;
+    CollectFocus(desk);
+    IF focusCount = 0 THEN RETURN END;
+    cur := CurrentFocus();
+    idx := -1;
+    FOR i := 0 TO focusCount - 1 DO
+        IF focusList[i] = cur THEN idx := i END
+    END;
+    IF idx < 0 THEN
+        nxt := focusList[0]
+    ELSIF backwards THEN
+        nxt := focusList[(idx + focusCount - 1) MOD focusCount]
+    ELSE
+        nxt := focusList[(idx + 1) MOD focusCount]
+    END;
+    FocusView(nxt)
+END FocusNext;
+
+PROCEDURE MoveViewBy (v : ViewPtr; dx, dy : INTEGER);
+    VAR c : ViewPtr;
+BEGIN
+    IF v = NIL THEN RETURN END;
+    v^.rect.x := v^.rect.x + dx;
+    v^.rect.y := v^.rect.y + dy;
+    c := v^.first;
+    WHILE c # NIL DO
+        MoveViewBy(c, dx, dy);
+        c := c^.next
+    END
+END MoveViewBy;
+
+PROCEDURE OffsetView(v : ViewPtr; dx, dy : INTEGER);
+BEGIN MoveViewBy(v, dx, dy) END OffsetView;
+
+(*========================================================================*)
+(*  Widget drawing                                                         *)
+(*========================================================================*)
+
+PROCEDURE IsFocused(v : ViewPtr) : BOOLEAN;
+BEGIN RETURN CurrentFocus() = v END IsFocused;
+
+PROCEDURE DrawWindow(v : ViewPtr);
+    VAR frame, title, shadow : Attr;
+        x2 : INTEGER;
+BEGIN
+    shadow := A(Black, Black, FALSE);
+    frame  := A(White, Blue, FALSE);
+    title  := A(Yellow, Blue, FALSE);
+    DrawShadow(v^.rect, shadow);
+    Fill(v^.rect.x, v^.rect.y, v^.rect.w, v^.rect.h,
+         VAL(CARDINAL, ORD(' ')), A(Black, Cyan, FALSE));
+    DrawBox(v^.rect, DoubleFrame, frame);
+    DrawText(v^.rect.x + 2, v^.rect.y, v^.text, title);
+    (* close box *)
+    x2 := v^.rect.x + v^.rect.w - 3;
+    DrawText(x2, v^.rect.y, " X ", A(White, Red, FALSE))
+END DrawWindow;
+
+PROCEDURE DrawInput(v : ViewPtr);
+    VAR bg, fg : Attr; s : ARRAY [0..MAXLINE - 1] OF CHAR;
+BEGIN
+    bg := A(Black, White, FALSE);
+    Fill(v^.rect.x, v^.rect.y, v^.rect.w, v^.rect.h,
+         VAL(CARDINAL, ORD(' ')), bg);
+    DrawText(v^.rect.x, v^.rect.y, v^.text, bg);
+    IF IsFocused(v) THEN
+        SetCursor(v^.rect.x + v^.pos, v^.rect.y)
+    END
+END DrawInput;
+
+PROCEDURE DrawButton(v : ViewPtr);
+    VAR s : ARRAY [0..MAXLINE - 1] OF CHAR;
+        attr : Attr; w : INTEGER;
+BEGIN
+    CopyText(s, "[ ");
+    AppendText(s, v^.text);
+    AppendText(s, " ]");
+    w := TextLen(s);
+    IF IsFocused(v) THEN
+        attr := A(White, Blue, FALSE)
+    ELSE
+        attr := A(Black, LightGray, FALSE)
+    END;
+    IF (v^.flags DIV vfPressed) MOD 2 = 1 THEN
+        attr := A(White, Green, FALSE)
+    END;
+    Fill(v^.rect.x, v^.rect.y, w, 1, VAL(CARDINAL, ORD(' ')),
+         A(Black, Cyan, FALSE));
+    DrawText(v^.rect.x, v^.rect.y, s, attr)
+END DrawButton;
+
+PROCEDURE DrawCheck(v : ViewPtr);
+    VAR s : ARRAY [0..1] OF CHAR; attr : Attr;
+BEGIN
+    IF v^.flags DIV vfSelected MOD 2 = 1 THEN s := "X" ELSE s := " " END;
+    attr := A(Black, Cyan, FALSE);
+    DrawText(v^.rect.x, v^.rect.y, "[", attr);
+    DrawText(v^.rect.x + 1, v^.rect.y, s, A(White, Blue, FALSE));
+    DrawText(v^.rect.x + 2, v^.rect.y, "]", attr);
+    DrawText(v^.rect.x + 4, v^.rect.y, v^.text, attr)
+END DrawCheck;
+
+PROCEDURE DrawRadio(v : ViewPtr);
+    VAR m : ARRAY [0..1] OF CHAR; attr : Attr;
+BEGIN
+    IF v^.flags DIV vfSelected MOD 2 = 1 THEN m := "*" ELSE m := " " END;
+    attr := A(Black, Cyan, FALSE);
+    DrawText(v^.rect.x, v^.rect.y, "(", attr);
+    DrawText(v^.rect.x + 1, v^.rect.y, m, A(White, Blue, FALSE));
+    DrawText(v^.rect.x + 2, v^.rect.y, ")", attr);
+    DrawText(v^.rect.x + 4, v^.rect.y, v^.text, attr)
+END DrawRadio;
+
+PROCEDURE DrawList(v : ViewPtr);
+    VAR i, first, vis, y : INTEGER; attr, sel : Attr;
+BEGIN
+    IF v^.list = NIL THEN RETURN END;
+    DrawBox(v^.rect, SingleFrame, A(White, Cyan, FALSE));
+    vis := v^.rect.h - 2;
+    first := v^.pos;
+    FOR i := 0 TO vis - 1 DO
+        y := v^.rect.y + 1 + i;
+        IF (first + i) < v^.list^.count THEN
+            IF (v^.list # NIL) AND (first + i = v^.value) THEN
+                attr := A(White, Blue, FALSE)
+            ELSE
+                attr := A(Black, Cyan, FALSE)
+            END;
+            Fill(v^.rect.x + 1, y, v^.rect.w - 2, 1,
+                 VAL(CARDINAL, ORD(' ')), attr);
+            DrawText(v^.rect.x + 1, y, v^.list^.items[first + i], attr)
+        END
+    END;
+    IF v^.list # NIL THEN
+        IF v^.list^.count > vis THEN
+            DrawText(v^.rect.x + v^.rect.w - 2, v^.rect.y + 1, "^",
+                     A(White, Cyan, FALSE));
+            DrawText(v^.rect.x + v^.rect.w - 2, v^.rect.y + v^.rect.h - 2, "v",
+                     A(White, Cyan, FALSE))
+        END
+    END
+END DrawList;
+
+PROCEDURE DrawScroll(v : ViewPtr);
+    VAR h, t, yy : INTEGER; attr : Attr;
+BEGIN
+    attr := A(Black, LightGray, FALSE);
+    Fill(v^.rect.x, v^.rect.y, v^.rect.w, v^.rect.h,
+         VAL(CARDINAL, ORD(' ')), attr);
+    IF v^.maxv > 0 THEN
+        h := v^.rect.h;
+        t := h DIV (v^.maxv + 1);
+        IF t < 1 THEN t := 1 END;
+        yy := v^.rect.y + (v^.value * (h - t)) DIV v^.maxv;
+        Fill(v^.rect.x, yy, v^.rect.w, t, VAL(CARDINAL, ORD(' ')),
+             A(White, Blue, FALSE))
+    END
+END DrawScroll;
+
+PROCEDURE DrawTextCtl(v : ViewPtr);
+BEGIN
+    Fill(v^.rect.x, v^.rect.y, v^.rect.w, 1,
+         VAL(CARDINAL, ORD(' ')), A(Black, Cyan, FALSE));
+    DrawText(v^.rect.x, v^.rect.y, v^.text, A(Black, Cyan, FALSE))
+END DrawTextCtl;
+
+PROCEDURE DrawView(v : ViewPtr);
+    VAR c : ViewPtr;
+BEGIN
+    IF v = NIL THEN RETURN END;
+    CASE v^.kind OF
+        VGroup : (* nothing *)
+        |
+        VWindow : DrawWindow(v)
+        |
+        VText : DrawTextCtl(v)
+        |
+        VFrame : DrawBox(v^.rect, SingleFrame, A(White, Cyan, FALSE))
+        |
+        VInput : DrawInput(v)
+        |
+        VButton : DrawButton(v)
+        |
+        VCheck : DrawCheck(v)
+        |
+        VRadio : DrawRadio(v)
+        |
+        VList : DrawList(v)
+        |
+        VScroll : DrawScroll(v)
+    END;
+    IF v^.first # NIL THEN
+        IF (v^.kind = VWindow) OR v^.clipKids THEN
+            PushClipRect(v^.rect)
+        END;
+        c := v^.first;
+        WHILE c # NIL DO
+            DrawView(c);
+            c := c^.next
+        END;
+        IF (v^.kind = VWindow) OR v^.clipKids THEN PopClip END
+    END
+END DrawView;
+
+PROCEDURE DrawTree;
+BEGIN
+    Clear(A(LightGray, Blue, FALSE));
+    HideCursor;
+    clipN := 0;
+    IF desk # NIL THEN DrawView(desk) END
+END DrawTree;
+
+PROCEDURE Finish;
+BEGIN
+    DrawTree;
+    Present
+END Finish;
+
+(*========================================================================*)
+(*  Geometry + hit testing                                                 *)
+(*========================================================================*)
+
+PROCEDURE Contains(r : Rect; x, y : INTEGER) : BOOLEAN;
+BEGIN
+    RETURN (x >= r.x) AND (x < r.x + r.w) AND (y >= r.y) AND (y < r.y + r.h)
+END Contains;
+
+PROCEDURE HitTest(v : ViewPtr; x, y : INTEGER) : ViewPtr;
+    VAR c, hit : ViewPtr;
+BEGIN
+    IF (v = NIL) OR (NOT Contains(v^.rect, x, y)) THEN RETURN NIL END;
+    c := v^.last;
+    WHILE c # NIL DO
+        IF Contains(c^.rect, x, y) THEN
+            hit := HitTest(c, x, y);
+            IF hit # NIL THEN RETURN hit END
+        END;
+        c := c^.prev
+    END;
+    RETURN v
+END HitTest;
+
+PROCEDURE WindowAncestor(v : ViewPtr) : ViewPtr;
+    VAR p : ViewPtr;
+BEGIN
+    p := v;
+    WHILE p # NIL DO
+        IF p^.kind = VWindow THEN RETURN p END;
+        p := p^.parent
+    END;
+    RETURN NIL
+END WindowAncestor;
+
+(*========================================================================*)
+(*  Event dispatch                                                         *)
+(*========================================================================*)
+
+PROCEDURE EditKey(v : ViewPtr; VAR e : Event);
+    VAR i : INTEGER;
+BEGIN
+    IF e.key = kLeft THEN
+        IF v^.pos > 0 THEN v^.pos := v^.pos - 1 END
+    ELSIF e.key = kRight THEN
+        IF v^.pos < v^.len THEN v^.pos := v^.pos + 1 END
+    ELSIF e.key = kHome THEN v^.pos := 0
+    ELSIF e.key = kEnd THEN v^.pos := v^.len
+    ELSIF e.key = kBack THEN
+        IF v^.pos > 0 THEN
+            i := v^.pos - 1;
+            WHILE i < v^.len - 1 DO v^.text[i] := v^.text[i + 1]; i := i + 1 END;
+            v^.len := v^.len - 1; v^.pos := v^.pos - 1; v^.text[v^.len] := 0C
+        END
+    ELSIF e.key = kDel THEN
+        IF v^.pos < v^.len THEN
+            i := v^.pos;
+            WHILE i < v^.len - 1 DO v^.text[i] := v^.text[i + 1]; i := i + 1 END;
+            v^.len := v^.len - 1; v^.text[v^.len] := 0C
+        END
+    ELSIF e.key = kChar THEN
+        IF (v^.len < MAXLINE - 1) AND (e.ch < 256) THEN
+            i := v^.len;
+            WHILE i > v^.pos DO v^.text[i] := v^.text[i - 1]; i := i - 1 END;
+            v^.text[v^.pos] := CHR(e.ch);
+            v^.len := v^.len + 1; v^.pos := v^.pos + 1
+        END
+    END
+END EditKey;
+
+PROCEDURE WidgetKey(v : ViewPtr; VAR e : Event) : INTEGER;
+    VAR cmd : INTEGER;
+BEGIN
+    cmd := cmNone;
+    IF v^.kind = VInput THEN
+        EditKey(v, e)
+    ELSIF v^.kind = VButton THEN
+        IF (e.key = kEnter) OR (e.key = kSpace) THEN cmd := v^.tag END
+    ELSIF v^.kind = VCheck THEN
+        IF (e.key = kEnter) OR (e.key = kSpace) THEN
+            v^.flags := v^.flags / 4 * 4;
+            IF v^.flags DIV vfSelected MOD 2 = 0 THEN
+                v^.flags := v^.flags + vfSelected
+            END;
+            cmd := v^.tag
+        END
+    ELSIF v^.kind = VRadio THEN
+        IF (e.key = kEnter) OR (e.key = kSpace) THEN cmd := v^.tag END
+    ELSIF v^.kind = VList THEN
+        IF e.key = kUp THEN
+            IF v^.value > 0 THEN v^.value := v^.value - 1 END;
+            IF v^.value < v^.pos THEN v^.pos := v^.value END
+        ELSIF e.key = kDown THEN
+            IF (v^.list # NIL) AND (v^.value < v^.list^.count - 1) THEN
+                v^.value := v^.value + 1
+            END;
+            IF v^.value >= v^.pos + (v^.rect.h - 2) THEN
+                v^.pos := v^.value - (v^.rect.h - 2) + 1
+            END
+        ELSIF (e.key = kEnter) OR (e.key = kSpace) THEN
+            cmd := v^.tag
+        END
+    ELSIF v^.kind = VScroll THEN
+        IF e.key = kUp THEN
+            IF v^.value > 0 THEN v^.value := v^.value - 1 END
+        ELSIF e.key = kDown THEN
+            IF v^.value < v^.maxv THEN v^.value := v^.value + 1 END
+        END
+    END;
+    IF cmd # cmNone THEN lastSender := v END;
+    RETURN cmd
+END WidgetKey;
+
+PROCEDURE WidgetMouse(v : ViewPtr; VAR e : Event) : INTEGER;
+    VAR cmd : INTEGER; rel : INTEGER;
+BEGIN
+    cmd := cmNone;
+    IF v^.kind = VButton THEN
+        IF e.mpressed THEN
+            v^.flags := v^.flags + vfPressed
+        ELSIF e.mreleased THEN
+            IF (v^.flags DIV vfPressed) MOD 2 = 1 THEN
+                cmd := v^.tag
+            END;
+            v^.flags := v^.flags / 8 * 8
+        END
+    ELSIF v^.kind = VCheck THEN
+        IF e.mreleased THEN
+            IF v^.flags DIV vfSelected MOD 2 = 1 THEN
+                v^.flags := v^.flags - vfSelected
+            ELSE
+                v^.flags := v^.flags + vfSelected
+            END;
+            cmd := v^.tag
+        END
+    ELSIF v^.kind = VRadio THEN
+        IF e.mreleased THEN cmd := v^.tag END
+    ELSIF v^.kind = VList THEN
+        IF e.mwheel # 0 THEN
+            IF (e.mwheel < 0) AND (v^.pos > 0) THEN v^.pos := v^.pos - 1 END;
+            IF (e.mwheel > 0) AND (v^.list # NIL) AND
+               (v^.pos + (v^.rect.h - 2) < v^.list^.count) THEN
+                v^.pos := v^.pos + 1
+            END
+        ELSIF e.mreleased THEN
+            rel := e.my - v^.rect.y - 1;
+            IF (rel >= 0) AND (v^.list # NIL) AND
+               (v^.pos + rel < v^.list^.count) THEN
+                v^.value := v^.pos + rel
+            END
+        END
+    ELSIF v^.kind = VInput THEN
+        IF e.mpressed THEN
+            rel := e.mx - v^.rect.x;
+            IF rel < 0 THEN rel := 0 END;
+            IF rel > v^.len THEN rel := v^.len END;
+            v^.pos := rel
+        END
+    ELSIF v^.kind = VScroll THEN
+        IF e.mpressed OR e.mreleased THEN
+            IF v^.rect.h > 1 THEN
+                v^.value := ((e.my - v^.rect.y) * v^.maxv) DIV (v^.rect.h - 1)
+            END;
+            IF v^.value < 0 THEN v^.value := 0 END;
+            IF v^.value > v^.maxv THEN v^.value := v^.maxv END
+        END
+    END;
+    IF cmd # cmNone THEN lastSender := v END;
+    RETURN cmd
+END WidgetMouse;
+
+PROCEDURE HandleWindow(v : ViewPtr; VAR e : Event) : INTEGER;
+    VAR w : ViewPtr; nx, ny : INTEGER;
+BEGIN
+    IF (e.kind = evMouse) AND e.mpressed THEN
+        (* close box? *)
+        IF (e.my = v^.rect.y) AND
+           (e.mx >= v^.rect.x + v^.rect.w - 3) AND (e.mx < v^.rect.x + v^.rect.w) THEN
+            lastSender := v;
+            RETURN cmClose
+        END;
+        (* drag by the title row *)
+        IF (e.my = v^.rect.y) AND (e.mx < v^.rect.x + v^.rect.w - 3) THEN
+            dragWin := v;
+            dragOX := e.mx - v^.rect.x;
+            dragOY := e.my - v^.rect.y
+        END
+    ELSIF (e.kind = evMouse) AND e.mreleased THEN
+        dragWin := NIL
+    ELSIF (e.kind = evMouse) AND (e.mbtn > 0) AND (NOT e.mpressed) AND
+          (NOT e.mreleased) THEN
+        IF dragWin # NIL THEN
+            nx := e.mx - dragOX;
+            ny := e.my - dragOY;
+            (* keep the window's title corner on screen *)
+            IF nx < 0 THEN nx := 0 END;
+            IF nx > cols - 6 THEN nx := cols - 6 END;
+            IF ny < 0 THEN ny := 0 END;
+            IF ny > rows - 1 THEN ny := rows - 1 END;
+            MoveViewBy(dragWin, nx - dragWin^.rect.x, ny - dragWin^.rect.y)
+        END
+    END;
+    RETURN cmNone
+END HandleWindow;
+
+PROCEDURE DispatchMouse(v : ViewPtr; VAR e : Event) : INTEGER;
+    VAR hit : ViewPtr; cmd : INTEGER; w : ViewPtr;
+BEGIN
+    cmd := cmNone;
+    hit := HitTest(v, e.mx, e.my);
+    IF hit = NIL THEN RETURN cmNone END;
+    IF hit # v THEN
+        w := WindowAncestor(hit);
+        IF (w # NIL) AND e.mpressed THEN BringToFront(w) END
+    END;
+    IF e.mpressed AND IsFocusable(hit) THEN FocusView(hit) END;
+    IF hit^.kind = VWindow THEN
+        cmd := HandleWindow(hit, e)
+    ELSE
+        cmd := WidgetMouse(hit, e)
+    END;
+    RETURN cmd
+END DispatchMouse;
+
+PROCEDURE DispatchKey(v : ViewPtr; VAR e : Event) : INTEGER;
+    VAR cmd : INTEGER; f : ViewPtr;
+BEGIN
+    cmd := cmNone;
+    IF (v^.kind = VGroup) OR (v^.kind = VWindow) THEN
+        f := v^.focus;
+        IF f # NIL THEN cmd := DispatchKey(f, e) END
+    ELSE
+        cmd := WidgetKey(v, e)
+    END;
+    RETURN cmd
+END DispatchKey;
+
+PROCEDURE HandleEvent (e : Event) : INTEGER;
+    VAR cmd : INTEGER; root : ViewPtr;
+BEGIN
+    IF e.kind = evNone THEN RETURN cmNone END;
+    root := modal;
+    IF root = NIL THEN root := desk END;
+    IF root = NIL THEN RETURN cmNone END;
+    IF e.kind = evKey THEN
+        IF e.key = kCtrlC THEN RETURN cmClose END;
+        IF e.key = kTab THEN
+            FocusNext(e.ctrl);
+            RETURN cmNone
+        END;
+        cmd := DispatchKey(root, e)
+    ELSE
+        (* while a window is being dragged, keep sending motion/release to it
+           even if the cursor has left the window (otherwise dragging up and
+           sideways stops as soon as the pointer leaves the frame) *)
+        IF dragWin # NIL THEN
+            cmd := HandleWindow(dragWin, e)
+        ELSE
+            cmd := DispatchMouse(root, e)
+        END
+    END;
+    RETURN cmd
+END HandleEvent;
+
+PROCEDURE Sender () : ViewPtr;
+BEGIN RETURN lastSender END Sender;
+
+(*========================================================================*)
+(*  Modal message box                                                      *)
+(*========================================================================*)
+
+PROCEDURE Message (title : ARRAY OF CHAR; text : ARRAY OF CHAR);
+    VAR w, t, b : ViewPtr; e : Event;
+        tw, ww, wx, wy : INTEGER; cmd : INTEGER; done : BOOLEAN;
+BEGIN
+    tw := TextLen(text);
+    ww := tw + 6;
+    IF ww < 24 THEN ww := 24 END;
+    IF ww > cols - 4 THEN ww := cols - 4 END;
+    wx := (cols - ww) DIV 2; wy := (rows - 7) DIV 2;
+    w := NewView(VWindow, wx, wy, ww, 7);
+    SetText(w, title);
+    t := NewView(VText, wx + 2, wy + 2, ww - 4, 1);
+    SetText(t, text);
+    AddView(w, t);
+    b := NewView(VButton, wx + (ww - 8) DIV 2, wy + 4, 8, 1);
+    SetText(b, "OK");
+    SetTag(b, cmOK);
+    AddView(w, b);
+    AddView(desk, w);
+    BringToFront(w);
+    FocusView(b);
+    modal := w;
+    done := FALSE;
+    WHILE NOT done DO
+        Finish;
+        cmd := cmNone;
+        IF ReadEvent(e) THEN
+            IF e.kind = evKey THEN
+                IF e.key = kEsc THEN cmd := cmOK END
+            END;
+            IF cmd = cmNone THEN cmd := HandleEvent(e) END
+        END;
+        IF (cmd = cmOK) OR (cmd = cmClose) THEN done := TRUE END
+    END;
+    modal := NIL;
+    DelView(w)
+END Message;
+
+END tv.

BIN
tvision-m2/tv.o


+ 19 - 0
tvision-m2/tvtty.def

@@ -0,0 +1,19 @@
+DEFINITION MODULE FOR "C" tvtty;
+
+(* Minimal libc binding used by the terminal backend.  This is the only
+   non-Modula-2 dependency of the TVision-style library: the ISO language
+   has no way to put a tty in raw mode or to query its size, so we call
+   tcgetattr/tcsetattr/ioctl/read/write directly.  Everything else is plain
+   GNU Modula-2. *)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED tcgetattr, tcsetattr, ioctl, read, write;
+
+PROCEDURE tcgetattr(fd : INTEGER; termios : ADDRESS) : INTEGER;
+PROCEDURE tcsetattr(fd, action : INTEGER; termios : ADDRESS) : INTEGER;
+PROCEDURE ioctl(fd : INTEGER; request : CARDINAL; arg : ADDRESS) : INTEGER;
+PROCEDURE read(fd : INTEGER; buf : ADDRESS; count : CARDINAL) : INTEGER;
+PROCEDURE write(fd : INTEGER; buf : ADDRESS; count : CARDINAL) : INTEGER;
+
+END tvtty.