14 Replies Latest reply on Mar 14, 2011 7:46 AM by John Hawkinson

    Javascript Shell for InDesign

    John Hawkinson Level 5

      Early in the week in the Re: ScriptUI : edittext live modifications problem thread, I mentioned

      Jesse Ruderman's Javascript shell and how nice it would be to have something like that for InDesign, where you could type in a quick script (oneliner or, since Javascript is so verbose, perhaps a 3-liner), and see the output immediately. Kind of like the ESTK's console window, except you don't have to switch to the ESTK and so you can see all your windows.

       

      Also, with command-line history (up-arrow; control up/down if it's a multiline entry), editing (arrow keys, emacs keybindings for navigation, at least on a Mac: C-a, C-f, C-b, C-e, C-n, C-p, etc.), and some other stuff -- OH YEAH, and TAB COMPLETION!

       

      shellshot.png

      The code is...kind of convoluted and it had a large number of browser dependencies. I tried to rip most of that stuff out and clean it up a bit, but I'm afraid I wasn't willing to spend too-too much time on it.

       

      The length of this is probably such that I should post a link to the source, but I'm just going to paste it in here anyhow so you can see how bad it is .

       

      The completion is pretty cool. Just type "app.act<TAB>" and it completes to "app.active",  and lists the choies in the window for you (activeBook, activeWindow, activeScript, activeDocument, activeScriptUndoMode), and then hit "D" and it completes to "activeDocument."

       

      One touchy thing about the tab completion is that TAB wants to select everything in the window, or take you out to the next UI element. I attempt to handle that by resetting the text selection in the keyup event handler (we catch the tab on the keydown). This seems to work...sometimes. Otherwise it doesn't. (and so you can lose what you just completed if you type another character too hastily...sigh).

       

      Anyhow, I don't know if I'm really going to put much effort into polishing this, but here it is. So have fun with it and let me know if its useful.

       

      // Javascript shell for Adobe InDesign (ExtendScript).
      // Based on Jesse Ruderman's Javascript Shell,
      //      http://squarefree.com/shell/shell.html
      // Extremely crude port by John Hawkinson <jhawk@mit.edu>
      // 11 March 2011
      
      /*jslint forin: true, debug: true, evil: true, sub: true, continue:
       true, css: true, cap: true, on: true, fragment: true, es5: true,
       maxerr: 50, indent: 4, white: false, undef: false */
      /*global $, ScriptUI, Window, alert,
      
        init: true, initTarget: true, inputKeydown: true,
        inputKeyup: true, caretInFirstLine: true, caretInLastLine: true,
        println: true, shellCommands: true,
        hist: true, tabcomplete: true, go: true, printError: true */
      
      
      #targetengine shell
      
      var 
           histList = [""], 
           histPos = 0, 
           _scope = {}, 
           _win, // a top-level context
           question,
           _in,
           _out,
           _help,
           tooManyMatches = null,
           lastError = null;
      
      var
           opener, window = {}, Shell;
      
      function init() {
           var
                g,
                i, j,
                help, win,
                winspec = [
      "palette  { alignChildren: 'left'",
      "  i: Group { orientation: 'row', alignChildren: 'left'",
      "      i: EditText {"+
      "        active:true, characters: 90",
      "        properties: { multiline: true, borderless: false}"+
      "      }",
      "      h: Panel { properties: { borderStyle: 'sunken' }, margins: 0",
      "        h: StaticText { text: '?' }"+
      "      }",
      "  }",
      //"t: StaticText { text: 'test' },",
      //"s: Scrollbar { size: [ 20, 100 ], value: 0}",
      "  o: EditText { characters: 100", 
      "      properties: { readonly: true, multiline: true",
      "        borderless: false }"+
      "  }",
      //"k: StaticText { text: 'Ctrl-Q: props(ans);  ' }",
      "  s: StaticText {characters: 80}",
      "}"
           ].join(", "),
           colors = {
                AntiqueWhite:               [250, 235, 215],
                AntiqueWhite1:               [255, 239, 219],
                AntiqueWhite2:               [238, 223, 204],
                AntiqueWhite3:               [205, 192, 176],
                AntiqueWhite4:               [139, 131, 120],
                PaleGoldenrod:               [238, 232, 170],
                LightGoldenrodYellow:     [250, 250, 210],
                LightGoldenrod:               [238, 221, 130],
                goldenrod:                    [218, 165, 32],
                DarkGoldenrod:               [184, 134, 11],
                LightGoldenrod1:          [255, 236, 139],
                LightGoldenrod2:          [238, 220, 130],
                LightGoldenrod3:          [205, 190, 112],
                LightGoldenrod4:          [139, 129, 76],
                goldenrod1:                    [255, 193, 37],
                goldenrod2:                    [238, 180, 34],
                goldenrod3:                    [205, 155, 29],
                goldenrod4:                    [139, 105, 20],
                DarkGoldenrod1:               [255, 185, 15],
                DarkGoldenrod2:               [238, 173, 14],
                DarkGoldenrod3:               [205, 149, 12],
                DarkGoldenrod4:               [139, 101, 8]
           },
           brushes = {}, pens = {}, fonts = {},
           helpText = [ "",
                'JavaScript Shell 1.4',
                "Features: autocompletion of property names with Tab,",
                "multiline input with Shift+Enter, "+
                "input history with (Ctrl+) Up/Down,",
      //~               '<a accesskey="M" href="javascript:go(scope(Math);
      //~mathHelp(););" title="Accesskey: M">Math</a>,',
      //~               '<a accesskey="H"
      //~href="http://www.squarefree.com/shell/?ignoreReferrerFrom=shell1.4"
      //~title="Accesskey: H">help</a>',
      //~               'Values and functions: ans, print(string), <a
      //~accesskey="P" href="javascript:go(props(ans))" title="Accesskey:
      //~P">props(object)</a>,',
      //~               '<a accesskey="B" href="javascript:go(blink(ans))"
      //~title="Accesskey: B">blink(node)</a>,',
                "Values and functions are: ans, print(string), "+
                "props(object), clear(), "+
              //"load(scriptURL), "+
               "scope(object)",
      //~               '<a accesskey="C" href="javascript:go(clear())"
      //~title="Accesskey: C">clear()</a>, load(scriptURL), scope(object)',
                ""
           ].join("\n");
          
          if (_win) {
              _win.close();
          }
           
           win = new Window(winspec, "Javascript Shell (squarefree.com)");
                     
           _in = win.i.i;
           _out = win.o;
           help = win.i.h.h;
           //~      _in.preferredSize = [ 400, 400];
           _in.preferredSize  = [ -1, _in.preferredSize[1]*4 ];
           _out.preferredSize = [ _in.preferredSize[0]+90, 600 ];
           _out.text = "";
           _out.textselection = helpText;
                    
           g = win.graphics;
           for (i in colors) {
                if (colors.hasOwnProperty(i)) {
                     j = colors[i];
                     brushes[i] = g.newBrush(g.BrushType.SOLID_COLOR,
                          [ j[0]/255, j[1]/255, j[2]/255, 0], 1);
                     pens[i] = g.newPen(g.PenType.SOLID_COLOR, 
                          [ j[0]/255, j[1]/255, j[2]/255, 0], 1);
                }
           }
           fonts.fat = ScriptUI.newFont(g.font.name,
                ScriptUI.FontStyle.BOLD, 36);
      
           win.graphics.backgroundColor  = brushes.AntiqueWhite3;
           _in.graphics.backgroundColor  = brushes.LightGoldenrod1;
           _out.graphics.backgroundColor = brushes.AntiqueWhite;
           
           help.graphics.foregroundColor = pens.DarkGoldenrod4;
           help.graphics.font = fonts.fat;
           
           _in.addEventListener("keydown", inputKeydown); // gets autorepeat
           _in.addEventListener("keyup", inputKeyup); // tab fixup
      
           // Don't use a button for help because then we can TAB to it
           // and that messes up completion. So attach an onClick handler
           // to the help button's Panel.
           win.i.h.addEventListener("click",
             function() { println(helpText, "help"); });
       
           _in.text="";
           win.show();
           
           _win = win;
      
           initTarget();
      }
      
      function initTarget()
      {
        _win.Shell = $.global;
      
         //$.writeln("shellCommands "+shellCommands); // how can this fail?
        // somehow we don't have shellCommands hoisted?
      
        _win.print = shellCommands && shellCommands.print; // xxx?
      }
      
      
      // This was hoped to serve the function of setTimeout() in a browser,
      // but it doesn't seem to fly...
      //~function defer(f, time) {
      //~          var task, listener;
      //~ //~          task = app.idleTasks.add({ name: "defer", sleep: time});
      //~ //~          listener = task.addEventListener(IdleEvent.ON_IDLE,
      //~               listener = _in.addEventListener("draw",
      //~               function() {
      //~                 beep();
      //~                 listener && listener.remove();
      //~                 task && task.remove();
      //~                 f.apply(f, arguments);
      //~               }
      //~          );
      //~ }
      
      function inputKeydown(e) {
           
           //println(new Array(40).join(" ") + e.keyIdentifier +
           //     "("+keyCode+")"+e.shiftKey ,"key");
      
      
           // status line
           //_win.s.text = e.keyName+"/"+e.keyIdentifier+"("+")";
           //     (e.shiftKey?"S":" ")+(e.ctrlKey?"C":" ")+
           //     (e.metaKey?"M":" ")+"\t\t"+histPos+" / "+
           //     histList.length+
           //     (e.cancellable?"cancellable":"notcancel");
      
      
           if (e.shiftKey && e.keyName === 'Enter') { // shift-enter
                // Allow the shift-enter to insert a line
                // break as normal
                _in.textselection+="\n";
           } else if (e.keyName === 'Enter') { // enter
                // execute the input on enter
                try { go(); } catch(er) { alert(er); }
      
                // xxx browsers?
                // can't preventDefault on input, so clear it later
                //setTimeout(function() { _in.text = ""; }, 0);
           } else if (e.keyName === 'Up') { // up
                // go up in history if at top or ctrl-up
                if (e.ctrlKey || caretInFirstLine(_in)) {
                     hist(true);
                }
           } else if (e.keyName === 'Down') { // down
                // go down in history if at end or ctrl-down
                if (e.ctrlKey || caretInLastLine(_in)) {
                     hist(false);
                }
           } else if (e.keyName === 'Tab') { // tab
                tabcomplete();
                _in.gotoEOL = true;
           } else if (e.shiftKey && e.keyName === 'Escape') {
                _win.close();
           } // else { }
      
      }
      
      function inputKeyup(e) {
           //win.s.text = "UP"+e.keyName+"::"+
           //     +ScriptUI.environment.keyName;
                //+e.keyName+"/"+e.keyIdentifier+"("+keyCode+")"+
                //e.shiftKey+"\t\t"+histPos+" /"+histList.length+
                //(e.cancellable?"cancellable":"notcancel");
      
           // Try to undo any selection that TAB may have done.
           if (_in.gotoEOL) {
                var t=_in.text;
                _in.active=true;
                _in.text="";
                _in.textselection=t;
                _in.gotoEOL=false;
           }
      }
      
      
      
      function caretInFirstLine(textbox) {
           var
                firstLineBreak = textbox.text.indexOf("\n");
        
           if (firstLineBreak !== -1) {
                _win.s.text="Use Ctrl-Up/Ctrl-Down for history in "+
                     "multiline input.";
                return false;
           } else {
                return true;
           }
        
      //     return ((firstLineBreak === -1) ||
      //               (textbox.selectionStart <= firstLineBreak));
      }
      
      function caretInLastLine(textbox) {
           var
                lastLineBreak = textbox.text.lastIndexOf("\n");
      
           if (lastLineBreak !== -1) {
                _win.s.text="Use Ctrl-Up/Ctrl-Down for history in "+
                     "multiline input.";
                return false;
           } else {
                return true;
           }
        
      //     return (textbox.selectionEnd > lastLineBreak);
      }
      
      function println(s, type)
      {
           // If I could only figure out how to set different
           // ScriptUI pens in the same object, that would be
           // awesome. All my attempts with onDraw and
           // drawString() failed. I suppose I could use multiple
           // StaticText objects, but that would make scrolling hell.
      
           var newtext;
           
           s=String(s);
           if (s) {
                // Make sure the pointer is at the end.
                newtext = _out.text+s+"\n";
                _out.text="";
                _out.textselection += newtext;
                return null;
           }
      }
      
      function printWithRunin(h, s, type)
      {
           _win.s.text = h+"("+type+")";
           var div = println(s, h+"::"+type);
      //~       var head = document.createElement("strong");
      //~       head.appendChild(document.createTextNode(h + ": "));
      //~       div.insertBefore(head, div.firstChild);
      }
      
      
      var shellCommands = 
      {
      //     load : function load(url) {
      //          var s = _win.document.createElement("script");
      //          s.type = "text/javascript";
      //          s.src = url;
      //          _win.document.getElementsByTagName("head")[0].appendChild(s);
      //          println("Loading " + url + "...", "message");
      //     },
           
           clear : function clear()
           {
              _out.text = "";
           },
           
           print : function print(s) { println(s, "print"); },
           
           // the normal function, "print", shouldn't return a value
           // (suggested by brendan; later noticed it was a problem when
           // showing others)
           pr : function pr(s) { 
                // need to specify shellCommands so it doesn't
                // try window.print()!
                shellCommands.print(s);
                return s;
           },
           
           props : function props(e, onePerLine) {
                var a;
      
                if (e === null) {
                     println("props called with null argument", "error");
                     return;
                }
                
                if (e === undefined) {
                     println("props called with undefined argument", "error");
                     return;
                }
                
                var ns = ["Methods", "Fields", "Unreachables"];
                var as = [[], [], []]; // array of (empty) arrays of arrays!
                var p, j, i; // loop variables, several used multiple times
                
                var protoLevels = 0;
                
                /* JSLint really doesn't like this:
                 * Problem at line 357 character 30: Reserved name '__proto__'.
                 * what are we supposed to do? aaaargh! */
                for (p = e; p; p = p.__proto__)
                {
                     for (i=0; i<ns.length; ++i) {
                          as[i][protoLevels] = [];
                     }
                     ++protoLevels;
                }
                
                /*jslint forin: false */
                for(a in e) {
                     // Shortcoming: doesn't check that VALUES are the same in
                     // object and prototype.
                     
                     var protoLevel = -1;
                     try
                     {
                          /*jslint forin: false */
                          for (p = e; p && (a in p); p = p.__proto__) {
                               ++protoLevel;
                          }
                     }
                     // "in" operator throws when param to props() is a string
                     catch(er) { protoLevel = 0; } 
      
                     var type = 1;
                     try
                     {
                          if ((typeof e[a]) === "function") {
                               type = 0;
                          }
                     }
                     catch (er1) { type = 2; }
                     
                     as[type][protoLevel].push(a);
                }
      
                function times(s, n) { return n ? s + times(s, n-1) : ""; }
      
                for (j=0; j<protoLevels; ++j) {
                     for (i=0;i<ns.length;++i) {
                          if (as[i][j].length) {
                               printWithRunin(
                                    ns[i] + times(" of prototype", j), 
                                    (onePerLine ? "\n\n" : "") +
                                         as[i][j].sort().join(
                                              onePerLine ? "\n" : ", ") +
                                         (onePerLine ? "\n\n" : ""), 
                                    "propList"
                               );
                          }
                     }
                }
           },
      
      //     blink : function blink(node)
      //     {
      //          if (!node)                          throw("blink: argument is null or undefined.");
      //          if (node.nodeType == null)      throw("blink: argument must be a node.");
      //          if (node.nodeType == 3)           throw("blink: argument must not be a text node");
      //          if (node.documentElement)           throw("blink: argument must not be the document object");
      //
      //          function setOutline(o) { 
      //               return function() {
      //                    if (node.style.outline != node.style.bogusProperty) {
      //                         // browser supports outline (Firefox 1.1 and newer, CSS3, Opera 8).
      //                         node.style.outline = o;
      //                    }
      //                    else if (node.style.MozOutline != node.style.bogusProperty) {
      //                         // browser supports MozOutline (Firefox 1.0.x and older)
      //                         node.style.MozOutline = o;
      //                    }
      //                    else {
      //                         // browser only supports border (IE). border is a fallback because it moves things around.
      //                         node.style.border = o;
      //                    }
      //               }
      //          } 
      //          
      //          function focusIt(a) {
      //               return function() {
      //                    a.focus(); 
      //               }
      //          }
      //
      //          if (node.ownerDocument) {
      //               var windowToFocusNow = (node.ownerDocument.defaultView || node.ownerDocument.parentWindow); // Moz vs. IE
      //               if (windowToFocusNow)
      //                    setTimeout(focusIt(windowToFocusNow.top), 0);
      //          }
      //
      //          for(var i=1;i<7;++i)
      //               setTimeout(setOutline((i%2)?'3px solid red':'none'), i*100);
      //
      //          setTimeout(focusIt(window), 800);
      //          setTimeout(focusIt(_in), 810);
      //     },
      
           scope : function scope(sc)
           {
                if (!sc) { sc = {}; }
                Shell._scope = sc;
                println("Scope is now " + sc +
                     ".      If a variable is not found in this scope, "+
                     "window will also be searched.     New variables will "+
                     "still go on window.", "message");
           },
      
           mathHelp : function mathHelp()
           {
                printWithRunin("Math constants", "E, LN2, LN10, LOG2E, "+
                     "LOG10E, PI, SQRT1_2, SQRT2", "propList");
                printWithRunin("Math methods", "abs, acos, asin, atan,"+
                     " atan2, ceil, cos, exp, floor, log, max, min, pow, "+
                     "random, round, sin, sqrt, tan", "propList");
           },
      
           ans : undefined,
      
           quit: function() { _win.close(); }
      };
      
      
      function hist(up)
      {
           // histList[0] = first command entered, [1] = second, etc.
           // type something, press up --> thing typed is now in "limbo"
           // (last item in histList) and should be reachable by pressing 
           // down again.
      
           var L = histList.length;
      
           if (L === 1) {
                return;
           }
      
           if (up)
           {
                if (histPos === L-1)
                {
                     // Save this entry in case the user hits the down key.
                     histList[histPos] = _in.text;
                }
      
                if (histPos > 0)
                {
                     histPos--;
      
                     // xxx browsers
                     // Use a timeout to prevent up from moving cursor within
                     // new text
                     // Set to nothing first for the same reason
                     //~            setTimeout(
                     (function() {
                          _in.text = ''; 
                          _in.text = histList[histPos];
                          var caretPos = _in.text.length;
                          if (_in.setSelectionRange) {
                               _in.setSelectionRange(caretPos, caretPos);
                          }
                     }());
                     //~               0
                     //~            );
                }
           } 
           else // down
           {
                if (histPos < L-1)
                {
                     histPos++;
                     _in.text = histList[histPos];
                }
                else if (histPos === L-1)
                {
                     // Already on the current entry: clear but save
                     if (_in.text)
                     {
                          histList[histPos] = _in.text;
                          ++histPos;
                          _in.text = "";
                     }
                }
           }
      }
      
      function tabcomplete()
      {
           /*
            * Working backwards from s[from], find the spot
            * where this expression starts.     It will scan
            * until it hits a mismatched ( or a space,
            * but it skips over quoted strings.
            * If stopAtDot is true, stop at a '.'
            */
           function findbeginning(s, from, stopAtDot)
           {
                /*
                 *     Complicated function.
                 *
                 *     Return true if s[i] == q BUT ONLY IF
                 *     s[i-1] is not a backslash.
                 */
                function equalButNotEscaped(s,i,q)
                {
                     if (s.charAt(i) !== q) { // not equal go no further
                          return false;
                     }
      
                     if (i===0) { // beginning of string
                          return true;
                     }
      
                     if (s.charAt(i-1) === '\\') { // escaped?
                          return false;
                     }
      
                     return true;
                }
      
                var nparens = 0;
                var i;
                for (i=from; i>=0; i--)
                {
                     if (s.charAt(i) === ' ') {
                          break;
                     }
      
                     if (stopAtDot && s.charAt(i) === '.') {
                          break;
                     }
                     
                     if (s.charAt(i) === ')') {
                          nparens++;
                     }
                     else if (s.charAt(i) === '(') {
                          nparens--;
                     }
      
                     if(nparens < 0) {
                          break;
                     }
      
                     // skip quoted strings
                     if(s.charAt(i) === '\'' || s.charAt(i) === '\"')
                     {
                          //dump("skipping quoted chars: ");
                          var quot = s.charAt(i);
                          i--;
                          while(i >= 0 && !equalButNotEscaped(s,i,quot)) {
                               //dump(s.charAt(i));
                               i--;
                          }
                          //dump("\n");
                     }
                }
                return i;
           }
      
           // XXX should be used more consistently (instead of using selectionStart/selectionEnd throughout code)
           // XXX doesn't work in IE, even though it contains IE-specific code
           function getcaretpos(inp)
           {
                if(inp.selectionEnd) {
                     return inp.selectionEnd;
                }
                
                if(inp.createTextRange)
                {
                     var docrange = _win.Shell.document.selection.createRange();
                     var inprange = inp.createTextRange();
                     if (inprange.setEndPoint)
                     {
                          inprange.setEndPoint('EndToStart', docrange);
                          return inprange.text.length;
                     }
                }
      
                return inp.text.length; // sucks, punt
           }
      
           function min(a,b){ return ((a<b)?a:b); }
      
      
           // get position of cursor within the input box
           var caret = getcaretpos(_in);
           var matches = [];
           var bestmatch = null;
           var a, objname;
      
           if (caret) {
                //dump("----\n");
                var dotpos, spacepos, complete, obj;
                //dump("caret pos: " + caret + "\n");
                // see if there's a dot before here
                dotpos = findbeginning(_in.text, caret-1, true);
                //dump("dot pos: " + dotpos + "\n");
                if(dotpos === -1 || _in.text.charAt(dotpos) !== '.') {
                     dotpos = caret;
                     //dump("changed dot pos: " + dotpos + "\n");
                }
      
                // look backwards for a non-variable-name character
                spacepos = findbeginning(_in.text, dotpos-1, false);
                //dump("space pos: " + spacepos + "\n");
                // get the object we're trying to complete on
                if (spacepos === dotpos || spacepos+1 === dotpos ||
                     dotpos === caret)
                {
                     // try completing function args
                     if(_in.text.charAt(dotpos) === '(' ||
                        (_in.text.charAt(spacepos) ==='('
                          && (spacepos+1) === dotpos))
                     {
                          var fn,fname;
                          var from = (_in.text.charAt(dotpos) === '(') ?
                               dotpos : spacepos;
                          spacepos = findbeginning(_in.text, from-1, false);
                          
                          fname = _in.text.substr(spacepos+1,from-(spacepos+1));
                          //dump("fname: " + fname + "\n");
                          try {
      
                               with(_win.Shell._scope)
                                    with(_win)
                                         with(Shell.shellCommands)
                                              fn = eval(fname);
                          }
                          catch(er) {
                               //dump('fn is not a valid object\n');
                               return;
                          }
                          if(fn === undefined) {
                               //dump('fn is undefined');
                               return;
                          }
                          if(fn instanceof Function)
                          {
                               // Print function definition, including argument names, but not function body
                               if(!fn.toString().match(
                                    /function .+?\(\) +\{\n +\[native code\]\n\}/
                               )) {
                                    println(fn.toString().match(
                                              /function .+?\(.*?\)/), "tabcomplete");
                               }
                          }
      
                          return;
                     }
                     else {
                          obj = _win;
                     }
                }
                else
                {
                     objname = _in.text.substr(spacepos+1,
                          dotpos-(spacepos+1));
                     //dump("objname: |" + objname + "|\n");
                     try {
      
                          with(_win.Shell._scope)
                               with(_win)
                                    obj = eval(objname);
                     }
                     catch(er2) {
                          printError(er2); 
                          return;
                     }
                     if(obj === undefined) {
                          // sometimes this is tabcomplete's fault, so don't
                          // print it :( e.g. completing from
                          // "print(document.getElements" println("Can't
                          // complete from null or undefined expression " +
                          // objname, "error");
                          return;
                     }
                }
                //dump("obj: " + obj + "\n");
                // get the thing we're trying to complete
                if(dotpos === caret)
                {
                     if(spacepos+1 === dotpos || spacepos === dotpos)
                     {
                          // nothing to complete
                          //dump("nothing to complete\n");
                          return;
                     }
      
                     complete = _in.text.substr(spacepos+1,dotpos-(spacepos+1));
                }
                else {
                     complete = _in.text.substr(dotpos+1,caret-(dotpos+1));
                }
                //dump("complete: " + complete + "\n");
                // ok, now look at all the props/methods of this obj
                // and find ones starting with 'complete'
                for(a in obj)
                {
                     //a = a.toString();
                     //XXX: making it lowercase could help some cases,
                     // but screws up my general logic.
                     if(a.substr(0,complete.length) === complete) {
                          matches.push(a);
                          ////dump("match: " + a + "\n");
                          // if no best match, this is the best match
                          if(bestmatch === null)
                          {
                               bestmatch = a;
                          }
                          else {
                               // the best match is the longest common string
                               var i;
                               for(i=0; i< min(bestmatch.length, a.length); i++)
                               {
                                    if(bestmatch.charAt(i) !== a.charAt(i)) {
                                         break;
                                    }
                               }
                               bestmatch = bestmatch.substr(0,i);
                               ////dump("bestmatch len: " + i + "\n");
                          }
                          ////dump("bestmatch: " + bestmatch + "\n");
                     }
                }
                bestmatch = (bestmatch || "");
                ////dump("matches: " + matches + "\n");
                var objAndComplete = (objname || obj) + "." + bestmatch;
                //dump("matches.length: " + matches.length + ",
                //     tooManyMatches: " + tooManyMatches +
                // ", objAndComplete: " + objAndComplete + "\n");
                if (matches.length > 1 &&
                     (tooManyMatches === objAndComplete || matches.length <= 10))
                {
                     printWithRunin("Matches: ", matches.join(', '),
                                       "tabcomplete");
                     tooManyMatches = null;
                }
                else if(matches.length > 10)
                {
                     println(matches.length +
                               " matches.      Press tab again to see them all",
                               "tabcomplete");
                     tooManyMatches = objAndComplete;
                }
                else {
                     tooManyMatches = null;
                }
                if (bestmatch !== "")
                {
                     var sstart;
                     if(dotpos === caret) {
                          sstart = spacepos+1;
                     }
                     else {
                          sstart = dotpos+1;
                     }
                     _in.text = _in.text.substr(0, sstart)
                          + bestmatch
                          + _in.text.substr(caret);
                     //setselectionto(_in,caret +
                     //       (bestmatch.length - complete.length));
                }
           }
      }
      
      function printQuestion(q)
      {
           println(">> "+q, "input");
      }
      
      function printAnswer(a)
      {
           if (a !== undefined) {
                println(a+"\n", "normalOutput");
                shellCommands.ans = a;
           } else {
                println(a, "undefinedOutput");
           }
      }
      
      function printError(er)
      { 
           var lineNumberString;
      
           lastError = er; // for debugging the shell
           if (er.name)
           {
                // lineNumberString should not be "",
                // to avoid a very wacky bug in IE 6.
                lineNumberString = (er.lineNumber !== undefined) ?
                     (" on line " + er.lineNumber + ": ") : ": ";
                // Because IE doesn't have error.toString.
                println(er.name + lineNumberString + er.message, "error"); 
           }
           else {
                // Because security errors in Moz /only/ have toString.
                println(er, "error"); 
           }
      }
      
      function go(s)
      {
           _in.text = question = s ? s : _in.text;
      
           if (question === "") {
                return;
           }
      
           histList[histList.length-1] = question;
           histList[histList.length] = "";
           histPos = histList.length - 1;
           
           // Unfortunately, this has to happen *before* the JavaScript is
           // run, so that print() output will go in the right place.
           _in.text='';
           printQuestion(question);
      
           if (_win.closed) {
                printError("Target window has been closed.");
                return;
           }
           
           try { ("Shell" in _win); }
           catch(er) {
                printError("The JavaScript Shell cannot access variables "+
                             "in the target window.     The most likely reason"+
                             " is that the target window now has a different"+
                             " page loaded and that page has a different"+
                             " hostname than the original page.");
                return;
           }
      
           if (!("Shell" in _win)) {
                initTarget(); // silent
           }
      
           // Evaluate Shell.question using _win's eval (this is why eval
           // isn't in the |with|, IIRC).
           //       _win.location.href = "javascript:try{
           // Shell.printAnswer(eval('with(Shell._scope)
           // with(Shell.shellCommands) {' + Shell.question +
           // String.fromCharCode(10) + '}')); } catch(er) {
           // Shell.printError(er); }; setTimeout(Shell.refocus, 0); void 0";
      
           try {
                printAnswer(eval(
                     "with (_scope) with (shellCommands) {"+
                          question+String.fromCharCode(10)+
                          "}")
                             );
           } catch(er3) {
                printError(er3);
           }
      }
      
      try {
           init();
      } catch (e) {
           // $.writeln("Caught an error: ");
           // $.writeln(e);
           alert("Caught an error "+e+" at line "+e.line);
      }
      
      

       

      Oh yeah...does anyone know how to paint text in different colors in the same ScriptUI control? I gave up trying to make that happen...