<> <> DIRECTORY Atom USING [GetPName], CIFS USING [GetFC, Open, read], Commander USING [CommandProc, CommandObject, Enumerate, Lookup, Register, Handle], ConvertUnsafe USING [AppendRope], Exec USING [commandLine], File USING [Capability, nullCapability], FileIO USING [Open, OpenFailed], Generator USING [Handle, Produce], IO USING [AppendStreams, ChangeDeliverWhen, Close, CreateFilterCommentsStream, CR, DeliverWhenProc, EndOf, EveryThing, GetOutputStreamRope, LookupData, PeekChar, PutChar, PutF, PutRope, RemoveData, rope, ROPE, RIS, ROS, SetEcho, SetIndex, SP, StoreData, STREAM, BreakProc, CharProc, GetSequence, GetToken, GetRefAny, IDProc, SkipOver, WhiteSpace], List USING [DotCons, Nconc1, Sort], MessageWindow USING [Append], ProcessProps USING [AddPropList], CommandProcOps USING [PutProperty], Rope USING [Cat, Concat, Equal, Fetch, Find, IsEmpty, Length, Replace, Substr], RTFiles USING [GetMapList], Spell USING [GeneratorFromEnumerator, SpellingGenerator, IsAPattern, Modes], TTY USING [Handle], SymTab USING [Create, Fetch, Store, Ref], UnsafeStorage USING [GetSystemUZone], UserExec USING [DoIt, AcquireResource, CommandProc, ExecHandle, GetMatchingFileList, GetStreams, GetTheOne, MethodProc, ReleaseResource, ErrorThisEvent, AcquireStreams, ReleaseStreams, TransformProc, HistoryEvent], UserExecExtras USING [CorrectionDisabled], UserExecPrivate USING [CommandRecord, commandString, EventFailed, ExecDeliverWhen, ExecPrivateRecord, GetExecFromStream, HistoryEventPrivateRecord, FileNotFound, methodList, MethodRecord, PrintCommand, RopeFromCMFile, RunBCDFile, StripComments, w, Zone, GetPrivateStuff], UserProfile USING [CallWhenProfileChanges, ProfileChangedProc, Token], VersionMapOps USING [SourceFileList, FindSource] ; UserExecRegCmdsImpl: CEDAR PROGRAM IMPORTS Atom, CIFS, Commander, CommandProcOps, ConvertUnsafe, Exec, FileIO, Generator, IO, List, MessageWindow, ProcessProps, Rope, RTFiles, Spell, SymTab, UnsafeStorage, UserExec, UserExecExtras, UserExecPrivate, UserProfile, VersionMapOps EXPORTS UserExec, UserExecPrivate = BEGIN OPEN IO; <> ExecPrivateRecord: PUBLIC TYPE = UserExecPrivate.ExecPrivateRecord; <> HistoryEventPrivateRecord: PUBLIC TYPE = UserExecPrivate.HistoryEventPrivateRecord; <<>> <> CallRegisteredProc: PUBLIC PROC [command: ROPE, event: UserExec.HistoryEvent, exec: UserExec.ExecHandle] RETURNS[handled: BOOLEAN _ TRUE] = { commandLineStream: STREAM = event.commandLineStream; registration: REF UserExecPrivate.CommandRecord _ GetRegistrationData[command]; commanderProc: Commander.CommandProc = Commander.Lookup[command].proc; nameOfBCD: ROPE; IF registration = NIL THEN RETURN[CallCommanderProc[command, event, exec]]; IF NOT Rope.Equal[registration.name, "_"] THEN SetUpCommandLine[event: event, exec: exec]; IF (nameOfBCD _ registration.nameOfBCD) # NIL AND commanderProc = undefined THEN -- check for commanderProc because the file may have been run and the command registered with commander directl, e.g. Walnut { doc: ROPE = registration.documentation; error: ROPE; notFound: BOOL _ FALSE; error _ UserExecPrivate.RunBCDFile[fileName: nameOfBCD, out: UserExec.GetStreams[exec].out ! UserExecPrivate.FileNotFound => { error _ Rope.Concat[nameOfBCD, " must be on your local disk to execute this command"]; notFound _ TRUE; CONTINUE; } ].error; IF notFound THEN TRUSTED { -- what errors to catch? lst: VersionMapOps.SourceFileList; cap: File.Capability _ File.nullCapability; MessageWindow.Append["Loading Version Map...", TRUE]; lst _ VersionMapOps.FindSource[short: nameOfBCD, mapList: RTFiles.GetMapList[]]; IF lst # NIL THEN { MessageWindow.Append[Rope.Cat["Loading ", lst.first.name, "..."]]; cap _ CIFS.GetFC[CIFS.Open[name: lst.first.name, mode: CIFS.read]]; }; IF cap # File.nullCapability THEN error _ UserExecPrivate.RunBCDFile[fileName: lst.first.name, fileCapability: cap, out: UserExec.GetStreams[exec].out].error; }; IF error # NIL THEN UserExecPrivate.EventFailed[event: event, msg: error, offender: nameOfBCD]; registration _ GetRegistrationData[name: registration.name]; registration.nameOfBCD _ NIL; IF registration.commandProc = NIL AND registration.transformProc = NIL AND registration.oldStyleProc = NIL AND Commander.Lookup[command].proc = undefined THEN { UserExec.GetStreams[exec].out.PutF["*n*m%g was run and did not register any commands*s\n", rope[nameOfBCD]]; RETURN[TRUE]; }; IF registration # NIL AND registration.documentation = NIL THEN registration.briefDocumentation _ registration.documentation _ doc; RETURN[CallRegisteredProc[command, event, exec]]; }; IF registration.commandProc # NIL THEN -- if there is a UserExec.commandProc, use it even though there is a commander CommandProc, because it may (probably) have more functionality { ok: BOOL; msg: ROPE; inner: Commander.CommandProc = { [ok, msg] _ registration.commandProc[event, exec, registration.clientData]; }; PushPropsAndCallProc[inner, command, event, exec]; IF NOT ok THEN ERROR UserExec.ErrorThisEvent[event, msg]; -- msg should be stored in history, even on success. } ELSE IF registration.oldStyleProc # NIL THEN { Done: PROC = {[] _ UserExec.ReleaseResource[$Exec]}; [] _ UserExec.AcquireResource[resource: $Exec, owner: registration.name, exec: exec]; TRUSTED { ENABLE UNWIND => Done[]; private: REF ExecPrivateRecord = exec.privateStuff; inner: Commander.CommandProc = TRUSTED { registration.oldStyleProc[] }; UserExecPrivate.w _ private.execDotW; UserExecPrivate.commandString.length _ 0; IF Rope.Length[event.commandLine] > UserExecPrivate.commandString.maxlength THEN -- long command file. make string larger. Exec.commandLine.s _ UserExecPrivate.commandString _ UnsafeStorage.GetSystemUZone[].NEW[StringBody[Rope.Length[event.commandLine]]]; ConvertUnsafe.AppendRope[to: UserExecPrivate.commandString, from: event.commandLine]; Exec.commandLine.i _ 0; PushPropsAndCallProc[inner, command, event, exec]; Done[]; }; } ELSE IF registration.transformProc # NIL THEN { input: ROPE = registration.transformProc[event, exec, registration.clientData]; privateEvent: REF UserExecPrivate.HistoryEventPrivateRecord = event.privateStuff; IF Rope.Equal[input, event.input] THEN ERROR UserExec.ErrorThisEvent[event: event, msg: "You are looping"]; privateEvent.showInput _ TRUE; UserExec.DoIt[input, exec, event]; } ELSE IF commanderProc # undefined THEN RETURN[CallCommanderProc[command, event, exec]] -- command had registrationData, but nothing to do. Occurs for commands obtained from catalogue which when run, register themselves with commander only, e.g. smodel ELSE RETURN[FALSE]; RETURN[TRUE]; }; SetUpCommandLine: PROC [event: UserExec.HistoryEvent, exec: UserExec.ExecHandle] = { commandLine: ROPE _ event.commandLine; pos: INT; commandLine _ ExpandCommandLine[line: commandLine, event: event, exec: exec]; <> WHILE (pos _ Rope.Find[s1: commandLine, s2: "\\"]) # -1 DO commandLine _ Rope.Replace[base: commandLine, start: pos, len: 1]; ENDLOOP; event.commandLine _ commandLine; [] _ RIS[rope: commandLine, oldStream: event.commandLineStream]; -- for commands registered by UserExec.RegisterCommand }; -- of SetUpCommandLine ExpandCommandLine: PUBLIC PROC [line: ROPE, modes: Spell.Modes _ NIL, event: UserExec.HistoryEvent, exec: UserExec.ExecHandle] RETURNS [ROPE] = { inRopeStream: STREAM; pos: INT; <> WHILE (pos _ Rope.Find[s1: line, s2: "@"]) # -1 AND (pos = 0 OR Rope.Fetch[line, pos - 1] # '\\) DO fileName: ROPE; inRopeStream _ RIS[rope: line, oldStream: inRopeStream]; inRopeStream.SetIndex[pos]; fileName _ IO.GetToken[inRopeStream, IO.IDProc]; line _ Rope.Replace[base: line, start: pos, len: Rope.Length[fileName], with: UserExecPrivate.RopeFromCMFile[file: fileName, event: event, exec: exec].contents]; ENDLOOP; line _ UserExecPrivate.StripComments[event, line]; <> <> <> <> <> <> <> IF Spell.IsAPattern[line] THEN { r: ROPE; asFile: BOOLEAN _ FALSE; inRopeStream _ RIS[rope: line, oldStream: inRopeStream]; WHILE Rope.Length[r _ IO.GetToken[inRopeStream, IO.IDProc]] # 0 DO IF Spell.IsAPattern[r] THEN {outRopeStream: STREAM = IO.ROS[]; FOR l: LIST OF ROPE _ -- IF ~asFile THEN LookupCommands[r, exec] ELSE-- UserExec.GetMatchingFileList[file: r, event: event, exec: exec, modes: modes], l.rest UNTIL l = NIL DO outRopeStream.PutRope[l.first]; IF l.rest # NIL THEN outRopeStream.PutChar[SP]; ENDLOOP; line _ Rope.Replace[base: line, start: Rope.Find[line, r], len: Rope.Length[r], with: outRopeStream.GetOutputStreamRope[]]; }; asFile _ TRUE; ENDLOOP; }; RETURN[line]; }; -- of ExpandCommandLine GetRestOfStream: PUBLIC PROC [in: STREAM, includeLast: BOOL _ TRUE] RETURNS[value: ROPE] = { value _ IO.GetSequence[in, IO.EveryThing]; IF NOT includeLast THEN value _ Rope.Substr[base: value, len: Rope.Length[value] - 1]; }; DoesUserMean: PUBLIC PROC [rope: ROPE, exec: UserExec.ExecHandle, intro: ROPE _ NIL] = { out: STREAM = UserExec.GetStreams[exec: exec].out; out.PutRope[IF intro = NIL THEN "perhaps you mean:\n" ELSE intro]; out.PutRope[rope]; ReadQuietly[rope, exec]; }; <> ReadQuietly: PUBLIC PROC [rope: ROPE, exec: UserExec.ExecHandle] = { in: STREAM; private: REF ExecPrivateRecord = UserExecPrivate.GetPrivateStuff[exec]; private.continuation _ TRUE; -- tells exec not to increment event number and to reuse event. private.dontPrompt _ TRUE; IF NOT rope.IsEmpty[] THEN { in _ UserExec.AcquireStreams[exec: exec].in; [] _ in.SetEcho[NIL]; -- turn off echoing. in.StoreData[key: $Count, data: NEW[INT _ rope.Length[]]]; [] _ IO.ChangeDeliverWhen[in, StuffBufferDeliverWhen]; -- turns echoing back on as soon as all characters consumed. AppendStreams[in, RIS[rope]]; }; }; <> StuffBufferDeliverWhen: IO.DeliverWhenProc -- PROC[char: CHAR, stream: STREAM] RETURNS[BOOL] -- = { exec: UserExec.ExecHandle = UserExecPrivate.GetExecFromStream[stream]; count: REF INT; private: REF UserExecPrivate.ExecPrivateRecord = UserExecPrivate.GetPrivateStuff[exec]; in, out: STREAM; [in, out] _ UserExec.GetStreams[exec: exec]; count _ NARROW[in.LookupData[$Count]]; count^ _ count^ - 1; IF count^ <= 0 THEN { [] _ IO.ChangeDeliverWhen[in, UserExecPrivate.ExecDeliverWhen]; [] _ in.SetEcho[out]; in.RemoveData[key: $Count]; UserExec.ReleaseStreams[exec: exec]; }; RETURN[FALSE]; }; <> RegisterCommand: PUBLIC PROC [name: ROPE, proc: UserExec.CommandProc, briefDoc, doc: ROPE _ NIL, clientData: REF ANY _ NIL] = { Register[name: name, proc: proc, briefDoc: briefDoc, doc: doc, clientData: clientData]; }; RegisterTransformation: PUBLIC PROC [name: ROPE, proc: UserExec.TransformProc, briefDoc, doc: ROPE _ NIL, clientData: REF ANY _ NIL]= { Register[name: name, transformProc: proc, briefDoc: briefDoc, doc: doc, clientData: clientData]; }; RegisterMethod: PUBLIC PROC [name: ROPE, proc: UserExec.MethodProc, doc: ROPE _ NIL, clientData: REF ANY _ NIL] = { r: REF UserExecPrivate.MethodRecord _ NIL; IF name # NIL THEN FOR lst: LIST OF REF UserExecPrivate.MethodRecord _ UserExecPrivate.methodList, lst.rest UNTIL lst = NIL DO IF Rope.Equal[lst.first.name, name] THEN {r _ lst.first; EXIT} ENDLOOP; IF r = NIL THEN TRUSTED { -- Loophole for polymorphism r _ UserExecPrivate.Zone.NEW[UserExecPrivate.MethodRecord _ []]; UserExecPrivate.methodList _ LOOPHOLE[List.Nconc1[LOOPHOLE[UserExecPrivate.methodList, LIST OF REF ANY], r]]; }; -- important that methods be executed in order defined r^ _ [proc: proc, name: name, doc: doc, clientData: clientData]; }; Register: PUBLIC PROC [ name: ROPE _ NIL, proc: UserExec.CommandProc _ NIL, oldStyleProc: UNSAFE PROCEDURE _ NIL, transformProc: UserExec.TransformProc _ NIL, briefDoc, doc: ROPE _ NIL, nameOfBCD: ROPE _ NIL, clientData: REF ANY _ NIL, fromCatalogue: BOOL _ FALSE ] = { <> <> <> <> <> <<};>> i: INT _ Rope.Find[s1: name, s2: "."]; r: REF UserExecPrivate.CommandRecord; commanderProc: Commander.CommandProc; IF i # -1 THEN name _ Rope.Substr[name, 0, i]; -- old style registration was to register mumble.~ commanderProc _ Commander.Lookup[key: name].proc; Commander.Register[key: name, proc: IF commanderProc = NIL THEN undefined ELSE commanderProc, doc: briefDoc]; -- if happen to be registering a command with same name as one already registered in commander, leave the commander one alone so when run from command tool, it will continue to work the same as before r _ GetRegistrationData[name]; IF r = NIL THEN { r _ UserExecPrivate.Zone.NEW[UserExecPrivate.CommandRecord _ []]; RegisterData[name, r]; } ELSE IF (r.commandProc # NIL OR r.oldStyleProc # NIL) AND nameOfBCD # NIL THEN { -- command already registered with procedure is being reregistered indirectly with nameOfBCD. In this case, user should have to explicitly run the bcd. IF doc # NIL THEN r.documentation _ doc; RETURN; }; r^ _ [name: name, commandProc: proc, transformProc: transformProc, briefDocumentation: IF briefDoc = NIL THEN doc ELSE briefDoc, documentation: IF doc = NIL THEN briefDoc ELSE doc, oldStyleProc: oldStyleProc, nameOfBCD: nameOfBCD, clientData: clientData, fromCatalogue: fromCatalogue]; <<{>> <> < NULL;>> <> <<};>> <> <<};>> }; -- Register registrationTable: SymTab.Ref _ SymTab.Create[30, FALSE]; GetRegistrationData: PUBLIC PROC [name: ROPE] RETURNS[REF UserExecPrivate.CommandRecord] = { RETURN[NARROW[SymTab.Fetch[registrationTable, name].val, REF UserExecPrivate.CommandRecord]]; }; RegisterData: PUBLIC PROC [command: ROPE, val: REF ANY] = { [] _ SymTab.Store[x: registrationTable, key: command, val: val]; }; <> LookupCommand: PUBLIC PROC [name: ROPE, event: UserExec.HistoryEvent, exec: UserExec.ExecHandle _ NIL] RETURNS[fullName: ROPE] = { command: ROPE = name; out: STREAM; matches: LIST OF ROPE; matches _ LookupCommands[name, event, exec]; IF matches # NIL THEN TRUSTED { -- LOOPHOLE for polymorphism ENABLE UNWIND => IF out # NIL THEN UserExec.ReleaseStreams[exec]; IF matches.rest = NIL THEN RETURN[matches.first] ; out _ UserExec.AcquireStreams[exec].out; out.PutF["*n%g is ambiguous. It matches with:\n", rope[name]]; FOR l: LIST OF ROPE _ LOOPHOLE[List.Sort[LOOPHOLE[matches]]], l.rest UNTIL l = NIL DO [] _ UserExecPrivate.PrintCommand[name: l.first, event: event, exec: exec]; ENDLOOP; UserExec.ReleaseStreams[exec]; RETURN[NIL]; }; }; LookupCommands: PROC [command: ROPE, event: UserExec.HistoryEvent, exec: UserExec.ExecHandle _ NIL] RETURNS[val: LIST OF ROPE _ NIL] = { r: ROPE; search: PROC[name: ROPE, proc: Commander.CommandProc, doc: ROPE] RETURNS[stop: BOOL] = { IF Rope.Equal[s1: command, s2: name, case: FALSE] THEN { val _ LIST[name]; RETURN[TRUE]; } ELSE IF Rope.Find[s1: name, s2: command, case: FALSE] = 0 THEN val _ CONS[name, val]; RETURN[FALSE]; }; Correct: PROC = { ENABLE UNWIND => NULL; r _ UserExec.GetTheOne[unknown: command, generator: commandGenerator, event: event, exec: exec]; }; IF Commander.Lookup[command].proc # NIL THEN RETURN[LIST[command]] ELSE IF exec = NIL OR UserExecExtras.CorrectionDisabled[event: event] THEN RETURN[NIL]; [] _ Commander.Enumerate[search]; IF val # NIL THEN RETURN; Correct[]; RETURN[IF r # NIL THEN LIST[r] ELSE NIL]; }; <> CallCommanderProc: PROC[command: ROPE, event: UserExec.HistoryEvent, exec: UserExec.ExecHandle] RETURNS[handled: BOOL] = { commandProc: Commander.CommandProc = Commander.Lookup[command].proc; IF commandProc = NIL THEN RETURN[FALSE]; PushPropsAndCallProc[commandProc, command, event, exec]; RETURN[TRUE]; }; PushPropsAndCallProc: PROC [proc: Commander.CommandProc, command: ROPE, event: UserExec.HistoryEvent, exec: UserExec.ExecHandle] = { in, out: IO.STREAM; commanderHandle: Commander.Handle; inner: PROCEDURE = { proc[commanderHandle]; }; [in, out] _ UserExec.GetStreams[exec]; commanderHandle _ NEW[Commander.CommandObject _ [ in: in, out: out, err: out, commandLine: event.commandLine, command: command, propertyList: NIL ]]; CommandProcOps.PutProperty[handle: commanderHandle, key: $UserExec, val: exec, thisEventOnly: TRUE]; CommandProcOps.PutProperty[handle: commanderHandle, key: $HistoryEvent, val: event, thisEventOnly: TRUE]; ProcessProps.AddPropList[ LIST[List.DotCons[$CommanderHandle, commanderHandle]], inner]; }; undefined: Commander.CommandProc = { cmd.out.PutRope["This command can only be executed in a Work Area."]; }; LookupEnumerate: PROC [self: Generator.Handle] = { proc: PROC[name: ROPE, proc: Commander.CommandProc, doc: ROPE] RETURNS[stop: BOOL] = { RETURN[Generator.Produce[self, name]]; -- somebody terminated the generator. }; [] _ Commander.Enumerate[proc]; }; commandGenerator: PUBLIC Spell.SpellingGenerator _ Spell.GeneratorFromEnumerator[enumerator: LookupEnumerate, clientData: NIL]; <> GetRegisteredCommands: UserProfile.ProfileChangedProc = { GetCommands: PROC [name: ROPE, fromCatalogue: BOOL] = { GetRope: PROC RETURNS [ROPE] = { whiteSpace: IO.BreakProc = { RETURN[IF char = CR THEN break ELSE IO.WhiteSpace[char]] }; ref: REF ANY _ IO.GetRefAny[source ! ANY => GOTO Exit]; IF ref = NIL THEN RETURN[NIL]; IO.SkipOver[source, whiteSpace]; -- skips spaces, tabs, etc., but not CR. WITH ref SELECT FROM a: ATOM => RETURN[Atom.GetPName[a]]; r: ROPE => RETURN[r]; ENDCASE => ERROR; -- ERROR TO ERROR LOG. EXITS Exit => RETURN[NIL] }; source: STREAM _ NIL; IF name # NIL THEN { IF Rope.Find[name, "\n"] = -1 THEN -- r is name of file source _ FileIO.Open[fileName: name, accessOptions: read, createOptions: oldOnly ! FileIO.OpenFailed => IF why = fileNotFound THEN CONTINUE] ELSE source _ IO.RIS[name]; }; IF source # NIL THEN { source _ IO.CreateFilterCommentsStream[source]; WHILE NOT source.EndOf[] DO name, nameOfBCD, documentation: ROPE _ NIL; name _ GetRope[]; IF name = NIL THEN EXIT; IF source.PeekChar[] # CR THEN documentation _ GetRope[]; IF source.PeekChar[] # CR THEN nameOfBCD _ GetRope[]; IF nameOfBCD = NIL THEN nameOfBCD _ name; IF Rope.Find[s1: nameOfBCD, s2: "."] = -1 THEN nameOfBCD _ Rope.Concat[nameOfBCD, ".bcd"]; Register[name: name, briefDoc: documentation, doc: documentation, nameOfBCD: nameOfBCD, fromCatalogue: fromCatalogue]; ENDLOOP; IO.Close[source]; }; }; GetCommands["RegisteredCommands.Catalogue", TRUE]; GetCommands[UserProfile.Token["RegisteredCommands"], FALSE]; }; UserProfile.CallWhenProfileChanges[GetRegisteredCommands]; END. -- of UserExecRegCmdsImpl <<>> <> <> <> Larry and I spent an interesting hour discussing the technical aspects of how we could have the best of both worlds, namely that clients could register themselves with the Commander and would be able to be invoked in leaner meaner cedar, but magically, when running in the full Cedar world, would behave as they now do. I then went off and spent several hours examining in detail the difference between the Commander interface and UserExec interface with an eye to implementing the latter in terms of the former, and trying to identify what functionality, if any would be lost. This process has led me to a clear(er) espousal of the differences (actually fairly small) between Commander and UserExec, and to a specific proposal consisting of two small changes to the Commander interface which would accomplish all of the desired goals. I submit both espousal and proposal for your consideration. A Commander CommandProc traffics in an object called a Handle, which basically consists of input and output streams, the commandLine and commandName, and a catch all propertyList. UserExec CommandProcs are more complicated. They take three arguments, an event and a handle, and a clientData field. Basically, the event corresponds to, and contains the commandLine/command information, and the handle contains the stream information (which must be accessed via a procedure rather than directly - more on this later). The clientData field contains information particular to the command so that the same commandProc can be used to implement several different commands, as is the case with the expansion of aliases, treatment of commands registered in catalogue, etc. This sort of technique is used in other interfaces such as Menus, and was originally championed by Russ. UserExec CommandProcs also return two values, one which says that everything was ok, and the other a rope. In most cases these are TRUE and NIL. One could imagine an implementation in which for the exceptional case, the client called a procedure which recorded the relevant information in the event, or raised a signal, etc. In other words, it would be possible to eliminate this difference and have a UserExec.CommandProc not return a value and still retain all of the current functionality. So we are left with two issues: why does the UserExec.CommandProc need two pieces of information and the Commander only one, and why are these two pieces of information so much more complicated than the Commander.Handle, which consists of only six fields? Let's consider the latter question first, since it really is a red herring. The UserExec ExecHandle is actually pretty simple. It just consists of a viewer and a privateData field. The event also has a private data field, but it in addition exposes some other information which I thought might be useful to some clients, such as an expression field which helps when dealing with interpreter, e.g. you can see if any corrections were made, a dontCorrect field, so that clients that call the spelling corrector etc. don't have to worry about those situations where they shouldn't, e.g. the run command does not attempt correction when it is executing an event from a command file, whereas it does when coming from the keyboard, etc. I could also have provided this information via procedures for those exceptional clients that needed it. We all know that, in general, procedural interfaces allow the implementor more freedom to change his representation without affecting his clients, but make it slightly more cumbersome for the clients to access the relevant information. The main reason that the UserExec type structure looks so complicated and the Commander so simple is that the behind the scenes implementation information in the Commander is stored on the property list of the handle, rather than in a declared field in the record. In the same way as the private data field in the UserExec represents an attempt to provide for more interface stability by using an opaque type in order not to commit to a particular representation, the Commander goes one step further by removing all mention of this information from the interface. This gives maximal freedom to the implementor while running the risk of postponing certain errors to runtime that would normally be caught by the compiler. Had I used the same approach for the UserExec, the data structure for an event and a handle would look pretty much the same as it does for Commander. So the main difference is the division of the information needed by the commandProc into two arguments, the event and the handle, and the fact that the streams have to be obtained via a procedure (both changes made for 4.0). What does this buy? The main area I was attacking was to make it possible for a client to invoke a command proc when the client didn't have an execHandle in hand (e.g. from Walnut, Russ's compiletool, or some other tool based application). I did not want to force the client to create an execHandle, complete with historylist, viewer, symbol table, etc., because in most of these applications, there would be no output/input unless something went wrong. Another related issue was that a command proc running under a particular execHandle ought to be able to FORK its execution, which might not complete until after several more events were executed under the same handle. The latter was not possible previously because all event-related information, such as the commandLine, was stored in the UserExec Handle, and got overwritten with each event. (Interestingly enough, this shortfall was first pointed out to me by Larry Stewart.) Given the possibility of events executing asynchronously and simultaneously under the same handle, I wanted some method of synchronizing, i.e. locking, the input/output streams. The procedural access to the streams provides a way of making sure these two events don't intermix their input/output by assigning the streams to one and only one event. Furthermore, I only assign those streams when they are needed so that if an event does not require input/output it can execute "silently" at the same time as another event. However, Larry pointed out that one could accomplish the same thing with a special kind of stream which did acquired the underlying streams when they were first needed. I wish I had thought of that earlier, and will use this idea in accomodating to the Commander interface which requires that the streams be available through the handle directly. So, the bottom line is, there are very few unresolved differences between the two ways of doing business. In fact, we can get the best of both worlds, as follows (each of these changes are independent and obtains the indicated benefit. They can be accepted or rejected independently.): (1) Change Commander.Register by adding a clientData field argument which is passed to the CommandProc. This change enables the UserExec to share the same registry with the Commander, i.e. UserExec.RegisterCommand will no longer have to exist, and the UserExec will not have to maintain its own internal data structures for command registry, yet will still be able to support the registeredcommands catalogue, aliases, etc. (2) Change the type of CommandProc by adding an event argument, where event is defined trivially as a REF RECORD[commandLine: Rope.ROPE, command: Rope.ROPE, propertyList: List.AList], i.e. split the information currently in the CommandObject into two pieces, one of which contains information global to the handle, and the second which contains the information that is particular to this event. This will allow UserExec.DoIt to continue working as it now does, and will even allow operations executed in the commandTool in leaner meaner cedar to FORK if they so desire. (I don't know if the commander currently has any event specific information other than the commandLine, e.g. the start time, number words allocated etc., but I could easily imagine a client wanting to store such information on a property list, and not want it to be overwritten by the next event.) What I could do with this is to store the UserExec Handle on the commander handle's property list, and similarly store the HistoryEvent on the Commander Events property list. I would provide procedural interfaces in the UserExec for obtaining this information for those clients who need to invoke operations available only through the UserExec interface, such as DoIt, or GetTheFile, etc, and to allow me to register all of my commandprocs as CommanderProcs. All would be wonderful! One more minor change is necessary to AskUser due to its special nature. The current version of AskUser provided by UserExec correctly handles type ahead in the presence of an AskUser interaction by posting a menu and ignoring the keyboard. In the new scheme of things, AskUser is being moved to IO because bringover/smodel use it, and they are in the kernel, whereas the UserExec is not. Therefore, a simpler version of AskUser was implemented which takes two streams as arguments and does all of its interactions with the keyboard. How can we arrange things so that when called from a WorkArea in a full Cedar, AskUser, i.e. smodel, bringover, etc., will automatically behave as they do now? (3) Instead of taking two streams, change IOMisc.AskUser to take a Commander.Handle (actually a REF ANY which is NARROWED to a Commander.Handle in order to avoid interface circularities), which contains these streams. Inside of this AskUser, we look on the property list of the commander handle for the property $AskUser. If found, its value is (NARROWED to) a procedure of the same type as AskUser and called. Otherwise, perform the simple minded AskUser which does not handle typeahead, menus, etc. In the full Cedar system, the UserExec will set up this property so that when invoked in a WorkArea, the fancy AskUser is called, on the same arguments, and it then obtains the UserExec Handle from the same property list. I have already implemented this and it works fine. I understand that Paul has already committed, i.e. compiled some of his code to use depend on Commander and IOMisc.AskUser. I would be willing to remake those components with the new interface if it would make his response more positive. I think that there are a lot of benefits to be obtained with the above proposal. warren <<>> <> <> <> <> <> <> <> <> <> <> <> <<>> <<>> <<>> <<>> <<>> <> <> <<>>