Created
June 26, 2017 03:24
-
-
Save snydergd/7a655f02d5faae00b8fdf37699b8751a to your computer and use it in GitHub Desktop.
This file contains hidden or 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