aboutsummaryrefslogtreecommitdiff
path: root/.mbs/lib/scroll_window.lua
diff options
context:
space:
mode:
Diffstat (limited to '.mbs/lib/scroll_window.lua')
-rw-r--r--.mbs/lib/scroll_window.lua459
1 files changed, 459 insertions, 0 deletions
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