diff options
| author | Charles Forsyth <charles.forsyth@gmail.com> | 2015-04-29 15:19:07 +0100 |
|---|---|---|
| committer | Charles Forsyth <charles.forsyth@gmail.com> | 2015-04-29 15:19:07 +0100 |
| commit | c714c442442ef137f20ca4ff9707d5480cb9ba7a (patch) | |
| tree | d0d285f05cb4292fa8f1f3c0bc70bec1251e956e /appl/examples/minitel | |
| parent | 1ac9729e9325d84db36c04b5cda3b5b1bc0d041f (diff) | |
remove obsolete minitel, but leave source as example
Diffstat (limited to 'appl/examples/minitel')
| -rw-r--r-- | appl/examples/minitel/README | 209 | ||||
| -rw-r--r-- | appl/examples/minitel/event.b | 19 | ||||
| -rw-r--r-- | appl/examples/minitel/event.m | 19 | ||||
| -rw-r--r-- | appl/examples/minitel/keyb.b | 367 | ||||
| -rw-r--r-- | appl/examples/minitel/mdisplay.b | 799 | ||||
| -rw-r--r-- | appl/examples/minitel/mdisplay.m | 115 | ||||
| -rw-r--r-- | appl/examples/minitel/miniterm.b | 1187 | ||||
| -rw-r--r-- | appl/examples/minitel/miniterm.m | 120 | ||||
| -rw-r--r-- | appl/examples/minitel/mkfile | 24 | ||||
| -rw-r--r-- | appl/examples/minitel/modem.b | 620 | ||||
| -rw-r--r-- | appl/examples/minitel/screen.b | 1610 | ||||
| -rw-r--r-- | appl/examples/minitel/socket.b | 49 | ||||
| -rw-r--r-- | appl/examples/minitel/swkeyb.b | 370 | ||||
| -rw-r--r-- | appl/examples/minitel/swkeyb.m | 21 |
14 files changed, 5529 insertions, 0 deletions
diff --git a/appl/examples/minitel/README b/appl/examples/minitel/README new file mode 100644 index 00000000..82f3202e --- /dev/null +++ b/appl/examples/minitel/README @@ -0,0 +1,209 @@ +Minitel Emulation for Inferno + +This directory contains the source of `miniterm', a minitel emulator +for Inferno. Miniterm is written in Limbo. The main components are: + + miniterm.m - common constants + miniterm.b - terminal emulator, messaging and Minitel `protocol` + event.[mb] - inter-module message format + keyb.b - Minitel keyboard module + modem.b - Minitel modem module + screen.b - Minitel screen module + socket.b - Minitel socket module + arg.m - basic command line argument handling + mdisplay.[mb] - Videotex display module + swkeyb.[mb] - Minitel aware software keyboard + + fonts.tgz which expands into: + + fonts/minitel - external and subfont directory (`bind -b' into /fonts) + fonts/minitel/f40x25 - 40 column external font + fonts/minitel/14x17 + fonts/minitel/14x17xoe + fonts/minitel/14x17arrow + fonts/minitel/f40x25g1 - 40 column semigraphic external font + fonts/minitel/vid14x17 + fonts/minitel/f40x25h - 40 column double height external font + fonts/minitel/14x34 + fonts/minitel/14x34xoe + fonts/minitel/14x34arrow + fonts/minitel/f40x25w - 40 column double width external font + fonts/minitel/28x17 + fonts/minitel/28x17xoe + fonts/minitel/28x17arrow + fonts/minitel/f40x25s - 40 column double size external font + fonts/minitel/28x34xoe + fonts/minitel/28x34arrow + fonts/minitel/f80x25 - 80 column external font + fonts/minitel/8x12 + fonts/minitel/8x12xoe + fonts/minitel/8x12arrow + +The fonts subdirectory should be bound into /fonts: + bind -b fonts /fonts +or the directory fonts/minitel copied to /fonts/minitel before invoking the emulator. +The names of the external fonts are +known to the Videotex display module. Similarly, the files: + /dev/modem + /dev/modemctl +are known to the modem module, but you can ignore them if +(as is almost certain) you are using the Internet-minitel gateway +and you haven't got appropriate modem hardware anyway. + +To build + mkdir /usr/inferno/dis/wm/minitel + mk install + +The code models the structure outlined in the Minitel 1B specification +provided by France Telecom. However, much more interpretation was +required to display the majority of screens currently seen on Minitel. +Additional information (although sketchy) was found on the Internet by +searching for Minitel or Videotex and also by examination of the codes +sent by minitel servers and experimenting with replies. There must be +some more up to date information somewhere! + +We don't support downloadable fonts, but correctly filter them out. + +The file miniterm.b contains the code for the minitel `terminal' with +which the other modules communicate. The keyboard, modem, socket, +screen and terminal are run as separate threads which communicate by +calling: + send(e: ref Event) +The clue to the intermodule communication is in Terminal.run which +does something like: + for(;;) { + ev =<- t.in => + eva := protocol(ev); + while(len eva > 0) { + post(eva[0]); + eva = eva[1:]; + } + # then deliver any `posted' messages (without blocking) + } +An Event `ev' may typically be an Edata type (say from the modem) or +an Eproto type for internal interpretation. In the call: + eva := protocol(ev) +The function protocol() dissects Edata messages to produce an inline +sequence of Edata and Eproto messages. The function post() queues +messages for delivery to the appropriate modules. For example, data +from the modem might be destined for the screen and the socket module. +Messages are queued until they can be delivered. That way the line: + ev =<- t.in +is executed in a timely way and the other modules can be written to +make blocking writes (via send()) and to service reads when they are +ready. + +In many places in the code lines appear with comments like: + if(p.skip < 1 || p.skip > 127) # 5.0 +These refer to sections of the Minitel specification which explain the +code. + +The mdisplay code provides a Videotex display using Inferno +primitives. The screen, keyboard and modem modules interpret data as +described in the equivalent section of the Minitel specification. The +socket module has not been implemented but currently performs a `null' +function and could easily be added if required. + + +- Namespace +We always expect the fonts to appear in /fonts and the softmodem +to appear as /dev/modem and /dev/modemctl. + +- Invocation +If invoked with no argument, miniterm uses the France Telecom +internet gateway by default (tcp!193.252.252.250!513). +If the argument starts with `modem' then +a direct connection through /dev/modem will be established. + +An argument beginning with anything other than `modem' will +be assumed to be an address suitable for dial(). For example: + + wm/minitel/miniterm tcp!193.252.252.250!513 + +will connect to the current France Telecom internet server. + +For direct connections a modem `init' string and an optional +phone number can follow the modem prefix, as in: + + wm/minitel/miniterm modem!F3!3615 + +or + + wm/minitel/miniterm modem!F3!01133836431414 + +The `F3' is the code which instructs the softmodem to enable V.23 +and needs to be passed when connecting to the FT servers. +To use pulse dialing instead of tone dialing the phone number +can be prefixed with a 'P' as in: + + wm/minitel/miniterm modem!F3!P3614 + +If the parameter specifies a network connection or a direct connection +with a phone number the software will attempt to connect immediately. +If Cx/Fin is used to disconnect and then re-connect it will use the +same IP address for a network connection or prompt for a new +phone number in the case of a direct connection. When prompting +for a new number the top row of the screen is used to allow the user +to edit the last used number. Simple editing is available, and the minitel +keys do the obvious things. + + + +** Notes on the 15th December 1998 Release ** + +- Software keyboard +A version of the software keyboard which understands some of +the minitel keyboard mappings is included. For example, hitting 'A' results +in a capital 'A' on the screen in spite of the Videotex case mapping. + +- Minitel function keys +The minitel keys are displayed on the right hand side of the screen +in 40 column mode on a network connection +and can be swapped to the left hand side by hitting the <- key. +In direct dial mode and 80 column network mode the keys are +displayed at the bottom of the screen. +In network mode they are re-displayed as appropriate on 40 to 80 +column mode changes. + + +Known Omission +------------- +- Error Correction (direct dial only) +There is no screen button to enable error correction in the release. +If a server asks for error correction it will be enabled. It looks as though +we need to include a key to enable it. Without it direct dial screens are +occasionally corrupted. + +- Software Keyboard Handling +We need to add some code to update the software keyboard and +bring it to the foreground on a mode change. + +- Full 80 column support +I am aware of some screens which don't look correct in 80 column +mode (and others that do). See `EMAIL' then choose USENET and +press SUITE a few times. I believe it behaves as specified but as we +have seen with the 40 column Videotex mode the specification +is not sufficient to display most of the minitel screens correctly. +80 column support needs just a little more work. +It may be, too, that the 80 column font could be made much more +readable by utilising a few more pixels on the screen now that we +are able to cover the toolbar. + +- Full toolbar integration +Experimentation will show whether there needs to be more +integration with the toolbar. + +Known Bugs +---------- +- Softmodem disconnection +Often, the modem does not hangup correctly. + +- Choose `USA' from a network connection +USA (from a network connection) gives an `iC' in bottom left hand +corner of screen. Possibly a server issue. Doesn't occur when +connecting directly. The server is really sending this sequence. +Both the FT emulator and their explorer plug-in suffer from it too. + + +John Bates +Vita Nuova Limited diff --git a/appl/examples/minitel/event.b b/appl/examples/minitel/event.b new file mode 100644 index 00000000..f751f55b --- /dev/null +++ b/appl/examples/minitel/event.b @@ -0,0 +1,19 @@ +# +# Copyright © 1998 Vita Nuova Limited. All rights reserved. +# + +Event.str(ev: self ref Event) : string +{ + s := "?"; + pick e := ev { + Edata => + s = sprint("Edata %d = ", len e.data); + for(i:=0; i<len e.data; i++) + s += hex(int e.data[i], 2) + " "; + Equit => + s = "Equit"; + Eproto => + s = sprint("Eproto %ux (%s)", e.cmd, e.s); + } + return s; +} diff --git a/appl/examples/minitel/event.m b/appl/examples/minitel/event.m new file mode 100644 index 00000000..1b524363 --- /dev/null +++ b/appl/examples/minitel/event.m @@ -0,0 +1,19 @@ +# +# Copyright © 1998 Vita Nuova Limited. All rights reserved. +# + +Event: adt { + path: int; # path for delivery + from: int; # sending module (for reply) + pick { + Edata => + data: array of byte; + Eproto => + cmd: int; + s: string; + a0, a1, a2: int; # parameters + Equit => + } + + str: fn(e: self ref Event) : string; # convert to readable form +}; diff --git a/appl/examples/minitel/keyb.b b/appl/examples/minitel/keyb.b new file mode 100644 index 00000000..aba5485d --- /dev/null +++ b/appl/examples/minitel/keyb.b @@ -0,0 +1,367 @@ +# +# Copyright © 1998 Vita Nuova Limited. All rights reserved. +# + +# special keyboard operations +Extend, # enable cursor and editing keys and control chars +C0keys, # cursor keys send BS,HT,LF and VT +Invert # case inversion + : con 1 << iota; + +Keyb: adt { + m: ref Module; # common attributes + in: chan of ref Event; + + cmd: chan of string; # from Tk (keypresses and focus) + spec: int; # special keyboard extensions + + init: fn(k: self ref Keyb, toplevel: ref Tk->Toplevel); + reset: fn(k: self ref Keyb); + run: fn(k: self ref Keyb); + quit: fn(k: self ref Keyb); + map: fn(k: self ref Keyb, key:int): array of byte; +}; + +Keyb.init(k: self ref Keyb, toplevel: ref Tk->Toplevel) +{ + k.in = chan of ref Event; + k.cmd = chan of string; + tk->namechan(toplevel, k.cmd, "keyb"); # Tk -> keyboard + k.reset(); +} + +Keyb.reset(k: self ref Keyb) +{ + k.m = ref Module(Pmodem|Psocket, 0); +} + +ask(in: chan of string, out: chan of string) +{ + keys: string; + + T.mode = Videotex; + S.setmode(Videotex); +# clear(S); + prompt: con "Numéroter: "; + number := M.lastdialstr; + S.msg(prompt); + +Input: + for(;;) { + n := len prompt + len number; + # guard length must be > len prompt + if (n > 30) + n -= 30; + else + n = 0; + S.msg(prompt + number[n:]); + keys = <- in; + if (keys == nil) + return; + + keys = canoncmd(keys); + + case keys { + "connect" or "send" => + break Input; + "correct" => + if(len number > 0) + number = number[0: len number -1]; + "cancel" => + number = ""; + break Input; + "repeat" or "index" or "guide" or "next" or "previous" => + ; + * => + number += keys; + } + } + + S.msg(nil); + for (;;) alt { + out <- = number => + return; + keys = <- in => + if (keys == nil) + return; + } +} + +Keyb.run(k: self ref Keyb) +{ + dontask := chan of string; + askchan := dontask; + askkeys := chan of string; +Runloop: + for(;;){ + alt { + ev := <- k.in => + pick e := ev { + Equit => + break Runloop; + Eproto => + case e.cmd { + Creset => + k.reset(); + Cproto => + case e.a0 { + START => + case e.a1 { + LOWERCASE => + k.spec |= Invert; + } + STOP => + case e.a1 { + LOWERCASE => + k.spec &= ~Invert; + } + } + * => break; + } + } + cmd := <- k.cmd => + if(debug['k'] > 0) { + fprint(stderr, "Tk %s\n", cmd); + } + (n, args) := sys->tokenize(cmd, " "); + if(n >0) + case hd args { + "key" => + (key, nil) := toint(hd tl args, 16); + if(askchan != dontask) { + s := minikey(key); + if (s == nil) + s[0] = key; + askkeys <-= s; + break; + } + keys := k.map(key); + if(keys != nil) { + send(ref Event.Edata(k.m.path, Mkeyb, keys)); + } + "skey" => # minitel key hit (soft key) + if(hd tl args == "Exit") { + if(askchan != dontask) { + askchan = dontask; + askkeys <-= nil; + } + if(T.state == Online || T.state == Connecting) { + seq := keyseq("connect"); + if(seq != nil) { + send(ref Event.Edata(k.m.path, Mkeyb, seq)); + send(ref Event.Edata(k.m.path, Mkeyb, seq)); + } + send(ref Event.Eproto(Pmodem, Mkeyb, Cdisconnect, "", 0,0,0)); + } + send(ref Event.Equit(0, 0)); + break; + } + if(askchan != dontask) { + askkeys <-= hd tl args; + break; + } + case hd tl args { + "Connect" => + case T.state { + Local => + if(M.connect == Network) + send(ref Event.Eproto(Pmodem, Mkeyb, Cconnect, "", 0,0,0)); + else { + askchan = chan of string; + spawn ask(askkeys, askchan); + } + Connecting => + send(ref Event.Eproto(Pmodem, Mkeyb, Cdisconnect, "", 0,0,0)); + Online => + seq := keyseq("connect"); + if(seq != nil) + send(ref Event.Edata(k.m.path, Mkeyb, seq)); + } + * => + seq := keyseq(hd tl args); + if(seq != nil) + send(ref Event.Edata(k.m.path, Mkeyb, seq)); + } + "click" => # fetch a word from the display + x := int hd tl args; + y := int hd tl tl args; + word := disp->GetWord(Point(x, y)); + if(word != nil) { + if (askchan != dontask) { + askkeys <- = word; + break; + } + if (T.state == Local) { + if (canoncmd(word) == "connect") { + if(M.connect == Network) + send(ref Event.Eproto(Pmodem, Mkeyb, Cconnect, "", 0,0,0)); + else { + askchan = chan of string; + spawn ask(askkeys, askchan); + } + break; + } + } + seq := keyseq(word); + if(seq != nil) + send(ref Event.Edata(k.m.path, Mkeyb, seq)); + else { + send(ref Event.Edata(k.m.path, Mkeyb, array of byte word )); + send(ref Event.Edata(k.m.path, Mkeyb, keyseq("send"))); + } + } + + } + dialstr := <-askchan => + askchan = dontask; + if(dialstr != nil) { + M.dialstr = dialstr; + send(ref Event.Eproto(Pmodem, Mkeyb, Cconnect, "", 0,0,0)); + } + } + } + send(nil); +} + + +# Perform mode specific key translation +# returns nil on invalid keypress, +Keyb.map(nil: self ref Keyb, key: int): array of byte +{ + # hardware to minitel keyboard mapping + cmd := minikey(key); + if (cmd != nil) { + seq := keyseq(cmd); + if(seq != nil) + return seq; + } + + # alphabetic (with case mapping) + case T.mode { + Videotex => + if(key >= 'A' && key <= 'Z') + return array [] of { byte ('a' + (key - 'A'))}; + if(key >= 'a' && key <= 'z') + return array [] of {byte ('A' + (key - 'a'))}; + Mixed or Ascii => + if(key >= 'A' && key <= 'Z' || key >= 'a' && key <= 'z') + return array [] of {byte key}; + }; + + # Numeric + if(key >= '0' && key <= '9') + return array [] of {byte key}; + + # Control-A -> Control-Z, Esc - columns 0 and 1 + if(key >= 16r00 && key <=16r1f) + case T.mode { + Videotex => + return nil; + Mixed or Ascii => + return array [] of {byte key}; + } + + # miscellaneous key mapping + case key { + 16r20 => ; # space + 16ra3 => return array [] of { byte 16r19, byte 16r23 }; # pound + '!' or '"' or '#' or '$' + or '%' or '&' or '\'' or '(' or ')' + or '*' or '+' or ',' or '-' + or '.' or ':' or ';' or '<' + or '=' or '>' or '?' or '@' => ; + KF13 => # request for error correction - usually Fnct M + C + if((M.spec&Ecp) == 0 && T.state == Online && T.connect == Direct) { +fprint(stderr, "requesting Ecp\n"); + return array [] of { byte SEP, byte 16r4a }; + } + return nil; + * => return nil; + } + return array [] of {byte key}; +} + +Keyb.quit(k: self ref Keyb) +{ + if(k==nil); +} + +canoncmd(s : string) : string +{ + s = tolower(s); + case s { + "connect" or "cx/fin" or + "connexion" or "fin" => return "connect"; + "send" or "envoi" => return "send"; + "repeat" or "repetition" => return "repeat"; + "index" or "sommaire" or "somm" + => return "index"; + "guide" => return "guide"; + "correct" or "correction" => return "correct"; + "cancel" or "annulation" or "annul" or "annu" + => return "cancel"; + "next" or "suite" => return "next"; + "previous" or "retour" or "retou" + => return "previous"; + } + return s; +} + +# map softkey names to the appropriate byte sequences +keyseq(skey: string): array of byte +{ + b2 := 0; + asterisk := 0; + if(skey == nil || len skey == 0) + return nil; + if(skey[0] == '*') { + asterisk = 1; + skey = skey[1:]; + } + skey = canoncmd(skey); + case skey { + "connect" => b2 = 16r49; + "send" => b2 = 16r41; + "repeat" => b2 = 16r43; + "index" => b2 = 16r46; + "guide" => b2 = 16r44; + "correct" => b2 = 16r47; + "cancel" => b2 = 16r45; + "next" => b2 = 16r48; + "previous" => b2 = 16r42; + } + if(b2) { + if(asterisk) + return array [] of { byte '*', byte SEP, byte b2}; + else + return array [] of { byte SEP, byte b2}; + } else + return nil; +} + +# map hardware or software keyboard presses to minitel functions +minikey(key: int): string +{ + case key { + Kup or KupPC => + return"previous"; + Kdown or KdownPC => + return "next"; + Kenter => + return "send"; + Kback => + return "correct"; + Kesc => + return "cancel"; + KF1 => + return "guide"; + KF2 => + return "connect"; + KF3 => + return "repeat"; + KF4 => + return "index"; + * => + return nil; + } +}
\ No newline at end of file diff --git a/appl/examples/minitel/mdisplay.b b/appl/examples/minitel/mdisplay.b new file mode 100644 index 00000000..b3c629f9 --- /dev/null +++ b/appl/examples/minitel/mdisplay.b @@ -0,0 +1,799 @@ +implement MDisplay; + +# +# Copyright © 1998 Vita Nuova Limited. All rights reserved. +# +# - best viewed with acme! + +include "sys.m"; +include "draw.m"; +include "mdisplay.m"; + +sys : Sys; +draw : Draw; + +Context, Point, Rect, Font, Image, Display, Screen : import draw; + + +# len cell == number of lines +# len cell[0] == number of cellmap cells per char +# (x,y)*cellsize == font glyph clipr + +cellS := array [] of {array [] of {(0, 0)}}; +cellW := array [] of {array [] of {(0, 0), (1, 0)}}; +cellH := array [] of {array [] of {(0, 1)}, array [] of {(0, 0)}}; +cellWH := array [] of {array [] of {(0, 1), (1, 1)}, array [] of {(0, 0), (1, 0)}}; + +Cellinfo : adt { + font : ref Font; + ch, attr : int; + clipmod : (int, int); +}; + + +# current display attributes +display : ref Display; +window : ref Image; +frames := array [2] of ref Image; +update : chan of int; + +colours : array of ref Image; +bright : ref Image; + +# current mode attributes +cellmap : array of Cellinfo; +nrows : int; +ncols : int; +ulheight : int; +curpos : Point; +winoff : Point; +cellsize : Point; +modeattr : con fgWhite | bgBlack; +showC := 0; +delims := 0; +modbbox := Rect((0,0),(0,0)); +blankrow : array of Cellinfo; + +ctxt : ref Context; +font : ref Font; # g0 videotex font - extended with unicode g2 syms +fonth : ref Font; # double height version of font +fontw : ref Font; # double width +fonts : ref Font; # double size +fontg1 : ref Font; # semigraphic videotex font (ch+128=separated) +fontfr : ref Font; # french character set +fontusa : ref Font; # american character set + + +Init(c : ref Context) : string +{ + sys = load Sys Sys->PATH; + draw = load Draw Draw->PATH; + + if (c == nil || c.display == nil) + return "no display context"; + + ctxt = c; + disp := ctxt.display; + + black := disp.rgb2cmap(0, 0, 0); + blue := disp.rgb2cmap(0, 0, 255); + red := disp.rgb2cmap(255, 0, 0); + magenta := disp.rgb2cmap(255, 0, 255); + green := disp.rgb2cmap(0, 255, 0); + cyan := disp.rgb2cmap(0, 255, 255); + yellow := disp.rgb2cmap(255, 255, 0); + white := disp.rgb2cmap(240, 240, 240); + + iblack := disp.color(black); + iblue := disp.color(blue); + ired := disp.color(red); + imagenta := disp.color(magenta); + igreen := disp.color(green); + icyan := disp.color(cyan); + iyellow := disp.color(yellow); + iwhite := disp.color(white); + + colours = array [] of { iblack, iblue, ired, imagenta, + igreen, icyan, iyellow, iwhite}; + bright = disp.color(disp.rgb2cmap(255, 255, 255)); + + update = chan of int; + spawn Update(update); + display = disp; + return nil; +} + +Quit() +{ + if (update != nil) + update <- = QuitUpdate; + update = nil; + window = nil; + frames[0] = nil; + frames[1] = nil; + cellmap = nil; + display = nil; +} + +Mode(r : Draw->Rect, w, h, ulh, d : int, fontpath : string) : (string, ref Draw->Image) +{ + if (display == nil) + # module not properly Init()'d + return ("not initialized", nil); + + curpos = Point(-1, -1); + if (window != nil) + update <- = Pause; + + cellmap = nil; + window = nil; + (dx, dy) := (r.dx(), r.dy()); + if (dx == 0 || dy == 0) { + return (nil, nil); + } + + black := display.rgb2cmap(0, 0, 0); + window = ctxt.screen.newwindow(r, Draw->Refbackup, black); + if (window == nil) + return ("cannot create window", nil); + + window.origin(Point(0,0), r.min); + winr := Rect((0,0), (dx, dy)); + frames[0] = display.newimage(winr, window.chans, 0, black); + frames[1] = display.newimage(winr, window.chans, 0, black); + + if (window == nil || frames[0] == nil || frames[1] == nil) { + window = nil; + return ("cannot allocate display resources", nil); + } + + ncols = w; + nrows = h; + ulheight = ulh; + delims = d; + showC = 0; + + cellmap = array [ncols * nrows] of Cellinfo; + + font = Font.open(display, fontpath); + fontw = Font.open(display, fontpath + "w"); + fonth = Font.open(display, fontpath + "h"); + fonts = Font.open(display, fontpath + "s"); + fontg1 = Font.open(display, fontpath + "g1"); + fontfr = Font.open(display, fontpath + "fr"); + fontusa = Font.open(display, fontpath + "usa"); + + if (font != nil) + cellsize = Point(font.width(" "), font.height); + else + cellsize = Point(dx/ncols, dy / nrows); + + winoff.x = (dx - (cellsize.x * ncols)) / 2; + winoff.y = (dy - (cellsize.y * nrows)) /2; + if (winoff.x < 0) + winoff.x = 0; + if (winoff.y < 0) + winoff.y = 0; + + blankrow = array [ncols] of {* => Cellinfo(font, ' ', modeattr | fgWhite, (0,0))}; + for (y := 0; y < nrows; y++) { + col0 := y * ncols; + cellmap[col0:] = blankrow; + } + +# frames[0].clipr = frames[0].r; +# frames[1].clipr = frames[1].r; +# frames[0].draw(frames[0].r, colours[0], nil, Point(0,0)); +# frames[1].draw(frames[1].r, colours[0], nil, Point(0,0)); +# window.draw(window.r, colours[0], nil, Point(0,0)); + update <- = Continue; + return (nil, window); +} + +Cursor(pt : Point) +{ + if (update == nil || cellmap == nil) + # update thread (cursor/character flashing) not running + return; + + # normalize pt + pt.x--; + + curpos = pt; + update <- = CursorSet; +} + +Put(str : string, pt : Point, charset, attr, insert : int) +{ + if (cellmap == nil || str == nil) + # nothing to do + return; + + # normalize pt + pt.x--; + + f : ref Font; + cell := cellS; + + case charset { + videotex => + if (!(attr & attrD)) + attr &= (fgMask | attrF | attrH | attrW | attrP); + if (attr & attrW && attr & attrH) { + cell = cellWH; + f = fonts; + } else if (attr & attrH) { + cell = cellH; + f = fonth; + } else if (attr & attrW) { + cell = cellW; + f = fontw; + } else { + f = font; + } + + semigraphic => + f = fontg1; + if (attr & attrL) { + # convert to "separated" + newstr := ""; + for (ix := 0; ix < len str; ix++) + newstr[ix] = str[ix] + 16r80; + str = newstr; + } + # semigraphic charset does not support size / polarity attributes + # attrD always set later once field attr established + attr &= ~(attrD | attrH | attrW | attrP | attrL); + + french => f = fontfr; + american => f = fontusa; + * => f = font; + } + + update <- = Pause; + + txty := pt.y - (len cell - 1); + for (cellix := len cell - 1; cellix >= 0; cellix--) { + y := pt.y - cellix; + + if (y < 0) + continue; + if (y >= nrows) + break; + + col0 := y * ncols; + colbase := pt.y * ncols; + + if (delims && !(attr & attrD)) { + # seek back for a delimiter + mask : int; + delimattr := modeattr; + + # semigraphics only inherit attrC from current field + if (charset == semigraphic) + mask = attrC; + else + mask = bgMask | attrC | attrL; + + for (ix := pt.x-1; ix >= 0; ix--) { + cix := ix + col0; + if (cellmap[cix].attr & attrD) { + if (cellmap[cix].font == fontg1 && f != fontg1) + # don't carry over attrL from semigraphic field + mask &= ~attrL; + + delimattr = cellmap[cix].attr; + break; + } + } + attr = (attr & ~mask) | (delimattr & mask); + + # semigraphics validate background colour + if (charset == semigraphic) + attr |= attrD; + } + + strlen := len cell[0] * len str; + gfxwidth := cellsize.x * strlen; + srco := Point(pt.x*cellsize.x, y*cellsize.y); + + if (insert) { + # copy existing cells and display to new position + if (pt.x + strlen < ncols) { + for (destx := ncols -1; destx > pt.x; destx--) { + srcx := destx - strlen; + if (srcx < 0) + break; + cellmap[col0 + destx] = cellmap[col0 + srcx]; + } + + # let draw() do the clipping for us + dsto := Point(srco.x + gfxwidth, srco.y); + dstr := Rect((dsto.x, srco.y), (ncols * cellsize.x, srco.y + cellsize.y)); + + frames[0].clipr = frames[0].r; + frames[1].clipr = frames[1].r; + frames[0].draw(dstr, frames[0], nil, srco); + frames[1].draw(dstr, frames[1], nil, srco); + if (modbbox.dx() == 0) + modbbox = dstr; + else + modbbox = boundingrect(modbbox, dstr); + } + } + + # copy-in new string + x := pt.x; + for (strix := 0; x < ncols && strix < len str; strix++) { + for (clipix := 0; clipix < len cell[cellix]; (x, clipix) = (x+1, clipix+1)) { + if (x < 0) + continue; + if (x >= ncols) + break; + cmix := col0 + x; + cellmap[cmix].font = f; + cellmap[cmix].ch = str[strix]; + cellmap[cmix].attr = attr; + cellmap[cmix].clipmod = cell[cellix][clipix]; + } + } + + # render the new string + txto := Point(srco.x, txty * cellsize.y); + strr := Rect(srco, (srco.x + gfxwidth, srco.y + cellsize.y)); + if (strr.max.x > ncols * cellsize.x) + strr.max.x = ncols * cellsize.x; + + drawstr(str, f, strr, txto, attr); + + # redraw remainder of line until find cell not needing redraw + + # this could be optimised by + # spotting strings with same attrs, font and clipmod pairs + # and write out whole string rather than processing + # a char at a time + + attr2 := attr; + mask := bgMask | attrC | attrL; + s := ""; + for (; delims && x < ncols; x++) { + if (x < 0) + continue; + newattr := cellmap[col0 + x].attr; + + if (cellmap[col0 + x].font == fontg1) { + # semigraphics act as bg colour delimiter + attr2 = (attr2 & ~bgMask) | (newattr & bgMask); + mask &= ~attrL; + } else + if (newattr & attrD) + break; + + if ((attr2 & mask) == (newattr & mask)) + break; + newattr = (newattr & ~mask) | (attr2 & mask); + cellmap[col0 + x].attr = newattr; + s[0] = cellmap[col0 + x].ch; + (cx, cy) := cellmap[col0 + x].clipmod; + f2 := cellmap[col0 + x].font; + + cellpos := Point(x * cellsize.x, y * cellsize.y); + clipr := Rect(cellpos, cellpos.add(Point(cellsize.x, cellsize.y))); + drawpt := cellpos.sub(Point(cx*cellsize.x, cy*cellsize.y)); + drawstr(s, f2, clipr, drawpt, newattr); + } + } + update <- = Continue; +} + +Scroll(topline, nlines : int) +{ + if (cellmap == nil || nlines == 0) + return; + + blankr : Rect; + scr := Rect((0,topline * cellsize.y), (ncols * cellsize.x, nrows * cellsize.y)); + + update <- = Pause; + + frames[0].clipr = scr; + frames[1].clipr = scr; + dstr := scr.subpt(Point(0, nlines * cellsize.y)); + + frames[0].draw(dstr, frames[0], nil, frames[0].clipr.min); + frames[1].draw(dstr, frames[1], nil, frames[1].clipr.min); + + if (nlines > 0) { + # scroll up - copy up from top + if (nlines > nrows - topline) + nlines = nrows - topline; + for (y := nlines + topline; y < nrows; y++) { + srccol0 := y * ncols; + dstcol0 := (y - nlines) * ncols; + cellmap[dstcol0:] = cellmap[srccol0:srccol0+ncols]; + } + for (y = nrows - nlines; y < nrows; y++) { + col0 := y * ncols; + cellmap[col0:] = blankrow; + } + blankr = Rect(Point(0, scr.max.y - (nlines * cellsize.y)), scr.max); + } else { + # scroll down - copy down from bottom + nlines = -nlines; + if (nlines > nrows - topline) + nlines = nrows - topline; + for (y := (nrows - 1) - nlines; y >= topline; y--) { + srccol0 := y * ncols; + dstcol0 := (y + nlines) * ncols; + cellmap[dstcol0:] = cellmap[srccol0:srccol0+ncols]; + } + for (y = topline; y < nlines; y++) { + col0 := y * ncols; + cellmap[col0:] = blankrow; + } + blankr = Rect(scr.min, (scr.max.x, scr.min.y + (nlines * cellsize.y))); + } + frames[0].draw(blankr, colours[0], nil, Point(0,0)); + frames[1].draw(blankr, colours[0], nil, Point(0,0)); + if (modbbox.dx() == 0) + modbbox = scr; + else + modbbox = boundingrect(modbbox, scr); + update <- = Continue; +} + +Reveal(show : int) +{ + showC = show; + if (cellmap == nil) + return; + + update <- = Pause; + for (y := 0; y < nrows; y++) { + col0 := y * ncols; + for (x := 0; x < ncols; x++) { + attr := cellmap[col0+x].attr; + if (!(attr & attrC)) + continue; + + s := ""; + s[0] = cellmap[col0 + x].ch; + (cx, cy) := cellmap[col0 + x].clipmod; + f := cellmap[col0 + x].font; + cellpos := Point(x * cellsize.x, y * cellsize.y); + clipr := Rect(cellpos, cellpos.add(Point(cellsize.x, cellsize.y))); + drawpt := cellpos.sub(Point(cx*cellsize.x, cy*cellsize.y)); + + drawstr(s, f, clipr, drawpt, attr); + } + } + update <- = Continue; +} + +# expects that pt.x already normalized +wordchar(pt : Point) : int +{ + if (pt.x < 0 || pt.x >= ncols) + return 0; + if (pt.y < 0 || pt.y >= nrows) + return 0; + + col0 := pt.y * ncols; + c := cellmap[col0 + pt.x]; + + if (c.attr & attrC && !showC) + # don't let clicking on screen 'reveal' concealed chars! + return 0; + + if (c.font == fontg1) + return 0; + + if (c.attr & attrW) { + # check for both parts of character + (modx, nil) := c.clipmod; + if (modx == 1) { + # rhs of char - check lhs is the same + if (pt.x <= 0) + return 0; + lhc := cellmap[col0 + pt.x-1]; + (lhmodx, nil) := lhc.clipmod; + if (!((lhc.attr & attrW) && (lhc.font == c.font) && (lhc.ch == c.ch) && (lhmodx == 0))) + return 0; + } else { + # lhs of char - check rhs is the same + if (pt.x >= ncols - 1) + return 0; + rhc := cellmap[col0 + pt.x + 1]; + (rhmodx, nil) := rhc.clipmod; + if (!((rhc.attr & attrW) && (rhc.font == c.font) && (rhc.ch == c.ch) && (rhmodx == 1))) + return 0; + } + } + if (c.ch >= 16r30 && c.ch <= 16r39) + # digits + return 1; + if (c.ch >= 16r41 && c.ch <= 16r5a) + # capitals + return 1; + if (c.ch >= 16r61 && c.ch <= 16r7a) + # lowercase + return 1; + if (c.ch == '*' || c.ch == '/') + return 1; + return 0; +} + +GetWord(gfxpt : Point) : string +{ + if (cellmap == nil) + return nil; + + scr := Rect((0,0), (ncols * cellsize.x, nrows * cellsize.y)); + gfxpt = gfxpt.sub(winoff); + + if (!gfxpt.in(scr)) + return nil; + + x := gfxpt.x / cellsize.x; + y := gfxpt.y / cellsize.y; + col0 := y * ncols; + + s := ""; + + # seek back + for (sx := x; sx >= 0; sx--) + if (!wordchar(Point(sx, y))) + break; + + if (sx++ == x) + return nil; + + # seek forward, constructing s + for (; sx < ncols; sx++) { + if (!wordchar(Point(sx, y))) + break; + c := cellmap[col0 + sx]; + s[len s] = c.ch; + if (c.attr & attrW) + sx++; + } + return s; +} + +Refresh() +{ + if (window == nil || modbbox.dx() == 0) + return; + + if (update != nil) + update <- = Redraw; +} + +framecolours(attr : int) : (ref Image, ref Image, ref Image, ref Image) +{ + fg : ref Image; + fgcol := attr & fgMask; + if (fgcol == fgWhite && attr & attrB) + fg = bright; + else + fg = colours[fgcol / fgBase]; + + bg : ref Image; + bgcol := attr & bgMask; + if (bgcol == bgWhite && attr & attrB) + bg = bright; + else + bg = colours[bgcol / bgBase]; + + (fg0, fg1) := (fg, fg); + (bg0, bg1) := (bg, bg); + + if (attr & attrP) + (fg0, bg0, fg1, bg1) = (bg1, fg1, bg0, fg0); + + if (attr & attrF) { + fg0 = fg; + fg1 = bg; + } + + if ((attr & attrC) && !showC) + (fg0, fg1) = (bg0, bg1); + return (fg0, bg0, fg1, bg1); +} + +kill(pid : int) +{ + prog := "/prog/" + string pid + "/ctl"; + fd := sys->open(prog, Sys->OWRITE); + if (fd != nil) { + cmd := array of byte "kill"; + sys->write(fd, cmd, len cmd); + } +} + +timer(ms : int, pc, tick : chan of int) +{ + pc <- = sys->pctl(0, nil); + for (;;) { + sys->sleep(ms); + tick <- = 1; + } +} + +# Update() commands +Redraw, Pause, Continue, CursorSet, QuitUpdate : con iota; + +Update(cmd : chan of int) +{ + flashtick := chan of int; + cursortick := chan of int; + pc := chan of int; + spawn timer(1000, pc, flashtick); + flashpid := <- pc; + spawn timer(500, pc, cursortick); + cursorpid := <- pc; + + cursor : Point; + showcursor := 0; + cursoron := 0; + quit := 0; + nultick := chan of int; + flashchan := nultick; + pcount := 1; + fgframe := 0; + + for (;!quit ;) alt { + c := <- cmd => + case c { + Redraw => + frames[0].clipr = frames[0].r; + frames[1].clipr = frames[1].r; + r := modbbox.addpt(winoff); + window.draw(r.addpt(window.r.min), frames[fgframe], nil, modbbox.min); + if (showcursor && cursoron) + drawcursor(cursor, fgframe, 1); + modbbox = Rect((0,0),(0,0)); + + Pause => + if (pcount++ == 0) + flashchan = nultick; + + Continue => + pcount--; + if (pcount == 0) + flashchan = flashtick; + + QuitUpdate => + quit++; + + CursorSet => + frames[0].clipr = frames[0].r; + frames[1].clipr = frames[1].r; + if (showcursor && cursoron) + drawcursor(cursor, fgframe, 0); + cursoron = 0; + if (curpos.x < 0 || curpos.x >= ncols || curpos.y < 0 || curpos.y >= nrows) + showcursor = 0; + else { + cursor = curpos; + showcursor = 1; + drawcursor(cursor, fgframe, 1); + cursoron = 1; + } + } + + <- flashchan => + # flip displays... + fgframe = (fgframe + 1 ) % 2; + modbbox = Rect((0,0),(0,0)); + frames[0].clipr = frames[0].r; + frames[1].clipr = frames[1].r; + window.draw(window.r.addpt(winoff), frames[fgframe], nil, Point(0,0)); + if (showcursor && cursoron) + drawcursor(cursor, fgframe, 1); + + <- cursortick => + if (showcursor) { + cursoron = !cursoron; + drawcursor(cursor, fgframe, cursoron); + } + } + kill(flashpid); + kill(cursorpid); +} + + +drawstr(s : string, f : ref Font, clipr : Rect, drawpt : Point, attr : int) +{ + (fg0, bg0, fg1, bg1) := framecolours(attr); + frames[0].clipr = clipr; + frames[1].clipr = clipr; + frames[0].draw(clipr, bg0, nil, Point(0,0)); + frames[1].draw(clipr, bg1, nil, Point(0,0)); + ulrect : Rect; + ul := (attr & attrL) && ! (attr & attrD); + + if (f != nil) { + if (ul) + ulrect = Rect((drawpt.x, drawpt.y + f.height - ulheight), (drawpt.x + clipr.dx(), drawpt.y + f.height)); + if (fg0 != bg0) { + frames[0].text(drawpt, fg0, Point(0,0), f, s); + if (ul) + frames[0].draw(ulrect, fg0, nil, Point(0,0)); + } + if (fg1 != bg1) { + frames[1].text(drawpt, fg1, Point(0,0), f, s); + if (ul) + frames[1].draw(ulrect, fg1, nil, Point(0,0)); + } + } + if (modbbox.dx() == 0) + modbbox = clipr; + else + modbbox = boundingrect(modbbox, clipr); +} + +boundingrect(r1, r2 : Rect) : Rect +{ + if (r2.min.x < r1.min.x) + r1.min.x = r2.min.x; + if (r2.min.y < r1.min.y) + r1.min.y = r2.min.y; + if (r2.max.x > r1.max.x) + r1.max.x = r2.max.x; + if (r2.max.y > r1.max.y) + r1.max.y = r2.max.y; + return r1; +} + +drawcursor(pt : Point, srcix, show : int) +{ + col0 := pt.y * ncols; + c := cellmap[col0 + pt.x]; + s := ""; + + s[0] = c.ch; + (cx, cy) := c.clipmod; + cellpos := Point(pt.x * cellsize.x, pt.y * cellsize.y); + clipr := Rect(cellpos, cellpos.add(Point(cellsize.x, cellsize.y))); + clipr = clipr.addpt(winoff); + clipr = clipr.addpt(window.r.min); + + drawpt := cellpos.sub(Point(cx*cellsize.x, cy*cellsize.y)); + drawpt = drawpt.add(winoff); + drawpt = drawpt.add(window.r.min); + + if (!show) { + # copy from appropriate frame buffer + window.draw(clipr, frames[srcix], nil, cellpos); + return; + } + + # invert colours + attr := c.attr ^ (fgMask | bgMask); + + fg, bg : ref Image; + f := c.font; + if (srcix == 0) + (fg, bg, nil, nil) = framecolours(attr); + else + (nil, nil, fg, bg) = framecolours(attr); + + prevclipr := window.clipr; + window.clipr = clipr; + + window.draw(clipr, bg, nil, Point(0,0)); + ulrect : Rect; + ul := (attr & attrL) && ! (attr & attrD); + + if (f != nil) { + if (ul) + ulrect = Rect((drawpt.x, drawpt.y + f.height - ulheight), (drawpt.x + clipr.dx(), drawpt.y + f.height)); + if (fg != bg) { + window.text(drawpt, fg, Point(0,0), f, s); + if (ul) + window.draw(ulrect, fg, nil, Point(0,0)); + } + } + window.clipr = prevclipr; +} diff --git a/appl/examples/minitel/mdisplay.m b/appl/examples/minitel/mdisplay.m new file mode 100644 index 00000000..24d7173f --- /dev/null +++ b/appl/examples/minitel/mdisplay.m @@ -0,0 +1,115 @@ +# +# Minitel display handling module +# +# © 1998 Vita Nuova Limited. All rights reserved. +# + +MDisplay: module +{ + + PATH: con "/dis/wm/minitel/mdisplay.dis"; + + # Available character sets + videotex, semigraphic, french, american : con iota; + + # Fill() attributes bit mask + # + # DL CFPH WBbb bfff + # + # D = Delimiter (set "serial" attributes for rest of line) + # L = Lining (underlined text & "separated" graphics) + # C = Concealing + # F = Flashing + # P = polarity (1 = "inverse") + # H = double height + # W = double width (set H+W for double size) + # B = bright (0: fgwhite=lt.grey, 1: fgwhite=white) + # bbb = background colour + # fff = foreground colour + + fgBase : con 8r001; + bgBase : con 8r010; + attrBase : con 8r100; + + fgMask : con 8r007; + bgMask : con 8r070; + attrMask : con ~0 ^ (fgMask | bgMask); + + fgBlack, fgBlue, fgRed, fgMagenta, + fgGreen, fgCyan, fgYellow, fgWhite : con iota * fgBase; + + bgBlack, bgBlue, bgRed, bgMagenta, + bgGreen, bgCyan, bgYellow, bgWhite : con iota * bgBase; + + attrB, attrW, attrH, attrP, attrF, attrC, attrL, attrD : con attrBase << iota; + + # + # Init (ctxt) : string + # performs general module initialisation + # creates the display window of size/position r using the + # given display context. + # spawns refresh thread + # returns reason for error, or nil on success + # + # Mode(rect, width, height, ulheight, delims, fontpath) : (string, ref Draw->Image) + # set/reset display to given rectangle and character grid size + # ulheight == underline height from bottom of character cell + # if delims != 0 then "field" attrs for Put() are derived from + # preceding delimiter otherwise Put() attrs are taken as is + # + # load fonts: + # <fontpath> videotex + # <fontpath>w videotex double width + # <fontpath>h videotex double height + # <fontpath>s videotex double size + # <fontpath>g1 videotex semigraphics + # <fontpath>fr french character set + # <fontpath>usa american character set + # Note: + # charset g2 is not directly supported, instead the symbols + # of g2 that do not appear in g0 (standard videotex charset) + # are available in videotex font using unicode char codes. + # Therefore controlling s/w must map g2 codes to unicode. + # + # Cursor(pt) + # move cursor to given position + # row number (y) is 0 based + # column number (x) is 1 based + # move cursor off-screen to hide + # + # Put(str, pt, charset, attr, insert) + # render string str at position pt in the given character set + # using specified attributes. + # if insert is non-zero, all characters from given position to end + # of line are moved right by len str positions. + # + # Scroll(topline, nlines) + # move the whole displayby nlines (+ve = scroll up). + # exposed lines of display are set to spaces rendered with + # the current mode attribute flags. + # scroll region is from topline to bottom of display + # + # Reveal(reveal) + # reveal/hide all chars affected by Concealing attribute. + # + # Refresh() + # force screen update + # + # GetWord(pt) : string + # returns on-screen word at given graphics co-ords + # returns nil if blank or semigraphic charset at location + # + # Quit() + # undo Init() + + + Init : fn (ctxt : ref Draw->Context) : string; + Mode : fn (r : Draw->Rect, width, height, ulh, attr : int, fontpath : string) : (string, ref Draw->Image); + Cursor : fn (pt : Draw->Point); + Put : fn (str : string, pt : Draw->Point, chset, attr, insert : int); + Scroll : fn (topline, nlines : int); + Reveal : fn (reveal : int); + Refresh : fn (); + GetWord : fn (gfxpt : Draw->Point) : string; + Quit : fn (); +}; 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]); +} diff --git a/appl/examples/minitel/miniterm.m b/appl/examples/minitel/miniterm.m new file mode 100644 index 00000000..e0345f81 --- /dev/null +++ b/appl/examples/minitel/miniterm.m @@ -0,0 +1,120 @@ +# +# Copyright © 1998 Vita Nuova Limited. All rights reserved. +# + +# Common control bytes +NUL: con 16r00; +SOH: con 16r01; +EOT: con 16r04; +ENQ: con 16r05; +BEL: con 16r07; +BS: con 16r08; +HT: con 16r09; +LF: con 16r0a; +VT: con 16r0b; +FF: con 16r0c; +CR: con 16r0d; +SO: con 16r0e; +SI: con 16r0f; +DLE: con 16r10; +CON: con 16r11; +XON: con 16r11; +REP: con 16r12; +SEP: con 16r13; +XOFF: con 16r13; +COFF: con 16r14; +NACK: con 16r15; +SYN: con 16r16; +CAN: con 16r18; +SS2: con 16r19; +SUB: con 16r1a; +ESC: con 16r1b; +SS3: con 16r1d; +RS: con 16r1e; +US: con 16r1f; + +SP: con 16r20; +DEL: con 16r7f; + +# Minitel Protocol - some are duplicated (chapter 6) +ASCII: con 16r31; +MIXED: con 16r32; +ETEN: con 16r41; +C0: con 16r43; +SCROLLING: con 16r43; +PROCEDURE: con 16r44; +LOWERCASE: con 16r45; +OFF: con 16r60; +ON: con 16r61; +TO: con 16r62; +FROM: con 16r63; +NOBROADCAST: con 16r64; +BROADCAST: con 16r65; +NONRETURN: con 16r64; +RETURN: con 16r65; +TRANSPARENCY: con 16r66; +DISCONNECT: con 16r67; +CONNECT: con 16r68; +START: con 16r69; +STOP: con 16r6a; +KEYBOARDSTATUS: con 16r72; +REPKEYBOARDSTATUS: con 16r73; +FUNCTIONINGSTATUS: con 16r72; +REPFUNCTIONINGSTATUS: con 16r73; +EXCHANGERATESTATUS: con 16r74; +REPEXCHANGERATESTATUS: con 16r75; +PROTOCOLSTATUS: con 16r76; +REPPROTOCOLSTATUS: con 16r77; +SETRAM1: con 16r78; +SETRAM2: con 16r79; +ENQROM: con 16r7b; +COPY: con 16r7c; +ASCII1: con 16r7d; +MIXED1: con 16r7d; +MIXED2: con 16r7e; +RESET: con 16r7f; + +# Module send and receive codes (chapter 6) +TxScreen: con 16r50; +TxKeyb: con 16r51; +TxModem: con 16r52; +TxSocket: con 16r53; +RxScreen: con 16r58; +RxKeyb: con 16r59; +RxModem: con 16r5a; +RxSocket: con 16r5b; + +# Internal Event.Eproto command constants +Cplay, # for testing +Cconnect, # e.s contains the address to dial +Cdisconnect, # +Crequestecp, # ask server to start ecp +Creset, # reset module +Cstartecp, # start error correction +Cstopecp, # stop error correction +Cproto, # minitel protocol +Ccursor, # update screen cursor +Cindicators, # update row 0 indicators + +# softmodem bug: Cscreenoff, Cscreenon +Cscreenoff, # screen: ignore data +Cscreenon, # screen: don't ignore data + +Clast + : con iota; + +# Special keys - hardware returned byte +KupPC: con 16r0203; # pc emu +KdownPC: con 16r0204; # pc emu +Kup: con 16rE012; +Kdown: con 16rE013; +Kenter: con 16r000a; +Kback: con 16r0008; +Kesc: con 16r001b; +KF1: con 16rE041; +KF2: con 16rE042; +KF3: con 16rE043; +KF4: con 16rE044; +KF13: con 16rE04D; + + diff --git a/appl/examples/minitel/mkfile b/appl/examples/minitel/mkfile new file mode 100644 index 00000000..16f816a6 --- /dev/null +++ b/appl/examples/minitel/mkfile @@ -0,0 +1,24 @@ +<../../../mkconfig + +TARG=\ + mdisplay.dis\ + miniterm.dis\ + swkeyb.dis\ + +MODULES=\ + mdisplay.m\ + miniterm.m\ + event.m\ + swkeyb.m\ + +SYSMODULES=\ + arg.m\ + sys.m\ + debug.m\ + draw.m\ + tk.m\ + wmlib.m\ + +DISBIN=$ROOT/dis/wm/minitel + +<$ROOT/mkfiles/mkdis diff --git a/appl/examples/minitel/modem.b b/appl/examples/minitel/modem.b new file mode 100644 index 00000000..b7a21c1d --- /dev/null +++ b/appl/examples/minitel/modem.b @@ -0,0 +1,620 @@ +# +# Copyright © 1998 Vita Nuova Limited. All rights reserved. +# + +#modem states for direct connection +MSstart, MSdialing, MSconnected, MSdisconnecting, + +# special features +Ecp # error correction + : con (1 << iota); + +Ecplen: con 17; # error correction block length: data[15], crc, validation (=0) + +Modem: adt { + m: ref Module; # common attributes + in: chan of ref Event; + + connect: int; # None, Direct, Network + state: int; # modem dialing state + saved: string; # response, so far (direct dial) + initstr: string; # softmodem init string (direct dial) + dialstr: string; # softmodem dial string (direct dial) + lastdialstr: string; + + spec: int; # special features + fd: ref Sys->FD; # modem data file, if != nil + cfd: ref Sys->FD; # modem ctl file, if != nil (direct dial only) + devpath: string; # path to the modem; + avail: array of byte; # already read + rd: chan of array of byte; # reader -> rd + pid: int; # reader pid if != 0 + + seq: int; # ECP block sequence number + waitsyn: int; # awaiting restart SYN SYN ... sequence + errforce: int; + addparity: int; # must add parity to outgoing data + + init: fn(m: self ref Modem, connect: int, initstr, dialstr: string); + reset: fn(m: self ref Modem); + run: fn(m: self ref Modem); + quit: fn(m: self ref Modem); + runstate: fn(m: self ref Modem, data: array of byte); + write: fn(m: self ref Modem, data: array of byte):int; # to network + reader: fn(m: self ref Modem, pidc: chan of int); +}; + +partab: array of byte; + +dump(a: array of byte, n: int): string +{ + s := sys->sprint("[%d]", n); + for(i := 0; i < n; i++) + s += sys->sprint(" %.2x", int a[i]); + return s; +} + +Modem.init(m: self ref Modem, connect: int, initstr, dialstr: string) +{ + partab = array[128] of byte; + for(c := 0; c < 128; c++) + if(parity(c)) + partab[c] = byte (c | 16r80); + else + partab[c] = byte c; + m.in = chan of ref Event; + m.connect = connect; + m.state = MSstart; + m.initstr = initstr; + m.dialstr = dialstr; + m.pid = 0; + m.spec = 0; + m.seq = 0; + m.waitsyn = 0; + m.errforce = 0; + m.addparity = 0; + m.avail = array[0] of byte; + m.rd = chan of array of byte; + m.reset(); +} + +Modem.reset(m: self ref Modem) +{ + m.m = ref Module(Pscreen, 0); +} + +Modem.run(m: self ref Modem) +{ + if(m.dialstr != nil) + send(ref Event.Eproto(Pmodem, Mmodem, Cconnect, "", 0,0,0)); +Runloop: + for(;;){ + alt { + ev := <- m.in => + pick e := ev { + Equit => + break Runloop; + Edata => + if(debug['m'] > 0) + fprint(stderr, "Modem <- %s\n", e.str()); + m.write(e.data); + if(T.state == Local || T.spec & Echo) { # loopback + if(e.from == Mkeyb) { + send(ref Event.Eproto(Pscreen, Mkeyb, Ccursor, "", 0,0,0)); + send(ref Event.Edata(Pscreen, Mkeyb, e.data)); + } + } + Eproto => + case e.cmd { + Creset => + m.reset(); + Cconnect => + if(m.pid != 0) + break; + m.addparity = 1; + T.state = Connecting; + send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0)); + + case m.connect { + Direct => + S.msg("Appel "+m.dialstr+" ..."); + dev := "/dev/modem"; + if(openmodem(m, dev) < 0) { + S.msg("Modem non prêt"); + T.state = Local; + send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0)); + break; + } + m.state = MSdialing; + m.saved = ""; + dialout(m); + T.terminalid = TERMINALID2; + Network => + S.msg("Connexion au serveur ..."); + if(debug['m'] > 0 || debug['M'] > 0) + sys->print("dial(%s)\n", m.dialstr); + (ok, cx) := sys->dial(m.dialstr, ""); + if (ok == -1){ + S.msg("Echec de la connexion"); + T.state = Local; + send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0)); + if(debug['m'] > 0) + sys->print("can't dial %s: %r\n", m.dialstr); + break; + } + m.fd = sys->open(cx.dir + "/data", Sys->ORDWR); + m.cfd = cx.cfd; + if(len m.dialstr >= 3 && m.dialstr[0:3] == "tcp") + m.addparity = 0; # Internet gateway apparently doesn't require parity + if(m.fd != nil) { + S.msg(nil); + m.state = MSconnected; + T.state = Online; + send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0)); + } + T.terminalid = TERMINALID1; + } + if(m.fd != nil) { + pidc := chan of int; + spawn m.reader(pidc); + m.pid = <-pidc; + } + Cdisconnect => + if(m.pid != 0) { + S.msg("Déconnexion ..."); + m.state = MSdisconnecting; + } + if(m.connect == Direct) + hangup(m); + else + nethangup(m); + Cplay => # for testing + case e.s { + "play" => + replay(m); + } + Crequestecp => + if(m.spec & Ecp){ # for testing: if already active, force an error + m.errforce = 1; + break; + } + m.write(array[] of {byte SEP, byte 16r4A}); +sys->print("sending request for ecp\n"); + Cstartecp => + m.spec |= Ecp; + m.seq = 0; # not in spec + m.waitsyn = 0; # not in spec + Cstopecp => + m.spec &= ~Ecp; + * => break; + } + } + b := <- m.rd => + if(debug['m'] > 0){ + fprint(stderr, "Modem -> %s\n", dump(b,len b)); + } + if(b == nil) { + m.pid = 0; + case m.state { + MSdialing => + S.msg("Echec appel"); + MSdisconnecting => + S.msg(nil); + } + m.state = MSstart; + T.state = Local; + send(ref Event.Eproto(Pscreen, Mmodem, Cscreenon, "",0,0,0)); + send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0)); + break; + } + m.runstate(b); + } + } + if(m.pid != 0) + kill(m.pid); + send(nil); +} + +Modem.quit(nil: self ref Modem) +{ +} + +Modem.runstate(m: self ref Modem, data: array of byte) +{ + if(debug['m']>0) + sys->print("runstate %d %s\n", m.state, dump(data, len data)); + case m.state { + MSstart => ; + MSdialing => + for(i:=0; i<len data; i++) { + ch := int data[i]; + if(ch != '\n' && ch != '\r') { + m.saved[len m.saved] = ch; + continue; + } + (code, str) := seenreply(m.saved); + case code { + Noise or Ok => ; + Success => + S.msg(nil); + m.state = MSconnected; + T.state = Online; + send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0)); + Failure => + hangup(m); + S.msg(str); + m.state = MSstart; + T.state = Local; + send(ref Event.Eproto(Pscreen, Mmodem, Cindicators, "",0,0,0)); + } + m.saved = ""; + } + MSconnected => + send(ref Event.Edata(m.m.path, Mmodem, data)); + MSdisconnecting => ; + } +} + +Modem.write(m: self ref Modem, data: array of byte): int +{ + if(m.fd == nil) + return -1; + if(len data == 0) + return 0; + if(m.addparity){ + # unfortunately must copy data to add parity for direct modem connection + pa := array[len data] of byte; + for(i := 0; i<len data; i++) + pa[i] = partab[int data[i] & 16r7F]; + data = pa; + } + if(debug['m']>0) + sys->print("WRITE %s\n", dump(data, len data)); + return sys->write(m.fd, data, len data); +} + +# +# minitel error correction protocol +# +# SYN, SYN, block number start of retransmission +# NUL ignored +# DLE escapes {DLE, SYN, NACK, NUL} +# NACK, block restart request +# + +crctab: array of int; +Crcpoly: con 16r9; # crc7 = x^7+x^3+1 + +# precalculate the CRC7 remainder for all bytes + +mktabs() +{ + crctab = array[256] of int; + for(c := 0; c < 256; c++){ + v := c; + crc := 0; + for(i := 0; i < 8; i++){ + crc <<= 1; # align remainder's MSB with value's + if((v^crc) & 16r80) + crc ^= Crcpoly; + v <<= 1; + } + crctab[c] = (crc<<1) & 16rFE; # pre-align the result to save <<1 later + } +} + +# return the index of the first non-NUL character (the start of a block) + +nextblock(a: array of byte, i: int, n: int): int +{ + for(; i < n; i++) + if(a[i] != byte NUL) + break; + return i; +} + +# return the data in the ecp block in a[0:Ecplen] (return nil for bad format) + +decode(a: array of byte): array of byte +{ + if(debug['M']>0) + sys->print("DECODE: %s\n", dump(a, Ecplen)); + badpar := 0; + oldcrc := int a[Ecplen-2]; + crc := 0; + op := 0; + dle := 0; + for(i:=0; i<Ecplen-2; i++){ # first byte is high-order byte of polynomial (MSB first) + c := int a[i]; + nc := c & 16r7F; # strip parity + if((c^int partab[nc]) & 16r80) + badpar++; + crc = crctab[crc ^ c]; + # collapse DLE sequences + if(!dle){ + if(nc == DLE && i+1 < Ecplen-2){ + dle = 1; + continue; + } + if(nc == NUL) + continue; # strip non-escaped NULs + } + dle = 0; + a[op++] = byte nc; + } + if(badpar){ + if(debug['E'] > 0) + sys->print("bad parity\n"); + return nil; + } + crc = (crc>>1)&16r7F; + if(int partab[crc] != oldcrc){ + if(debug['E'] > 0) + sys->print("bad crc: in %ux got %ux\n", oldcrc, int partab[crc]); + return nil; + } + b := array[op] of byte; + b[0:] = a[0:op]; + if(debug['M'] > 0) + sys->print("OUT: %s [%x :: %x]\n", dump(b,op), crc, oldcrc); + return b; +} + +Modem.reader(m: self ref Modem, pidc: chan of int) +{ + pidc <-= sys->pctl(0, nil); + if(crctab == nil) + mktabs(); + a := array[Sys->ATOMICIO] of byte; + inbuf := 0; + while(m.fd != nil) { + while((n := read(m.fd, a[inbuf:], len a-inbuf)) > 0){ + n += inbuf; + inbuf = 0; + if((m.spec & Ecp) == 0){ + b := array[n] of byte; + for(i := 0; i<n; i++) + b[i] = byte (int a[i] & 16r7F); # strip parity + m.rd <-= b; + }else{ + #sys->print("IN: %s\n", dump(a,n)); + i := 0; + if(m.waitsyn){ + sys->print("seeking SYN #%x\n", m.seq); + syn := byte (SYN | 16r80); + lim := n-3; + for(; i <= lim; i++) + if(a[i] == syn && a[i+1] == syn && (int a[i+2]&16r0F) == m.seq){ + i += 3; + m.waitsyn = 0; + sys->print("found SYN #%x@%d\n", m.seq, i-3); + break; + } + } + lim := n-Ecplen; + for(; (i = nextblock(a, i, n)) <= lim; i += Ecplen){ + b := decode(a[i:]); + if(m.errforce || b == nil){ + m.errforce = 0; + b = array[2] of byte; + b[0] = byte NACK; + b[1] = byte (m.seq | 16r40); + sys->print("NACK #%x\n", m.seq); + m.write(b); + m.waitsyn = 1; + i = n; # discard rest of block + break; + } + m.seq = (m.seq+1) & 16rF; # mod 16 counter + m.rd <-= b; + } + if(i < n){ + a[0:] = a[i:n]; + inbuf = n-i; + } + } + } + if(n <= 0) + break; + } +# m.fd = nil; + m.rd <-= nil; +} + +playfd: ref Sys->FD; +in_code, in_char: con iota; + +replay(m: ref Modem) +{ + buf := array[8192] of byte; + DMAX: con 10; + d := 0; + da := array[DMAX] of byte; + playfd = nil; + if(playfd == nil) + playfd = sys->open("minitel.txt", Sys->OREAD); + if(playfd == nil) + return; + nl := 1; + discard := 1; + state := in_code; + hs := ""; + start := 0; +mainloop: + for(;;) { + n := sys->read(playfd, buf, len buf); + if(n <= 0) + break; + for(i:=0; i<n; i++) { + ch := int buf[i]; + if(nl) + case ch { + '>' => discard = 0; + '<' => discard = 1; + if(start) + sys->sleep(1000); + '{' => start = 1; + '}' => break mainloop; + } + if(ch == '\n') + nl = 1; + else + nl = 0; + if(discard) + continue; + if(!start) + continue; + if(state == in_code && ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z'))) + hs[len hs] = ch; + else if(ch == '(') { + state = in_char; + (v, nil) := toint(hs, 16); + da[d++] = byte v; + if(d == DMAX) { + send(ref Event.Edata(m.m.path, Mmodem, da)); + d = 0; + da = array[DMAX] of byte; + sys->sleep(50); + } + hs = ""; + }else if(ch == ')') + state = in_code; + } + } + playfd = nil; + +} + +kill(pid : int) +{ + prog := "#p/" + string pid + "/ctl"; + fd := sys->open(prog, Sys->OWRITE); + if (fd != nil) { + cmd := array of byte "kill"; + sys->write(fd, cmd, len cmd); + } +} + + +# Modem stuff + + +# modem return codes +Ok, Success, Failure, Noise, Found: con iota; + +# +# modem return messages +# +Msg: adt { + text: string; + trans: string; + code: int; +}; + +msgs: array of Msg = array [] of { + ("OK", "Ok", Ok), + ("NO CARRIER", "No carrier", Failure), + ("ERROR", "Bad modem command", Failure), + ("NO DIALTONE", "No dial tone", Failure), + ("BUSY", "Busy tone", Failure), + ("NO ANSWER", "No answer", Failure), + ("CONNECT", "", Success), +}; + +msend(m: ref Modem, x: string): int +{ + a := array of byte x; + return sys->write(m.fd, a, len a); +} + +# +# apply a string of commands to modem +# +apply(m: ref Modem, s: string): int +{ + buf := ""; + for(i := 0; i < len s; i++){ + c := s[i]; + buf[len buf] = c; # assume no Unicode + if(c == '\r' || i == (len s -1)){ + if(c != '\r') + buf[len buf] = '\r'; + if(msend(m, buf) < 0) + return Failure; + buf = ""; + } + } + return Ok; +} + +openmodem(m: ref Modem, dev: string): int +{ + m.fd = sys->open(dev, Sys->ORDWR); + m.cfd = sys->open(dev+"ctl", Sys->ORDWR); + if(m.fd == nil || m.cfd == nil) + return -1; +# hangup(m); +# m.fd = sys->open(dev, Sys->ORDWR); +# m.cfd = sys->open(dev+"ctl", Sys->ORDWR); +# if(m.fd == nil || m.cfd == nil) +# return -1; + return 0; +} + +hangup(m: ref Modem) +{ + sys->sleep(1020); + msend(m, "+++"); + sys->sleep(1020); + apply(m, "ATH0"); + m.fd = nil; +# sys->write(m.cfd, array of byte "f", 1); + sys->write(m.cfd, array of byte "h", 1); + m.cfd = nil; + # HACK: shannon softmodem "off-hook" bug fix + sys->open("/dev/modem", Sys->OWRITE); +} + +nethangup(m: ref Modem) +{ + m.fd = nil; + sys->write(m.cfd, array of byte "hangup", 6); + m.cfd = nil; +} + + +# +# check `s' for a known reply or `substr' +# +seenreply(s: string): (int, string) +{ + for(k := 0; k < len msgs; k++) + if(len s >= len msgs[k].text && s[0:len msgs[k].text] == msgs[k].text) { + return (msgs[k].code, msgs[k].trans); + } + return (Noise, s); +} + +contains(s, t: string): int +{ + if(t == nil) + return 1; + if(s == nil) + return 0; + n := len t; + for(i := 0; i+n <= len s; i++) + if(s[i:i+n] == t) + return 1; + return 0; +} + +dialout(m: ref Modem) +{ + if(m.initstr != nil) + apply(m, "AT"+m.initstr); + if(m.dialstr != nil) { + apply(m, "ATD"+m.dialstr); + m.lastdialstr = m.dialstr; + m.dialstr = nil; + } +} diff --git a/appl/examples/minitel/screen.b b/appl/examples/minitel/screen.b new file mode 100644 index 00000000..4313d48d --- /dev/null +++ b/appl/examples/minitel/screen.b @@ -0,0 +1,1610 @@ +# +# Occasional references are made to sections and tables in the +# France Telecom Minitel specification +# +# Copyright © 1998 Vita Nuova Limited. All rights reserved. +# + +include "mdisplay.m"; + +disp: MDisplay; + +Rect, Point : import Draw; + +# display character sets +videotex, semigraphic, french, american :import MDisplay; + +# display foreground colour attributes +fgBlack, fgBlue, fgRed, fgMagenta, +fgGreen, fgCyan, fgYellow, fgWhite :import MDisplay; + +# display background colour attributes +bgBlack, bgBlue, bgRed, bgMagenta, +bgGreen, bgCyan, bgYellow, bgWhite :import MDisplay; + +fgMask, bgMask : import MDisplay; + +# display formatting attributes +attrB, attrW, attrH, attrP, attrF, attrC, attrL, attrD :import MDisplay; + +# Initial attributes - white on black +ATTR0: con fgWhite|bgBlack&~(attrB|attrW|attrH|attrP|attrF|attrC|attrL|attrD); + +# special features +Cursor, Scroll, Insert + : con (1 << iota); + +# Screen states +Sstart, Sss2, Sesc, Srepeat, Saccent, Scsi0, Scsi1, Sus0, Sus1, Sskip, +Siso2022, Siso6429, Stransparent, Sdrcs, Sconceal, Swaitfor + : con iota; + +# Filter states +FSstart, FSesc, FSsep, FS6429, FS2022: con iota; + +Screen: adt { + m: ref Module; # common attributes + ctxt: ref Draw->Context; + in: chan of ref Event; # from the terminal + + image: ref Draw->Image; # Mdisplay image + dispr40, dispr80: Rect; # 40 and 80 column display region + oldtmode: int; # old terminal mode + rows: int; # number of screen rows (25 for minitel) + cols: int; # number of screen cols (40 or 80) + cset: int; # current display charset + + pos: Point; # current writing position (x:1, y:0) + attr: int; # display attribute set + spec: int; # special features + savepos: Point; # `pos' before moving to row zero + saveattr: int; # `attr' before moving to row zero + savech: int; # last character `Put' + delimit: int; # attr changed, make next space a delimiter + cursor: int; # update cursor soon + + state: int; # recogniser state + a0: int; # recogniser arg 0 + a1: int; # recogniser arg 1 + + fstate: int; # filter state + fsaved: array of byte; # filter `chars so far' + badp: int; # filter because of bad parameter + + ignoredata: int; # ignore data from + + init: fn(s: self ref Screen, ctxt: ref Draw->Context, r40, r80: Rect); + reset: fn(s: self ref Screen); + run: fn(s: self ref Screen); + quit: fn(s: self ref Screen); + setmode: fn(s: self ref Screen, tmode: int); + runstate: fn(s: self ref Screen, data: array of byte); + put: fn(s: self ref Screen, str: string); + msg: fn(s: self ref Screen, str: string); +}; + +Screen.init(s: self ref Screen, ctxt: ref Draw->Context, r40, r80: Rect) +{ + disp = load MDisplay MDisplay->PATH; + if(disp == nil) + fatal("can't load the display module: "+MDisplay->PATH); + + s.m = ref Module(0, 0); + s.ctxt = ctxt; + s.dispr40 = r40; + s.dispr80 = r80; + s.oldtmode = -1; + s.in = chan of ref Event; + disp->Init(s.ctxt); + s.reset(); + s.pos = Point(1, 1); + s.savech = 0; + s.cursor = 1; + s.ignoredata = 0; + s.fstate = FSstart; +} + +Screen.reset(s: self ref Screen) +{ + s.setmode(T.mode); + indicators(s); + s.state = Sstart; +} + +Screen.run(s: self ref Screen) +{ +Runloop: + for(;;) alt { + ev := <- s.in => + pick e := ev { + Equit => + break Runloop; + Eproto => + case e.cmd { + Creset => + s.reset(); + Cproto => + case e.a0 { + START => + case e.a1 { + SCROLLING => + s.spec |= Scroll; + } + STOP => + case e.a1 { + SCROLLING => + s.spec &= ~Scroll; + } + MIXED => + case e.a1 { + MIXED1 => # videotex -> mixed + if(T.mode != Mixed) + s.setmode(Mixed); + T.mode = Mixed; + MIXED2 => # mixed -> videotex + if(T.mode != Videotex) + s.setmode(Videotex); + T.mode = Videotex; + } + } + Ccursor => # update the cursor soon + s.cursor = 1; + Cindicators => + indicators(s); + Cscreenoff => + s.ignoredata = 1; + s.state = Sstart; + Cscreenon => + s.ignoredata = 0; + * => break; + } + Edata => + if(s.ignoredata) + continue; + oldpos := s.pos; + oldspec := s.spec; + da := filter(s, e.data); + while(len da > 0) { + s.runstate(da[0]); + da = da[1:]; + } + + if(s.pos.x != oldpos.x || s.pos.y != oldpos.y || (s.spec&Cursor)^(oldspec&Cursor)) + s.cursor = 1; + if(s.cursor) { + if(s.spec & Cursor) + disp->Cursor(s.pos); + else + disp->Cursor(Point(-1,-1)); + s.cursor = 0; + refresh(); + } else if(e.from == Mkeyb) + refresh(); + } + } + send(nil); +} + +# row0 indicators (1.2.2) +indicators(s: ref Screen) +{ + col: int; + ch: string; + + attr := fgWhite|bgBlack; + case T.state { + Local => + ch = "F"; + Connecting => + ch = "C"; + attr |= attrF; + Online => + ch = "C"; + } + if(s.cols == 40) { + col = 39; + attr |= attrP; + } else + col = 77; + disp->Put(ch, Point(col, 0), videotex, attr, 0); +} + +Screen.setmode(s: self ref Screen, tmode: int) +{ + dispr: Rect; + delims: int; + ulheight: int; + s.rows = 25; + s.spec = 0; + s.attr = s.saveattr = ATTR0; + s.delimit = 0; + s.pos = s.savepos = Point(-1, -1); + s.cursor = 1; + case tmode { + Videotex => + s.cset = videotex; + s.cols = 40; + dispr = s.dispr40; + delims = 1; + ulheight = 2; + s.pos = Point(1,1); + s.spec &= ~Cursor; + Mixed => +# s.cset = french; + s.cset = videotex; + s.cols = 80; + dispr = s.dispr80; + delims = 0; + ulheight = 1; + s.spec |= Scroll; + s.pos = Point(1, 1); + Ascii => + s.cset = french; + s.cols = 80; + dispr = s.dispr80; + delims = 0; + ulheight = 1; + }; + if(tmode != s.oldtmode) { + (nil, s.image) = disp->Mode(((0,0),(0,0)), 0, 0, 0, 0, nil); + T.layout(s.cols); + fontpath := sprint("/fonts/minitel/f%dx%d", s.cols, s.rows); + (nil, s.image) = disp->Mode(dispr, s.cols, s.rows, ulheight, delims, fontpath); + T.setkbmode(tmode); + } + disp->Reveal(0); # concealing enabled (1.2.2) + disp->Cursor(Point(-1,-1)); + s.oldtmode = tmode; +} + +Screen.quit(nil: self ref Screen) +{ + disp->Quit(); +} + +Screen.runstate(s: self ref Screen, data: array of byte) +{ + while(len data > 0) + case T.mode { + Videotex => + data = vstate(s, data); + Mixed => + data = mstate(s, data); + Ascii => + data = astate(s, data); + }; +} + +# process a byte from set C0 +vc0(s: ref Screen, ch: int) +{ + case ch { +# SOH => # not in spec, wait for 16r04 +# s.a0 = 16r04; +# s.state = Swaitfor; + SS2 => + s.state = Sss2; + SYN => + s.state = Sss2; # not in the spec, but acts like SS2 + ESC => + s.state = Sesc; + SO => + s.cset = semigraphic; + s.attr &= ~(attrH|attrW|attrP); # 1.2.4.2 + s.attr &= ~attrL; # 1.2.4.3 + SI => + s.cset = videotex; + s.attr &= ~attrL; # 1.2.4.3 + s.attr &= ~(attrH|attrW|attrP); # some servers seem to assume this too + SEP or SS3 => # 1.2.7 + s.state = Sskip; + BS => + if(s.pos.x == 1) { + if(s.pos.y == 0) + break; + if(s.pos.y == 1) + s.pos.y = s.rows - 1; + else + s.pos.y -= 1; + s.pos.x = s.cols; + } else + s.pos.x -= 1; + HT => + if(s.pos.x == s.cols) { + if(s.pos.y == 0) + break; + if(s.pos.y == s.rows - 1) + s.pos.y = 1; + else + s.pos.y += 1; + s.pos.x = 1; + } else + s.pos.x += 1; + LF => + if(s.pos.y == s.rows - 1) + if(s.spec&Scroll) + scroll(1, 1); + else + s.pos.y = 1; + else if(s.pos.y == 0) { # restore attributes on leaving row zero + s.pos = s.savepos; + s.attr = s.saveattr; + } else + s.pos.y += 1; + VT => + if(s.pos.y == 1) + if(s.spec&Scroll) + scroll(1, -1); + else + s.pos.y = s.rows - 1; + else if(s.pos.y == 0) + break; + else + s.pos.y -= 1; + CR => + s.pos.x = 1; + CAN => + cols := s.cols - s.pos.x + 1; + disp->Put(dup(' ', cols), Point(s.pos.x,s.pos.y), s.cset, s.attr, 0); + US => + # expect US row, col + s.state = Sus0; + FF => + s.cset = videotex; + s.attr = ATTR0; + s.pos = Point(1,1); + s.spec &= ~Cursor; + s.cursor = 1; + clear(s); + RS => + s.cset = videotex; + s.attr = ATTR0; + s.pos = Point(1,1); + s.spec &= ~Cursor; + s.cursor = 1; + CON => + s.spec |= Cursor; + s.cursor = 1; + COFF => + s.spec &= ~Cursor; + s.cursor = 1; + REP => + # repeat + s.state = Srepeat; + NUL => + # padding character - ignore, but may appear anywhere + ; + BEL => + # ah ... + ; + } +} + +# process a byte from the set c1 - introduced by the ESC character +vc1(s: ref Screen, ch: int) +{ + if(ISC0(ch)) { + s.state = Sstart; + vc0(s, ch); + return; + } + if(ch >= 16r20 && ch <= 16r2f) { + if(ch == 16r25) + s.state = Stransparent; + else if(ch == 16r23) + s.state = Sconceal; + else + s.state = Siso2022; + s.a0 = s.a1 = 0; + return; + } + + fg := bg := -1; + case ch { + 16r35 or + 16r36 or + 16r37 => + s.state = Sskip; # skip next char unless C0 + return; + + 16r5b => # CSI sequence + s.a0 = s.a1 = 0; + if(s.pos.y > 0) # 1.2.5.2 + s.state = Scsi0; + return; + + # foreground colour + 16r40 => fg = fgBlack; + 16r41 => fg = fgRed; + 16r42 => fg = fgGreen; + 16r43 => fg = fgYellow; + 16r44 => fg = fgBlue; + 16r45 => fg = fgMagenta; + 16r46 => fg = fgCyan; + 16r47 => fg = fgWhite; + + # background colour + 16r50 => bg = bgBlack; + 16r51 => bg = bgRed; + 16r52 => bg = bgGreen; + 16r53 => bg = bgYellow; + 16r54 => bg = bgBlue; + 16r55 => bg = bgMagenta; + 16r56 => bg = bgCyan; + 16r57 => bg = bgWhite; + + # flashing + 16r48 => s.attr |= attrF; + 16r49 => s.attr &= ~attrF; + + # conceal (serial attribute) + 16r58 => s.attr |= attrC; + s.delimit = 1; + 16r5f => s.attr &= ~attrC; + s.delimit = 1; + + # start lining (+separated graphics) (serial attribute) + 16r5a => s.attr |= attrL; + s.delimit = 1; + 16r59 => s.attr &= ~attrL; + s.delimit = 1; + + # reverse polarity + 16r5d => s.attr |= attrP; + 16r5c => s.attr &= ~attrP; + + # normal size + 16r4c => + s.attr &= ~(attrW|attrH); + + # double height + 16r4d => + if(s.pos.y < 2) + break; + s.attr &= ~(attrW|attrH); + s.attr |= attrH; + + # double width + 16r4e => + if(s.pos.y < 1) + break; + s.attr &= ~(attrW|attrH); + s.attr |= attrW; + + # double size + 16r4f => + if(s.pos.y < 2) + break; + s.attr |= (attrW|attrH); + } + if(fg >= 0) { + s.attr &= ~fgMask; + s.attr |= fg; + } + if(bg >= 0) { + s.attr &= ~bgMask; + s.attr |= bg; + s.delimit = 1; + } + s.state = Sstart; +} + + +# process a SS2 character +vss2(s: ref Screen, ch: int) +{ + if(ISC0(ch)) { + s.state = Sstart; + vc0(s, ch); + return; + } + case ch { + 16r41 or # grave # 5.1.2 + 16r42 or # acute + 16r43 or # circumflex + 16r48 or # umlaut + 16r4b => # cedilla + s.a0 = ch; + s.state = Saccent; + return; + 16r23 => ch = '£'; # Figure 2.8 + 16r24 => ch = '$'; + 16r26 => ch = '#'; + 16r27 => ch = '§'; + 16r2c => ch = 16rc3; # '←'; + 16r2d => ch = 16rc0; # '↑'; + 16r2e => ch = 16rc4; # '→'; + 16r2f => ch = 16rc5; # '↓'; + 16r30 => ch = '°'; + 16r31 => ch = '±'; + 16r38 => ch = '÷'; + 16r3c => ch = '¼'; + 16r3d => ch = '½'; + 16r3e => ch = '¾'; + 16r7a => ch = 'œ'; + 16r6a => ch = 'Œ'; + 16r7b => ch = 'ß'; + } + s.put(tostr(ch)); + s.savech = ch; + s.state = Sstart; +} + +# process CSI functions +vcsi(s: ref Screen, ch: int) +{ + case s.state { + Scsi0 => + case ch { + # move cursor up n rows, stop at top of screen + 'A' => + s.pos.y -= s.a0; + if(s.pos.y < 1) + s.pos.y = 1; + + # move cursor down n rows, stop at bottom of screen + 'B' => + s.pos.y += s.a0; + if(s.pos.y >= s.rows) + s.pos.y = s.rows - 1; + + # move cursor n columns right, stop at edge of screen + 'C' => + s.pos.x += s.a0; + if(s.pos.x > s.cols) + s.pos.x = s.cols; + + # move cursor n columns left, stop at edge of screen + 'D' => + s.pos.x -= s.a0; + if(s.pos.x < 1) + s.pos.x = 1; + + # direct cursor addressing + ';' => + s.state = Scsi1; + return; + + 'J' => + case s.a0 { + # clears from the cursor to the end of the screen inclusive + 0 => + rowclear(s.pos.y, s.pos.x, s.cols); + for(r:=s.pos.y+1; r<s.rows; r++) + rowclear(r, 1, s.cols); + # clears from the beginning of the screen to the cursor inclusive + 1 => + for(r:=1; r<s.pos.y; r++) + rowclear(r, 1, s.cols); + rowclear(s.pos.y, 1, s.pos.x); + # clears the entire screen + 2 => + clear(s); + } + + 'K' => + case s.a0 { + # clears from the cursor to the end of the row + 0 => rowclear(s.pos.y, s.pos.x, s.cols); + + # clears from the start of the row to the cursor + 1 => rowclear(s.pos.y, 1, s.pos.x); + + # clears the entire row in which the cursor is positioned + 2 => rowclear(s.pos.y, 1, s.cols); + } + + # deletes n characters from cursor position + 'P' => + rowclear(s.pos.y, s.pos.x, s.pos.x+s.a0-1); + + # inserts n characters from cursor position + '@' => + disp->Put(dup(' ', s.a0), Point(s.pos.x,s.pos.y), s.cset, s.attr, 1); + + # starts cursor insert mode + 'h' => + if(s.a0 == 4) + s.spec |= Insert; + + 'l' => # ends cursor insert mode + if(s.a0 == 4) + s.spec &= ~Insert; + + # deletes n rows from cursor row + 'M' => + scroll(s.pos.y, s.a0); + + # inserts n rows from cursor row + 'L' => + scroll(s.pos.y, -1*s.a0); + } + s.state = Sstart; + Scsi1 => + case ch { + # direct cursor addressing + 'H' => + if(s.a0 > 0 && s.a0 < s.rows && s.a1 > 0 && s.a1 <= s.cols) + s.pos = Point(s.a1, s.a0); + } + s.state = Sstart; + } +} + +# Screen state - Videotex mode +vstate(s: ref Screen, data: array of byte): array of byte +{ + i: int; + for(i = 0; i < len data; i++) { + ch := int data[i]; + + if(debug['s']) { + cs:=""; + if(s.cset==videotex) cs = "v"; else cs="s"; + fprint(stderr, "vstate %d, %ux (%c) %.4ux %.4ux %s (%d,%d)\n", s.state, ch, ch, s.attr, s.spec, cs, s.pos.y, s.pos.x); + } + case s.state { + Sstart => + if(ISG0(ch) || ch == SP) { + n := 0; + str := ""; + while(i < len data) { + ch = int data[i]; + if(ISG0(ch) || ch == SP) + str[n++] = int data[i++]; + else { + i--; + break; + } + } + if(n > 0) { + if(debug['s']) + fprint(stderr, "vstate puts(%s)\n", str); + s.put(str); + s.savech = str[n-1]; + } + } else if(ISC0(ch)) + vc0(s, ch); + else if(ch == DEL) { + if(s.cset == semigraphic) + ch = 16r5f; + s.put(tostr(ch)); + s.savech = ch; + } + Sss2 => + if(ch == NUL) # 1.2.6.1 + continue; + if(s.cset == semigraphic) # 1.2.3.4 + continue; + vss2(s, ch); + Sesc => + if(ch == NUL) + continue; + vc1(s, ch); + Srepeat => + # byte from `columns' 4 to 7 gives repeat count on 6 bits + # of the last `Put' character + if(ch == NUL) + continue; + if(ISC0(ch)) { + s.state = Sstart; + vc0(s, ch); + break; + } + if(ch >= 16r40 && ch <= 16r7f) + s.put(dup(s.savech, (ch-16r40))); + s.state = Sstart; + Saccent => + case s.a0 { + 16r41 => # grave + case ch { + 'a' => ch = 'à'; + 'e' => ch = 'è'; + 'u' => ch = 'ù'; + } + 16r42 => # acute + case ch { + 'e' => ch = 'é'; + } + 16r43 => # circumflex + case ch { + 'a' => ch = 'â'; + 'e' => ch = 'ê'; + 'i' => ch = 'î'; + 'o' => ch = 'ô'; + 'u' => ch = 'û'; + } + 16r48 => # umlaut + case ch { + 'a' => ch = 'ä'; + 'e' => ch = 'ë'; + 'i' => ch = 'ï'; + 'o' => ch = 'ö'; + 'u' => ch = 'ü'; + } + 16r4b => # cedilla + case ch { + 'c' => ch = 'ç'; + } + } + s.put(tostr(ch)); + s.savech = ch; + s.state = Sstart; + Scsi0 => + if(ch >= 16r30 && ch <= 16r39) { + s.a0 *= 10; + s.a0 += (ch - 16r30); + } else if((ch >= 16r20 && ch <= 16r29) || (ch >= 16r3a && ch <= 16r3f)) { # 1.2.7 + s.a0 = 0; + s.state = Siso6429; + } else + vcsi(s, ch); + Scsi1 => + if(ch >= 16r30 && ch <= 16r39) { + s.a1 *= 10; + s.a1 += (ch - 16r30); + } else + vcsi(s, ch); + Sus0 => + if(ch == 16r23) { # start DRCS definition + s.state = Sdrcs; + s.a0 = 0; + break; + } + if(ch >= 16r40 && ch < 16r80) + s.a0 = (ch - 16r40); + else if(ch >= 16r30 && ch <= 16r32) + s.a0 = (ch - 16r30); + else + s.a0 = -1; + s.state = Sus1; + Sus1 => + if(ch >= 16r40 && ch < 16r80) + s.a1 = (ch - 16r40); + else if(ch >= 16r30 && ch <= 16r39) { + s.a1 = (ch - 16r30); + s.a0 = s.a0*10 + s.a1; # shouldn't be used any more + s.a1 = 1; + } else + s.a1 = -1; + # US row, col : this is how you get to row zero + if(s.a0 >= 0 && s.a0 < s.rows && s.a1 > 0 && s.a1 <= s.cols) { + if(s.a0 == 0 && s.pos.y > 0) { + s.savepos = s.pos; + s.saveattr = s.attr; + } + s.pos = Point(s.a1, s.a0); + s.delimit = 0; # 1.2.5.3, don't reset serial attributes + s.attr = ATTR0; + s.cset = videotex; + } + s.state = Sstart; + Sskip => + # swallow the next character unless from C0 + s.state = Sstart; + if(ISC0(ch)) + vc0(s, ch); + Swaitfor => + # ignore characters until the character in a0 inclusive + if(ch == s.a0) + s.state = Sstart; + Siso2022 => + # 1.2.7 + # swallow (upto) 3 characters from column 2, + # then 1 character from columns 3 to 7 + if(ch == NUL) + continue; + if(ISC0(ch)) { + s.state = Sstart; + vc0(s, ch); + break; + } + s.a0++; + if(s.a0 <= 3) { + if(ch >= 16r20 && ch <= 16r2f) + break; + } + if (s.a0 <= 4 && ch >= 16r30 && ch <= 16r7f) { + s.state = Sstart; + break; + } + s.state = Sstart; + s.put(tostr(DEL)); + Siso6429 => + # 1.2.7 + # swallow characters from column 3, + # or column 2, then 1 from column 4 to 7 + if(ISC0(ch)) { + s.state = Sstart; + vc0(s, ch); + break; + } + if(ch >= 16r20 && ch <= 16r3f) + break; + if(ch >= 16r40 && ch <= 16r7f) { + s.state = Sstart; + break; + } + s.state = Sstart; + s.put(tostr(DEL)); + Stransparent => + # 1.2.7 + # ignore all codes until ESC, 25, 40 or ESC, 2F, 3F + # progress in s.a0 and s.a1 + match := array [] of { + array [] of { ESC, 16r25, 16r40 }, + array [] of { ESC, 16r2f, 16r3f }, + }; + if(ch == ESC) { + s.a0 = s.a1 = 1; + break; + } + if(ch == match[0][s.a0]) + s.a0++; + else + s.a0 = 0; + if(ch == match[1][s.a1]) + s.a1++; + else + s.a1 = 0; + if(s.a0 == 3 || s.a1 == 3) + s.state = Sstart; + Sdrcs => + if(s.a0 > 0) { # fixed number of bytes to skip in a0 + s.a0--; + if(s.a0 == 0) { + s.state = Sstart; + break; + } + } else if(ch == US) # US XX YY - end of DRCS + s.state = Sus0; + else if(ch == 16r20) # US 23 20 20 20 4[23] 49 + s.a0 = 4; + Sconceal => + # 1.2.4.4 + # ESC 23 20 58 - Conceal fields + # ESC 23 20 5F - Reveal fields + # ESC 23 21 XX - Filter + # progress in s.a0 + case s.a0 { + 0 => + if(ch == 16r20 || ch == 16r21) + s.a0 = ch; + 16r20 => + case ch { + 16r58 => + disp->Reveal(0); + disp->Refresh(); + 16r5f => + disp->Reveal(1); + disp->Refresh(); + } + s.state = Sstart; + 16r21 => + s.state = Sstart; + } + } + } + if (i < len data) + return data[i:]; + else + return nil; +} + +# Screen state - Mixed mode +mstate(s: ref Screen, data: array of byte): array of byte +{ + i: int; +Stateloop: + for(i = 0; i < len data; i++) { + ch := int data[i]; + + if(debug['s']) { + cs:=""; + if(s.cset==videotex) cs = "v"; else cs="s"; + fprint(stderr, "mstate %d, %ux (%c) %.4ux %.4ux %s (%d,%d)\n", s.state, ch, ch, s.attr, s.fstate, cs, s.pos.y, s.pos.x); + } + case s.state { + Sstart => + if(ISG0(ch) || ch == SP) { + n := 0; + str := ""; + while(i < len data) { + ch = int data[i]; + if(ISG0(ch) || ch == SP) + str[n++] = int data[i++]; + else { + i--; + break; + } + } + if(n > 0) { + if(debug['s']) + fprint(stderr, "mstate puts(%s)\n", str); + s.put(str); + s.savech = str[n-1]; + } + } else if(ISC0(ch)) + mc0(s, ch); + else if(ch == DEL) { + if(s.cset == semigraphic) + ch = 16r5f; + s.put(tostr(ch)); + s.savech = ch; + } + Sesc => + if(ch == NUL) + continue; + mc1(s, ch); + Scsi0 => + if(ch >= 16r30 && ch <= 16r39) { + s.a0 *= 10; + s.a0 += (ch - 16r30); + } else if(ch == '?') { + s.a0 = '?'; + } else + mcsi(s, ch); + if(T.mode != Mixed) # CSI ? { changes to Videotex mode + break Stateloop; + Scsi1 => + if(ch >= 16r30 && ch <= 16r39) { + s.a1 *= 10; + s.a1 += (ch - 16r30); + } else + mcsi(s, ch); + Sus0 => + if(ch >= 16r40 && ch < 16r80) + s.a0 = (ch - 16r40); + else if(ch >= 16r30 && ch <= 16r32) + s.a0 = (ch - 16r30); + else + s.a0 = -1; + s.state = Sus1; + Sus1 => + if(ch >= 16r40 && ch < 16r80) + s.a1 = (ch - 16r40); + else if(ch >= 16r30 && ch <= 16r39) { + s.a1 = (ch - 16r30); + s.a0 = s.a0*10 + s.a1; # shouldn't be used any more + s.a1 = 1; + } else + s.a1 = -1; + # US row, col : this is how you get to row zero + if(s.a0 >= 0 && s.a0 < s.rows && s.a1 > 0 && s.a1 <= s.cols) { + if(s.a0 == 0 && s.pos.y > 0) { + s.savepos = s.pos; + s.saveattr = s.attr; + } + s.pos = Point(s.a1, s.a0); + s.delimit = 0; # 1.2.5.3, don't reset serial attributes + s.attr = ATTR0; + s.cset = videotex; + } + s.state = Sstart; + Siso6429 => + # 1.2.7 + # swallow characters from column 3, + # or column 2, then 1 from column 4 to 7 + if(ISC0(ch)) { + s.state = Sstart; + mc0(s, ch); + break; + } + if(ch >= 16r20 && ch <= 16r3f) + break; + if(ch >= 16r40 && ch <= 16r7f) { + s.state = Sstart; + break; + } + s.state = Sstart; + s.put(tostr(DEL)); + } + } + if (i < len data) + return data[i:]; + else + return nil; + return nil; +} + +# process a byte from set C0 - Mixed mode +mc0(s: ref Screen, ch: int) +{ + case ch { + ESC => + s.state = Sesc; + SO => +# s.cset = french; + ; + SI => +# s.cset = american; + ; + BS => + if(s.pos.x > 1) + s.pos.x -= 1; + HT => + s.pos.x += 8; + if(s.pos.x > s.cols) + s.pos.x = s.cols; + LF or VT or FF => + if(s.pos.y == s.rows - 1) + if(s.spec&Scroll) + scroll(1, 1); + else + s.pos.y = 1; + else if(s.pos.y == 0) { # restore attributes on leaving row zero + if(ch == LF) { # 4.5 + s.pos = s.savepos; + s.attr = s.saveattr; + } + } else + s.pos.y += 1; + CR => + s.pos.x = 1; + CAN or SUB => # displays the error symbol - filled in rectangle + disp->Put(dup(16r5f, 1), Point(s.pos.x,s.pos.y), s.cset, s.attr, 0); + NUL => + # padding character - ignore, but may appear anywhere + ; + BEL => + # ah ... + ; + XON => # screen copying + ; + XOFF => # screen copying + ; + US => + # expect US row, col + s.state = Sus0; + } +} + +# process a byte from the set c1 - introduced by the ESC character - Mixed mode +mc1(s: ref Screen, ch: int) +{ + if(ISC0(ch)) { + s.state = Sstart; + mc0(s, ch); + return; + } + case ch { + 16r5b => # CSI sequence + s.a0 = s.a1 = 0; + if(s.pos.y > 0) # 1.2.5.2 + s.state = Scsi0; + return; + + 16r44 or # IND like LF + 16r45 => # NEL like CR LF + if(ch == 16r45) + s.pos.x = 1; + if(s.pos.y == s.rows - 1) + if(s.spec&Scroll) + scroll(1, 1); + else + s.pos.y = 1; + else if(s.pos.y == 0) { # restore attributes on leaving row zero + s.pos = s.savepos; + s.attr = s.saveattr; + } else + s.pos.y += 1; + 16r4d => # RI + if(s.pos.y == 1) + if(s.spec&Scroll) + scroll(1, -1); + else + s.pos.y = s.rows - 1; + else if(s.pos.y == 0) + break; + else + s.pos.y -= 1; + } + s.state = Sstart; +} + + +# process CSI functions - Mixed mode +mcsi(s: ref Screen, ch: int) +{ + case s.state { + Scsi0 => + case ch { + # move cursor up n rows, stop at top of screen + 'A' => + if(s.a0 == 0) + s.a0 = 1; + s.pos.y -= s.a0; + if(s.pos.y < 1) + s.pos.y = 1; + + # move cursor down n rows, stop at bottom of screen + 'B' => + if(s.a0 == 0) + s.a0 = 1; + s.pos.y += s.a0; + if(s.pos.y >= s.rows) + s.pos.y = s.rows - 1; + + # move cursor n columns right, stop at edge of screen + 'C' => + if(s.a0 == 0) + s.a0 = 1; + s.pos.x += s.a0; + if(s.pos.x > s.cols) + s.pos.x = s.cols; + + # move cursor n columns left, stop at edge of screen + 'D' => + if(s.a0 == 0) + s.a0 = 1; + s.pos.x -= s.a0; + if(s.pos.x < 1) + s.pos.x = 1; + + # second parameter + ';' => + s.state = Scsi1; + return; + + 'J' => + case s.a0 { + # clears from the cursor to the end of the screen inclusive + 0 => + rowclear(s.pos.y, s.pos.x, s.cols); + for(r:=s.pos.y+1; r<s.rows; r++) + rowclear(r, 1, s.cols); + # clears from the beginning of the screen to the cursor inclusive + 1 => + for(r:=1; r<s.pos.y; r++) + rowclear(r, 1, s.cols); + rowclear(s.pos.y, 1, s.pos.x); + # clears the entire screen + 2 => + clear(s); + } + + 'K' => + case s.a0 { + # clears from the cursor to the end of the row + 0 => rowclear(s.pos.y, s.pos.x, s.cols); + + # clears from the start of the row to the cursor + 1 => rowclear(s.pos.y, 1, s.pos.x); + + # clears the entire row in which the cursor is positioned + 2 => rowclear(s.pos.y, 1, s.cols); + } + + # inserts n characters from cursor position + '@' => + disp->Put(dup(' ', s.a0), Point(s.pos.x,s.pos.y), s.cset, s.attr, 1); + + # starts cursor insert mode + 'h' => + if(s.a0 == 4) + s.spec |= Insert; + + 'l' => # ends cursor insert mode + if(s.a0 == 4) + s.spec &= ~Insert; + + # inserts n rows from cursor row + 'L' => + scroll(s.pos.y, -1*s.a0); + s.pos.x = 1; + + # deletes n rows from cursor row + 'M' => + scroll(s.pos.y, s.a0); + s.pos.x = 1; + + # deletes n characters from cursor position + 'P' => + rowclear(s.pos.y, s.pos.x, s.pos.x+s.a0-1); + + # select Videotex mode + '{' => + if(s.a0 == '?') { + T.mode = Videotex; + s.setmode(T.mode); + } + + # display attributes + 'm' => + case s.a0 { + 0 => s.attr &= ~(attrL|attrF|attrP|attrB); + 1 => s.attr |= attrB; + 4 => s.attr |= attrL; + 5 => s.attr |= attrF; + 7 => s.attr |= attrP; + 22 => s.attr &= ~attrB; + 24 => s.attr &= ~attrL; + 25 => s.attr &= ~attrF; + 27 => s.attr &= ~attrP; + } + # direct cursor addressing + 'H' => + if(s.a0 == 0) + s.a0 = 1; + if(s.a1 == 0) + s.a1 = 1; + if(s.a0 > 0 && s.a0 < s.rows && s.a1 > 0 && s.a1 <= s.cols) + s.pos = Point(s.a1, s.a0); + } + s.state = Sstart; + Scsi1 => + case ch { + # direct cursor addressing + 'H' => + if(s.a0 == 0) + s.a0 = 1; + if(s.a1 == 0) + s.a1 = 1; + if(s.a0 > 0 && s.a0 < s.rows && s.a1 > 0 && s.a1 <= s.cols) + s.pos = Point(s.a1, s.a0); + } + s.state = Sstart; + } +} + + +# Screen state - ASCII mode +astate(nil: ref Screen, nil: array of byte): array of byte +{ + return nil; +} + +# Put a string in the current attributes to the current writing position +Screen.put(s: self ref Screen, str: string) +{ + while((l := len str) > 0) { + n := s.cols - s.pos.x + 1; # characters that will fit on this row + if(s.attr & attrW) { + if(n > 1) # fit normal width character in last column + n /= 2; + } + if(n > l) + n = l; + if(s.delimit) { # set delimiter bit on 1st space (if any) + for(i:=0; i<n; i++) + if(str[i] == ' ') + break; + if(i > 0) { + disp->Put(str[0:i], s.pos, s.cset, s.attr, s.spec&Insert); + incpos(s, i); + } + if(i < n) { + if(debug['s']) { + cs:=""; + if(s.cset==videotex) cs = "v"; else cs="s"; + fprint(stderr, "D %ux %s\n", s.attr|attrD, cs); + } + disp->Put(tostr(str[i]), s.pos, s.cset, s.attr|attrD, s.spec&Insert); + incpos(s, 1); + s.delimit = 0; + # clear serial attributes once used + # hang onto background attribute - needed for semigraphics + case s.cset { + videotex => + s.attr &= ~(attrL|attrC); + semigraphic => + s.attr &= ~(attrC); + } + } + if(i < n-1) { + disp->Put(str[i+1:n], s.pos, s.cset, s.attr, s.spec&Insert); + incpos(s, n-(i+1)); + } + } else { + disp->Put(str[0:n], s.pos, s.cset, s.attr, s.spec&Insert); + incpos(s, n); + } + if(n < len str) + str = str[n:]; + else + str = nil; + } +# if(T.state == Local || T.spec&Echo) +# refresh(); +} + +# increment the current writing position by `n' cells. +# caller must ensure that `n' characters can fit +incpos(s: ref Screen, n: int) +{ + if(s.attr & attrW) + s.pos.x += 2*n; + else + s.pos.x += n; + if(s.pos.x > s.cols) + if(s.pos.y == 0) # no wraparound from row zero + s.pos.x = s.cols; + else { + s.pos.x = 1; + if(s.pos.y == s.rows - 1 && s.spec&Scroll) { + if(s.attr & attrH) { + scroll(1, 2); + } else { + scroll(1, 1); + rowclear(s.pos.y, 1, s.cols); + } + } else { + if(s.attr & attrH) + s.pos.y += 2; + else + s.pos.y += 1; + if(s.pos.y >= s.rows) + s.pos.y -= (s.rows-1); + } + } +} + +# clear row `r' from `first' to `last' column inclusive +rowclear(r, first, last: int) +{ + # 16r5f is the semi-graphic black rectangle + disp->Put(dup(16r5f, last-first+1), Point(first,r), semigraphic, fgBlack, 0); +# disp->Put(dup(' ', last-first+1), Point(first,r), S.cset, fgBlack, 0); +} + +clear(s: ref Screen) +{ + for(r:=1; r<s.rows; r++) + rowclear(r, 1, s.cols); +} + +# called to suggest a display update +refresh() +{ + disp->Refresh(); +} + +# scroll the screen +scroll(topline, nlines: int) +{ + disp->Scroll(topline, nlines); + disp->Refresh(); +} + +# filter the specified ISO6429 and ISO2022 codes from the screen input +# TODO: filter some ISO2022 sequences +filter(s: ref Screen, data: array of byte): array of array of byte +{ + case T.mode { + Videotex => + return vfilter(s, data); + Mixed => + return mfilter(s, data); + Ascii => + return afilter(s, data); + } + return nil; +} + +# filter the specified ISO6429 and ISO2022 codes from the screen input +vfilter(s: ref Screen, data: array of byte): array of array of byte +{ + ba := array [0] of array of byte; + changed := 0; + + d0 := 0; + for(i:=0; i<len data; i++) { + ch := int data[i]; + case s.fstate { + FSstart => + if(ch == ESC) { + s.fstate = FSesc; + changed = 1; + if(i > d0) + ba = dappend(ba, data[d0:i]); + d0 = i+1; + } + FSesc => + d0 = i+1; + changed = 1; + if(ch == '[') { + s.fstate = FS6429; + s.fsaved = array [0] of byte; + s.badp = 0; +# } else if(ch == 16r20) { +# s.fstate = FS2022; +# s.fsaved = array [0] of byte; + s.badp = 0; + } else if(ch == ESC) { + ba = dappend(ba, array [] of { byte ESC }); + s.fstate = FSesc; + } else { + # false alarm - don't filter + ba = dappend(ba, array [] of { byte ESC, byte ch }); + s.fstate = FSstart; + } + FS6429 => # filter out invalid CSI sequences + d0 = i+1; + changed = 1; + if(ch >= 16r20 && ch <= 16r3f) { + if((ch < 16r30 || ch > 16r39) && ch != ';') + s.badp = 1; + a := array [len s.fsaved + 1] of byte; + a[0:] = s.fsaved[0:]; + a[len a - 1] = byte ch; + s.fsaved = a; + } else { + valid := 1; + case ch { + 'A' => ; + 'B' => ; + 'C' => ; + 'D' => ; + 'H' => ; + 'J' => ; + 'K' => ; + 'P' => ; + '@' => ; + 'h' => ; + 'l' => ; + 'M' => ; + 'L' => ; + * => + valid = 0; + } + if(s.badp) + valid = 0; + if(debug['f']) + fprint(stderr, "vfilter %d: %s%c\n", valid, string s.fsaved, ch); + if(valid) { # false alarm - don't filter + ba = dappend(ba, array [] of { byte ESC, byte '[' }); + ba = dappend(ba, s.fsaved); + ba = dappend(ba, array [] of { byte ch } ); + } + s.fstate = FSstart; + } + FS2022 => ; + } + } + if(changed) { + if(i > d0) + ba = dappend(ba, data[d0:i]); + return ba; + } + return array [] of { data }; +} + +# filter the specified ISO6429 and ISO2022 codes from the screen input - Videotex +mfilter(s: ref Screen, data: array of byte): array of array of byte +{ + ba := array [0] of array of byte; + changed := 0; + + d0 := 0; + for(i:=0; i<len data; i++) { + ch := int data[i]; + case s.fstate { + FSstart => + case ch { + ESC => + s.fstate = FSesc; + changed = 1; + if(i > d0) + ba = dappend(ba, data[d0:i]); + d0 = i+1; + SEP => + s.fstate = FSsep; + changed = 1; + if(i > d0) + ba = dappend(ba, data[d0:i]); + d0 = i+1; + } + FSesc => + d0 = i+1; + changed = 1; + if(ch == '[') { + s.fstate = FS6429; + s.fsaved = array [0] of byte; + s.badp = 0; + } else if(ch == ESC) { + ba = dappend(ba, array [] of { byte ESC }); + s.fstate = FSesc; + } else { + # false alarm - don't filter + ba = dappend(ba, array [] of { byte ESC, byte ch }); + s.fstate = FSstart; + } + FSsep => + d0 = i+1; + changed = 1; + if(ch == ESC) { + ba = dappend(ba, array [] of { byte SEP }); + s.fstate = FSesc; + } else if(ch == SEP) { + ba = dappend(ba, array [] of { byte SEP }); + s.fstate = FSsep; + } else { + if(ch >= 16r00 && ch <= 16r1f) + ba = dappend(ba, array [] of { byte SEP , byte ch }); + # consume the character + s.fstate = FSstart; + } + FS6429 => # filter out invalid CSI sequences + d0 = i+1; + changed = 1; + if(ch >= 16r20 && ch <= 16r3f) { + if((ch < 16r30 || ch > 16r39) && ch != ';' && ch != '?') + s.badp = 1; + a := array [len s.fsaved + 1] of byte; + a[0:] = s.fsaved[0:]; + a[len a - 1] = byte ch; + s.fsaved = a; + } else { + valid := 1; + case ch { + 'm' => ; + 'A' => ; + 'B' => ; + 'C' => ; + 'D' => ; + 'H' => ; + 'J' => ; + 'K' => ; + '@' => ; + 'h' => ; + 'l' => ; + 'L' => ; + 'M' => ; + 'P' => ; + '{' => # allow CSI ? { + n := len s.fsaved; + if(n == 0 || s.fsaved[n-1] != byte '?') + s.badp = 1; + * => + valid = 0; + } + if(s.badp) # only decimal params + valid = 0; + if(debug['f']) + fprint(stderr, "mfilter %d: %s%c\n", valid, string s.fsaved, ch); + if(valid) { # false alarm - don't filter + ba = dappend(ba, array [] of { byte ESC, byte '[' }); + ba = dappend(ba, s.fsaved); + ba = dappend(ba, array [] of { byte ch } ); + } + s.fstate = FSstart; + } + FS2022 => ; + } + } + if(changed) { + if(i > d0) + ba = dappend(ba, data[d0:i]); + return ba; + } + return array [] of { data }; +} + +# filter the specified ISO6429 and ISO2022 codes from the screen input - Videotex +afilter(nil: ref Screen, data: array of byte): array of array of byte +{ + return array [] of { data }; +} + +# append to an array of array of byte +dappend(ba: array of array of byte, b: array of byte): array of array of byte +{ + l := len ba; + na := array [l+1] of array of byte; + na[0:] = ba[0:]; + na[l] = b; + return na; +} + +# Put a diagnostic string to row 0 +Screen.msg(s: self ref Screen, str: string) +{ + blank := string array [s.cols -4] of {* => byte ' '}; + n := len str; + if(n > s.cols - 4) + n = s.cols - 4; + disp->Put(blank, Point(1, 0), videotex, 0, 0); + if(str != nil) + disp->Put(str[0:n], Point(1, 0), videotex, fgWhite|attrB, 0); + disp->Refresh(); +}
\ No newline at end of file diff --git a/appl/examples/minitel/socket.b b/appl/examples/minitel/socket.b new file mode 100644 index 00000000..b3ce7fcf --- /dev/null +++ b/appl/examples/minitel/socket.b @@ -0,0 +1,49 @@ +# +# Copyright © 1998 Vita Nuova Limited. All rights reserved. +# + +Socket: adt { + m: ref Module; # common attributes + in: chan of ref Event; + + init: fn(c: self ref Socket); + reset: fn(c: self ref Socket); + run: fn(c: self ref Socket); + quit: fn(c: self ref Socket); +}; + +Socket.init(c: self ref Socket) +{ + c.in = chan of ref Event; + c.reset(); +} + +Socket.reset(c: self ref Socket) +{ + c.m = ref Module(Pscreen, 0); +} + +Socket.run(c: self ref Socket) +{ +Runloop: + for(;;){ + ev := <- c.in; + pick e := ev { + Equit => + break Runloop; + Eproto => + case e.cmd { + Creset => + c.reset(); + * => break; + } + Edata => + } + } + send(nil); +} + +Socket.quit(c: self ref Socket) +{ + if(c==nil); +} diff --git a/appl/examples/minitel/swkeyb.b b/appl/examples/minitel/swkeyb.b new file mode 100644 index 00000000..50cb238f --- /dev/null +++ b/appl/examples/minitel/swkeyb.b @@ -0,0 +1,370 @@ +### +### This data and information is not to be used as the basis of manufacture, +### or be reproduced or copied, or be distributed to another party, in whole +### or in part, without the prior written consent of Lucent Technologies. +### +### (C) Copyright 1997 Lucent Technologies +### +### Written by N. W. Knauft +### +# +# Revisions Copyright © 1998 Vita Nuova Limited. + +implement Keyboard; + +include "sys.m"; + sys: Sys; + +include "draw.m"; + draw: Draw; + +include "tk.m"; + tk: Tk; + +include "tkclient.m"; + tkclient: Tkclient; + +include "swkeyb.m"; + +#Icon path +ICPATH: con "keybd/"; + +#Font +FONT: con "/fonts/lucidasans/latin1.7.font"; +SPECFONT: con "/fonts/lucidasans/latin1.6.font"; + +# Dimension constants +KBDWIDTH: con 360; +KBDHEIGHT: con 120; +KEYSIZE: con "19"; +KEYSPACE: con 5; +KEYBORDER: con 1; +KEYGAP: con KEYSPACE - (2 * KEYBORDER); +ENDGAP: con 2 - KEYBORDER; + +# Row size constants (cumulative) +ROW1: con 14; +ROW2: con 28; +ROW3: con 41; +ROW4: con 53; +NKEYS: con 63; + +#Special key number constants +DELKEY: con 13; +TABKEY: con 14; +BACKSLASHKEY: con 27; +CAPSLOCKKEY: con 28 ; +RETURNKEY: con 40; +LSHIFTKEY: con 41; +RSHIFTKEY: con 52; +ESCKEY: con 53; +CTRLKEY: con 54; +METAKEY: con 55; +ALTKEY: con 56; +SPACEKEY: con 57; +ENTERKEY: con 58; +LEFTKEY: con 59; +RIGHTKEY: con 60; +DOWNKEY: con 61; +UPKEY: con 62; + +#Special key code constants +CAPSLOCK: con -1 ; +SHIFT: con -2; +CTRL: con -3; +ALT: con -4; +META: con -5; +MAGIC_PREFIX: con 256; +ARROW_OFFSET: con 57344; +ARROW_PREFIX: con ARROW_OFFSET + 18; + +#Special key width constants +DELSIZE: con 44; +TABSIZE: con 32; +BACKSLASHSIZE: con 31; +CAPSLOCKSIZE: con 44; +RETURNSIZE: con 43; +LSHIFTSIZE: con 56; +RSHIFTSIZE: con 55; +ESCSIZE: con 21; +CTRLSIZE: con 23; +METASIZE: con 38; +ALTSIZE: con 22; +SPACESIZE: con 100; +ENTERSIZE: con 31; + +#Arrow key code constants +UP: con ARROW_PREFIX; +DOWN: con ARROW_PREFIX + 1; +LEFT: con ARROW_PREFIX + 2; +RIGHT: con ARROW_PREFIX + 3; + +direction:= array[] of {"up", "down", "left", "right"}; +row_dimensions:= array[] of {0, ROW1, ROW2, ROW3, ROW4, NKEYS}; + +special_keys:= array[] of { + (DELKEY, DELSIZE), + (TABKEY, TABSIZE), + (BACKSLASHKEY, BACKSLASHSIZE), + (CAPSLOCKKEY, CAPSLOCKSIZE), + (RETURNKEY, RETURNSIZE), + (LSHIFTKEY, LSHIFTSIZE), + (RSHIFTKEY, RSHIFTSIZE), + (ESCKEY, ESCSIZE), + (CTRLKEY, CTRLSIZE), + (METAKEY, METASIZE), + (ALTKEY, ALTSIZE), + (SPACEKEY, SPACESIZE), + (ENTERKEY, ENTERSIZE), +}; + +keys:= array[] of { + # Unshifted + "`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "Delete", + "Tab", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", "\\\\", + "CapLoc", "a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "\'", "Return", + "Shift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "Shift", + "Esc", "Ctrl", " ", "Alt", " ", "Enter", "<-", "->", "v", "^", + # Shifted + "~", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "+", "Delete", + "Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "\\{", "\\}", "|", + "CapLoc", "A", "S", "D", "F", "G", "H", "J", "K", "L", ":", "\"", "Return", + "Shift", "Z", "X", "C", "V", "B", "N", "M", "<", ">", "?", "Shift", + "Esc", "Ctrl", " ", "Alt", " ", "Enter", "<-", "->", "v", "^", +}; + +keyvals:= array[] of { + # Unshifted + '`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\b', + '\t', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\', + CAPSLOCK, 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '\n', + SHIFT, 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', SHIFT, + 27, CTRL, META, ALT, 32, '\n', LEFT, RIGHT, DOWN, UP, + # Shifted + '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', '\b', + '\t', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '|', + CAPSLOCK, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"', '\n', + SHIFT, 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', SHIFT, + 27, CTRL, META, ALT, 32, '\n', LEFT, RIGHT, DOWN, UP, +}; + +rowlayout := array[] of { + "frame .f1", + "frame .f2", + "frame .f3", + "frame .f4", + "frame .f5", + "frame .dummy0 -height " + string (ENDGAP), + "frame .dummy1 -height " + string KEYGAP, + "frame .dummy2 -height " + string KEYGAP, + "frame .dummy3 -height " + string KEYGAP, + "frame .dummy4 -height " + string KEYGAP, + "frame .dummy5 -height " + string (ENDGAP + 1), +}; + +# Move key flags +move_key_enabled := 0; +meta_active := 0; + +# Create keyboard widget, spawn keystroke handler +initialize(t: ref Tk->Toplevel, ctxt : ref Draw->Context, dot: string): chan of string +{ + return chaninit(t, ctxt, dot, chan of string); +} + +chaninit(t: ref Tk->Toplevel, ctxt : ref Draw->Context, dot: string, rc: chan of string): chan of string +{ + sys = load Sys Sys->PATH; + draw = load Draw Draw->PATH; + tk = load Tk Tk->PATH; + tkclient = load Tkclient Tkclient->PATH; + + tkclient->init(); + + tk->cmd(t, "frame " + dot + " -bd 2 -relief raised -width " + string KBDWIDTH + + " -height " + string KBDHEIGHT); + tkcmds(t, rowlayout); + + for(i := 0; i < NKEYS; i++) { + tk->cmd(t, "button .b" + string i + " -font " + FONT + " -width " + KEYSIZE + + " -height " + KEYSIZE + " -bd " + string KEYBORDER); + + tk->cmd(t, ".b" + string i + " configure -text {" + keys[i] + + "} -command 'send keypress " + string keyvals[i]); + } + + for(i = 0; i < len special_keys; i++) { + (keynum, keysize) := special_keys[i]; + tk->cmd(t, ".b" + string keynum + " configure -font " + SPECFONT + " -width " + string keysize); + } + + tk->cmd(t, "image create bitmap Capslock_on -file " + ICPATH + "capson.bit -maskfile " + ICPATH + "capson.bit"); + tk->cmd(t, "image create bitmap Capslock_off -file " + ICPATH + "capsoff.bit -maskfile " + ICPATH + "capsoff.bit"); + tk->cmd(t, "image create bitmap Left_arrow -file " + ICPATH + "larrow.bit -maskfile " + ICPATH + "larrow.bit"); + tk->cmd(t, "image create bitmap Right_arrow -file " + ICPATH + "rarrow.bit -maskfile " + ICPATH + "rarrow.bit"); + tk->cmd(t, "image create bitmap Down_arrow -file " + ICPATH + "darrow.bit -maskfile " + ICPATH + "darrow.bit"); + tk->cmd(t, "image create bitmap Up_arrow -file " + ICPATH + "uarrow.bit -maskfile " + ICPATH + "uarrow.bit"); + tk->cmd(t, "image create bitmap Move_on -file " + ICPATH + "moveon.bit -maskfile " + ICPATH + "moveon.bit"); + tk->cmd(t, "image create bitmap Move_off -file " + ICPATH + "moveoff.bit -maskfile " + ICPATH + "moveoff.bit"); + tk->cmd(t, "image create bitmap None -file " + ICPATH + "none.bit -maskfile " + ICPATH + "none.bit"); + tk->cmd(t, ".b" + string CAPSLOCKKEY + " configure -image Capslock_off"); + tk->cmd(t, ".b" + string LEFTKEY + " configure -image Left_arrow"); + tk->cmd(t, ".b" + string RIGHTKEY + " configure -image Right_arrow"); + tk->cmd(t, ".b" + string DOWNKEY + " configure -image Down_arrow"); + tk->cmd(t, ".b" + string UPKEY + " configure -image Up_arrow"); + + for(j:=1; j < len row_dimensions; j++) { + rowstart := row_dimensions[j-1]; + rowend := row_dimensions[j]; + for(i=rowstart; i<rowend; i++) { + if (i == rowstart) { + tk->cmd(t, "frame .f" + string j + ".dummy -width " + string ENDGAP); + tk->cmd(t, "pack .f" + string j + ".dummy -side left"); + } + tk->cmd(t, "pack .b" + string i + " -in .f" + string j + " -side left"); + if (i == rowend-1) + tk->cmd(t, "frame .f" + string j + ".dummy" + string i + " -width " + string ENDGAP); + else + tk->cmd(t, "frame .f" + string j + ".dummy" + string i + " -width " + string KEYGAP); + tk->cmd(t, "pack .f" + string j + ".dummy" + string i + " -side left"); + } + } + + tk->cmd(t, "pack .dummy0 .f1 .dummy1 .f2 .dummy2 .f3 .dummy3 .f4 .dummy4 .f5 .dummy5 -in " + dot); + tk->cmd(t,"update"); + + key := chan of string; + spawn handle_keyclicks(t, ctxt, key, rc); + return key; +} + +tkcmds(t: ref Tk->Toplevel, cmds: array of string) +{ + for(i := 0; i < len cmds; i++) + tk->cmd(t, cmds[i]); +} + +# Process key clicks and hand keycodes off to Tk +handle_keyclicks(t: ref Tk->Toplevel, ctxt : ref Draw->Context, sc, rc: chan of string) +{ + keypress := chan of string; + tk->namechan(t, keypress, "keypress"); + + minitel := 0; + caps_locked := 0; + shifted := 0; + ctrl_active := 0; + alt_active := 0; + +Work: + for(;;){ + alt { + k := <-keypress => + (n, cmdstr) := sys->tokenize(k, " \t\n"); + keycode := int hd cmdstr; + case keycode { + CAPSLOCK => + redisplay_keyboard(t, minitel, caps_locked ^= 1, caps_locked); + shifted = 0; + ctrl_active = 0; + alt_active = 0; + SHIFT => + redisplay_keyboard(t, minitel, (shifted ^= 1) ^ caps_locked, caps_locked); + CTRL => + ctrl_active ^= 1; + if (shifted) { + redisplay_keyboard(t, minitel, caps_locked, caps_locked); + shifted = 0; + } + alt_active = 0; + ALT => + alt_active ^= 1; + if (shifted) { + redisplay_keyboard(t, minitel, caps_locked, caps_locked); + shifted = 0; + } + ctrl_active = 0; + META => + if (move_key_enabled) { + if (meta_active ^= 1) + tk->cmd(t, ".b" + string METAKEY + " configure -image Move_on"); + else + tk->cmd(t, ".b" + string METAKEY + " configure -image Move_off"); + } + redisplay_keyboard(t, minitel, caps_locked, caps_locked); + shifted = 0; + ctrl_active = 0; + alt_active = 0; + * => + if (ctrl_active) { + keycode &= 16r1F; + ctrl_active = 0; + } else if (alt_active) { + keycode += MAGIC_PREFIX; + alt_active = 0; + } + if (meta_active && UP <= keycode && keycode <= RIGHT) { + spawn send_move_msg(direction[keycode - ARROW_PREFIX], sc); + } else + tk->keyboard(t, keycode); + if (shifted) { + redisplay_keyboard(t, minitel, caps_locked, caps_locked); + shifted = 0; + } + } + s := <-rc => + case s { + "kill" => + break Work; + "minitel" => + if (!minitel) { + minitel = 1; + redisplay_keyboard(t, minitel, shifted, caps_locked); + } + "standard" => + if (minitel) { + minitel = 0; + redisplay_keyboard(t, minitel, shifted, caps_locked); + } + } + } + } +} + +send_move_msg(dir: string, ch: chan of string) +{ + ch <-= dir; +} + + +# Redisplay keyboard to reflect current state (shifted or unshifted) +redisplay_keyboard(t: ref Tk->Toplevel, minitel, shifted, caps_locked: int) +{ + base: int; + + if (shifted) + base = NKEYS; + else + base = 0; + + for(i:=0; i<NKEYS; i++) { + n := base + i; + val := keyvals[n]; + key := keys[n]; + if (minitel) { + if (val >= int 'A' && val <= int 'Z') { + key = keys[n-NKEYS]; + } else if (val >= int 'a' && val <= int 'z') { + key = keys[n+NKEYS]; + } + } + + tk->cmd(t, ".b" + string i + " configure -text {" + key + + "} -command 'send keypress " + string val); + } + if (caps_locked) + tk->cmd(t, ".b" + string CAPSLOCKKEY + " configure -image Capslock_on"); + else + tk->cmd(t, ".b" + string CAPSLOCKKEY + " configure -image Capslock_off"); + tk->cmd(t, "update"); +} diff --git a/appl/examples/minitel/swkeyb.m b/appl/examples/minitel/swkeyb.m new file mode 100644 index 00000000..52206801 --- /dev/null +++ b/appl/examples/minitel/swkeyb.m @@ -0,0 +1,21 @@ +### +### This data and information is not to be used as the basis of manufacture, +### or be reproduced or copied, or be distributed to another party, in whole +### or in part, without the prior written consent of Lucent Technologies. +### +### (C) Copyright 1997 Lucent Technologies +### +### Written by N. W. Knauft +### + +# Revisions Copyright © 1998 Vita Nuova Limited. + +Keyboard: module +{ + PATH: con "/dis/wm/minitel/swkeyb.dis"; + + initialize: fn(t: ref Tk->Toplevel, ctxt : ref Draw->Context, + dot: string): chan of string; + chaninit: fn(t: ref Tk->Toplevel, ctxt : ref Draw->Context, + dot: string, rc: chan of string): chan of string; +}; |
