// jsmacs
// Copyright 2009 Samuel Hughes

/*jslint plusplus: false, white: false, onevar: false, browser: true */
/*global window: false, KeyEvent: false */

"use strict";

// Check a boolean variable, throw an assertion message if false.
// Yes, we're throwing the raw string.  Change this function if you
// want different behavior.
function assert(b, msg) {
    if (!b) {
        throw ("Assertion failed!" + (msg && ("  " + msg)));
    }
}

// map.  What more needs to be said?
function map(xs, f) {
    var ret = [];
    var n = xs.length;
    for (var i = 0; i < n; ++i) {
        ret[i] = f(xs[i], i);
    }
    return ret;
}

// A 'solid' character is one for which forwardWord and backwardWord
// operations recognize as being a word character.
function isSolid(ch) {
    return (ch >= "a" && ch <= "z") ||
        (ch >= "A" && ch <= "Z") ||
        (ch >= "0" && ch <= "9");
}

// useful for logging
function dump(obj, props) {
    var ps = map(props, function(prop) { return prop + ": " + obj[prop]; });
    return ps.join(", ");
}

// A Clipboard object contains the entire yank history -- or a subset
// of it, if memory is tight and if we added that feature.  Which we
// haven't.
function Clipboard() {
    // A list of strings stored in the clipboard.
    this.clips = [];
    // Did we just record some text?  Future text recordings will be
    // appended to the previous For example, if we typed C-k C-k C-k,
    // we'd want those contiguous cuttings to be concatenated into
    // one.
    this.justRecorded = false;
    // How many times have we pasted in a row, using M-y?
    this.pasteNumber = 0;
    // Did we just yank some text from the clipboard?  This number
    // tells how much text we just yanked.
    this.justYanked = null;
}

Clipboard.Left = "Clipboard.Left";
Clipboard.Right = "Clipboard.Right";

Clipboard.prototype = {
    // everything the user does results in a call to one of these
    // functions: recordYank, yank, or nothing.  Each of these updates
    // this.justRecorded and this.justYanked.

    // Tells the clipboard that a chunk of text has been cut from the
    // buffer and should be added to the clipboard.  yankSide tells
    // which side of the cursor the text was on -- which direction the
    // deletion went.  For example, C-k deletes to the right side,
    // towards the end of the line.
    recordYank: function(text, yankSide) {
        if (this.justRecorded) {
            var m = this.clips.length - 1;
	    if (yankSide === Clipboard.Left) {
                this.clips[m] = text + this.clips[m];
            }
            else if (yankSide === Clipboard.Right) {
                this.clips[m] = this.clips[m] + text;
            }
            else {
                console.log("Invalid yankSide: " + yankSide);
            }
        }
        else {
            this.clips.push(text);
        }
        this.pasteNumber = 0;
        
        this.justRecorded = true;
        this.justYanked = null;
    },

    // Yanks some text from the clipboard.  
    yank: function() {
        var sz = this.clips.length;
        var ret = (sz > 0) ? this.clips[sz - 1 - (this.pasteNumber % sz)] : "";

        this.justRecorded = false;
        this.justYanked = ret.length;

        return ret;
    },
    // Indicates that an action didn't use the clipboard.
    nothing: function() {
        this.justRecorded = false;
        this.justYanked = null;
    },
    // Updates the yank number, for use by M-y.
    stepYank: function() {
        this.pasteNumber = this.pasteNumber + 1;
    }
};

// a Buffer is a pure data structure describing a buffer with a cursor
// and an opinion about what its column number should be.
function Buffer(text) {
    // the text before/after the cursor
    this.bef = "";
    this.aft = text;
    // the absolute offset of the mark
    this.mark = null;
    // the virtual column number, which tells what column we _would_
    // be in, if the line were long enough.
    this.virtualColumn = 0;
    // past and future are lists of undo items.  An undo item
    // describes a change -- one can apply the change and one can also
    // take the inverse of the item.  The past array is ordered such
    // that if you apply its changes in reverse order, starting at
    // past.length-1, you'll return the buffer to its initial state.
    // If you apply the changes in 'future' in order, starting at 0
    // and ending at future.length - 1, you'll change the buffer to
    // its "most futuristic" state.  The act of undoing pops a value
    // from past and shifts its inverse onto future.
    this.undoInfo = { past: [], future: [] };

    this.clipboard = new Clipboard();
}

