Files
Basalt2/release/project.lua
2025-02-09 13:41:10 +00:00

1315 lines
37 KiB
Lua

local project = {}
local Render = {}
Render.__index = Render
local colorChars = require("libraries/colorHex")
local log = require("log")
function Render.new(terminal)
local self = setmetatable({}, Render)
self.terminal = terminal
self.width, self.height = terminal.getSize()
self.buffer = {
text = {},
fg = {},
bg = {},
changed = {}
}
for y=1, self.height do
self.buffer.text[y] = string.rep(" ", self.width)
self.buffer.fg[y] = string.rep("0", self.width)
self.buffer.bg[y] = string.rep("f", self.width)
self.buffer.changed[y] = false
end
return self
end
function Render:blit(x, y, text, fg, bg)
if y < 1 or y > self.height then return self end
if(#text ~= #fg or #text ~= #bg)then
error("Text, fg, and bg must be the same length")
end
self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text)
self.buffer.fg[y] = self.buffer.fg[y]:sub(1,x-1) .. fg .. self.buffer.fg[y]:sub(x+#fg)
self.buffer.bg[y] = self.buffer.bg[y]:sub(1,x-1) .. bg .. self.buffer.bg[y]:sub(x+#bg)
self.buffer.changed[y] = true
return self
end
function Render:multiBlit(x, y, width, height, text, fg, bg)
if y < 1 or y > self.height then return self end
if(#text ~= #fg or #text ~= #bg)then
error("Text, fg, and bg must be the same length")
end
text = text:rep(width)
fg = fg:rep(width)
bg = bg:rep(width)
for dy=0, height-1 do
local cy = y + dy
if cy >= 1 and cy <= self.height then
self.buffer.text[cy] = self.buffer.text[cy]:sub(1,x-1) .. text .. self.buffer.text[cy]:sub(x+#text)
self.buffer.fg[cy] = self.buffer.fg[cy]:sub(1,x-1) .. fg .. self.buffer.fg[cy]:sub(x+#fg)
self.buffer.bg[cy] = self.buffer.bg[cy]:sub(1,x-1) .. bg .. self.buffer.bg[cy]:sub(x+#bg)
self.buffer.changed[cy] = true
end
end
return self
end
function Render:textFg(x, y, text, fg)
if y < 1 or y > self.height then return self end
fg = colorChars[fg] or "0"
self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text)
self.buffer.fg[y] = self.buffer.fg[y]:sub(1,x-1) .. fg:rep(#text) .. self.buffer.fg[y]:sub(x+#text)
self.buffer.changed[y] = true
return self
end
function Render:text(x, y, text)
if y < 1 or y > self.height then return self end
self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text)
self.buffer.changed[y] = true
return self
end
function Render:fg(x, y, fg)
if y < 1 or y > self.height then return self end
self.buffer.fg[y] = self.buffer.fg[y]:sub(1,x-1) .. fg .. self.buffer.fg[y]:sub(x+#fg)
self.buffer.changed[y] = true
return self
end
function Render:bg(x, y, bg)
if y < 1 or y > self.height then return self end
self.buffer.bg[y] = self.buffer.bg[y]:sub(1,x-1) .. bg .. self.buffer.bg[y]:sub(x+#bg)
self.buffer.changed[y] = true
return self
end
function Render:clear(bg)
local bgChar = colorChars[bg] or "f"
for y=1, self.height do
self.buffer.text[y] = string.rep(" ", self.width)
self.buffer.fg[y] = string.rep("0", self.width)
self.buffer.bg[y] = string.rep(bgChar, self.width)
self.buffer.changed[y] = true
end
return self
end
function Render:render()
for y=1, self.height do
if self.buffer.changed[y] then
self.terminal.setCursorPos(1, y)
self.terminal.blit(
self.buffer.text[y],
self.buffer.fg[y],
self.buffer.bg[y]
)
self.buffer.changed[y] = false
end
end
return self
end
function Render:clearArea(x, y, width, height, bg)
local bgChar = colorChars[bg] or "f"
for dy=0, height-1 do
local cy = y + dy
if cy >= 1 and cy <= self.height then
local text = string.rep(" ", width)
local color = string.rep(bgChar, width)
self:blit(x, cy, text, "0", bgChar)
end
end
return self
end
function Render:getSize()
return self.width, self.height
end
return Renderlocal args = table.pack(...)
local dir = fs.getDir(args[2] or "basalt")
if(dir==nil)then
error("Unable to find directory "..args[2].." please report this bug to our discord.")
end
local log = require("log")
local ElementManager = {}
ElementManager._elements = {}
ElementManager._plugins = {}
local elementsDirectory = fs.combine(dir, "elements")
local pluginsDirectory = fs.combine(dir, "plugins")
log.info("Loading elements from "..elementsDirectory)
if fs.exists(elementsDirectory) then
for _, file in ipairs(fs.list(elementsDirectory)) do
local name = file:match("(.+).lua")
if name then
log.debug("Found element: "..name)
ElementManager._elements[name] = {
class = nil,
plugins = {},
loaded = false
}
end
end
end
function ElementManager.extendMethod(element, methodName, newMethod, originalMethod)
if not originalMethod then
element[methodName] = newMethod
return
end
element[methodName] = function(self, ...)
if newMethod.before then
newMethod.before(self, ...)
end
local results
if newMethod.override then
results = {newMethod.override(self, originalMethod, ...)}
else
results = {originalMethod(self, ...)}
end
if newMethod.after then
newMethod.after(self, ...)
end
return table.unpack(results)
end
end
function ElementManager.loadPlugin(name)
local plugin = require("plugins/"..name)
-- Apply plugin to each targeted element
for elementName, pluginData in pairs(plugin) do
local element = ElementManager._elements[elementName]
if element then
-- Register properties
if pluginData.properties then
element.class.initialize(elementName.."Plugin")
for propName, config in pairs(pluginData.properties) do
element.class.registerProperty(propName, config)
end
end
-- Register/extend methods
if pluginData.methods then
for methodName, methodData in pairs(pluginData.methods) do
ElementManager.extendMethod(
element.class,
methodName,
methodData,
element.class[methodName]
)
end
end
end
end
end
function ElementManager.loadElement(name)
if not ElementManager._elements[name].loaded then
local element = require("elements/"..name)
ElementManager._elements[name] = {
class = element,
plugins = element.plugins,
loaded = true
}
log.debug("Loaded element: "..name)
-- Load element's required plugins
if element.requires then
for pluginName, _ in pairs(element.requires) do
--ElementManager.loadPlugin(pluginName)
end
end
end
end
function ElementManager.registerPlugin(name, plugin)
if not plugin.provides then
error("Plugin must specify what it provides")
end
ElementManager._plugins[name] = plugin
end
function ElementManager.getElement(name)
if not ElementManager._elements[name].loaded then
ElementManager.loadElement(name)
end
return ElementManager._elements[name].class
end
function ElementManager.getElementList()
return ElementManager._elements
end
function ElementManager.generateId()
return string.format('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
math.random(0, 0xffff), math.random(0, 0xffff), math.random(0, 0xffff),
math.random(0, 0x0fff) + 0x4000, math.random(0, 0x3fff) + 0x8000,
math.random(0, 0xffff), math.random(0, 0xffff), math.random(0, 0xffff))
end
return ElementManager
local args = {...}
local basaltPath = args[1] or "basalt"
local defaultPath = package.path
local format = "path;/path/?.lua;/path/?/init.lua;"
local main = format:gsub("path", basaltPath)
package.path = main.."rom/?"
local function errorHandler(err)
local errorManager = require("errorManager")
errorManager.header = "Basalt Loading Error"
errorManager.error(err)
end
-- Use xpcall with error handler
local ok, result = pcall(require, "main")
if not ok then
errorHandler(result)
else
return result
endlocal Container = require("elements/Container")
local Render = require("render")
local BaseFrame = setmetatable({}, Container)
BaseFrame.__index = BaseFrame
---@diagnostic disable-next-line: duplicate-set-field
function BaseFrame.new(id, basalt)
local self = setmetatable({}, BaseFrame):__init()
self:init(id, basalt)
self.terminal = term.current() -- change to :setTerm later!!
self._render = Render.new(self.terminal)
self._renderUpdate = true
local width, height = self.terminal.getSize()
self.set("width", width)
self.set("height", height)
self.set("background", colors.red)
self.set("type", "BaseFrame")
return self
end
function BaseFrame:multiBlit(x, y, width, height, text, fg, bg)
self._render:multiBlit(x, y, width, height, text, fg, bg)
end
function BaseFrame:textFg(x, y, text, fg)
self._render:textFg(x, y, text, fg)
end
---@diagnostic disable-next-line: duplicate-set-field
function BaseFrame:render()
if(self._renderUpdate) then
Container.render(self)
self._render:render()
self._renderUpdate = false
end
end
return BaseFramelocal VisualElement = require("elements/VisualElement")
local getCenteredPosition = require("libraries/utils").getCenteredPosition
local Button = setmetatable({}, VisualElement)
Button.__index = Button
Button.defineProperty(Button, "text", {default = "Button", type = "string"})
Button.listenTo(Button, "mouse_click")
---@diagnostic disable-next-line: duplicate-set-field
function Button.new(id, basalt)
local self = setmetatable({}, Button):__init()
self:init(id, basalt)
self.set("type", "Button")
self.set("width", 10)
self.set("height", 3)
self.set("z", 5)
return self
end
---@diagnostic disable-next-line: duplicate-set-field
function Button:render()
VisualElement.render(self)
local text = self.get("text")
local xO, yO = getCenteredPosition(text, self.get("width"), self.get("height"))
self:textFg(xO, yO, text, self.get("foreground"))
end
return Buttonlocal BaseElement = require("elements/BaseElement")
local VisualElement = setmetatable({}, BaseElement)
VisualElement.__index = VisualElement
local tHex = require("libraries/colorHex")
BaseElement.defineProperty(VisualElement, "x", {default = 1, type = "number", canTriggerRender = true})
BaseElement.defineProperty(VisualElement, "y", {default = 1, type = "number", canTriggerRender = true})
BaseElement.defineProperty(VisualElement, "z", {default = 1, type = "number", canTriggerRender = true, setter = function(self, value)
self.basalt.LOGGER.debug("Setting z to " .. value)
if self.parent then
self.parent:sortChildren()
end
return value
end})
BaseElement.defineProperty(VisualElement, "width", {default = 1, type = "number", canTriggerRender = true})
BaseElement.defineProperty(VisualElement, "height", {default = 1, type = "number", canTriggerRender = true})
BaseElement.defineProperty(VisualElement, "background", {default = colors.black, type = "number", canTriggerRender = true})
BaseElement.defineProperty(VisualElement, "foreground", {default = colors.white, type = "number", canTriggerRender = true})
BaseElement.defineProperty(VisualElement, "clicked", {default = false, type = "boolean"})
---@diagnostic disable-next-line: duplicate-set-field
function VisualElement.new(id, basalt)
local self = setmetatable({}, VisualElement):__init()
self:init(id, basalt)
self.set("type", "VisualElement")
return self
end
function VisualElement:multiBlit(x, y, width, height, text, fg, bg)
x = x + self.get("x") - 1
y = y + self.get("y") - 1
self.parent:multiBlit(x, y, width, height, text, fg, bg)
end
function VisualElement:textFg(x, y, text, fg)
x = x + self.get("x") - 1
y = y + self.get("y") - 1
self.parent:textFg(x, y, text, fg)
end
function VisualElement:isInBounds(x, y)
local xPos, yPos = self.get("x"), self.get("y")
local width, height = self.get("width"), self.get("height")
return x >= xPos and x <= xPos + width - 1 and
y >= yPos and y <= yPos + height - 1
end
function VisualElement:mouse_click(button, x, y)
if self:isInBounds(x, y) then
self.set("clicked", true)
self:fireEvent("mouse_click", button, x, y)
return true
end
end
function VisualElement:mouse_up(button, x, y)
if self:isInBounds(x, y) then
self.set("clicked", false)
self:fireEvent("mouse_up", button, x, y)
return true
end
self:fireEvent("mouse_release", button, x, y)
end
function VisualElement:mouse_release()
self.set("clicked", false)
end
function VisualElement:handleEvent(event, ...)
if(self[event])then
return self[event](self, ...)
end
end
--- Returns the absolute position of the element or the given coordinates.
---@param x? number -- x position
---@param y? number -- y position
function VisualElement:getAbsolutePosition(x, y)
if (x == nil) or (y == nil) then
x, y = self.get("x"), self.get("y")
end
local parent = self.parent
while parent do
local px, py = parent.get("x"), parent.get("y")
x = x + px - 1
y = y + py - 1
parent = parent.parent
end
return x, y
end
--- Returns the relative position of the element or the given coordinates.
---@param x? number -- x position
---@param y? number -- y position
---@return number, number
function VisualElement:getRelativePosition(x, y)
if (x == nil) or (y == nil) then
x, y = self.get("x"), self.get("y")
end
local parentX, parentY = 1, 1
if self.parent then
parentX, parentY = self.parent:getRelativePosition()
end
local elementX = self.get("x")
local elementY = self.get("y")
return x - (elementX - 1) - (parentX - 1),
y - (elementY - 1) - (parentY - 1)
end
---@diagnostic disable-next-line: duplicate-set-field
function VisualElement:render()
local width, height = self.get("width"), self.get("height")
self:multiBlit(1, 1, width, height, " ", tHex[self.get("foreground")], tHex[self.get("background")])
end
return VisualElementlocal PropertySystem = require("propertySystem") -- muss geändert werden.
local BaseElement = setmetatable({}, PropertySystem)
BaseElement.__index = BaseElement
BaseElement._events = {}
BaseElement.defineProperty(BaseElement, "type", {default = "BaseElement", type = "string"})
BaseElement.defineProperty(BaseElement, "eventCallbacks", {default = {}, type = "table"})
function BaseElement.new(id, basalt)
local self = setmetatable({}, BaseElement):__init()
self:init(id, basalt)
self.set("type", "BaseElement")
return self
end
function BaseElement:init(id, basalt)
self.id = id
self.basalt = basalt
self._registeredEvents = {}
if BaseElement._events then
for event in pairs(BaseElement._events) do
self._registeredEvents[event] = true
local handlerName = "on" .. event:gsub("_(%l)", function(c)
return c:upper()
end):gsub("^%l", string.upper)
self[handlerName] = function(self, ...)
self:registerCallback(event, ...)
end
end
end
return self
end
function BaseElement.listenTo(class, eventName)
if not class._events then
class._events = {}
end
class._events[eventName] = true
end
function BaseElement:listenEvent(eventName, enable)
enable = enable ~= false
if enable ~= (self._registeredEvents[eventName] or false) then
if enable then
self._registeredEvents[eventName] = true
if self.parent then
self.parent:registerChildEvent(self, eventName)
end
else
self._registeredEvents[eventName] = nil
if self.parent then
self.parent:unregisterChildEvent(self, eventName)
end
end
end
return self
end
function BaseElement:registerCallback(event, callback)
if not self._registeredEvents[event] then
self:listenEvent(event, true)
end
if not self._values.eventCallbacks[event] then
self._values.eventCallbacks[event] = {}
end
table.insert(self._values.eventCallbacks[event], callback)
return self
end
function BaseElement:fireEvent(event, ...)
if self._values.eventCallbacks[event] then
for _, callback in ipairs(self._values.eventCallbacks[event]) do
local result = callback(self, ...)
return result
end
end
return self
end
function BaseElement:updateRender()
if(self.parent) then
self.parent:updateRender()
else
self._renderUpdate = true
end
end
return BaseElementlocal VisualElement = require("elements/VisualElement")
local elementManager = require("elementManager")
local expect = require("libraries/expect")
local max = math.max
local Container = setmetatable({}, VisualElement)
Container.__index = Container
Container.defineProperty(Container, "children", {default = {}, type = "table"})
Container.defineProperty(Container, "childrenEvents", {default = {}, type = "table"})
Container.defineProperty(Container, "eventListenerCount", {default = {}, type = "table"})
for k, _ in pairs(elementManager:getElementList()) do
local capitalizedName = k:sub(1,1):upper() .. k:sub(2)
--if not capitalizedName == "BaseFrame" then
Container["add"..capitalizedName] = function(self, ...)
expect(1, self, "table")
local element = self.basalt.create(k, ...)
self.basalt.LOGGER.debug(capitalizedName.." created with ID: " .. element.id)
self:addChild(element)
return element
end
--end
end
---@diagnostic disable-next-line: duplicate-set-field
function Container.new(id, basalt)
local self = setmetatable({}, Container):__init()
self:init(id, basalt)
self.set("type", "Container")
return self
end
function Container:addChild(child)
if child == self then
error("Cannot add container to itself")
end
local childZ = child.get("z")
local pos = 1
for i, existing in ipairs(self._values.children) do
if existing.get("z") > childZ then
break
end
pos = i + 1
end
table.insert(self._values.children, pos, child)
child.parent = self
self:registerChildrenEvents(child)
return self
end
function Container:sortChildren()
table.sort(self._values.children, function(a, b)
return a.get("z") < b.get("z")
end)
end
function Container:sortChildrenEvents(eventName)
if self._values.childrenEvents[eventName] then
table.sort(self._values.childrenEvents[eventName], function(a, b)
return a.get("z") > b.get("z")
end)
end
end
function Container:registerChildrenEvents(child)
for event in pairs(child._registeredEvents) do
self:registerChildEvent(child, event)
end
end
function Container:registerChildEvent(child, eventName)
if not self._values.childrenEvents[eventName] then
self._values.childrenEvents[eventName] = {}
self._values.eventListenerCount[eventName] = 0
if self.parent then
self.parent:registerChildEvent(self, eventName)
end
end
for _, registeredChild in ipairs(self._values.childrenEvents[eventName]) do
if registeredChild == child then
return
end
end
local childZ = child.get("z")
local pos = 1
for i, existing in ipairs(self._values.childrenEvents[eventName]) do
if existing.get("z") < childZ then
break
end
pos = i + 1
end
table.insert(self._values.childrenEvents[eventName], pos, child)
self._values.eventListenerCount[eventName] = self._values.eventListenerCount[eventName] + 1
end
function Container:removeChildrenEvents(child)
for event in pairs(child._registeredEvents) do
self:unregisterChildEvent(child, event)
end
end
function Container:unregisterChildEvent(child, eventName)
if self._values.childrenEvents[eventName] then
for i, listener in ipairs(self._values.childrenEvents[eventName]) do
if listener == child then
table.remove(self._values.childrenEvents[eventName], i)
self._values.eventListenerCount[eventName] = self._values.eventListenerCount[eventName] - 1
if self._values.eventListenerCount[eventName] <= 0 then
self._values.childrenEvents[eventName] = nil
self._values.eventListenerCount[eventName] = nil
if self.parent then
self.parent:unregisterChildEvent(self, eventName)
end
end
break
end
end
end
end
function Container:removeChild(child)
for i,v in ipairs(self.children) do
if v == child then
table.remove(self._values.children, i)
child.parent = nil
break
end
end
return self
end
function Container:handleEvent(event, ...)
if(VisualElement.handleEvent(self, event, ...))then
local args = {...}
if event:find("mouse_") then
local button, absX, absY = ...
local relX, relY = self:getRelativePosition(absX, absY)
args = {button, relX, relY}
end
if self._values.childrenEvents[event] then
for _, child in ipairs(self._values.childrenEvents[event]) do
if(child:handleEvent(event, table.unpack(args)))then
return true
end
end
end
end
end
--[[function Container:mouse_click(button, x, y)
if VisualElement.mouse_click(self, button, x, y) then
if self._values.childrenEvents.mouse_click then
for _, child in ipairs(self._values.childrenEvents.mouse_click) do
if child:mouse_click(button, x, y) then
return true
end
end
end
end
end]]
function Container:multiBlit(x, y, width, height, text, fg, bg)
local w, h = self.get("width"), self.get("height")
width = x < 1 and math.min(width + x - 1, w) or math.min(width, math.max(0, w - x + 1))
height = y < 1 and math.min(height + y - 1, h) or math.min(height, math.max(0, h - y + 1))
if width <= 0 or height <= 0 then return end
VisualElement.multiBlit(self, math.max(1, x), math.max(1, y), width, height, text, fg, bg)
end
function Container:textFg(x, y, text, fg)
local w, h = self.get("width"), self.get("height")
if y < 1 or y > h then return end
local textStart = x < 1 and (2 - x) or 1
local textLen = math.min(#text - textStart + 1, w - math.max(1, x) + 1)
if textLen <= 0 then return end
VisualElement.textFg(self, math.max(1, x), math.max(1, y), text:sub(textStart, textStart + textLen - 1), fg)
end
---@diagnostic disable-next-line: duplicate-set-field
function Container:render()
VisualElement.render(self)
for _, child in ipairs(self._values.children) do
if child == self then
self.basalt.LOGGER.error("CIRCULAR REFERENCE DETECTED!")
return
end
child:render()
end
end
return Containerlocal Container = require("elements/Container")
local Frame = setmetatable({}, Container)
Frame.__index = Frame
---@diagnostic disable-next-line: duplicate-set-field
function Frame.new(id, basalt)
local self = setmetatable({}, Frame):__init()
self:init(id, basalt)
self.set("width", 12)
self.set("height", 6)
self.set("background", colors.blue)
self.set("type", "Frame")
self.set("z", 10)
return self
end
return Framelocal LOGGER = require("log")
local errorHandler = {
tracebackEnabled = true,
header = "Basalt Error"
}
local function coloredPrint(message, color)
term.setTextColor(color)
print(message)
term.setTextColor(colors.white)
end
function errorHandler.error(errMsg)
term.setBackgroundColor(colors.black)
term.clear()
term.setCursorPos(1, 1)
coloredPrint(errorHandler.header..":", colors.red)
print()
local level = 2
local topInfo
while true do
local info = debug.getinfo(level, "Sl")
if not info then break end
topInfo = info
level = level + 1
end
local info = topInfo or debug.getinfo(2, "Sl")
local fileName = info.source:sub(2)
local lineNumber = info.currentline
local errorMessage = errMsg
if(errorHandler.tracebackEnabled)then
local stackTrace = debug.traceback()
if stackTrace then
--coloredPrint("Stack traceback:", colors.gray)
for line in stackTrace:gmatch("[^\r\n]+") do
local fileNameInTraceback, lineNumberInTraceback = line:match("([^:]+):(%d+):")
if fileNameInTraceback and lineNumberInTraceback then
term.setTextColor(colors.lightGray)
term.write(fileNameInTraceback)
term.setTextColor(colors.gray)
term.write(":")
term.setTextColor(colors.lightBlue)
term.write(lineNumberInTraceback)
term.setTextColor(colors.gray)
line = line:gsub(fileNameInTraceback .. ":" .. lineNumberInTraceback, "")
end
coloredPrint(line, colors.gray)
end
print()
end
end
if fileName and lineNumber then
term.setTextColor(colors.red)
term.write("Error in ")
term.setTextColor(colors.white)
term.write(fileName)
term.setTextColor(colors.red)
term.write(":")
term.setTextColor(colors.lightBlue)
term.write(lineNumber)
term.setTextColor(colors.red)
term.write(": ")
if errorMessage then
errorMessage = string.gsub(errorMessage, "stack traceback:.*", "")
if errorMessage ~= "" then
coloredPrint(errorMessage, colors.red)
else
coloredPrint("Error message not available", colors.gray)
end
else
coloredPrint("Error message not available", colors.gray)
end
local file = fs.open(fileName, "r")
if file then
local lineContent = ""
local currentLineNumber = 1
repeat
lineContent = file.readLine()
if currentLineNumber == tonumber(lineNumber) then
coloredPrint("\149Line " .. lineNumber, colors.cyan)
coloredPrint(lineContent, colors.lightGray)
break
end
currentLineNumber = currentLineNumber + 1
until not lineContent
file.close()
end
end
term.setBackgroundColor(colors.black)
LOGGER.error(errMsg)
error()
end
return errorHandlerlocal floor, len = math.floor, string.len
local utils = {}
function utils.getCenteredPosition(text, totalWidth, totalHeight)
local textLength = len(text)
local x = floor((totalWidth - textLength+1) / 2 + 0.5)
local y = floor(totalHeight / 2 + 0.5)
return x, y
end
function utils.deepCopy(obj)
if type(obj) ~= "table" then
return obj
end
local copy = {}
for k, v in pairs(obj) do
copy[utils.deepCopy(k)] = utils.deepCopy(v)
end
return copy
end
return utilslocal errorManager = require("errorManager")
-- Simple type checking without stack traces
local function expect(position, value, expectedType)
local valueType = type(value)
if expectedType == "element" then
if valueType == "table" and value.get("type") ~= nil then
return true
end
end
if expectedType == "color" then
if valueType == "number" and value >= 1 and value <= 32768 then
return true
end
if valueType == "string" and colors[value] then
return true
end
end
if valueType ~= expectedType then
errorManager.header = "Basalt Type Error"
errorManager.error(string.format(
"Bad argument #%d: expected %s, got %s",
position,
expectedType,
valueType
))
end
return true
end
return expectlocal colorHex = {}
for i = 0, 15 do
colorHex[2^i] = ("%x"):format(i)
colorHex[("%x"):format(i)] = 2^i
end
return colorHexlocal Log = {}
Log._logs = {}
Log._enabled = true
Log._logToFile = true
Log._logFile = "basalt.log"
fs.delete(Log._logFile)
-- Log levels
Log.LEVEL = {
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4
}
local levelMessages = {
[Log.LEVEL.DEBUG] = "Debug",
[Log.LEVEL.INFO] = "Info",
[Log.LEVEL.WARN] = "Warn",
[Log.LEVEL.ERROR] = "Error"
}
local levelColors = {
[Log.LEVEL.DEBUG] = colors.lightGray,
[Log.LEVEL.INFO] = colors.white,
[Log.LEVEL.WARN] = colors.yellow,
[Log.LEVEL.ERROR] = colors.red
}
function Log.setLogToFile(enable)
Log._logToFile = enable
end
function Log.setEnabled(enable)
Log._enabled = enable
end
local function writeToFile(message)
if Log._logToFile then
local file = io.open(Log._logFile, "a")
if file then
file:write(message.."\n")
file:close()
end
end
end
local function log(level, ...)
if not Log._enabled then return end
local timeStr = os.date("%H:%M:%S")
-- Get caller info (skip log function and Log.debug/info/etc functions)
local info = debug.getinfo(3, "Sl")
local source = info.source:match("@?(.*)")
local line = info.currentline
local levelStr = string.format("[%s:%d]", source:match("([^/\\]+)%.lua$"), line)
local levelMsg = "[" .. levelMessages[level] .. "]"
local message = ""
for i, v in ipairs(table.pack(...)) do
if i > 1 then
message = message .. " "
end
message = message .. tostring(v)
end
local fullMessage = string.format("%s %s%s %s", timeStr, levelStr, levelMsg, message)
-- File output
writeToFile(fullMessage)
-- Store in memory
table.insert(Log._logs, {
time = timeStr,
level = level,
message = message
})
end
function Log.debug(...) log(Log.LEVEL.DEBUG, ...) end
function Log.info(...) log(Log.LEVEL.INFO, ...) end
function Log.warn(...) log(Log.LEVEL.WARN, ...) end
function Log.error(...) log(Log.LEVEL.ERROR, ...) end
Log.info("Logger initialized")
return Loglocal elementManager = require("elementManager")
local expect = require("libraries/expect")
local errorManager = require("errorManager")
local basalt = {}
basalt.traceback = true
basalt._events = {}
basalt._schedule = {}
basalt._plugins = {}
basalt.LOGGER = require("log")
local mainFrame = nil
local updaterActive = false
function basalt.create(type, id)
if(id==nil)then id = elementManager.generateId() end
local element = elementManager.getElement(type).new(id, basalt)
local ok, result = pcall(require, "main")
if not ok then
errorManager(false, result)
end
return element
end
function basalt.createFrame()
local frame = basalt.create("BaseFrame")
mainFrame = frame
return frame
end
function basalt.getElementManager()
return elementManager
end
function basalt.getMainFrame()
if(mainFrame == nil)then
mainFrame = basalt.createFrame()
end
return mainFrame
end
function basalt.setActiveFrame(frame)
mainFrame = frame
return false
end
function basalt.scheduleUpdate(func)
table.insert(basalt._schedule, func)
return #basalt._schedule
end
function basalt.removeSchedule(id)
basalt._schedule[id] = nil
end
local function updateEvent(event, ...)
if(event=="terminate")then basalt.stop() end
if event:find("mouse") then
if mainFrame then
mainFrame:handleEvent(event, ...)
end
end
if event:find("key") then
if mainFrame then
mainFrame:handleEvent(event, ...)
end
end
if basalt._events[event] then
for _, callback in ipairs(basalt._events[event]) do
callback(...)
end
end
end
local function renderFrames()
if(mainFrame)then
mainFrame:render()
end
end
function basalt.update()
for k,v in pairs(basalt._schedule) do
if type(v)=="function" then
v()
end
end
end
function basalt.stop()
term.clear()
term.setCursorPos(1,1)
updaterActive = false
end
function basalt.run(isActive)
updaterActive = isActive
if(isActive==nil)then updaterActive = true end
local function f()
renderFrames()
while updaterActive do
updateEvent(os.pullEventRaw())
end
end
while updaterActive do
local ok, err = pcall(f)
if not(ok)then
errorManager.header = "Basalt Runtime Error"
errorManager.error(err)
end
end
end
basalt.autoUpdate = basalt.run
return basaltlocal deepCopy = require("libraries/utils").deepCopy
local expect = require("libraries/expect")
local PropertySystem = {}
PropertySystem.__index = PropertySystem
PropertySystem._properties = {}
function PropertySystem.defineProperty(class, name, config)
if not rawget(class, '_properties') then
class._properties = {}
end
class._properties[name] = {
type = config.type,
default = config.default,
canTriggerRender = config.canTriggerRender,
getter = config.getter,
setter = config.setter,
}
local capitalizedName = name:sub(1,1):upper() .. name:sub(2)
class["get" .. capitalizedName] = function(self)
expect(1, self, "element")
local value = self._values[name]
return config.getter and config.getter(value) or value
end
class["set" .. capitalizedName] = function(self, value)
expect(1, self, "element")
expect(2, value, config.type)
if config.setter then
value = config.setter(self, value)
end
self:_updateProperty(name, value)
return self
end
end
function PropertySystem:__init()
self._values = {}
self._observers = {}
self.set = function(name, value)
local oldValue = self._values[name]
self._values[name] = value
if(self._properties[name].setter) then
value = self._properties[name].setter(self, value)
end
if oldValue ~= value and self._observers[name] then
for _, callback in ipairs(self._observers[name]) do
callback(self, value, oldValue)
end
end
end
self.get = function(name)
return self._values[name]
end
local properties = {}
local currentClass = getmetatable(self).__index
while currentClass do
if rawget(currentClass, '_properties') then
for name, config in pairs(currentClass._properties) do
if not properties[name] then
properties[name] = config
end
end
end
currentClass = getmetatable(currentClass) and rawget(getmetatable(currentClass), '__index')
end
self._properties = properties
local originalMT = getmetatable(self)
local originalIndex = originalMT.__index
setmetatable(self, {
__index = function(t, k)
if self._properties[k] then
return self._values[k]
end
if type(originalIndex) == "function" then
return originalIndex(t, k)
else
return originalIndex[k]
end
end,
__newindex = function(t, k, v)
if self._properties[k] then
if self._properties[k].setter then
v = self._properties[k].setter(self, v)
end
self:_updateProperty(k, v)
else
rawset(t, k, v)
end
end,
__tostring = function(self)
return string.format("Object: %s (id: %s)", self._values.type, self.id)
end
})
for name, config in pairs(properties) do
if self._values[name] == nil then
if type(config.default) == "table" then
self._values[name] = deepCopy(config.default)
else
self._values[name] = config.default
end
end
end
return self
end
function PropertySystem:_updateProperty(name, value)
local oldValue = self._values[name]
if oldValue ~= value then
self._values[name] = value
if self._properties[name].canTriggerRender then
self:updateRender()
end
if self._observers[name] then
for _, callback in ipairs(self._observers[name]) do
callback(self, value, oldValue)
end
end
end
end
function PropertySystem:observe(name, callback)
self._observers[name] = self._observers[name] or {}
table.insert(self._observers[name], callback)
return self
end
return PropertySystem