Created
June 26, 2017 03:24
-
-
Save snydergd/7a655f02d5faae00b8fdf37699b8751a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
var tty = require('tty') | |
var readline = require('readline') | |
var util = require('util') | |
// Config | |
var keyBindings = { | |
"C-x": "quit", | |
"C-r": "draw-screen", | |
"left": "left", | |
"right": "right", | |
"up": "up", | |
"down": "down", | |
"C-p": "up", | |
"C-n": "down", | |
"return": "insert-newline", | |
"backspace": "backspace", | |
"C-e": "end", | |
"C-a": "home", | |
"tab": "tab", | |
"S-tab": "de-indent", | |
"pageup": "page-up", | |
"pagedown": "page-down" | |
} | |
var config = { | |
"gutter-size": 5, | |
"auto-indent": true, | |
"tab-width": 4, | |
"soft-tabs": true | |
} | |
// Setup?? | |
function EventBinder() { | |
if (!(this instanceof EventBinder)) return new EventBinder(); | |
this.listeners = {} | |
} | |
EventBinder.prototype.on = function(name, cb) { | |
if (!(name in this.listeners)) this.listeners[name] = [] | |
this.listeners[name].push(cb) | |
} | |
EventBinder.prototype.trigger = function() { | |
var name = Array.prototype.splice.call(arguments, 0, 1) | |
var i; | |
if (name in this.listeners) { | |
for (i = 0; i < this.listeners[name].length; i++) { | |
this.listeners[name][i].apply(this, arguments) | |
} | |
} | |
} | |
var editorEvents = new EventBinder(); | |
// INPUT | |
process.stdin.setRawMode(true) | |
editorEvents.on("quit", () => { | |
process.stdin.setRawMode(false) | |
}) | |
readline.emitKeypressEvents(process.stdin) | |
function do_input() { | |
process.stdin.on('keypress', (value,key) => { | |
var str = ""; | |
if (typeof(key) == "undefined" || " ".charCodeAt(0) <= key.sequence.charCodeAt(0) && key.sequence.charCodeAt(0) <= "}".charCodeAt(0)) { | |
commandCallbacks["insert-string"](value.toString()) | |
// console.log("Key:", key, value) | |
// process.stdout.write(value) | |
} else { | |
if (typeof(editorState.content[editorState.cursorLine-1]) !== 'string') { | |
editorState.content[editorState.cursorLine-1] = editorState.content[editorState.cursorLine-1].join('') | |
} | |
// special cases where (e.g.) Ctrl-h gets mapped to backspace by terminal | |
if (key.name == "enter") { | |
key.ctrl = true | |
key.name = "j" | |
} else if (key.sequence == "\b") { | |
key.ctrl = true | |
key.name = "h" | |
} | |
if (key.ctrl) str += "C-" | |
if (key.meta) str += "M-" | |
if (key.shift) str += "S-" | |
str += key.name | |
if (str in keyBindings) { | |
// console.log("triggering command:", keyBindings[str]) | |
if (!(keyBindings[str] in commandCallbacks)) { | |
status("Command " + keybindings[str] + " for keybinding not valid.") | |
} else { | |
commandCallbacks[keyBindings[str]]() | |
} | |
} else { | |
status("Unknown key binding:", str) | |
} | |
} | |
}) | |
} | |
// PROCESS | |
var buffer = "hello\n\tthere\nyou\n"; | |
var editorState = { | |
startLine: 1, | |
cursorLine: 1, | |
cursorCol: 1, | |
cursorColWanted: 1, | |
content: buffer.split("\n"), | |
drawRequired: true, | |
cursorMoved: true | |
} | |
function colWithTabs(line, regularPos) { | |
line = line || editorState.cursorLine | |
var subject = editorState.content[line-1] | |
if (typeof(subject) !== 'string') subject = subject.join('') | |
if (typeof(regularPos) !== "undefined") subject = subject.substr(0, regularPos-1) | |
return subject.replace('\t', ' '.repeat(config["tab-width"])).length+1 | |
} | |
function colWithoutTabs(line, tabPos) { | |
line = line || editorState.cursorLine | |
var i, count = 0 | |
var subject = editorState.content[line-1] | |
if (typeof(subject) !== 'string') subject = subject.join('') | |
for (i = 0; count < tabPos && i <= subject.length; i++) { | |
if (i < subject.length && subject[i] == '\t') count += config["tab-width"] | |
else count += 1 | |
} | |
return i | |
} | |
var commandCallbacks = { | |
"draw-screen": () => { | |
draw(); | |
}, | |
"quit": () => { | |
editorEvents.trigger("quit") | |
}, | |
"scroll-to-cursor": () => { | |
if (editorState.cursorLine < editorState.startLine) { | |
editorState.startLine = editorState.cursorLine | |
editorState.drawRequired = true | |
} else if (editorState.cursorLine-editorState.startLine >= process.stdout.rows) { | |
editorState.startLine = editorState.cursorLine - process.stdout.rows+1 | |
editorState.drawRequired = true | |
} | |
return draw() | |
}, | |
"up": () => { | |
if (editorState.cursorLine <= 1) return | |
editorState.cursorLine-- | |
editorState.cursorCol = Math.min(colWithoutTabs(null, editorState.cursorColWanted), | |
editorState.content[editorState.cursorLine-1].length+1) | |
editorState.cursorMoved = true | |
commandCallbacks["scroll-to-cursor"]() | |
}, | |
"down": () => { | |
if (editorState.cursorLine >= editorState.content.length) return | |
editorState.cursorLine++ | |
editorState.cursorCol = Math.min(colWithoutTabs(null, editorState.cursorColWanted), | |
editorState.content[editorState.cursorLine-1].length+1) | |
editorState.cursorMoved = true | |
commandCallbacks["scroll-to-cursor"]() | |
}, | |
"left": () => { | |
if (editorState.cursorCol <= 1) { | |
if (editorState.cursorLine <= 1) return | |
editorState.cursorLine-- | |
editorState.cursorCol = editorState.cursorColWanted = editorState.content[editorState.cursorLine-1].length+1 | |
editorState.cursorMoved = true | |
commandCallbacks["scroll-to-cursor"]() | |
return | |
} | |
editorState.cursorCol-- | |
editorState.cursorColWanted = colWithTabs(null, editorState.cursorCol) | |
editorState.cursorMoved = true | |
commandCallbacks["scroll-to-cursor"]() | |
}, | |
"right": () => { | |
if (editorState.cursorCol >= editorState.content[editorState.cursorLine-1].length+1) { | |
if (editorState.cursorLine >= editorState.content.length) return | |
editorState.cursorLine++ | |
editorState.cursorCol = editorState.cursorColWanted = 1 | |
editorState.cursorMoved = true | |
commandCallbacks["scroll-to-cursor"]() | |
return | |
} | |
editorState.cursorCol++ | |
editorState.cursorColWanted = colWithTabs(null, editorState.cursorCol) | |
editorState.cursorMoved = true | |
commandCallbacks["scroll-to-cursor"]() | |
}, | |
"insert-newline": () => { | |
var str = editorState.content[editorState.cursorLine-1] | |
var indent = ((config["auto-indent"] && editorState.cursorLine > 1) ? editorState.content[editorState.cursorLine-1].match(/^\s*/)[0] : "" ) | |
editorState.content[editorState.cursorLine-1] = str.substr(0, editorState.cursorCol-1) | |
editorState.content.splice(editorState.cursorLine, | |
0, | |
indent + str.substr(editorState.cursorCol-1) | |
) | |
editorState.cursorLine++ | |
editorState.cursorCol = 1 + indent.length | |
editorState.cursorColWanted = editorState.cursorCol | |
editorState.drawRequired = true | |
commandCallbacks["scroll-to-cursor"]() | |
}, | |
"insert-string": (value) => { | |
if (arguments.length == 0) { | |
// TODO: prompt for value here instead | |
status("insert-string requires a value as a parameter") | |
return | |
} | |
if (typeof(editorState.content[editorState.cursorLine-1]) == 'string') { | |
editorState.content[editorState.cursorLine-1] = [ | |
editorState.content[editorState.cursorLine-1].substr(0,editorState.cursorCol-1), | |
editorState.content[editorState.cursorLine-1].substr(editorState.cursorCol-1) | |
] | |
} | |
editorState.content[editorState.cursorLine-1][0] += value | |
editorState.cursorCol += value.length | |
editorState.cursorColWanted = editorState.cursorCol | |
commandCallbacks["scroll-to-cursor"]() || drawLine() | |
}, | |
"backspace": () => { | |
var parts | |
if (typeof(editorState.content[editorState.cursorLine-1]) == 'string') { | |
parts = [ | |
editorState.content[editorState.cursorLine-1].substr(0,editorState.cursorCol-1), | |
editorState.content[editorState.cursorLine-1].substr(editorState.cursorCol-1) | |
] | |
} else { | |
parts = editorState.content[editorState.cursorLine-1] | |
} | |
if (parts[0].length) { | |
parts[0] = parts[0].slice(0,-1) | |
editorState.cursorCol-- | |
editorState.cursorColWanted = editorState.cursorCol | |
editorState.content[editorState.cursorLine-1] = parts | |
} else { | |
editorState.cursorCol = editorState.cursorColWanted = editorState.content[editorState.cursorLine-2].length+1 | |
editorState.content[editorState.cursorLine-2] += parts[1] | |
editorState.content.splice(editorState.cursorLine-1, 1) | |
editorState.cursorLine-- | |
editorState.drawRequired = true | |
} | |
commandCallbacks["scroll-to-cursor"]() || drawLine() | |
}, | |
"end": () => { | |
editorState.cursorCol = editorState.cursorColWanted = editorState.content[editorState.cursorLine-1].length+1 | |
goToCursor() | |
}, | |
"home": () => { | |
editorState.cursorCol = editorState.cursorColWanted = 1 | |
goToCursor() | |
}, | |
"tab": () => { | |
var whatToAdd | |
if (config["soft-tabs"]) { | |
whatToAdd = ' '.repeat(config["tab-width"] - (editorState.cursorCol-1) % config["tab-width"]) | |
} else { | |
whatToAdd = '\t' | |
} | |
commandCallbacks["insert-string"](whatToAdd) | |
}, | |
"de-indent": () => { | |
var line = editorState.content[editorState.cursorLine-1] | |
if (typeof(line) !== 'string') line = line.join('') | |
var indent = line.match(/^\s*/)[0] | |
if (indent.length) { | |
if (line[0] == '\t') line = line.substr(1) | |
else line = editorState.content[editorState.cursorLine-1].substr(indent.length % config["tab-width"] || config["tab-width"]) | |
editorState.content[editorState.cursorLine-1] = line | |
drawLine() | |
} | |
}, | |
"page-up": () => { | |
}, | |
"page-down": () => { | |
} | |
} | |
// OUTPUT | |
editorEvents.on("quit", () => { | |
readline.cursorTo(process.stdout, 0, 0) | |
readline.clearScreenDown(process.stdout) | |
}) | |
process.stdout.on("resize", () => { | |
status(`resized to ${process.stdout.columns}x${process.stdout.rows}`) | |
editorState.drawRequired = true | |
draw() | |
}) | |
function goToCursor() { | |
readline.cursorTo(process.stdout, colWithTabs(null, editorState.cursorCol)+config["gutter-size"], editorState.cursorLine - editorState.startLine) | |
editorState.cursorMoved = false | |
} | |
function drawLine(i) { | |
var str | |
if (arguments.length == 0) i = editorState.cursorLine - editorState.startLine; | |
var offset = config["gutter-size"]-Math.floor(Math.log10(editorState.startLine+i))-1 | |
readline.cursorTo(process.stdout, offset, i) | |
readline.clearLine(process.stdout, 0) | |
if (editorState.startLine+i > editorState.content.length) return | |
process.stdout.write((editorState.startLine+i).toString()) | |
readline.cursorTo(process.stdout, config["gutter-size"]+1, i) | |
if (typeof(editorState.content[editorState.startLine+i-1]) == 'string') { | |
str = editorState.content[editorState.startLine+i-1] | |
} else { | |
str = editorState.content[editorState.startLine+i-1].join("") | |
} | |
process.stdout.write(str.replace('\t', ' '.repeat(config["tab-width"]))) | |
if (arguments.length == 0) goToCursor() | |
} | |
function draw() { | |
var i, retval = false | |
if (editorState.drawRequired) { | |
for (i = 0; i < process.stdout.rows; i++) { | |
drawLine(i) | |
} | |
editorState.drawRequired = false | |
editorState.cursorMoved = true | |
retval = true | |
} | |
if (editorState.cursorMoved && (editorState.cursorLine >= editorState.startLine && editorState.cursorLine < editorState.startLine + process.stdout.rows)) { | |
goToCursor() | |
} | |
return retval | |
} | |
function status() { | |
var i, str = "" | |
for (i = 0; i < arguments.length; i++) { | |
if (typeof(arguments[i]) !== "string") str += arguments[i].toString() | |
else str += arguments[i] | |
str += " " | |
} | |
readline.cursorTo(process.stdout, 0, process.stdout.rows-1) | |
readline.clearLine(process.stdout, 0) | |
process.stdout.write(str) | |
goToCursor() | |
} | |
// Main | |
if (!process.stdout.isTTY) { | |
console.error("Looks like my output isn't a terminal. That's a problem...") | |
editorEvents.trigger("quit") | |
} | |
editorEvents.on("quit", () => { | |
process.exit() | |
}) | |
draw() | |
do_input() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment