diff options
| author | Andrew Lee <alee14498@gmail.com> | 2019-07-19 14:10:39 -0400 |
|---|---|---|
| committer | Andrew Lee <alee14498@gmail.com> | 2019-07-19 14:10:39 -0400 |
| commit | fe08446d84e0aa939780ad013f0545778703da6d (patch) | |
| tree | f45358ae6470dc894c41abcb278c4898c7ef1b18 /.mbs/lib | |
| parent | 21446806b264d8fd6694b1495f0c51bf24d26b35 (diff) | |
| download | bits-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.lua | 297 | ||||
| -rw-r--r-- | .mbs/lib/readline.lua | 506 | ||||
| -rw-r--r-- | .mbs/lib/scroll_window.lua | 459 | ||||
| -rw-r--r-- | .mbs/lib/stack_trace.lua | 88 |
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 |