Buffer.Left = "Buffer.Left";
Buffer.Right = "Buffer.Right";
Buffer.Insert = "Buffer.Insert";
Buffer.Delete = "Buffer.Delete";
Buffer.Atomic = "Buffer.Atomic";
Buffer.Mountain = "Buffer.Mountain";

function opposite(change) {
    return {
        type: Buffer.Atomic,
        beg: change.beg,
        text: change.text,
        action: change.action === Buffer.Delete ? Buffer.Insert : Buffer.Delete,
        side: change.side
    };
}

Buffer.prototype = {
    // which functions are permitted to modify this.virtualColumn?
    // Which are permitted to modify this.mark?  Which can touch
    // this.undoInfo?  Itisamystery.

    // Yanks a piece of text from the clipboard.
    yank: function() {
        var text = this.clipboard.yank();
        var undoItem = this.doAppendLeft(text);

        this.addEdit(undoItem);
    },
    // If we just yanked, yanks the next-most recent text from the
    // clipboard.
    altYank: function() {
        if (this.clipboard.justYanked !== null) {
            this.clipboard.stepYank();
            this.deleteLeft(this.clipboard.justYanked);
            this.yank();
        }
    },
    // Our real, physical, current column number.
    currentColumn: function() {
        return this.bef.length - this.bef.lastIndexOf("\n") - 1;
    },
    // Adds an edit to the undo info, de-looping the future too.
    addEdit: function(edit) {
        if (this.undoInfo.future.length > 0) {
	    this.undoInfo.past.push({
                type: Buffer.Mountain,
                history: this.undoInfo.future
            });
            this.undoInfo.future = [];
        }
        
        this.undoInfo.past.push(edit);
    },
    // Adds an edit to the future.
    reverseAddEdit: function(edit) {
        this.undoInfo.future.push(edit);
    },
    // Appends text to the left of the cursor, returning said action's
    // reverse edit.
    doAppendLeft: function(text) {
        var i = this.bef.length;
        this.bef = this.bef + text;
        this.virtualColumn = this.currentColumn();
        this.mark = this.mark + (this.mark > i ? text.length : 0);

        return {
            type: Buffer.Atomic,
            beg: i,
	    text: text,
            action: Buffer.Delete,
            side: Buffer.Left
        };
    },
    // Appends text to the right of the cursor, returning said
    // action's reverse edit.
    doAppendRight: function(text) {
        var i = this.bef.length;
        this.aft = text + this.aft;
        this.mark = this.mark + (this.mark > i ? text.length : 0);

        return {
            type: Buffer.Atomic,
	    beg: i,
	    text: text,
            action: Buffer.Delete,
            side: Buffer.Right
        };
    },
    // Deletes text to the left of the cursor, returning said action's
    // reverse edit and the deleted text.
    doDeleteLeft: function(n) {
        var pt = this.bef.length;
        n = Math.min(pt, n);
        var deletedText = this.bef.slice(pt - n);
        this.bef = this.bef.slice(0, pt - n);
        this.virtualColumn = this.currentColumn();
        var mk = this.mark;
        this.mark = (mk > pt - n ? Math.max(0, pt - n, mk - n) : mk);

        return {
            undoItem: {
                type: Buffer.Atomic,
                beg: this.bef.length,
                text: deletedText,
                action: Buffer.Insert,
                side: Buffer.Left
            },
            deletedText: deletedText
        };
    },
    // Deletes text to the right of the cursor, returning said
    // action's reverse edit and the deleted text.
    doDeleteRight: function(n) {
        var pt = this.bef.length;
	n = Math.min(this.aft.length, n);
        var deletedText = this.aft.substr(0, n);
        this.aft = this.aft.substr(n);
        this.mark = (this.mark > pt ? Math.max(pt, this.mark - n) : this.mark);

	return {
	    undoItem: {
                type: Buffer.Atomic,
                beg: this.bef.length,
                text: deletedText,
                action: Buffer.Insert,
                side: Buffer.Right
            },
            deletedText: deletedText
        };
    },
    // Appends text to the left of the cursor.
    appendLeft: function(text, noundo) {
        var undoItem = this.doAppendLeft(text);

        this[noundo ? "reverseAddEdit" : "addEdit"](undoItem);

        this.clipboard.nothing();
    },
    // Appends text to the right of the cursor.
    appendRight: function(text, noundo) {
        var undoItem = this.doAppendRight(text);

        this[noundo ? "reverseAddEdit" : "addEdit"](undoItem);

        this.clipboard.nothing();
    },
    // Deletes text to the left of the cursor.
    deleteLeft: function(n, noundo, yank) {
        var info = this.doDeleteLeft(n);
    
        this[noundo ? "reverseAddEdit" : "addEdit"](info.undoItem);

        if (yank) {
            this.clipboard.recordYank(info.deletedText, Clipboard.Left);
        }
        else {
            this.clipboard.nothing();
        }
    },
    // Deletes text to the right of the cursor.
    deleteRight: function(n, noundo, yank) {
        var info = this.doDeleteRight(n);

        this[noundo ? "reverseAddEdit" : "addEdit"](info.undoItem);

        if (yank) {
            this.clipboard.recordYank(info.deletedText, Clipboard.Right);
        }
        else {
            this.clipboard.nothing();
        }
    },
    // Moves the cursor left upto n characters.
    moveLeft: function(n) {
        var mid = this.bef.substr(Math.max(0, this.bef.length - n));
        this.bef = this.bef.substr(0, this.bef.length - n);
        this.aft = mid + this.aft;
        this.virtualColumn = this.currentColumn();
        this.clipboard.nothing();
    },
    // Moves the cursor right upto n characters.
    moveRight: function(n) {
        var mid = this.aft.substr(0, n);
        this.aft = this.aft.substr(n);
        this.bef = this.bef + mid;
        this.virtualColumn = this.currentColumn();
        this.clipboard.nothing();
    },
    // Moves the cursor to the beginning of the line.
    moveToBeginningOfLine: function() {
        var c = this.currentColumn();
        this.moveLeft(c);
    },
    // Calculates the distance to the end of the line.
    distanceToEndOfLine: function() {
        var index = this.aft.indexOf("\n");
        return index === -1 ? this.aft.length : index;
    },
    // Moves the cursor to the end of the line.
    moveToEndOfLine: function() {
        var offset = this.distanceToEndOfLine();
        this.moveRight(offset);
    },
    // Moves the cursor "down" a row.
    moveDown: function() {
        var c = this.virtualColumn;
        this.moveToEndOfLine();
        this.moveRight(1);
        var d = this.distanceToEndOfLine();
        this.moveRight(Math.min(c, d));
        this.virtualColumn = c;
    },
    // Moves the cursor "up" a row.
    moveUp: function() {
        // hurf
        var c = this.virtualColumn;
        this.moveToBeginningOfLine();
        this.moveLeft(1);
        this.moveToEndOfLine();
        var d = this.currentColumn();
        this.moveToBeginningOfLine();
        this.moveRight(Math.min(d, c));
        this.virtualColumn = c;
    },
    // The distance one would move if one were to move forward a word.
    forwardWordDistance: function() {
        var count = 0;
        var reachedSolid = false;
        while (count < this.aft.length) {
            var ch = this.aft.substr(count, 1);
            if (isSolid(ch)) {
                reachedSolid = true;
            }
            else if (reachedSolid) {
                break;
            }

            count += 1;
        }
        return count;
    },
    // The distance one would move if one were to move backward a
    // word.
    backwardWordDistance: function() {
        var count = 1;
        var reachedSolid = false;
        while (count <= this.bef.length) {
            var ch = this.bef.substr(-count, 1);
            if (isSolid(ch)) {
                reachedSolid = true;
            }
            else if (reachedSolid) {
                break;
            }

            count += 1;
        }
        return count - 1;
    },
    // Moves the cursor forward a word.
    forwardWord: function() {
        var distance = this.forwardWordDistance();
        this.moveRight(distance);
    },
    // Moves the cursor backward a word.
    backwardWord: function() {
        var distance = this.backwardWordDistance();
        this.moveLeft(distance);
    },
    // Deletes the next word.
    deleteForwardWord: function() {
        var distance = this.forwardWordDistance();
        this.deleteRight(distance, false, true);
    },
    // Deletes the previous word.
    deleteBackwardWord: function() {
        var distance = this.backwardWordDistance();
        this.deleteLeft(distance, false, true);
    },
    // Kills to the end of the line, or, if we're at the line, kills
    // the newline character.
    killLine: function() {
        var i = this.aft.indexOf("\n");
        if (i === -1) {
            this.deleteRight(this.aft.length, false, true);
        }
        else if (i === 0) {
            this.deleteRight(1, false, true);
        }
        else {
            this.deleteRight(i, false, true);
        }
    },
    // Sets the mark to the current position.
    setMark: function() {
        this.mark = this.bef.length;
    },
    // Kills the region of text between the cursor and the mark.
    killRegion: function() {
        if (this.mark !== null) {
            if (this.mark < this.bef.length) {
                this.deleteLeft(this.bef.length - this.mark, false, true);
            }
            else if (this.mark > this.bef.length) {
                this.deleteRight(this.mark - this.bef.length, false, true);
            }
        }
    },
    // Sets the cursor to a given offset.
    setCursor: function(offset) {
        if (offset < this.bef.length) {
            this.moveLeft(this.bef.length - offset);
        }
        else if (offset > this.bef.length) {
            this.moveRight(offset - this.bef.length);
        }
    },

    undo: function() {
        // So how does undo work?  It seems that undo generally
        // follows the following scheme:
        //
        // undoInfo : { past: [Change], future: [Change] }.
        //
        // Given a piece of undo information u, when you make an edit
        // with change c, our new piece of undo information looks
        // like:
        //
        // { past: u.past + u.future + reverse(u.future) + c,
        //   future: [] }.
        //
        // We'd like not to be so wasteful; we can use a tree, but I
        // am lazy.

        var undoItem = this.undoInfo.past.pop();


	var that = this;

	var atomicUndo = function(undo) {
	    if (undo.action === Buffer.Insert) {
	       that.setCursor(undo.beg);
		if (Buffer.Left === undo.side) {
                    that.appendLeft(undo.text, true);
                }
                else if (Buffer.Right === undo.side) {
                    that.appendRight(undo.text, true);
                }
                else {
                    // good old string exceptions
                    throw ("error: " + undo.side);
                }
            }
            else if (undo.action === Buffer.Delete) {
                if (Buffer.Left === undo.side) {
                    that.setCursor(undo.beg + undo.text.length);
                    that.deleteLeft(undo.text.length, true);
                }
                else if (Buffer.Right === undo.side) {
                    that.setCursor(undo.beg);
                    that.deleteRight(undo.text.length, true);
                }
                else {
                    throw ("error delete: " + dump(undo));
                }
            }
        };
        
        if (undoItem) {
            if (undoItem.type === Buffer.Atomic) {
                atomicUndo(undoItem);
            }
            else if (undoItem.type === Buffer.Mountain) {
                var item = undoItem.history.pop();
                this.undoInfo.past.push(opposite(item));
                if (undoItem.history.length > 0) {
                    this.undoInfo.past.push(undoItem);
                }
                atomicUndo(item);
            }
        }
    }
};

