UNPKG

bnash

Version:

A command-line editor for bash or compatible shells, aiming to provide great UX and productivity

287 lines (242 loc) 6.81 kB
const { bindKey, getKeyBindings, getLastBinding, getBoundKeys } = require('../key-bindings') const { getPromptPosition } = require('../prompt') const runner = require('../runner') const { history } = require('../history') const { getOption } = require('../startup') const { colorize } = require('../colors') const editor = require('../editor') const terminal = require('../terminal') //--------------- Line movement and deletion --------------- function removeLastChar(str) { return [ str.charAt(str.length - 1), str.substring(0, str.length - 1) ] } function removeFirstChar(str) { return [ str.charAt(0), str.substr(1) ] } function backwardDeleteChar(line) { if (line.left.length == 0) return line let [ch, left] = removeLastChar(line.left) return { left, right: line.right } } function deleteChar(line) { if (line.right.length == 0) return line let [ch, right] = removeFirstChar(line.right) return { left: line.left, right } } function backwardChar(line) { if (line.left.length == 0) return line let [ch, left] = removeLastChar(line.left) return { left, right: ch + line.right } } function forwardChar(line) { if (line.right.length == 0) return line let [ch, right] = removeFirstChar(line.right) return { left: line.left + ch, right } } function isLetterOrNum(ch) { if (ch >= '0' && ch <= '9') return true return ch.toLowerCase() != ch.toUpperCase() } function isStartOfWord(str, pos) { if (pos <= 0) return true return isLetterOrNum(str[pos]) && !isLetterOrNum(str[pos - 1]) } function isEndOfWord(str, pos) { if (pos >= str.length) return true return !isLetterOrNum(str[pos]) && isLetterOrNum(str[pos - 1]) } function backwardWord(line) { if (line.left.length == 0) return line for (let pos = line.left.length - 1; pos >= 0; pos--) { if (isStartOfWord(line.left, pos)) { return { left: line.left.substr(0, pos), right: line.left.substr(pos) + line.right } } } } function forwardWord(line) { if (line.right.length == 0) return line for (let pos = 1; pos <= line.right.length; pos++) { if (isEndOfWord(line.right, pos)) { return { left: line.left + line.right.substr(0, pos), right: line.right.substr(pos) } } } } function beginningOfLine(line) { return { left: '', right: line.left + line.right } } function endOfLine(line) { return { left: line.left + line.right, right: '' } } function clearScreen(line) { process.stdout.cursorTo(0, 0) process.stdout.clearScreenDown() return { showPrompt: true, ...line } } let clipboard = '' function killWholeLine(line) { let cursor = getPromptPosition() if (cursor) { process.stdout.cursorTo(cursor.x, cursor.y) } process.stdout.clearScreenDown() clipboard = line.left + line.right return { left: '', right: '' } } function recoverDeletion(line) { // Recover line from killWholeLine, add it at cursor position return { left: line.left + clipboard, right: line.right } } //-------------------- History navigation -------------------- let hSearch = '' function getSearchText(txt) { let lb = getLastBinding() if (lb != upLineOrHistory && lb != downLineOrHistory) { history.toEnd() hSearch = txt } return hSearch } function upLineOrHistory(line) { let left = history.back(getSearchText(line.left)) if (left) return { left, right: '' } else return line } function downLineOrHistory(line) { let left = history.forward(getSearchText(line.left)) if (left) return { left, right: '' } else return line } //--------------- Keys that break the editing process --------------- function enterRemoteMode() { process.stdout.write(terminal.HIDE_TEXT) runner.write("\x05\x15PS1=$'\\x1E\\x1E>'\n") } function acceptLine(line) { let cmd = line.left + line.right if (cmd.trim().length > 0) history.push(cmd) editor.onKeyPressed(key => { if (key.name == 'ctrl-r') enterRemoteMode() else runner.write(key.ch || key.sequence) }) editor.writeLine(line) return runner.runCommand(cmd) } function goodbye(line) { if (line.left.length + line.right.length > 0) return line history.close() line.left = 'exit' return acceptLine(line) } function discardLine() { return acceptLine({ left: '', right: '' }) } //--------------- Interactive commands --------------- function describeKeyBinding(kname, binding, cols) { return colorize(cols.quote, kname.padEnd(15) + ' ') + colorize(cols.program, binding.code.name.padEnd(20) + ' ') + colorize(cols.parameter, binding.desc) } function describeNextKey(line) { if (line.left.length + line.right.length > 0) return line return new Promise(resolve => { process.stdout.write('\nType a key: ') editor.onKeyPressed(key => { process.stdout.write(key.name ? key.name : key.ch) let bnds = getKeyBindings(key.name) if (bnds) { let hlColors = getOption('colors.syntaxHighlight') for (let bnd of bnds) { let desc = describeKeyBinding(key.name, bnd, hlColors) process.stdout.write('\r' + desc + '\n') } } else { process.stdout.write('\n') } resolve() }) }) } function listKeys(line) { let hlColors = getOption('colors.syntaxHighlight') for (let kname of getBoundKeys()) { let bnds = getKeyBindings(kname) for (let b of bnds) process.stdout.write('\n' + describeKeyBinding(kname, b, hlColors)) } process.stdout.write('\n') return { left: line.left, right: line.right, showPrompt: true } } function start() { // Line movement and deletion bindKey('backspace', backwardDeleteChar, 'Delete character left of cursor') bindKey('delete', deleteChar, 'Delete char at cursor') bindKey('left', backwardChar, 'Move cursor left') bindKey('right', forwardChar, 'Move cursor right') bindKey(['home', 'ctrl-a'], beginningOfLine, 'Move cursor to beginning of line') bindKey('ctrl-b', backwardWord, 'Move cursor one word to the left') bindKey('ctrl-f', forwardWord, 'Move cursor one word to the right') bindKey(['end', 'ctrl-e'], endOfLine, 'Move cursor to end of line') bindKey(['escape', 'ctrl-u'], killWholeLine, 'Clears the current line') bindKey('ctrl-y', recoverDeletion, 'Recover cleared line') bindKey('ctrl-l', clearScreen, 'Clears the screen') // History navigation bindKey('up', upLineOrHistory, 'Move backwards through history') bindKey('down', downLineOrHistory, 'Move forward through history') // Keys that break the editing process bindKey('return', acceptLine, 'Run command in line') bindKey('ctrl-d', goodbye, 'Close terminal (only if line is empty)') bindKey('ctrl-c', discardLine, 'Discards the line') // Interactive commands bindKey('ctrl-k', describeNextKey, 'Names typed key (to be used with the binkdKey function)') bindKey('f1', listKeys, 'Lists all key bindings') } module.exports = { start }