summaryrefslogtreecommitdiff
path: root/appl/examples/minitel/miniterm.b
diff options
context:
space:
mode:
Diffstat (limited to 'appl/examples/minitel/miniterm.b')
-rw-r--r--appl/examples/minitel/miniterm.b1187
1 files changed, 1187 insertions, 0 deletions
diff --git a/appl/examples/minitel/miniterm.b b/appl/examples/minitel/miniterm.b
new file mode 100644
index 00000000..1c6ff759
--- /dev/null
+++ b/appl/examples/minitel/miniterm.b
@@ -0,0 +1,1187 @@
+#
+# Copyright © 1998 Vita Nuova Limited. All rights reserved.
+#
+
+implement Miniterm;
+
+include "sys.m";
+ sys: Sys;
+ print, fprint, sprint, read: import sys;
+include "draw.m";
+ draw: Draw;
+include "tk.m";
+ tk: Tk;
+include "tkclient.m";
+ tkclient: Tkclient;
+
+include "miniterm.m";
+
+Miniterm: module
+{
+ init: fn(ctxt: ref Draw->Context, argv: list of string);
+
+};
+
+pgrp: int = 0;
+debug: array of int = array[256] of {* => 0};
+stderr: ref Sys->FD;
+
+# Minitel terminal identification request - reply sequence
+TERMINALID1 := array [] of {
+ byte SOH,
+ byte 'S', byte 'X', byte '1', byte 'H', byte 'N',
+ byte EOT
+};
+TERMINALID2 := array [] of {
+ byte SOH,
+ byte 'C', byte 'g', byte '1',
+ byte EOT
+};
+
+# Minitel module identifiers
+Mscreen, Mmodem, Mkeyb, Msocket, Nmodule: con iota;
+Pscreen, Pmodem, Pkeyb, Psocket: con (1 << iota);
+Modname := array [Nmodule] of {
+ Mscreen => "S",
+ Mmodem => "M",
+ Mkeyb => "K",
+ Msocket => "C",
+ * => "?",
+};
+
+# attributes common to all modules
+Module: adt {
+ path: int; # bitset to connected modules
+ disabled: int;
+};
+
+# A BufChan queues events from the terminal to the modules
+BufChan: adt {
+ path: int; # id bit
+ ch: chan of ref Event; # set to `in' or `dummy' channel
+ ev: ref Event; # next event to send
+ in: chan of ref Event; # real channel for Events to the device
+ q: array of ref Event; # subsequent events to send
+};
+
+# holds state information for the minitel `protocol` (chapter 6)
+PState: adt {
+ state: int;
+ arg: array of int; # up to 3 arguments: X,Y,Z
+ nargs: int; # expected number of arguments
+ n: int; # progress
+ skip: int; # transparency; bytes to skip
+};
+PSstart, PSesc, PSarg: con iota; # states
+
+# Terminal display modes
+Videotex, Mixed, Ascii,
+
+# Connection methods
+Direct, Network,
+
+# Terminal connection states
+Local, Connecting, Online,
+
+# Special features
+Echo
+ : con (1 << iota);
+
+Terminal: adt {
+ in: chan of ref Event;
+ out: array of ref BufChan; # buffered output to the minitel modules
+
+ mode: int; # display mode
+ state: int; # connection state
+ spec: int; # special features
+ connect: int; # Direct, or Network
+ toplevel: ref Tk->Toplevel;
+ cmd: chan of string; # from Tk
+ proto: array of ref PState; # minitel protocol state
+ netaddr: string; # network address to dial
+ buttonsleft: int; # display buttons on the LHS (40 cols)
+ terminalid: array of byte; # ENQROM response
+ kbctl: chan of string; # softkeyboard control
+ kbmode: string; # softkeyboard mode
+
+ init: fn(t: self ref Terminal, toplevel: ref Tk->Toplevel, connect: int);
+ run: fn(t: self ref Terminal, done: chan of int);
+ reset: fn(t: self ref Terminal);
+ quit: fn(t: self ref Terminal);
+ layout: fn(t: self ref Terminal, cols: int);
+ setkbmode: fn(t: self ref Terminal, tmode: int);
+};
+
+include "arg.m";
+include "event.m";
+include "event.b";
+
+include "keyb.b";
+include "modem.b";
+include "socket.b";
+include "screen.b";
+
+K: ref Keyb;
+M: ref Modem;
+C: ref Socket;
+S: ref Screen;
+T: ref Terminal;
+Modules: array of ref Module;
+
+
+init(ctxt: ref Draw->Context, argv: list of string)
+{
+ s: string;
+ netaddr: string = nil;
+
+ sys = load Sys Sys->PATH;
+ tk = load Tk Tk->PATH;
+ tkclient = load Tkclient Tkclient->PATH;
+ tkclient->init();
+ draw = load Draw Draw->PATH;
+ stderr = sys->fildes(2);
+ pgrp = sys->pctl(Sys->NEWPGRP|Sys->FORKNS, nil);
+
+ arg := load Arg Arg->PATH;
+ arg->init(argv);
+ arg->setusage("miniterm [netaddr]");
+ while((c := arg->opt()) != 0){
+ case c {
+ 'D' =>
+ s = arg->earg();
+ for(i := 0; i < len s; i++){
+ c = s[i];
+ if(c < len debug)
+ debug[c] += 1;
+ }
+ * =>
+ arg->usage();
+ }
+ }
+ argv = arg->argv();
+ if(len argv > 0) {
+ netaddr = hd argv;
+ argv = tl argv;
+ }
+
+ if(argv != nil)
+ arg->usage();
+ arg = nil;
+
+ # usage: miniterm modem[!init[!number]]
+ # or miniterm tcp!a.b.c.d
+ connect: int;
+ initstr := dialstr := string nil;
+ if(netaddr == nil)
+ netaddr = "tcp!pdc.minitelfr.com!513"; # gateway
+ (nil, words) := sys->tokenize(netaddr, "!");
+ if(len words == 0) {
+ connect = Direct;
+ words = "modem" :: nil;
+ }
+ if(hd words == "modem") {
+ connect = Direct;
+ words = tl words;
+ if(words != nil) {
+ initstr = hd words;
+ words = tl words;
+ if(words != nil)
+ dialstr = hd words;
+ }
+ if(initstr == "*")
+ initstr = nil;
+ if(dialstr == "*")
+ dialstr = nil;
+ } else {
+ connect = Network;
+ dialstr = netaddr;
+ }
+
+ T = ref Terminal;
+ K = ref Keyb;
+ M = ref Modem;
+ C = ref Socket;
+ S = ref Screen;
+ Modules = array [Nmodule] of {
+ Mscreen => S.m,
+ Mmodem => M.m,
+ Mkeyb => K.m,
+ Msocket => C.m,
+ };
+
+ toplevel := tk->toplevel(ctxt.display, "");
+ inittk(toplevel, connect);
+
+ T.init(toplevel, connect);
+ K.init(toplevel);
+ M.init(connect, initstr, dialstr);
+ C.init();
+ case connect {
+ Direct =>
+ S.init(ctxt, Rect((0,0), (640,425)), Rect((0,0), (640,425)));
+ Network =>
+ S.init(ctxt, Rect((0,0), (596,440)), Rect((0,50), (640,350)));
+ }
+
+ done := chan of int;
+ spawn K.run();
+ spawn M.run();
+ spawn C.run();
+ spawn S.run();
+ spawn T.run(done);
+ <- done;
+
+ # now tidy up
+ K.quit();
+ M.quit();
+ C.quit();
+ S.quit();
+ T.quit();
+}
+
+# the keyboard module handles keypresses and focus
+BTN40x25: con "-height 24 -font {/fonts/lucidasans/unicode.6.font}";
+BTNCTL: con "-width 60 -height 20 -font {/fonts/lucidasans/unicode.7.font}";
+BTNMAIN: con "-width 80 -height 20 -font {/fonts/lucidasans/unicode.7.font}";
+
+tkinitbs := array[] of {
+ "button .cxfin -text {Cx/Fin} -command {send keyb skey Connect}",
+ "button .done -text {Quitter} -command {send keyb skey Exit}",
+ "button .hup -text {Raccr.} -command {send term hangup}",
+ "button .somm -text {Somm.} -command {send keyb skey Index}",
+ "button .guide -text {Guide} -command {send keyb skey Guide}",
+ "button .annul -text {Annul.} -command {send keyb skey Cancel}",
+ "button .corr -text {Corr.} -command {send keyb skey Correct}",
+ "button .retour -text {Retour} -command {send keyb skey Previous}",
+ "button .suite -text {Suite} -command {send keyb skey Next}",
+ "button .repet -text {Répét.} -command {send keyb skey Repeat}",
+ "button .envoi -text {Envoi} -command {send keyb skey Send}",
+ "button .play -text {P} -command {send term play}",
+# "button .db -text {D} -command {send term debug}" ,
+ "button .kb -text {Clavier} -command {send term keyboard}",
+ "button .move -text {<-} -command {send term buttonsleft} " + BTN40x25,
+};
+
+tkinitdirect := array [] of {
+ ". configure -background black -height 480 -width 640",
+
+ ".cxfin configure " + BTNCTL,
+ ".hup configure " + BTNCTL,
+ ".done configure " + BTNCTL,
+ ".somm configure " + BTNMAIN,
+ ".guide configure " + BTNMAIN,
+ ".annul configure " + BTNMAIN,
+ ".corr configure " + BTNMAIN,
+ ".retour configure " + BTNMAIN,
+ ".suite configure " + BTNMAIN,
+ ".repet configure " + BTNMAIN,
+ ".envoi configure " + BTNMAIN,
+# ".play configure " + BTNCTL,
+# ".db configure " + BTNCTL,
+ ".kb configure " + BTNCTL,
+
+ "canvas .c -height 425 -width 640 -background black",
+ "bind .c <Configure> {send term resize}",
+ "bind .c <Key> {send keyb key %K}",
+ "bind .c <FocusIn> {send keyb focusin}",
+ "bind .c <FocusOut> {send keyb focusout}",
+ "bind .c <ButtonRelease> {focus .c; send keyb click %x %y}",
+ "frame .k -height 55 -width 640 -background black",
+ "pack propagate .k no",
+ "frame .klhs -background black",
+ "frame .krhs -background black",
+ "frame .krows -background black",
+ "frame .k1 -background black",
+ "frame .k2 -background black",
+ "pack .cxfin -in .klhs -anchor w -pady 4",
+ "pack .hup -in .klhs -anchor w",
+ "pack .somm .annul .retour .repet -in .k1 -side left -padx 2",
+ "pack .guide .corr .suite .envoi -in .k2 -side left -padx 2",
+ "pack .kb -in .krhs -anchor e -pady 4",
+ "pack .done -in .krhs -anchor e",
+ "pack .k1 -in .krows -pady 4",
+ "pack .k2 -in .krows",
+ "pack .klhs .krows .krhs -in .k -side left -expand 1 -fill x",
+ "pack .c .k",
+ "focus .c",
+ "update",
+};
+
+tkinitip := array [] of {
+ ". configure -background black -height 440 -width 640",
+
+ # ip 40x25 mode support
+ "canvas .c40 -height 440 -width 596 -background black",
+ "bind .c40 <Configure> {send term resize}",
+ "bind .c40 <Key> {send keyb key %K}",
+ "bind .c40 <FocusIn> {send keyb focusin}",
+ "bind .c40 <FocusOut> {send keyb focusout}",
+ "bind .c40 <ButtonRelease> {focus .c40; send keyb click %x %y}",
+ "frame .k -height 427 -width 44 -background black",
+ "frame .gap1 -background black",
+ "frame .gap2 -background black",
+ "pack propagate .k no",
+
+ # ip 80x25 mode support
+ "frame .padtop -height 50",
+ "canvas .c80 -height 300 -width 640 -background black",
+ "bind .c80 <Configure> {send term resize}",
+ "bind .c80 <Key> {send keyb key %K}",
+ "bind .c80 <FocusIn> {send keyb focusin}",
+ "bind .c80 <FocusOut> {send keyb focusout}",
+ "bind .c80 <ButtonRelease> {focus .c80; send keyb click %x %y}",
+ "frame .k80 -height 90 -width 640 -background black",
+ "pack propagate .k80 no",
+ "frame .klhs -background black",
+ "frame .krows -background black",
+ "frame .krow1 -background black",
+ "frame .krow2 -background black",
+ "frame .krhs -background black",
+ "pack .krow1 .krow2 -in .krows -pady 2",
+ "pack .klhs -in .k80 -side left",
+ "pack .krows -in .k80 -side left -expand 1",
+ "pack .krhs -in .k80 -side left",
+};
+
+tkip40x25show := array [] of {
+ ".cxfin configure " + BTN40x25,
+ ".hup configure " + BTN40x25,
+ ".done configure " + BTN40x25,
+ ".somm configure " + BTN40x25,
+ ".guide configure " + BTN40x25,
+ ".annul configure " + BTN40x25,
+ ".corr configure " + BTN40x25,
+ ".retour configure " + BTN40x25,
+ ".suite configure " + BTN40x25,
+ ".repet configure " + BTN40x25,
+ ".envoi configure " + BTN40x25,
+ ".play configure " + BTN40x25,
+# ".db configure " + BTN40x25,
+ ".kb configure " + BTN40x25,
+ "pack .cxfin -in .k -side top -fill x",
+ "pack .gap1 -in .k -side top -expand 1",
+ "pack .guide .repet .somm .annul .corr .retour .suite .envoi -in .k -side top -fill x",
+ "pack .gap2 -in .k -side top -expand 1",
+ "pack .done .hup .kb .move -in .k -side bottom -pady 2 -fill x",
+# "pack .db -in .k -side bottom",
+};
+
+tkip40x25lhs := array [] of {
+ ".move configure -text {->} -command {send term buttonsright}",
+ "pack .k .c40 -side left",
+ "focus .c40",
+ "update",
+};
+
+tkip40x25rhs := array [] of {
+ ".move configure -text {<-} -command {send term buttonsleft}",
+ "pack .c40 .k -side left",
+ "focus .c40",
+ "update",
+};
+
+tkip40x25hide := array [] of {
+ "pack forget .k .c40",
+};
+
+tkip80x25show := array [] of {
+ ".cxfin configure " + BTNCTL,
+ ".hup configure " + BTNCTL,
+ ".done configure " + BTNCTL,
+ ".somm configure " + BTNMAIN,
+ ".guide configure " + BTNMAIN,
+ ".annul configure " + BTNMAIN,
+ ".corr configure " + BTNMAIN,
+ ".retour configure " + BTNMAIN,
+ ".suite configure " + BTNMAIN,
+ ".repet configure " + BTNMAIN,
+ ".envoi configure " + BTNMAIN,
+# ".play configure " + BTNCTL,
+# ".db configure " + BTNCTL,
+ ".kb configure " + BTNCTL,
+
+ "pack .cxfin .hup -in .klhs -anchor w -pady 2",
+ "pack .somm .annul .retour .repet -in .krow1 -side left -padx 2",
+ "pack .guide .corr .suite .envoi -in .krow2 -side left -padx 2",
+ "pack .done .kb -in .krhs -anchor e -pady 2",
+ "pack .padtop .c80 .k80 -side top",
+ "focus .c80",
+ "update",
+};
+
+tkip80x25hide := array [] of {
+ "pack forget .padtop .c80 .k80",
+};
+
+inittk(toplevel: ref Tk->Toplevel, connect: int)
+{
+ tkcmds(toplevel, tkinitbs);
+ if(connect == Direct)
+ tkcmds(toplevel, tkinitdirect);
+ else
+ tkcmds(toplevel, tkinitip);
+}
+
+Terminal.layout(t: self ref Terminal, cols: int)
+{
+ if(t.connect == Direct)
+ return;
+ if(cols == 80) {
+ tkcmds(t.toplevel, tkip40x25hide);
+ tkcmds(t.toplevel, tkip80x25show);
+ } else {
+ tkcmds(t.toplevel, tkip80x25hide);
+ tkcmds(t.toplevel, tkip40x25show);
+ if (t.buttonsleft)
+ tkcmds(t.toplevel, tkip40x25lhs);
+ else
+ tkcmds(t.toplevel, tkip40x25rhs);
+ }
+}
+
+Terminal.init(t: self ref Terminal, toplevel: ref Tk->Toplevel, connect: int)
+{
+ t.in = chan of ref Event;
+ t.proto = array [Nmodule] of {
+ Mscreen => ref PState(PSstart, array [] of {0,0,0}, 0, 0, 0),
+ Mmodem => ref PState(PSstart, array [] of {0,0,0}, 0, 0, 0),
+ Mkeyb => ref PState(PSstart, array [] of {0,0,0}, 0, 0, 0),
+ Msocket => ref PState(PSstart, array [] of {0,0,0}, 0, 0, 0),
+ };
+
+ t.toplevel = toplevel;
+ t.connect = connect;
+ if (t.connect == Direct)
+ t.spec = 0;
+ else
+ t.spec = Echo;
+ t.cmd = chan of string;
+ tk->namechan(t.toplevel, t.cmd, "term"); # Tk -> terminal
+ t.state = Local;
+ t.buttonsleft = 0;
+ t.kbctl = nil;
+ t.kbmode = "minitel";
+ t.reset();
+}
+
+Terminal.reset(t: self ref Terminal)
+{
+ t.mode = Videotex;
+}
+
+Terminal.run(t: self ref Terminal, done: chan of int)
+{
+ t.out = array [Nmodule] of {
+ Mscreen => ref BufChan(Pscreen, nil, nil, S.in, array [0] of ref Event),
+ Mmodem => ref BufChan(Pmodem, nil, nil, M.in, array [0] of ref Event),
+ Mkeyb => ref BufChan(Pkeyb, nil, nil, K.in, array [0] of ref Event),
+ Msocket => ref BufChan(Psocket, nil, nil, C.in, array [0] of ref Event),
+ };
+ modcount := Nmodule;
+ if(debug['P'])
+ post(ref Event.Eproto(Pmodem, 0, Cplay, "play", 0,0,0));
+Evloop:
+ for(;;) {
+ ev: ref Event = nil;
+ post(nil);
+ alt {
+ # recv message from one of the modules
+ ev =<- t.in =>
+ if(ev == nil) { # modules ack Equit with nil
+ if(--modcount == 0)
+ break Evloop;
+ continue;
+ }
+ pick e := ev {
+ Equit => # close modules down
+ post(ref Event.Equit(Pscreen|Pmodem|Pkeyb|Psocket,0));
+ continue;
+ }
+
+ eva := protocol(ev);
+ while(len eva > 0) {
+ post(eva[0]);
+ eva = eva[1:];
+ }
+
+ # send message to `plumbed' modules
+ t.out[Mscreen].ch <- = t.out[Mscreen].ev =>
+ t.out[Mscreen].ev = nil;
+ t.out[Mmodem].ch <- = t.out[Mmodem].ev =>
+ t.out[Mmodem].ev = nil;
+ t.out[Mkeyb].ch <- = t.out[Mkeyb].ev =>
+ t.out[Mkeyb].ev = nil;
+ t.out[Msocket].ch <- = t.out[Msocket].ev =>
+ t.out[Msocket].ev = nil;
+
+ # recv message from Tk
+ cmd := <- t.cmd =>
+ (n, word) := sys->tokenize(cmd, " ");
+ if(n >0)
+ case hd word {
+ "resize" => ;
+ "play" => # for testing only
+ post(ref Event.Eproto(Pmodem, Mmodem, Cplay, "play", 0,0,0));
+ "keyboard" =>
+ if (t.kbctl == nil) {
+ e: string;
+ (e, t.kbctl) = kb(t);
+ if (e != nil)
+ sys->print("cannot start keyboard: %s\n", e);
+ } else
+ t.kbctl <- = "click";
+ "hangup" =>
+ if(T.state == Online || T.state == Connecting)
+ post(ref Event.Eproto(Pmodem, 0, Cdisconnect, "",0,0,0));
+ "buttonsleft" =>
+ tkcmds(t.toplevel, tkip40x25lhs);
+ t.buttonsleft = 1;
+ if(S.image != nil)
+ draw->(S.image.origin)(Point(0,0), Point(44, 0));
+ if (t.kbctl != nil)
+ t.kbctl <- = "fg";
+ "buttonsright" =>
+ tkcmds(t.toplevel, tkip40x25rhs);
+ t.buttonsleft = 0;
+ if(S.image != nil)
+ draw->(S.image.origin)(Point(0,0), Point(0, 0));
+ if (t.kbctl != nil)
+ t.kbctl <- = "fg";
+ "debug" =>
+ debug['s'] ^= 1;
+ debug['m'] ^= 1;
+ }
+ }
+
+ }
+ if (t.kbctl != nil)
+ t.kbctl <- = "quit";
+ t.kbctl = nil;
+ done <-= 0;
+}
+
+kb(t: ref Terminal): (string, chan of string)
+{
+ s := chan of string;
+ spawn dokb(t, s);
+ e := <- s;
+ if (e != nil)
+ return (e, nil);
+ return (nil, s);
+}
+
+Terminal.setkbmode(t: self ref Terminal, tmode: int)
+{
+ case tmode {
+ Videotex =>
+ t.kbmode = "minitel";
+ Mixed or Ascii =>
+ t.kbmode = "standard";
+ }
+ if(t.kbctl != nil) {
+ t.kbctl <-= "mode";
+ t.kbctl <-= "fg";
+ }
+}
+
+include "swkeyb.m";
+dokb(t: ref Terminal, c: chan of string)
+{
+ keyboard := load Keyboard Keyboard->PATH;
+ if (keyboard == nil) {
+ c <- = "cannot load keyboard";
+ return;
+ }
+
+ kbctl := chan of string;
+ (top, m) := tkclient->toplevel(S.ctxt, "", "Keyboard", 0);
+ tk->cmd(top, "pack .Wm_t -fill x");
+ tk->cmd(top, "update");
+ keyboard->chaninit(top, S.ctxt, ".keys", kbctl);
+ tk->cmd(top, "pack .keys");
+
+ kbctl <-= t.kbmode ;
+
+ kbon := 1;
+ c <- = nil; # all ok, we are now ready to accept commands
+
+ for (;;) alt {
+ mcmd := <- m =>
+ if (mcmd == "exit") {
+ if (kbon) {
+ tk->cmd(top, ". unmap; update");
+ kbon = 0;
+ }
+ } else
+ tkclient->wmctl(top, mcmd);
+ kbcmd := <- c =>
+ case kbcmd {
+ "fg" =>
+ if (kbon)
+ tk->cmd(top, "raise .;update");
+ "click" =>
+ if (kbon) {
+ tk->cmd(top, ". unmap; update");
+ kbon = 0;
+ } else {
+ tk->cmd(top, ". map; raise .");
+ kbon = 1;
+ }
+ "mode" =>
+ kbctl <- = t.kbmode;
+ "quit" =>
+ kbctl <- = "kill";
+ top = nil;
+ # ensure tkclient not blocked on a send to us (probably overkill!)
+ alt {
+ <- m => ;
+ * => ;
+ }
+ return;
+ }
+ }
+}
+
+
+Terminal.quit(nil: self ref Terminal)
+{
+}
+
+# a minitel module sends an event to the terminal for routing
+send(e: ref Event)
+{
+ if(debug['e'] && e != nil)
+ fprint(stderr, "%s: -> %s\n", Modname[e.from], e.str());
+ T.in <- = e;
+}
+
+# post an event to one or more modules
+post(e: ref Event)
+{
+ i,l: int;
+ for(i=0; i<Nmodule; i++) {
+ # `ev' is cleared once sent, reload it from the front of `q'
+ b: ref BufChan = T.out[i];
+ l = len b.q;
+ if(b.ev == nil && l != 0) {
+ b.ev = b.q[0];
+ na := array [l-1] of ref Event;
+ na[0:] = b.q[1:];
+ b.q = na;
+ }
+ if (e != nil) {
+ if(e.path & b.path) {
+ if(debug['e'] > 0) {
+ pick de := e {
+ * =>
+ fprint(stderr, "[%s<-%s] %s\n", Modname[i], Modname[e.from], e.str());
+ }
+ }
+ if(b.ev == nil) # nothing queued
+ b.ev = e;
+ else { # enqueue it
+ l = len b.q;
+ na := array [l+1] of ref Event;
+ na[0:] = b.q[0:];
+ na[l] = e;
+ b.q = na;
+ }
+ }
+ }
+ # set a dummy channel if nothing to send
+ if(b.ev == nil)
+ b.ch = chan of ref Event;
+ else
+ b.ch = b.in;
+ }
+}
+
+# run the terminal protocol
+protocol(ev: ref Event): array of ref Event
+{
+ # Introduced by the following sequences, the minitel protocol can be
+ # embedded in any normal data sequence
+ # ESC,0x39,X
+ # ESC,0x3a,X,Y
+ # ESC,0x3b,X,Y,Z
+ # ESC,0x61 - cursor position request
+
+ ea := array [0] of ref Event; # resulting sequence of Events
+ changed := 0; # if set, results are found in `ea'
+
+ pick e := ev {
+ Edata =>
+ d0 := 0; # offset of start of last data sequence
+ p := T.proto[e.from];
+ for(i:=0; i<len e.data; i++) {
+ ch := int e.data[i];
+# if(debug['p'])
+# fprint(stderr, "protocol: [%s] %d %ux (%c)\n", Modname[e.from], p.state, ch, ch);
+ if(p.skip > 0) { # in transparency mode
+ if(ch == 0 && e.from == Mmodem) # 5.0
+ continue;
+ p.skip--;
+ continue;
+ }
+ case p.state {
+ PSstart =>
+ if(ch == ESC) {
+ p.state = PSesc;
+ changed = 1;
+ if(i > d0)
+ ea = eappend(ea, ref Event.Edata(e.path, e.from, e.data[d0:i]));
+ d0 = i+1;
+ }
+ PSesc =>
+ p.state = PSarg;
+ p.n = 0;
+ d0 = i+1;
+ changed = 1;
+ if(ch >= 16r39 && ch <= 16r3b) #PRO1,2,3
+ p.nargs = ch - 16r39 + 1;
+ else if(ch == 16r61) # cursor position request
+ p.nargs = 0;
+ else if(ch == ESC) {
+ ea = eappend(ea, ref Event.Edata(e.path, e.from, array [] of { byte ESC }));
+ p.state = PSesc;
+ } else {
+ # false alarm, restore as data
+ ea = eappend(ea, ref Event.Edata(e.path, e.from, array [] of { byte ESC, byte ch }));
+ p.state = PSstart;
+ }
+ PSarg => # expect `nargs' bytes
+ d0 = i+1;
+ changed =1;
+ if(p.n < p.nargs)
+ p.arg[p.n++] = ch;
+ if(p.n == p.nargs) {
+ # got complete protocol sequence
+ pe := proto(e.from, p);
+ if(pe != nil)
+ ea = eappend(ea, pe);
+ p.state = PSstart;
+ }
+ }
+ }
+ if(changed) { # some interpretation, results in `ea'
+ if(i > d0)
+ ea = eappend(ea, ref Event.Edata(e.path, e.from, e.data[d0:i]));
+ return ea;
+ }
+ ev = e;
+ return array [] of {ev};
+ }
+ return array [] of {ev};
+}
+
+# append to an Event array
+eappend(ea: array of ref Event, e: ref Event): array of ref Event
+{
+ l := len ea;
+ na := array [l+1] of ref Event;
+ na[0:] = ea[0:];
+ na[l] = e;
+ return na;
+}
+
+# act on a received protocol sequence
+# some sequences are handled here by the terminal and result in a posted reply
+# others are returned `inline' as Eproto events with the normal data stream.
+proto(from: int, p: ref PState): ref Event
+{
+ if(debug['p']) {
+ fprint(stderr, "PRO%d: %ux", p.nargs, p.arg[0]);
+ if(p.nargs > 1)
+ fprint(stderr, " %ux", p.arg[1]);
+ if(p.nargs > 2)
+ fprint(stderr, " %ux", p.arg[2]);
+ fprint(stderr, " (%s)\n", Modname[from]);
+ }
+ case p.nargs {
+ 0 => # cursor position request ESC 0x61
+ reply := array [] of { byte US, byte S.pos.y, byte S.pos.x };
+ post(ref Event.Edata(Pmodem, from, reply));
+ 1 =>
+ case p.arg[0] {
+ PROTOCOLSTATUS => ;
+ ENQROM => # identification request
+ post(ref Event.Edata(Pmodem, from, T.terminalid));
+ if(T.terminalid == TERMINALID1)
+ T.terminalid = TERMINALID2;
+ SETRAM1 or SETRAM2 => ;
+ FUNCTIONINGSTATUS => # 11.3
+ PRO2(Pmodem, from, REPFUNCTIONINGSTATUS, osb());
+ CONNECT => ;
+ DISCONNECT =>
+ return ref Event.Eproto(Pscreen, from, Cscreenoff, "",0,0,0);
+ RESET => # reset the minitel terminal
+ all := Pscreen|Pmodem|Pkeyb|Psocket;
+ post(ref Event.Eproto(all, from, Creset, "",0,0,0)); # check
+ T.reset();
+ reply := array [] of { byte SEP, byte 16r5E };
+ post(ref Event.Edata(Pmodem, from, reply));
+ }
+ 2 =>
+ case p.arg[0] {
+ TO => # request for module status
+ PRO3(Pmodem, from, FROM, p.arg[1], psb(p.arg[1]));
+ NOBROADCAST => ;
+ BROADCAST => ;
+ TRANSPARENCY => # transparency mode - skip bytes
+ p.skip = p.arg[1];
+ if(p.skip < 1 || p.skip > 127) # 5.0
+ p.skip = 0;
+ else {
+ reply := array [] of { byte SEP, byte 16r57 };
+ post(ref Event.Edata(Pmodem, from, reply));
+ }
+ KEYBOARDSTATUS =>
+ if(p.arg[1] == RxKeyb)
+ PRO3(Pmodem, from, REPKEYBOARDSTATUS, RxKeyb, kosb());
+ START =>
+ x := osb();
+ if(p.arg[1] == PROCEDURE)
+ x |= 16r04;
+ if(p.arg[1] == SCROLLING)
+ x |= 16r02;
+ PRO2(Pmodem, from, REPFUNCTIONINGSTATUS, x);
+ case p.arg[1] {
+ PROCEDURE => # activate error correction procedure
+ sys->print("activate error correction\n");
+ return ref Event.Eproto(Pmodem, from, Cstartecp, "",0,0,0);
+ SCROLLING => # set screen to scroll
+ return ref Event.Eproto(Pscreen, from, Cproto, "",START,SCROLLING,0);
+ LOWERCASE => # set keyb to invert case
+ return ref Event.Eproto(Pkeyb, from, Cproto, "",START,LOWERCASE,0);
+ }
+ STOP =>
+ x := osb();
+ if(p.arg[1] == SCROLLING)
+ x &= ~16r02;
+ PRO2(Pmodem, from, REPFUNCTIONINGSTATUS, osb());
+ case p.arg[1] {
+ PROCEDURE => # deactivate error correction procedure
+ sys->print("deactivate error correction\n");
+ return ref Event.Eproto(Pmodem, from, Cstopecp, "",0,0,0);
+ SCROLLING => # set screen to no scroll
+ return ref Event.Eproto(Pscreen, from, Cproto, "",STOP,SCROLLING,0);
+ LOWERCASE => # set keyb to not invert case
+ return ref Event.Eproto(Pkeyb, from, Cproto, "",STOP,LOWERCASE,0);
+ }
+ COPY => # copy screen to socket
+ # not implemented
+ ;
+ MIXED => # change video mode (12.1)
+ case p.arg[1] {
+ MIXED1 => # videotex -> mixed
+ reply := array [] of { byte SEP, byte 16r70 };
+ return ref Event.Eproto(Pscreen, from, Cproto, "",MIXED,MIXED1,0);
+ MIXED2 => # mixed -> videotex
+ reply := array [] of { byte SEP, byte 16r71 };
+ return ref Event.Eproto(Pscreen, from, Cproto, "",MIXED,MIXED2,0);
+ }
+ ASCII => # change video mode (12.2)
+ # TODO
+ ;
+ }
+ 3 =>
+ case p.arg[0] {
+ OFF or ON => # link, unlink, enable, disable
+ modcmd(p.arg[0], p.arg[1], p.arg[2]);
+ PRO3(Pmodem, from, FROM, p.arg[1], psb(TxCode(p.arg[1])));
+ START =>
+ case p.arg[1] {
+ RxKeyb => # keyboard mode
+ case p.arg[2] {
+ ETEN => # extended keyboard
+ K.spec |= Extend;
+ C0 => # cursor control key coding from col 0
+ K.spec |= C0keys;
+ }
+ PRO3(Pmodem, from, REPKEYBOARDSTATUS, RxKeyb, kosb());
+ }
+ STOP => # keyboard mode
+ case p.arg[1] {
+ RxKeyb => # keyboard mode
+ case p.arg[2] {
+ ETEN => # extended keyboard
+ K.spec &= ~Extend;
+ C0 => # cursor control key coding from col 0
+ K.spec &= ~C0keys;
+ }
+ PRO3(Pmodem, from, REPKEYBOARDSTATUS, RxKeyb, kosb());
+ }
+ }
+ }
+ return nil;
+}
+
+# post a PRO3 sequence to all modules on `path'
+PRO3(path, from, x, y, z: int)
+{
+ data := array [] of { byte ESC, byte 16r3b, byte x, byte y, byte z};
+ post(ref Event.Edata(path, from, data));
+}
+
+# post a PRO2 sequence to all modules on `path'
+PRO2(path, from, x, y: int)
+{
+ data := array [] of { byte ESC, byte 16r3a, byte x, byte y};
+ post(ref Event.Edata(path, from, data));
+}
+
+# post a PRO1 sequence to all modules on `path'
+PRO1(path, from, x: int)
+{
+ data := array [] of { byte ESC, byte 16r39, byte x};
+ post(ref Event.Edata(path, from, data));
+}
+
+# make or break links between modules, or enable and disable
+modcmd(cmd, from, targ: int)
+{
+ from = RxTx(from);
+ targ = RxTx(targ);
+ if(from == targ) # enable or disable module
+ if(cmd == ON)
+ Modules[from].disabled = 0;
+ else
+ Modules[from].disabled = 1;
+ else # modify path
+ if(cmd == ON)
+ Modules[from].path |= (1<<targ);
+ else
+ Modules[from].path &= ~(1<<targ);
+}
+
+# determine the path status byte (3.4)
+# if bit 3 of `code' is set then a receive path status byte is returned
+# otherwise a transmit path status byte
+psb(code: int): int
+{
+ this := RxTx(code);
+ b := 16r40; # bit 6 always set
+ if(code == RxCode(code)) { # want a receive path status byte
+ mask := (1<<this);
+ if(Modules[Mscreen].path & mask)
+ b |= 16r01;
+ if(Modules[Mkeyb].path & mask)
+ b |= 16r02;
+ if(Modules[Mmodem].path & mask)
+ b |= 16r04;
+ if(Modules[Msocket].path & mask)
+ b |= 16r08;
+ } else {
+ mod := Modules[this];
+ if(mod.path & Mscreen)
+ b |= 16r01;
+ if(mod.path & Mkeyb)
+ b |= 16r02;
+ if(mod.path & Mmodem)
+ b |= 16r04;
+ if(mod.path & Msocket)
+ b |= 16r08;
+ }
+# if(parity(b))
+# b ^= 16r80;
+ return b;
+}
+
+# convert `code' to a receive code by setting bit 3
+RxCode(code: int): int
+{
+ return (code | 16r08)&16rff;
+}
+
+# covert `code' to a send code by clearing bit 3
+TxCode(code: int): int
+{
+ return (code & ~16r08)&16rff;
+}
+
+# return 0 on even parity, 1 otherwise
+# only the bottom 8 bits are considered
+parity(b: int): int
+{
+ bits := 8;
+ p := 0;
+ while(bits-- > 0) {
+ if(b&1)
+ p ^= 1;
+ b >>= 1;
+ }
+ return p;
+}
+
+# convert Rx or Tx code to a module code
+RxTx(code: int): int
+{
+ rv := 0;
+ case code {
+ TxScreen or RxScreen => rv = Mscreen;
+ TxKeyb or RxKeyb => rv = Mkeyb;
+ TxModem or RxModem => rv = Mmodem;
+ TxSocket or RxSocket => rv = Msocket;
+ * =>
+ fatal("invalid module code");
+ }
+ return rv;
+}
+
+# generate an operating status byte (11.2)
+osb(): int
+{
+ b := 16r40;
+ if(S.cols == 80)
+ b |= 16r01;
+ if(S.spec & Scroll)
+ b |= 16r02;
+ if(M.spec & Ecp)
+ b |= 16r04;
+ if(K.spec & Invert)
+ b |= 16r08;
+# if(parity(b))
+# b ^= 16r80;
+ return b;
+}
+
+# generate a keyboard operating status byte (9.1.2)
+kosb(): int
+{
+ b := 16r40;
+ if(K.spec & Extend)
+ b |= 16r01;
+ if(K.spec & C0keys)
+ b |= 16r04;
+# if(parity(b))
+# b ^= 16r80;
+ return b;
+}
+
+hex(v, n: int): string
+{
+ return sprint("%.*ux", n, v);
+}
+
+tostr(ch: int): string
+{
+ str := "";
+ str[0] = ch;
+ return str;
+}
+
+toint(s: string, base: int): (int, string)
+{
+ if(base < 0 || base > 36)
+ return (0, s);
+
+ c := 0;
+ for(i := 0; i < len s; i++) {
+ c = s[i];
+ if(c != ' ' && c != '\t' && c != '\n')
+ break;
+ }
+
+ neg := 0;
+ if(c == '+' || c == '-') {
+ if(c == '-')
+ neg = 1;
+ i++;
+ }
+
+ ok := 0;
+ n := 0;
+ for(; i < len s; i++) {
+ c = s[i];
+ v := base;
+ case c {
+ 'a' to 'z' =>
+ v = c - 'a' + 10;
+ 'A' to 'Z' =>
+ v = c - 'A' + 10;
+ '0' to '9' =>
+ v = c - '0';
+ }
+ if(v >= base)
+ break;
+ ok = 1;
+ n = n * base + v;
+ }
+
+ if(!ok)
+ return (0, s);
+ if(neg)
+ n = -n;
+ return (n, s[i:]);
+}
+
+tolower(s: string): string
+{
+ r := s;
+ for(i := 0; i < len r; i++) {
+ c := r[i];
+ if(c >= int 'A' && c <= int 'Z')
+ r[i] = r[i] + (int 'a' - int 'A');
+ }
+ return r;
+}
+
+# duplicate `ch' exactly `n' times
+dup(ch, n: int): string
+{
+ str := "";
+ for(i:=0; i<n; i++)
+ str[i] = ch;
+ return str;
+}
+
+fatal(msg: string)
+{
+ fprint(stderr, "fatal: %s\n", msg);
+ exits(msg);
+}
+
+exits(s: string)
+{
+ if(s==nil);
+# raise "fail: miniterm " + s;
+ fd := sys->open("#p/" + string pgrp + "/ctl", sys->OWRITE);
+ if(fd != nil)
+ sys->fprint(fd, "killgrp");
+ exit;
+}
+
+# Minitel byte MSB and LSB classification (p.87)
+MSB(ch: int): int
+{
+ return (ch&16r70)>>4;
+}
+LSB(ch: int): int
+{
+ return (ch&16r0f);
+}
+
+# Minitel character set classification (p.92)
+ISC0(ch: int): int
+{
+ msb := (ch&16r70)>>4;
+ return msb == 0 || msb == 1;
+}
+
+ISC1(ch: int): int
+{
+ return ch >= 16r40 && ch <= 16r5f;
+}
+
+ISG0(ch: int): int
+{
+ # 0x20 (space) and 0x7f (DEL) are not in G0
+ return ch > 16r20 && ch < 16r7f;
+}
+
+tkcmds(t: ref Tk->Toplevel, cmds: array of string)
+{
+ n := len cmds;
+ for (ix := 0; ix < n; ix++)
+ tk->cmd(t, cmds[ix]);
+}