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