aboutsummaryrefslogtreecommitdiff
path: root/.mbs/lib
diff options
context:
space:
mode:
authorAndrew Lee <alee14498@gmail.com>2019-07-19 14:10:39 -0400
committerAndrew Lee <alee14498@gmail.com>2019-07-19 14:10:39 -0400
commitfe08446d84e0aa939780ad013f0545778703da6d (patch)
treef45358ae6470dc894c41abcb278c4898c7ef1b18 /.mbs/lib
parent21446806b264d8fd6694b1495f0c51bf24d26b35 (diff)
downloadbits-UI-fe08446d84e0aa939780ad013f0545778703da6d.tar.gz
bits-UI-fe08446d84e0aa939780ad013f0545778703da6d.tar.bz2
bits-UI-fe08446d84e0aa939780ad013f0545778703da6d.zip
MBS as default shell
Diffstat (limited to '.mbs/lib')
-rw-r--r--.mbs/lib/blit_window.lua297
-rw-r--r--.mbs/lib/readline.lua506
-rw-r--r--.mbs/lib/scroll_window.lua459
-rw-r--r--.mbs/lib/stack_trace.lua88
4 files changed, 1350 insertions, 0 deletions
diff --git a/.mbs/lib/blit_window.lua b/.mbs/lib/blit_window.lua
new file mode 100644
index 0000000..39cd843
--- /dev/null
+++ b/.mbs/lib/blit_window.lua
@@ -0,0 +1,297 @@
+local colour_lookup = {}
+for i = 0, 16 do
+ colour_lookup[2 ^ i] = string.format("%x", i)
+end
+
+function create(original)
+ if not original then original = term.current() end
+
+ local text = {}
+ local text_colour = {}
+ local back_colour = {}
+ local palette = {}
+
+ local cursor_x, cursor_y = 1, 1
+
+ local cursor_blink = false
+ local cur_text_colour = "0"
+ local cur_back_colour = "f"
+
+ local sizeX, sizeY = original.getSize()
+ local color = original.isColor()
+
+ local bubble = true
+
+ local redirect = {}
+
+ if original.getPaletteColour then
+ for i = 0, 15 do
+ local c = 2 ^ i
+ palette[c] = { original.getPaletteColour(c) }
+ end
+ end
+
+ function redirect.write(writeText)
+ writeText = tostring(writeText)
+ if bubble then original.write(writeText) end
+
+ local pos = cursor_x
+
+ -- If we're off the screen then just emulate a write
+ if cursor_y > sizeY or cursor_y < 1 then
+ cursor_x = pos + #writeText
+ return
+ end
+
+ if pos + #writeText <= 1 or pos > sizeX then
+ -- If we're too far off the left then skip.
+ cursor_x = pos + #writeText
+ return
+ elseif pos < 1 then
+ -- Adjust text to fit on screen starting at one.
+ writeText = string.sub(writeText, math.abs(cursor_x) + 2)
+ pos = 1
+ end
+
+ local lineText = text[cursor_y]
+ local lineColor = text_colour[cursor_y]
+ local lineBack = back_colour[cursor_y]
+ local preStop = pos - 1
+ local preStart = math.min(1, preStop)
+ local postStart = pos + string.len(writeText)
+ local postStop = sizeX
+ local sub, rep = string.sub, string.rep
+
+ text[cursor_y] = sub(lineText, preStart, preStop)..writeText..sub(lineText, postStart, postStop)
+ text_colour[cursor_y] = sub(lineColor, preStart, preStop)..rep(cur_text_colour, #writeText)..sub(lineColor, postStart, postStop)
+ back_colour[cursor_y] = sub(lineBack, preStart, preStop)..rep(cur_back_colour, #writeText)..sub(lineBack, postStart, postStop)
+ cursor_x = pos + string.len(writeText)
+ end
+
+ function redirect.blit(writeText, writeFore, writeBack)
+ if type(writeText) ~= "string" then error("bad argument #1 (expected string, got " .. type(writeText) .. ")", 2) end
+ if type(writeFore) ~= "string" then error("bad argument #2 (expected string, got " .. type(writeFore) .. ")", 2) end
+ if type(writeBack) ~= "string" then error("bad argument #3 (expected string, got " .. type(writeBack) .. ")", 2) end
+ if #writeFore ~= #writeText or #writeBack ~= #writeText then error("Arguments must be the same length", 2) end
+
+ if bubble then original.blit(writeText, writeFore, writeBack) end
+ local pos = cursor_x
+
+ -- If we're off the screen then just emulate a write
+ if cursor_y > sizeY or cursor_y < 1 then
+ cursor_x = pos + #writeText
+ return
+ end
+
+ if pos + #writeText <= 1 then
+ --skip entirely.
+ cursor_x = pos + #writeText
+ return
+ elseif pos < 1 then
+ --adjust text to fit on screen starting at one.
+ writeText = string.sub(writeText, math.abs(cursor_x) + 2)
+ writeFore = string.sub(writeFore, math.abs(cursor_x) + 2)
+ writeBack = string.sub(writeBack, math.abs(cursor_x) + 2)
+ cursor_x = 1
+ elseif pos > sizeX then
+ --if we're off the edge to the right, skip entirely.
+ cursor_x = pos + #writeText
+ return
+ end
+
+ local lineText = text[cursor_y]
+ local lineColor = text_colour[cursor_y]
+ local lineBack = back_colour[cursor_y]
+ local preStop = cursor_x - 1
+ local preStart = math.min(1, preStop)
+ local postStart = cursor_x + string.len(writeText)
+ local postStop = sizeX
+ local sub = string.sub
+
+ text[cursor_y] = sub(lineText, preStart, preStop)..writeText..sub(lineText, postStart, postStop)
+ text_colour[cursor_y] = sub(lineColor, preStart, preStop)..writeFore..sub(lineColor, postStart, postStop)
+ back_colour[cursor_y] = sub(lineBack, preStart, preStop)..writeBack..sub(lineBack, postStart, postStop)
+ cursor_x = pos + string.len(writeText)
+ end
+
+ function redirect.clear()
+ for i = 1, sizeY do
+ text[i] = string.rep(" ", sizeX)
+ text_colour[i] = string.rep(cur_text_colour, sizeX)
+ back_colour[i] = string.rep(cur_back_colour, sizeX)
+ end
+
+ if bubble then return original.clear() end
+ end
+
+ function redirect.clearLine()
+ -- If we're off the screen then just emulate a clearLine
+ if cursor_y > sizeY or cursor_y < 1 then
+ return
+ end
+
+ text[cursor_y] = string.rep(" ", sizeX)
+ text_colour[cursor_y] = string.rep(cur_text_colour, sizeX)
+ back_colour[cursor_y] = string.rep(cur_back_colour, sizeX)
+
+ if bubble then return original.clearLine() end
+ end
+
+ function redirect.getCursorPos()
+ return cursor_x, cursor_y
+ end
+
+ function redirect.setCursorPos(x, y)
+ if type(x) ~= "number" then error("bad argument #1 (expected number, got " .. type(x) .. ")", 2) end
+ if type(y) ~= "number" then error("bad argument #2 (expected number, got " .. type(y) .. ")", 2) end
+
+ cursor_x = math.floor(x)
+ cursor_y = math.floor(y)
+ if bubble then return original.setCursorPos(x, y) end
+ end
+
+ function redirect.setCursorBlink(b)
+ cursor_blink = b
+ if bubble then return original.setCursorBlink(b) end
+ end
+
+ function redirect.getSize()
+ return sizeX, sizeY
+ end
+
+ function redirect.scroll(n)
+ if type(n) ~= "number" then error("bad argument #1 (expected number, got " .. type(n) .. ")", 2) end
+
+ local empty_text = string.rep(" ", sizeX)
+ local empty_text_colour = string.rep(cur_text_colour, sizeX)
+ local empty_back_colour = string.rep(cur_back_colour, sizeX)
+ if n > 0 then
+ for i = 1, sizeY do
+ text[i] = text[i + n] or empty_text
+ text_colour[i] = text_colour[i + n] or empty_text_colour
+ back_colour[i] = back_colour[i + n] or empty_back_colour
+ end
+ elseif n < 0 then
+ for i = sizeY, 1, -1 do
+ text[i] = text[i + n] or empty_text
+ text_colour[i] = text_colour[i + n] or empty_text_colour
+ back_colour[i] = back_colour[i + n] or empty_back_colour
+ end
+ end
+
+ if bubble then return original.scroll(n) end
+ end
+
+ function redirect.setTextColour(clr)
+ if type(clr) ~= "number" then error("bad argument #1 (expected number, got " .. type(clr) .. ")", 2) end
+ cur_text_colour = colour_lookup[clr] or error("Invalid colour (got " .. clr .. ")" , 2)
+ if bubble then return original.setTextColour(clr) end
+ end
+ redirect.setTextColor = redirect.setTextColour
+
+ function redirect.setBackgroundColour(clr)
+ if type(clr) ~= "number" then error("bad argument #1 (expected number, got " .. type(clr) .. ")", 2) end
+ cur_back_colour = colour_lookup[clr] or error("Invalid colour (got " .. clr .. ")" , 2)
+ if bubble then return original.setBackgroundColour(clr) end
+ end
+ redirect.setBackgroundColor = redirect.setBackgroundColour
+
+ function redirect.isColour()
+ return color == true
+ end
+ redirect.isColor = redirect.isColour
+
+ function redirect.getTextColour()
+ return 2 ^ tonumber(cur_text_colour, 16)
+ end
+ redirect.getTextColor = redirect.getTextColour
+
+ function redirect.getBackgroundColour()
+ return 2 ^ tonumber(cur_back_colour, 16)
+ end
+ redirect.getBackgroundColor = redirect.getBackgroundColour
+
+ if original.getPaletteColour then
+ function redirect.setPaletteColour(colour, r, g, b)
+ local palcol = palette[colour]
+ if not palcol then error("Invalid colour (got " .. tostring(colour) .. ")", 2) end
+ if type(r) == "number" and g == nil and b == nil then
+ palcol[1], palcol[2], palcol[3] = colours.rgb8(r)
+ else
+ if type(r) ~= "number" then error("bad argument #2 (expected number, got " .. type(r) .. ")", 2) end
+ if type(g) ~= "number" then error("bad argument #3 (expected number, got " .. type(g) .. ")", 2) end
+ if type(b) ~= "number" then error("bad argument #4 (expected number, got " .. type(b) .. ")", 2) end
+
+ palcol[1], palcol[2], palcol[3] = r, g, b
+ end
+
+ if bubble then return original.setPaletteColour(colour, r, g, b) end
+ end
+ redirect.setPaletteColor = redirect.setPaletteColour
+
+ function redirect.getPaletteColour(colour)
+ local palcol = palette[colour]
+ if not palcol then error("Invalid colour (got " .. tostring(colour) .. ")", 2) end
+ return palcol[1], palcol[2], palcol[3]
+ end
+ redirect.getPaletteColor = redirect.getPaletteColour
+ end
+
+ function redirect.draw(target)
+ if not target then target = original end
+
+ if target.getPaletteColour then
+ for colour, pal in pairs(palette) do
+ target.setPaletteColour(colour, pal[1], pal[2], pal[3])
+ end
+ end
+
+ for i=1, sizeY do
+ target.setCursorPos(1,i)
+ target.blit(text[i], text_colour[i], back_colour[i])
+ end
+
+ target.setCursorPos(cursor_x, cursor_y)
+ target.setTextColour(2 ^ tonumber(cur_text_colour, 16))
+ target.setBackgroundColor(2 ^ tonumber(cur_back_colour, 16))
+ target.setCursorBlink(cursor_blink)
+ end
+
+ function redirect.bubble(b)
+ bubble = b
+ end
+
+ function redirect.updateSize()
+ local new_x, new_y = original.getSize()
+ if new_x == sizeX and new_y == sizeY then return end
+
+ -- For any existing lines, trim them
+ for y = 1, sizeY do
+ if new_x < sizeX then
+ text[y] = text[y]:sub(1, new_x)
+ text_colour[y] = text_colour[y]:sub(1, new_x)
+ back_colour[y] = back_colour[y]:sub(1, new_x)
+ elseif new_x > sizeX then
+ text[y] = text[y] .. (" "):rep(new_x - sizeX)
+ text_colour[y] = text_colour[y] .. (cur_text_colour):rep(new_x - sizeX)
+ back_colour[y] = back_colour[y] .. (cur_back_colour):rep(new_x - sizeX)
+ end
+ end
+
+ -- Add any new lines we might need.
+ local text_line = (" "):rep(new_x)
+ local fore_line = (cur_text_colour):rep(new_x)
+ local back_line = (cur_back_colour):rep(new_x)
+ for y = sizeY + 1, new_y do
+ text[y] = text_line
+ text_colour[y] = fore_line
+ back_colour[y] = back_line
+ end
+
+ sizeX = new_x
+ sizeY = new_y
+ end
+
+ redirect.clear()
+ return redirect
+end \ No newline at end of file
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
diff --git a/.mbs/lib/scroll_window.lua b/.mbs/lib/scroll_window.lua
new file mode 100644
index 0000000..75a6fb1
--- /dev/null
+++ b/.mbs/lib/scroll_window.lua
@@ -0,0 +1,459 @@
+local type = type
+
+local colour_lookup = {}
+for i = 0, 16 do
+ colour_lookup[2 ^ i] = string.format("%x", i)
+end
+
+function create(original)
+ if not original then original = term.current() end
+
+ local text = {}
+ local text_colour = {}
+ local back_colour = {}
+ local palette = {}
+
+ local cursor_x, cursor_y = 1, 1
+
+ local scroll_offset = 0
+ local scroll_cursor_y = cursor_y
+
+ local cursor_blink = false
+ local cur_text_colour = "0"
+ local cur_back_colour = "f"
+
+ local sizeX, sizeY = original.getSize()
+ local color = original.isColor()
+
+ local max_scrollback = 100
+ local bubble, delegate = true, nil
+
+ local cursor_threshold = 0
+
+ local redirect = {}
+
+ if original.getPaletteColour then
+ for i = 0, 15 do
+ local c = 2 ^ i
+ palette[c] = { original.getPaletteColour(c) }
+ end
+ end
+
+ local function trim()
+ if max_scrollback > -1 then
+ while scroll_offset > max_scrollback do
+ table.remove(text, 1)
+ table.remove(text_colour, 1)
+ table.remove(back_colour, 1)
+ scroll_offset = scroll_offset - 1
+ end
+ end
+ scroll_cursor_y = scroll_offset + cursor_y
+ end
+
+ function redirect.write(writeText)
+ if delegate then return delegate.write(writeText) end
+
+ writeText = tostring(writeText)
+ if bubble then original.write(writeText) end
+
+ local pos = cursor_x
+
+ -- If we're off the screen then just emulate a write
+ if cursor_y > sizeY or cursor_y < 1 then
+ cursor_x = pos + #writeText
+ return
+ end
+
+ if pos + #writeText <= 1 or pos > sizeX then
+ -- If we're too far off the left then skip.
+ cursor_x = pos + #writeText
+ return
+ elseif pos < 1 then
+ -- Adjust text to fit on screen starting at one.
+ writeText = string.sub(writeText, -pos + 2)
+ pos = 1
+ end
+
+ local lineText = text[scroll_cursor_y]
+ local lineColor = text_colour[scroll_cursor_y]
+ local lineBack = back_colour[scroll_cursor_y]
+ local preStop = pos - 1
+ local preStart = math.min(1, preStop)
+ local postStart = pos + string.len(writeText)
+ local postStop = sizeX
+ local sub, rep = string.sub, string.rep
+
+ text[scroll_cursor_y] = sub(lineText, preStart, preStop)..writeText..sub(lineText, postStart, postStop)
+ text_colour[scroll_cursor_y] = sub(lineColor, preStart, preStop)..rep(cur_text_colour, #writeText)..sub(lineColor, postStart, postStop)
+ back_colour[scroll_cursor_y] = sub(lineBack, preStart, preStop)..rep(cur_back_colour, #writeText)..sub(lineBack, postStart, postStop)
+ cursor_x = pos + string.len(writeText)
+ end
+
+ function redirect.blit(writeText, writeFore, writeBack)
+ if delegate then return delegate.blit(writeText, writeFore, writeBack) end
+
+ if type(writeText) ~= "string" then error("bad argument #1 (expected string, got " .. type(writeText) .. ")", 2) end
+ if type(writeFore) ~= "string" then error("bad argument #2 (expected string, got " .. type(writeFore) .. ")", 2) end
+ if type(writeBack) ~= "string" then error("bad argument #3 (expected string, got " .. type(writeBack) .. ")", 2) end
+ if #writeFore ~= #writeText or #writeBack ~= #writeText then error("Arguments must be the same length", 2) end
+
+ if bubble then original.blit(writeText, writeFore, writeBack) end
+
+ local pos = cursor_x
+
+ -- If we're off the screen then just emulate a write
+ if cursor_y > sizeY or cursor_y < 1 then
+ cursor_x = pos + #writeText
+ return
+ end
+
+ if pos + #writeText <= 1 then
+ --skip entirely.
+ cursor_x = pos + #writeText
+ return
+ elseif pos < 1 then
+ --adjust text to fit on screen starting at one.
+ writeText = string.sub(writeText, math.abs(cursor_x) + 2)
+ writeFore = string.sub(writeFore, math.abs(cursor_x) + 2)
+ writeBack = string.sub(writeBack, math.abs(cursor_x) + 2)
+ cursor_x = 1
+ elseif pos > sizeX then
+ --if we're off the edge to the right, skip entirely.
+ cursor_x = pos + #writeText
+ return
+ else
+ writeText = writeText
+ end
+
+ local lineText = text[scroll_cursor_y]
+ local lineColor = text_colour[scroll_cursor_y]
+ local lineBack = back_colour[scroll_cursor_y]
+ local preStop = cursor_x - 1
+ local preStart = math.min(1, preStop)
+ local postStart = cursor_x + string.len(writeText)
+ local postStop = sizeX
+ local sub = string.sub
+
+ text[scroll_cursor_y] = sub(lineText, preStart, preStop)..writeText..sub(lineText, postStart, postStop)
+ text_colour[scroll_cursor_y] = sub(lineColor, preStart, preStop)..writeFore..sub(lineColor, postStart, postStop)
+ back_colour[scroll_cursor_y] = sub(lineBack, preStart, preStop)..writeBack..sub(lineBack, postStart, postStop)
+ cursor_x = pos + #writeText
+ end
+
+ function redirect.clear()
+ if delegate then return delegate.clear() end
+
+ if cursor_threshold > 0 then
+ return redirect.beginPrivateMode().clear()
+ end
+
+ local text_line = (" "):rep(sizeX)
+ local fore_line = (cur_text_colour):rep(sizeX)
+ local back_line = (cur_back_colour):rep(sizeX)
+
+ for i = scroll_offset + 1, sizeY + scroll_offset do
+ text[i] = text_line
+ text_colour[i] = fore_line
+ back_colour[i] = back_line
+ end
+
+ if bubble then return original.clear() end
+ end
+ function redirect.clearLine()
+ if delegate then return delegate.clearLine() end
+
+ -- If we're off the screen then just emulate a clearLine
+ if cursor_y > sizeY or cursor_y < 1 then
+ return
+ end
+
+ text[scroll_cursor_y] = string.rep(" ", sizeX)
+ text_colour[scroll_cursor_y] = string.rep(cur_text_colour, sizeX)
+ back_colour[scroll_cursor_y] = string.rep(cur_back_colour, sizeX)
+
+ if bubble then return original.clearLine() end
+ end
+
+ function redirect.getCursorPos()
+ if delegate then return delegate.getCursorPos() end
+ return cursor_x, cursor_y
+ end
+
+ function redirect.setCursorPos(x, y)
+ if delegate then return delegate.setCursorPos(x, y) end
+
+ if type(x) ~= "number" then error("bad argument #1 (expected number, got " .. type(x) .. ")", 2) end
+ if type(y) ~= "number" then error("bad argument #2 (expected number, got " .. type(y) .. ")", 2) end
+
+ local new_y = math.floor(y)
+ if new_y >= 1 and new_y < cursor_threshold then
+ -- If we're writing within a protected region then start a private buffer
+ return redirect.beginPrivateMode().setCursorPos(x, y)
+ end
+
+ cursor_x = math.floor(x)
+ cursor_y = new_y
+ scroll_cursor_y = new_y + scroll_offset
+
+ if bubble then return original.setCursorPos(x, y) end
+ end
+
+ function redirect.setCursorBlink(b)
+ if delegate then return delegate.setCursorBlink(b) end
+
+ if type(b) ~= "boolean" then error("bad argument #1 (expected boolean, got " .. type(b) .. ")", 2) end
+
+ cursor_blink = b
+ if bubble then return original.setCursorBlink(b) end
+ end
+
+ function redirect.getSize()
+ if delegate then return delegate.getSize() end
+
+ return sizeX, sizeY
+ end
+
+ function redirect.scroll(n)
+ if delegate then return delegate.scroll(n) end
+
+ if type(n) ~= "number" then error("bad argument #1 (expected number, got " .. type(n) .. ")", 2) end
+
+ if n > 0 then
+ scroll_offset = scroll_offset + n
+ for i = sizeY + scroll_offset - n + 1, sizeY + scroll_offset do
+ text[i] = string.rep(" ", sizeX)
+ text_colour[i] = string.rep(cur_text_colour, sizeX)
+ back_colour[i] = string.rep(cur_back_colour, sizeX)
+ end
+
+ trim()
+ elseif n < 0 then
+ for i = sizeY + scroll_cursor_y, math.abs(n) + 1 + scroll_cursor_y, -1 do
+ if text[i + n] then
+ text[i] = text[i + n]
+ text_colour[i] = text_colour[i + n]
+ back_colour[i] = back_colour[i + n]
+ end
+ end
+
+ for i = scroll_cursor_y, math.abs(n) + scroll_cursor_y do
+ text[i] = string.rep(" ", sizeX)
+ text_colour[i] = string.rep(cur_text_colour, sizeX)
+ back_colour[i] = string.rep(cur_back_colour, sizeX)
+ end
+ end
+
+ cursor_threshold = cursor_threshold - n
+
+ if bubble then return original.scroll(n) end
+ end
+
+ function redirect.setTextColour(clr)
+ if delegate then return delegate.setTextColour(clr) end
+
+ if type(clr) ~= "number" then error("bad argument #1 (expected number, got " .. type(clr) .. ")", 2) end
+ cur_text_colour = colour_lookup[clr] or error("Invalid colour (got " .. clr .. ")" , 2)
+ if bubble then return original.setTextColour(clr) end
+ end
+ redirect.setTextColor = redirect.setTextColour
+
+ function redirect.setBackgroundColour(clr)
+ if delegate then return delegate.setBackgroundColour(clr) end
+
+ if type(clr) ~= "number" then error("bad argument #1 (expected number, got " .. type(clr) .. ")", 2) end
+ cur_back_colour = colour_lookup[clr] or error("Invalid colour (got " .. clr .. ")" , 2)
+ if bubble then return original.setBackgroundColour(clr) end
+ end
+ redirect.setBackgroundColor = redirect.setBackgroundColour
+
+ function redirect.isColour()
+ if delegate then return delegate.isColour() end
+ return color == true
+ end
+ redirect.isColor = redirect.isColour
+
+ function redirect.getTextColour()
+ if delegate then return delegate.getTextColour() end
+ return 2 ^ tonumber(cur_text_colour, 16)
+ end
+ redirect.getTextColor = redirect.getTextColour
+
+ function redirect.getBackgroundColour()
+ if delegate then return delegate.getBackgroundColour() end
+ return 2 ^ tonumber(cur_back_colour, 16)
+ end
+ redirect.getBackgroundColor = redirect.getBackgroundColour
+
+ if original.getPaletteColour then
+ function redirect.setPaletteColour(colour, r, g, b)
+ if delegate then return delegate.setPaletteColour(colour, r, g, b) end
+
+ local palcol = palette[colour]
+ if not palcol then error("Invalid colour (got " .. tostring(colour) .. ")", 2) end
+ if type(r) == "number" and g == nil and b == nil then
+ palcol[1], palcol[2], palcol[3] = colours.rgb8(r)
+ else
+ if type(r) ~= "number" then error("bad argument #2 (expected number, got " .. type(r) .. ")", 2) end
+ if type(g) ~= "number" then error("bad argument #3 (expected number, got " .. type(g) .. ")", 2) end
+ if type(b) ~= "number" then error("bad argument #4 (expected number, got " .. type(b) .. ")", 2) end
+
+ palcol[1], palcol[2], palcol[3] = r, g, b
+ end
+
+ if bubble then return original.setPaletteColour(colour, r, g, b) end
+ end
+ redirect.setPaletteColor = redirect.setPaletteColour
+
+ function redirect.getPaletteColour(colour)
+ if delegate then return delegate.getPaletteColour(colour) end
+
+ local palcol = palette[colour]
+ if not palcol then error("Invalid colour (got " .. tostring(colour) .. ")", 2) end
+ return palcol[1], palcol[2], palcol[3]
+ end
+ redirect.getPaletteColor = redirect.getPaletteColour
+ end
+
+ function redirect.draw(offset, clear)
+ if delegate then return end
+
+ if original.getPaletteColour then
+ for colour, pal in pairs(palette) do
+ original.setPaletteColour(colour, pal[1], pal[2], pal[3])
+ end
+ end
+
+ original.setTextColour(2 ^ tonumber(cur_text_colour, 16))
+ original.setBackgroundColor(2 ^ tonumber(cur_back_colour, 16))
+ if clear then original.clear() end
+
+ local original = original
+ local scroll_offset = scroll_offset + (offset or 0)
+ for i = 1, sizeY do
+ original.setCursorPos(1,i)
+ local yOffset = scroll_offset + i
+ original.blit(text[yOffset], text_colour[yOffset], back_colour[yOffset])
+ end
+
+ original.setCursorPos(cursor_x, cursor_y - offset)
+ original.setCursorBlink(cursor_blink)
+ end
+
+ function redirect.bubble(b)
+ bubble = b
+ end
+
+ function redirect.setCursorThreshold(y)
+ cursor_threshold = y
+ end
+
+ function redirect.endPrivateMode(redraw)
+ if delegate then
+ local old_delegate = delegate
+ delegate = nil
+ redirect.draw(0)
+
+ -- If we should redraw the old buffer then blit it to the canvas
+ if redraw then
+ if cursor_threshold > 0 then
+ redirect.scroll(cursor_threshold)
+ end
+
+ old_delegate.draw(redirect)
+ end
+ end
+ end
+
+ function redirect.beginPrivateMode()
+ if not delegate then
+ delegate = blit_window.create(original)
+
+ for y = 1, sizeY do
+ delegate.setCursorPos(1, y)
+ delegate.blit(text[y + scroll_offset], text_colour[y + scroll_offset], back_colour[y + scroll_offset])
+ end
+
+ delegate.setCursorPos(cursor_x, cursor_y)
+ delegate.setCursorBlink(cursor_blink)
+ delegate.setTextColour(2 ^ tonumber(cur_text_colour, 16))
+ delegate.setBackgroundColor(2 ^ tonumber(cur_back_colour, 16))
+
+ if original.getPaletteColour then
+ for i = 0, 15 do
+ local palcol = palette[2 ^ i]
+ delegate.setPaletteColour(2 ^ i, palcol[1], palcol[2], palcol[3])
+ end
+ end
+ end
+
+ return delegate
+ end
+
+ function redirect.isPrivateMode()
+ return delegate ~= nil
+ end
+
+ function redirect.getTotalHeight() return scroll_offset end
+
+ function redirect.setMaxScrollback(n)
+ local old_scrollback = max_scrollback
+ max_scrollback = n
+
+ if old_scrollback > max_scrollback then trim() end
+ end
+
+ function redirect.updateSize()
+ -- Update the delegate window.
+ if delegate then delegate.updateSize() end
+
+ -- If nothing has changed then just skip.
+ local new_x, new_y = original.getSize()
+ if new_x == sizeX and new_y == sizeY then return end
+
+ -- If we have an insufficient number of lines then add some in.
+ local total_height = #text
+
+ -- For any existing lines, trim them
+ for y = 1, total_height do
+ if new_x < sizeX then
+ text[y] = text[y]:sub(1, new_x)
+ text_colour[y] = text_colour[y]:sub(1, new_x)
+ back_colour[y] = back_colour[y]:sub(1, new_x)
+ elseif new_x > sizeX then
+ text[y] = text[y] .. (" "):rep(new_x - sizeX)
+ text_colour[y] = text_colour[y] .. (cur_text_colour):rep(new_x - sizeX)
+ back_colour[y] = back_colour[y] .. (cur_back_colour):rep(new_x - sizeX)
+ end
+ end
+
+ if new_y > sizeY then
+ -- Append any new lines we might need.
+ local text_line = (" "):rep(new_x)
+ local fore_line = (cur_text_colour):rep(new_x)
+ local back_line = (cur_back_colour):rep(new_x)
+ for y = total_height + 1, new_y do
+ text[y] = text_line
+ text_colour[y] = fore_line
+ back_colour[y] = back_line
+ end
+ elseif new_y < sizeY then
+ -- Move the cursor "up" the screen, as we're going to scroll the rest of
+ -- the terminal up.
+ -- Note, this is a little ugly (we lose the top of the screen even if we)
+ -- don't need to, but it's the best we can do for now.
+ cursor_y = cursor_y - sizeY + new_y
+ end
+
+ sizeX = new_x
+ sizeY = new_y
+
+ -- Update the scroll offset. For now we just go back to the bottom
+ scroll_offset = #text - sizeY
+ scroll_cursor_y = scroll_offset + cursor_y
+ trim()
+ end
+
+ redirect.clear()
+ return redirect
+end \ No newline at end of file
diff --git a/.mbs/lib/stack_trace.lua b/.mbs/lib/stack_trace.lua
new file mode 100644
index 0000000..7674813
--- /dev/null
+++ b/.mbs/lib/stack_trace.lua
@@ -0,0 +1,88 @@
+local type = type
+local debug_traceback = type(debug) == "table" and type(debug.traceback) == "function" and debug.traceback
+
+local function traceback(x)
+ -- Attempt to detect error() and error("xyz", 0).
+ -- This probably means they're erroring the program intentionally and so we
+ -- shouldn't display anything.
+ if x == nil or (type(x) == "string" and not x:find(":%d+:")) then
+ return x
+ end
+
+ if debug_traceback then
+ -- The parens are important, as they prevent a tail call occuring, meaning
+ -- the stack level is preserved. This ensures the code behaves identically
+ -- on LuaJ and PUC Lua.
+ return (debug_traceback(tostring(x), 2))
+ else
+ local level = 3
+ local out = { tostring(x), "stack traceback:" }
+ while true do
+ local _, msg = pcall(error, "", level)
+ if msg == "" then break end
+
+ out[#out + 1] = " " .. msg
+ level = level + 1
+ end
+
+ return table.concat(out, "\n")
+ end
+end
+
+local function trim_traceback(target, marker)
+ local ttarget, tmarker = {}, {}
+ for line in target:gmatch("([^\n]*)\n?") do ttarget[#ttarget + 1] = line end
+ for line in marker:gmatch("([^\n]*)\n?") do tmarker[#tmarker + 1] = line end
+
+ -- Trim identical suffixes
+ local t_len, m_len = #ttarget, #tmarker
+ while t_len >= 3 and ttarget[t_len] == tmarker[m_len] do
+ table.remove(ttarget, t_len)
+ t_len, m_len = t_len - 1, m_len - 1
+ end
+
+ -- Trim elements from this file and xpcall invocations
+ while t_len >= 1 and ttarget[t_len]:find("^\tstack_trace%.lua:%d+:") or
+ ttarget[t_len] == "\t[C]: in function 'xpcall'" or ttarget[t_len] == " xpcall: " do
+ table.remove(ttarget, t_len)
+ t_len = t_len - 1
+ end
+
+ return ttarget
+end
+
+--- Run a function with
+local function xpcall_with(fn)
+ -- So this is rather grim: we need to get the full traceback and current one and remove
+ -- the common prefix
+ local trace
+ local res = table.pack(xpcall(fn, traceback)) if not res[1] then trace = traceback("trace.lua:1:") end
+ local ok, err = res[1], res[2]
+
+ if not ok and err ~= nil then
+ trace = trim_traceback(err, trace)
+
+ -- Find the position where the stack traceback actually starts
+ local trace_starts
+ for i = #trace, 1, -1 do
+ if trace[i] == "stack traceback:" then trace_starts = i; break end
+ end
+
+ -- If this traceback is more than 15 elements long, keep the first 9, last 5
+ -- and put an ellipsis between the rest
+ local max = 15
+ if trace_starts and #trace - trace_starts > max then
+ local keep_starts = trace_starts + 10
+ for i = #trace - trace_starts - max, 0, -1 do table.remove(trace, keep_starts + i) end
+ table.insert(trace, keep_starts, " ...")
+ end
+
+ return false, table.concat(trace, "\n")
+ end
+
+ return table.unpack(res, 1, res.n)
+end
+
+_ENV.traceback = traceback
+_ENV.trim_traceback = trim_traceback
+_ENV.xpcall_with = xpcall_with \ No newline at end of file