// Computes the style properties of the element in the DOM.
function getComputedStyles(element, styleProps) {
    if (element.currentStyle) {
        return map(styleProps, function(p) { return element.currentStyle[p]; });
    }
    else if (window.getComputedStyle) {
        var sty = document.defaultView.getComputedStyle(element, null);
        return map(styleProps, function(p) { return sty.getPropertyValue(p); });
    }
    else {
        return null;
    }
}

// Gets the width of a piece of text if it were in the DOM under the
// given parent element.
function getTextWidth(parent, text) {
    var div = document.createElement("div");
    var textNode = document.createTextNode(text);
    div.appendChild(textNode);

    // force width to be minimal.
    div.style.cssFloat = "left";

    parent.appendChild(div);
    var width = getComputedStyles(div, ["width"])[0];
    parent.removeChild(div);

    // width should be like "123px".  We want an int.

    return parseInt(width, 10);
}

// Removes all the child nodes from a DOM element.
function clearChildNodes(element) {
    while (element.firstChild) {
        element.removeChild(element.firstChild);
    }
}


// Creates a ticker for a cursor that blinks a cursor once you
// activate it.
function CursorTicker(blinkInterval, colors) {
    // How many milliseconds per blink?  The default's 500.
    this.blinkInterval = blinkInterval || 500;
    // What should the cursor colors be?  The default's black on lime.
    this.colors = colors || { cursorColor: "black", cursorBackground: "lime" };
    // Contains the current setTimeout return value, that gets
    // triggered when the blink interval comes around.
    this.currentTimeout = null;
}

