--[[
Screen Buffer Trystan Cannon
17 July 2013
Contains the Screen Buffer class which contains
the accurate behavior for a buffer when working
with Windows or multiple threads which require
their own buffer to read and write from/to when
running concurrently.
]]
-- Variables -------------------------------------------
local bufferMetatable = { __index = getfenv (1) }
-- Variables -------------------------------------------
-- Returns a new 1d table of pixels with the given width and colors.
-- The colors, by default, will be white and black.
local function newPixelLine (width, textColor, backColor)
local pixelLine = {}
for space = 1, width do
pixelLine[space] = Pixel.new (textColor, backColor)
end
return pixelLine
end
-- Returns a new 2d table of pixels with the given dimensions and colors.
-- The colors, by default, will be white and black.
local function newPixelTable (width, height, textColor, backColor)
local pixels = {}
for lineNumber = 1, height do
pixels[lineNumber] = newPixelLine (width, textColor, backColor)
end
return pixels
end
-- Returns a table which will allow us to redirect the terminal to use
-- the buffer as a redirection object.
function getRedirectTable (buffer)
local redirect = {}
redirect.getSize = function()
return buffer.width, buffer.height
end
redirect.getCursorPos = function()
return buffer.cursorX, buffer.cursorY
end
redirect.setCursorPos = function (x, y)
buffer.cursorX = math.floor (tostring (x) ~= "nan" and tonumber (x) or buffer.cursorX)
buffer.cursorY = math.floor (tostring (y) ~= "nan" and tonumber (y) or buffer.cursorY)
-- Before setting the position of the cursor, make sure that the buffer is visible and the cursor
-- is within bounds of the buffer's borders.
if buffer.isVisible then
term.native.setCursorPos (buffer.cursorX + buffer.xPos - 1, buffer.cursorY + buffer.yPos - 1)
end
end
redirect.setCursorBlink = function (shouldCursorBlink)
buffer.shouldCursorBlink = shouldCursorBlink
if buffer.isVisible then
term.native.setCursorBlink (shouldCursorBlink)
end
end
redirect.isColor = function()
return buffer.isColor
end
redirect.setTextColor = function (color)
buffer.textColor = color
if buffer.isVisible then
return term.native.setTextColor (color)
end
end
redirect.setBackgroundColor = function (color)
buffer.backColor = color
if buffer.isVisible then
return term.native.setBackgroundColor (color)
end
end
redirect.isColour = redirect.isColor
redirect.setTextColour = redirect.setTextColor
redirect.setBackgroundColour = redirect.setBackgroundColor
redirect.clearLine = function (lineNumber)
lineNumber = lineNumber or buffer.cursorY
buffer.pixels[buffer.cursorY] = newPixelLine (buffer.width, buffer.textColor, buffer.backColor)
-- Set the background color to that of the buffer so the colors match when cleared.
if buffer.isVisible then
term.native.setBackgroundColor (buffer.backColor)
for space = 1, buffer.width do
-- Only clear the space that the buffer is specified to take up on the screen.
term.native.setCursorPos (space + buffer.xPos - 1, lineNumber + buffer.yPos - 1)
term.native.write (string.rep (' ', buffer.width))
end
-- Reset the native cursor after clearing the line.
term.native.setCursorPos (buffer.cursorX + buffer.xPos - 1, buffer.cursorY + buffer.yPos - 1)
end
end
redirect.clear = function()
buffer.pixels = newPixelTable (buffer.width, buffer.height, buffer.textColor, buffer.backColor)
local currentCursorY = buffer.cursorY
for lineNumber = 1, buffer.height do
redirect.clearLine (lineNumber)
end
end
redirect.scroll = function (timesToScroll)
if timesToScroll > 0 then
for timesScrolled = 1, timesToScroll do
for lineNumber = 1, buffer.height - 1 do
buffer.pixels[lineNumber] = buffer.pixels[lineNumber + 1]
buffer.pixels[lineNumber + 1] = newPixelLine (buffer.width, buffer.textColor, buffer.backColor)
end
end
elseif timesToScroll < 0 then
for timesScrolled = 1, math.abs (timesToScroll) do
for lineNumber = buffer.height, 2, -1 do
buffer.pixels[lineNumber] = buffer.pixels[lineNumber - 1]
buffer.pixels[lineNumber - 1] = newPixelLine (buffer.width, buffer.textColor, buffer.backColor)
end
end
end
-- If the buffer has a height of one, then the previous code won't have affected the buffer.
-- Therefore, we should just set the only line in the buffer to a blank line of pixels.
if buffer.height == 1 then
buffer.pixels[buffer.height] = newPixelLine (buffer.width, buffer.textColor, buffer.backColor)
end
-- Render the buffer now that the buffer has been scrolled.
-- This will scroll the buffer on the screen without scrolling the entire terminal.
if buffer.isVisible then
buffer:render()
end
end
redirect.write = function (text)
-- Remove escape sequences (control characers) from the string
-- just as term.write would.
text = tostring (text):gsub ("\t", " ")
text = text:gsub ("%c", "?")
-- Make sure that the cursor is in bounds.
-- Truncate the string as necessary.
if buffer.cursorX > buffer.width or buffer.cursorY < 1 or buffer.cursorY > buffer.height then
buffer.cursorX = buffer.cursorX + text:len()
return
end
if buffer.cursorX < 1 then
local textLength = text:len()
text = text:sub (math.abs (buffer.cursorX) + 2)
-- If the cursor still won't reach the inside of the buffer even after truncation,
-- just update the buffer's cursor and don't attempt to write anything.
if text:len() == 0 then
buffer.cursorX = buffer.cursorX + textLength
return
end
buffer.cursorX = 1
end
if buffer.cursorX + text:len() - 1 > buffer.width then
text = text:sub (1, buffer.width - buffer.cursorX + 1)
end
-- Write the text to the native terminal.
if buffer.isVisible then
term.native.setTextColor (buffer.textColor)
term.native.setBackgroundColor (buffer.backColor)
term.native.setCursorPos (buffer.cursorX + buffer.xPos - 1, buffer.cursorY + buffer.yPos - 1)
term.native.write (text)
end
-- Write the text to the buffer character for character into pixels.
for character in text:gmatch ('.') do
buffer.pixels[buffer.cursorY][buffer.cursorX] = Pixel.new (buffer.textColor, buffer.backColor, character)
buffer.cursorX = buffer.cursorX + 1
end
end
return redirect
end
-- Renders the given buffer and returns the terminal's colors and cursor
-- back to their previous states (before rendering).
function render (buffer)
term.native.setCursorBlink (false)
-- Define the current colors for drawing pixels without disturbing
-- the buffers current colors. This way, we can check if we need to
-- change the colors already set in order to draw the pixel accurately.
local currentTextColor = buffer.textColor
local currentBackColor = buffer.backColor
-- Set the native terminal colors to the current colors that we defined above.
term.native.setTextColor (currentTextColor)
term.native.setBackgroundColor (currentBackColor)
-- Iterate over the 2d pixel table of the buffer and draw all of the pixels
-- in it.
for lineNumber = 1, buffer.height do
for space = 1, buffer.width do
local pixel = buffer.pixels[lineNumber][space]
-- Try to avoid setting the text and background colors if
-- the current text and or background color is already that of
-- the pixel.
if currentTextColor ~= pixel.textColor then
currentTextColor = pixel.textColor
term.native.setTextColor (currentTextColor)
end
if currentBackColor ~= pixel.backColor then
currentBackColor = pixel.backColor
term.native.setBackgroundColor (currentBackColor)
end
-- Position the cursor properly for the buffer and draw the pixel for the iteration.
term.native.setCursorPos (space + buffer.xPos - 1, lineNumber + buffer.yPos - 1)
term.native.write (pixel.character)
end
end
-- Return the state of the terminal to what it was before we rendered
-- the buffer.
term.native.setTextColor (buffer.textColor)
term.native.setBackgroundColor (buffer.backColor)
-- Make sure that the cursor is in bounds of the buffer and that the buffer is visible before setting the position and blink state to that
-- of the the cursor to that of the buffer.
if buffer.isVisible and buffer.cursorX + buffer.xPos - 1 >= buffer.xPos and buffer.cursorX + buffer.xPos - 1 <= buffer.xPos + buffer.width and
buffer.cursorY + buffer.yPos - 1 >= buffer.yPos and buffer.cursorY + buffer.yPos - 1 <= buffer.yPos + buffer.height then
term.native.setCursorPos (buffer.cursorX + buffer.xPos - 1, buffer.cursorY + buffer.yPos - 1)
term.native.setCursorBlink (buffer.shouldCursorBlink)
-- Since the cursor will end up at the lower right hand corner of the window on the screen, we should turn off the cursor
-- blink so that it doesn't look like the cursor is actually in the lower right corner of the window.
else
term.native.setCursorBlink (false)
end
end
-- Constructs and returns a new Buffer object.
function new (width, height, xPos, yPos, isVisible, isColor)
if isVisible == nil then
isVisible = true
end
if isColor == nil then
isColor = true
end
local this = {
width = width,
height = height,
textColor = colors.white,
backColor = colors.black,
xPos = xPos or 1,
yPos = yPos or 1,
cursorX = 1,
cursorY = 1,
shouldCursorBlink = true,
isVisible = isVisible,
isColor = isColor,
pixels = newPixelTable (width, height)
}
setmetatable (this, bufferMetatable)
return this
end