<> <> <> <> <> <> DIRECTORY BasicTime USING [earliestGMT, GMT, Now, Period], <> Commander USING [CommandProc, Register], Convert USING [IntFromRope, Error], Icons USING [IconFlavor, NewIconFromFile], InputFocus, IO, Menus, PFS, PFSCanonicalNames, PFSNames, PopUpSelection USING [Request], Process, Rope, RuntimeError USING [UNCAUGHT], SystemNames, TerminalIO, TiogaMenuOps USING [Open], TypeScript USING [Create, PutRope], UserProfile USING [Number], ViewerClasses USING [Viewer], ViewerIO USING [CreateViewerStreams, GetViewerFromStream], ViewerOps USING [PaintViewer, OpenIcon, EnumerateViewers, EnumProc], ViewerTools USING [SetSelection]; TerminalIOImpl: CEDAR MONITOR IMPORTS BasicTime, <> Commander, Convert, Icons, InputFocus, IO, Menus, PFS, PFSCanonicalNames, PFSNames, PopUpSelection, Process, Rope, RuntimeError, SystemNames, TiogaMenuOps, TypeScript, UserProfile, ViewerIO, ViewerOps, ViewerTools EXPORTS TerminalIO = BEGIN ROPE: TYPE = Rope.ROPE; UserAbort: PUBLIC SIGNAL = CODE; TimedOut: PUBLIC SIGNAL = CODE; viewer: ViewerClasses.Viewer _ NIL; in, out: IO.STREAM _ NIL; normalName: ROPE = "Terminal Viewer"; inputName: ROPE = "Terminal Viewer [Input Expected]"; normalIcon, inputIcon: Icons.IconFlavor; del: ROPE = "\177"; <<>> log: IO.STREAM; <<--using a backing file for viewer makes log unaccessible>> shouldFlush: BOOL _ FALSE; <<--whether log file should be flushed>> <<>> <<-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -->> <<--locking procedures>> occupied: BOOL _ FALSE; --module occupied next: CONDITION; -- another client might try entering Enter: ENTRY PROC [] = { ENABLE UNWIND => NULL; WHILE occupied DO WAIT next ENDLOOP; occupied _ TRUE; }; Leave: ENTRY PROC [] = { ENABLE UNWIND => NULL; occupied _ FALSE; BROADCAST next; }; EnterReader: PROC [] = { v: ViewerClasses.Viewer; Lock[]; Enter[]; v _ viewer; WHILE v=NIL OR v.destroyed DO InternalCreate[FALSE]; v _ viewer ENDLOOP; v.inhibitDestroy _ TRUE; WHILE v=NIL OR v.destroyed DO InternalCreate[TRUE]; v _ viewer ENDLOOP; v.icon _ inputIcon; v.name _ inputName; ViewerOps.PaintViewer[v, caption]; InputFocus.PushInputFocus[v, $TerminalIO]; ViewerTools.SetSelection[v];--order! SetSelection fools with the InputFocus! a feature }; LeaveReader: PROC [] = { v: ViewerClasses.Viewer _ viewer; foc: InputFocus.Focus _ InputFocus.GetInputFocus[]; IF v#NIL THEN { v.icon _ normalIcon; v.name _ normalName; v.inhibitDestroy _ FALSE; --conveniant here; usefull in all places where called ViewerOps.PaintViewer[v, caption]; }; IF foc#NIL AND foc.info = $TerminalIO THEN InputFocus.PopInputFocus[]; Leave[]; Free[]; }; ProcList: TYPE = LIST OF PROC; lockList: ProcList _ NIL; freeList: ProcList _ NIL; AddLock: PUBLIC ENTRY PROC [lock, free: PROC] = { ENABLE UNWIND => NULL; IF free#NIL THEN freeList _ CONS[free, freeList]; IF lock#NIL THEN IF lockList=NIL THEN lockList _ LIST[lock] ELSE FOR l: ProcList _ lockList, l.rest DO IF l.rest=NIL THEN {l.rest _ LIST[lock]; EXIT} ENDLOOP; }; Lock: PROC = { FOR l: ProcList _ lockList, l.rest WHILE l#NIL DO l.first[! RuntimeError.UNCAUGHT => CONTINUE] ENDLOOP }; Free: PROC = { FOR l: ProcList _ freeList, l.rest WHILE l#NIL DO l.first[! RuntimeError.UNCAUGHT => CONTINUE] ENDLOOP }; ReaderProtected: PROC [p: PROC] = { ENABLE UNWIND => LeaveReader[]; EnterReader[]; p[]; LeaveReader[]; }; <<>> <<-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -->> <<--creation of viewer>> Create: PROC [protected: BOOL] = { <<--its ok to risk creating accidently too many streams or viewer's >> <<--since this won't cause any wedge but only discomfort [orphaned viewers lying around] >> InternalCreate[protected] }; InternalCreate: PROC [protected: BOOL] = { <<--afterwards in any case: viewer #NIL (doesnt really mean good, but certainly not NIL) >> FindOldGuys: ViewerOps.EnumProc = { MakeOrphan: PROC [v: ViewerClasses.Viewer] = { IF v#NIL THEN { v.inhibitDestroy _ FALSE; v.icon _ typescript; v.name _ "orphaned split terminal viewer"; IO.PutRope[log, "terminal viewer orphaned\n"]; IO.Flush[log]; ViewerOps.PaintViewer[v, caption]; TypeScript.PutRope[v, "\n... terminal viewer orphaned ...\n"]; }; }; IF ~v.destroyed AND v.file=NIL AND v.class.flavor=$Typescript THEN IF Rope.Equal[v.name, normalName] OR Rope.Equal[v.name, inputName] THEN MakeOrphan[v ! RuntimeError.UNCAUGHT => CONTINUE]; }; ReallyCreate: PROC [] = { v: ViewerClasses.Viewer _ TypeScript.Create[info: [name: normalName, inhibitDestroy: protected, iconic: FALSE, icon: normalIcon, column: right]]; Menus.AppendMenuEntry[v.menu, Menus.CreateEntry[ name: "Previous", fork: TRUE, proc: PreviousLog ]]; [in: in, out: out] _ ViewerIO.CreateViewerStreams[name: normalName, viewer: v, editedStream: TRUE]; viewer _ v; IO.PutRope[log, "terminal viewer created\n"]; IO.Flush[log]; lastTimeMsg _ BasicTime.earliestGMT; --so the time will be printed }; <<--try re-using old pieces, but only if not from input>> oldOut: IO.STREAM _ out; oldIn: IO.STREAM _ in; IF oldOut#NIL AND oldIn#NIL AND ~protected THEN { v: ViewerClasses.Viewer; err: BOOL _ FALSE; v _ ViewerIO.GetViewerFromStream[oldOut]; IF v#NIL AND ~v.destroyed AND (v=ViewerIO.GetViewerFromStream[oldIn]) THEN { <<--we dare trying to reuse a split viewer, >> <<--but first we try whether it wont cause errors>> BEGIN IO.Flush[oldOut ! RuntimeError.UNCAUGHT => GOTO oops]; IO.Reset[oldOut ! RuntimeError.UNCAUGHT => GOTO oops]; IO.Reset[oldIn ! RuntimeError.UNCAUGHT => GOTO oops]; v.icon _ normalIcon; IF ~v.destroyed THEN { [in: in, out: out] _ ViewerIO.CreateViewerStreams[name: normalName, viewer: v, editedStream: TRUE]; viewer _ v; IO.PutRope[log, "usage of different terminal viewer\n"]; IO.Flush[log]; RETURN; }; EXITS oops => {} END; }; }; <<--try to mark other [split] viewers as bad>> ViewerOps.EnumerateViewers[FindOldGuys ! RuntimeError.UNCAUGHT => CONTINUE]; <<--the actual creation>> ReallyCreate[] }; <<-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -->> <<--short cuts>> InternalPut: PROC [text: ROPE] = { IF viewer=NIL OR viewer.destroyed THEN InternalCreate[FALSE]; <<--asking for viewer.destroyed helps to discover bad splits, but is not mandatory >> IO.PutRope[out, text ! IO.Error => { IF ec=StreamClosed THEN { InternalCreate[TRUE]; IO.PutRope[out, text]; CONTINUE } }]; IO.PutRope[log, text]; shouldFlush _ TRUE; }; PutRope: PUBLIC PROC [text: ROPE] = { ENABLE UNWIND => Leave[]; Enter[]; InternalPut[text]; Leave[] }; PutRopes: PUBLIC PROC [t1, t2, t3: ROPE] = { ENABLE UNWIND => Leave[]; Enter[]; InternalPut[t1]; InternalPut[t2]; InternalPut[t3]; Leave[] }; PutF: PUBLIC PROC [format: ROPE _ NIL, v1, v2, v3: IO.Value_[null[]]] = { <<--actual usage of IO.PutF inside lock allows clients to use looks>> ENABLE UNWIND => Leave[]; Enter[]; IF viewer=NIL OR viewer.destroyed THEN InternalCreate[FALSE]; IO.PutF[out, format, v1, v2, v3 ! IO.Error => { IF ec=StreamClosed THEN { InternalCreate[TRUE]; IO.PutF[out, format, v1, v2, v3]; CONTINUE } } ]; IO.PutF[log, format, v1, v2, v3]; shouldFlush _ TRUE; Leave[] }; PutFL: PUBLIC PROC [format: ROPE _ NIL, list: LIST OF IO.Value] = { <<--actual usage of IO.PutFL inside lock allows clients to use looks>> ENABLE UNWIND => Leave[]; Enter[]; IF viewer=NIL OR viewer.destroyed THEN InternalCreate[FALSE]; IO.PutFL[out, format, list ! IO.Error => { IF ec=StreamClosed THEN { InternalCreate[TRUE]; IO.PutFL[out, format, list]; CONTINUE } } ]; IO.PutFL[log, format, list]; shouldFlush _ TRUE; Leave[] }; PutF1: PUBLIC PROC [format: ROPE _ NIL, value: IO.Value] = { <<--actual usage of IO.PutF1 inside lock allows clients to use looks>> ENABLE UNWIND => Leave[]; Enter[]; IF viewer=NIL OR viewer.destroyed THEN InternalCreate[FALSE]; IO.PutF1[out, format, value ! IO.Error => { IF ec=StreamClosed THEN { InternalCreate[TRUE]; IO.PutF1[out, format, value]; CONTINUE } } ]; IO.PutF1[log, format, value]; shouldFlush _ TRUE; Leave[] }; <<>> <<-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -->> <<--input procedures>> RequestRope: PUBLIC PROC [prompt: ROPE _ NIL, timeOut: NAT] RETURNS [ROPE] = { RETURN [IF timeOut=0 THEN RequestRope0[prompt] ELSE RequestRope1[prompt, timeOut]] }; IOState: TYPE = {ok, repeat, abort, undef}; <<>> InternalGetLine: PROC [promp: ROPE] RETURNS [line: ROPE, state: IOState_ok] = { ENABLE { IO.Error => IF ec=StreamClosed THEN {state_repeat; CONTINUE}; IO.Rubout => {state_abort; IO.Reset[in]; CONTINUE}; RuntimeError.UNCAUGHT => {state_repeat; CONTINUE}; }; IF viewer.iconic THEN ViewerOps.OpenIcon[viewer]; InternalPut[promp]; IO.Flush[out ! RuntimeError.UNCAUGHT => CONTINUE]; ViewerTools.SetSelection[viewer]; line _ IO.GetLineRope[in]; IF Rope.Find[line, del]>=0 THEN state_abort; }; RequestRope0: PROC [prompt: ROPE _ NIL] RETURNS [r: ROPE] = { state: IOState _ ok; DoIt: PROC [] = {[r, state] _ InternalGetLine[prompt]}; DO ReaderProtected[DoIt]; SELECT state FROM ok => RETURN; repeat => PutRope["XXX\n"]; abort => {PutRope[" ..user abort\n"]; SIGNAL UserAbort} ; ENDCASE => {PutRope[" ..??\n"]; SIGNAL UserAbort}; ENDLOOP; }; xProcess: PROCESS; xStateCondition: CONDITION; xState: IOState _ undef; xResult: ROPE; RequestRope1: PUBLIC PROC [prompt: ROPE, timeOut: NAT] RETURNS [result: ROPE] = { state: IOState _ ok; DoIt: PROC [] = { xState _ undef; IF timeOut=0 THEN TRUSTED {Process.DisableTimeout[@xStateCondition]} ELSE TRUSTED { Process.SetTimeout[@xStateCondition, Process.SecondsToTicks[timeOut]] }; ForkRopeGetter[prompt]; TRUSTED {JOIN xProcess}; state _ xState; result _ xResult; IF state=ok THEN IF Rope.Find[result, del]>=0 THEN state _ abort }; DO ReaderProtected[DoIt]; SELECT state FROM ok => RETURN; repeat => PutRope["XXX\n"]; abort => {PutRope[" ..user abort\n"]; SIGNAL UserAbort} ; undef => SIGNAL TimedOut ENDCASE => {PutRope[" ..??\n"]; SIGNAL UserAbort}; ENDLOOP }; ForkRopeGetter: ENTRY PROC [prompt: ROPE_NIL] = { ENABLE UNWIND => NULL; xProcess _ FORK ForkedTryToGetRope[prompt]; WAIT xStateCondition; IF xState#ok AND xState#abort THEN TRUSTED {Process.Abort[xProcess]}; }; SetGlobalState: ENTRY PROC [s: IOState] = { ENABLE UNWIND => NULL; Process.CheckForAbort[]; xState _ s; BROADCAST xStateCondition; }; ForkedTryToGetRope: PROC [prompt: ROPE_NIL] = { ReallyTryToGetRope: PROC [prompt: ROPE] = { d: IOState; DoIt: PROC [] = { ENABLE { UNWIND => IO.Reset[in]; IO.Error => IF ec=StreamClosed THEN {d_repeat; IO.Reset[in]; CONTINUE}; IO.Rubout => {d_abort; IO.Reset[in]; CONTINUE}; }; InternalPut[prompt]; IF viewer.iconic THEN ViewerOps.OpenIcon[viewer]; xResult _ IO.GetLineRope[in]; d _ ok; }; DO d _ undef; DoIt[]; IF d=ok OR d=abort THEN {SetGlobalState[d]; RETURN}; IF d=repeat THEN InternalPut["XXX\n"] ENDLOOP; }; <<>> ReallyTryToGetRope[prompt ! ABORTED => GOTO NeverMind] EXITS NeverMind => NULL; }; <<-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -->> <<--stream input stuff>> <<-- This is unprotected code, so you should be sure that no one else will be doing terminalIO while inputing from the stream>> TIS: PUBLIC PROC [] RETURNS [stream: IO.STREAM] = { RETURN[IO.CreateStream[streamProcs: myinputStreamProcs, streamData: NIL]]; }; myinputStreamProcs: REF IO.StreamProcs _ IO.CreateStreamProcs[ variety: $input, class: $TerminalIO, getChar: MyGetChar, endOf: MyEndOf, charsAvail: MyCharsAvail, close: MyClose ]; MyGetChar: PROC [self: IO.STREAM] RETURNS [char: CHAR] = { ENABLE UNWIND => Leave[]; Enter[]; IF viewer=NIL OR viewer.destroyed THEN InternalCreate[FALSE]; IF viewer.iconic THEN ViewerOps.OpenIcon[viewer]; IO.Flush[out ! RuntimeError.UNCAUGHT => CONTINUE]; ViewerTools.SetSelection[viewer]; char _ IO.GetChar[in]; Leave[]; }; MyEndOf: PROC [self: IO.STREAM] RETURNS [endOf: BOOL _ TRUE] = { endOf _ FALSE; }; MyCharsAvail: PROC [self: IO.STREAM, wait: BOOL _ FALSE] RETURNS [INT] = { RETURN[INT.LAST]; }; <<-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -->> <<-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -->> <<--stream output stuff>> TOS: PUBLIC PROC [] RETURNS [stream: IO.STREAM] = { RETURN[IO.CreateStream[streamProcs: myStreamProcs, streamData: NIL]]; }; myStreamProcs: REF IO.StreamProcs _ IO.CreateStreamProcs[ variety: $output, class: $TerminalIO, putChar: MyPutChar, putBlock: MyPutBlock, unsafePutBlock: MyUnsafePutBlock, flush: MyFlush, eraseChar: MyEraseChar, reset: MyFlush, close: MyClose ]; MyPutChar: PROC [self: IO.STREAM, char: CHAR] = { ENABLE UNWIND => Leave[]; Enter[]; IF viewer=NIL OR viewer.destroyed THEN InternalCreate[FALSE]; IO.PutChar[out, char ! IO.Error => { IF ec=StreamClosed THEN { InternalCreate[TRUE]; IO.PutChar[out, char]; CONTINUE } }]; IO.PutChar[log, char]; shouldFlush _ TRUE; Leave[]; }; MyPutBlock: PROC [self: IO.STREAM, block: REF READONLY TEXT, startIndex: NAT, count: NAT] = { ENABLE UNWIND => Leave[]; Enter[]; IF viewer=NIL OR viewer.destroyed THEN InternalCreate[FALSE]; IO.PutBlock[out, block, startIndex, count ! IO.Error => { IF ec=StreamClosed THEN { InternalCreate[TRUE]; IO.PutBlock[out, block, startIndex, count]; CONTINUE } }]; IO.PutBlock[log, block, startIndex, count]; shouldFlush _ TRUE; Leave[]; }; MyUnsafePutBlock: PROC [self: IO.STREAM, block: IO.UnsafeBlock] = { ENABLE UNWIND => Leave[]; Enter[]; IF viewer=NIL OR viewer.destroyed THEN InternalCreate[FALSE]; IO.UnsafePutBlock[out, block ! IO.Error => { IF ec=StreamClosed THEN { InternalCreate[TRUE]; IO.UnsafePutBlock[out, block]; CONTINUE } }]; IO.UnsafePutBlock[log, block]; shouldFlush _ TRUE; Leave[]; }; MyFlush: PROC [self: IO.STREAM] = { IO.Flush[log]; }; MyEraseChar: PROC [self: IO.STREAM, char: CHAR] = { IO.EraseChar[out, char]; IO.EraseChar[log, char]; }; MyClose: PROC [self: IO.STREAM, abort: BOOL] = { IO.Flush[log]; }; <<-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -->> <<--input conveniances>> RequestInt: PUBLIC PROC [prompt: Rope.ROPE _ NIL, timeOut: NAT _ 0] RETURNS [i: INT] = { DO ok: BOOL _ TRUE; r: Rope.ROPE _ RequestRope[prompt, timeOut]; i _ Convert.IntFromRope[r ! RuntimeError.UNCAUGHT => {ok_FALSE; CONTINUE}]; IF ok THEN RETURN; PutRope[" ?? (integer), please repeat: \n"]; ENDLOOP; }; Confirm: PUBLIC PROC [text: Rope.ROPE, help: Rope.ROPE, timeOut: NAT, dontLog: BOOL] RETURNS [yes: BOOL] = { Ask: PROC RETURNS [BOOL] = { RETURN [ PopUpSelection.Request[ header: "Confirm", headerDoc: "Confirm by hitting entry; discard by hitting outside", choice: LIST[text], choiceDoc: LIST[help], timeOut: timeOut ]=1 ]; }; AskLogged: PROC = { InternalPut["confirm: "]; InternalPut[text]; InternalPut[": "]; yes _ Ask[]; InternalPut[(IF yes THEN "yes\n" ELSE "no\n")]; }; IF dontLog THEN yes _ Ask[] ELSE ReaderProtected[AskLogged] }; RequestSelection: PUBLIC PROC [ header: Rope.ROPE _ NIL, choice: LIST OF Rope.ROPE, headerDoc: Rope.ROPE _ NIL, choiceDoc: LIST OF Rope.ROPE _ NIL, default: NAT _ 0, timeOut: NAT _ 0, position: REF _ NIL, dontLog:BOOL _ FALSE ] RETURNS [n: INT _ 0] = { Request: PROC = { IF header#NIL AND ~dontLog THEN { InternalPut["pop up selection: "]; InternalPut[header]; InternalPut["\n "]; }; n _ PopUpSelection.Request[header: header, headerDoc: headerDoc, choice: choice, choiceDoc: choiceDoc, timeOut: timeOut, default: default, position: position]; IF ~dontLog THEN { IF n<0 THEN InternalPut["timed out\n"] ELSE IF n=0 THEN InternalPut["skipped\n"] ELSE {c: LIST OF Rope.ROPE _ choice; WHILE n>1 DO c _ c.rest; IF c=NIL THEN RETURN; ENDLOOP; InternalPut["select: "]; InternalPut[c.first]; InternalPut["\n"]; }; }; }; IF (choice=NIL) OR (choice.first=NIL) THEN ERROR; ReaderProtected[Request] }; <<>> <<-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -->> <<--log and commands>> LogFileName: PROC [] RETURNS [Rope.ROPE] = { RETURN [PFS.RopeFromPath[PFS.GetInfo[PFS.OpenFileFromStream[log]].fullFName]] }; CreateLog: PROC [] = { log _ PFS.StreamOpen[fileName: PFS.PathFromRope["-ux:/tmp/TerminalIO.log"], accessOptions: append, createOptions: [keep: 5, mutability: mutable]]; IO.SetIndex[log, 0]; IO.SetLength[log, 0]; IO.PutF1[log, "log file for terminal created %g\n", IO.time[]] }; PreviousLog: Menus.ClickProc = { fullName: Rope.ROPE _ LogFileName[]; filePath: PFSNames.PATH _ PFS.PathFromRope[fullName]; name: Rope.ROPE _ PFS.RopeFromPath[PFSNames.StripVersionNumber[filePath]]; versionRope: Rope.ROPE _ PFSCanonicalNames.UnparseVersion[PFSCanonicalNames.ParseVersion[fullName].version]; version: INT _ Convert.IntFromRope[versionRope ! Convert.Error => GOTO oops]; IF version<2 THEN GOTO oops; name _ IO.PutFR["%g!%g", [rope[name]], [integer[version-1]]]; [] _ TiogaMenuOps.Open[fileName: name]; PutRope["previous log file opened\n"]; EXITS oops => PutRope["opening previous log file failed\n"]; }; Command: Commander.CommandProc = { ENABLE Convert.Error => GOTO convertError; IF Rope.Match["*open*", cmd.commandLine, FALSE] THEN { PutRope["open terminal viewer\n"]; --side effect: creates viewer ViewerOps.OpenIcon[viewer]; }; IF Rope.Match["*new*", cmd.commandLine, FALSE] OR Rope.Match["*close*", cmd.commandLine, FALSE] THEN { oldLog: IO.STREAM _ log; oldName: Rope.ROPE _ LogFileName[]; PutF1["start new terminal log file [%g]\n", IO.time[]]; CreateLog[]; PutF1["previous log file ""%g"" closed\n", IO.rope[oldName]]; IO.Close[oldLog]; IO.PutF1[cmd.out, "previous log file ""%g"" closed\n", IO.rope[oldName]]; }; IF Rope.Match["*time*", cmd.commandLine, FALSE] THEN { time: INT; timePos: INT _ Rope.SkipTo[ s: cmd.commandLine, pos: Rope.Index[s1: cmd.commandLine, s2: "time", case: FALSE], skip: " " ]; time _ Convert.IntFromRope[Rope.Substr[base: cmd.commandLine, start: timePos]]; timeMsgInterval _ IF time<0 OR time>600 THEN 0 ELSE time*60; lastTimeMsg _ BasicTime.earliestGMT; }; EXITS convertError => IO.PutRope[cmd.err, "syntax error in time\n"]; }; CheckInhibitDestroy: ENTRY PROC [] = { v: ViewerClasses.Viewer _ viewer; IF v#NIL AND ~occupied THEN v.inhibitDestroy _ FALSE; }; lastTimeMsg: BasicTime.GMT _ BasicTime.earliestGMT; timeMsgInterval: INT _ 60*UserProfile.Number["TerminalIO.TimeMsg", 30]; Flusher: PROC [] = { <<--directly flushing after each output would make TerminalIO far to slow>> <<--process also used to write periodic time mesages>> flush: BOOL; DO Process.PauseMsec[32000]; --a little bit more than 30 seconds [so doesn't skip time msg] <<--flushing>> IF flush _ shouldFlush THEN { shouldFlush _ FALSE; IO.Flush[log ! RuntimeError.UNCAUGHT => CONTINUE]; }; CheckInhibitDestroy[]; <<--time message>> IF viewer#NIL AND ~occupied AND ~viewer.destroyed AND timeMsgInterval>0 AND (BasicTime.Period[from: lastTimeMsg, to: BasicTime.Now[]]>timeMsgInterval) THEN { lastTimeMsg _ BasicTime.Now[]; PutF["%l%g%l\n", [rope["i"]], [time[lastTimeMsg]], [rope[" "]]]; }; ENDLOOP }; <> <> <> <> <<};>> <<>> <> <> <> <<};>> <<>> CreateLog[]; normalIcon _ Icons.NewIconFromFile["TerminalIO.icons", 0 ! RuntimeError.UNCAUGHT => CONTINUE]; inputIcon _ Icons.NewIconFromFile["TerminalIO.icons", 1 ! RuntimeError.UNCAUGHT => CONTINUE]; Commander.Register[Rope.Cat["///", SystemNames.ReleaseName[], "/Commands/TerminalIO"], Command, "usage {TerminalIO ~ | open | new}"]; TRUSTED {Process.Detach[FORK Flusher]}; <> END.