CursorTicker.prototype = {
    // Resets the cursor and starts making 'element' blink, starting
    // with the initial, visible-cursor state.
    resetCursor: function(element) {
        clearTimeout(this.currentTimeout);
        element.style.color = this.colors.cursorColor;
        element.style.backgroundColor = this.colors.cursorBackground;
        var that = this;

	// in honor of all the flips
	var flip = function() { that.inverseResetCursor(element); };
        this.currentTimeout = setTimeout(flip, this.blinkInterval);
    },
    // Makes 'element' blink, starting with the invisible-cursor
    // state.
    inverseResetCursor: function(element) {
        clearTimeout(this.currentTimeout);
        element.style.color = "inherit";
        element.style.backgroundColor = "inherit";
        var that = this;
	var flip = function() { that.resetCursor(element); };
        this.currentTimeout = setTimeout(flip, this.blinkInterval);
    }
};

// TextChopper does dynamic linewrapping, relative to the window
// width.  It doesn't do "word wrapping," since we're not rendering
// the buffer that way.  We're going for Emacs-like behavior.
function TextChopper(pre) {
    // This value is merely the previous char width.  We use it to
    // guess future char widths, for performance reasons.
    this.expectedCharWidth = 10;
    // The DOM element into which we're trying to fit our text.
    this.pre = pre;
}

