/** * Takes a div element and runs a console simulator within. */ (function () { var noop = function () { return false; }, // ignore keypress defaultKeyMappings = { // keyed by modifiers, each either 1 or 0: shift || ctrl || alt '000': { 'ArrowLeft': function () { this.moveLeft(); return false; }, 'ArrowRight': function () { this.moveRight(); return false; }, 'ArrowUp': noop, 'ArrowDown': noop, 'Backspace': function () { this.deleteCharacterLeft(); return false; }, 'Delete': function () { this.deleteCharacterRight(); return false; }, 'End': function () { this.moveToEndOfLine(); return false; }, 'Home': function () { this.moveToStartOfLine(); return false; }, 'Enter': function () { return true; }, // if it returns false in line-buffered mode, then a line will never be formed -> no input ever 'Tab': noop } }; // Conversion table from legacy key codes for assorted keys // For details see: // * https://w3c.github.io/uievents/ // * http://www.w3.org/TR/2015/WD-DOM-Level-3-Events-code-20150428/ // * http://www.w3.org/TR/2015/WD-DOM-Level-3-Events-key-20150427/ // and their references. var legacyKeyConversion = { 0x03: {'code': 'Cancel'}, 0x06: {'code': 'Help'}, 0x08: {'code': 'Backspace'}, 0x09: {'code': 'Tab'}, 0x0c: {'code': 'Clear'}, 0x0d: {'code': 'Enter'}, 0x13: {'code': 'Pause'}, 0x1b: {'code': 'Escape'}, 0x20: {'code': 'Space', 'key': ' '}, 0x21: {'code': 'PageUp'}, 0x22: {'code': 'PageDown'}, 0x23: {'code': 'End'}, 0x24: {'code': 'Home'}, 0x25: {'code': 'ArrowLeft'}, 0x26: {'code': 'ArrowUp'}, 0x27: {'code': 'ArrowRight'}, 0x28: {'code': 'ArrowDown'}, 0x2c: {'code': 'PrintScreen'}, 0x2d: {'code': 'Insert'}, 0x2e: {'code': 'Delete'}, 0x6a: {'code': 'NumpadMultiply', 'key': '*'}, 0x6b: {'code': 'NumpadAdd', 'key': '+'}, 0x6c: {'code': 'NumpadComma', 'key': ','}, 0x6d: {'code': 'NumpadSubtract', 'key': '-'}, 0x6e: {'code': 'NumpadDecimal', 'key': '.'}, 0x6f: {'code': 'NumpadDivide', 'key': '/'}, 0x90: {'code': 'NumLock'}, 0x91: {'code': 'ScrollLock'}, 0xba: {'code': 'Semicolon', 'key': ';'}, 0xbb: {'code': 'Equal', 'key': '='}, 0xbc: {'code': 'Comma', 'key': ','}, 0xbd: {'code': 'Minus', 'key': '-'}, 0xbe: {'code': 'Period', 'key': '.'}, 0xbf: {'code': 'Slash', 'key': '/'}, 0xc0: {'code': 'Backquote', 'key': '`'}, 0xdb: {'code': 'BracketLeft', 'key': '['}, 0xdc: {'code': 'Backslash', 'key': '\\'}, 0xdd: {'code': 'BracketRight', 'key': ']'}, 0xde: {'code': 'Quote', 'key': "'"} }; // generate the rest of the mappings (function (table) { var userAgent = navigator.userAgent, i; // digits for (i = 0; i < 10; i++) table[0x30 + i] = {'code': 'Digit' + i, 'key': '' + i}; // characters: 'key' is the lower-cased character for (i = 0; i < 26; i++) table[0x41 + i] = {'code': 'Key' + String.fromCharCode(0x41 + i), 'key': String.fromCharCode(0x61 + i)}; // numpad for (i = 0; i < 10; i++) table[0x60 + i] = {'code': 'Numpad' + i, 'key': '' + i}; // function keys F1 to F24 for (i = 1; i < 25; i++) table[0x6f + i] = {'code': 'F' + i}; // override tables if (userAgent.indexOf('Gecko/') >= 0) { // Firefox table[0x3b] = {'code': 'Semicolon', 'key': ';'}; table[0x3d] = {'code': 'Equal', 'key': '='}; table[0x6b] = {'code': 'Equal', 'key': '='}; table[0x6d] = {'code': 'Minus', 'key': '-'}; table[0xbb] = {'code': 'NumpadAdd', 'key': '+'}; table[0xbd] = {'code': 'NumpadSubtract', 'key': '-'}; if (navigator.platform.indexOf('Mac') >= 0) { table[0x0c] = {'code': 'NumLock'}; table[0xad] = {'code': 'Minus', 'key': '-'}; } else if (navigator.platform.indexOf('Win') >= 0) { table[0xad] = {'code': 'Minus', 'key': '-'}; } } if (navigator.vendor && (navigator.vendor.indexOf('Apple') >= 0)) { // Safari table[0x03] = {'code': 'Enter'}; table[0x19] = {'code': 'Tab'}; } if ((userAgent.indexOf('iPad') >= 0) || (userAgent.indexOf('iPod') >= 0) || (userAgent.indexOf('iPhone') >= 0)) { // ios table[0x0a] = {'code': 'Enter'}; } })(legacyKeyConversion); // some (older) browsers specify incorrect key codes, this is a fix-up mapping var codeFixTable = { 'Esc': 'Escape', 'Left': 'ArrowLeft', 'Up': 'ArrowUp', 'Right': 'ArrowRight', 'Down': 'ArrowDown', 'Del': 'Delete' }; /** * Takes the given DOM Keyboard Event and returns a normalized representation. * @param event */ var processKeyboardEvent = function (event) { var keyCode, converted, result, c; if (('code' in event) && (('key' in event) || ('keyIdentifier') in event)) { // we have DOM3 KeyboardEvent, no conversion needed result = {'code': codeFixTable[event.code] || event.code, 'key': event.key || event.keyIdentifier}; } else { keyCode = event.keyCode || event.which; converted = legacyKeyConversion[keyCode]; if (converted) { result = {'code': converted.code}; if (converted.key) result.key = converted.key; else result.key = converted.code; // as per DOM3 spec: unprintables have the key the same as the code TODO: not true for all of them, e.g. AltLeft -> Alt } else { // unknown key, try to set at least some info result = {'code': ''}; c = String.fromCharCode(keyCode); if (c !== '') result.key = c; else result.key = 'Unidentified'; } } // char is set only in keypress events, never in keydown and keyup // (keypress is not a part of any standard) if ('char' in event) { result.char = event.char; } else if ('charCode' in event) { result.char = String.fromCharCode(event.charCode); } else if (event.type == 'keypress') { // TODO: generate result.char somehow? Probably not needed, we should use the input event instead of the keypress event anyway. } return result; }; codeq.makeConsole = function (jqElt, options) { var lines = [], // console content maxLines = (options && options.maxLines) || 10000, // how many lines to display at most currentRow = 0, // cursor position currentCol = 0, jqContent = $('
'), // the rendering of the console jqInput = $(''), // for receiving keypresses, clipboard content inputDisabled = false, // whether to ignore input lineBuffered = true, // whether we are line-buffered or not buffered renderLine = function (lineDescriptor) { var jqLine = lineDescriptor.jqLine, className = 'cq-con-text ' + lineDescriptor.className, content = lineDescriptor.content || '', jq1, jq2, jqCursor; if (!jqLine) { jqLine = $('
');
                    if (lineDescriptor.row == 0) {
                        jqContent.prepend(jqLine);
                    }
                    else if ((lineDescriptor.row < lines.length) && lines[lineDescriptor.row - 1].jqLine) {
                        lines[lineDescriptor.row - 1].jqLine.after(jqLine);
                    }
                    else {
                        // bail-out line, we should never end up here
                        jqContent.append(jqLine);
                    }
                    lineDescriptor.jqLine = jqLine;
                }
                else {
                    jqLine.empty();
                }

                if (currentRow == lineDescriptor.row) {
                    // mark the cursor in the current line
                    if (currentCol >= content.length) {
                        jq1 = $('');
                        jqCursor = $(' ');
                        jqLine.append(jq1);
                        jqLine.append(jqCursor);
                        jq1.text(content);
                    }
                    else if (currentCol <= 0) {
                        jq2 = $('');
                        jqCursor = $('');
                        jqLine.append(jqCursor);
                        jqLine.append(jq2);
                        jqCursor.text(content.charAt(0));
                        jq2.text(content.substr(1));
                    }
                    else {
                        jq1 = $('');
                        jq2 = $('');
                        jqCursor = $('');
                        jqLine.append(jq1);
                        jqLine.append(jqCursor);
                        jqLine.append(jq2);
                        jq1.text(content.substr(0, currentCol));
                        jqCursor.text(content.charAt(currentCol));
                        jq2.text(content.substr(currentCol + 1));
                    }
                }
                else {
                    jq1 = $('');
                    jqLine.append(jq1);
                    jq1.text(lineDescriptor.content);
                }
            },

            render = function () {
                var i, lineDescriptor, n = lines.length;
                jqContent.empty();
                for (i = 0; i < n; i++) {
                    lineDescriptor = lines[i];
                    lineDescriptor.row = i;
                    if (lineDescriptor.jqLine) {
                        lineDescriptor.jqLine.remove();
                        lineDescriptor.jqLine = null;
                    }
                    renderLine(lineDescriptor);
                }
            },

            formNewLines = function (newText) {
                var newLines, newTextLines;
                if (!newText) newLines = [];
                else if (newText instanceof Array) {
                    newLines = [];
                    for (i = 0; i < newText.length; i++) {
                        newTextLines = ('' + newText[i]).split('\n');
                        for (j = 0; j < newTextLines.length; j++) newLines.push(newTextLines[j]);
                    }
                }
                else {
                    newLines = ('' + newText).split('\n');
                }
                return newLines;
            },

            // the handler object that is returned
            handler = {
                'setLineBuffered': function () { lineBuffered = true; },
                'setNotBuffered': function () { lineBuffered = false; },
                'onInput': false, // the caller should assign a function here, that takes input as parameter, and optionally returns a promise which is then waited to be resolved before further input is accepted
                'onKeypress': false, // same, but it takes a keycode, and returns a keycode
                'inputEnable': function () { inputDisabled = false; },
                'inputDisable': function () { inputDisabled = true; },
                'keyMaps': {}, // will be filled in later
                'leftmostCol': 0, // the column from where editing is possible; it is not possible to delete or change a character to the left of this column

                'reflow': function () {
                    var startRow, endRow, i, lineDescriptor;
                    if (arguments.length == 0) {
                        // no arguments, reflow all lines
                        startRow = 0;
                        endRow = lines.length - 1;
                    }
                    else if (arguments.length == 1) {
                        // only the starting row given, reflow all lines from there on
                        startRow = arguments[0];
                        endRow = lines.length - 1;
                    }
                    else {
                        // both arguments are given
                        startRow = arguments[0];
                        endRow = arguments[1];
                    }
                    for (i = startRow; i <= endRow; i++) {
                        lineDescriptor = lines[i];
                        lineDescriptor.row = i;
                        renderLine(lineDescriptor);
                    }
                },

                'removeRow': function (row, deferReflow) {
                    var lineDescriptor;
                    if (row >= lines.length) return; // already absent
                    lineDescriptor = lines[row];
                    lines.splice(row, 1);
                    if (lineDescriptor.jqLine) {
                        lineDescriptor.jqLine.remove();
                        lineDescriptor.jqLine = null;
                    }
                    if (!deferReflow) this.reflow(row);
                    return lineDescriptor;
                },

                'insertRow': function (row, content, className, deferReflow) {
                    // inserts a new row at the position specified
                    var lineDescriptor, i, jqLine;
                    for (i = lines.length; i < row; i++) {
                        // insert any empty rows
                        lineDescriptor = {content: '', className: className, row: i};
                        lines.push(lineDescriptor);
                        if (!deferReflow) renderLine(lineDescriptor);
                    }
                    lineDescriptor = {content: content, className: className, row: row};
                    if (row >= lines.length) lines.push(lineDescriptor);
                    else lines.splice(row, 0, lineDescriptor);
                    if (!deferReflow) this.reflow(row);
                    return lineDescriptor;
                },

                'splice': function (startRow, startCol, endRow, endCol, newText, className) {
                    var i, j, n, startLine, endLine, newLines, newTextLines, part1, part2;

                    if (!className) className = 'output';
                    // pre-process newText, convert it to newLines: an array of strings without newlines
                    newLines = formNewLines(newText);
                    if (newLines.length == 0) newLines.push(''); // there must at least be an empty "glue"

                    if (lines.length > 0) {
                        // there is something to remove
                        // verify sanity
                        if (startRow < 0) startRow = 0;
                        else if (startRow >= lines.length) startRow = lines.length - 1;
                        if (endRow < 0) endRow = 0;
                        else if (endRow >= lines.length) endRow = lines.length - 1;
                        if (endRow < startRow) {
                            // swap the meaning of the start and end coordinates
                            i = startRow;
                            startRow = endRow;
                            endRow = i;
                            i = startCol;
                            startCol = endCol;
                            endCol = i;
                        }
                        if (startRow == endRow) {
                            if (endCol < startCol) {
                                i = startCol;
                                startCol = endCol;
                                endCol = i;
                            }
                        }
                        startLine = lines[startRow];
                        endLine = lines[endRow];
                        if (startCol < 0) startCol = 0;
                        else if (startCol > startLine.length) startCol = startLine.length;
                        if (endCol < 0) endCol = 0;
                        else if (endCol > endLine.length) endCol = endLine.length;

                        // execute splice
                        part1 = startLine.content.substring(0, startCol); // the remaining content on the first and last lines
                        part2 = endLine.content.substring(endCol);
                        // modify the first and last lines, and make the list of the old lines to remove
                        j = startRow + 1; // where to start removing lines
                        n = endRow - startRow; // how many lines to remove
                        if (newLines.length > 1) {
                            // the first and the last new lines are separate existing lines, modify them
                            startLine.content = part1 + newLines[0];
                            if (startRow == endRow) {
                                // we need to create one additional line to hold the compositum of the last line and part2
                                this.insertRow(j, newLines[newLines.length - 1] + part2, className, true);
                                j++;
                            }
                            else {
                                endLine.content = newLines[newLines.length - 1] + part2;
                                n--; // retain the last line
                            }
                        }
                        else {
                            // the first and the last new lines are the same existing line
                            startLine.content = part1 + newLines[0] + part2;
                        }
                        // remove the rest of the old lines
                        while (n > 0) {
                            this.removeRow(j, true);
                            n--;
                        }
                        // insert new lines in between
                        j = newLines.length - 1; // the first and the last lines have already been set up
                        for (i = 1; i < j; i++) {
                            this.insertRow(startRow + i, newLines[i], className, true);
                        }
                        // optimization step: if only one line changed, then only reflow the line
                        if ((n == 0) && (newLines.length == 1)) {
                            renderLine(startLine);
                        }
                        else {
                            this.reflow(startRow);
                        }
                    }
                    else {
                        // no content exists, add all lines
                        for (i = 0; i < newLines.length; i++) {
                            this.insertRow(i, newLines[i], className, true);
                        }
                        this.reflow();
                    }
                },

                'insert': function (text, row, col, className) {
                    this.splice(row, col, row, col, text, className || 'output');
                },

                'append': function (text, className, dontMoveCursor) {
                    // we add to the end, so optimize: do not call splice(), it is too heavy for this purpose
                    var newLines = formNewLines(text),
                        originalCurrentRow = currentRow,
                        startRow = lines.length - 1,
                        i, lineDescriptor;
                    if (newLines.length == 0) return; // nothing to do
                    lineDescriptor = lines[startRow];
                    lineDescriptor.content = lineDescriptor.content + newLines[0];
                    for (i = 1; i < newLines.length; i++) {
                        lineDescriptor = this.insertRow(startRow + i, newLines[i], className, true);
                    }
                    if (!dontMoveCursor) {
                        currentRow = lines.length - 1;
                        currentCol = lines[currentRow].content.length;
                    }
                    this.reflow(startRow < originalCurrentRow ? startRow : originalCurrentRow);
                },

                'insertAtCursor': function (text, className, dontMoveCursor) {
                    var startRow = currentRow,
                        currentLine = lines[currentRow],
                        currentContent = currentLine.content,
                        part1 = currentContent.substring(0, currentCol),
                        part2 = currentContent.substring(currentCol),
                        newLines = formNewLines(text),
                        i, n;
                    if (newLines.length == 0) return; // nothing to do
                    else if (newLines.length == 1) {
                        currentLine.content = part1 + newLines[0] + part2;
                        if (!dontMoveCursor) currentCol += newLines[0].length;
                        renderLine(currentLine);
                    }
                    else {
                        currentLine.content = part1 + newLines[0];
                        n = newLines.length - 1;
                        for (i = 1; i < n; i++) {
                            this.insertRow(startRow + i, newLines[i], className, true);
                        }
                        this.insertRow(startRow + n, newLines[n] + part2, className, true);
                        if (!dontMoveCursor) {
                            currentRow = startRow + n;
                            currentCol = newLines[n].length;
                        }
                        this.reflow(startRow);
                    }
                },

                'moveLeft': function () {
                    var leftmost = typeof this.leftmostCol === 'number' && this.leftmostCol || 0;
                    if (currentCol > leftmost) {
                        currentCol--;
                        renderLine(lines[currentRow]);
                    }
                },

                'moveRight': function () {
                    var lineDescriptor = lines[currentRow];
                    if (currentCol < lineDescriptor.content.length) {
                        currentCol++;
                        renderLine(lineDescriptor);
                    }
                },

                'deleteCharacterLeft': function () {
                    var lineDescriptor = lines[currentRow],
                        content = lineDescriptor.content,
                        leftmost = typeof this.leftmostCol === 'number' && this.leftmostCol || 0;
                    if ((currentCol > leftmost) && (currentCol <= content.length)) {
                        currentCol--;
                        lineDescriptor.content = content.substring(0, currentCol) + content.substring(currentCol + 1);
                        renderLine(lineDescriptor);
                    }
                },

                'deleteCharacterRight': function () {
                    var lineDescriptor = lines[currentRow],
                        content = lineDescriptor.content;
                    if ((currentCol >= 0) && (currentCol < content.length)) {
                        lineDescriptor.content = content.substring(0, currentCol) + content.substring(currentCol + 1);
                        renderLine(lineDescriptor);
                    }
                },

                'moveToEndOfLine': function () {
                    var lineDescriptor = lines[currentRow],
                        n = lineDescriptor.content.length;
                    if (currentCol != n) {
                        currentCol = n;
                        renderLine(lineDescriptor);
                    }
                },

                'moveToStartOfLine': function () {
                    var lineDescriptor = lines[currentRow];
                    if (currentCol != 0) {
                        currentCol = 0;
                        renderLine(lineDescriptor);
                    }
                },

                'destroy': function () {
                    var n = lines.length,
                        i, lineDescriptor;
                    if (blinkTimer !== null) {
                        clearInterval(blinkTimer);
                        blinkTimer = null;
                    }
                    // detach all jQuery objects from internal structures
                    for (i = 0; i < n; i++) {
                        lineDescriptor = lines[i];
                        if (lineDescriptor.jqLine) {
                            lineDescriptor.jqLine.remove();
                            lineDescriptor.jqLine = null;
                        }
                    }
                    lines.length = 0;
                    // clear and detach the rest
                    render = null;
                    renderLine = null;
                    jqContent.remove();
                    jqContent = null;
                    jqInput.remove();
                    jqInput = null;
                    for (i in handler) {
                        if (handler.hasOwnProperty(i)) {
                            delete handler[i];
                        }
                    }
                    jqElt.empty();
                    jqElt = null;
                    options = null;
                }
            },

            internallyDisabled = false,

            handleInput = function (chars) {
                var isPastedFromClipboard = clipboardPasteInProgress || !aKeyIsDown,
                    newLines = chars.split('\n'),
                    N = newLines.length,
                    lastLineIndex = N - 1,
                    i, promise;

                if (inputDisabled || internallyDisabled) {
                    // flatly ignore
                    clipboardPasteInProgress = false;
                    return;
                }

                internallyDisabled = true;
                promise = Q();

                for (i = 0; i < N; i++) {
                    promise = promise
                        .then((function (newLine, lineIndex) {return function () {
                            var cookedChars = lineIndex < lastLineIndex ? newLine + '\n' : newLine, // default
                                thisRow = currentRow,
                                leftmost = typeof handler.leftmostCol === 'number' && handler.leftmostCol || 0,
                                dh;

                            // first cook the input
                            if (typeof handler.onKeypress === 'function') {
                                // only invoke the handler if the newLine is not an empty string, otherwise cookedChars = ''
                                try {
                                    cookedChars = cookedChars && handler.onKeypress(cookedChars, isPastedFromClipboard);
                                }
                                catch (e) {
                                    codeq.log.error('Error during invocation of terminal onKeypress: ' + e, e);
                                }
                            }

                            if (!cookedChars) {
                                // nothing to append
                                return;
                            }

                            // now serve the cooking
                            handler.insertAtCursor(cookedChars, 'input'); // append what we have to the display
                            // scroll to bottom on input
                            dh = jqContent.height() - jqElt.height();
                            if (dh > 0)
                                jqElt.scrollTop(dh);

                            if (typeof handler.onInput === 'function') { // now tell the owner what we've done
                                if (lineBuffered) {
                                    if (thisRow < currentRow) {
                                        // in line-buffered mode emit each line separately, except for the last (current) line
                                        try {
                                            return handler.onInput(lines[thisRow].content.substring(leftmost));
                                        }
                                        catch (e) {
                                            codeq.log.error('Error while invoking terminal onInput: ' + e, e);
                                        }
                                    }
                                }
                                else if (cookedChars) {
                                    try {
                                        return handler.onInput(cookedChars);
                                    }
                                    catch (e) {
                                        codeq.log.error('Error while invoking terminal onInput: ' + e, e);
                                    }
                                }
                            }

                        };})(newLines[i], i))
                        .fail(function (e) {
                            codeq.log.error('Error in handleInput loop: ' + e, e);
                        });
                }

                promise.fin(function () {
                    internallyDisabled = false;
                }).done();

                clipboardPasteInProgress = false;
            },

            clipboardPasteInProgress = false, // whether the previous keydown was a CTRL+V or shift+insert
            aKeyIsDown = false, // whether a key is currently pressed: if it's not and there is input, then we got a paste via mouse
            cursorBlinkRate = (options && options.cursorBlinkRate) || 750, // default: 750 ms
            blinkTimer = null,
            i, j, a, b, c;

        // copy default key handlers
        a = handler.keyMaps;
        for (i in defaultKeyMappings) {
            if (!defaultKeyMappings.hasOwnProperty(i)) continue;
            b = defaultKeyMappings[i];
            c = {};
            a[i] = c;
            for (j in b) {
                if (!b.hasOwnProperty(j)) continue;
                c[j] = b[j];
            }
        }
        a = null;
        b = null;
        c = null;

        // initialize the DOM container
        jqElt.empty();
        jqElt.append(jqContent);
        jqElt.append(jqInput);
        jqElt.on('click', function () {
            // set focus to the textarea so we pick up keypresses
            jqInput.css('position', 'fixed').focus();
        });
        jqElt.on('blur', function () {
            jqInput.css('position', 'absolute');
        });

        // register key handlers
        jqInput.on('keydown', function (evt) {
            var modifiers = ['' + (0 + evt.shiftKey), '' + (0 + (evt.ctrlKey || evt.metaKey)), '' + (0 + evt.altKey)].join(''),
                handlersForModifiers = handler.keyMaps[modifiers],
                acceptKeydown = !(inputDisabled || internallyDisabled), // the default value
                eventInfo = processKeyboardEvent(evt),
                handlerForKeydown = handlersForModifiers && handlersForModifiers[eventInfo.code];

            if (acceptKeydown && (typeof handlerForKeydown === 'function')) {
                try {
                    acceptKeydown = handlerForKeydown.apply(handler, [eventInfo]);
                }
                catch (e) {
                    // ignore errors, TODO: log errors
                }
            }

            if (!acceptKeydown) {
                evt.preventDefault();
                evt.stopPropagation();
                return false;
            }

            // paste: CTRL+V or shift+insert
            clipboardPasteInProgress = ((modifiers === '010') && (eventInfo.code === 'KeyV')) || ((modifiers === '100') && (eventInfo.code === 'Insert'));
            aKeyIsDown = true;

            return true;
        });
        jqInput.on('keyup', function (evt) {
            aKeyIsDown = false;
        });

        if ('oninput' in jqInput[0]) {
            // the element supports input events, use them in preference to keypress events
            jqInput.on('input', function (evt) {
                // this event cannot be prevented/cancelled in the DOM scheme of things, but we can simulate prevention, because we have a "detached" display
                var t = jqInput.val();
                jqInput.val('');
                handleInput(t);
            });
        }
        else {
            // the element does not support the "modern" input events, so it ought to support the old keypress events
            jqInput.on('keypress', function (evt) {
                // this event cannot be prevented/cancelled in the DOM scheme of things, but we can simulate prevention, because we have a "detached" display
                var keyChar;
                try {
                    if ('char' in evt) keyChar = evt.char;
                    else if ('charCode' in evt) keyChar = String.fromCharCode(evt.charCode);
                    else return; // ignore input
                    handleInput(keyChar);
                }
                finally {
                    jqInput.val('');
                }
            });
        }

        // start the cursor blinking if so instructed
        if ((typeof cursorBlinkRate === 'number') && (cursorBlinkRate >= 50)) { // have some sense of sanity
            blinkTimer = setInterval(function () {
                jqContent.find('.cq-con-cursor').toggleClass('inverted');
            }, cursorBlinkRate);
        }

        // emit greeting if provided
        if (options && options.greeting) {
            lines.push({content: options.greeting, className: 'greeting'});
            currentRow++;
        }
        lines.push({content: '', className: 'input'}); // initial input line
        render();

        return handler;
    };
})();