aboutsummaryrefslogtreecommitdiff
path: root/.mbs/lib/readline.lua
diff options
context:
space:
mode:
Diffstat (limited to '.mbs/lib/readline.lua')
-rw-r--r--.mbs/lib/readline.lua506
1 files changed, 506 insertions, 0 deletions
diff --git a/.mbs/lib/readline.lua b/.mbs/lib/readline.lua
new file mode 100644
index 0000000..ba322a6
--- /dev/null
+++ b/.mbs/lib/readline.lua
@@ -0,0 +1,506 @@
+
+--- A mapping of colours, to aid reading settings.
+local colour_table = { }
+
+for k, v in pairs(colours) do
+ if type(v) == "number" then colour_table[k] = v end
+end
+
+for k, v in pairs(colors) do
+ if type(v) == "number" then colour_table[k] = v end
+end
+
+--- Keys which are used in combinations involving the "meta" key.
+local meta_keys = "ulcdbf"
+
+--- Clamp a [value] within a range
+local function clamp(value, min, max)
+ if value < min then return min end
+ if value > max then return max end
+ return value
+end
+
+--- Verify a [tbl] has a [key] of nil or the given [type]
+local type = type
+local function check_key(tbl, key, ty)
+ local actual_type = type(tbl[key])
+ if actual_type ~= "nil" and actual_type ~= ty then
+ error(("bad key %s (expected %s, got %s)"):format(key, ty, actual_type), 3)
+ end
+end
+
+function read(opts)
+ if opts == nil then
+ opts = {}
+ elseif type(opts) ~= "table" then
+ error("bad argument #1 (expected table, got " .. type(opts) .. ")", 2)
+ end
+
+ check_key(opts, "replace_char", "string") -- Character to show instead
+ check_key(opts, "history", "table") -- List of previous items
+ check_key(opts, "complete", "function") -- Completion function
+ check_key(opts, "default", "string") -- Initial string
+ check_key(opts, "complete_fg", "number") -- Foreground for completion
+ check_key(opts, "complete_bg", "number") -- Background for completion
+
+ -- Highlight function: (line: string, start: number) -> (end: number, colour:colour)
+ check_key(opts, "highlight", "function")
+
+ local w = term.getSize()
+ local sx = term.getCursorPos()
+
+ local sLine = opts.default or ""
+ local nPos, nScroll = #sLine, 0
+ local tKillRing, nKillRing = {}, 0
+
+ local nHistoryPos
+ local tDown = {}
+ local nMod = 0
+ local replace_char = opts.replace_char and opts.replace_char:sub(1, 1)
+ local complete_fg = opts.complete_fg or colour_table[settings.get("mbs.readline.complete_fg")] or -1
+ local complete_bg = opts.complete_bg or colour_table[settings.get("mbs.readline.complete_bg")] or -1
+
+ local tCompletions
+ local nCompletion
+ local function recomplete()
+ if opts.complete and nPos == #sLine then
+ tCompletions = opts.complete(sLine)
+ if tCompletions and #tCompletions > 0 then
+ nCompletion = 1
+ else
+ nCompletion = nil
+ end
+ else
+ tCompletions = nil
+ nCompletion = nil
+ end
+ end
+
+ local function uncomplete()
+ tCompletions = nil
+ nCompletion = nil
+ end
+
+ local function updateModifier()
+ nMod = 0
+ if tDown[keys.leftCtrl] or tDown[keys.rightCtrl] then nMod = nMod + 1 end
+ if tDown[keys.leftAlt] or tDown[keys.rightAlt] then nMod = nMod + 2 end
+ end
+
+ local function nextWord()
+ -- Attempt to find the position of the next word
+ local nOffset = sLine:find("%w%W", nPos + 1)
+ if nOffset then return nOffset else return #sLine end
+ end
+
+ local function prevWord()
+ -- Attempt to find the position of the previous word
+ local nOffset = 1
+ while nOffset <= #sLine do
+ local nNext = sLine:find("%W%w", nOffset)
+ if nNext and nNext < nPos then
+ nOffset = nNext + 1
+ else
+ break
+ end
+ end
+ return nOffset - 1
+ end
+
+ local function redraw(_bClear)
+ local cursor_pos = nPos - nScroll
+ if sx + cursor_pos >= w then
+ -- We've moved beyond the RHS, ensure we're on the edge.
+ nScroll = sx + nPos - w
+ elseif cursor_pos < 0 then
+ -- We've moved beyond the LHS, ensure we're on the edge.
+ nScroll = nPos
+ end
+
+ local _, cy = term.getCursorPos()
+ term.setCursorPos(sx, cy)
+ local sReplace = (_bClear and " ") or replace_char
+
+ if opts.highlight and not _bClear then
+ -- We've a highlighting function: step through each line of input
+ local old_col = term.getTextColor()
+ local hl_pos, hl_max, hl_col = 1, #sLine, old_col
+ while hl_pos <= hl_max do
+ local next_pos, next_col = opts.highlight(sLine, hl_pos)
+ if next_pos < hl_pos then error("Highlighting function consumed no input") end
+
+ if next_pos >= nScroll + 1 then
+ if next_col ~= hl_col then term.setTextColor(next_col) hl_col = next_col end
+ if sReplace then
+ term.write(string.rep(sReplace, next_pos - math.max(nScroll + 1, hl_pos) + 1))
+ else
+ term.write(string.sub(sLine, math.max(nScroll + 1, hl_pos), next_pos))
+ end
+ end
+
+ hl_pos = next_pos + 1
+ end
+ term.setTextColor(old_col)
+ else
+ -- If we've no highlighting function, we can go the "fast" path.
+ if sReplace then
+ term.write(string.rep(sReplace, math.max(#sLine - nScroll, 0)))
+ else
+ term.write(string.sub(sLine, nScroll + 1))
+ end
+ end
+
+ if nCompletion then
+ local sCompletion = tCompletions[ nCompletion ]
+ local oldText, oldBg
+ if not _bClear then
+ oldText = term.getTextColor()
+ oldBg = term.getBackgroundColor()
+ if complete_fg > -1 then term.setTextColor(complete_fg) end
+ if complete_bg > -1 then term.setBackgroundColor(complete_bg) end
+ end
+ if sReplace then
+ term.write(string.rep(sReplace, #sCompletion))
+ else
+ term.write(sCompletion)
+ end
+ if not _bClear then
+ term.setTextColor(oldText)
+ term.setBackgroundColor(oldBg)
+ end
+ end
+
+ term.setCursorPos(sx + nPos - nScroll, cy)
+ end
+
+ local function nsub(start, fin)
+ if start < 1 or fin < start then return "" end
+ return sLine:sub(start, fin)
+ end
+
+ local function clear()
+ redraw(true)
+ end
+
+ local function kill(text)
+ if #text == "" then return end
+ nKillRing = nKillRing + 1
+ tKillRing[nKillRing] = text
+ end
+
+ local function acceptCompletion()
+ if nCompletion then
+ -- Clear
+ clear()
+
+ -- Find the common prefix of all the other suggestions which start with the same letter as the current one
+ local sCompletion = tCompletions[ nCompletion ]
+ sLine = sLine .. sCompletion
+ nPos = #sLine
+
+ -- Redraw
+ recomplete()
+ redraw()
+ end
+ end
+
+ term.setCursorBlink(true)
+ recomplete()
+ redraw()
+ while true do
+ local sEvent, param, param1, param2 = os.pullEvent()
+ if sEvent == "char" and (nMod == 0 or nMod == 3 or (nMod == 2 and not meta_keys:find(param, 1, true))) then
+ -- Typed key
+ -- Alt+X will queue a char event, so we limit ourselves to cases where
+ -- no modifier is used, or Ctrl+Alt are (equivalent to AltGr), or the Alt
+ -- key is used and we have no known combination.
+ clear()
+ sLine = string.sub(sLine, 1, nPos) .. param .. string.sub(sLine, nPos + 1)
+ nPos = nPos + 1
+ recomplete()
+ redraw()
+ elseif sEvent == "paste" then
+ -- Pasted text
+ clear()
+ sLine = string.sub(sLine, 1, nPos) .. param .. string.sub(sLine, nPos + 1)
+ nPos = nPos + #param
+ recomplete()
+ redraw()
+ elseif sEvent == "key" then
+ -- All keybindigns within the read loop.
+ -- IMPORTANT: Please update the meta_keys variable up top. Ideally we'd
+ -- make each function operate on a state, and run outside the loop, but
+ -- this will do for now.
+ if param == keys.leftCtrl or param == keys.rightCtrl or param == keys.leftAlt or param == keys.rightAlt then
+ tDown[param] = true
+ updateModifier()
+ elseif param == keys.enter then
+ -- Enter
+ if nCompletion then
+ clear()
+ uncomplete()
+ redraw()
+ end
+ break
+
+ -- Moving through text/completions
+ elseif nMod == 1 and param == keys.d then
+ -- End of stream, abort
+ if nCompletion then
+ clear()
+ uncomplete()
+ redraw()
+ end
+ sLine = nil
+ nPos = 0
+ break
+ elseif (nMod == 0 and param == keys.left) or (nMod == 1 and param == keys.b) then
+ -- Left
+ if nPos > 0 then
+ clear()
+ nPos = nPos - 1
+ recomplete()
+ redraw()
+ end
+ elseif (nMod == 0 and param == keys.right) or (nMod == 1 and param == keys.f) then
+ -- Right
+ if nPos < #sLine then
+ -- Move right
+ clear()
+ nPos = nPos + 1
+ recomplete()
+ redraw()
+ else
+ -- Accept autocomplete
+ acceptCompletion()
+ end
+ elseif nMod == 2 and param == keys.b then
+ -- Word left
+ local nNewPos = prevWord()
+ if nNewPos ~= nPos then
+ clear()
+ nPos = nNewPos
+ recomplete()
+ redraw()
+ end
+ elseif nMod == 2 and param == keys.f then
+ -- Word right
+ local nNewPos = nextWord()
+ if nNewPos ~= nPos then
+ clear()
+ nPos = nNewPos
+ recomplete()
+ redraw()
+ end
+ elseif (nMod == 0 and (param == keys.up or param == keys.down))
+ or (nMod == 1 and (param == keys.p or param == keys.n)) then
+ -- Up or down
+ if nCompletion then
+ -- Cycle completions
+ clear()
+ if param == keys.up or param == keys.p then
+ nCompletion = nCompletion - 1
+ if nCompletion < 1 then
+ nCompletion = #tCompletions
+ end
+ elseif param == keys.down or param == keys.n then
+ nCompletion = nCompletion + 1
+ if nCompletion > #tCompletions then
+ nCompletion = 1
+ end
+ end
+ redraw()
+ elseif opts.history then
+ -- Cycle history
+ clear()
+ if param == keys.up or param == keys.p then
+ -- Up
+ if nHistoryPos == nil then
+ if #opts.history > 0 then
+ nHistoryPos = #opts.history
+ end
+ elseif nHistoryPos > 1 then
+ nHistoryPos = nHistoryPos - 1
+ end
+ elseif param == keys.down or param == keys.n then
+ -- Down
+ if nHistoryPos == #opts.history then
+ nHistoryPos = nil
+ elseif nHistoryPos ~= nil then
+ nHistoryPos = nHistoryPos + 1
+ end
+ end
+ if nHistoryPos then
+ sLine = opts.history[nHistoryPos]
+ nPos, nScroll = #sLine, 0
+ else
+ sLine = ""
+ nPos, nScroll = 0, 0
+ end
+ uncomplete()
+ redraw()
+ end
+ elseif (nMod == 0 and param == keys.home)
+ or (nMod == 1 and param == keys.a) then
+ -- Home
+ if nPos > 0 then
+ clear()
+ nPos = 0
+ recomplete()
+ redraw()
+ end
+ elseif (nMod == 0 and param == keys["end"])
+ or (nMod == 1 and param == keys.e) then
+ -- End
+ if nPos < #sLine then
+ clear()
+ nPos = #sLine
+ recomplete()
+ redraw()
+ end
+ -- Changing text
+ elseif nMod == 1 and param == keys.t then
+ -- Transpose char
+ local prev, cur
+ if nPos == #sLine then prev, cur = nPos - 1, nPos
+ elseif nPos == 0 then prev, cur = 1, 2
+ else prev, cur = nPos, nPos + 1
+ end
+
+ sLine = nsub(1, prev - 1) .. nsub(cur, cur) .. nsub(prev, prev) .. nsub(cur + 1, #sLine)
+ nPos = math.min(#sLine, cur)
+
+ -- We need the clear to remove the completion
+ clear(); recomplete(); redraw()
+ elseif nMod == 2 and param == keys.u then
+ -- Upcase word
+ if nPos < #sLine then
+ local nNext = nextWord()
+ sLine = nsub(1, nPos) .. nsub(nPos + 1, nNext):upper() .. nsub(nNext + 1, #sLine)
+ nPos = nNext
+ clear(); recomplete(); redraw()
+ end
+ elseif nMod == 2 and param == keys.l then
+ -- Lowercase word
+ if nPos < #sLine then
+ local nNext = nextWord()
+ sLine = nsub(1, nPos) .. nsub(nPos + 1, nNext):lower() .. nsub(nNext + 1, #sLine)
+ nPos = nNext
+ clear(); recomplete(); redraw()
+ end
+ elseif nMod == 2 and param == keys.c then
+ -- Capitalize word
+ if nPos < #sLine then
+ local nNext = nextWord()
+ sLine = nsub(1, nPos) .. nsub(nPos + 1, nPos + 1):upper()
+ .. nsub(nPos + 2, nNext):lower() .. nsub(nNext + 1, #sLine)
+ nPos = nNext
+ clear(); recomplete(); redraw()
+ end
+
+ -- Killing text
+ elseif nMod == 0 and param == keys.backspace then
+ -- Backspace
+ if nPos > 0 then
+ clear()
+ sLine = string.sub(sLine, 1, nPos - 1) .. string.sub(sLine, nPos + 1)
+ nPos = nPos - 1
+ if nScroll > 0 then nScroll = nScroll - 1 end
+ recomplete()
+ redraw()
+ end
+ elseif nMod == 0 and param == keys.delete then
+ -- Delete
+ if nPos < #sLine then
+ clear()
+ sLine = string.sub(sLine, 1, nPos) .. string.sub(sLine, nPos + 2)
+ recomplete()
+ redraw()
+ end
+ elseif nMod == 1 and param == keys.u then
+ -- Delete from cursor to beginning of line
+ if nPos > 0 then
+ clear()
+ kill(sLine:sub(1, nPos))
+ sLine = sLine:sub(nPos + 1)
+ nPos = 0
+ recomplete(); redraw()
+ end
+ elseif nMod == 1 and param == keys.k then
+ -- Delete from cursor to end of line
+ if nPos < #sLine then
+ clear()
+ kill(sLine:sub(nPos + 1))
+ sLine = sLine:sub(1, nPos)
+ nPos = #sLine
+ recomplete(); redraw()
+ end
+ elseif nMod == 2 and param == keys.d then
+ -- Delete from cursor to end of next word
+ if nPos < #sLine then
+ local nNext = nextWord()
+ if nNext ~= nPos then
+ clear()
+ kill(sLine:sub(nPos + 1, nNext))
+ sLine = sLine:sub(1, nPos) .. sLine:sub(nNext + 1)
+ recomplete(); redraw()
+ end
+ end
+ elseif nMod == 1 and param == keys.w then
+ -- Delete from cursor to beginning of previous word
+ if nPos > 0 then
+ local nPrev = prevWord(nPos)
+ if nPrev ~= nPos then
+ clear()
+ kill(sLine:sub(nPrev + 1, nPos))
+ sLine = sLine:sub(1, nPrev) .. sLine:sub(nPos + 1)
+ nPos = nPrev
+ recomplete(); redraw()
+ end
+ end
+ elseif nMod == 1 and param == keys.y then
+ local insert = tKillRing[nKillRing]
+ if insert then
+ clear()
+ sLine = sLine:sub(1, nPos) .. insert .. sLine:sub(nPos + 1)
+ nPos = nPos + #insert
+ recomplete(); redraw()
+ end
+ -- Misc
+ elseif nMod == 0 and param == keys.tab then
+ -- Tab (accept autocomplete)
+ acceptCompletion()
+ end
+ elseif sEvent == "key_up" then
+ -- Update the status of the modifier flag
+ if param == keys.leftCtrl or param == keys.rightCtrl
+ or param == keys.leftAlt or param == keys.rightAlt then
+ tDown[param] = false
+ updateModifier()
+ end
+ elseif sEvent == "mouse_click" or sEvent == "mouse_drag" and param == 1 then
+ local _, cy = term.getCursorPos()
+ if param2 == cy then
+ -- We first clamp the x position with in the start and end points
+ -- to ensure we don't scroll beyond the visible region.
+ local x = clamp(param1, sx, w)
+
+ -- Then ensure we don't scroll beyond the current line
+ nPos = clamp(nScroll + x - sx, 0, #sLine)
+
+ redraw()
+ end
+ elseif sEvent == "term_resize" then
+ -- Terminal resized
+ w = term.getSize()
+ redraw()
+ end
+ end
+
+ local _, cy = term.getCursorPos()
+ term.setCursorBlink(false)
+ term.setCursorPos(w + 1, cy)
+ print()
+
+ return sLine
+end \ No newline at end of file