TextChopper.prototype = {
    // Finds the point within the buffer at which the text should be chopped.
    findLineChopPoint: function(text) {
        assert(text.length !== 0, "text.length !== 0");

        // the width of the pre in pixels
        var preWidth = parseInt(getComputedStyles(this.pre, ["width"])[0], 10);

        // a text width that fits but might be suboptimal.  (We
        // initialize this to 1, but it might be the case that even 1
        // character won't fit within the browser window.  This
        // function will not return 0, because that will just result
        // in an infinite loop.
        var lo = 1;

        // the highest text width that we haven't proven doesn't fit.
        var hi = text.length;

        // what the average character width seems to be, in pixels.
        var observedCharWidth = this.expectedCharWidth;
    
        var n, w;

        // we know hi !== 0 because we handle the text.length ===
        // 0 case.
        assert(lo <= hi, "lo <= hi");

        while (lo < hi) {
            // add 1 to observedCharWidth to account for cursor
	    n = Math.floor(preWidth / (observedCharWidth + 1));
            n = Math.max(lo + 1, Math.min(hi, n));

            // At this point, lo < n <= hi.
            assert(lo < n, "lo < n");
            assert(n <= hi, "n <= hi");

            // add a space to account for cursor
            w = getTextWidth(this.pre, text.substr(0, n) + " ");

            // The important thing here is that either hi shrinks or
            // lo increases.
            if (w > preWidth) {
                // n <= hi, thus hi is about to decrease.
                hi = n - 1;
            }
            else {
                // n > lo, thus lo is about to increase.
                lo = n;
            }

            // add 1 to n to account for cursor.
            observedCharWidth = w / (n + 1);
        }

        // We're done converging!
        this.expectedCharWidth = observedCharWidth;

        assert(lo === hi, "lo === hi");
        assert(lo > 0, "lo > 0");  // this function will _not_ return zero.
        return lo;
    }
};

