Skip to content

Instantly share code, notes, and snippets.

@snydergd
Created June 26, 2017 03:24
Show Gist options
  • Save snydergd/7a655f02d5faae00b8fdf37699b8751a to your computer and use it in GitHub Desktop.
Save snydergd/7a655f02d5faae00b8fdf37699b8751a to your computer and use it in GitHub Desktop.
#!/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