Skip to content

Instantly share code, notes, and snippets.

@MineRobber9000
Created December 10, 2022 07:47
Show Gist options
  • Save MineRobber9000/f1593695cbdf767ba76ddc295a3b701b to your computer and use it in GitHub Desktop.
Save MineRobber9000/f1593695cbdf767ba76ddc295a3b701b to your computer and use it in GitHub Desktop.
multiMon

multiMon

An update of the original by Shazz. Adds all term functions that have been added since 2014, and changes the architecture a bit.

To use

Unfortunately, due to the nature of this library, you still need to use os.loadAPI to use it (yuck, I know).

os.loadAPI("multiMon")

To create a multi-monitor, use multiMon.create.

create( name, monitors, [, wide [, tall [, width [, height ]]]] )

The API assumes all monitors in a multi-monitor setup are the same size. The first argument is a name for the multi-monitor, which will be the name the monitor appears under as a peripheral. The second argument is a list of monitor objects or peripheral names of monitor objects. The table is arranged much like the monitors in the multi-monitor setup. In the following example:

local mon=multiMon.create("example",{
    {"monitor_0","monitor_1"},
    {"monitor_2","monitor_3"}
})

monitor_0 is the top-left monitor, monitor_1 is the top-right monitor, monitor_2 is the buttom-left monitor, and monitor_3 is the bottom-right monitor.

wide and tall are the width and height of the multi-monitor setup in monitors, respectively. These values will be determined by the values in the monitors array if not explicitly supplied. Similarly, width and height, which are the width and height of each monitor, respectively, will be determined from the top-left monitor if not explicitly supplied.

In addition to that function, which comes from the original, I added 2 more. To remove a multi-monitor which has already been added, use multiMon.remove, supplying the name of the multi-monitor to remove as the first argument. To uninstall the multiMon API and undo all environment changes, use multiMon.uninstall(). multiMon will be removed from the environment entirely after that function finishes executing.

