-
-
Save creationix/aab84d54b15a2baa97c9 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
--[[ | |
Copyright 2014 The Luvit Authors. All Rights Reserved. | |
Licensed under the Apache License, Version 2.0 (the "License"); | |
you may not use this file except in compliance with the License. | |
You may obtain a copy of the License at | |
http://www.apache.org/licenses/LICENSE-2.0 | |
Unless required by applicable law or agreed to in writing, software | |
distributed under the License is distributed on an "AS-IS" BASIS, | |
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
See the License for the specific language governing permissions and | |
limitations under the License. | |
--]] | |
-- Heavily inspired by ljlinenoise : <http://fperrad.github.io/ljlinenoise/> | |
local traceback = require('debug').traceback | |
local utils = require('utils') | |
local stdin = utils.stdin | |
local stdout = utils.stdout | |
local sub = string.sub | |
local gmatch = string.gmatch | |
local remove = table.remove | |
local insert = table.insert | |
local concat = table.concat | |
local History = {} | |
exports.History = History | |
function History:add(line) | |
assert(type(line) == "string", "line must be string") | |
while #self >= self.maxLength do | |
remove(self, 1) | |
end | |
insert(self, line) | |
return true | |
end | |
function History:setMaxLength(length) | |
assert(type(length) == "number", "max length length must be number") | |
self.maxLength = length | |
while #self > length do | |
remove(self, 1) | |
end | |
return true | |
end | |
function History:clean() | |
for i = 1, #self do | |
self[i] = nil | |
end | |
return true | |
end | |
function History:dump() | |
return concat(self, "\n") .. '\n' | |
end | |
function History:load(data) | |
assert(type(data) == "string", "history dump required as string") | |
for line in gmatch(data, "[^\n]+") do | |
insert(self, line) | |
end | |
return true | |
end | |
function History:updateLastLine(line) | |
self[#self] = line | |
end | |
History.__index = History | |
function History.new() | |
local history = { maxLength = 100 } | |
return setmetatable(history, History) | |
end | |
local Editor = {} | |
function Editor:refreshLine() | |
local line = self.line | |
local position = self.position | |
-- Cursor to left edge | |
local command = "\x1b[0G" | |
-- Write the prompt and the current buffer content | |
.. self.prompt .. line | |
-- Erase to right | |
.. "\x1b[0K" | |
-- Move cursor to original position. | |
.. "\x1b[0G\x1b[" .. tostring(position + self.promptLength - 1) .. "C" | |
stdout:write(command) | |
end | |
function Editor:insert(character) | |
local line = self.line | |
local position = self.position | |
if #line == position - 1 then | |
self.line = line .. character | |
self.position = position + 1 | |
if self.promptLength + #self.line < self.columns then | |
stdout:write(character) | |
else | |
self:refreshLine() | |
end | |
else | |
-- Insert the letter in the middle of the line | |
self.line = sub(line, 1, position - 1) .. character .. sub(line, position) | |
self.position = position + 1 | |
self:refreshLine() | |
end | |
self.history:updateLastLine(self.line) | |
end | |
function Editor:moveLeft() | |
if self.position > 1 then | |
self.position = self.position - 1 | |
self:refreshLine() | |
end | |
end | |
function Editor:moveRight() | |
if self.position - 1 ~= #self.line then | |
self.position = self.position + 1 | |
self:refreshLine() | |
end | |
end | |
function Editor:getHistory(delta) | |
local history = self.history | |
local length = #history | |
local index = self.historyIndex | |
if length > 1 then | |
index = index + delta | |
if index < 1 then | |
self.historyIndex = 1 | |
return | |
elseif index > length then | |
self.historyIndex = length | |
return | |
end | |
local line = self.history[index] | |
self.line = line | |
self.historyIndex = index | |
self.position = #line + 1 | |
self:refreshLine() | |
end | |
end | |
function Editor:backspace() | |
local line = self.line | |
local position = self.position | |
if position > 1 and #line > 0 then | |
self.line = sub(line, 1, position - 2) .. sub(line, position) | |
self.position = position - 1 | |
self.history:updateLastLine(self.line) | |
self:refreshLine() | |
end | |
end | |
function Editor:delete() | |
local line = self.line | |
local position = self.position | |
if position > 0 and #line > 0 then | |
self.line = sub(line, 1, position - 1) .. sub(line, position + 1) | |
self.history:updateLastLine(self.line) | |
self:refreshLine() | |
end | |
end | |
function Editor:swap() | |
local line = self.line | |
local position = self.position | |
if position > 1 and position <= #line then | |
self.line = sub(line, 1, position - 2) | |
.. sub(line, position, position) | |
.. sub(line, position - 1, position - 1) | |
.. sub(line, position + 1) | |
if position ~= #line then | |
self.position = position + 1 | |
end | |
self.history:updateLastLine(self.line) | |
self:refreshLine() | |
end | |
end | |
function Editor:deleteLine() | |
self.line = '' | |
self.position = 1 | |
self.history:updateLastLine(self.line) | |
self:refreshLine() | |
end | |
function Editor:deleteEnd() | |
self.line = sub(self.line, 1, self.position - 1) | |
self.history:updateLastLine(self.line) | |
self:refreshLine() | |
end | |
function Editor:moveHome() | |
self.position = 1 | |
self:refreshLine() | |
end | |
function Editor:moveEnd() | |
self.position = #self.line + 1 | |
self:refreshLine() | |
end | |
local function findLeft(line, position, wordPattern) | |
local pattern = wordPattern .. "$" | |
if position == 1 then return 1 end | |
local s | |
repeat | |
local start = sub(line, 1, position - 1) | |
s = string.find(start, pattern) | |
if not s then | |
position = position - 1 | |
end | |
until s or position == 1 | |
return s or position | |
end | |
function Editor:deleteWord() | |
local position = self.position | |
local line = self.line | |
self.position = findLeft(line, position, self.wordPattern) | |
self.line = sub(line, 1, self.position - 1) .. sub(line, position) | |
self:refreshLine() | |
end | |
function Editor:jumpLeft() | |
self.position = findLeft(self.line, self.position, self.wordPattern) | |
self:refreshLine() | |
end | |
function Editor:jumpRight() | |
local _, e = string.find(self.line, self.wordPattern, self.position) | |
self.position = e and e + 1 or #self.line + 1 | |
self:refreshLine() | |
end | |
function Editor:clearScreen() | |
stdin:write('\x1b[H\x1b[2J') | |
self:refreshLine() | |
end | |
function Editor:onKey(key) | |
local char = string.byte(key, 1) | |
if char == 13 then -- Enter | |
return self.line | |
elseif char == 3 then -- Control-C | |
return false | |
elseif char == 127 -- Backspace | |
or char == 8 then -- Control-H | |
self:backspace() | |
elseif char == 4 then -- Control-D | |
if #self.line > 0 then | |
self:delete() | |
else | |
self.history:updateLastLine() | |
return false | |
end | |
elseif char == 20 then -- Control-T | |
self:swap() | |
elseif key == '\027[A' -- Up Arrow | |
or char == 16 then -- Control-P | |
self:getHistory(-1) | |
elseif key == '\027[B' -- Down Arrow | |
or char == 14 then -- Control-N | |
self:getHistory(1) | |
elseif key == '\027[C' -- Right Arrow | |
or char == 6 then -- Control-F | |
self:moveRight() | |
elseif key == '\027[D' -- Left Arrow | |
or char == 2 then -- Control-B | |
self:moveLeft() | |
elseif key == '\027[H' -- Home Key | |
or char == 1 then -- Control-A | |
self:moveHome() | |
elseif key == '\027[F' -- End Key | |
or char == 5 then -- Control-E | |
self:moveEnd() | |
elseif char == 21 then -- Control-U | |
self:deleteLine() | |
elseif char == 11 then -- Control-K | |
self:deleteEnd() | |
elseif char == 12 then -- Control-L | |
self:clearScreen() | |
elseif char == 23 then -- Control-W | |
self:deleteWord() | |
elseif key == '\027[3~' then -- Delete Key | |
self:delete() | |
elseif key == '\027[1;5D' then -- Control Left Arrow | |
self:jumpLeft() | |
elseif key == '\027[1;5C' then -- Control Right Arrow | |
self:jumpRight() | |
elseif char > 31 then | |
self:insert(key) | |
else | |
p(char, key) | |
end | |
end | |
function Editor:getLine(callback) | |
local onKey, finish | |
function onKey(err, key) | |
local r, out = xpcall(function () | |
assert(not err, err) | |
return self:onKey(key) | |
end, traceback) | |
if r then | |
if out == nil then return end | |
return finish(nil, out or nil) | |
else | |
return finish(out) | |
end | |
end | |
function finish(...) | |
stdin:set_mode(0) | |
stdout:write('\n') | |
stdin:read_stop() | |
return callback(...) | |
end | |
self.line = "" | |
self.position = 1 | |
stdout:write(self.prompt) | |
self.history:add(self.line) | |
self.historyIndex = #self.history, | |
stdin:set_mode(1) | |
stdin:read_start(onKey) | |
end | |
Editor.__index = Editor | |
function Editor.new(options) | |
options = options or {} | |
local prompt = options.prompt or "> " | |
local history = options.history or History.new() | |
local editor = { | |
wordPattern = options.wordPattern or "%w+", | |
columns = (stdin:get_winsize()), | |
history = history, | |
prompt = prompt, | |
promptLength = #prompt, | |
completionCallback = options.completionCallback, | |
} | |
return setmetatable(editor, Editor) | |
end | |
local history = History.new() | |
history:load("uv = require('uv')\nuv\nuv.cpu_info()") | |
local editor = Editor.new({history=history}) | |
local function onLine(err, line) | |
assert(not err, err) | |
p(line) | |
if line then | |
editor.history:add(line) | |
editor:getLine(onLine) | |
end | |
end | |
editor:getLine(onLine) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment