UNPKG

bnash

Version:

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

187 lines (159 loc) 5.87 kB
const { tableMenu } = require('node-terminal-menu') const { memoize, removeRepeatedItems } = require('../utils') const { getOption, setDefaultOptions } = require('../startup') const { bindKey } = require('../key-bindings') const prompt = require('../prompt') const { history, dirHistory } = require('../history') const { colorizer } = require('../colors') const { runCommand } = require('../runner') const editor = require('../editor') const env = require('../env') const terminal = require('../terminal') const syntaxHL = require('./syntax-highlight') function highlightCommand(cmd) { let hls = syntaxHL.highlight(cmd) return syntaxHL.colorize(cmd, hls) } function startsWith(item, text) { return item.startsWith(text) } function includes(item, text) { return item.includes(text) } function inverse(str) { return terminal.INVERSE + str + terminal.NORMAL } function white(str) { return terminal.WHITE + str + terminal.NORMAL } //------------------------- Menu class ------------------------- class HistoryMenu { constructor(line, items, { decorate, updateLine = l => l, runIt = false, filter = startsWith }) { this.line = line this.items = items this.decorate = decorate this.updateLine = updateLine this.runIt = runIt this.filter = filter this.width = this.computeWidth(items) } open() { if (this.items.length == 0) return this.line if (this.items.length == 1) return this.updateLine({ left: this.items[0], right: '' }) return this.showHistoryMenu() } showHistoryMenu() { let menuDone = () => {} let initialItems = this.items let initialLen = this.line.left.length this.menu = this.openMenuWidget(sel => { process.stdout.clearScreenDown() if (sel >= 0) this.line = this.updateLine({ left: this.items[sel], right: '' }) if (this.runIt && sel >= 0) runCommand(this.line.left).then(menuDone) else menuDone({ ...this.line, showPrompt: false }) }) editor.writeLine(this.line) editor.onKeyPressed(key => { if (key.ch || key.name == 'backspace') this.items = this.updateMenu(key, initialItems, initialLen) else { process.stdout.write('\n') if (key.name == 'ctrl-c') key.name = 'escape' if (key.name == 'escape' || this.items.length > 0) this.menu.keyHandler(key.ch, key) editor.writeLine(this.line) } }) return new Promise(resolve => menuDone = resolve) } openMenuWidget(done) { process.stdout.write('\n') let height = Math.min(process.stdout.rows - 10, this.items.length) prompt.adjustPromptPosition(height + 1) return tableMenu({ items: this.items, height, columnWidth: this.width, columns: 1, scrollBarCol: this.width + 1, selection: this.items.length - 1, colors: this.initColors(), done }) } updateMenu(key, initialItems, initialLen) { if (key.ch) this.line.left += key.ch else if (this.line.left.length > initialLen) this.line.left = this.line.left.slice(0, -1) process.stdout.write('\n') let items = initialItems.filter(i => this.filter(i, this.line.left)) if (items.length > 0) { let height = Math.min(process.stdout.rows - 10, items.length) let selection = items.length - 1 this.menu.update({ items, selection, height, scrollStart: 0 }) } else { process.stdout.clearScreenDown() } editor.writeLine(this.line) return items } initColors() { let colors = getOption('colors.history') return { item: i => colorizer(colors.item)(this.decorate(i, false)), selectedItem: i => colorizer(colors.selectedItem)(this.decorate(i, true)), scrollArea: colorizer(colors.scrollArea), scrollBar: colorizer(colors.scrollBar) } } computeWidth(items) { let w = items.reduce((w, i) => Math.max(w, i.length), 0) return Math.min(w + 2, process.stdout.columns - 3) } } //------------------------- Main ------------------------- function cmdHistoryMenu(line) { let highlightCommandMemo = memoize(highlightCommand) let items = history.matchLines(line.left) let decorate = (o, sel) => sel ? inverse(o) : highlightCommandMemo(o) let hm = new HistoryMenu(line, items, { decorate }) return hm.open() } function dirHistoryMenu(line) { let items = dirHistory.matchLines(line.left, includes) // Ensure items are chronological and unique items = removeRepeatedItems(items.reverse()).reverse() let cwd = env.cwd() // Remove current directory from list (user is already there) items = items.filter(i => i != cwd) let decorate = (o, sel) => sel ? inverse(o) : white(o) let updateLine = l => ({ left: 'cd ' + l.left, right: l.right }) let hm = new HistoryMenu(line, items, { decorate, updateLine, runIt: true, filter: includes }) return hm.open() } function setDefaults() { let defaultColors = { item: '/#272822', selectedItem: '', scrollArea: '/#272822', scrollBar: 'whiteBright', } setDefaultOptions('colors.history', defaultColors) } function start() { setDefaults() bindKey('pageup', cmdHistoryMenu, 'Show menu with matching command history lines') bindKey('pagedown', dirHistoryMenu, 'Show menu with matching directory history') } module.exports = { start }