<NME> readline.lua
<BEF> --[[

Copyright 2014-2015 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.

--]]
--[[lit-meta
  name = "luvit/readline"
  version = "2.1.0"
  dependencies = {
    "luvit/ustring@2.0.0",
  }
  homepage = "https://github.com/luvit/luvit/blob/master/deps/readline.lua"
  description = "A readline interface for terminals in pure lua."
  tags = {"readline", "tty"}
  license = "Apache 2"
  author = { name = "Tim Caswell" }
]]

-- Heavily inspired by ljlinenoise : <http://fperrad.github.io/ljlinenoise/>

local ustring = require "ustring"
local emptyline = ustring.new()
local sub = ustring.sub
local chlen = ustring.chlen
local gmatch = ustring.gmatch
local find = ustring.find
local remove = table.remove
local insert = table.insert
local concat = table.concat
local tostring = tostring

local 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
  if self.cover then
    line = string.rep(self.cover, #line)
  end

  local position = self.position
  if self.cover then
    position = (#self.cover * (position - 1)) + 1
  end

  -- Cursor to left edge
  local command = "\x1b[0G"
  -- Write the prompt and the data before cursor in buffer content.
               .. self.prompt .. tostring(line:sub(1,self.position - 1))
  -- Save the position,erase to right and write data left.
               .. "\x1b7\x1b[0K" .. tostring(line:sub(self.position,-1))
  -- Move cursor to original position.
               .. "\x1b8"
  self.stdout:write(command)
end
function Editor:insertAbove(line)
  -- Cursor to left edge
  local command = "\x1b[0G"
  -- Erase to right
               .. "\x1b[0K"

  self.stdout:write(command .. line .. "\n", function()
    self:refreshLine()
  end)
end
function Editor:insert(character)
  local display = self.cover and string.rep(self.cover, #character) or character
  local line = self.line
  local position = self.position
  character = ustring.new(character)
  if #line == position - 1 then
    self.line = line .. character
    self.position = position + #character
    self.stdout:write(display)
  else
    -- Insert the letter in the middle of the line
    self.line = sub(line, 1, position - 1) .. ustring.new(character) .. sub(line, position)
    self.position = position + #character
    self:refreshLine()
  end
  self.history:updateLastLine(tostring(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
      index = 1
    elseif index > length then
      index = length
    end
    if index == self.historyIndex then return end
    local line = ustring.new(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(tostring(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(tostring(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(tostring(self.line))
    self:refreshLine()
  end
end
function Editor:deleteLine()
  self.line = emptyline
  self.position = 1
  self.history:updateLastLine(tostring(self.line))
  self:refreshLine()
end
function Editor:deleteEnd()
  self.line = sub(self.line, 1, self.position - 1)
  self.history:updateLastLine(tostring(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 = 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 = find(self.line, self.wordPattern, self.position)
  self.position = e and e + 1 or #self.line + 1
  self:refreshLine()
end
function Editor:clearScreen()
  self.stdout:write('\x1b[H\x1b[2J')
  self:refreshLine()
end
function Editor:beep()
  self.stdout:write('\x07')
end
function Editor:complete()
  if not self.completionCallback then
    return self:beep()
  end
  local line = self.line
  local position = self.position
  local res = self.completionCallback(tostring(sub(line, 1, position)))
  if not res then
    return self:beep()
  end
  local typ = type(res)
  if typ == "string" then
    res = ustring.new(res)
    self.line = res .. sub(line, position + 1)
    self.position = #res + 1
    self.history:updateLastLine(tostring(self.line))
  elseif typ == "table" then
    print()
    print(unpack(res))
  end
  self:refreshLine()
end

local function escapeKeysForDisplay(keys)
  return string.gsub(keys, '[%c\\\128-\255]', function(c)
    local b = string.byte(c, 1)
    if b < 10 then return '\\00' .. b end
    if b <= 31 then return '\\0' .. b end
    if b == 92 then return '\\\\' end
    if b >= 128 and b <= 255 then return '\\' .. b end
  end)
end

-- an array of tables so that the iteration order is consistent
-- each entry is an array with two entries: a table and a function
-- the table can contain any number of the following:
--   numbers (to be compared to the char value),
--   strings (to be compared to the input string that has been truncated to the same length),
--   functions (to be called with the (key, char) values and returns either the consumed keys or nil)
-- the function recieves the Editor instance as the first parameter and the consumedKeys as the second
--   its returns will be propagated to Editor:onKey if either of them are non-nil
--   note: the function is only called if the key handler is the one doing the consuming
local keyHandlers =
{
  -- Enter
  {{13}, function(self)
    local history = self.history
    local line = self.line
    -- Only record new history if it's non-empty and new
    if #line > 0 and history[#history - 1] ~= line then
      history[#history] = tostring(line)
    else
      history[#history] = nil
    end
    return tostring(self.line)
  end},
  -- Tab
  {{9}, function(self)
    self:complete()
  end},
  -- Control-C
  {{3}, function(self)
    self.stdout:write("^C\n")
    if #self.line > 0 then
      self:deleteLine()
    else
      return false, "SIGINT in readLine"
    end
  end},
  -- Backspace, Control-H
  {{127, 8}, function(self)
    self:backspace()
  end},
  -- Control-D
  {{4}, function(self)
    if #self.line > 0 then
      self:delete()
    else
      self.history:updateLastLine()
      return nil, "EOF in readLine"
    end
  end},
  -- Control-T
  {{20}, function(self)
    self:swap()
  end},
  -- Up Arrow, Control-P
  {{'\027[A', 16}, function(self)
    self:getHistory(-1)
  end},
  -- Down Arrow, Control-N
  {{'\027[B', 14}, function(self)
    self:getHistory(1)
  end},
  -- Right Arrow, Control-F
  {{'\027[C', 6}, function(self)
    self:moveRight()
  end},
  -- Left Arrow, Control-B
  {{'\027[D', 2}, function(self)
    self:moveLeft()
  end},
  -- Home Key, Home for terminator, Home for CMD.EXE, Control-A
  {{'\027[H', '\027OH', '\027[1~', 1}, function(self)
    self:moveHome()
  end},
  -- End Key, End for terminator, End for CMD.EXE, Control-E
  {{'\027[F', '\027OF', '\027[4~', 5}, function(self)
    self:moveEnd()
  end},
  -- Control-U
  {{21}, function(self)
    self:deleteLine()
  end},
  -- Control-K
  {{11}, function(self)
    self:deleteEnd()
  end},
  -- Control-L
  {{12}, function(self)
    self:clearScreen()
  end},
  -- Control-W
  {{23}, function(self)
    self:deleteWord()
  end},
  -- Delete Key
  {{'\027[3~'}, function(self)
    self:delete()
  end},
  -- Control Left Arrow, Alt Left Arrow (iTerm.app), Alt Left Arrow (Terminal.app)
  {{'\027[1;5D', '\027\027[D', '\027b'}, function(self)
    self:jumpLeft()
  end},
  -- Control Right Arrow, Alt Right Arrow (iTerm.app), Alt Right Arrow (Terminal.app)
  {{'\027[1;5C', '\027\027[C', '\027f'}, function(self)
    self:jumpRight()
  end},
  -- Alt Up Arrow (iTerm.app), Page Up
  {{'\027\027[A', '\027[5~'}, function(self)
    self:getHistory(-10)
  end},
  -- Alt Down Arrow (iTerm.app), Page Down
  {{'\027\027[B', '\027[6~'}, function(self)
    self:getHistory(10)
  end},
  -- Printable characters
  {{function(key, char) return char > 31 and key:sub(1,chlen(key:byte())) or nil end}, function(self, consumedKeys)
    self:insert(consumedKeys)
  end},
}

function Editor:onKey(key)
  local char = string.byte(key, 1)
  local consumedKeys = nil

  for _, keyHandler in ipairs(keyHandlers) do
    local handledKeys = keyHandler[1]
    local handlerFn = keyHandler[2]
    for _, handledKey in ipairs(handledKeys) do
      if type(handledKey) == "number" then
        consumedKeys = handledKey == char and key:sub(1,1) or nil
      elseif type(handledKey) == "string" then
        -- test against the first key using the same strlen as the handled key
        local testKey = (type(handledKey) == "string" and #key >= #handledKey) and key:sub(1,#handledKey) or nil
        consumedKeys = (testKey and testKey == handledKey) and testKey or nil
      elseif type(handledKey) == "function" then
        consumedKeys = handledKey(key, char)
      end
      if consumedKeys ~= nil then
        local ret, err = handlerFn(self, consumedKeys)
        if err ~= nil or ret ~= nil then
          return ret, err
        end
        break
      end
    end
    if consumedKeys ~= nil then break end
  end

  if consumedKeys ~= nil then
    assert(#consumedKeys > 0)
    if #consumedKeys < #key then
      local unconsumedKeys = key:sub(#consumedKeys+1)
      if #unconsumedKeys > 0 then
        self:onKey(unconsumedKeys)
      end
    end
  else
    self:insertAbove(string.format("Unhandled key(s): %s", escapeKeysForDisplay(key)))
  end
  return true
end
function Editor:readLine(prompt, callback)

  local onKey, finish

  self.prompt = prompt
  self.promptLength = #prompt
  self.columns = self.stdout.get_winsize and self.stdout:get_winsize() or 80

  function onKey(err, key)
    local r, out, reason = pcall(function ()
      assert(not err, err)
      return self:onKey(key)
    end)
    if r then
      if out == true then return end
      return finish(nil, out, reason)
    else
      return finish(out)
    end
  end

  function finish(...)
    self.stdin:read_stop()
    self.stdin:set_mode(0)
    self.stdout:write('\n')
    return callback(...)
  end

  self.line = emptyline
  self.position = 1
  self.stdout:write(self.prompt)
  self.history:add(tostring(self.line))
  self.historyIndex = #self.history

  self.stdin:set_mode(1)
  self.stdin:read_start(function (...)
    return coroutine.wrap(onKey)(...)
  end)

end
Editor.__index = Editor
function Editor.new(options)
  options = options or {}
  local history = options.history or History.new()

  if not (options.stdin and options.stdout) then
    local prettyPrint = require('pretty-print')
    options.stdin = options.stdin or prettyPrint.stdin
    options.stdout = options.stdout or prettyPrint.stdout
  end

  local editor = {
    wordPattern = options.wordPattern or "%w+",
    history = history,
    completionCallback = options.completionCallback,
    stdin = options.stdin,
    stdout = options.stdout,
    cover = options.cover,
  }
  return setmetatable(editor, Editor)
end

local function readLine(prompt, options, callback)
  if type(options) == "function" and callback == nil then
    callback, options = options, nil
  end
  local editor = Editor.new(options)
  editor:readLine(prompt, callback)
  return editor
end

return {
  History = History,
  Editor = Editor,
  readLine = readLine,
}

<MSG> Release readline 2.2.0

<DFF> @@ -17,7 +17,7 @@ limitations under the License.
 --]]
 --[[lit-meta
   name = "luvit/readline"
-  version = "2.1.0"
+  version = "2.2.0"
   dependencies = {
     "luvit/ustring@2.0.0",
   }

{'commit': 'diff --git a/deps/readline.lua b/deps/readline.lua\nindex 026f912..3061fe9 100644\n--- a/deps/readline.lua\n+++ b/deps/readline.lua\n@@ -17,7 +17,7 @@ limitations under the License.\n --]]\n --[[lit-meta\n   name = "luvit/readline"\n-  version = "2.1.0"\n+  version = "2.2.0"\n   dependencies = {\n     "luvit/ustring@2.0.0",\n   }\n', 'commit_subject': 'Release readline 2.2.0', 'repo_name': 'luvit/luvit', 'committer': {'name': 'Tim Caswell', 'email': '5ee0edb9e2229c0838f1959779f1949031de0123@creationix.com', 'time_sec': '1481912759', 'tz_offset': '-360', 'date': {'seconds': '1481912759'}}, 'commit_tree': '36d1fd5b31aba4089e35ddb8a387402a2d596eb6', 'commit_message': 'Release readline 2.2.0\n', 'language_name': 'Shell', 'license': 'apache-2.0', 'hunks': '@@ -17,7 +17,7 @@ limitations under the License.\n --]]\n --[[lit-meta\n   name = "luvit/readline"\n-  version = "2.1.0"\n+  version = "2.2.0"\n   dependencies = {\n     "luvit/ustring@2.0.0",\n   }\n', 'addition_count': 1, 'deletion_count': 1, 'src_file': 'a/deps/readline.lua', 'tgt_file': 'b/deps/readline.lua', 'file_extension': '.lua', 'before_file': '--[[\n\nCopyright 2014-2015 The Luvit Authors. All Rights Reserved.\n\nLicensed under the Apache License, Version 2.0 (the "License");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an "AS-IS" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\n--]]\n--[[lit-meta\n  name = "luvit/readline"\n  version = "2.1.0"\n  dependencies = {\n    "luvit/ustring@2.0.0",\n  }\n  homepage = "https://github.com/luvit/luvit/blob/master/deps/readline.lua"\n  description = "A readline interface for terminals in pure lua."\n  tags = {"readline", "tty"}\n  license = "Apache 2"\n  author = { name = "Tim Caswell" }\n]]\n\n-- Heavily inspired by ljlinenoise : <http://fperrad.github.io/ljlinenoise/>\n\nlocal ustring = require "ustring"\nlocal emptyline = ustring.new()\nlocal sub = ustring.sub\nlocal chlen = ustring.chlen\nlocal gmatch = ustring.gmatch\nlocal find = ustring.find\nlocal remove = table.remove\nlocal insert = table.insert\nlocal concat = table.concat\nlocal tostring = tostring\n\nlocal History = {}\nfunction History:add(line)\n  assert(type(line) == "string", "line must be string")\n  while #self >= self.maxLength do\n    remove(self, 1)\n  end\n  insert(self, line)\n  return true\nend\nfunction History:setMaxLength(length)\n  assert(type(length) == "number", "max length length must be number")\n  self.maxLength = length\n  while #self > length do\n    remove(self, 1)\n  end\n  return true\nend\nfunction History:clean()\n  for i = 1, #self do\n    self[i] = nil\n  end\n  return true\nend\nfunction History:dump()\n  return concat(self, "\\n") .. \'\\n\'\nend\nfunction History:load(data)\n  assert(type(data) == "string", "history dump required as string")\n  for line in gmatch(data, "[^\\n]+") do\n    insert(self, line)\n  end\n  return true\nend\nfunction History:updateLastLine(line)\n  self[#self] = line\nend\nHistory.__index = History\nfunction History.new()\n  local history = { maxLength = 100 }\n  return setmetatable(history, History)\nend\n\nlocal Editor = {}\nfunction Editor:refreshLine()\n  local line = self.line\n  if self.cover then\n    line = string.rep(self.cover, #line)\n  end\n\n  local position = self.position\n  if self.cover then\n    position = (#self.cover * (position - 1)) + 1\n  end\n\n  -- Cursor to left edge\n  local command = "\\x1b[0G"\n  -- Write the prompt and the data before cursor in buffer content.\n               .. self.prompt .. tostring(line:sub(1,self.position - 1))\n  -- Save the position,erase to right and write data left.\n               .. "\\x1b7\\x1b[0K" .. tostring(line:sub(self.position,-1))\n  -- Move cursor to original position.\n               .. "\\x1b8"\n  self.stdout:write(command)\nend\nfunction Editor:insertAbove(line)\n  -- Cursor to left edge\n  local command = "\\x1b[0G"\n  -- Erase to right\n               .. "\\x1b[0K"\n\n  self.stdout:write(command .. line .. "\\n", function()\n    self:refreshLine()\n  end)\nend\nfunction Editor:insert(character)\n  local display = self.cover and string.rep(self.cover, #character) or character\n  local line = self.line\n  local position = self.position\n  character = ustring.new(character)\n  if #line == position - 1 then\n    self.line = line .. character\n    self.position = position + #character\n    self.stdout:write(display)\n  else\n    -- Insert the letter in the middle of the line\n    self.line = sub(line, 1, position - 1) .. ustring.new(character) .. sub(line, position)\n    self.position = position + #character\n    self:refreshLine()\n  end\n  self.history:updateLastLine(tostring(self.line))\nend\nfunction Editor:moveLeft()\n  if self.position > 1 then\n    self.position = self.position - 1\n    self:refreshLine()\n  end\nend\nfunction Editor:moveRight()\n  if self.position - 1 ~= #self.line then\n    self.position = self.position + 1\n    self:refreshLine()\n  end\nend\nfunction Editor:getHistory(delta)\n  local history = self.history\n  local length = #history\n  local index = self.historyIndex\n  if length > 1 then\n    index = index + delta\n    if index < 1 then\n      index = 1\n    elseif index > length then\n      index = length\n    end\n    if index == self.historyIndex then return end\n    local line = ustring.new(self.history[index])\n    self.line = line\n    self.historyIndex = index\n    self.position = #line + 1\n    self:refreshLine()\n  end\nend\nfunction Editor:backspace()\n  local line = self.line\n  local position = self.position\n  if position > 1 and #line > 0 then\n    self.line = sub(line, 1, position - 2) .. sub(line, position)\n    self.position = position - 1\n    self.history:updateLastLine(tostring(self.line))\n    self:refreshLine()\n  end\nend\nfunction Editor:delete()\n  local line = self.line\n  local position = self.position\n  if position > 0 and #line > 0 then\n    self.line = sub(line, 1, position - 1) .. sub(line, position + 1)\n    self.history:updateLastLine(tostring(self.line))\n    self:refreshLine()\n  end\nend\nfunction Editor:swap()\n  local line = self.line\n  local position = self.position\n  if position > 1 and position <= #line then\n    self.line = sub(line, 1, position - 2)\n             .. sub(line, position, position)\n             .. sub(line, position - 1, position - 1)\n             .. sub(line, position + 1)\n    if position ~= #line then\n      self.position = position + 1\n    end\n    self.history:updateLastLine(tostring(self.line))\n    self:refreshLine()\n  end\nend\nfunction Editor:deleteLine()\n  self.line = emptyline\n  self.position = 1\n  self.history:updateLastLine(tostring(self.line))\n  self:refreshLine()\nend\nfunction Editor:deleteEnd()\n  self.line = sub(self.line, 1, self.position - 1)\n  self.history:updateLastLine(tostring(self.line))\n  self:refreshLine()\nend\nfunction Editor:moveHome()\n  self.position = 1\n  self:refreshLine()\nend\n\nfunction Editor:moveEnd()\n  self.position = #self.line + 1\n  self:refreshLine()\nend\nlocal function findLeft(line, position, wordPattern)\n  local pattern = wordPattern .. "$"\n  if position == 1 then return 1 end\n  local s\n  repeat\n    local start = sub(line, 1, position - 1)\n    s = find(start, pattern)\n    if not s then\n      position = position - 1\n    end\n  until s or position == 1\n  return s or position\nend\n\nfunction Editor:deleteWord()\n  local position = self.position\n  local line = self.line\n  self.position = findLeft(line, position, self.wordPattern)\n  self.line = sub(line, 1, self.position - 1) .. sub(line, position)\n  self:refreshLine()\nend\n\nfunction Editor:jumpLeft()\n  self.position = findLeft(self.line, self.position, self.wordPattern)\n  self:refreshLine()\nend\nfunction Editor:jumpRight()\n  local _, e = find(self.line, self.wordPattern, self.position)\n  self.position = e and e + 1 or #self.line + 1\n  self:refreshLine()\nend\nfunction Editor:clearScreen()\n  self.stdout:write(\'\\x1b[H\\x1b[2J\')\n  self:refreshLine()\nend\nfunction Editor:beep()\n  self.stdout:write(\'\\x07\')\nend\nfunction Editor:complete()\n  if not self.completionCallback then\n    return self:beep()\n  end\n  local line = self.line\n  local position = self.position\n  local res = self.completionCallback(tostring(sub(line, 1, position)))\n  if not res then\n    return self:beep()\n  end\n  local typ = type(res)\n  if typ == "string" then\n    res = ustring.new(res)\n    self.line = res .. sub(line, position + 1)\n    self.position = #res + 1\n    self.history:updateLastLine(tostring(self.line))\n  elseif typ == "table" then\n    print()\n    print(unpack(res))\n  end\n  self:refreshLine()\nend\n\nlocal function escapeKeysForDisplay(keys)\n  return string.gsub(keys, \'[%c\\\\\\128-\\255]\', function(c)\n    local b = string.byte(c, 1)\n    if b < 10 then return \'\\\\00\' .. b end\n    if b <= 31 then return \'\\\\0\' .. b end\n    if b == 92 then return \'\\\\\\\\\' end\n    if b >= 128 and b <= 255 then return \'\\\\\' .. b end\n  end)\nend\n\n-- an array of tables so that the iteration order is consistent\n-- each entry is an array with two entries: a table and a function\n-- the table can contain any number of the following:\n--   numbers (to be compared to the char value),\n--   strings (to be compared to the input string that has been truncated to the same length),\n--   functions (to be called with the (key, char) values and returns either the consumed keys or nil)\n-- the function recieves the Editor instance as the first parameter and the consumedKeys as the second\n--   its returns will be propagated to Editor:onKey if either of them are non-nil\n--   note: the function is only called if the key handler is the one doing the consuming\nlocal keyHandlers =\n{\n  -- Enter\n  {{13}, function(self)\n    local history = self.history\n    local line = self.line\n    -- Only record new history if it\'s non-empty and new\n    if #line > 0 and history[#history - 1] ~= line then\n      history[#history] = tostring(line)\n    else\n      history[#history] = nil\n    end\n    return tostring(self.line)\n  end},\n  -- Tab\n  {{9}, function(self)\n    self:complete()\n  end},\n  -- Control-C\n  {{3}, function(self)\n    self.stdout:write("^C\\n")\n    if #self.line > 0 then\n      self:deleteLine()\n    else\n      return false, "SIGINT in readLine"\n    end\n  end},\n  -- Backspace, Control-H\n  {{127, 8}, function(self)\n    self:backspace()\n  end},\n  -- Control-D\n  {{4}, function(self)\n    if #self.line > 0 then\n      self:delete()\n    else\n      self.history:updateLastLine()\n      return nil, "EOF in readLine"\n    end\n  end},\n  -- Control-T\n  {{20}, function(self)\n    self:swap()\n  end},\n  -- Up Arrow, Control-P\n  {{\'\\027[A\', 16}, function(self)\n    self:getHistory(-1)\n  end},\n  -- Down Arrow, Control-N\n  {{\'\\027[B\', 14}, function(self)\n    self:getHistory(1)\n  end},\n  -- Right Arrow, Control-F\n  {{\'\\027[C\', 6}, function(self)\n    self:moveRight()\n  end},\n  -- Left Arrow, Control-B\n  {{\'\\027[D\', 2}, function(self)\n    self:moveLeft()\n  end},\n  -- Home Key, Home for terminator, Home for CMD.EXE, Control-A\n  {{\'\\027[H\', \'\\027OH\', \'\\027[1~\', 1}, function(self)\n    self:moveHome()\n  end},\n  -- End Key, End for terminator, End for CMD.EXE, Control-E\n  {{\'\\027[F\', \'\\027OF\', \'\\027[4~\', 5}, function(self)\n    self:moveEnd()\n  end},\n  -- Control-U\n  {{21}, function(self)\n    self:deleteLine()\n  end},\n  -- Control-K\n  {{11}, function(self)\n    self:deleteEnd()\n  end},\n  -- Control-L\n  {{12}, function(self)\n    self:clearScreen()\n  end},\n  -- Control-W\n  {{23}, function(self)\n    self:deleteWord()\n  end},\n  -- Delete Key\n  {{\'\\027[3~\'}, function(self)\n    self:delete()\n  end},\n  -- Control Left Arrow, Alt Left Arrow (iTerm.app), Alt Left Arrow (Terminal.app)\n  {{\'\\027[1;5D\', \'\\027\\027[D\', \'\\027b\'}, function(self)\n    self:jumpLeft()\n  end},\n  -- Control Right Arrow, Alt Right Arrow (iTerm.app), Alt Right Arrow (Terminal.app)\n  {{\'\\027[1;5C\', \'\\027\\027[C\', \'\\027f\'}, function(self)\n    self:jumpRight()\n  end},\n  -- Alt Up Arrow (iTerm.app), Page Up\n  {{\'\\027\\027[A\', \'\\027[5~\'}, function(self)\n    self:getHistory(-10)\n  end},\n  -- Alt Down Arrow (iTerm.app), Page Down\n  {{\'\\027\\027[B\', \'\\027[6~\'}, function(self)\n    self:getHistory(10)\n  end},\n  -- Printable characters\n  {{function(key, char) return char > 31 and key:sub(1,chlen(key:byte())) or nil end}, function(self, consumedKeys)\n    self:insert(consumedKeys)\n  end},\n}\n\nfunction Editor:onKey(key)\n  local char = string.byte(key, 1)\n  local consumedKeys = nil\n\n  for _, keyHandler in ipairs(keyHandlers) do\n    local handledKeys = keyHandler[1]\n    local handlerFn = keyHandler[2]\n    for _, handledKey in ipairs(handledKeys) do\n      if type(handledKey) == "number" then\n        consumedKeys = handledKey == char and key:sub(1,1) or nil\n      elseif type(handledKey) == "string" then\n        -- test against the first key using the same strlen as the handled key\n        local testKey = (type(handledKey) == "string" and #key >= #handledKey) and key:sub(1,#handledKey) or nil\n        consumedKeys = (testKey and testKey == handledKey) and testKey or nil\n      elseif type(handledKey) == "function" then\n        consumedKeys = handledKey(key, char)\n      end\n      if consumedKeys ~= nil then\n        local ret, err = handlerFn(self, consumedKeys)\n        if err ~= nil or ret ~= nil then\n          return ret, err\n        end\n        break\n      end\n    end\n    if consumedKeys ~= nil then break end\n  end\n\n  if consumedKeys ~= nil then\n    assert(#consumedKeys > 0)\n    if #consumedKeys < #key then\n      local unconsumedKeys = key:sub(#consumedKeys+1)\n      if #unconsumedKeys > 0 then\n        self:onKey(unconsumedKeys)\n      end\n    end\n  else\n    self:insertAbove(string.format("Unhandled key(s): %s", escapeKeysForDisplay(key)))\n  end\n  return true\nend\nfunction Editor:readLine(prompt, callback)\n\n  local onKey, finish\n\n  self.prompt = prompt\n  self.promptLength = #prompt\n  self.columns = self.stdout.get_winsize and self.stdout:get_winsize() or 80\n\n  function onKey(err, key)\n    local r, out, reason = pcall(function ()\n      assert(not err, err)\n      return self:onKey(key)\n    end)\n    if r then\n      if out == true then return end\n      return finish(nil, out, reason)\n    else\n      return finish(out)\n    end\n  end\n\n  function finish(...)\n    self.stdin:read_stop()\n    self.stdin:set_mode(0)\n    self.stdout:write(\'\\n\')\n    return callback(...)\n  end\n\n  self.line = emptyline\n  self.position = 1\n  self.stdout:write(self.prompt)\n  self.history:add(tostring(self.line))\n  self.historyIndex = #self.history\n\n  self.stdin:set_mode(1)\n  self.stdin:read_start(function (...)\n    return coroutine.wrap(onKey)(...)\n  end)\n\nend\nEditor.__index = Editor\nfunction Editor.new(options)\n  options = options or {}\n  local history = options.history or History.new()\n\n  if not (options.stdin and options.stdout) then\n    local prettyPrint = require(\'pretty-print\')\n    options.stdin = options.stdin or prettyPrint.stdin\n    options.stdout = options.stdout or prettyPrint.stdout\n  end\n\n  local editor = {\n    wordPattern = options.wordPattern or "%w+",\n    history = history,\n    completionCallback = options.completionCallback,\n    stdin = options.stdin,\n    stdout = options.stdout,\n    cover = options.cover,\n  }\n  return setmetatable(editor, Editor)\nend\n\nlocal function readLine(prompt, options, callback)\n  if type(options) == "function" and callback == nil then\n    callback, options = options, nil\n  end\n  local editor = Editor.new(options)\n  editor:readLine(prompt, callback)\n  return editor\nend\n\nreturn {\n  History = History,\n  Editor = Editor,\n  readLine = readLine,\n}\n', 'message': 'Release readline 2.2.0\n', 'author_name': 'Tim Caswell', 'lang': 'lua'}