// A constructor that, given a pre, makes it editable.
function Terminal(pre) {
    this.pre = pre;
    this.buffer = new Buffer(pre.textContent);
    this.ticker = new CursorTicker();
    this.textChopper = new TextChopper(pre);

    var that = this;
    document.onkeypress = function(event) {
        return that.handleKeyPress(event);
    };

    this.renderBuffer();
}

// Returns things like "C-x", "C-M-S-y", "<M-delete>", etc, to
// describe the state of the keypress.
function getFullKeyDescription(event) {
    var keyName = event.charCode ?
        String.fromCharCode(event.charCode) :
        Terminal.keyCodeTable[event.keyCode];

    keyName = (keyName === " " ? "<space>" : keyName);

    if (keyName) {
        var isArrowName = keyName.length > 2 &&
            keyName.substr(0, 1) === "<" && keyName.substr(-1) === ">";
        var prefix = (event.ctrlKey ? "C-" : "") + (event.altKey ? "M-" : "");
        if (isArrowName) {
            prefix += (event.shiftKey ? "S-" : "");
            return "<" + prefix + keyName.slice(1, -1) + ">";
        }
        else {
            return prefix + keyName;
        }
    }
    else {
        return null;
    }
}

Terminal.prototype = {
    // This rather long function renders the buffer.
    renderBuffer: function() {
        // Clear the child nodes.  They need to be clear so that we
        // can properly measure the text chopping.
        clearChildNodes(this.pre);

        var lines = [];
        var choppeeLines = this.buffer.bef.split("\n");
        var i, line, chopPoint;

        assert(choppeeLines.length > 0, "choppeeLines.length > 0");

        for (i = 0; i < choppeeLines.length; ++i) {
            line = choppeeLines[i];
	    if (line.length === 0) {
                lines.push("");
            }
            else {
                while (line.length > 0) {
                    chopPoint = this.textChopper.findLineChopPoint(line);
                    lines.push(line.substr(0, chopPoint));
                    line = line.substr(chopPoint);
                }
            }
        }
        assert(lines.length > 0, "lines.length > 0");

        var aftColumnOffset = lines[lines.length - 1];

        var aftLines = [];
        var aftChoppeeLines = this.buffer.aft.split("\n");
        assert(aftChoppeeLines.length > 0, "aftChoppeeLines.length > 0");

        aftChoppeeLines[0] = aftColumnOffset + aftChoppeeLines[0];
        for (i = 0; i < aftChoppeeLines.length; ++i) {
            line = aftChoppeeLines[i];
            if (line.length === 0) {
                aftLines.push("");
            }
            else {
                while (line.length > 0) {
                    chopPoint = this.textChopper.findLineChopPoint(line);
                    aftLines.push(line.substr(0, chopPoint));
                    line = line.substr(chopPoint);
                }
            }
        }

        assert(aftLines.length > 0, "aftLines.length > 0");
        aftLines[0] = aftLines[0].substr(aftColumnOffset.length);

        var befText = lines.join("\n");
        var aftText = aftLines.join("\n");
        // This is some web-specific hackery, to get cursor-rendering
        // right.
        if (aftText.substr(0, 1) === "\n" && this.buffer.aft.substr(0, 1) !== "\n") {
            befText = befText + "\n";
            aftText = aftText.substr(1);
        }

        // Yes, we clear the child nodes again.
        clearChildNodes(this.pre);
        // The node for text before the cursor.
        var befNode = document.createTextNode(befText);
        // The node for text in the cursor (with the weird background).
        var aftNode = document.createTextNode(aftText.substr(1));
        // The node for text after the cursor.
        var curNode = document.createElement("span");
        var curNodeTextString = aftText.substr(0, 1);
        if (curNodeTextString === "\n" || curNodeTextString === "") {
            curNodeTextString = " " + curNodeTextString;
        }
        var curNodeText = document.createTextNode(curNodeTextString);
        curNode.appendChild(curNodeText);
        this.ticker.resetCursor(curNode);
        
        this.pre.appendChild(befNode);
        this.pre.appendChild(curNode);
        this.pre.appendChild(aftNode);

        // the last newline of a pre is usually not rendered, so we
        // add an extra.  This is more web-specific hackery to get
        // cursor rendering right.
        if (aftText.substr(-1) === "\n") {
            this.pre.appendChild(document.createTextNode("\n"));
        }
    },
    // a dictionary of keybindings
    keyTable: {
        "C-a": function(term) { term.buffer.moveToBeginningOfLine(); },
        "C-b": function(term) { term.buffer.moveLeft(1); },
        "M-b": function(term) { term.buffer.backwardWord(); },
        "C-d": function(term) { term.buffer.deleteRight(1); },      
        "M-d": function(term) { term.buffer.deleteForwardWord(); },
        "C-e": function(term) { term.buffer.moveToEndOfLine(); },
        "C-f": function(term) { term.buffer.moveRight(1); },
        "M-f": function(term) { term.buffer.forwardWord(); },
        "C-k": function(term) { term.buffer.killLine(); },
        "C-n": function(term) { term.buffer.moveDown(); },
        "C-p": function(term) { term.buffer.moveUp(); },
        "C-w": function(term) { term.buffer.killRegion(); },
        "C-y": function(term) { term.buffer.yank() ; },
        "M-y": function(term) { term.buffer.altYank(); },
        "C-_": function(term) { term.buffer.undo(); },
        "<space>": function(term) { term.buffer.appendLeft(" "); },
        "<C-space>": function(term) { term.buffer.setMark(); },
        "<backspace>": function(term) { term.buffer.deleteLeft(1); },
	"<C-backspace>": function(term) { term.buffer.deleteBackwardWord(); },
        "<M-backspace>": function(term) { term.buffer.deleteBackwardWord(); },
        "<return>": function(term) { term.buffer.appendLeft("\n"); },
        "<delete>": function(term) { term.keyTable["C-d"](term); },
        "<M-delete>": function(term) { term.keyTable["M-d"](term); },
        "<left>": function(term) { term.buffer.moveLeft(1); },
        "<right>": function(term) { term.buffer.moveRight(1); },
        "<up>": function(term) { term.buffer.moveUp(); },
        "<down>": function(term) { term.buffer.moveDown(); },
        "<end>": function(term) { term.buffer.moveToEndOfLine(); },
        "<home>": function(term) { term.buffer.moveToBeginningOfLine(); }
    },
    // handle our key presses.
    handleKeyPress: function(event) {
        var unrecognized = false;
        var key = getFullKeyDescription(event);
        if (key.length === 1) {
            this.buffer.appendLeft(key);
        }
        else {
            var f = this.keyTable[key];
            if (f) {
                f(this);
            }
            else {
                unrecognized = true;
            }
        }

        this.renderBuffer();

        if (unrecognized) {
            console.log(dump(event, ["keyCode", "charCode"]) + ", " + key);
        }
        return unrecognized;
    },

    error: function(msg) {
        clearChildNodes(this.pre);
        this.pre.appendChild(document.createTextNode(msg));
    }
};

function transformKeyCodeTable(table) {
    // takes (DOM_VK_FOO: bar) pairings and makes
    // (KeyEvent.DOM_VK_FOO: bar) pairings.
    var ret = {};
    for (var prop in table) {
        if (table.hasOwnProperty(prop)) {
	    ret[KeyEvent[prop]] = table[prop];
        }
    }
    return ret;
}

// keyCode to key name mappings.
Terminal.keyCodeTable = transformKeyCodeTable({
    DOM_VK_BACK_SPACE: "<backspace>",
    DOM_VK_RETURN: "<return>",
    DOM_VK_DELETE: "<delete>",
    DOM_VK_LEFT: "<left>",
    DOM_VK_RIGHT: "<right>",
    DOM_VK_UP: "<up>",
    DOM_VK_DOWN: "<down>",
    DOM_VK_END: "<end>",
    DOM_VK_HOME: "<home>"
});


// Attaches a buffer to an existing pre element.
function attachBuffer(pre) {
    var t = new Terminal(pre);
    return t;
}