--[[
multiMon v1.1 - An API which allows multiple monitor objects to act as a single virtual monitor.
v1.0 Copyright (C) 2014 Shazz
v1.1 Copyright (C) 2022 MineRobber___T
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
--]]
if multiMon then
for k,v in pairs(multiMon) do
_ENV[k] = v
end
return
end
local multiMons={}
-- peripheral.getNames() override.
local oGetNames = peripheral.getNames
function peripheral.getNames()
local tbl = oGetNames()
for k,v in pairs(multiMons) do table.insert(tbl,k) end
return tbl
end
-- peripheral.isPresent() override.
local oIsPresent = peripheral.isPresent
function peripheral.isPresent(side)
if multiMons[side] then
return true
else
return oIsPresent(side)
end
end
-- peripheral.getType() override.
local oGetType = peripheral.getType
function peripheral.getType(side)
if multiMons[side] then
return 'monitor'
else
return oGetType(side)
end
end
local function startsWith(s,b)
return s:sub(1,#b)==b
end
-- peripheral.getMethods() override.
local oGetMethods = peripheral.getMethods
function peripheral.getMethods(side)
if multiMons[side] then
local retTbl = {}
for k, v in pairs(multiMons[side]) do
if not startsWith(k,"__") then
table.insert(retTbl, k)
end
end
return retTbl
else
return oGetMethods(side)
end
end
-- peripheral.call() override.
local oCall = peripheral.call
function peripheral.call(side, method, ...)
if multiMons[side] and type(method) == 'string' then
if type(multiMons[side][method]) ~= 'function' or startsWith(method,"__") then
error('No such method ' .. method, 2)
else
return multiMons[side][method](...)
end
else
return oCall(side, method, ...)
end
end
-- os.pullEventRaw() override.
local oPullEventRaw = os.pullEventRaw
function os.pullEventRaw(...)
local ev = {oPullEventRaw(...)}
if ev[1] == 'monitor_touch' then
for name, public in pairs(multiMons) do
local tall, wide, monitors, getVirtual = public.__tall, public.__wide, public.__monitors, public.__getVirtual
local toBreak = false
for monY = 1, tall do
for monX = 1, wide do
if ev[2] == peripheral.getName(monitors[monY][monX]) then
os.queueEvent('monitor_touch', name, getVirtual(monX, monY, ev[3], ev[4]))
oPullEventRaw(...) -- load-bearing pullEventRaw call; without it the monitor touch event gets queued 4 times for some ungodly reason
toBreak = true
break
end
end
if toBreak then
break
end
end
if toBreak then
break
end
end
end
return unpack(ev)
end
function uninstall()
peripheral.getNames=oGetNames
peripheral.isPresent=oIsPresent
peripheral.getType=oGetType
peripheral.getMethods=oGetMethods
peripheral.call=oCall
os.pullEventRaw=oPullEventRaw
_G.multiMon=nil
return
end
--// Function: create - Creates a virtual monitor out of multiple monitor objects stitched together.
---- Argument: name - Name of the virtual monitor.
---- Argument: monitors - A table in format: { { object [, ...] } [, ...] } contaning the monitor objects to be stitched.
---- Argument: wide - Optional (default: determined by 'monitors' argument). Amount of monitor objects stacked horizontally.
---- Argument: tall - Optional (default: determined by 'monitors' argument). Amount of monitor objects stacked vertically.
---- Argument: width - Optional (default: the width of the first object in 'monitors' argument). Width of each monitor object.
---- Argument: height - Optional (default: the height of the first object in 'monitors' argument). Height of each monitor object.
---- Return: handle - A handle to the virtual monitor created. Same as calling peripheral.wrap(name).
function create(name, monitors, _wide, _tall, _width, _height)
--// Parameters
-- Make sure all parameters are of correct type.
if
type(name) ~= 'string' or
type(monitors) ~= 'table' or
(_wide ~= nil and type(_wide) ~= 'number') or
(_tall ~= nil and type(_tall) ~= 'number') or
(_width ~= nil and type(_width) ~= 'number') or
(_height ~= nil and type(_height) ~= 'number')
then
error('Expected string, table [, table [, number [, number [, number [, number ]]]]]', 2)
end
-- Make sure 'name' doesn't already exist.
if peripheral.isPresent(name) then
error('name already exists', 2)
end
-- Make sure the 'monitors' object is in the correct format.
if
#monitors < 1 or
type(monitors[1]) ~= 'table' or
#monitors[1] < 1
then
error('monitors expected in format: { { object [, ...] } [, ...] }', 2)
end
for y=1,#monitors do
for x=1,#monitors[y] do
if type(monitors[y][x])=="string" then
if peripheral.isPresent(monitors[y][x]) then
monitors[y][x]=peripheral.wrap(monitors[y][x])
else
error(("bad peripheral %q (no such peripheral)"):format(monitors[y][x]))
end
end
if type(monitors[y][x])~="table" or (getmetatable(monitors[y][x]) or {}).__name~="peripheral" then
error(("item at monitors[%d][%d] is not a peripheral (is type %q)"):format(y,x,type(monitors[y][x])))
end
if peripheral.getType(monitors[y][x])~="monitor" then error(("bad peripheral %q (expected monitor, got %s)"):format(peripheral.getName(monitors[y][x]),peripheral.getType(monitors[y][x]))) end
end
end
-- Make sure that the 'monitors' object doesn't have an empty row and get the smallest width.
local minWide
for i = 1, #monitors do
if not minWide then
minWide = #monitors[i]
elseif #monitors[i] < minWide then
minWide = #monitors[i]
end
end
if minWide < 1 then
error('monitors has empty row(s)', 2)
end
--// Private
local wide, tall = _wide or minWide, _tall or #monitors
local monWidth, monHeight
local width, height
local isColour = (function()
-- Only set 'isColour' to true if all monitors are advanced.
local isColourCount = 0
for monY = 1, tall do
for monX = 1, wide do
if monitors[monY][monX].isColour and monitors[monY][monX].isColour() then
isColourCount = isColourCount + 1
end
end
end
if isColourCount < tall * wide then
return false
else
return true
end
end)()
local posX, posY = 1, 1
local textColour = colours.white
local backColour = colours.black
local cursorBlink = false
local textScale = 1
local emptyLine
local buffer
local log2 = math.log(2)
local tHex = {['0'] = 1, ['1'] = 2, ['2'] = 4, ['3'] = 8, ['4'] = 16, ['5'] = 32, ['6'] = 64, ['7'] = 128, ['8'] = 256, ['9'] = 512, ['A'] = 1024, ['B'] = 2048, ['C'] = 4096, ['D'] = 8192, ['E'] = 16384, ['F'] = 32768}
-- Converts from a colour (int) to an uppercase hex character.
local function colourToHex(colour)
return string.format('%X', math.floor(math.log(colour) / log2))
end
-- Converts an uppercase hex character into a colour (int).
local function hexToColour(hex)
return tHex[hex]
end
-- Mocks the Java .toString() invoked when calling mon.write with non-strings.
local function javaToString(val)
local valType = type(val)
if valType == 'string' then
return val
elseif valType == 'number' then
if val % 1 == 0 then
return tostring(val) .. '.0'
else
return tostring(val)
end
elseif valType == 'boolean' then
return tostring(val)
elseif valType == 'table' then
local function tbl(val, tables)
tables = tables or {}
if tables[val] then
return '(this Map)'
end
tables[val] = true
local str = '{'
for k, v in pairs(val) do
local key
if type(k) == 'table' then
key = tbl(k, tables)
else
key = javaToString(k)
end
if key then
local value
if type(v) == 'table' then
value = tbl(v, tables)
else
value = javaToString(v)
end
if value then
str = str .. key .. '=' .. value .. ', '
end
end
end
tables[val] = nil
if #str > 1 then
return string.sub(str, 1, -3) .. '}'
else
return str .. '}'
end
end
return tbl(val)
else
return nil
end
end
-- Calculate width & height variables and clear the buffer.
local function calculate()
monWidth = _width or select(1, monitors[1][1].getSize())
monHeight = _height or select(2, monitors[1][1].getSize())
width, height = monWidth * wide, monHeight * tall
emptyLine = string.rep(' ', width)
buffer = {}
for y = 1, height do
buffer[y] = {text = emptyLine, textColour = string.rep(colourToHex(textColour), width), backColour = string.rep(colourToHex(backColour), width)}
end
end
-- Gets the real x from the virtual x.
local function getRealX(x)
local monX = math.ceil(x / monWidth)
if x > monWidth then
if x % monWidth == 0 then
x = monWidth
else
x = x % monWidth
end
end
return monX, x
end
-- Gets the real y from the virtual y.
local function getRealY(y)
local monY = math.ceil(y / monHeight)
if y > monHeight then
if y % monHeight == 0 then
y = monHeight
else
y = y % monHeight
end
end
return monY, y
end
-- Gets the real x, y from the virtual x, y.
local function getReal(x, y)
local monX, x = getRealX(x)
local monY, y = getRealY(y)
return monitors[monY][monX], x, y
end
-- Gets the virtual x, y from the real x, y.
local function getVirtual(monX, monY, x, y)
x = x + (monWidth * (monX - 1))
y = y + (monHeight * (monY - 1))
return x, y
end
-- Updates cursor's colour (to make sure the cursor blinks in the right colour).
local function updateCursorColour()
for monY = 1, tall do
for monX = 1, wide do
monitors[monY][monX].setTextColour(textColour)
end
end
end
-- Updates cursor's position (to make sure the cursor blinks in the right position).
local function updateCursorPos()
for monY = 1, tall do
for monX = 1, wide do
monitors[monY][monX].setCursorPos(0, 0)
end
end
if posX >= 1 and posX <= width and posY >= 1 and posY <= height then
local mon, x, y = getReal(posX, posY)
mon.setCursorPos(x, y)
end
end
-- Updates cursor's blink state.
local function updateCursorBlink()
for monY = 1, tall do
for monX = 1, wide do
monitors[monY][monX].setCursorBlink(cursorBlink)
end
end
end
-- Updates the text scale.
local function updateTextScale()
for monY = 1, tall do
for monX = 1, wide do
-- Protected call in case object is not a monitor.
pcall(function()
monitors[monY][monX].setTextScale(textScale)
end)
end
end
end
-- Writes to the display.
local clock = os.clock() + 4.5
local function rawWrite(text)
-- Hackish way to prevent 'too long without yielding' on huge displays.
if os.clock() >= clock then
clock = os.clock() + 4.5
os.queueEvent('')
coroutine.yield()
end
local monY, y = getRealY(posY)
local x_ = posX
local spacesLeft = 0
repeat
text = string.sub(text, spacesLeft + 1)
local monX, x = getRealX(x_)
spacesLeft = monWidth - x + 1
x_ = x_ + spacesLeft
if not monitors[monY][monX] then
break
end
monitors[monY][monX].setTextColour(textColour)
monitors[monY][monX].setBackgroundColour(backColour)
monitors[monY][monX].setCursorPos(x, y)
monitors[monY][monX].write(text)
until #text <= spacesLeft
updateCursorColour()
updateCursorPos()
end
local function rawBlit(text,fg,bg)
-- Hackish way to prevent 'too long without yielding' on huge displays.
if os.clock() >= clock then
clock = os.clock() + 4.5
os.queueEvent('')
coroutine.yield()
end
local monY, y = getRealY(posY)
local x_ = posX
local spacesLeft = 0
repeat
text = string.sub(text, spacesLeft + 1)
fg = string.sub(fg, spacesLeft + 1)
bg = string.sub(bg, spacesLeft + 1)
local monX, x = getRealX(x_)
spacesLeft = monWidth - x + 1
x_ = x_ + spacesLeft
if not monitors[monY][monX] then
break
end
monitors[monY][monX].setTextColour(textColour)
monitors[monY][monX].setBackgroundColour(backColour)
monitors[monY][monX].setCursorPos(x, y)
monitors[monY][monX].blit(text,fg,bg)
until #text <= spacesLeft
updateCursorColour()
updateCursorPos()
end
-- Clears a line on the display.
local function rawClearLine()
local monY, y = getRealY(posY)
for monX = 1, wide do
monitors[monY][monX].setBackgroundColour(backColour)
monitors[monY][monX].setCursorPos(0, y)
monitors[monY][monX].clearLine()
end
updateCursorPos()
end
-- Clears the whole display.
local function rawClear()
for monY = 1, tall do
for monX = 1, wide do
monitors[monY][monX].setBackgroundColour(backColour)
monitors[monY][monX].clear()
end
end
end
--// Public
local public = {}
public.__wide = wide
public.__tall = tall
public.__monitors = monitors
public.__getVirtual = getVirtual
-- Mocks mon.write().
function public.write(text)
text = javaToString(text)
if not text or #text < 1 then
return
end
local endX = (posX + #text - 1)
local textLength = #text
if posY >= 1 and posY <= height and posX <= width and endX >= 1 then
local textStart, textEnd = 1, #text
if posX < 1 then
textStart = math.abs(posX) + 2
end
if endX > width then
textEnd = width - endX - 1
end
text = string.sub(text, textStart, textEnd)
rawWrite(text)
buffer[posY].text = string.sub(buffer[posY].text, 1, posX - 1) .. text .. string.sub(buffer[posY].text, endX + 1)
buffer[posY].textColour = string.sub(buffer[posY].textColour, 1, posX - 1) .. string.rep(colourToHex(textColour), #text) .. string.sub(buffer[posY].textColour, endX + 1)
buffer[posY].backColour = string.sub(buffer[posY].backColour, 1, posX - 1) .. string.rep(colourToHex(backColour), #text) .. string.sub(buffer[posY].backColour, endX + 1)
end
posX = posX + textLength
end
function public.blit(text,fg,bg)
if type(text)~="string" then error("bad argument #1 (expected string, got "..type(text)..")") end
if type(fg)~="string" then error("bad argument #1 (expected string, got "..type(fg)..")") end
if type(bg)~="string" then error("bad argument #1 (expected string, got "..type(bg)..")") end
if #text~=#fg or #text~=#bg or #fg~=#bg then error("Arguments must be the same length") end
local endX = (posX + #text - 1)
local textLength = #text
if posY >= 1 and posY <= height and posX <= width and endX >= 1 then
local textStart, textEnd = 1, #text
if posX < 1 then
textStart = math.abs(posX) + 2
end
if endX > width then
textEnd = width - endX - 1
end
text = string.sub(text, textStart, textEnd)
fg = string.sub(fg, textStart, textEnd)
bg = string.sub(bg, textStart, textEnd)
rawBlit(text,fg,bg)
buffer[posY].text = string.sub(buffer[posY].text, 1, posX - 1) .. text .. string.sub(buffer[posY].text, endX + 1)
buffer[posY].textColour = string.sub(buffer[posY].textColour, 1, posX - 1) .. fg:upper() .. string.sub(buffer[posY].textColour, endX + 1)
buffer[posY].backColour = string.sub(buffer[posY].backColour, 1, posX - 1) .. bg:upper() .. string.sub(buffer[posY].backColour, endX + 1)
end
posX = posX + textLength
end
-- Mocks mon.clearLine().
function public.clearLine()
if posY >= 1 and posY <= height then
rawClearLine()
buffer[posY].text = emptyLine
buffer[posY].backColour = string.rep(colourToHex(backColour), width)
end
end
-- Mocks mon.clear().
function public.clear()
rawClear()
for y = 1, height do
buffer[y].text = emptyLine
buffer[y].backColour = string.rep(colourToHex(backColour), width)
end
end
-- Mocks mon.scroll().
function public.scroll(n)
local function _write(y, text, sTextColour, sBackColour)
local _posX, _posY = posX, posY
local _textColour, _backColour = textColour, backColour
posY = y
local cPosX = 1
local cText = ''
local cTextColour, cBackColour = string.sub(sTextColour, 1, 1), string.sub(sBackColour, 1, 1)
for x = 1, width + 1 do
local _cTextColour = string.sub(sTextColour, x, x)
local _cBackColour = string.sub(sBackColour, x, x)
local _cText = string.sub(text, x, x)
if _cTextColour == cTextColour and _cBackColour == cBackColour then
cText = cText .. _cText
else
posX = cPosX
textColour = hexToColour(cTextColour)
backColour = hexToColour(cBackColour)
public.write(cText)
cPosX = x
cText = _cText
cTextColour, cBackColour = _cTextColour, _cBackColour
end
end
posX, posY = _posX, _posY
textColour, backColour = _textColour, _backColour
end
local function _clearLine(y)
local _posY = posY
posY = y
public.clearLine()
posY = _posY
end
if n ~= 0 then
local startY, endY, step
if n < 0 then
startY, endY, step = height, 1, -1
elseif n > 0 then
startY, endY, step = 1, height, 1
end
for y = startY, endY, step do
if buffer[y + n] then
_write(y, buffer[y + n].text, buffer[y + n].textColour, buffer[y + n].backColour)
else
_clearLine(y)
end
end
end
end
-- Mocks mon.isColour().
function public.isColour()
return isColour
end
public.isColor = public.isColour
-- Always returns true, used to identify if monitor object is a virtual monitor.
function public.isMulti()
return true
end
-- Mocks mon.getSize().
function public.getSize()
return width, height
end
-- Mocks mon.getCursorPos().
function public.getCursorPos()
return posX, posY
end
-- Mocks mon.setTextScale().
function public.setTextScale(scale)
if type(scale) ~= 'number' then
error('Expected number', 2)
end
scale = math.floor(scale * 2) / 2
if scale < 0.5 or scale > 5 then
error('Expected number in range 0.5-5', 2)
end
textScale = scale
updateTextScale()
public.clear()
calculate()
end
function public.getTextScale()
return textScale
end
-- Mocks mon.setCursorBlink().
function public.setCursorBlink(bool)
if type(bool) ~= 'boolean' then
error('Expected boolean', 2)
end
cursorBlink = bool
updateCursorBlink()
end
function public.getCursorBlink()
return cursorBlink
end
-- Mocks mon.setCursorPos().
function public.setCursorPos(x, y)
if type(x) ~= 'number' or type(y) ~= 'number' then
error('Expected number, number', 2)
end
x, y = math.floor(x), math.floor(y)
posX, posY = x, y
updateCursorPos()
end
-- Mocks mon.setTextColour().
function public.setTextColour(colour)
if type(colour) ~= 'number' then
error('Expected number', 2)
end
if colour <= 0 then
error('Colour out of range', 2)
end
_colour = tonumber(colourToHex(colour), 16)
if _colour < 0 or _colour > 15 then
error('Colour out of range', 2)
end
textColour = colour
updateCursorColour()
end
public.setTextColor = public.setTextColour
function public.getTextColour()
return textColour
end
public.getTextColor = public.getTextColour
-- Mocks mon.setBackgroundColour().
function public.setBackgroundColour(colour)
if type(colour) ~= 'number' then
error('Expected number', 2)
end
if colour <= 0 then
error('Colour out of range', 2)
end
_colour = tonumber(colourToHex(colour), 16)
if _colour < 0 or _colour > 15 then
error('Colour out of range', 2)
end
backColour = colour
end
public.setBackgroundColor = public.setBackgroundColour
function public.getBackgroundColour()
return backColour
end
public.getBackgroundColor = public.getBackgroundColour
function public.setPaletteColour(colour,...)
if type(colour) ~= 'number' then
error('Expected number', 2)
end
if colour <= 0 then
error('Colour out of range', 2)
end
_colour = tonumber(colourToHex(colour), 16)
if _colour < 0 or _colour > 15 then
error('Colour out of range', 2)
end
local tArgs = {...}
if #tArgs==1 then tArgs=table.pack(colours.unpackRGB(tArgs[1])) end
if #tArgs<3 then error("bad argument #4 (expected number, got nil)") end
if #tArgs>3 then for i=#tArgs,4,-1 do tArgs[i]=nil end end
if not isColour then
local avg = (tArgs[1]+tArgs[2]+tArgs[3])/3
for i=1,3 do tArgs[i]=avg end
end
for monY = 1, tall do
for monX = 1, wide do
monitors[monY][monX].setPaletteColour(colour,table.unpack(tArgs))
end
end
end
public.setPaletteColor = public.setPaletteColour
function public.getPaletteColour(colour)
if type(colour) ~= 'number' then
error('Expected number', 2)
end
if colour <= 0 then
error('Colour out of range', 2)
end
_colour = tonumber(colourToHex(colour), 16)
if _colour < 0 or _colour > 15 then
error('Colour out of range', 2)
end
return monitors[1][1].getPaletteColour(color)
end
public.getPaletteColor = public.getPaletteColour
multiMons[name]=public
updateTextScale()
calculate()
return peripheral.wrap(name)
end
function remove(name)
multiMons[name]=nil
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment