diff --git a/config.lua b/config.lua index 0dc2999..b04dac6 100644 --- a/config.lua +++ b/config.lua @@ -1,262 +1,5 @@ return { ["files"] = { - ["libraries/expect.lua"] = "local 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 expect", - ["elements/BaseElement.lua"] = "local PropertySystem = require("propertySystem") -local uuid = require("/libraries/utils").uuid - ---- The base class for all UI elements in Basalt ---- @class BaseElement : PropertySystem -local BaseElement = setmetatable({}, PropertySystem) -BaseElement.__index = BaseElement -BaseElement._events = {} - ---- @property type string BaseElement The type identifier of the element -BaseElement.defineProperty(BaseElement, "type", {default = {"BaseElement"}, type = "string", setter=function(self, value) - if type(value) == "string" then - table.insert(self._values.type, 1, value) - return self._values.type - end - return value -end, getter = function(self, _, index) - if index~= nil and index < 1 then - return self._values.type - end - return self._values.type[index or 1] -end}) - ---- @property id string BaseElement The unique identifier for the element -BaseElement.defineProperty(BaseElement, "id", {default = "", type = "string", readonly = true}) - ---- @property name string BaseElement The name of the element -BaseElement.defineProperty(BaseElement, "name", {default = "", type = "string"}) - ---- @property eventCallbacks table {} Table containing all registered event callbacks -BaseElement.defineProperty(BaseElement, "eventCallbacks", {default = {}, type = "table"}) - ---- Registers an event that this class can listen to ---- @param class table The class to add the event to ---- @param eventName string The name of the event to register ---- @usage BaseElement.listenTo(MyClass, "mouse_click") -function BaseElement.listenTo(class, eventName) - if not class._events then - class._events = {} - end - class._events[eventName] = true -end - ---- Creates a new BaseElement instance ---- @param props table The properties to initialize the element with ---- @param basalt table The basalt instance ---- @return table The newly created BaseElement instance ---- @usage local element = BaseElement.new("myId", basalt) -function BaseElement.new() - local self = setmetatable({}, BaseElement):__init() - return self -end - ---- Initializes the BaseElement instance ---- @param props table The properties to initialize the element with ---- @param basalt table The basalt instance ---- @return table self The initialized instance -function BaseElement:init(props, basalt) - self._props = props - self._values.id = uuid() - 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, ...) - return self - end - end - end - return self -end - ---- Post initialization hook ---- @return table self The BaseElement instance -function BaseElement:postInit() - if(self._props)then - for k,v in pairs(self._props)do - self.set(k, v) - end - end - self._props = nil - return self -end - ---- Checks if the element is a specific type ---- @param type string The type to check for ---- @return boolean Whether the element is of the specified type -function BaseElement:isType(type) - for _, t in ipairs(self._values.type) do - if t == type then - return true - end - end - return false -end - ---- Enables or disables event listening for a specific event ---- @param eventName string The name of the event to listen for ---- @param enable? boolean Whether to enable or disable the event (default: true) ---- @return table self The BaseElement instance ---- @usage element:listenEvent("mouse_click", true) -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 - ---- Registers a callback function for an event ---- @param event string The event to register the callback for ---- @param callback function The callback function to register ---- @return table self The BaseElement instance ---- @usage element:registerCallback("mouse_click", function(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 - ---- Triggers an event and calls all registered callbacks ---- @param event string The event to fire ---- @param ... any Additional arguments to pass to the callbacks ---- @return table self The BaseElement instance ---- @usage element:fireEvent("mouse_click", 1, 2) -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 - ---- Handles all events ---- @param event string The event to handle ---- @vararg any The arguments for the event ---- @return boolean? handled Whether the event was handled -function BaseElement:dispatchEvent(event, ...) - if self[event] then - return self[event](self, ...) - end - return self:handleEvent(event, ...) -end - ---- The default event handler for all events ---- @param event string The event to handle ---- @vararg any The arguments for the event ---- @return boolean? handled Whether the event was handled -function BaseElement:handleEvent(event, ...) - return false -end - -function BaseElement:getBaseFrame() - if self.parent then - return self.parent:getBaseFrame() - end - return self -end - -function BaseElement:destroy() - -end - ---- Requests a render update for this element ---- @usage element:updateRender() -function BaseElement:updateRender() - if(self.parent) then - self.parent:updateRender() - else - self._renderUpdate = true - end -end - -return BaseElement", - ["plugins/pluginTemplate.lua"] = "-- Will temporary exist so that we don't lose track of how the plugin system works - -local VisualElement = {hooks={init={}}} - --- Called on Class level to define properties and setup before instance is created -function VisualElement.setup(element) - element.defineProperty(element, "testProp", {default = 5, type = "number"}) -end - --- Hooks into existing methods (you can also use init.pre or init.post) -function VisualElement.hooks.init(self) - --self.basalt.LOGGER.debug("VisualElement initialized") -end - --- Adds a new method to the class -function VisualElement:testFunc() - --self.basalt.LOGGER.debug("Hello World", self.get("testProp")) -end - -return { - VisualElement = VisualElement -} - -", ["elements/VisualElement.lua"] = "local elementManager = require("elementManager") local BaseElement = elementManager.getElement("BaseElement") local tHex = require("libraries/colorHex") @@ -492,406 +235,189 @@ function VisualElement:render() end return VisualElement", - ["log.lua"] = "local 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 Log", - ["elements/Program.lua"] = "local elementManager = require("elementManager") + ["elements/Label.lua"] = "local elementManager = require("elementManager") local VisualElement = elementManager.getElement("VisualElement") -local errorManager = require("errorManager") ---TODO: --- Rendering optimization (only render when screen changed) --- Eventsystem improvement --- Cursor is sometimes not visible on time +---@class Label : VisualElement +local Label = setmetatable({}, VisualElement) +Label.__index = Label ----@class Program : VisualElement -local Program = setmetatable({}, VisualElement) -Program.__index = Program +---@property text string Label Label text to be displayed +Label.defineProperty(Label, "text", {default = "Label", type = "string", setter = function(self, value) + if(type(value)=="function")then value = value() end + self.set("width", #value) + return value +end}) -Program.defineProperty(Program, "program", {default = nil, type = "table"}) -Program.defineProperty(Program, "path", {default = "", type = "string"}) -Program.defineProperty(Program, "running", {default = false, type = "boolean"}) - -Program.listenTo(Program, "key") -Program.listenTo(Program, "char") -Program.listenTo(Program, "key_up") -Program.listenTo(Program, "paste") -Program.listenTo(Program, "mouse_click") -Program.listenTo(Program, "mouse_drag") -Program.listenTo(Program, "mouse_scroll") -Program.listenTo(Program, "mouse_up") - -local BasaltProgram = {} -BasaltProgram.__index = BasaltProgram -local newPackage = dofile("rom/modules/main/cc/require.lua").make - -function BasaltProgram.new() - local self = setmetatable({}, BasaltProgram) - self.env = {} - self.args = {} +--- Creates a new Label instance +--- @return Label object The newly created Label instance +--- @usage local element = Label.new("myId", basalt) +function Label.new() + local self = setmetatable({}, Label):__init() + self.set("z", 3) + self.set("foreground", colors.black) + self.set("backgroundEnabled", false) return self end -function BasaltProgram:run(path, width, height) - self.window = window.create(term.current(), 1, 1, width, height, false) - local pPath = shell.resolveProgram(path) - if(pPath~=nil)then - if(fs.exists(pPath)) then - local file = fs.open(pPath, "r") - local content = file.readAll() - file.close() - local env = setmetatable(self.env, {__index=_ENV}) - env.shell = shell - env.term = self.window - env.require, env.package = newPackage(env, fs.getDir(pPath)) - env.term.current = term.current - env.term.redirect = term.redirect - env.term.native = term.native - - self.coroutine = coroutine.create(function() - local program = load(content, path, "bt", env) - if program then - local current = term.current() - term.redirect(self.window) - local result = program(path, table.unpack(self.args)) - term.redirect(current) - return result - end - end) - local current = term.current() - term.redirect(self.window) - local ok, result = coroutine.resume(self.coroutine) - term.redirect(current) - if not ok then - errorManager.header = "Basalt Program Error ".. path - errorManager.error(result) - end - else - errorManager.header = "Basalt Program Error ".. path - errorManager.error("File not found") - end - else - errorManager.header = "Basalt Program Error" - errorManager.error("Program "..path.." not found") - end -end - -function BasaltProgram:resize(width, height) - self.window.reposition(1, 1, width, height) -end - -function BasaltProgram:resume(event, ...) - if self.coroutine==nil or coroutine.status(self.coroutine)=="dead" then return end - if(self.filter~=nil)then - if(event~=self.filter)then return end - self.filter=nil - end - local current = term.current() - term.redirect(self.window) - local ok, result = coroutine.resume(self.coroutine, event, ...) - term.redirect(current) - - if ok then - self.filter = result - else - errorManager.header = "Basalt Program Error" - errorManager.error(result) - end - return ok, result -end - -function BasaltProgram:stop() - -end - ---- Creates a new Program instance ---- @return Program object The newly created Program instance ---- @usage local element = Program.new("myId", basalt) -function Program.new() - local self = setmetatable({}, Program):__init() - self.set("z", 5) - self.set("width", 30) - self.set("height", 12) - return self -end - -function Program:init(props, basalt) +function Label:init(props, basalt) VisualElement.init(self, props, basalt) - self.set("type", "Program") + self.set("type", "Label") end -function Program:execute(path) - self.set("path", path) - self.set("running", true) - local program = BasaltProgram.new() - self.set("program", program) - program:run(path, self.get("width"), self.get("height")) +function Label:render() + VisualElement.render(self) + local text = self.get("text") + self:textFg(1, 1, text, self.get("foreground")) +end + +return Label", + ["elements/Tree.lua"] = "local VisualElement = require("elements/VisualElement") +local tHex = require("libraries/colorHex") + +---@class Tree : VisualElement +local Tree = setmetatable({}, VisualElement) +Tree.__index = Tree + +Tree.defineProperty(Tree, "nodes", {default = {}, type = "table", canTriggerRender = true}) +Tree.defineProperty(Tree, "selectedNode", {default = nil, type = "table", canTriggerRender = true}) +Tree.defineProperty(Tree, "expandedNodes", {default = {}, type = "table", canTriggerRender = true}) +Tree.defineProperty(Tree, "scrollOffset", {default = 0, type = "number", canTriggerRender = true}) +Tree.defineProperty(Tree, "nodeColor", {default = colors.white, type = "number"}) +Tree.defineProperty(Tree, "selectedColor", {default = colors.lightBlue, type = "number"}) + +Tree.listenTo(Tree, "mouse_click") +Tree.listenTo(Tree, "mouse_scroll") + +function Tree.new() + local self = setmetatable({}, Tree):__init() + self.set("width", 30) + self.set("height", 10) + self.set("z", 5) + return self +end + +function Tree:init(props, basalt) + VisualElement.init(self, props, basalt) + self.set("type", "Tree") + return self +end + +function Tree:setNodes(nodes) + self.set("nodes", nodes) + if #nodes > 0 then + self.get("expandedNodes")[nodes[1]] = true + end + return self +end + +function Tree:expandNode(node) + self.get("expandedNodes")[node] = true self:updateRender() return self end -function Program:dispatchEvent(event, ...) - local program = self.get("program") - local result = VisualElement.dispatchEvent(self, event, ...) - if program then - program:resume(event, ...) - if(self.get("focused"))then - local cursorBlink = program.window.getCursorBlink() - local cursorX, cursorY = program.window.getCursorPos() - self:setCursor(cursorX, cursorY, cursorBlink) +function Tree:collapseNode(node) + self.get("expandedNodes")[node] = nil + self:updateRender() + return self +end + +function Tree:toggleNode(node) + if self.get("expandedNodes")[node] then + self:collapseNode(node) + else + self:expandNode(node) + end + return self +end + +local function flattenTree(nodes, expandedNodes, level, result) + result = result or {} + level = level or 0 + + for _, node in ipairs(nodes) do + table.insert(result, {node = node, level = level}) + if expandedNodes[node] and node.children then + flattenTree(node.children, expandedNodes, level + 1, result) end - self:updateRender() end return result end -function Program:focus() - if(VisualElement.focus(self))then - local program = self.get("program") - if program then - local cursorBlink = program.window.getCursorBlink() - local cursorX, cursorY = program.window.getCursorPos() - self:setCursor(cursorX, cursorY, cursorBlink) +function Tree:mouse_click(button, x, y) + if not VisualElement.mouse_click(self, button, x, y) then return false end + + local relX, relY = self:getRelativePosition(x, y) + local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes")) + local visibleIndex = relY + self.get("scrollOffset") + + if flatNodes[visibleIndex] then + local nodeInfo = flatNodes[visibleIndex] + local node = nodeInfo.node + + if relX <= nodeInfo.level * 2 + 2 then + self:toggleNode(node) end + + self.set("selectedNode", node) + self:fireEvent("node_select", node) end + return true end -function Program:render() +function Tree:onSelect(callback) + self:registerCallback("node_select", callback) + return self +end + +function Tree:mouse_scroll(direction) + local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes")) + local maxScroll = math.max(0, #flatNodes - self.get("height")) + local newScroll = math.min(maxScroll, math.max(0, self.get("scrollOffset") + direction)) + + self.set("scrollOffset", newScroll) + return true +end + +function Tree:render() VisualElement.render(self) - local program = self.get("program") - if program then - local _, height = program.window.getSize() - for y = 1, height do - local text, fg, bg = program.window.getLine(y) - if text then - self:blit(1, y, text, fg, bg) + + local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes")) + local height = self.get("height") + local selectedNode = self.get("selectedNode") + local expandedNodes = self.get("expandedNodes") + local scrollOffset = self.get("scrollOffset") + + for y = 1, height do + local nodeInfo = flatNodes[y + scrollOffset] + if nodeInfo then + local node = nodeInfo.node + local level = nodeInfo.level + local indent = string.rep(" ", level) + + -- Expand/Collapse Symbol + local symbol = " " + if node.children and #node.children > 0 then + symbol = expandedNodes[node] and "\31" or "\16" end - end - end -end -return Program", - ["plugins/state.lua"] = "local PropertySystem = require("propertySystem") -local errorManager = require("errorManager") -local BaseElement = {} - -function BaseElement.setup(element) - element.defineProperty(element, "states", {default = {}, type = "table"}) - element.defineProperty(element, "computedStates", {default = {}, type = "table"}) - element.defineProperty(element, "stateUpdate", { - default = {key = "", value = nil, oldValue = nil}, - type = "table" - }) -end - -function BaseElement:initializeState(name, default, canTriggerRender, persist, path) - local states = self.get("states") - - if states[name] then - errorManager.error("State '" .. name .. "' already exists") - return self - end - - if persist then - local file = path or ("states/" .. self.get("name") .. "_" .. name .. ".state") - - if fs.exists(file) then - local f = fs.open(file, "r") - states[name] = { - value = textutils.unserialize(f.readAll()), - persist = true, - file = file - } - f.close() + local bg = node == selectedNode and self.get("selectedColor") or self.get("background") + local text = indent .. symbol .." " .. (node.text or "Node") + + self:blit(1, y, text .. string.rep(" ", self.get("width") - #text), + string.rep(tHex[self.get("nodeColor")], self.get("width")), + string.rep(tHex[bg], self.get("width"))) else - states[name] = { - value = default, - persist = true, - file = file, - canTriggerRender = canTriggerRender - } + -- Leere Zeile + self:blit(1, y, string.rep(" ", self.get("width")), + string.rep(tHex[self.get("foreground")], self.get("width")), + string.rep(tHex[self.get("background")], self.get("width"))) end - else - states[name] = { - value = default, - canTriggerRender = canTriggerRender - } end - return self end -function BaseElement:setState(name, value) - local states = self.get("states") - if not states[name] then - error("State '"..name.."' not initialized") - end - - local oldValue = states[name].value - states[name].value = value - - if states[name].persist then - local dir = fs.getDir(states[name].file) - if not fs.exists(dir) then - fs.makeDir(dir) - end - local f = fs.open(states[name].file, "w") - f.write(textutils.serialize(value)) - f.close() - end - - if states[name].canTriggerRender then - self:updateRender() - end - - self.set("stateUpdate", { - key = name, - value = value, - oldValue = oldValue - }) - return self -end - -function BaseElement:getState(name) - local states = self.get("states") - if not states[name] then - errorManager.error("State '"..name.."' not initialized") - end - return states[name].value -end - -function BaseElement:computed(key, computeFn) - local computed = self.get("computedStates") - computed[key] = setmetatable({}, { - __call = function() - return computeFn(self) - end - }) - return self -end - -function BaseElement:shareState(stateKey, ...) - local value = self:getState(stateKey) - - for _, element in ipairs({...}) do - if element.get("states")[stateKey] then - errorManager.error("Cannot share state '" .. stateKey .. "': Target element already has this state") - return self - end - - element:initializeState(stateKey, value) - - self:observe("stateUpdate", function(self, update) - if update.key == stateKey then - element:setState(stateKey, update.value) - end - end) - end - return self -end - -function BaseElement:onStateChange(stateName, callback) - if not self.get("states")[stateName] then - errorManager.error("Cannot observe state '" .. stateName .. "': State not initialized") - return self - end - - self:observe("stateUpdate", function(self, update) - if update.key == stateName then - callback(self, update.value, update.oldValue) - end - end) - return self -end - -return { - BaseElement = BaseElement -} +return Tree ", ["elements/Table.lua"] = "local VisualElement = require("elements/VisualElement") local tHex = require("libraries/colorHex") @@ -1074,6 +600,292 @@ function Table:render() end return Table", + ["elements/Input.lua"] = "local VisualElement = require("elements/VisualElement") +local tHex = require("libraries/colorHex") + +---@class Input : VisualElement +local Input = setmetatable({}, VisualElement) +Input.__index = Input + +---@property text string Input - text to be displayed +Input.defineProperty(Input, "text", {default = "", type = "string", canTriggerRender = true}) + +---@property cursorPos number Input - current cursor position +Input.defineProperty(Input, "cursorPos", {default = 1, type = "number"}) + +---@property viewOffset number Input - offset of view +Input.defineProperty(Input, "viewOffset", {default = 0, type = "number", canTriggerRender = true}) + +-- Neue Properties +Input.defineProperty(Input, "maxLength", {default = nil, type = "number"}) +Input.defineProperty(Input, "placeholder", {default = "asd", type = "string"}) +Input.defineProperty(Input, "placeholderColor", {default = colors.gray, type = "number"}) +Input.defineProperty(Input, "focusedColor", {default = colors.blue, type = "number"}) +Input.defineProperty(Input, "pattern", {default = nil, type = "string"}) + +Input.listenTo(Input, "mouse_click") +Input.listenTo(Input, "key") +Input.listenTo(Input, "char") + +--- Creates a new Input instance +--- @return Input object The newly created Input instance +--- @usage local element = Input.new("myId", basalt) +function Input.new() + local self = setmetatable({}, Input):__init() + self.set("width", 8) + self.set("z", 3) + return self +end + +function Input:init(props, basalt) + VisualElement.init(self, props, basalt) + self.set("type", "Input") +end + +function Input:char(char) + if not self.get("focused") then return end + local text = self.get("text") + local pos = self.get("cursorPos") + local maxLength = self.get("maxLength") + local pattern = self.get("pattern") + + if maxLength and #text >= maxLength then return end + if pattern and not char:match(pattern) then return end + + self.set("text", text:sub(1, pos-1) .. char .. text:sub(pos)) + self.set("cursorPos", pos + 1) + self:updateRender() + self:updateViewport() +end + +function Input:key(key) + if not self.get("focused") then return end + local pos = self.get("cursorPos") + local text = self.get("text") + local viewOffset = self.get("viewOffset") + local width = self.get("width") + + if key == keys.left then + if pos > 1 then + self.set("cursorPos", pos - 1) + if pos - 1 <= viewOffset then + self.set("viewOffset", math.max(0, pos - 2)) + end + end + elseif key == keys.right then + if pos <= #text then + self.set("cursorPos", pos + 1) + if pos - viewOffset >= width then + self.set("viewOffset", pos - width + 1) + end + end + elseif key == keys.backspace then + if pos > 1 then + self.set("text", text:sub(1, pos-2) .. text:sub(pos)) + self.set("cursorPos", pos - 1) + self:updateRender() + self:updateViewport() + end + end + + local relativePos = self.get("cursorPos") - self.get("viewOffset") + self:setCursor(relativePos, 1, true) +end + +function Input:focus() + VisualElement.focus(self) + self:updateRender() +end + +function Input:blur() + VisualElement.blur(self) + self:updateRender() +end + +function Input:mouse_click(button, x, y) + if VisualElement.mouse_click(self, button, x, y) then + local relX, relY = self:getRelativePosition(x, y) + local text = self.get("text") + self:setCursor(math.min(relX, #text + 1), relY, true) + self:set("cursorPos", relX + self.get("viewOffset")) + return true + end +end + +function Input:updateViewport() + local width = self.get("width") + local cursorPos = self.get("cursorPos") + local viewOffset = self.get("viewOffset") + local textLength = #self.get("text") + + if cursorPos - viewOffset > width then + self.set("viewOffset", cursorPos - width) + elseif cursorPos <= viewOffset then + + self.set("viewOffset", math.max(0, cursorPos - 1)) + end + + if viewOffset > textLength - width then + self.set("viewOffset", math.max(0, textLength - width)) + end +end + +function Input:render() + local text = self.get("text") + local viewOffset = self.get("viewOffset") + local width = self.get("width") + local placeholder = self.get("placeholder") + local focusedColor = self.get("focusedColor") + local focused = self.get("focused") + local width, height = self.get("width"), self.get("height") + self:multiBlit(1, 1, width, height, " ", tHex[self.get("foreground")], tHex[focused and focusedColor or self.get("background")]) + + if #text == 0 and #placeholder ~= 0 and self.get("focused") == false then + self:textFg(1, 1, placeholder:sub(1, width), self.get("placeholderColor")) + return + end + + local visibleText = text:sub(viewOffset + 1, viewOffset + width) + self:textFg(1, 1, visibleText, self.get("foreground")) +end + +return Input", + ["libraries/utils.lua"] = "local 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 + +function utils.copy(obj) + local new = {} + for k,v in pairs(obj)do + new[k] = v + end + return new +end + +function utils.reverse(t) + local reversed = {} + for i = #t, 1, -1 do + table.insert(reversed, t[i]) + end + return reversed +end + +function utils.uuid() + 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 + +function utils.split(str, sep) + local parts = {} + local start = 1 + local len = len(str) + local splitIndex = 1 + + while true do + local index = str:find(sep, start, true) + if not index then + parts[splitIndex] = str:sub(start, len) + break + end + + parts[splitIndex] = str:sub(start, index - 1) + start = index + 1 + splitIndex = splitIndex + 1 + end + + return parts +end + +return utils", + ["elements/BaseFrame.lua"] = "local elementManager = require("elementManager") +local Container = elementManager.getElement("Container") +local Render = require("render") + +---@class BaseFrame : Container +local BaseFrame = setmetatable({}, Container) +BaseFrame.__index = BaseFrame + +---@property text term term nil text +BaseFrame.defineProperty(BaseFrame, "term", {default = nil, type = "table", setter = function(self, value) + if value == nil or value.setCursorPos == nil then + return value + end + self._render = Render.new(value) + self._renderUpdate = true + local width, height = value.getSize() + self.set("width", width) + self.set("height", height) + return value +end}) + +function BaseFrame.new() + local self = setmetatable({}, BaseFrame):__init() + self.set("term", term.current()) + self.set("background", colors.lightGray) + return self +end + +function BaseFrame:init(props, basalt) + Container.init(self, props, basalt) + self.set("type", "BaseFrame") +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 + +function BaseFrame:textBg(x, y, text, bg) + self._render:textBg(x, y, text, bg) +end + +function BaseFrame:blit(x, y, text, fg, bg) + self._render:blit(x, y, text, fg, bg) +end + +function BaseFrame:setCursor(x, y, blink) + local term = self.get("term") + self._render:setCursor(x, y, blink) +end + +function BaseFrame:render() + if(self._renderUpdate) then + if self._render ~= nil then + Container.render(self) + self._render:render() + self._renderUpdate = false + end + end +end + +return BaseFrame", ["init.lua"] = "local args = {...} local basaltPath = fs.getDir(args[2]) @@ -1097,200 +909,41 @@ if not ok then else return result end", - ["elements/Slider.lua"] = "local VisualElement = require("elements/VisualElement") + ["libraries/expect.lua"] = "local errorManager = require("errorManager") ----@class Slider : VisualElement -local Slider = setmetatable({}, VisualElement) -Slider.__index = Slider +-- Simple type checking without stack traces +local function expect(position, value, expectedType) + local valueType = type(value) ----@property step number 1 Current step position (1 to width/height) -Slider.defineProperty(Slider, "step", {default = 1, type = "number", canTriggerRender = true}) ----@property max number 100 Maximum value for value conversion -Slider.defineProperty(Slider, "max", {default = 100, type = "number"}) ----@property horizontal boolean true Whether the slider is horizontal -Slider.defineProperty(Slider, "horizontal", {default = true, type = "boolean", canTriggerRender = true}) ----@property barColor color color Colors for the slider bar -Slider.defineProperty(Slider, "barColor", {default = colors.gray, type = "number", canTriggerRender = true}) ----@property sliderColor color The color of the slider handle -Slider.defineProperty(Slider, "sliderColor", {default = colors.blue, type = "number", canTriggerRender = true}) - -Slider.listenTo(Slider, "mouse_click") -Slider.listenTo(Slider, "mouse_drag") -Slider.listenTo(Slider, "mouse_up") - -function Slider.new() - local self = setmetatable({}, Slider):__init() - self.set("width", 8) - self.set("height", 1) - self.set("backgroundEnabled", false) - return self -end - -function Slider:init(props, basalt) - VisualElement.init(self, props, basalt) - self.set("type", "Slider") -end - -function Slider:getValue() - local step = self.get("step") - local max = self.get("max") - local maxSteps = self.get("horizontal") and self.get("width") or self.get("height") - return math.floor((step - 1) * (max / (maxSteps - 1))) -end - -function Slider:mouse_click(button, x, y) - if button == 1 and self:isInBounds(x, y) then - local relX, relY = self:getRelativePosition(x, y) - local pos = self.get("horizontal") and relX or relY - local maxSteps = self.get("horizontal") and self.get("width") or self.get("height") - - self.set("step", math.min(maxSteps, math.max(1, pos))) - self:updateRender() - return true - end -end -Slider.mouse_drag = Slider.mouse_click - -function Slider:mouse_scroll(direction, x, y) - if self:isInBounds(x, y) then - local step = self.get("step") - local maxSteps = self.get("horizontal") and self.get("width") or self.get("height") - self.set("step", math.min(maxSteps, math.max(1, step + direction))) - self:updateRender() - return true - end -end - -function Slider:render() - VisualElement.render(self) - local width = self.get("width") - local height = self.get("height") - local horizontal = self.get("horizontal") - local step = self.get("step") - - local barChar = horizontal and "\140" or "│" - local text = string.rep(barChar, horizontal and width or height) - - if horizontal then - self:textFg(1, 1, text, self.get("barColor")) - self:textBg(step, 1, " ", self.get("sliderColor")) - else - for y = 1, height do - self:textFg(1, y, barChar, self.get("barColor")) - end - self:textFg(1, step, "\140", self.get("sliderColor")) - end -end - -return Slider", - ["errorManager.lua"] = "local 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) - if errorHandler.errorHandled then - error() - end - 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() + if expectedType == "element" then + if valueType == "table" and value.get("type") ~= nil then + return true end end - term.setBackgroundColor(colors.black) - LOGGER.error(errMsg) - errorHandler.errorHandled = true - error() + 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 errorHandler", +return expect", ["elements/Container.lua"] = "local elementManager = require("elementManager") local VisualElement = elementManager.getElement("VisualElement") local expect = require("libraries/expect") @@ -1655,6 +1308,537 @@ function Container:render() end return Container", + ["elements/Checkbox.lua"] = "local VisualElement = require("elements/VisualElement") + +---@class Checkbox : VisualElement +local Checkbox = setmetatable({}, VisualElement) +Checkbox.__index = Checkbox + +---@property checked boolean Whether checkbox is checked +Checkbox.defineProperty(Checkbox, "checked", {default = false, type = "boolean", canTriggerRender = true}) +---@property text string Label text +Checkbox.defineProperty(Checkbox, "text", {default = "", type = "string", canTriggerRender = true}) +---@property symbol string Check symbol +Checkbox.defineProperty(Checkbox, "symbol", {default = "x", type = "string"}) + +Checkbox.listenTo(Checkbox, "mouse_click") + +function Checkbox.new() + local self = setmetatable({}, Checkbox):__init() + self.set("width", 1) + self.set("height", 1) + return self +end + +function Checkbox:init(props, basalt) + VisualElement.init(self, props, basalt) + self.set("type", "Checkbox") +end + +function Checkbox:mouse_click(button, x, y) + if VisualElement.mouse_click(self, button, x, y) then + self.set("checked", not self.get("checked")) + self:fireEvent("change", self.get("checked")) + return true + end +end + +function Checkbox:render() + VisualElement.render(self) + + local text = self.get("checked") and self.get("symbol") or " " + self:textFg(1, 1, "["..text.."]", self.get("foreground")) + + local label = self.get("text") + if #label > 0 then + self:textFg(4, 1, label, self.get("foreground")) + end +end + +return Checkbox", + ["elements/List.lua"] = "local VisualElement = require("elements/VisualElement") + +---@class List : VisualElement +local List = setmetatable({}, VisualElement) +List.__index = List + +---@property items table List of items to display +List.defineProperty(List, "items", {default = {}, type = "table", canTriggerRender = true}) +---@property selectedIndex number Currently selected item index +List.defineProperty(List, "selectedIndex", {default = 0, type = "number", canTriggerRender = true}) +---@property selectable boolean Whether items can be selected +List.defineProperty(List, "selectable", {default = true, type = "boolean"}) +---@property offset number Scrolling offset +List.defineProperty(List, "offset", {default = 0, type = "number", canTriggerRender = true}) +---@property selectedColor color Color for selected item +List.defineProperty(List, "selectedColor", {default = colors.blue, type = "number"}) + +List.listenTo(List, "mouse_click") +List.listenTo(List, "mouse_scroll") + +function List.new() + local self = setmetatable({}, List):__init() + self.set("width", 16) + self.set("height", 8) + self.set("background", colors.gray) + return self +end + +function List:init(props, basalt) + VisualElement.init(self, props, basalt) + self.set("type", "List") +end + +function List:addItem(text) + local items = self.get("items") + table.insert(items, text) + self:updateRender() + return self +end + +function List:removeItem(index) + local items = self.get("items") + table.remove(items, index) + self:updateRender() + return self +end + +function List:clear() + self.set("items", {}) + self.set("selectedIndex", 0) + self:updateRender() + return self +end + +function List:mouse_click(button, x, y) + if button == 1 and self:isInBounds(x, y) and self.get("selectable") then + local _, index = self:getRelativePosition(x, y) + + local adjustedIndex = index + self.get("offset") + local items = self.get("items") + + if adjustedIndex <= #items then + local item = items[adjustedIndex] + self.set("selectedIndex", adjustedIndex) + + if type(item) == "table" and item.callback then + item.callback(self) + end + + self:fireEvent("select", adjustedIndex, item) + self:updateRender() + return true + end + end + return false +end + +function List:mouse_scroll(direction, x, y) + if self:isInBounds(x, y) then + local offset = self.get("offset") + local maxOffset = math.max(0, #self.get("items") - self.get("height")) + + offset = math.min(maxOffset, math.max(0, offset + direction)) + self.set("offset", offset) + return true + end +end + +function List:onSelect(callback) + self:registerCallback("select", callback) + return self +end + +function List:render() + VisualElement.render(self) + + local items = self.get("items") + local height = self.get("height") + local offset = self.get("offset") + local selected = self.get("selectedIndex") + local width = self.get("width") + + for i = 1, height do + local itemIndex = i + offset + local item = items[itemIndex] + + if item then + if type(item) == "table" and item.separator then + local separatorChar = (item.text or "-"):sub(1,1) + local separatorText = string.rep(separatorChar, width) + local fg = item.foreground or self.get("foreground") + local bg = item.background or self.get("background") + + self:textBg(1, i, string.rep(" ", width), bg) + self:textFg(1, i, separatorText, fg) + else + local text = type(item) == "table" and item.text or item + local isSelected = itemIndex == selected + + local bg = isSelected and + (item.selectedBackground or self.get("selectedColor")) or + (item.background or self.get("background")) + + local fg = isSelected and + (item.selectedForeground or colors.white) or + (item.foreground or self.get("foreground")) + + self:textBg(1, i, string.rep(" ", width), bg) + self:textFg(1, i, text, fg) + end + end + end +end + +return List +", + ["log.lua"] = "local 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 Log", + ["plugins/state.lua"] = "local PropertySystem = require("propertySystem") +local errorManager = require("errorManager") +local BaseElement = {} + +function BaseElement.setup(element) + element.defineProperty(element, "states", {default = {}, type = "table"}) + element.defineProperty(element, "computedStates", {default = {}, type = "table"}) + element.defineProperty(element, "stateUpdate", { + default = {key = "", value = nil, oldValue = nil}, + type = "table" + }) +end + +function BaseElement:initializeState(name, default, canTriggerRender, persist, path) + local states = self.get("states") + + if states[name] then + errorManager.error("State '" .. name .. "' already exists") + return self + end + + if persist then + local file = path or ("states/" .. self.get("name") .. "_" .. name .. ".state") + + if fs.exists(file) then + local f = fs.open(file, "r") + states[name] = { + value = textutils.unserialize(f.readAll()), + persist = true, + file = file + } + f.close() + else + states[name] = { + value = default, + persist = true, + file = file, + canTriggerRender = canTriggerRender + } + end + else + states[name] = { + value = default, + canTriggerRender = canTriggerRender + } + end + return self +end + +function BaseElement:setState(name, value) + local states = self.get("states") + if not states[name] then + error("State '"..name.."' not initialized") + end + + local oldValue = states[name].value + states[name].value = value + + if states[name].persist then + local dir = fs.getDir(states[name].file) + if not fs.exists(dir) then + fs.makeDir(dir) + end + local f = fs.open(states[name].file, "w") + f.write(textutils.serialize(value)) + f.close() + end + + if states[name].canTriggerRender then + self:updateRender() + end + + self.set("stateUpdate", { + key = name, + value = value, + oldValue = oldValue + }) + return self +end + +function BaseElement:getState(name) + local states = self.get("states") + if not states[name] then + errorManager.error("State '"..name.."' not initialized") + end + return states[name].value +end + +function BaseElement:computed(key, computeFn) + local computed = self.get("computedStates") + computed[key] = setmetatable({}, { + __call = function() + return computeFn(self) + end + }) + return self +end + +function BaseElement:shareState(stateKey, ...) + local value = self:getState(stateKey) + + for _, element in ipairs({...}) do + if element.get("states")[stateKey] then + errorManager.error("Cannot share state '" .. stateKey .. "': Target element already has this state") + return self + end + + element:initializeState(stateKey, value) + + self:observe("stateUpdate", function(self, update) + if update.key == stateKey then + element:setState(stateKey, update.value) + end + end) + end + return self +end + +function BaseElement:onStateChange(stateName, callback) + if not self.get("states")[stateName] then + errorManager.error("Cannot observe state '" .. stateName .. "': State not initialized") + return self + end + + self:observe("stateUpdate", function(self, update) + if update.key == stateName then + callback(self, update.value, update.oldValue) + end + end) + return self +end + +return { + BaseElement = BaseElement +} +", + ["elements/Dropdown.lua"] = "local VisualElement = require("elements/VisualElement") +local List = require("elements/List") +local tHex = require("libraries/colorHex") + +---@class Dropdown : List +local Dropdown = setmetatable({}, List) +Dropdown.__index = Dropdown + +Dropdown.defineProperty(Dropdown, "isOpen", {default = false, type = "boolean", canTriggerRender = true}) +Dropdown.defineProperty(Dropdown, "dropdownHeight", {default = 5, type = "number"}) +Dropdown.defineProperty(Dropdown, "selectedText", {default = "", type = "string"}) +Dropdown.defineProperty(Dropdown, "dropSymbol", {default = "\31", type = "string"}) -- ▼ Symbol + +function Dropdown.new() + local self = setmetatable({}, Dropdown):__init() + self.set("width", 16) + self.set("height", 1) + self.set("z", 8) + return self +end + +function Dropdown:init(props, basalt) + List.init(self, props, basalt) + self.set("type", "Dropdown") +end + +function Dropdown:mouse_click(button, x, y) + if not VisualElement.mouse_click(self, button, x, y) then return false end + + local relX, relY = self:getRelativePosition(x, y) + + if relY == 1 then -- Klick auf Header + self.set("isOpen", not self.get("isOpen")) + if not self.get("isOpen") then + self.set("height", 1) + else + self.set("height", 1 + math.min(self.get("dropdownHeight"), #self.get("items"))) + end + return true + elseif self.get("isOpen") and relY > 1 then + -- Offset für die Liste korrigieren (relY - 1 wegen Header) + local index = relY - 1 + self.get("offset") + local items = self.get("items") + + if index <= #items then + local item = items[index] + if type(item) == "table" and item.separator then + return false + end + + self.set("selectedIndex", index) + self.set("isOpen", false) + self.set("height", 1) + + if type(item) == "table" and item.callback then + item.callback(self) + end + + self:fireEvent("select", index, item) + return true + end + end + return false +end + +function Dropdown:render() + VisualElement.render(self) + + -- Header rendern + local text = self.get("selectedText") + if #text == 0 and self.get("selectedIndex") > 0 then + local item = self.get("items")[self.get("selectedIndex")] + text = type(item) == "table" and item.text or tostring(item) + end + + self:blit(1, 1, text .. string.rep(" ", self.get("width") - #text - 1) .. (self.get("isOpen") and "\31" or "\17"), + string.rep(tHex[self.get("foreground")], self.get("width")), + string.rep(tHex[self.get("background")], self.get("width"))) + + -- Items nur rendern wenn offen + if self.get("isOpen") then + local items = self.get("items") + local offset = self.get("offset") + local selected = self.get("selectedIndex") + local width = self.get("width") + + -- Liste ab Zeile 2 rendern (unterhalb des Headers) + for i = 2, self.get("height") do + local itemIndex = i - 1 + offset -- -1 wegen Header + local item = items[itemIndex] + + if item then + if type(item) == "table" and item.separator then + local separatorChar = (item.text or "-"):sub(1,1) + local separatorText = string.rep(separatorChar, width) + local fg = item.foreground or self.get("foreground") + local bg = item.background or self.get("background") + + self:textBg(1, i, string.rep(" ", width), bg) + self:textFg(1, i, separatorText, fg) + else + local itemText = type(item) == "table" and item.text or tostring(item) + local isSelected = itemIndex == selected + + local bg = isSelected and + (item.selectedBackground or self.get("selectedColor")) or + (item.background or self.get("background")) + + local fg = isSelected and + (item.selectedForeground or colors.white) or + (item.foreground or self.get("foreground")) + + self:textBg(1, i, string.rep(" ", width), bg) + self:textFg(1, i, itemText, fg) + end + end + end + end +end + +return Dropdown +", ["libraries/colorHex.lua"] = "local colorHex = {} for i = 0, 15 do @@ -1957,2139 +2141,6 @@ function Flexbox:render() end return Flexbox", - ["elements/Label.lua"] = "local elementManager = require("elementManager") -local VisualElement = elementManager.getElement("VisualElement") - ----@class Label : VisualElement -local Label = setmetatable({}, VisualElement) -Label.__index = Label - ----@property text string Label Label text to be displayed -Label.defineProperty(Label, "text", {default = "Label", type = "string", setter = function(self, value) - if(type(value)=="function")then value = value() end - self.set("width", #value) - return value -end}) - ---- Creates a new Label instance ---- @return Label object The newly created Label instance ---- @usage local element = Label.new("myId", basalt) -function Label.new() - local self = setmetatable({}, Label):__init() - self.set("z", 3) - self.set("foreground", colors.black) - self.set("backgroundEnabled", false) - return self -end - -function Label:init(props, basalt) - VisualElement.init(self, props, basalt) - self.set("type", "Label") -end - -function Label:render() - VisualElement.render(self) - local text = self.get("text") - self:textFg(1, 1, text, self.get("foreground")) -end - -return Label", - ["plugins/theme.lua"] = "local defaultTheme = { - default = { - background = colors.lightGray, - foreground = colors.black, - }, - BaseFrame = { - background = colors.white, - foreground = colors.black, - - Frame = { - background = colors.black, - names = { - basaltDebugLogClose = { - background = colors.blue, - foreground = colors.white - } - }, - }, - Button = { - background = "{self.clicked and colors.black or colors.cyan}", - foreground = "{self.clicked and colors.cyan or colors.black}", - }, - - names = { - basaltDebugLog = { - background = colors.red, - foreground = colors.white - }, - test = { - background = "{self.clicked and colors.black or colors.green}", - foreground = "{self.clicked and colors.green or colors.black}" - } - }, - } -} - -local themes = { - default = defaultTheme -} - -local currentTheme = "default" - -local BaseElement = { - hooks = { - postInit = { - pre = function(self) - self:applyTheme() - end} - } -} - -function BaseElement.____getElementPath(self, types) - if types then - table.insert(types, 1, self._values.type) - else - types = {self._values.type} - end - local parent = self.parent - if parent then - return parent.____getElementPath(parent, types) - else - return types - end -end - -local function lookUpTemplate(theme, path) - local current = theme - - for i = 1, #path do - local found = false - local types = path[i] - - for _, elementType in ipairs(types) do - if current[elementType] then - current = current[elementType] - found = true - break - end - end - - if not found then - return nil - end - end - - return current -end - -local function getDefaultProperties(theme, elementType) - local result = {} - if theme.default then - for k,v in pairs(theme.default) do - if type(v) ~= "table" then - result[k] = v - end - end - - if theme.default[elementType] then - for k,v in pairs(theme.default[elementType]) do - if type(v) ~= "table" then - result[k] = v - end - end - end - end - return result -end - -local function applyNamedStyles(result, theme, elementType, elementName, themeTable) - if theme.default and theme.default.names and theme.default.names[elementName] then - for k,v in pairs(theme.default.names[elementName]) do - if type(v) ~= "table" then result[k] = v end - end - end - - if theme.default and theme.default[elementType] and theme.default[elementType].names - and theme.default[elementType].names[elementName] then - for k,v in pairs(theme.default[elementType].names[elementName]) do - if type(v) ~= "table" then result[k] = v end - end - end - - if themeTable and themeTable.names and themeTable.names[elementName] then - for k,v in pairs(themeTable.names[elementName]) do - if type(v) ~= "table" then result[k] = v end - end - end -end - -local function collectThemeProps(theme, path, elementType, elementName) - local result = {} - local themeTable = lookUpTemplate(theme, path) - if themeTable then - for k,v in pairs(themeTable) do - if type(v) ~= "table" then - result[k] = v - end - end - end - - if next(result) == nil then - result = getDefaultProperties(theme, elementType) - end - - applyNamedStyles(result, theme, elementType, elementName, themeTable) - - return result -end - - function BaseElement:applyTheme() - local styles = self:getTheme() - if(styles ~= nil) then - for prop, value in pairs(styles) do - self.set(prop, value) - end - end -end - -function BaseElement:getTheme() - local path = self:____getElementPath() - local elementType = self.get("type") - local elementName = self.get("name") - - return collectThemeProps(themes[currentTheme], path, elementType, elementName) -end - -local themeAPI = { - setTheme = function(newTheme) - defaultTheme = newTheme - end, - - getTheme = function() - return defaultTheme - end, - - loadTheme = function(path) - local file = fs.open(path, "r") - if file then - local content = file.readAll() - file.close() - defaultTheme = textutils.unserializeJSON(content) - end - end -} - -local Theme = { - BaseElement = BaseElement, - API = themeAPI -} - -return Theme -", - ["plugins/animation.lua"] = "local Animation = {} -Animation.__index = Animation - -local registeredAnimations = {} - -function Animation.registerAnimation(name, handlers) - registeredAnimations[name] = handlers - - Animation[name] = function(self, ...) - local args = {...} - local easing = "linear" - if(type(args[#args]) == "string") then - easing = table.remove(args, #args) - end - local duration = table.remove(args, #args) - return self:addAnimation(name, args, duration, easing) - end -end - -local easings = { - linear = function(progress) - return progress - end, - - easeInQuad = function(progress) - return progress * progress - end, - - easeOutQuad = function(progress) - return 1 - (1 - progress) * (1 - progress) - end, - - easeInOutQuad = function(progress) - if progress < 0.5 then - return 2 * progress * progress - end - return 1 - (-2 * progress + 2)^2 / 2 - end -} - -function Animation.registerEasing(name, func) - easings[name] = func -end - -local AnimationInstance = {} -AnimationInstance.__index = AnimationInstance - -function AnimationInstance.new(element, animType, args, duration, easing) - local self = setmetatable({}, AnimationInstance) - self.element = element - self.type = animType - self.args = args - self.duration = duration - self.startTime = 0 - self.isPaused = false - self.handlers = registeredAnimations[animType] - self.easing = easing - return self -end - -function AnimationInstance:start() - self.startTime = os.epoch("local") / 1000 - if self.handlers.start then - self.handlers.start(self) - end -end - -function AnimationInstance:update(elapsed) - local rawProgress = math.min(1, elapsed / self.duration) - local progress = easings[self.easing](rawProgress) - return self.handlers.update(self, progress) -end - -function AnimationInstance:complete() - if self.handlers.complete then - self.handlers.complete(self) - end -end - -function Animation.new(element) - local self = {} - self.element = element - self.sequences = {{}} - self.sequenceCallbacks = {} - self.currentSequence = 1 - self.timer = nil - setmetatable(self, Animation) - return self -end - -function Animation:sequence() - table.insert(self.sequences, {}) - self.currentSequence = #self.sequences - self.sequenceCallbacks[self.currentSequence] = { - start = nil, - update = nil, - complete = nil - } - return self -end - -function Animation:onStart(callback) - if not self.sequenceCallbacks[self.currentSequence] then - self.sequenceCallbacks[self.currentSequence] = {} - end - self.sequenceCallbacks[self.currentSequence].start = callback - return self -end - -function Animation:onUpdate(callback) - if not self.sequenceCallbacks[self.currentSequence] then - self.sequenceCallbacks[self.currentSequence] = {} - end - self.sequenceCallbacks[self.currentSequence].update = callback - return self -end - -function Animation:onComplete(callback) - if not self.sequenceCallbacks[self.currentSequence] then - self.sequenceCallbacks[self.currentSequence] = {} - end - self.sequenceCallbacks[self.currentSequence].complete = callback - return self -end - -function Animation:addAnimation(type, args, duration, easing) - local anim = AnimationInstance.new(self.element, type, args, duration, easing) - table.insert(self.sequences[self.currentSequence], anim) - return self -end - -function Animation:start() - - self.currentSequence = 1 - if(self.sequenceCallbacks[self.currentSequence])then - if(self.sequenceCallbacks[self.currentSequence].start) then - self.sequenceCallbacks[self.currentSequence].start(self.element) - end - end - if #self.sequences[self.currentSequence] > 0 then - self.timer = os.startTimer(0.05) - for _, anim in ipairs(self.sequences[self.currentSequence]) do - anim:start() - end - end - return self -end - -function Animation:event(event, timerId) - if event == "timer" and timerId == self.timer then - local currentTime = os.epoch("local") / 1000 - local sequenceFinished = true - local remaining = {} - local callbacks = self.sequenceCallbacks[self.currentSequence] - - for _, anim in ipairs(self.sequences[self.currentSequence]) do - local elapsed = currentTime - anim.startTime - local progress = elapsed / anim.duration - local finished = anim:update(elapsed) - - if callbacks and callbacks.update then - callbacks.update(self.element, progress) - end - - if not finished then - table.insert(remaining, anim) - sequenceFinished = false - else - anim:complete() - end - end - - if sequenceFinished then - if callbacks and callbacks.complete then - callbacks.complete(self.element) - end - - if self.currentSequence < #self.sequences then - self.currentSequence = self.currentSequence + 1 - remaining = {} - - local nextCallbacks = self.sequenceCallbacks[self.currentSequence] - if nextCallbacks and nextCallbacks.start then - nextCallbacks.start(self.element) - end - - for _, anim in ipairs(self.sequences[self.currentSequence]) do - anim:start() - table.insert(remaining, anim) - end - end - end - - if #remaining > 0 then - self.timer = os.startTimer(0.05) - end - end -end - -Animation.registerAnimation("move", { - start = function(anim) - anim.startX = anim.element.get("x") - anim.startY = anim.element.get("y") - end, - - update = function(anim, progress) - local x = anim.startX + (anim.args[1] - anim.startX) * progress - local y = anim.startY + (anim.args[2] - anim.startY) * progress - anim.element.set("x", math.floor(x)) - anim.element.set("y", math.floor(y)) - return progress >= 1 - end, - - complete = function(anim) - anim.element.set("x", anim.args[1]) - anim.element.set("y", anim.args[2]) - end -}) - -Animation.registerAnimation("morphText", { - start = function(anim) - local startText = anim.element.get(anim.args[1]) - local targetText = anim.args[2] - local maxLength = math.max(#startText, #targetText) - local startSpace = string.rep(" ", math.floor(maxLength - #startText)/2) - anim.startText = startSpace .. startText .. startSpace - anim.targetText = targetText .. string.rep(" ", maxLength - #targetText) - anim.length = maxLength - end, - - update = function(anim, progress) - local currentText = "" - - for i = 1, anim.length do - local startChar = anim.startText:sub(i,i) - local targetChar = anim.targetText:sub(i,i) - - if progress < 0.5 then - currentText = currentText .. (math.random() > progress*2 and startChar or " ") - else - currentText = currentText .. (math.random() > (progress-0.5)*2 and " " or targetChar) - end - end - - anim.element.set(anim.args[1], currentText) - return progress >= 1 - end, - - complete = function(anim) - anim.element.set(anim.args[1], anim.targetText:gsub("%s+$", "")) -- Entferne trailing spaces - end -}) - -Animation.registerAnimation("typewrite", { - start = function(anim) - anim.targetText = anim.args[2] - anim.element.set(anim.args[1], "") - end, - - update = function(anim, progress) - local length = math.floor(#anim.targetText * progress) - anim.element.set(anim.args[1], anim.targetText:sub(1, length)) - return progress >= 1 - end -}) - -Animation.registerAnimation("fadeText", { - start = function(anim) - anim.chars = {} - for i=1, #anim.args[2] do - anim.chars[i] = {char = anim.args[2]:sub(i,i), visible = false} - end - end, - - update = function(anim, progress) - local text = "" - for i, charData in ipairs(anim.chars) do - if math.random() < progress then - charData.visible = true - end - text = text .. (charData.visible and charData.char or " ") - end - anim.element.set(anim.args[1], text) - return progress >= 1 - end -}) - -Animation.registerAnimation("scrollText", { - start = function(anim) - anim.width = anim.element.get("width") - anim.targetText = anim.args[2] - anim.element.set(anim.args[1], "") - end, - - update = function(anim, progress) - local offset = math.floor(anim.width * (1-progress)) - local spaces = string.rep(" ", offset) - anim.element.set(anim.args[1], spaces .. anim.targetText) - return progress >= 1 - end -}) - -local VisualElement = {hooks={}} - -function VisualElement.hooks.dispatchEvent(self, event, ...) - if event == "timer" then - local animation = self.get("animation") - if animation then - animation:event(event, ...) - end - end -end - -function VisualElement.setup(element) - VisualElementBaseDispatchEvent = element.dispatchEvent - element.defineProperty(element, "animation", {default = nil, type = "table"}) - element.listenTo(element, "timer") -end - -function VisualElement:animate() - local animation = Animation.new(self) - self.set("animation", animation) - return animation -end - -return { - VisualElement = VisualElement -}", - ["plugins/debug.lua"] = "local log = require("log") -local tHex = require("libraries/colorHex") - -local maxLines = 10 -local isVisible = false - -local function createDebugger(element) - local elementInfo = { - renderCount = 0, - eventCount = {}, - lastRender = os.epoch("utc"), - properties = {}, - children = {} - } - - return { - trackProperty = function(name, value) - elementInfo.properties[name] = value - end, - - trackRender = function() - elementInfo.renderCount = elementInfo.renderCount + 1 - elementInfo.lastRender = os.epoch("utc") - end, - - trackEvent = function(event) - elementInfo.eventCount[event] = (elementInfo.eventCount[event] or 0) + 1 - end, - - dump = function() - return { - type = element.get("type"), - id = element.get("id"), - stats = elementInfo - } - end - } -end - -local BaseElement = { - debug = function(self, level) - self._debugger = createDebugger(self) - self._debugLevel = level or DEBUG_LEVELS.INFO - return self - end, - - dumpDebug = function(self) - if not self._debugger then return end - return self._debugger.dump() - end -} - -local BaseFrame = { - showDebugLog = function(self) - if not self._debugFrame then - local width = self.get("width") - local height = self.get("height") - self._debugFrame = self:addFrame("basaltDebugLog") - :setWidth(width) - :setHeight(height) - :setZ(999) - :listenEvent("mouse_scroll", true) - self.basalt.LOGGER.debug("Created debug log frame " .. self._debugFrame.get("name")) - - self._debugFrame:addButton("basaltDebugLogClose") - :setWidth(9) - :setHeight(1) - :setX(width - 8) - :setY(height) - :setText("Close") - :onMouseClick(function() - self:hideDebugLog() - end) - - self._debugFrame._scrollOffset = 0 - self._debugFrame._processedLogs = {} - - local function wrapText(text, width) - local lines = {} - while #text > 0 do - local line = text:sub(1, width) - table.insert(lines, line) - text = text:sub(width + 1) - end - return lines - end - - local function processLogs() - local processed = {} - local width = self._debugFrame.get("width") - - for _, entry in ipairs(log._logs) do - local lines = wrapText(entry.message, width) - for _, line in ipairs(lines) do - table.insert(processed, { - text = line, - level = entry.level - }) - end - end - return processed - end - - local totalLines = #processLogs() - self.get("height") - self._scrollOffset = totalLines - - local originalRender = self._debugFrame.render - self._debugFrame.render = function(frame) - originalRender(frame) - frame._processedLogs = processLogs() - - local height = frame.get("height")-2 - local totalLines = #frame._processedLogs - local maxScroll = math.max(0, totalLines - height) - frame._scrollOffset = math.min(frame._scrollOffset, maxScroll) - - for i = 1, height-2 do - local logIndex = i + frame._scrollOffset - local entry = frame._processedLogs[logIndex] - - if entry then - local color = entry.level == log.LEVEL.ERROR and colors.red - or entry.level == log.LEVEL.WARN and colors.yellow - or entry.level == log.LEVEL.DEBUG and colors.lightGray - or colors.white - - frame:textFg(2, i, entry.text, color) - end - end - end - - local baseDispatchEvent = self._debugFrame.dispatchEvent - self._debugFrame.dispatchEvent = function(self, event, direction, ...) - if(event == "mouse_scroll") then - self._scrollOffset = math.max(0, self._scrollOffset + direction) - self:updateRender() - return true - else - baseDispatchEvent(self, event, direction, ...) - end - end - end - self._debugFrame.set("visible", true) - return self - end, - - hideDebugLog = function(self) - if self._debugFrame then - self._debugFrame.set("visible", false) - end - return self - end, - - toggleDebugLog = function(self) - if self._debugFrame and self._debugFrame:isVisible() then - self:hideDebugLog() - else - self:showDebugLog() - end - return self - end -} - - -local Container = { - debugChildren = function(self, level) - self:debug(level) - for _, child in pairs(self.get("children")) do - if child.debug then - child:debug(level) - end - end - return self - end -} - -return { - BaseElement = BaseElement, - Container = Container, - BaseFrame = BaseFrame, -} -", - ["elements/Frame.lua"] = "local elementManager = require("elementManager") -local Container = elementManager.getElement("Container") - ----@class Frame : Container -local Frame = setmetatable({}, Container) -Frame.__index = Frame - ---- Creates a new Frame instance ---- @return Frame object The newly created Frame instance ---- @usage local element = Frame.new("myId", basalt) -function Frame.new() - local self = setmetatable({}, Frame):__init() - self.set("width", 12) - self.set("height", 6) - self.set("background", colors.gray) - self.set("z", 10) - return self -end - -function Frame:init(props, basalt) - Container.init(self, props, basalt) - self.set("type", "Frame") -end - -return Frame", - ["render.lua"] = "local Render = {} -Render.__index = Render -local colorChars = require("libraries/colorHex") - -function Render.new(terminal) - local self = setmetatable({}, Render) - self.terminal = terminal - self.width, self.height = terminal.getSize() - - self.buffer = { - text = {}, - fg = {}, - bg = {}, - dirtyRects = {} - } - - 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) - end - - return self -end - -function Render:addDirtyRect(x, y, width, height) - table.insert(self.buffer.dirtyRects, { - x = x, - y = y, - width = width, - height = height - }) -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:addDirtyRect(x, y, #text, 1) - - 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) - end - end - - self:addDirtyRect(x, y, width, height) - 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:addDirtyRect(x, y, #text, 1) - - return self -end - -function Render:textBg(x, y, text, bg) - if y < 1 or y > self.height then return self end - bg = colorChars[bg] or "f" - - self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text) - self.buffer.bg[y] = self.buffer.bg[y]:sub(1,x-1) .. bg:rep(#text) .. self.buffer.bg[y]:sub(x+#text) - self:addDirtyRect(x, y, #text, 1) - - 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:addDirtyRect(x, y, #text, 1) - - 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:addDirtyRect(x, y, #fg, 1) - - 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:addDirtyRect(x, y, #bg, 1) - - 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:addDirtyRect(x, y, #text, 1) - - 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:addDirtyRect(1, y, self.width, 1) - end - return self -end - -function Render:render() - - local mergedRects = {} - for _, rect in ipairs(self.buffer.dirtyRects) do - local merged = false - for _, existing in ipairs(mergedRects) do - if self:rectOverlaps(rect, existing) then - self:mergeRects(existing, rect) - merged = true - break - end - end - if not merged then - table.insert(mergedRects, rect) - end - end - - for _, rect in ipairs(mergedRects) do - for y = rect.y, rect.y + rect.height - 1 do - if y >= 1 and y <= self.height then - self.terminal.setCursorPos(rect.x, y) - self.terminal.blit( - self.buffer.text[y]:sub(rect.x, rect.x + rect.width - 1), - self.buffer.fg[y]:sub(rect.x, rect.x + rect.width - 1), - self.buffer.bg[y]:sub(rect.x, rect.x + rect.width - 1) - ) - end - end - end - - self.buffer.dirtyRects = {} - - if self.blink then - self.terminal.setCursorPos(self.xCursor, self.yCursor) - self.terminal.setCursorBlink(true) - else - self.terminal.setCursorBlink(false) - end - - return self -end - -function Render:rectOverlaps(r1, r2) - return not (r1.x + r1.width <= r2.x or - r2.x + r2.width <= r1.x or - r1.y + r1.height <= r2.y or - r2.y + r2.height <= r1.y) -end - -function Render:mergeRects(target, source) - local x1 = math.min(target.x, source.x) - local y1 = math.min(target.y, source.y) - local x2 = math.max(target.x + target.width, source.x + source.width) - local y2 = math.max(target.y + target.height, source.y + source.height) - - target.x = x1 - target.y = y1 - target.width = x2 - x1 - target.height = y2 - y1 -end - -function Render:setCursor(x, y, blink) - self.terminal.setCursorPos(x, y) - self.terminal.setCursorBlink(blink) - self.xCursor = x - self.yCursor = y - self.blink = blink - 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 Render", - ["propertySystem.lua"] = "local deepCopy = require("libraries/utils").deepCopy -local expect = require("libraries/expect") -local errorManager = require("errorManager") -local log = require("log") - ---- @class PropertySystem -local PropertySystem = {} -PropertySystem.__index = PropertySystem - -PropertySystem._properties = {} -local blueprintTemplates = {} - -PropertySystem._setterHooks = {} - -function PropertySystem.addSetterHook(hook) - table.insert(PropertySystem._setterHooks, hook) -end - -local function applyHooks(element, propertyName, value, config) - for _, hook in ipairs(PropertySystem._setterHooks) do - local newValue = hook(element, propertyName, value, config) - if newValue ~= nil then - value = newValue - end - end - return value -end - -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] - if type(value) == "function" and config.type ~= "function" then - value = value(self) - end - return config.getter and config.getter(self, value, ...) or value - end - - class["set" .. capitalizedName] = function(self, value, ...) - expect(1, self, "element") - value = applyHooks(self, name, value, config) - - if type(value) ~= "function" then - expect(2, value, config.type) - end - - if config.setter then - value = config.setter(self, value, ...) - end - - self:_updateProperty(name, value) - return self - end -end - -function PropertySystem.combineProperties(class, name, ...) - local properties = {...} - for k,v in pairs(properties)do - if not class._properties[v] then errorManager.error("Property not found: "..v) end - end - local capitalizedName = name:sub(1,1):upper() .. name:sub(2) - - class["get" .. capitalizedName] = function(self, ...) - expect(1, self, "element") - local value = {} - for _,v in pairs(properties)do - value[v] = self.get(v) - end - return table.unpack(value) - end - - class["set" .. capitalizedName] = function(self, ...) - expect(1, self, "element") - local values = {...} - for i,v in pairs(properties)do - self.set(v, values[i]) - end - return self - end -end - ---- Creates a blueprint of an element class with all its properties ---- @param elementClass table The element class to create a blueprint from ---- @return table blueprint A table containing all property definitions -function PropertySystem.blueprint(elementClass, properties, basalt, parent) - if not blueprintTemplates[elementClass] then - local template = { - basalt = basalt, - __isBlueprint = true, - _values = properties or {}, - _events = {}, - render = function() end, - dispatchEvent = function() end, - init = function() end, - } - - template.loaded = function(self, callback) - self.loadedCallback = callback - return template - end - - template.create = function(self) - local element = elementClass.new() - element:init({}, self.basalt) - for name, value in pairs(self._values) do - element._values[name] = value - end - for name, callbacks in pairs(self._events) do - for _, callback in ipairs(callbacks) do - element[name](element, callback) - end - end - if(parent~=nil)then - parent:addChild(element) - end - element:updateRender() - self.loadedCallback(element) - element:postInit() - return element - end - - local currentClass = elementClass - while currentClass do - if rawget(currentClass, '_properties') then - for name, config in pairs(currentClass._properties) do - if type(config.default) == "table" then - template._values[name] = deepCopy(config.default) - else - template._values[name] = config.default - end - end - end - currentClass = getmetatable(currentClass) and rawget(getmetatable(currentClass), '__index') - end - - blueprintTemplates[elementClass] = template - end - - local blueprint = { - _values = {}, - _events = {}, - loadedCallback = function() end, - } - - blueprint.get = function(name) - local value = blueprint._values[name] - local config = elementClass._properties[name] - if type(value) == "function" and config.type ~= "function" then - value = value(blueprint) - end - return value - end - blueprint.set = function(name, value) - blueprint._values[name] = value - return blueprint - end - - setmetatable(blueprint, { - __index = function(self, k) - if k:match("^on%u") then - return function(_, callback) - self._events[k] = self._events[k] or {} - table.insert(self._events[k], callback) - return self - end - end - if k:match("^get%u") then - local propName = k:sub(4,4):lower() .. k:sub(5) - return function() - return self._values[propName] - end - end - if k:match("^set%u") then - local propName = k:sub(4,4):lower() .. k:sub(5) - return function(_, value) - self._values[propName] = value - return self - end - end - return blueprintTemplates[elementClass][k] - end - }) - - return blueprint -end - -function PropertySystem.createFromBlueprint(elementClass, blueprint, basalt) - local element = elementClass.new({}, basalt) - for name, value in pairs(blueprint._values) do - if type(value) == "table" then - element._values[name] = deepCopy(value) - else - element._values[name] = value - end - end - - return element -end - -function PropertySystem:__init() - self._values = {} - self._observers = {} - - self.set = function(name, value, ...) - local oldValue = self._values[name] - local config = self._properties[name] - if(config~=nil)then - if(config.setter) then - value = config.setter(self, value, ...) - end - if config.canTriggerRender then - self:updateRender() - end - self._values[name] = applyHooks(self, name, value, config) - if oldValue ~= value and self._observers[name] then - for _, callback in ipairs(self._observers[name]) do - callback(self, value, oldValue) - end - end - end - end - - self.get = function(name, ...) - local value = self._values[name] - local config = self._properties[name] - if(config==nil)then errorManager.error("Property not found: "..name) return end - if type(value) == "function" and config.type ~= "function" then - value = value(self) - end - return config.getter and config.getter(self, value, ...) or value - 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) - local config = self._properties[k] - if config then - local value = self._values[k] - if type(value) == "function" and config.type ~= "function" then - value = value(self) - end - return value - end - if type(originalIndex) == "function" then - return originalIndex(t, k) - else - return originalIndex[k] - end - end, - __newindex = function(t, k, v) - local config = self._properties[k] - if config then - if config.setter then - v = config.setter(self, v) - end - v = applyHooks(self, k, v, config) - 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 type(oldValue) == "function" then - oldValue = oldValue(self) - end - - self._values[name] = value - local newValue = type(value) == "function" and value(self) or value - - if oldValue ~= newValue then - if self._properties[name].canTriggerRender then - self:updateRender() - end - if self._observers[name] then - for _, callback in ipairs(self._observers[name]) do - callback(self, newValue, 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 - -function PropertySystem:removeObserver(name, callback) - if self._observers[name] then - for i, cb in ipairs(self._observers[name]) do - if cb == callback then - table.remove(self._observers[name], i) - if #self._observers[name] == 0 then - self._observers[name] = nil - end - break - end - end - end - return self -end - -function PropertySystem:removeAllObservers(name) - if name then - self._observers[name] = nil - else - self._observers = {} - end - return self -end - -function PropertySystem:instanceProperty(name, config) - PropertySystem.defineProperty(self, name, config) - self._values[name] = config.default - return self -end - -function PropertySystem:removeProperty(name) - self._values[name] = nil - self._properties[name] = nil - self._observers[name] = nil - - local capitalizedName = name:sub(1,1):upper() .. name:sub(2) - self["get" .. capitalizedName] = nil - self["set" .. capitalizedName] = nil - return self -end - -function PropertySystem:getPropertyConfig(name) - return self._properties[name] -end - -return PropertySystem", - ["elements/Dropdown.lua"] = "local VisualElement = require("elements/VisualElement") -local List = require("elements/List") -local tHex = require("libraries/colorHex") - ----@class Dropdown : List -local Dropdown = setmetatable({}, List) -Dropdown.__index = Dropdown - -Dropdown.defineProperty(Dropdown, "isOpen", {default = false, type = "boolean", canTriggerRender = true}) -Dropdown.defineProperty(Dropdown, "dropdownHeight", {default = 5, type = "number"}) -Dropdown.defineProperty(Dropdown, "selectedText", {default = "", type = "string"}) -Dropdown.defineProperty(Dropdown, "dropSymbol", {default = "\31", type = "string"}) -- ▼ Symbol - -function Dropdown.new() - local self = setmetatable({}, Dropdown):__init() - self.set("width", 16) - self.set("height", 1) - self.set("z", 8) - return self -end - -function Dropdown:init(props, basalt) - List.init(self, props, basalt) - self.set("type", "Dropdown") -end - -function Dropdown:mouse_click(button, x, y) - if not VisualElement.mouse_click(self, button, x, y) then return false end - - local relX, relY = self:getRelativePosition(x, y) - - if relY == 1 then -- Klick auf Header - self.set("isOpen", not self.get("isOpen")) - if not self.get("isOpen") then - self.set("height", 1) - else - self.set("height", 1 + math.min(self.get("dropdownHeight"), #self.get("items"))) - end - return true - elseif self.get("isOpen") and relY > 1 then - -- Offset für die Liste korrigieren (relY - 1 wegen Header) - local index = relY - 1 + self.get("offset") - local items = self.get("items") - - if index <= #items then - local item = items[index] - if type(item) == "table" and item.separator then - return false - end - - self.set("selectedIndex", index) - self.set("isOpen", false) - self.set("height", 1) - - if type(item) == "table" and item.callback then - item.callback(self) - end - - self:fireEvent("select", index, item) - return true - end - end - return false -end - -function Dropdown:render() - VisualElement.render(self) - - -- Header rendern - local text = self.get("selectedText") - if #text == 0 and self.get("selectedIndex") > 0 then - local item = self.get("items")[self.get("selectedIndex")] - text = type(item) == "table" and item.text or tostring(item) - end - - self:blit(1, 1, text .. string.rep(" ", self.get("width") - #text - 1) .. (self.get("isOpen") and "\31" or "\17"), - string.rep(tHex[self.get("foreground")], self.get("width")), - string.rep(tHex[self.get("background")], self.get("width"))) - - -- Items nur rendern wenn offen - if self.get("isOpen") then - local items = self.get("items") - local offset = self.get("offset") - local selected = self.get("selectedIndex") - local width = self.get("width") - - -- Liste ab Zeile 2 rendern (unterhalb des Headers) - for i = 2, self.get("height") do - local itemIndex = i - 1 + offset -- -1 wegen Header - local item = items[itemIndex] - - if item then - if type(item) == "table" and item.separator then - local separatorChar = (item.text or "-"):sub(1,1) - local separatorText = string.rep(separatorChar, width) - local fg = item.foreground or self.get("foreground") - local bg = item.background or self.get("background") - - self:textBg(1, i, string.rep(" ", width), bg) - self:textFg(1, i, separatorText, fg) - else - local itemText = type(item) == "table" and item.text or tostring(item) - local isSelected = itemIndex == selected - - local bg = isSelected and - (item.selectedBackground or self.get("selectedColor")) or - (item.background or self.get("background")) - - local fg = isSelected and - (item.selectedForeground or colors.white) or - (item.foreground or self.get("foreground")) - - self:textBg(1, i, string.rep(" ", width), bg) - self:textFg(1, i, itemText, fg) - end - end - end - end -end - -return Dropdown -", - ["elements/Input.lua"] = "local VisualElement = require("elements/VisualElement") -local tHex = require("libraries/colorHex") - ----@class Input : VisualElement -local Input = setmetatable({}, VisualElement) -Input.__index = Input - ----@property text string Input - text to be displayed -Input.defineProperty(Input, "text", {default = "", type = "string", canTriggerRender = true}) - ----@property cursorPos number Input - current cursor position -Input.defineProperty(Input, "cursorPos", {default = 1, type = "number"}) - ----@property viewOffset number Input - offset of view -Input.defineProperty(Input, "viewOffset", {default = 0, type = "number", canTriggerRender = true}) - --- Neue Properties -Input.defineProperty(Input, "maxLength", {default = nil, type = "number"}) -Input.defineProperty(Input, "placeholder", {default = "asd", type = "string"}) -Input.defineProperty(Input, "placeholderColor", {default = colors.gray, type = "number"}) -Input.defineProperty(Input, "focusedColor", {default = colors.blue, type = "number"}) -Input.defineProperty(Input, "pattern", {default = nil, type = "string"}) - -Input.listenTo(Input, "mouse_click") -Input.listenTo(Input, "key") -Input.listenTo(Input, "char") - ---- Creates a new Input instance ---- @return Input object The newly created Input instance ---- @usage local element = Input.new("myId", basalt) -function Input.new() - local self = setmetatable({}, Input):__init() - self.set("width", 8) - self.set("z", 3) - return self -end - -function Input:init(props, basalt) - VisualElement.init(self, props, basalt) - self.set("type", "Input") -end - -function Input:char(char) - if not self.get("focused") then return end - local text = self.get("text") - local pos = self.get("cursorPos") - local maxLength = self.get("maxLength") - local pattern = self.get("pattern") - - if maxLength and #text >= maxLength then return end - if pattern and not char:match(pattern) then return end - - self.set("text", text:sub(1, pos-1) .. char .. text:sub(pos)) - self.set("cursorPos", pos + 1) - self:updateRender() - self:updateViewport() -end - -function Input:key(key) - if not self.get("focused") then return end - local pos = self.get("cursorPos") - local text = self.get("text") - local viewOffset = self.get("viewOffset") - local width = self.get("width") - - if key == keys.left then - if pos > 1 then - self.set("cursorPos", pos - 1) - if pos - 1 <= viewOffset then - self.set("viewOffset", math.max(0, pos - 2)) - end - end - elseif key == keys.right then - if pos <= #text then - self.set("cursorPos", pos + 1) - if pos - viewOffset >= width then - self.set("viewOffset", pos - width + 1) - end - end - elseif key == keys.backspace then - if pos > 1 then - self.set("text", text:sub(1, pos-2) .. text:sub(pos)) - self.set("cursorPos", pos - 1) - self:updateRender() - self:updateViewport() - end - end - - local relativePos = self.get("cursorPos") - self.get("viewOffset") - self:setCursor(relativePos, 1, true) -end - -function Input:focus() - VisualElement.focus(self) - self:updateRender() -end - -function Input:blur() - VisualElement.blur(self) - self:updateRender() -end - -function Input:mouse_click(button, x, y) - if VisualElement.mouse_click(self, button, x, y) then - local relX, relY = self:getRelativePosition(x, y) - local text = self.get("text") - self:setCursor(math.min(relX, #text + 1), relY, true) - self:set("cursorPos", relX + self.get("viewOffset")) - return true - end -end - -function Input:updateViewport() - local width = self.get("width") - local cursorPos = self.get("cursorPos") - local viewOffset = self.get("viewOffset") - local textLength = #self.get("text") - - if cursorPos - viewOffset > width then - self.set("viewOffset", cursorPos - width) - elseif cursorPos <= viewOffset then - - self.set("viewOffset", math.max(0, cursorPos - 1)) - end - - if viewOffset > textLength - width then - self.set("viewOffset", math.max(0, textLength - width)) - end -end - -function Input:render() - local text = self.get("text") - local viewOffset = self.get("viewOffset") - local width = self.get("width") - local placeholder = self.get("placeholder") - local focusedColor = self.get("focusedColor") - local focused = self.get("focused") - local width, height = self.get("width"), self.get("height") - self:multiBlit(1, 1, width, height, " ", tHex[self.get("foreground")], tHex[focused and focusedColor or self.get("background")]) - - if #text == 0 and #placeholder ~= 0 and self.get("focused") == false then - self:textFg(1, 1, placeholder:sub(1, width), self.get("placeholderColor")) - return - end - - local visibleText = text:sub(viewOffset + 1, viewOffset + width) - self:textFg(1, 1, visibleText, self.get("foreground")) -end - -return Input", - ["plugins/benchmark.lua"] = "local log = require("log") - -local activeProfiles = setmetatable({}, {__mode = "k"}) - -local function createProfile() - return { - methods = {}, - } -end - -local function wrapMethod(element, methodName) - local originalMethod = element[methodName] - - if not activeProfiles[element] then - activeProfiles[element] = createProfile() - end - if not activeProfiles[element].methods[methodName] then - activeProfiles[element].methods[methodName] = { - calls = 0, - totalTime = 0, - minTime = math.huge, - maxTime = 0, - lastTime = 0, - startTime = 0, - path = {}, - methodName = methodName, - originalMethod = originalMethod - } - end - - element[methodName] = function(self, ...) - self:startProfile(methodName) - local result = originalMethod(self, ...) - self:endProfile(methodName) - return result - end -end - -local BaseElement = {} - -function BaseElement:startProfile(methodName) - local profile = activeProfiles[self] - if not profile then - profile = createProfile() - activeProfiles[self] = profile - end - - if not profile.methods[methodName] then - profile.methods[methodName] = { - calls = 0, - totalTime = 0, - minTime = math.huge, - maxTime = 0, - lastTime = 0, - startTime = 0, - path = {}, - methodName = methodName - } - end - - local methodProfile = profile.methods[methodName] - methodProfile.startTime = os.clock() * 1000 - methodProfile.path = {} - - local current = self - while current do - table.insert(methodProfile.path, 1, current.get("name") or current.get("id")) - current = current.parent - end - return self -end - -function BaseElement:endProfile(methodName) - local profile = activeProfiles[self] - if not profile or not profile.methods[methodName] then return self end - - local methodProfile = profile.methods[methodName] - local endTime = os.clock() * 1000 - local duration = endTime - methodProfile.startTime - - methodProfile.calls = methodProfile.calls + 1 - methodProfile.totalTime = methodProfile.totalTime + duration - methodProfile.minTime = math.min(methodProfile.minTime, duration) - methodProfile.maxTime = math.max(methodProfile.maxTime, duration) - methodProfile.lastTime = duration - - return self -end - -function BaseElement:benchmark(methodName) - if not self[methodName] then - log.error("Method " .. methodName .. " does not exist") - return self - end - - activeProfiles[self] = createProfile() - activeProfiles[self].methodName = methodName - activeProfiles[self].isRunning = true - - wrapMethod(self, methodName) - return self -end - -function BaseElement:logBenchmark(methodName) - local profile = activeProfiles[self] - if not profile or not profile.methods[methodName] then return self end - - local stats = profile.methods[methodName] - if stats then - local averageTime = stats.calls > 0 and (stats.totalTime / stats.calls) or 0 - log.info(string.format( - "Benchmark results for %s.%s: " .. - "Path: %s " .. - "Calls: %d " .. - "Average time: %.2fms " .. - "Min time: %.2fms " .. - "Max time: %.2fms " .. - "Last time: %.2fms " .. - "Total time: %.2fms", - table.concat(stats.path, "."), - stats.methodName, - table.concat(stats.path, "/"), - stats.calls, - averageTime, - stats.minTime ~= math.huge and stats.minTime or 0, - stats.maxTime, - stats.lastTime, - stats.totalTime - )) - end - return self -end - -function BaseElement:stopBenchmark(methodName) - local profile = activeProfiles[self] - if not profile or not profile.methods[methodName] then return self end - - local stats = profile.methods[methodName] - if stats and stats.originalMethod then - self[methodName] = stats.originalMethod - end - - profile.methods[methodName] = nil - if not next(profile.methods) then - activeProfiles[self] = nil - end - return self -end - -function BaseElement:getBenchmarkStats(methodName) - local profile = activeProfiles[self] - if not profile or not profile.methods[methodName] then return nil end - - local stats = profile.methods[methodName] - return { - averageTime = stats.totalTime / stats.calls, - totalTime = stats.totalTime, - calls = stats.calls, - minTime = stats.minTime, - maxTime = stats.maxTime, - lastTime = stats.lastTime - } -end - -local Container = {} - -function Container:benchmarkContainer(methodName) - self:benchmark(methodName) - - for _, child in pairs(self.get("children")) do - child:benchmark(methodName) - - if child:isType("Container") then - child:benchmarkContainer(methodName) - end - end - return self -end - -function Container:logContainerBenchmarks(methodName, depth) - depth = depth or 0 - local indent = string.rep(" ", depth) - local childrenTotalTime = 0 - local childrenStats = {} - - for _, child in pairs(self.get("children")) do - local profile = activeProfiles[child] - if profile and profile.methods[methodName] then - local stats = profile.methods[methodName] - childrenTotalTime = childrenTotalTime + stats.totalTime - table.insert(childrenStats, { - element = child, - type = child.get("type"), - calls = stats.calls, - totalTime = stats.totalTime, - avgTime = stats.totalTime / stats.calls - }) - end - end - - local profile = activeProfiles[self] - if profile and profile.methods[methodName] then - local stats = profile.methods[methodName] - local selfTime = stats.totalTime - childrenTotalTime - local avgSelfTime = selfTime / stats.calls - - log.info(string.format( - "%sBenchmark %s (%s): " .. - "%.2fms/call (Self: %.2fms/call) " .. - "[Total: %dms, Calls: %d]", - indent, - self.get("type"), - methodName, - stats.totalTime / stats.calls, - avgSelfTime, - stats.totalTime, - stats.calls - )) - - if #childrenStats > 0 then - for _, childStat in ipairs(childrenStats) do - if childStat.element:isType("Container") then - childStat.element:logContainerBenchmarks(methodName, depth + 1) - else - log.info(string.format("%s> %s: %.2fms/call [Total: %dms, Calls: %d]", - indent .. " ", - childStat.type, - childStat.avgTime, - childStat.totalTime, - childStat.calls - )) - end - end - end - end - - return self -end - -function Container:stopContainerBenchmark(methodName) - for _, child in pairs(self.get("children")) do - if child:isType("Container") then - child:stopContainerBenchmark(methodName) - else - child:stopBenchmark(methodName) - end - end - - self:stopBenchmark(methodName) - return self -end - -local API = { - start = function(name, options) - options = options or {} - local profile = createProfile() - profile.name = name - profile.startTime = os.clock() * 1000 - profile.custom = true - activeProfiles[name] = profile - end, - - stop = function(name) - local profile = activeProfiles[name] - if not profile or not profile.custom then return end - - local endTime = os.clock() * 1000 - local duration = endTime - profile.startTime - - profile.calls = profile.calls + 1 - profile.totalTime = profile.totalTime + duration - profile.minTime = math.min(profile.minTime, duration) - profile.maxTime = math.max(profile.maxTime, duration) - profile.lastTime = duration - - log.info(string.format( - "Custom Benchmark '%s': " .. - "Calls: %d " .. - "Average time: %.2fms " .. - "Min time: %.2fms " .. - "Max time: %.2fms " .. - "Last time: %.2fms " .. - "Total time: %.2fms", - name, - profile.calls, - profile.totalTime / profile.calls, - profile.minTime, - profile.maxTime, - profile.lastTime, - profile.totalTime - )) - end, - - getStats = function(name) - local profile = activeProfiles[name] - if not profile then return nil end - - return { - averageTime = profile.totalTime / profile.calls, - totalTime = profile.totalTime, - calls = profile.calls, - minTime = profile.minTime, - maxTime = profile.maxTime, - lastTime = profile.lastTime - } - end, - - clear = function(name) - activeProfiles[name] = nil - end, - - clearAll = function() - for k,v in pairs(activeProfiles) do - if v.custom then - activeProfiles[k] = nil - end - end - end -} - -return { - BaseElement = BaseElement, - Container = Container, - API = API -}", - ["elements/Menu.lua"] = "local VisualElement = require("elements/VisualElement") -local List = require("elements/List") -local tHex = require("libraries/colorHex") - ----@class Menu : List -local Menu = setmetatable({}, List) -Menu.__index = Menu - -Menu.defineProperty(Menu, "separatorColor", {default = colors.gray, type = "number"}) - -function Menu.new() - local self = setmetatable({}, Menu):__init() - self.set("width", 30) - self.set("height", 1) - self.set("background", colors.gray) - return self -end - -function Menu:init(props, basalt) - List.init(self, props, basalt) - self.set("type", "Menu") - return self -end - -function Menu:setItems(items) - local listItems = {} - local totalWidth = 0 - for _, item in ipairs(items) do - if item.separator then - table.insert(listItems, {text = item.text or "|", selectable = false}) - totalWidth = totalWidth + 1 - else - local text = " " .. item.text .. " " - item.text = text - table.insert(listItems, item) - totalWidth = totalWidth + #text - end - end - self.set("width", totalWidth) - return List.setItems(self, listItems) -end - -function Menu:render() - VisualElement.render(self) - local currentX = 1 - - for i, item in ipairs(self.get("items")) do - local isSelected = i == self.get("selectedIndex") - - local fg = item.selectable == false and self.get("separatorColor") or - (isSelected and (item.selectedForeground or self.get("foreground")) or - (item.foreground or self.get("foreground"))) - - local bg = isSelected and - (item.selectedBackground or self.get("selectedColor")) or - (item.background or self.get("background")) - - self:blit(currentX, 1, item.text, - string.rep(tHex[fg], #item.text), - string.rep(tHex[bg], #item.text)) - - currentX = currentX + #item.text - end -end - -function Menu:mouse_click(button, x, y) - if not VisualElement.mouse_click(self, button, x, y) then return false end - if(self.get("selectable") == false) then return false end - local relX = select(1, self:getRelativePosition(x, y)) - local currentX = 1 - - for i, item in ipairs(self.get("items")) do - if relX >= currentX and relX < currentX + #item.text then - if item.selectable ~= false then - self.set("selectedIndex", i) - if type(item) == "table" then - if item.callback then - item.callback(self) - end - end - self:fireEvent("select", i, item) - end - return true - end - currentX = currentX + #item.text - end - return false -end - -return Menu -", - ["elements/BaseFrame.lua"] = "local elementManager = require("elementManager") -local Container = elementManager.getElement("Container") -local Render = require("render") - ----@class BaseFrame : Container -local BaseFrame = setmetatable({}, Container) -BaseFrame.__index = BaseFrame - ----@property text term term nil text -BaseFrame.defineProperty(BaseFrame, "term", {default = nil, type = "table", setter = function(self, value) - if value == nil or value.setCursorPos == nil then - return value - end - self._render = Render.new(value) - self._renderUpdate = true - local width, height = value.getSize() - self.set("width", width) - self.set("height", height) - return value -end}) - -function BaseFrame.new() - local self = setmetatable({}, BaseFrame):__init() - self.set("term", term.current()) - self.set("background", colors.lightGray) - return self -end - -function BaseFrame:init(props, basalt) - Container.init(self, props, basalt) - self.set("type", "BaseFrame") -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 - -function BaseFrame:textBg(x, y, text, bg) - self._render:textBg(x, y, text, bg) -end - -function BaseFrame:blit(x, y, text, fg, bg) - self._render:blit(x, y, text, fg, bg) -end - -function BaseFrame:setCursor(x, y, blink) - local term = self.get("term") - self._render:setCursor(x, y, blink) -end - -function BaseFrame:render() - if(self._renderUpdate) then - if self._render ~= nil then - Container.render(self) - self._render:render() - self._renderUpdate = false - end - end -end - -return BaseFrame", ["LuaLS.lua"] = "---@class BaseFrame ---@field text term local BaseFrame = {} @@ -4874,758 +2925,6 @@ function VisualElement:setFocused(focused) return self end ", - ["elements/Tree.lua"] = "local VisualElement = require("elements/VisualElement") -local tHex = require("libraries/colorHex") - ----@class Tree : VisualElement -local Tree = setmetatable({}, VisualElement) -Tree.__index = Tree - -Tree.defineProperty(Tree, "nodes", {default = {}, type = "table", canTriggerRender = true}) -Tree.defineProperty(Tree, "selectedNode", {default = nil, type = "table", canTriggerRender = true}) -Tree.defineProperty(Tree, "expandedNodes", {default = {}, type = "table", canTriggerRender = true}) -Tree.defineProperty(Tree, "scrollOffset", {default = 0, type = "number", canTriggerRender = true}) -Tree.defineProperty(Tree, "nodeColor", {default = colors.white, type = "number"}) -Tree.defineProperty(Tree, "selectedColor", {default = colors.lightBlue, type = "number"}) - -Tree.listenTo(Tree, "mouse_click") -Tree.listenTo(Tree, "mouse_scroll") - -function Tree.new() - local self = setmetatable({}, Tree):__init() - self.set("width", 30) - self.set("height", 10) - self.set("z", 5) - return self -end - -function Tree:init(props, basalt) - VisualElement.init(self, props, basalt) - self.set("type", "Tree") - return self -end - -function Tree:setNodes(nodes) - self.set("nodes", nodes) - if #nodes > 0 then - self.get("expandedNodes")[nodes[1]] = true - end - return self -end - -function Tree:expandNode(node) - self.get("expandedNodes")[node] = true - self:updateRender() - return self -end - -function Tree:collapseNode(node) - self.get("expandedNodes")[node] = nil - self:updateRender() - return self -end - -function Tree:toggleNode(node) - if self.get("expandedNodes")[node] then - self:collapseNode(node) - else - self:expandNode(node) - end - return self -end - -local function flattenTree(nodes, expandedNodes, level, result) - result = result or {} - level = level or 0 - - for _, node in ipairs(nodes) do - table.insert(result, {node = node, level = level}) - if expandedNodes[node] and node.children then - flattenTree(node.children, expandedNodes, level + 1, result) - end - end - return result -end - -function Tree:mouse_click(button, x, y) - if not VisualElement.mouse_click(self, button, x, y) then return false end - - local relX, relY = self:getRelativePosition(x, y) - local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes")) - local visibleIndex = relY + self.get("scrollOffset") - - if flatNodes[visibleIndex] then - local nodeInfo = flatNodes[visibleIndex] - local node = nodeInfo.node - - if relX <= nodeInfo.level * 2 + 2 then - self:toggleNode(node) - end - - self.set("selectedNode", node) - self:fireEvent("node_select", node) - end - return true -end - -function Tree:onSelect(callback) - self:registerCallback("node_select", callback) - return self -end - -function Tree:mouse_scroll(direction) - local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes")) - local maxScroll = math.max(0, #flatNodes - self.get("height")) - local newScroll = math.min(maxScroll, math.max(0, self.get("scrollOffset") + direction)) - - self.set("scrollOffset", newScroll) - return true -end - -function Tree:render() - VisualElement.render(self) - - local flatNodes = flattenTree(self.get("nodes"), self.get("expandedNodes")) - local height = self.get("height") - local selectedNode = self.get("selectedNode") - local expandedNodes = self.get("expandedNodes") - local scrollOffset = self.get("scrollOffset") - - for y = 1, height do - local nodeInfo = flatNodes[y + scrollOffset] - if nodeInfo then - local node = nodeInfo.node - local level = nodeInfo.level - local indent = string.rep(" ", level) - - -- Expand/Collapse Symbol - local symbol = " " - if node.children and #node.children > 0 then - symbol = expandedNodes[node] and "\31" or "\16" - end - - local bg = node == selectedNode and self.get("selectedColor") or self.get("background") - local text = indent .. symbol .." " .. (node.text or "Node") - - self:blit(1, y, text .. string.rep(" ", self.get("width") - #text), - string.rep(tHex[self.get("nodeColor")], self.get("width")), - string.rep(tHex[bg], self.get("width"))) - else - -- Leere Zeile - self:blit(1, y, string.rep(" ", self.get("width")), - string.rep(tHex[self.get("foreground")], self.get("width")), - string.rep(tHex[self.get("background")], self.get("width"))) - end - end -end - -return Tree -", - ["plugins/xml.lua"] = "local errorManager = require("errorManager") - -local function parseTag(str) - local tag = { - attributes = {} - } - tag.name = str:match("<(%w+)") - for k,v in str:gmatch('%s(%w+)="([^"]-)"') do - tag.attributes[k] = v - end - return tag -end - -local function parseXML(self, xmlString) - local stack = {} - local root = {children = {}} - local current = root - local inCDATA = false - local cdataContent = "" - - for line in xmlString:gmatch("[^\r\n]+") do - line = line:match("^%s*(.-)%s*$") - self.basalt.LOGGER.debug("Parsing line: " .. line) - - if line:match("^$") and inCDATA then - inCDATA = false - current.content = cdataContent - elseif inCDATA then - cdataContent = cdataContent .. line .. "\n" - elseif line:match("^<[^/]") then - local tag = parseTag(line) - tag.children = {} - tag.content = "" - table.insert(current.children, tag) - - if not line:match("/>$") then - table.insert(stack, current) - current = tag - end - elseif line:match("^ 0 then - createElements(node, element, scope) - end - end - end - end - - createElements(tree, self, scope) - return self -end - -return { - BaseElement = BaseElement, - Container = Container -} - -", - ["elements/List.lua"] = "local VisualElement = require("elements/VisualElement") - ----@class List : VisualElement -local List = setmetatable({}, VisualElement) -List.__index = List - ----@property items table List of items to display -List.defineProperty(List, "items", {default = {}, type = "table", canTriggerRender = true}) ----@property selectedIndex number Currently selected item index -List.defineProperty(List, "selectedIndex", {default = 0, type = "number", canTriggerRender = true}) ----@property selectable boolean Whether items can be selected -List.defineProperty(List, "selectable", {default = true, type = "boolean"}) ----@property offset number Scrolling offset -List.defineProperty(List, "offset", {default = 0, type = "number", canTriggerRender = true}) ----@property selectedColor color Color for selected item -List.defineProperty(List, "selectedColor", {default = colors.blue, type = "number"}) - -List.listenTo(List, "mouse_click") -List.listenTo(List, "mouse_scroll") - -function List.new() - local self = setmetatable({}, List):__init() - self.set("width", 16) - self.set("height", 8) - self.set("background", colors.gray) - return self -end - -function List:init(props, basalt) - VisualElement.init(self, props, basalt) - self.set("type", "List") -end - -function List:addItem(text) - local items = self.get("items") - table.insert(items, text) - self:updateRender() - return self -end - -function List:removeItem(index) - local items = self.get("items") - table.remove(items, index) - self:updateRender() - return self -end - -function List:clear() - self.set("items", {}) - self.set("selectedIndex", 0) - self:updateRender() - return self -end - -function List:mouse_click(button, x, y) - if button == 1 and self:isInBounds(x, y) and self.get("selectable") then - local _, index = self:getRelativePosition(x, y) - - local adjustedIndex = index + self.get("offset") - local items = self.get("items") - - if adjustedIndex <= #items then - local item = items[adjustedIndex] - self.set("selectedIndex", adjustedIndex) - - if type(item) == "table" and item.callback then - item.callback(self) - end - - self:fireEvent("select", adjustedIndex, item) - self:updateRender() - return true - end - end - return false -end - -function List:mouse_scroll(direction, x, y) - if self:isInBounds(x, y) then - local offset = self.get("offset") - local maxOffset = math.max(0, #self.get("items") - self.get("height")) - - offset = math.min(maxOffset, math.max(0, offset + direction)) - self.set("offset", offset) - return true - end -end - -function List:onSelect(callback) - self:registerCallback("select", callback) - return self -end - -function List:render() - VisualElement.render(self) - - local items = self.get("items") - local height = self.get("height") - local offset = self.get("offset") - local selected = self.get("selectedIndex") - local width = self.get("width") - - for i = 1, height do - local itemIndex = i + offset - local item = items[itemIndex] - - if item then - if type(item) == "table" and item.separator then - local separatorChar = (item.text or "-"):sub(1,1) - local separatorText = string.rep(separatorChar, width) - local fg = item.foreground or self.get("foreground") - local bg = item.background or self.get("background") - - self:textBg(1, i, string.rep(" ", width), bg) - self:textFg(1, i, separatorText, fg) - else - local text = type(item) == "table" and item.text or item - local isSelected = itemIndex == selected - - local bg = isSelected and - (item.selectedBackground or self.get("selectedColor")) or - (item.background or self.get("background")) - - local fg = isSelected and - (item.selectedForeground or colors.white) or - (item.foreground or self.get("foreground")) - - self:textBg(1, i, string.rep(" ", width), bg) - self:textFg(1, i, text, fg) - end - end - end -end - -return List -", - ["libraries/utils.lua"] = "local 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 - -function utils.copy(obj) - local new = {} - for k,v in pairs(obj)do - new[k] = v - end - return new -end - -function utils.reverse(t) - local reversed = {} - for i = #t, 1, -1 do - table.insert(reversed, t[i]) - end - return reversed -end - -function utils.uuid() - 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 - -function utils.split(str, sep) - local parts = {} - local start = 1 - local len = len(str) - local splitIndex = 1 - - while true do - local index = str:find(sep, start, true) - if not index then - parts[splitIndex] = str:sub(start, len) - break - end - - parts[splitIndex] = str:sub(start, index - 1) - start = index + 1 - splitIndex = splitIndex + 1 - end - - return parts -end - -return utils", - ["elements/ProgressBar.lua"] = "local VisualElement = require("elements/VisualElement") - ----@class ProgressBar : VisualElement -local ProgressBar = setmetatable({}, VisualElement) -ProgressBar.__index = ProgressBar - ----@property progress number Current progress (0-100) -ProgressBar.defineProperty(ProgressBar, "progress", {default = 0, type = "number", canTriggerRender = true}) ----@property showPercentage boolean Show percentage text -ProgressBar.defineProperty(ProgressBar, "showPercentage", {default = false, type = "boolean"}) ----@property progressColor color Progress bar color -ProgressBar.defineProperty(ProgressBar, "progressColor", {default = colors.lime, type = "number"}) - -function ProgressBar.new() - local self = setmetatable({}, ProgressBar):__init() - self.set("width", 10) - self.set("height", 1) - return self -end - -function ProgressBar:init(props, basalt) - VisualElement.init(self, props, basalt) - self.set("type", "ProgressBar") -end - -function ProgressBar:render() - VisualElement.render(self) - local width = self.get("width") - local progress = math.min(100, math.max(0, self.get("progress"))) - local fillWidth = math.floor((width * progress) / 100) - - self:textBg(1, 1, string.rep(" ", fillWidth), self.get("progressColor")) - - if self.get("showPercentage") then - local text = tostring(progress).."%" - local x = math.floor((width - #text) / 2) + 1 - self:textFg(x, 1, text, self.get("foreground")) - end -end - -return ProgressBar", - ["elementManager.lua"] = "local args = table.pack(...) -local dir = fs.getDir(args[2] or "basalt") -local subDir = args[1] -if(dir==nil)then - error("Unable to find directory "..args[2].." please report this bug to our discord.") -end - -local log = require("log") -local defaultPath = package.path -local format = "path;/path/?.lua;/path/?/init.lua;" -local main = format:gsub("path", dir) - -local ElementManager = {} -ElementManager._elements = {} -ElementManager._plugins = {} -ElementManager._APIs = {} -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 - -log.info("Loading plugins from "..pluginsDirectory) -if fs.exists(pluginsDirectory) then - for _, file in ipairs(fs.list(pluginsDirectory)) do - local name = file:match("(.+).lua") - if name then - log.debug("Found plugin: "..name) - local plugin = require(fs.combine("plugins", name)) - if type(plugin) == "table" then - for k,v in pairs(plugin) do - if(k ~= "API")then - if(ElementManager._plugins[k]==nil)then - ElementManager._plugins[k] = {} - end - table.insert(ElementManager._plugins[k], v) - else - ElementManager._APIs[name] = v - end - end - end - end - end -end - -function ElementManager.loadElement(name) - if not ElementManager._elements[name].loaded then - package.path = main.."rom/?" - local element = require(fs.combine("elements", name)) - package.path = defaultPath - ElementManager._elements[name] = { - class = element, - plugins = element.plugins, - loaded = true - } - log.debug("Loaded element: "..name) - - if(ElementManager._plugins[name]~=nil)then - for _, plugin in pairs(ElementManager._plugins[name]) do - if(plugin.setup)then - plugin.setup(element) - end - - if(plugin.hooks)then - for methodName, hooks in pairs(plugin.hooks) do - local original = element[methodName] - if(type(original)~="function")then - error("Element "..name.." does not have a method "..methodName) - end - if(type(hooks)=="function")then - element[methodName] = function(self, ...) - local result = original(self, ...) - local hookResult = hooks(self, ...) - return hookResult == nil and result or hookResult - end - elseif(type(hooks)=="table")then - element[methodName] = function(self, ...) - if hooks.pre then hooks.pre(self, ...) end - local result = original(self, ...) - if hooks.post then hooks.post(self, ...) end - return result - end - end - end - end - - for funcName, func in pairs(plugin) do - if funcName ~= "setup" and funcName ~= "hooks" then - element[funcName] = func - end - end - end - end - end -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.getAPI(name) - return ElementManager._APIs[name] -end - -return ElementManager", - ["elements/Checkbox.lua"] = "local VisualElement = require("elements/VisualElement") - ----@class Checkbox : VisualElement -local Checkbox = setmetatable({}, VisualElement) -Checkbox.__index = Checkbox - ----@property checked boolean Whether checkbox is checked -Checkbox.defineProperty(Checkbox, "checked", {default = false, type = "boolean", canTriggerRender = true}) ----@property text string Label text -Checkbox.defineProperty(Checkbox, "text", {default = "", type = "string", canTriggerRender = true}) ----@property symbol string Check symbol -Checkbox.defineProperty(Checkbox, "symbol", {default = "x", type = "string"}) - -Checkbox.listenTo(Checkbox, "mouse_click") - -function Checkbox.new() - local self = setmetatable({}, Checkbox):__init() - self.set("width", 1) - self.set("height", 1) - return self -end - -function Checkbox:init(props, basalt) - VisualElement.init(self, props, basalt) - self.set("type", "Checkbox") -end - -function Checkbox:mouse_click(button, x, y) - if VisualElement.mouse_click(self, button, x, y) then - self.set("checked", not self.get("checked")) - self:fireEvent("change", self.get("checked")) - return true - end -end - -function Checkbox:render() - VisualElement.render(self) - - local text = self.get("checked") and self.get("symbol") or " " - self:textFg(1, 1, "["..text.."]", self.get("foreground")) - - local label = self.get("text") - if #label > 0 then - self:textFg(4, 1, label, self.get("foreground")) - end -end - -return Checkbox", ["plugins/reactive.lua"] = "local errorManager = require("errorManager") local PropertySystem = require("propertySystem") local log = require("log") @@ -5840,6 +3139,263 @@ BaseElement.hooks = { return { BaseElement = BaseElement } +", + ["render.lua"] = "local Render = {} +Render.__index = Render +local colorChars = require("libraries/colorHex") + +function Render.new(terminal) + local self = setmetatable({}, Render) + self.terminal = terminal + self.width, self.height = terminal.getSize() + + self.buffer = { + text = {}, + fg = {}, + bg = {}, + dirtyRects = {} + } + + 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) + end + + return self +end + +function Render:addDirtyRect(x, y, width, height) + table.insert(self.buffer.dirtyRects, { + x = x, + y = y, + width = width, + height = height + }) +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:addDirtyRect(x, y, #text, 1) + + 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) + end + end + + self:addDirtyRect(x, y, width, height) + 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:addDirtyRect(x, y, #text, 1) + + return self +end + +function Render:textBg(x, y, text, bg) + if y < 1 or y > self.height then return self end + bg = colorChars[bg] or "f" + + self.buffer.text[y] = self.buffer.text[y]:sub(1,x-1) .. text .. self.buffer.text[y]:sub(x+#text) + self.buffer.bg[y] = self.buffer.bg[y]:sub(1,x-1) .. bg:rep(#text) .. self.buffer.bg[y]:sub(x+#text) + self:addDirtyRect(x, y, #text, 1) + + 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:addDirtyRect(x, y, #text, 1) + + 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:addDirtyRect(x, y, #fg, 1) + + 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:addDirtyRect(x, y, #bg, 1) + + 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:addDirtyRect(x, y, #text, 1) + + 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:addDirtyRect(1, y, self.width, 1) + end + return self +end + +function Render:render() + + local mergedRects = {} + for _, rect in ipairs(self.buffer.dirtyRects) do + local merged = false + for _, existing in ipairs(mergedRects) do + if self:rectOverlaps(rect, existing) then + self:mergeRects(existing, rect) + merged = true + break + end + end + if not merged then + table.insert(mergedRects, rect) + end + end + + for _, rect in ipairs(mergedRects) do + for y = rect.y, rect.y + rect.height - 1 do + if y >= 1 and y <= self.height then + self.terminal.setCursorPos(rect.x, y) + self.terminal.blit( + self.buffer.text[y]:sub(rect.x, rect.x + rect.width - 1), + self.buffer.fg[y]:sub(rect.x, rect.x + rect.width - 1), + self.buffer.bg[y]:sub(rect.x, rect.x + rect.width - 1) + ) + end + end + end + + self.buffer.dirtyRects = {} + + if self.blink then + self.terminal.setCursorPos(self.xCursor, self.yCursor) + self.terminal.setCursorBlink(true) + else + self.terminal.setCursorBlink(false) + end + + return self +end + +function Render:rectOverlaps(r1, r2) + return not (r1.x + r1.width <= r2.x or + r2.x + r2.width <= r1.x or + r1.y + r1.height <= r2.y or + r2.y + r2.height <= r1.y) +end + +function Render:mergeRects(target, source) + local x1 = math.min(target.x, source.x) + local y1 = math.min(target.y, source.y) + local x2 = math.max(target.x + target.width, source.x + source.width) + local y2 = math.max(target.y + target.height, source.y + source.height) + + target.x = x1 + target.y = y1 + target.width = x2 - x1 + target.height = y2 - y1 +end + +function Render:setCursor(x, y, blink) + self.terminal.setCursorPos(x, y) + self.terminal.setCursorBlink(blink) + self.xCursor = x + self.yCursor = y + self.blink = blink + 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 Render", + ["plugins/pluginTemplate.lua"] = "-- Will temporary exist so that we don't lose track of how the plugin system works + +local VisualElement = {hooks={init={}}} + +-- Called on Class level to define properties and setup before instance is created +function VisualElement.setup(element) + element.defineProperty(element, "testProp", {default = 5, type = "number"}) +end + +-- Hooks into existing methods (you can also use init.pre or init.post) +function VisualElement.hooks.init(self) + --self.basalt.LOGGER.debug("VisualElement initialized") +end + +-- Adds a new method to the class +function VisualElement:testFunc() + --self.basalt.LOGGER.debug("Hello World", self.get("testProp")) +end + +return { + VisualElement = VisualElement +} + ", ["elements/Button.lua"] = "local elementManager = require("elementManager") local VisualElement = elementManager.getElement("VisualElement") @@ -5877,6 +3433,114 @@ function Button:render() end return Button", + ["errorManager.lua"] = "local 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) + if errorHandler.errorHandled then + error() + end + 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) + errorHandler.errorHandled = true + error() +end + +return errorHandler", ["main.lua"] = "local elementManager = require("elementManager") local errorManager = require("errorManager") local propertySystem = require("propertySystem") @@ -6082,5 +3746,2341 @@ end return basalt", + ["elements/ProgressBar.lua"] = "local VisualElement = require("elements/VisualElement") + +---@class ProgressBar : VisualElement +local ProgressBar = setmetatable({}, VisualElement) +ProgressBar.__index = ProgressBar + +---@property progress number Current progress (0-100) +ProgressBar.defineProperty(ProgressBar, "progress", {default = 0, type = "number", canTriggerRender = true}) +---@property showPercentage boolean Show percentage text +ProgressBar.defineProperty(ProgressBar, "showPercentage", {default = false, type = "boolean"}) +---@property progressColor color Progress bar color +ProgressBar.defineProperty(ProgressBar, "progressColor", {default = colors.lime, type = "number"}) + +function ProgressBar.new() + local self = setmetatable({}, ProgressBar):__init() + self.set("width", 10) + self.set("height", 1) + return self +end + +function ProgressBar:init(props, basalt) + VisualElement.init(self, props, basalt) + self.set("type", "ProgressBar") +end + +function ProgressBar:render() + VisualElement.render(self) + local width = self.get("width") + local progress = math.min(100, math.max(0, self.get("progress"))) + local fillWidth = math.floor((width * progress) / 100) + + self:textBg(1, 1, string.rep(" ", fillWidth), self.get("progressColor")) + + if self.get("showPercentage") then + local text = tostring(progress).."%" + local x = math.floor((width - #text) / 2) + 1 + self:textFg(x, 1, text, self.get("foreground")) + end +end + +return ProgressBar", + ["plugins/theme.lua"] = "local defaultTheme = { + default = { + background = colors.lightGray, + foreground = colors.black, + }, + BaseFrame = { + background = colors.white, + foreground = colors.black, + + Frame = { + background = colors.black, + names = { + basaltDebugLogClose = { + background = colors.blue, + foreground = colors.white + } + }, + }, + Button = { + background = "{self.clicked and colors.black or colors.cyan}", + foreground = "{self.clicked and colors.cyan or colors.black}", + }, + + names = { + basaltDebugLog = { + background = colors.red, + foreground = colors.white + }, + test = { + background = "{self.clicked and colors.black or colors.green}", + foreground = "{self.clicked and colors.green or colors.black}" + } + }, + } +} + +local themes = { + default = defaultTheme +} + +local currentTheme = "default" + +local BaseElement = { + hooks = { + postInit = { + pre = function(self) + self:applyTheme() + end} + } +} + +function BaseElement.____getElementPath(self, types) + if types then + table.insert(types, 1, self._values.type) + else + types = {self._values.type} + end + local parent = self.parent + if parent then + return parent.____getElementPath(parent, types) + else + return types + end +end + +local function lookUpTemplate(theme, path) + local current = theme + + for i = 1, #path do + local found = false + local types = path[i] + + for _, elementType in ipairs(types) do + if current[elementType] then + current = current[elementType] + found = true + break + end + end + + if not found then + return nil + end + end + + return current +end + +local function getDefaultProperties(theme, elementType) + local result = {} + if theme.default then + for k,v in pairs(theme.default) do + if type(v) ~= "table" then + result[k] = v + end + end + + if theme.default[elementType] then + for k,v in pairs(theme.default[elementType]) do + if type(v) ~= "table" then + result[k] = v + end + end + end + end + return result +end + +local function applyNamedStyles(result, theme, elementType, elementName, themeTable) + if theme.default and theme.default.names and theme.default.names[elementName] then + for k,v in pairs(theme.default.names[elementName]) do + if type(v) ~= "table" then result[k] = v end + end + end + + if theme.default and theme.default[elementType] and theme.default[elementType].names + and theme.default[elementType].names[elementName] then + for k,v in pairs(theme.default[elementType].names[elementName]) do + if type(v) ~= "table" then result[k] = v end + end + end + + if themeTable and themeTable.names and themeTable.names[elementName] then + for k,v in pairs(themeTable.names[elementName]) do + if type(v) ~= "table" then result[k] = v end + end + end +end + +local function collectThemeProps(theme, path, elementType, elementName) + local result = {} + local themeTable = lookUpTemplate(theme, path) + if themeTable then + for k,v in pairs(themeTable) do + if type(v) ~= "table" then + result[k] = v + end + end + end + + if next(result) == nil then + result = getDefaultProperties(theme, elementType) + end + + applyNamedStyles(result, theme, elementType, elementName, themeTable) + + return result +end + + function BaseElement:applyTheme() + local styles = self:getTheme() + if(styles ~= nil) then + for prop, value in pairs(styles) do + self.set(prop, value) + end + end +end + +function BaseElement:getTheme() + local path = self:____getElementPath() + local elementType = self.get("type") + local elementName = self.get("name") + + return collectThemeProps(themes[currentTheme], path, elementType, elementName) +end + +local themeAPI = { + setTheme = function(newTheme) + defaultTheme = newTheme + end, + + getTheme = function() + return defaultTheme + end, + + loadTheme = function(path) + local file = fs.open(path, "r") + if file then + local content = file.readAll() + file.close() + defaultTheme = textutils.unserializeJSON(content) + end + end +} + +local Theme = { + BaseElement = BaseElement, + API = themeAPI +} + +return Theme +", + ["elements/BaseElement.lua"] = "local PropertySystem = require("propertySystem") +local uuid = require("/libraries/utils").uuid + +--- The base class for all UI elements in Basalt +--- @class BaseElement : PropertySystem +local BaseElement = setmetatable({}, PropertySystem) +BaseElement.__index = BaseElement +BaseElement._events = {} + +--- @property type string BaseElement The type identifier of the element +BaseElement.defineProperty(BaseElement, "type", {default = {"BaseElement"}, type = "string", setter=function(self, value) + if type(value) == "string" then + table.insert(self._values.type, 1, value) + return self._values.type + end + return value +end, getter = function(self, _, index) + if index~= nil and index < 1 then + return self._values.type + end + return self._values.type[index or 1] +end}) + +--- @property id string BaseElement The unique identifier for the element +BaseElement.defineProperty(BaseElement, "id", {default = "", type = "string", readonly = true}) + +--- @property name string BaseElement The name of the element +BaseElement.defineProperty(BaseElement, "name", {default = "", type = "string"}) + +--- @property eventCallbacks table {} Table containing all registered event callbacks +BaseElement.defineProperty(BaseElement, "eventCallbacks", {default = {}, type = "table"}) + +--- Registers an event that this class can listen to +--- @param class table The class to add the event to +--- @param eventName string The name of the event to register +--- @usage BaseElement.listenTo(MyClass, "mouse_click") +function BaseElement.listenTo(class, eventName) + if not class._events then + class._events = {} + end + class._events[eventName] = true +end + +--- Creates a new BaseElement instance +--- @param props table The properties to initialize the element with +--- @param basalt table The basalt instance +--- @return table The newly created BaseElement instance +--- @usage local element = BaseElement.new("myId", basalt) +function BaseElement.new() + local self = setmetatable({}, BaseElement):__init() + return self +end + +--- Initializes the BaseElement instance +--- @param props table The properties to initialize the element with +--- @param basalt table The basalt instance +--- @return table self The initialized instance +function BaseElement:init(props, basalt) + self._props = props + self._values.id = uuid() + 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, ...) + return self + end + end + end + return self +end + +--- Post initialization hook +--- @return table self The BaseElement instance +function BaseElement:postInit() + if(self._props)then + for k,v in pairs(self._props)do + self.set(k, v) + end + end + self._props = nil + return self +end + +--- Checks if the element is a specific type +--- @param type string The type to check for +--- @return boolean Whether the element is of the specified type +function BaseElement:isType(type) + for _, t in ipairs(self._values.type) do + if t == type then + return true + end + end + return false +end + +--- Enables or disables event listening for a specific event +--- @param eventName string The name of the event to listen for +--- @param enable? boolean Whether to enable or disable the event (default: true) +--- @return table self The BaseElement instance +--- @usage element:listenEvent("mouse_click", true) +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 + +--- Registers a callback function for an event +--- @param event string The event to register the callback for +--- @param callback function The callback function to register +--- @return table self The BaseElement instance +--- @usage element:registerCallback("mouse_click", function(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 + +--- Triggers an event and calls all registered callbacks +--- @param event string The event to fire +--- @param ... any Additional arguments to pass to the callbacks +--- @return table self The BaseElement instance +--- @usage element:fireEvent("mouse_click", 1, 2) +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 + +--- Handles all events +--- @param event string The event to handle +--- @vararg any The arguments for the event +--- @return boolean? handled Whether the event was handled +function BaseElement:dispatchEvent(event, ...) + if self[event] then + return self[event](self, ...) + end + return self:handleEvent(event, ...) +end + +--- The default event handler for all events +--- @param event string The event to handle +--- @vararg any The arguments for the event +--- @return boolean? handled Whether the event was handled +function BaseElement:handleEvent(event, ...) + return false +end + +function BaseElement:getBaseFrame() + if self.parent then + return self.parent:getBaseFrame() + end + return self +end + +function BaseElement:destroy() + +end + +--- Requests a render update for this element +--- @usage element:updateRender() +function BaseElement:updateRender() + if(self.parent) then + self.parent:updateRender() + else + self._renderUpdate = true + end +end + +return BaseElement", + ["plugins/benchmark.lua"] = "local log = require("log") + +local activeProfiles = setmetatable({}, {__mode = "k"}) + +local function createProfile() + return { + methods = {}, + } +end + +local function wrapMethod(element, methodName) + local originalMethod = element[methodName] + + if not activeProfiles[element] then + activeProfiles[element] = createProfile() + end + if not activeProfiles[element].methods[methodName] then + activeProfiles[element].methods[methodName] = { + calls = 0, + totalTime = 0, + minTime = math.huge, + maxTime = 0, + lastTime = 0, + startTime = 0, + path = {}, + methodName = methodName, + originalMethod = originalMethod + } + end + + element[methodName] = function(self, ...) + self:startProfile(methodName) + local result = originalMethod(self, ...) + self:endProfile(methodName) + return result + end +end + +local BaseElement = {} + +function BaseElement:startProfile(methodName) + local profile = activeProfiles[self] + if not profile then + profile = createProfile() + activeProfiles[self] = profile + end + + if not profile.methods[methodName] then + profile.methods[methodName] = { + calls = 0, + totalTime = 0, + minTime = math.huge, + maxTime = 0, + lastTime = 0, + startTime = 0, + path = {}, + methodName = methodName + } + end + + local methodProfile = profile.methods[methodName] + methodProfile.startTime = os.clock() * 1000 + methodProfile.path = {} + + local current = self + while current do + table.insert(methodProfile.path, 1, current.get("name") or current.get("id")) + current = current.parent + end + return self +end + +function BaseElement:endProfile(methodName) + local profile = activeProfiles[self] + if not profile or not profile.methods[methodName] then return self end + + local methodProfile = profile.methods[methodName] + local endTime = os.clock() * 1000 + local duration = endTime - methodProfile.startTime + + methodProfile.calls = methodProfile.calls + 1 + methodProfile.totalTime = methodProfile.totalTime + duration + methodProfile.minTime = math.min(methodProfile.minTime, duration) + methodProfile.maxTime = math.max(methodProfile.maxTime, duration) + methodProfile.lastTime = duration + + return self +end + +function BaseElement:benchmark(methodName) + if not self[methodName] then + log.error("Method " .. methodName .. " does not exist") + return self + end + + activeProfiles[self] = createProfile() + activeProfiles[self].methodName = methodName + activeProfiles[self].isRunning = true + + wrapMethod(self, methodName) + return self +end + +function BaseElement:logBenchmark(methodName) + local profile = activeProfiles[self] + if not profile or not profile.methods[methodName] then return self end + + local stats = profile.methods[methodName] + if stats then + local averageTime = stats.calls > 0 and (stats.totalTime / stats.calls) or 0 + log.info(string.format( + "Benchmark results for %s.%s: " .. + "Path: %s " .. + "Calls: %d " .. + "Average time: %.2fms " .. + "Min time: %.2fms " .. + "Max time: %.2fms " .. + "Last time: %.2fms " .. + "Total time: %.2fms", + table.concat(stats.path, "."), + stats.methodName, + table.concat(stats.path, "/"), + stats.calls, + averageTime, + stats.minTime ~= math.huge and stats.minTime or 0, + stats.maxTime, + stats.lastTime, + stats.totalTime + )) + end + return self +end + +function BaseElement:stopBenchmark(methodName) + local profile = activeProfiles[self] + if not profile or not profile.methods[methodName] then return self end + + local stats = profile.methods[methodName] + if stats and stats.originalMethod then + self[methodName] = stats.originalMethod + end + + profile.methods[methodName] = nil + if not next(profile.methods) then + activeProfiles[self] = nil + end + return self +end + +function BaseElement:getBenchmarkStats(methodName) + local profile = activeProfiles[self] + if not profile or not profile.methods[methodName] then return nil end + + local stats = profile.methods[methodName] + return { + averageTime = stats.totalTime / stats.calls, + totalTime = stats.totalTime, + calls = stats.calls, + minTime = stats.minTime, + maxTime = stats.maxTime, + lastTime = stats.lastTime + } +end + +local Container = {} + +function Container:benchmarkContainer(methodName) + self:benchmark(methodName) + + for _, child in pairs(self.get("children")) do + child:benchmark(methodName) + + if child:isType("Container") then + child:benchmarkContainer(methodName) + end + end + return self +end + +function Container:logContainerBenchmarks(methodName, depth) + depth = depth or 0 + local indent = string.rep(" ", depth) + local childrenTotalTime = 0 + local childrenStats = {} + + for _, child in pairs(self.get("children")) do + local profile = activeProfiles[child] + if profile and profile.methods[methodName] then + local stats = profile.methods[methodName] + childrenTotalTime = childrenTotalTime + stats.totalTime + table.insert(childrenStats, { + element = child, + type = child.get("type"), + calls = stats.calls, + totalTime = stats.totalTime, + avgTime = stats.totalTime / stats.calls + }) + end + end + + local profile = activeProfiles[self] + if profile and profile.methods[methodName] then + local stats = profile.methods[methodName] + local selfTime = stats.totalTime - childrenTotalTime + local avgSelfTime = selfTime / stats.calls + + log.info(string.format( + "%sBenchmark %s (%s): " .. + "%.2fms/call (Self: %.2fms/call) " .. + "[Total: %dms, Calls: %d]", + indent, + self.get("type"), + methodName, + stats.totalTime / stats.calls, + avgSelfTime, + stats.totalTime, + stats.calls + )) + + if #childrenStats > 0 then + for _, childStat in ipairs(childrenStats) do + if childStat.element:isType("Container") then + childStat.element:logContainerBenchmarks(methodName, depth + 1) + else + log.info(string.format("%s> %s: %.2fms/call [Total: %dms, Calls: %d]", + indent .. " ", + childStat.type, + childStat.avgTime, + childStat.totalTime, + childStat.calls + )) + end + end + end + end + + return self +end + +function Container:stopContainerBenchmark(methodName) + for _, child in pairs(self.get("children")) do + if child:isType("Container") then + child:stopContainerBenchmark(methodName) + else + child:stopBenchmark(methodName) + end + end + + self:stopBenchmark(methodName) + return self +end + +local API = { + start = function(name, options) + options = options or {} + local profile = createProfile() + profile.name = name + profile.startTime = os.clock() * 1000 + profile.custom = true + activeProfiles[name] = profile + end, + + stop = function(name) + local profile = activeProfiles[name] + if not profile or not profile.custom then return end + + local endTime = os.clock() * 1000 + local duration = endTime - profile.startTime + + profile.calls = profile.calls + 1 + profile.totalTime = profile.totalTime + duration + profile.minTime = math.min(profile.minTime, duration) + profile.maxTime = math.max(profile.maxTime, duration) + profile.lastTime = duration + + log.info(string.format( + "Custom Benchmark '%s': " .. + "Calls: %d " .. + "Average time: %.2fms " .. + "Min time: %.2fms " .. + "Max time: %.2fms " .. + "Last time: %.2fms " .. + "Total time: %.2fms", + name, + profile.calls, + profile.totalTime / profile.calls, + profile.minTime, + profile.maxTime, + profile.lastTime, + profile.totalTime + )) + end, + + getStats = function(name) + local profile = activeProfiles[name] + if not profile then return nil end + + return { + averageTime = profile.totalTime / profile.calls, + totalTime = profile.totalTime, + calls = profile.calls, + minTime = profile.minTime, + maxTime = profile.maxTime, + lastTime = profile.lastTime + } + end, + + clear = function(name) + activeProfiles[name] = nil + end, + + clearAll = function() + for k,v in pairs(activeProfiles) do + if v.custom then + activeProfiles[k] = nil + end + end + end +} + +return { + BaseElement = BaseElement, + Container = Container, + API = API +}", + ["plugins/debug.lua"] = "local log = require("log") +local tHex = require("libraries/colorHex") + +local maxLines = 10 +local isVisible = false + +local function createDebugger(element) + local elementInfo = { + renderCount = 0, + eventCount = {}, + lastRender = os.epoch("utc"), + properties = {}, + children = {} + } + + return { + trackProperty = function(name, value) + elementInfo.properties[name] = value + end, + + trackRender = function() + elementInfo.renderCount = elementInfo.renderCount + 1 + elementInfo.lastRender = os.epoch("utc") + end, + + trackEvent = function(event) + elementInfo.eventCount[event] = (elementInfo.eventCount[event] or 0) + 1 + end, + + dump = function() + return { + type = element.get("type"), + id = element.get("id"), + stats = elementInfo + } + end + } +end + +local BaseElement = { + debug = function(self, level) + self._debugger = createDebugger(self) + self._debugLevel = level or DEBUG_LEVELS.INFO + return self + end, + + dumpDebug = function(self) + if not self._debugger then return end + return self._debugger.dump() + end +} + +local BaseFrame = { + showDebugLog = function(self) + if not self._debugFrame then + local width = self.get("width") + local height = self.get("height") + self._debugFrame = self:addFrame("basaltDebugLog") + :setWidth(width) + :setHeight(height) + :setZ(999) + :listenEvent("mouse_scroll", true) + self.basalt.LOGGER.debug("Created debug log frame " .. self._debugFrame.get("name")) + + self._debugFrame:addButton("basaltDebugLogClose") + :setWidth(9) + :setHeight(1) + :setX(width - 8) + :setY(height) + :setText("Close") + :onMouseClick(function() + self:hideDebugLog() + end) + + self._debugFrame._scrollOffset = 0 + self._debugFrame._processedLogs = {} + + local function wrapText(text, width) + local lines = {} + while #text > 0 do + local line = text:sub(1, width) + table.insert(lines, line) + text = text:sub(width + 1) + end + return lines + end + + local function processLogs() + local processed = {} + local width = self._debugFrame.get("width") + + for _, entry in ipairs(log._logs) do + local lines = wrapText(entry.message, width) + for _, line in ipairs(lines) do + table.insert(processed, { + text = line, + level = entry.level + }) + end + end + return processed + end + + local totalLines = #processLogs() - self.get("height") + self._scrollOffset = totalLines + + local originalRender = self._debugFrame.render + self._debugFrame.render = function(frame) + originalRender(frame) + frame._processedLogs = processLogs() + + local height = frame.get("height")-2 + local totalLines = #frame._processedLogs + local maxScroll = math.max(0, totalLines - height) + frame._scrollOffset = math.min(frame._scrollOffset, maxScroll) + + for i = 1, height-2 do + local logIndex = i + frame._scrollOffset + local entry = frame._processedLogs[logIndex] + + if entry then + local color = entry.level == log.LEVEL.ERROR and colors.red + or entry.level == log.LEVEL.WARN and colors.yellow + or entry.level == log.LEVEL.DEBUG and colors.lightGray + or colors.white + + frame:textFg(2, i, entry.text, color) + end + end + end + + local baseDispatchEvent = self._debugFrame.dispatchEvent + self._debugFrame.dispatchEvent = function(self, event, direction, ...) + if(event == "mouse_scroll") then + self._scrollOffset = math.max(0, self._scrollOffset + direction) + self:updateRender() + return true + else + baseDispatchEvent(self, event, direction, ...) + end + end + end + self._debugFrame.set("visible", true) + return self + end, + + hideDebugLog = function(self) + if self._debugFrame then + self._debugFrame.set("visible", false) + end + return self + end, + + toggleDebugLog = function(self) + if self._debugFrame and self._debugFrame:isVisible() then + self:hideDebugLog() + else + self:showDebugLog() + end + return self + end +} + + +local Container = { + debugChildren = function(self, level) + self:debug(level) + for _, child in pairs(self.get("children")) do + if child.debug then + child:debug(level) + end + end + return self + end +} + +return { + BaseElement = BaseElement, + Container = Container, + BaseFrame = BaseFrame, +} +", + ["plugins/animation.lua"] = "local Animation = {} +Animation.__index = Animation + +local registeredAnimations = {} + +function Animation.registerAnimation(name, handlers) + registeredAnimations[name] = handlers + + Animation[name] = function(self, ...) + local args = {...} + local easing = "linear" + if(type(args[#args]) == "string") then + easing = table.remove(args, #args) + end + local duration = table.remove(args, #args) + return self:addAnimation(name, args, duration, easing) + end +end + +local easings = { + linear = function(progress) + return progress + end, + + easeInQuad = function(progress) + return progress * progress + end, + + easeOutQuad = function(progress) + return 1 - (1 - progress) * (1 - progress) + end, + + easeInOutQuad = function(progress) + if progress < 0.5 then + return 2 * progress * progress + end + return 1 - (-2 * progress + 2)^2 / 2 + end +} + +function Animation.registerEasing(name, func) + easings[name] = func +end + +local AnimationInstance = {} +AnimationInstance.__index = AnimationInstance + +function AnimationInstance.new(element, animType, args, duration, easing) + local self = setmetatable({}, AnimationInstance) + self.element = element + self.type = animType + self.args = args + self.duration = duration + self.startTime = 0 + self.isPaused = false + self.handlers = registeredAnimations[animType] + self.easing = easing + return self +end + +function AnimationInstance:start() + self.startTime = os.epoch("local") / 1000 + if self.handlers.start then + self.handlers.start(self) + end +end + +function AnimationInstance:update(elapsed) + local rawProgress = math.min(1, elapsed / self.duration) + local progress = easings[self.easing](rawProgress) + return self.handlers.update(self, progress) +end + +function AnimationInstance:complete() + if self.handlers.complete then + self.handlers.complete(self) + end +end + +function Animation.new(element) + local self = {} + self.element = element + self.sequences = {{}} + self.sequenceCallbacks = {} + self.currentSequence = 1 + self.timer = nil + setmetatable(self, Animation) + return self +end + +function Animation:sequence() + table.insert(self.sequences, {}) + self.currentSequence = #self.sequences + self.sequenceCallbacks[self.currentSequence] = { + start = nil, + update = nil, + complete = nil + } + return self +end + +function Animation:onStart(callback) + if not self.sequenceCallbacks[self.currentSequence] then + self.sequenceCallbacks[self.currentSequence] = {} + end + self.sequenceCallbacks[self.currentSequence].start = callback + return self +end + +function Animation:onUpdate(callback) + if not self.sequenceCallbacks[self.currentSequence] then + self.sequenceCallbacks[self.currentSequence] = {} + end + self.sequenceCallbacks[self.currentSequence].update = callback + return self +end + +function Animation:onComplete(callback) + if not self.sequenceCallbacks[self.currentSequence] then + self.sequenceCallbacks[self.currentSequence] = {} + end + self.sequenceCallbacks[self.currentSequence].complete = callback + return self +end + +function Animation:addAnimation(type, args, duration, easing) + local anim = AnimationInstance.new(self.element, type, args, duration, easing) + table.insert(self.sequences[self.currentSequence], anim) + return self +end + +function Animation:start() + + self.currentSequence = 1 + if(self.sequenceCallbacks[self.currentSequence])then + if(self.sequenceCallbacks[self.currentSequence].start) then + self.sequenceCallbacks[self.currentSequence].start(self.element) + end + end + if #self.sequences[self.currentSequence] > 0 then + self.timer = os.startTimer(0.05) + for _, anim in ipairs(self.sequences[self.currentSequence]) do + anim:start() + end + end + return self +end + +function Animation:event(event, timerId) + if event == "timer" and timerId == self.timer then + local currentTime = os.epoch("local") / 1000 + local sequenceFinished = true + local remaining = {} + local callbacks = self.sequenceCallbacks[self.currentSequence] + + for _, anim in ipairs(self.sequences[self.currentSequence]) do + local elapsed = currentTime - anim.startTime + local progress = elapsed / anim.duration + local finished = anim:update(elapsed) + + if callbacks and callbacks.update then + callbacks.update(self.element, progress) + end + + if not finished then + table.insert(remaining, anim) + sequenceFinished = false + else + anim:complete() + end + end + + if sequenceFinished then + if callbacks and callbacks.complete then + callbacks.complete(self.element) + end + + if self.currentSequence < #self.sequences then + self.currentSequence = self.currentSequence + 1 + remaining = {} + + local nextCallbacks = self.sequenceCallbacks[self.currentSequence] + if nextCallbacks and nextCallbacks.start then + nextCallbacks.start(self.element) + end + + for _, anim in ipairs(self.sequences[self.currentSequence]) do + anim:start() + table.insert(remaining, anim) + end + end + end + + if #remaining > 0 then + self.timer = os.startTimer(0.05) + end + end +end + +Animation.registerAnimation("move", { + start = function(anim) + anim.startX = anim.element.get("x") + anim.startY = anim.element.get("y") + end, + + update = function(anim, progress) + local x = anim.startX + (anim.args[1] - anim.startX) * progress + local y = anim.startY + (anim.args[2] - anim.startY) * progress + anim.element.set("x", math.floor(x)) + anim.element.set("y", math.floor(y)) + return progress >= 1 + end, + + complete = function(anim) + anim.element.set("x", anim.args[1]) + anim.element.set("y", anim.args[2]) + end +}) + +Animation.registerAnimation("morphText", { + start = function(anim) + local startText = anim.element.get(anim.args[1]) + local targetText = anim.args[2] + local maxLength = math.max(#startText, #targetText) + local startSpace = string.rep(" ", math.floor(maxLength - #startText)/2) + anim.startText = startSpace .. startText .. startSpace + anim.targetText = targetText .. string.rep(" ", maxLength - #targetText) + anim.length = maxLength + end, + + update = function(anim, progress) + local currentText = "" + + for i = 1, anim.length do + local startChar = anim.startText:sub(i,i) + local targetChar = anim.targetText:sub(i,i) + + if progress < 0.5 then + currentText = currentText .. (math.random() > progress*2 and startChar or " ") + else + currentText = currentText .. (math.random() > (progress-0.5)*2 and " " or targetChar) + end + end + + anim.element.set(anim.args[1], currentText) + return progress >= 1 + end, + + complete = function(anim) + anim.element.set(anim.args[1], anim.targetText:gsub("%s+$", "")) -- Entferne trailing spaces + end +}) + +Animation.registerAnimation("typewrite", { + start = function(anim) + anim.targetText = anim.args[2] + anim.element.set(anim.args[1], "") + end, + + update = function(anim, progress) + local length = math.floor(#anim.targetText * progress) + anim.element.set(anim.args[1], anim.targetText:sub(1, length)) + return progress >= 1 + end +}) + +Animation.registerAnimation("fadeText", { + start = function(anim) + anim.chars = {} + for i=1, #anim.args[2] do + anim.chars[i] = {char = anim.args[2]:sub(i,i), visible = false} + end + end, + + update = function(anim, progress) + local text = "" + for i, charData in ipairs(anim.chars) do + if math.random() < progress then + charData.visible = true + end + text = text .. (charData.visible and charData.char or " ") + end + anim.element.set(anim.args[1], text) + return progress >= 1 + end +}) + +Animation.registerAnimation("scrollText", { + start = function(anim) + anim.width = anim.element.get("width") + anim.targetText = anim.args[2] + anim.element.set(anim.args[1], "") + end, + + update = function(anim, progress) + local offset = math.floor(anim.width * (1-progress)) + local spaces = string.rep(" ", offset) + anim.element.set(anim.args[1], spaces .. anim.targetText) + return progress >= 1 + end +}) + +local VisualElement = {hooks={}} + +function VisualElement.hooks.dispatchEvent(self, event, ...) + if event == "timer" then + local animation = self.get("animation") + if animation then + animation:event(event, ...) + end + end +end + +function VisualElement.setup(element) + VisualElementBaseDispatchEvent = element.dispatchEvent + element.defineProperty(element, "animation", {default = nil, type = "table"}) + element.listenTo(element, "timer") +end + +function VisualElement:animate() + local animation = Animation.new(self) + self.set("animation", animation) + return animation +end + +return { + VisualElement = VisualElement +}", + ["elements/Frame.lua"] = "local elementManager = require("elementManager") +local Container = elementManager.getElement("Container") + +---@class Frame : Container +local Frame = setmetatable({}, Container) +Frame.__index = Frame + +--- Creates a new Frame instance +--- @return Frame object The newly created Frame instance +--- @usage local element = Frame.new("myId", basalt) +function Frame.new() + local self = setmetatable({}, Frame):__init() + self.set("width", 12) + self.set("height", 6) + self.set("background", colors.gray) + self.set("z", 10) + return self +end + +function Frame:init(props, basalt) + Container.init(self, props, basalt) + self.set("type", "Frame") +end + +return Frame", + ["propertySystem.lua"] = "local deepCopy = require("libraries/utils").deepCopy +local expect = require("libraries/expect") +local errorManager = require("errorManager") +local log = require("log") + +--- @class PropertySystem +local PropertySystem = {} +PropertySystem.__index = PropertySystem + +PropertySystem._properties = {} +local blueprintTemplates = {} + +PropertySystem._setterHooks = {} + +function PropertySystem.addSetterHook(hook) + table.insert(PropertySystem._setterHooks, hook) +end + +local function applyHooks(element, propertyName, value, config) + for _, hook in ipairs(PropertySystem._setterHooks) do + local newValue = hook(element, propertyName, value, config) + if newValue ~= nil then + value = newValue + end + end + return value +end + +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] + if type(value) == "function" and config.type ~= "function" then + value = value(self) + end + return config.getter and config.getter(self, value, ...) or value + end + + class["set" .. capitalizedName] = function(self, value, ...) + expect(1, self, "element") + value = applyHooks(self, name, value, config) + + if type(value) ~= "function" then + expect(2, value, config.type) + end + + if config.setter then + value = config.setter(self, value, ...) + end + + self:_updateProperty(name, value) + return self + end +end + +function PropertySystem.combineProperties(class, name, ...) + local properties = {...} + for k,v in pairs(properties)do + if not class._properties[v] then errorManager.error("Property not found: "..v) end + end + local capitalizedName = name:sub(1,1):upper() .. name:sub(2) + + class["get" .. capitalizedName] = function(self, ...) + expect(1, self, "element") + local value = {} + for _,v in pairs(properties)do + value[v] = self.get(v) + end + return table.unpack(value) + end + + class["set" .. capitalizedName] = function(self, ...) + expect(1, self, "element") + local values = {...} + for i,v in pairs(properties)do + self.set(v, values[i]) + end + return self + end +end + +--- Creates a blueprint of an element class with all its properties +--- @param elementClass table The element class to create a blueprint from +--- @return table blueprint A table containing all property definitions +function PropertySystem.blueprint(elementClass, properties, basalt, parent) + if not blueprintTemplates[elementClass] then + local template = { + basalt = basalt, + __isBlueprint = true, + _values = properties or {}, + _events = {}, + render = function() end, + dispatchEvent = function() end, + init = function() end, + } + + template.loaded = function(self, callback) + self.loadedCallback = callback + return template + end + + template.create = function(self) + local element = elementClass.new() + element:init({}, self.basalt) + for name, value in pairs(self._values) do + element._values[name] = value + end + for name, callbacks in pairs(self._events) do + for _, callback in ipairs(callbacks) do + element[name](element, callback) + end + end + if(parent~=nil)then + parent:addChild(element) + end + element:updateRender() + self.loadedCallback(element) + element:postInit() + return element + end + + local currentClass = elementClass + while currentClass do + if rawget(currentClass, '_properties') then + for name, config in pairs(currentClass._properties) do + if type(config.default) == "table" then + template._values[name] = deepCopy(config.default) + else + template._values[name] = config.default + end + end + end + currentClass = getmetatable(currentClass) and rawget(getmetatable(currentClass), '__index') + end + + blueprintTemplates[elementClass] = template + end + + local blueprint = { + _values = {}, + _events = {}, + loadedCallback = function() end, + } + + blueprint.get = function(name) + local value = blueprint._values[name] + local config = elementClass._properties[name] + if type(value) == "function" and config.type ~= "function" then + value = value(blueprint) + end + return value + end + blueprint.set = function(name, value) + blueprint._values[name] = value + return blueprint + end + + setmetatable(blueprint, { + __index = function(self, k) + if k:match("^on%u") then + return function(_, callback) + self._events[k] = self._events[k] or {} + table.insert(self._events[k], callback) + return self + end + end + if k:match("^get%u") then + local propName = k:sub(4,4):lower() .. k:sub(5) + return function() + return self._values[propName] + end + end + if k:match("^set%u") then + local propName = k:sub(4,4):lower() .. k:sub(5) + return function(_, value) + self._values[propName] = value + return self + end + end + return blueprintTemplates[elementClass][k] + end + }) + + return blueprint +end + +function PropertySystem.createFromBlueprint(elementClass, blueprint, basalt) + local element = elementClass.new({}, basalt) + for name, value in pairs(blueprint._values) do + if type(value) == "table" then + element._values[name] = deepCopy(value) + else + element._values[name] = value + end + end + + return element +end + +function PropertySystem:__init() + self._values = {} + self._observers = {} + + self.set = function(name, value, ...) + local oldValue = self._values[name] + local config = self._properties[name] + if(config~=nil)then + if(config.setter) then + value = config.setter(self, value, ...) + end + if config.canTriggerRender then + self:updateRender() + end + self._values[name] = applyHooks(self, name, value, config) + if oldValue ~= value and self._observers[name] then + for _, callback in ipairs(self._observers[name]) do + callback(self, value, oldValue) + end + end + end + end + + self.get = function(name, ...) + local value = self._values[name] + local config = self._properties[name] + if(config==nil)then errorManager.error("Property not found: "..name) return end + if type(value) == "function" and config.type ~= "function" then + value = value(self) + end + return config.getter and config.getter(self, value, ...) or value + 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) + local config = self._properties[k] + if config then + local value = self._values[k] + if type(value) == "function" and config.type ~= "function" then + value = value(self) + end + return value + end + if type(originalIndex) == "function" then + return originalIndex(t, k) + else + return originalIndex[k] + end + end, + __newindex = function(t, k, v) + local config = self._properties[k] + if config then + if config.setter then + v = config.setter(self, v) + end + v = applyHooks(self, k, v, config) + 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 type(oldValue) == "function" then + oldValue = oldValue(self) + end + + self._values[name] = value + local newValue = type(value) == "function" and value(self) or value + + if oldValue ~= newValue then + if self._properties[name].canTriggerRender then + self:updateRender() + end + if self._observers[name] then + for _, callback in ipairs(self._observers[name]) do + callback(self, newValue, 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 + +function PropertySystem:removeObserver(name, callback) + if self._observers[name] then + for i, cb in ipairs(self._observers[name]) do + if cb == callback then + table.remove(self._observers[name], i) + if #self._observers[name] == 0 then + self._observers[name] = nil + end + break + end + end + end + return self +end + +function PropertySystem:removeAllObservers(name) + if name then + self._observers[name] = nil + else + self._observers = {} + end + return self +end + +function PropertySystem:instanceProperty(name, config) + PropertySystem.defineProperty(self, name, config) + self._values[name] = config.default + return self +end + +function PropertySystem:removeProperty(name) + self._values[name] = nil + self._properties[name] = nil + self._observers[name] = nil + + local capitalizedName = name:sub(1,1):upper() .. name:sub(2) + self["get" .. capitalizedName] = nil + self["set" .. capitalizedName] = nil + return self +end + +function PropertySystem:getPropertyConfig(name) + return self._properties[name] +end + +return PropertySystem", + ["plugins/xml.lua"] = "local errorManager = require("errorManager") + +local function parseTag(str) + local tag = { + attributes = {} + } + tag.name = str:match("<(%w+)") + for k,v in str:gmatch('%s(%w+)="([^"]-)"') do + tag.attributes[k] = v + end + return tag +end + +local function parseXML(self, xmlString) + local stack = {} + local root = {children = {}} + local current = root + local inCDATA = false + local cdataContent = "" + + for line in xmlString:gmatch("[^\r\n]+") do + line = line:match("^%s*(.-)%s*$") + self.basalt.LOGGER.debug("Parsing line: " .. line) + + if line:match("^$") and inCDATA then + inCDATA = false + current.content = cdataContent + elseif inCDATA then + cdataContent = cdataContent .. line .. "\n" + elseif line:match("^<[^/]") then + local tag = parseTag(line) + tag.children = {} + tag.content = "" + table.insert(current.children, tag) + + if not line:match("/>$") then + table.insert(stack, current) + current = tag + end + elseif line:match("^ 0 then + createElements(node, element, scope) + end + end + end + end + + createElements(tree, self, scope) + return self +end + +return { + BaseElement = BaseElement, + Container = Container +} + +", + ["elements/Menu.lua"] = "local VisualElement = require("elements/VisualElement") +local List = require("elements/List") +local tHex = require("libraries/colorHex") + +---@class Menu : List +local Menu = setmetatable({}, List) +Menu.__index = Menu + +Menu.defineProperty(Menu, "separatorColor", {default = colors.gray, type = "number"}) + +function Menu.new() + local self = setmetatable({}, Menu):__init() + self.set("width", 30) + self.set("height", 1) + self.set("background", colors.gray) + return self +end + +function Menu:init(props, basalt) + List.init(self, props, basalt) + self.set("type", "Menu") + return self +end + +function Menu:setItems(items) + local listItems = {} + local totalWidth = 0 + for _, item in ipairs(items) do + if item.separator then + table.insert(listItems, {text = item.text or "|", selectable = false}) + totalWidth = totalWidth + 1 + else + local text = " " .. item.text .. " " + item.text = text + table.insert(listItems, item) + totalWidth = totalWidth + #text + end + end + self.set("width", totalWidth) + return List.setItems(self, listItems) +end + +function Menu:render() + VisualElement.render(self) + local currentX = 1 + + for i, item in ipairs(self.get("items")) do + local isSelected = i == self.get("selectedIndex") + + local fg = item.selectable == false and self.get("separatorColor") or + (isSelected and (item.selectedForeground or self.get("foreground")) or + (item.foreground or self.get("foreground"))) + + local bg = isSelected and + (item.selectedBackground or self.get("selectedColor")) or + (item.background or self.get("background")) + + self:blit(currentX, 1, item.text, + string.rep(tHex[fg], #item.text), + string.rep(tHex[bg], #item.text)) + + currentX = currentX + #item.text + end +end + +function Menu:mouse_click(button, x, y) + if not VisualElement.mouse_click(self, button, x, y) then return false end + if(self.get("selectable") == false) then return false end + local relX = select(1, self:getRelativePosition(x, y)) + local currentX = 1 + + for i, item in ipairs(self.get("items")) do + if relX >= currentX and relX < currentX + #item.text then + if item.selectable ~= false then + self.set("selectedIndex", i) + if type(item) == "table" then + if item.callback then + item.callback(self) + end + end + self:fireEvent("select", i, item) + end + return true + end + currentX = currentX + #item.text + end + return false +end + +return Menu +", + ["elements/Program.lua"] = "local elementManager = require("elementManager") +local VisualElement = elementManager.getElement("VisualElement") +local errorManager = require("errorManager") + +--TODO: +-- Rendering optimization (only render when screen changed) +-- Eventsystem improvement +-- Cursor is sometimes not visible on time + +---@class Program : VisualElement +local Program = setmetatable({}, VisualElement) +Program.__index = Program + +Program.defineProperty(Program, "program", {default = nil, type = "table"}) +Program.defineProperty(Program, "path", {default = "", type = "string"}) +Program.defineProperty(Program, "running", {default = false, type = "boolean"}) + +Program.listenTo(Program, "key") +Program.listenTo(Program, "char") +Program.listenTo(Program, "key_up") +Program.listenTo(Program, "paste") +Program.listenTo(Program, "mouse_click") +Program.listenTo(Program, "mouse_drag") +Program.listenTo(Program, "mouse_scroll") +Program.listenTo(Program, "mouse_up") + +local BasaltProgram = {} +BasaltProgram.__index = BasaltProgram +local newPackage = dofile("rom/modules/main/cc/require.lua").make + +function BasaltProgram.new() + local self = setmetatable({}, BasaltProgram) + self.env = {} + self.args = {} + return self +end + +function BasaltProgram:run(path, width, height) + self.window = window.create(term.current(), 1, 1, width, height, false) + local pPath = shell.resolveProgram(path) + if(pPath~=nil)then + if(fs.exists(pPath)) then + local file = fs.open(pPath, "r") + local content = file.readAll() + file.close() + local env = setmetatable(self.env, {__index=_ENV}) + env.shell = shell + env.term = self.window + env.require, env.package = newPackage(env, fs.getDir(pPath)) + env.term.current = term.current + env.term.redirect = term.redirect + env.term.native = term.native + + self.coroutine = coroutine.create(function() + local program = load(content, path, "bt", env) + if program then + local current = term.current() + term.redirect(self.window) + local result = program(path, table.unpack(self.args)) + term.redirect(current) + return result + end + end) + local current = term.current() + term.redirect(self.window) + local ok, result = coroutine.resume(self.coroutine) + term.redirect(current) + if not ok then + errorManager.header = "Basalt Program Error ".. path + errorManager.error(result) + end + else + errorManager.header = "Basalt Program Error ".. path + errorManager.error("File not found") + end + else + errorManager.header = "Basalt Program Error" + errorManager.error("Program "..path.." not found") + end +end + +function BasaltProgram:resize(width, height) + self.window.reposition(1, 1, width, height) +end + +function BasaltProgram:resume(event, ...) + if self.coroutine==nil or coroutine.status(self.coroutine)=="dead" then return end + if(self.filter~=nil)then + if(event~=self.filter)then return end + self.filter=nil + end + local current = term.current() + term.redirect(self.window) + local ok, result = coroutine.resume(self.coroutine, event, ...) + term.redirect(current) + + if ok then + self.filter = result + else + errorManager.header = "Basalt Program Error" + errorManager.error(result) + end + return ok, result +end + +function BasaltProgram:stop() + +end + +--- Creates a new Program instance +--- @return Program object The newly created Program instance +--- @usage local element = Program.new("myId", basalt) +function Program.new() + local self = setmetatable({}, Program):__init() + self.set("z", 5) + self.set("width", 30) + self.set("height", 12) + return self +end + +function Program:init(props, basalt) + VisualElement.init(self, props, basalt) + self.set("type", "Program") +end + +function Program:execute(path) + self.set("path", path) + self.set("running", true) + local program = BasaltProgram.new() + self.set("program", program) + program:run(path, self.get("width"), self.get("height")) + self:updateRender() + return self +end + +function Program:dispatchEvent(event, ...) + local program = self.get("program") + local result = VisualElement.dispatchEvent(self, event, ...) + if program then + program:resume(event, ...) + if(self.get("focused"))then + local cursorBlink = program.window.getCursorBlink() + local cursorX, cursorY = program.window.getCursorPos() + self:setCursor(cursorX, cursorY, cursorBlink) + end + self:updateRender() + end + return result +end + +function Program:focus() + if(VisualElement.focus(self))then + local program = self.get("program") + if program then + local cursorBlink = program.window.getCursorBlink() + local cursorX, cursorY = program.window.getCursorPos() + self:setCursor(cursorX, cursorY, cursorBlink) + end + end +end + +function Program:render() + VisualElement.render(self) + local program = self.get("program") + if program then + local _, height = program.window.getSize() + for y = 1, height do + local text, fg, bg = program.window.getLine(y) + if text then + self:blit(1, y, text, fg, bg) + end + end + end +end + +return Program", + ["elementManager.lua"] = "local args = table.pack(...) +local dir = fs.getDir(args[2] or "basalt") +local subDir = args[1] +if(dir==nil)then + error("Unable to find directory "..args[2].." please report this bug to our discord.") +end + +local log = require("log") +local defaultPath = package.path +local format = "path;/path/?.lua;/path/?/init.lua;" +local main = format:gsub("path", dir) + +local ElementManager = {} +ElementManager._elements = {} +ElementManager._plugins = {} +ElementManager._APIs = {} +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 + +log.info("Loading plugins from "..pluginsDirectory) +if fs.exists(pluginsDirectory) then + for _, file in ipairs(fs.list(pluginsDirectory)) do + local name = file:match("(.+).lua") + if name then + log.debug("Found plugin: "..name) + local plugin = require(fs.combine("plugins", name)) + if type(plugin) == "table" then + for k,v in pairs(plugin) do + if(k ~= "API")then + if(ElementManager._plugins[k]==nil)then + ElementManager._plugins[k] = {} + end + table.insert(ElementManager._plugins[k], v) + else + ElementManager._APIs[name] = v + end + end + end + end + end +end + +function ElementManager.loadElement(name) + if not ElementManager._elements[name].loaded then + package.path = main.."rom/?" + local element = require(fs.combine("elements", name)) + package.path = defaultPath + ElementManager._elements[name] = { + class = element, + plugins = element.plugins, + loaded = true + } + log.debug("Loaded element: "..name) + + if(ElementManager._plugins[name]~=nil)then + for _, plugin in pairs(ElementManager._plugins[name]) do + if(plugin.setup)then + plugin.setup(element) + end + + if(plugin.hooks)then + for methodName, hooks in pairs(plugin.hooks) do + local original = element[methodName] + if(type(original)~="function")then + error("Element "..name.." does not have a method "..methodName) + end + if(type(hooks)=="function")then + element[methodName] = function(self, ...) + local result = original(self, ...) + local hookResult = hooks(self, ...) + return hookResult == nil and result or hookResult + end + elseif(type(hooks)=="table")then + element[methodName] = function(self, ...) + if hooks.pre then hooks.pre(self, ...) end + local result = original(self, ...) + if hooks.post then hooks.post(self, ...) end + return result + end + end + end + end + + for funcName, func in pairs(plugin) do + if funcName ~= "setup" and funcName ~= "hooks" then + element[funcName] = func + end + end + end + end + end +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.getAPI(name) + return ElementManager._APIs[name] +end + +return ElementManager", + ["elements/Slider.lua"] = "local VisualElement = require("elements/VisualElement") + +---@class Slider : VisualElement +local Slider = setmetatable({}, VisualElement) +Slider.__index = Slider + +---@property step number 1 Current step position (1 to width/height) +Slider.defineProperty(Slider, "step", {default = 1, type = "number", canTriggerRender = true}) +---@property max number 100 Maximum value for value conversion +Slider.defineProperty(Slider, "max", {default = 100, type = "number"}) +---@property horizontal boolean true Whether the slider is horizontal +Slider.defineProperty(Slider, "horizontal", {default = true, type = "boolean", canTriggerRender = true}) +---@property barColor color color Colors for the slider bar +Slider.defineProperty(Slider, "barColor", {default = colors.gray, type = "number", canTriggerRender = true}) +---@property sliderColor color The color of the slider handle +Slider.defineProperty(Slider, "sliderColor", {default = colors.blue, type = "number", canTriggerRender = true}) + +Slider.listenTo(Slider, "mouse_click") +Slider.listenTo(Slider, "mouse_drag") +Slider.listenTo(Slider, "mouse_up") + +function Slider.new() + local self = setmetatable({}, Slider):__init() + self.set("width", 8) + self.set("height", 1) + self.set("backgroundEnabled", false) + return self +end + +function Slider:init(props, basalt) + VisualElement.init(self, props, basalt) + self.set("type", "Slider") +end + +function Slider:getValue() + local step = self.get("step") + local max = self.get("max") + local maxSteps = self.get("horizontal") and self.get("width") or self.get("height") + return math.floor((step - 1) * (max / (maxSteps - 1))) +end + +function Slider:mouse_click(button, x, y) + if button == 1 and self:isInBounds(x, y) then + local relX, relY = self:getRelativePosition(x, y) + local pos = self.get("horizontal") and relX or relY + local maxSteps = self.get("horizontal") and self.get("width") or self.get("height") + + self.set("step", math.min(maxSteps, math.max(1, pos))) + self:updateRender() + return true + end +end +Slider.mouse_drag = Slider.mouse_click + +function Slider:mouse_scroll(direction, x, y) + if self:isInBounds(x, y) then + local step = self.get("step") + local maxSteps = self.get("horizontal") and self.get("width") or self.get("height") + self.set("step", math.min(maxSteps, math.max(1, step + direction))) + self:updateRender() + return true + end +end + +function Slider:render() + VisualElement.render(self) + local width = self.get("width") + local height = self.get("height") + local horizontal = self.get("horizontal") + local step = self.get("step") + + local barChar = horizontal and "\140" or "│" + local text = string.rep(barChar, horizontal and width or height) + + if horizontal then + self:textFg(1, 1, text, self.get("barColor")) + self:textBg(step, 1, " ", self.get("sliderColor")) + else + for y = 1, height do + self:textFg(1, y, barChar, self.get("barColor")) + end + self:textFg(1, step, "\140", self.get("sliderColor")) + end +end + +return Slider", }, } \ No newline at end of file diff --git a/release/basalt.lua b/release/basalt.lua index c7b1c33..91732fe 100644 --- a/release/basalt.lua +++ b/release/basalt.lua @@ -2,280 +2,650 @@ local minified = true local project = {} local baseRequire = require require = function(path) return project[path] or baseRequire(path) end -project["render.lua"] = function(...) local d={}d.__index=d;local _a=require("libraries/colorHex") -local aa=require("log") -function d.new(ba)local ca=setmetatable({},d)ca.terminal=ba -ca.width,ca.height=ba.getSize()ca.buffer={text={},fg={},bg={},changed={}} -for y=1,ca.height do -ca.buffer.text[y]=string.rep(" ",ca.width)ca.buffer.fg[y]=string.rep("0",ca.width) -ca.buffer.bg[y]=string.rep("f",ca.width)ca.buffer.changed[y]=false end;return ca end -function d:blit(ba,ca,da,_b,ab)if ca<1 or ca>self.height then return self end;if(#da~=#_b or -#da~=#ab)then -error("Text, fg, and bg must be the same length")end -self.buffer.text[ca]=self.buffer.text[ca]:sub(1, -ba-1)..da.. -self.buffer.text[ca]:sub(ba+#da) -self.buffer.fg[ca]= -self.buffer.fg[ca]:sub(1,ba-1).._b..self.buffer.fg[ca]:sub(ba+#_b) -self.buffer.bg[ca]= -self.buffer.bg[ca]:sub(1,ba-1)..ab..self.buffer.bg[ca]:sub(ba+#ab)self.buffer.changed[ca]=true;return self end -function d:multiBlit(ba,ca,da,_b,ab,bb,cb)if ca<1 or ca>self.height then return self end;if( -#ab~=#bb or#ab~=#cb)then -error("Text, fg, and bg must be the same length")end;ab=ab:rep(da) -bb=bb:rep(da)cb=cb:rep(da) -for dy=0,_b-1 do local db=ca+dy -if db>=1 and db<=self.height then -self.buffer.text[db]=self.buffer.text[db]:sub(1, -ba-1)..ab.. -self.buffer.text[db]:sub(ba+#ab) -self.buffer.fg[db]= -self.buffer.fg[db]:sub(1,ba-1)..bb..self.buffer.fg[db]:sub(ba+#bb) -self.buffer.bg[db]= -self.buffer.bg[db]:sub(1,ba-1)..cb..self.buffer.bg[db]:sub(ba+#cb)self.buffer.changed[db]=true end end;return self end -function d:textFg(ba,ca,da,_b)if ca<1 or ca>self.height then return self end -_b=_a[_b]or"0" -self.buffer.text[ca]= -self.buffer.text[ca]:sub(1,ba-1).. -da..self.buffer.text[ca]:sub(ba+#da) -self.buffer.fg[ca]= -self.buffer.fg[ca]:sub(1,ba-1).. -_b:rep(#da)..self.buffer.fg[ca]:sub(ba+#da)self.buffer.changed[ca]=true;return self end -function d:text(ba,ca,da)if ca<1 or ca>self.height then return self end -self.buffer.text[ca]=self.buffer.text[ca]:sub(1, -ba-1)..da.. -self.buffer.text[ca]:sub(ba+#da)self.buffer.changed[ca]=true;return self end -function d:fg(ba,ca,da)if ca<1 or ca>self.height then return self end -self.buffer.fg[ca]=self.buffer.fg[ca]:sub(1, -ba-1)..da.. -self.buffer.fg[ca]:sub(ba+#da)self.buffer.changed[ca]=true;return self end -function d:bg(ba,ca,da)if ca<1 or ca>self.height then return self end -self.buffer.bg[ca]=self.buffer.bg[ca]:sub(1, -ba-1)..da.. -self.buffer.bg[ca]:sub(ba+#da)self.buffer.changed[ca]=true;return self end -function d:clear(ba)local ca=_a[ba]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(ca,self.width)self.buffer.changed[y]=true end;return self end -function d: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 d:clearArea(ba,ca,da,_b,ab)local bb=_a[ab]or"f" -for dy=0,_b-1 do local cb=ca+dy;if -cb>=1 and cb<=self.height then local db=string.rep(" ",da)local _c=string.rep(bb,da) -self:blit(ba,cb,db,"0",bb)end end;return self end;function d:getSize()return self.width,self.height end;return d end -project["elementManager.lua"] = function(...) local ba=table.pack(...) -local ca=fs.getDir(ba[2]or"basalt")if(ca==nil)then -error("Unable to find directory "..ba[2].." please report this bug to our discord.")end -local da=require("log")local _b={}_b._elements={}_b._plugins={} -local ab=fs.combine(ca,"elements")local bb=fs.combine(ca,"plugins") -da.info("Loading elements from "..ab) -if fs.exists(ab)then -for cb,db in ipairs(fs.list(ab))do local _c=db:match("(.+).lua")if _c then da.debug( -"Found element: ".._c) -_b._elements[_c]={class=nil,plugins={},loaded=false}end end end -function _b.extendMethod(cb,db,_c,ac)if not ac then cb[db]=_c;return end -cb[db]=function(bc,...)if _c.before then -_c.before(bc,...)end;local cc;if _c.override then -cc={_c.override(bc,ac,...)}else cc={ac(bc,...)}end;if _c.after then -_c.after(bc,...)end;return table.unpack(cc)end end -function _b.loadPlugin(cb)local db=require("plugins/"..cb) -for _c,ac in pairs(db)do -local bc=_b._elements[_c] -if bc then if ac.properties then bc.class.initialize(_c.."Plugin") -for cc,dc in -pairs(ac.properties)do bc.class.registerProperty(cc,dc)end end;if ac.methods then -for cc,dc in -pairs(ac.methods)do _b.extendMethod(bc.class,cc,dc,bc.class[cc])end end end end end -function _b.loadElement(cb) -if not _b._elements[cb].loaded then -local db=require("elements/"..cb)_b._elements[cb]={class=db,plugins=db.plugins,loaded=true}da.debug( -"Loaded element: "..cb)if db.requires then -for _c,ac in pairs(db.requires)do end end end end -function _b.registerPlugin(cb,db)if not db.provides then -error("Plugin must specify what it provides")end;_b._plugins[cb]=db end -function _b.getElement(cb)if not _b._elements[cb].loaded then -_b.loadElement(cb)end;return _b._elements[cb].class end;function _b.getElementList()return _b._elements end -function _b.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 _b end -project["init.lua"] = function(...) local da={...}local _b=da[1]or"basalt"local ab=package.path +project["elementManager.lua"] = function(...) local ab=table.pack(...) +local bb=fs.getDir(ab[2]or"basalt")local cb=ab[1]if(bb==nil)then +error("Unable to find directory ".. +ab[2].." please report this bug to our discord.")end +local db=require("log")local _c=package.path;local ac="path;/path/?.lua;/path/?/init.lua;" +local bc=ac:gsub("path",bb)local cc={}cc._elements={}cc._plugins={}cc._APIs={} +local dc=fs.combine(bb,"elements")local _d=fs.combine(bb,"plugins") +db.info("Loading elements from "..dc) +if fs.exists(dc)then +for ad,bd in ipairs(fs.list(dc))do local cd=bd:match("(.+).lua")if cd then db.debug( +"Found element: "..cd) +cc._elements[cd]={class=nil,plugins={},loaded=false}end end end;db.info("Loading plugins from ".._d) +if +fs.exists(_d)then +for ad,bd in ipairs(fs.list(_d))do local cd=bd:match("(.+).lua") +if cd then +db.debug("Found plugin: "..cd)local dd=require(fs.combine("plugins",cd)) +if +type(dd)=="table"then +for __a,a_a in pairs(dd)do if(__a~="API")then +if(cc._plugins[__a]==nil)then cc._plugins[__a]={}end;table.insert(cc._plugins[__a],a_a)else +cc._APIs[cd]=a_a end end end end end end +function cc.loadElement(ad) +if not cc._elements[ad].loaded then +package.path=bc.."rom/?"local bd=require(fs.combine("elements",ad)) +package.path=_c;cc._elements[ad]={class=bd,plugins=bd.plugins,loaded=true}db.debug( +"Loaded element: "..ad) +if(cc._plugins[ad]~=nil)then +for cd,dd in +pairs(cc._plugins[ad])do if(dd.setup)then dd.setup(bd)end +if(dd.hooks)then +for __a,a_a in pairs(dd.hooks)do +local b_a=bd[__a]if(type(b_a)~="function")then +error("Element ".. +ad.." does not have a method "..__a)end +if(type(a_a)=="function")then +bd[__a]=function(c_a,...) +local d_a=b_a(c_a,...)local _aa=a_a(c_a,...)return _aa==nil and d_a or _aa end elseif(type(a_a)=="table")then +bd[__a]=function(c_a,...)if a_a.pre then a_a.pre(c_a,...)end +local d_a=b_a(c_a,...)if a_a.post then a_a.post(c_a,...)end;return d_a end end end end;for __a,a_a in pairs(dd)do +if __a~="setup"and __a~="hooks"then bd[__a]=a_a end end end end end end +function cc.getElement(ad)if not cc._elements[ad].loaded then +cc.loadElement(ad)end;return cc._elements[ad].class end;function cc.getElementList()return cc._elements end;function cc.getAPI(ad) +return cc._APIs[ad]end;return cc end +project["propertySystem.lua"] = function(...) local ca=require("libraries/utils").deepCopy +local da=require("libraries/expect")local _b=require("errorManager")local ab=require("log")local bb={} +bb.__index=bb;bb._properties={}local cb={}bb._setterHooks={}function bb.addSetterHook(_c) +table.insert(bb._setterHooks,_c)end +local function db(_c,ac,bc,cc)for dc,_d in ipairs(bb._setterHooks)do +local ad=_d(_c,ac,bc,cc)if ad~=nil then bc=ad end end;return bc end +function bb.defineProperty(_c,ac,bc) +if not rawget(_c,'_properties')then _c._properties={}end +_c._properties[ac]={type=bc.type,default=bc.default,canTriggerRender=bc.canTriggerRender,getter=bc.getter,setter=bc.setter}local cc=ac:sub(1,1):upper()..ac:sub(2) +_c[ +"get"..cc]=function(dc,...)da(1,dc,"element")local _d=dc._values[ac] +if type(_d)== +"function"and bc.type~="function"then _d=_d(dc)end +return bc.getter and bc.getter(dc,_d,...)or _d end +_c["set"..cc]=function(dc,_d,...)da(1,dc,"element")_d=db(dc,ac,_d,bc)if +type(_d)~="function"then da(2,_d,bc.type)end;if bc.setter then +_d=bc.setter(dc,_d,...)end;dc:_updateProperty(ac,_d)return dc end end +function bb.combineProperties(_c,ac,...)local bc={...}for dc,_d in pairs(bc)do +if not _c._properties[_d]then _b.error("Property not found: ".. +_d)end end;local cc= +ac:sub(1,1):upper()..ac:sub(2) +_c["get"..cc]=function(dc,...) +da(1,dc,"element")local _d={}for ad,bd in pairs(bc)do _d[bd]=dc.get(bd)end;return +table.unpack(_d)end +_c["set"..cc]=function(dc,...)da(1,dc,"element")local _d={...}for ad,bd in pairs(bc)do +dc.set(bd,_d[ad])end;return dc end end +function bb.blueprint(_c,ac,bc,cc) +if not cb[_c]then +local _d={basalt=bc,__isBlueprint=true,_values=ac or{},_events={},render=function()end,dispatchEvent=function()end,init=function()end} +_d.loaded=function(bd,cd)bd.loadedCallback=cd;return _d end +_d.create=function(bd)local cd=_c.new()cd:init({},bd.basalt)for dd,__a in pairs(bd._values)do +cd._values[dd]=__a end;for dd,__a in pairs(bd._events)do for a_a,b_a in ipairs(__a)do +cd[dd](cd,b_a)end end;if(cc~=nil)then +cc:addChild(cd)end;cd:updateRender()bd.loadedCallback(cd) +cd:postInit()return cd end;local ad=_c +while ad do +if rawget(ad,'_properties')then for bd,cd in pairs(ad._properties)do +if +type(cd.default)=="table"then _d._values[bd]=ca(cd.default)else _d._values[bd]=cd.default end end end +ad=getmetatable(ad)and rawget(getmetatable(ad),'__index')end;cb[_c]=_d end;local dc={_values={},_events={},loadedCallback=function()end} +dc.get=function(_d) +local ad=dc._values[_d]local bd=_c._properties[_d]if +type(ad)=="function"and bd.type~="function"then ad=ad(dc)end;return ad end +dc.set=function(_d,ad)dc._values[_d]=ad;return dc end +setmetatable(dc,{__index=function(_d,ad) +if ad:match("^on%u")then return +function(bd,cd) +_d._events[ad]=_d._events[ad]or{}table.insert(_d._events[ad],cd)return _d end end +if ad:match("^get%u")then +local bd=ad:sub(4,4):lower()..ad:sub(5)return function()return _d._values[bd]end end;if ad:match("^set%u")then +local bd=ad:sub(4,4):lower()..ad:sub(5) +return function(cd,dd)_d._values[bd]=dd;return _d end end +return cb[_c][ad]end})return dc end +function bb.createFromBlueprint(_c,ac,bc)local cc=_c.new({},bc) +for dc,_d in pairs(ac._values)do if type(_d)=="table"then +cc._values[dc]=ca(_d)else cc._values[dc]=_d end end;return cc end +function bb:__init()self._values={}self._observers={} +self.set=function(dc,_d,...) +local ad=self._values[dc]local bd=self._properties[dc] +if(bd~=nil)then if(bd.setter)then +_d=bd.setter(self,_d,...)end +if bd.canTriggerRender then self:updateRender()end;self._values[dc]=db(self,dc,_d,bd)if +ad~=_d and self._observers[dc]then +for cd,dd in ipairs(self._observers[dc])do dd(self,_d,ad)end end end end +self.get=function(dc,...)local _d=self._values[dc]local ad=self._properties[dc] +if +(ad==nil)then _b.error("Property not found: "..dc)return end;if type(_d)=="function"and ad.type~="function"then +_d=_d(self)end;return +ad.getter and ad.getter(self,_d,...)or _d end;local _c={}local ac=getmetatable(self).__index +while ac do if +rawget(ac,'_properties')then +for dc,_d in pairs(ac._properties)do if not _c[dc]then _c[dc]=_d end end end;ac=getmetatable(ac)and +rawget(getmetatable(ac),'__index')end;self._properties=_c;local bc=getmetatable(self)local cc=bc.__index +setmetatable(self,{__index=function(dc,_d) +local ad=self._properties[_d] +if ad then local bd=self._values[_d]if +type(bd)=="function"and ad.type~="function"then bd=bd(self)end;return bd end +if type(cc)=="function"then return cc(dc,_d)else return cc[_d]end end,__newindex=function(dc,_d,ad) +local bd=self._properties[_d] +if bd then if bd.setter then ad=bd.setter(self,ad)end +ad=db(self,_d,ad,bd)self:_updateProperty(_d,ad)else rawset(dc,_d,ad)end end,__tostring=function(dc)return +string.format("Object: %s (id: %s)",dc._values.type,dc.id)end}) +for dc,_d in pairs(_c)do if self._values[dc]==nil then +if type(_d.default)=="table"then +self._values[dc]=ca(_d.default)else self._values[dc]=_d.default end end end;return self end +function bb:_updateProperty(_c,ac)local bc=self._values[_c] +if type(bc)=="function"then bc=bc(self)end;self._values[_c]=ac +local cc=type(ac)=="function"and ac(self)or ac +if bc~=cc then +if self._properties[_c].canTriggerRender then self:updateRender()end +if self._observers[_c]then for dc,_d in ipairs(self._observers[_c])do +_d(self,cc,bc)end end end end +function bb:observe(_c,ac) +self._observers[_c]=self._observers[_c]or{}table.insert(self._observers[_c],ac)return self end +function bb:removeObserver(_c,ac) +if self._observers[_c]then +for bc,cc in ipairs(self._observers[_c])do if cc==ac then +table.remove(self._observers[_c],bc) +if#self._observers[_c]==0 then self._observers[_c]=nil end;break end end end;return self end;function bb:removeAllObservers(_c) +if _c then self._observers[_c]=nil else self._observers={}end;return self end +function bb:instanceProperty(_c,ac) +bb.defineProperty(self,_c,ac)self._values[_c]=ac.default;return self end +function bb:removeProperty(_c)self._values[_c]=nil;self._properties[_c]=nil;self._observers[_c]= +nil +local ac=_c:sub(1,1):upper().._c:sub(2)self["get"..ac]=nil;self["set"..ac]=nil;return self end +function bb:getPropertyConfig(_c)return self._properties[_c]end;return bb end +project["main.lua"] = function(...) local bc=require("elementManager") +local cc=require("errorManager")local dc=require("propertySystem")local _d={}_d.traceback=true +_d._events={}_d._schedule={}_d._plugins={}_d.LOGGER=require("log") +_d.path=fs.getDir(select(2,...))local ad=nil;local bd=false;local cd=type;local dd={}local __a=10;local a_a=0;local b_a=false;local function c_a()if(b_a)then return end +a_a=os.startTimer(0.2)b_a=true end;local function d_a(caa) +for _=1,caa do local daa=dd[1]if(daa)then +daa:create()end;table.remove(dd,1)end end +local function _aa(caa,daa)if(caa=="timer")then +if(daa==a_a)then +d_a(__a)b_a=false;a_a=0;if(#dd>0)then c_a()end;return true end end end +function _d.create(caa,daa,_ba,aba)if(cd(daa)=="string")then daa={name=daa}end;if(daa==nil)then +daa={name=caa}end;local bba=bc.getElement(caa) +if(_ba)then +local cba=dc.blueprint(bba,daa,_d,aba)table.insert(dd,cba)c_a()return cba else local cba=bba.new() +cba:init(daa,_d)return cba end end;function _d.createFrame()local caa=_d.create("BaseFrame")caa:postInit()ad=caa +return caa end +function _d.getElementManager()return bc end +function _d.getMainFrame()if(ad==nil)then ad=_d.createFrame()end;return ad end;function _d.setActiveFrame(caa)ad=caa end;function _d.scheduleUpdate(caa) +table.insert(_d._schedule,caa)return#_d._schedule end;function _d.removeSchedule(caa)_d._schedule[caa]= +nil end +local function aaa(caa,...) +if(caa=="terminate")then _d.stop()end;if _aa(caa,...)then return end;if(ad)then +if(ad:dispatchEvent(caa,...))then return end end;if _d._events[caa]then for daa,_ba in ipairs(_d._events[caa])do +_ba(...)end end end;local function baa()if(ad)then ad:render()end end;function _d.update() +for caa,daa in +pairs(_d._schedule)do if type(daa)=="function"then daa()end end end;function _d.stop()term.clear() +term.setCursorPos(1,1)bd=false end +function _d.run(caa)bd=caa +if(caa==nil)then bd=true end;local function daa()baa() +while bd do aaa(os.pullEventRaw())baa()end end +while bd do local _ba,aba=pcall(daa)if not(_ba)then +cc.header="Basalt Runtime Error"cc.error(aba)end end end;function _d.getAPI(caa)return bc.getAPI(caa)end;return _d end +project["init.lua"] = function(...) local da={...}local _b=fs.getDir(da[2])local ab=package.path local bb="path;/path/?.lua;/path/?/init.lua;"local cb=bb:gsub("path",_b)package.path=cb.."rom/?" local function db(bc) -local cc=require("errorManager")cc.header="Basalt Loading Error"cc.error(bc)end;local _c,ac=pcall(require,"main")if not _c then db(ac)else return ac end end -project["elements/BaseFrame.lua"] = function(...) local d=require("elements/Container") -local _a=require("render")local aa=setmetatable({},d)aa.__index=aa -function aa.new(ba,ca) -local da=setmetatable({},aa):__init()da:init(ba,ca)da.terminal=term.current() -da._render=_a.new(da.terminal)da._renderUpdate=true;local _b,ab=da.terminal.getSize() -da.set("width",_b)da.set("height",ab) -da.set("background",colors.red)da.set("type","BaseFrame")return da end;function aa:multiBlit(ba,ca,da,_b,ab,bb,cb) -self._render:multiBlit(ba,ca,da,_b,ab,bb,cb)end;function aa:textFg(ba,ca,da,_b) -self._render:textFg(ba,ca,da,_b)end -function aa:render()if(self._renderUpdate)then d.render(self) -self._render:render()self._renderUpdate=false end end;return aa end -project["elements/Button.lua"] = function(...) local d=require("elements/VisualElement") -local _a=require("libraries/utils").getCenteredPosition;local aa=setmetatable({},d)aa.__index=aa -aa.defineProperty(aa,"text",{default="Button",type="string"})aa.listenTo(aa,"mouse_click") -function aa.new(ba,ca) -local da=setmetatable({},aa):__init()da:init(ba,ca)da.set("type","Button") -da.set("width",10)da.set("height",3)da.set("z",5)return da end -function aa:render()d.render(self)local ba=self.get("text") -local ca,da=_a(ba,self.get("width"),self.get("height")) -self:textFg(ca,da,ba,self.get("foreground"))end;return aa end -project["elements/VisualElement.lua"] = function(...) local d=require("elements/BaseElement") -local _a=setmetatable({},d)_a.__index=_a;local aa=require("libraries/colorHex") -d.defineProperty(_a,"x",{default=1,type="number",canTriggerRender=true}) -d.defineProperty(_a,"y",{default=1,type="number",canTriggerRender=true}) -d.defineProperty(_a,"z",{default=1,type="number",canTriggerRender=true,setter=function(ba,ca) -ba.basalt.LOGGER.debug("Setting z to "..ca)if ba.parent then ba.parent:sortChildren()end;return ca end}) -d.defineProperty(_a,"width",{default=1,type="number",canTriggerRender=true}) -d.defineProperty(_a,"height",{default=1,type="number",canTriggerRender=true}) -d.defineProperty(_a,"background",{default=colors.black,type="number",canTriggerRender=true}) -d.defineProperty(_a,"foreground",{default=colors.white,type="number",canTriggerRender=true}) -d.defineProperty(_a,"clicked",{default=false,type="boolean"}) -function _a.new(ba,ca)local da=setmetatable({},_a):__init() -da:init(ba,ca)da.set("type","VisualElement")return da end;function _a:multiBlit(ba,ca,da,_b,ab,bb,cb)ba=ba+self.get("x")-1;ca=ca+self.get("y")- -1 -self.parent:multiBlit(ba,ca,da,_b,ab,bb,cb)end;function _a:textFg(ba,ca,da,_b)ba=ba+ -self.get("x")-1;ca=ca+self.get("y")-1 -self.parent:textFg(ba,ca,da,_b)end -function _a:isInBounds(ba,ca) -local da,_b=self.get("x"),self.get("y")local ab,bb=self.get("width"),self.get("height")return -ba>=da and ba<= -da+ab-1 and ca>=_b and ca<=_b+bb-1 end -function _a:mouse_click(ba,ca,da)if self:isInBounds(ca,da)then self.set("clicked",true) -self:fireEvent("mouse_click",ba,ca,da)return true end end -function _a:mouse_up(ba,ca,da)if self:isInBounds(ca,da)then self.set("clicked",false) -self:fireEvent("mouse_up",ba,ca,da)return true end -self:fireEvent("mouse_release",ba,ca,da)end;function _a:mouse_release()self.set("clicked",false)end -function _a:handleEvent(ba,...)if -(self[ba])then return self[ba](self,...)end end -function _a:getAbsolutePosition(ba,ca)if(ba==nil)or(ca==nil)then -ba,ca=self.get("x"),self.get("y")end;local da=self.parent -while da do -local _b,ab=da.get("x"),da.get("y")ba=ba+_b-1;ca=ca+ab-1;da=da.parent end;return ba,ca end -function _a:getRelativePosition(ba,ca)if(ba==nil)or(ca==nil)then -ba,ca=self.get("x"),self.get("y")end;local da,_b=1,1;if self.parent then -da,_b=self.parent:getRelativePosition()end;local ab=self.get("x") -local bb=self.get("y")return ba- (ab-1)- (da-1),ca- (bb-1)- (_b-1)end -function _a:render()local ba,ca=self.get("width"),self.get("height") -self:multiBlit(1,1,ba,ca," ",aa[self.get("foreground")],aa[self.get("background")])end;return _a end -project["elements/BaseElement.lua"] = function(...) local c=require("propertySystem")local d=setmetatable({},c) -d.__index=d;d._events={} -d.defineProperty(d,"type",{default="BaseElement",type="string"}) -d.defineProperty(d,"eventCallbacks",{default={},type="table"}) -function d.new(_a,aa)local ba=setmetatable({},d):__init() -ba:init(_a,aa)ba.set("type","BaseElement")return ba end -function d:init(_a,aa)self.id=_a;self.basalt=aa;self._registeredEvents={} -if d._events then -for ba in -pairs(d._events)do self._registeredEvents[ba]=true -local ca="on".. -ba:gsub("_(%l)",function(da)return da:upper()end):gsub("^%l",string.upper) -self[ca]=function(da,...)da:registerCallback(ba,...)end end end;return self end;function d.listenTo(_a,aa)if not _a._events then _a._events={}end -_a._events[aa]=true end -function d:listenEvent(_a,aa)aa=aa~=false -if aa~= ( -self._registeredEvents[_a]or false)then -if aa then -self._registeredEvents[_a]=true -if self.parent then self.parent:registerChildEvent(self,_a)end else self._registeredEvents[_a]=nil;if self.parent then -self.parent:unregisterChildEvent(self,_a)end end end;return self end -function d:registerCallback(_a,aa)if not self._registeredEvents[_a]then -self:listenEvent(_a,true)end +local cc=require("errorManager")cc.header="Basalt Loading Error"cc.error(bc)end;local _c,ac=pcall(require,"main")package.path=ab +if not _c then db(ac)else return ac end end +project["libraries/expect.lua"] = function(...) local c=require("errorManager") +local function d(_a,aa,ba)local ca=type(aa) +if ba=="element"then if ca=="table"and +aa.get("type")~=nil then return true end end +if ba=="color"then +if ca=="number"and aa>=1 and aa<=32768 then return true end;if ca=="string"and colors[aa]then return true end end;if ca~=ba then c.header="Basalt Type Error" +c.error(string.format("Bad argument #%d: expected %s, got %s",_a,ba,ca))end;return true end;return d end +project["libraries/colorHex.lua"] = function(...) local b={}for i=0,15 do b[2 ^i]=("%x"):format(i) +b[("%x"):format(i)]=2 ^i end;return b end +project["libraries/utils.lua"] = function(...) local d,_a=math.floor,string.len;local aa={} +function aa.getCenteredPosition(ba,ca,da)local _b=_a(ba)local ab=d( +(ca-_b+1)/2 +0.5)local bb=d(da/2 +0.5)return ab,bb end +function aa.deepCopy(ba)if type(ba)~="table"then return ba end;local ca={}for da,_b in pairs(ba)do +ca[aa.deepCopy(da)]=aa.deepCopy(_b)end;return ca end +function aa.copy(ba)local ca={}for da,_b in pairs(ba)do ca[da]=_b end;return ca end;function aa.reverse(ba)local ca={}for i=#ba,1,-1 do table.insert(ca,ba[i])end +return ca end +function aa.uuid() +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 +function aa.split(ba,ca)local da={}local _b=1;local ab=_a(ba)local bb=1 +while true do local cb=ba:find(ca,_b,true)if not cb then +da[bb]=ba:sub(_b,ab)break end;da[bb]=ba:sub(_b,cb-1)_b=cb+1;bb=bb+1 end;return da end;return aa end +project["plugins/debug.lua"] = function(...) local da=require("log")local _b=require("libraries/colorHex") +local ab=10;local bb=false +local function cb(bc) +local cc={renderCount=0,eventCount={},lastRender=os.epoch("utc"),properties={},children={}} +return +{trackProperty=function(dc,_d)cc.properties[dc]=_d end,trackRender=function() +cc.renderCount=cc.renderCount+1;cc.lastRender=os.epoch("utc")end,trackEvent=function(dc)cc.eventCount[dc]=( +cc.eventCount[dc]or 0)+1 end,dump=function()return +{type=bc.get("type"),id=bc.get("id"),stats=cc}end}end +local db={debug=function(bc,cc)bc._debugger=cb(bc) +bc._debugLevel=cc or DEBUG_LEVELS.INFO;return bc end,dumpDebug=function(bc) +if not bc._debugger then return end;return bc._debugger.dump()end} +local _c={showDebugLog=function(bc) +if not bc._debugFrame then local cc=bc.get("width")local dc=bc.get("height") +bc._debugFrame=bc:addFrame("basaltDebugLog"):setWidth(cc):setHeight(dc):setZ(999):listenEvent("mouse_scroll",true) +bc.basalt.LOGGER.debug("Created debug log frame "..bc._debugFrame.get("name")) +bc._debugFrame:addButton("basaltDebugLogClose"):setWidth(9):setHeight(1):setX( +cc-8):setY(dc):setText("Close"):onMouseClick(function() +bc:hideDebugLog()end)bc._debugFrame._scrollOffset=0 +bc._debugFrame._processedLogs={} +local function _d(__a,a_a)local b_a={}while#__a>0 do local c_a=__a:sub(1,a_a)table.insert(b_a,c_a)__a=__a:sub( +a_a+1)end;return b_a end +local function ad()local __a={}local a_a=bc._debugFrame.get("width") +for b_a,c_a in +ipairs(da._logs)do local d_a=_d(c_a.message,a_a)for _aa,aaa in ipairs(d_a)do +table.insert(__a,{text=aaa,level=c_a.level})end end;return __a end;local bd=#ad()-bc.get("height")bc._scrollOffset=bd +local cd=bc._debugFrame.render +bc._debugFrame.render=function(__a)cd(__a)__a._processedLogs=ad() +local a_a=__a.get("height")-2;local b_a=#__a._processedLogs;local c_a=math.max(0,b_a-a_a) +__a._scrollOffset=math.min(__a._scrollOffset,c_a) +for i=1,a_a-2 do local d_a=i+__a._scrollOffset +local _aa=__a._processedLogs[d_a] +if _aa then +local aaa= + +_aa.level==da.LEVEL.ERROR and colors.red or _aa.level== +da.LEVEL.WARN and colors.yellow or _aa.level==da.LEVEL.DEBUG and colors.lightGray or colors.white;__a:textFg(2,i,_aa.text,aaa)end end end;local dd=bc._debugFrame.dispatchEvent +bc._debugFrame.dispatchEvent=function(__a,a_a,b_a,...) if -not self._values.eventCallbacks[_a]then self._values.eventCallbacks[_a]={}end -table.insert(self._values.eventCallbacks[_a],aa)return self end -function d:fireEvent(_a,...) -if self._values.eventCallbacks[_a]then for aa,ba in -ipairs(self._values.eventCallbacks[_a])do local ca=ba(self,...)return ca end end;return self end -function d:updateRender()if(self.parent)then self.parent:updateRender()else -self._renderUpdate=true end end;return d end -project["elements/Container.lua"] = function(...) local aa=require("elements/VisualElement") -local ba=require("elementManager")local ca=require("libraries/expect")local da=math.max -local _b=setmetatable({},aa)_b.__index=_b -_b.defineProperty(_b,"children",{default={},type="table"}) -_b.defineProperty(_b,"childrenEvents",{default={},type="table"}) -_b.defineProperty(_b,"eventListenerCount",{default={},type="table"}) -for ab,bb in pairs(ba:getElementList())do -local cb=ab:sub(1,1):upper()..ab:sub(2) -_b["add"..cb]=function(db,...)ca(1,db,"table") -local _c=db.basalt.create(ab,...) -db.basalt.LOGGER.debug(cb.." created with ID: ".._c.id)db:addChild(_c)return _c end end -function _b.new(ab,bb)local cb=setmetatable({},_b):__init() -cb:init(ab,bb)cb.set("type","Container")return cb end -function _b:addChild(ab) -if ab==self then error("Cannot add container to itself")end;local bb=ab.get("z")local cb=1;for db,_c in ipairs(self._values.children)do if -_c.get("z")>bb then break end;cb=db+1 end -table.insert(self._values.children,cb,ab)ab.parent=self;self:registerChildrenEvents(ab)return self end;function _b:sortChildren() -table.sort(self._values.children,function(ab,bb) -return ab.get("z")cb.get("z")end)end end -function _b:registerChildrenEvents(ab)for bb in pairs(ab._registeredEvents)do -self:registerChildEvent(ab,bb)end end -function _b:registerChildEvent(ab,bb) -if not self._values.childrenEvents[bb]then -self._values.childrenEvents[bb]={}self._values.eventListenerCount[bb]=0;if self.parent then -self.parent:registerChildEvent(self,bb)end end;for _c,ac in ipairs(self._values.childrenEvents[bb])do -if ac==ab then return end end;local cb=ab.get("z")local db=1 -for _c,ac in -ipairs(self._values.childrenEvents[bb])do if ac.get("z")ac then return end;local bc=ab<1 and(2 -ab)or 1 -local cc=math.min(#cb-bc+1, -_c-math.max(1,ab)+1)if cc<=0 then return end -aa.textFg(self,math.max(1,ab),math.max(1,bb),cb:sub(bc,bc+cc-1),db)end -function _b:render()aa.render(self) -for ab,bb in ipairs(self._values.children)do if bb==self then -self.basalt.LOGGER.error("CIRCULAR REFERENCE DETECTED!")return end;bb:render()end end;return _b end -project["elements/Frame.lua"] = function(...) local c=require("elements/Container") -local d=setmetatable({},c)d.__index=d -function d.new(_a,aa)local ba=setmetatable({},d):__init() -ba:init(_a,aa)ba.set("width",12)ba.set("height",6) -ba.set("background",colors.blue)ba.set("type","Frame")ba.set("z",10)return ba end;return d end +bd.default and bd.default.names and bd.default.names[dd]then for a_a,b_a in pairs(bd.default.names[dd])do +if type(b_a)~="table"then ad[a_a]=b_a end end end +if + +bd.default and bd.default[cd]and bd.default[cd].names and bd.default[cd].names[dd]then +for a_a,b_a in pairs(bd.default[cd].names[dd])do if +type(b_a)~="table"then ad[a_a]=b_a end end end;if __a and __a.names and __a.names[dd]then +for a_a,b_a in pairs(__a.names[dd])do if +type(b_a)~="table"then ad[a_a]=b_a end end end end +local function cc(ad,bd,cd,dd)local __a={}local a_a=_c(ad,bd) +if a_a then for b_a,c_a in pairs(a_a)do +if type(c_a)~="table"then __a[b_a]=c_a end end end;if next(__a)==nil then __a=ac(ad,cd)end +bc(__a,ad,cd,dd,a_a)return __a end +function db:applyTheme()local ad=self:getTheme()if(ad~=nil)then +for bd,cd in pairs(ad)do self.set(bd,cd)end end end +function db:getTheme()local ad=self:____getElementPath() +local bd=self.get("type")local cd=self.get("name")return cc(bb[cb],ad,bd,cd)end +local dc={setTheme=function(ad)ab=ad end,getTheme=function()return ab end,loadTheme=function(ad)local bd=fs.open(ad,"r") +if bd then +local cd=bd.readAll()bd.close()ab=textutils.unserializeJSON(cd)end end}local _d={BaseElement=db,API=dc}return _d end +project["plugins/reactive.lua"] = function(...) local bb=require("errorManager") +local cb=require("propertySystem")local db=require("log") +local _c={colors=true,math=true,clamp=true,round=true} +local ac={clamp=function(cd,dd,__a)return math.min(math.max(cd,dd),__a)end,round=function(cd)return math.floor( +cd+0.5)end} +local function bc(cd,dd,__a)cd=cd:gsub("^{(.+)}$","%1") +cd=cd:gsub("([%w_]+)%$([%w_]+)",function(d_a,_aa) +if d_a=="self"then return +string.format('__getState("%s")',_aa)elseif d_a=="parent"then return +string.format('__getParentState("%s")',_aa)else return +string.format('__getElementState("%s", "%s")',d_a,_aa)end end) +cd=cd:gsub("([%w_]+)%.([%w_]+)",function(d_a,_aa)if _c[d_a]then return d_a..".".._aa end;return +string.format('__getProperty("%s", "%s")',d_a,_aa)end) +local a_a=setmetatable({colors=colors,math=math,tostring=tostring,tonumber=tonumber,__getState=function(d_a)return dd:getState(d_a)end,__getParentState=function(d_a)return +dd.parent:getState(d_a)end,__getElementState=function(d_a,_aa) +local aaa=dd:getBaseFrame():getChild(d_a)if not aaa then bb.header="Reactive evaluation error" +bb.error("Could not find element: "..d_a)return nil end;return +aaa:getState(_aa).value end,__getProperty=function(d_a,_aa) +if +d_a=="self"then return dd.get(_aa)elseif d_a=="parent"then +return dd.parent.get(_aa)else local aaa=dd:getBaseFrame():getChild(d_a)if not aaa then +bb.header="Reactive evaluation error" +bb.error("Could not find element: "..d_a)return nil end +return aaa.get(_aa)end end},{__index=ac})if(dd._properties[__a].type=="string")then +cd="tostring("..cd..")"elseif(dd._properties[__a].type=="number")then +cd="tonumber("..cd..")"end;local b_a,c_a=load( +"return "..cd,"reactive","t",a_a) +if not b_a then +bb.header="Reactive evaluation error"bb.error("Invalid expression: "..c_a)return +function()return nil end end;return b_a end +local function cc(cd,dd) +for __a in cd:gmatch("([%w_]+)%.")do +if not _c[__a]then +if __a=="self"then elseif __a=="parent"then +if not dd.parent then +bb.header="Reactive evaluation error"bb.error("No parent element available")return false end else local a_a=dd:getBaseFrame():getChild(__a)if not a_a then +bb.header="Reactive evaluation error" +bb.error("Referenced element not found: "..__a)return false end end end end;return true end;local dc=setmetatable({},{__mode="k"}) +local _d=setmetatable({},{__mode="k",__index=function(cd,dd)cd[dd]={} +return cd[dd]end}) +local function ad(cd,dd,__a) +if _d[cd][__a]then for b_a,c_a in ipairs(_d[cd][__a])do +c_a.target:removeObserver(c_a.property,c_a.callback)end end;local a_a={} +for b_a,c_a in dd:gmatch("([%w_]+)%.([%w_]+)")do +if not _c[b_a]then local d_a;if b_a=="self"then d_a=cd elseif +b_a=="parent"then d_a=cd.parent else +d_a=cd:getBaseFrame():getChild(b_a)end;if d_a then +local _aa={target=d_a,property=c_a,callback=function() +cd:updateRender()end}d_a:observe(c_a,_aa.callback) +table.insert(a_a,_aa)end end end;_d[cd][__a]=a_a end +cb.addSetterHook(function(cd,dd,__a,a_a) +if type(__a)=="string"and __a:match("^{.+}$")then +local b_a=__a:gsub("^{(.+)}$","%1")if not cc(b_a,cd)then return a_a.default end;ad(cd,b_a,dd)if not +dc[cd]then dc[cd]={}end;if not dc[cd][__a]then local c_a=bc(__a,cd,dd) +dc[cd][__a]=c_a end +return +function(c_a)local d_a,_aa=pcall(dc[cd][__a]) +if +not d_a then bb.header="Reactive evaluation error"if type(_aa)=="string"then bb.error( +"Error evaluating expression: ".._aa)else +bb.error("Error evaluating expression")end +return a_a.default end;return _aa end end end)local bd={} +bd.hooks={destroy=function(cd) +if _d[cd]then +for dd,__a in pairs(_d[cd])do for a_a,b_a in ipairs(__a)do +b_a.target:removeObserver(b_a.property,b_a.callback)end end;_d[cd]=nil end end}return{BaseElement=bd} end +project["plugins/xml.lua"] = function(...) local da=require("errorManager") +local function _b(bc)local cc={attributes={}} +cc.name=bc:match("<(%w+)") +for dc,_d in bc:gmatch('%s(%w+)="([^"]-)"')do cc.attributes[dc]=_d end;return cc end +local function ab(bc,cc)local dc={}local _d={children={}}local ad=_d;local bd=false;local cd="" +for dd in cc:gmatch("[^\r\n]+")do +dd=dd:match("^%s*(.-)%s*$") +bc.basalt.LOGGER.debug("Parsing line: "..dd) +if dd:match("^$")and bd then bd=false +ad.content=cd elseif bd then cd=cd..dd.."\n"elseif dd:match("^<[^/]")then local __a=_b(dd)__a.children={} +__a.content=""table.insert(ad.children,__a)if not dd:match("/>$")then +table.insert(dc,ad)ad=__a end elseif dd:match("^0 then _d(__a,b_a,cd)end end end end;_d(dc,self,cc)return self end;return{BaseElement=_c,Container=ac} end +project["plugins/animation.lua"] = function(...) local aa={}aa.__index=aa;local ba={} +function aa.registerAnimation(ab,bb)ba[ab]=bb +aa[ab]=function(cb,...)local db={...} +local _c="linear" +if(type(db[#db])=="string")then _c=table.remove(db,#db)end;local ac=table.remove(db,#db) +return cb:addAnimation(ab,db,ac,_c)end end +local ca={linear=function(ab)return ab end,easeInQuad=function(ab)return ab*ab end,easeOutQuad=function(ab) +return 1 - (1 -ab)* (1 -ab)end,easeInOutQuad=function(ab)if ab<0.5 then return 2 *ab*ab end;return 1 - +(-2 *ab+2)^2 /2 end}function aa.registerEasing(ab,bb)ca[ab]=bb end;local da={}da.__index=da +function da.new(ab,bb,cb,db,_c) +local ac=setmetatable({},da)ac.element=ab;ac.type=bb;ac.args=cb;ac.duration=db;ac.startTime=0 +ac.isPaused=false;ac.handlers=ba[bb]ac.easing=_c;return ac end +function da:start()self.startTime=os.epoch("local")/1000;if +self.handlers.start then self.handlers.start(self)end end +function da:update(ab)local bb=math.min(1,ab/self.duration) +local cb=ca[self.easing](bb)return self.handlers.update(self,cb)end;function da:complete()if self.handlers.complete then +self.handlers.complete(self)end end +function aa.new(ab) +local bb={}bb.element=ab;bb.sequences={{}}bb.sequenceCallbacks={} +bb.currentSequence=1;bb.timer=nil;setmetatable(bb,aa)return bb end +function aa:sequence()table.insert(self.sequences,{})self.currentSequence=# +self.sequences;self.sequenceCallbacks[self.currentSequence]={start=nil,update=nil,complete= +nil}return self end +function aa:onStart(ab) +if +not self.sequenceCallbacks[self.currentSequence]then self.sequenceCallbacks[self.currentSequence]={}end +self.sequenceCallbacks[self.currentSequence].start=ab;return self end +function aa:onUpdate(ab) +if +not self.sequenceCallbacks[self.currentSequence]then self.sequenceCallbacks[self.currentSequence]={}end +self.sequenceCallbacks[self.currentSequence].update=ab;return self end +function aa:onComplete(ab) +if +not self.sequenceCallbacks[self.currentSequence]then self.sequenceCallbacks[self.currentSequence]={}end +self.sequenceCallbacks[self.currentSequence].complete=ab;return self end +function aa:addAnimation(ab,bb,cb,db)local _c=da.new(self.element,ab,bb,cb,db) +table.insert(self.sequences[self.currentSequence],_c)return self end +function aa:start()self.currentSequence=1 +if +(self.sequenceCallbacks[self.currentSequence])then if(self.sequenceCallbacks[self.currentSequence].start)then +self.sequenceCallbacks[self.currentSequence].start(self.element)end end +if +#self.sequences[self.currentSequence]>0 then self.timer=os.startTimer(0.05)for ab,bb in +ipairs(self.sequences[self.currentSequence])do bb:start()end end;return self end +function aa:event(ab,bb) +if ab=="timer"and bb==self.timer then +local cb=os.epoch("local")/1000;local db=true;local _c={} +local ac=self.sequenceCallbacks[self.currentSequence] +for bc,cc in ipairs(self.sequences[self.currentSequence])do +local dc=cb-cc.startTime;local _d=dc/cc.duration;local ad=cc:update(dc)if ac and ac.update then +ac.update(self.element,_d)end;if not ad then table.insert(_c,cc)db=false else +cc:complete()end end +if db then +if ac and ac.complete then ac.complete(self.element)end +if self.currentSequence<#self.sequences then +self.currentSequence=self.currentSequence+1;_c={} +local bc=self.sequenceCallbacks[self.currentSequence]if bc and bc.start then bc.start(self.element)end +for cc,dc in +ipairs(self.sequences[self.currentSequence])do dc:start()table.insert(_c,dc)end end end;if#_c>0 then self.timer=os.startTimer(0.05)end end end +aa.registerAnimation("move",{start=function(ab)ab.startX=ab.element.get("x") +ab.startY=ab.element.get("y")end,update=function(ab,bb)local cb=ab.startX+ +(ab.args[1]-ab.startX)*bb;local db=ab.startY+ +(ab.args[2]-ab.startY)*bb +ab.element.set("x",math.floor(cb))ab.element.set("y",math.floor(db))return bb>=1 end,complete=function(ab) +ab.element.set("x",ab.args[1])ab.element.set("y",ab.args[2])end}) +aa.registerAnimation("morphText",{start=function(ab)local bb=ab.element.get(ab.args[1]) +local cb=ab.args[2]local db=math.max(#bb,#cb) +local _c=string.rep(" ",math.floor(db-#bb)/2)ab.startText=_c..bb.._c +ab.targetText=cb..string.rep(" ",db-#cb)ab.length=db end,update=function(ab,bb) +local cb="" +for i=1,ab.length do local db=ab.startText:sub(i,i) +local _c=ab.targetText:sub(i,i) +if bb<0.5 then +cb=cb.. (math.random()>bb*2 and db or" ")else cb=cb.. +(math.random()> (bb-0.5)*2 and" "or _c)end end;ab.element.set(ab.args[1],cb)return bb>=1 end,complete=function(ab) +ab.element.set(ab.args[1],ab.targetText:gsub("%s+$",""))end}) +aa.registerAnimation("typewrite",{start=function(ab)ab.targetText=ab.args[2] +ab.element.set(ab.args[1],"")end,update=function(ab,bb) +local cb=math.floor(#ab.targetText*bb) +ab.element.set(ab.args[1],ab.targetText:sub(1,cb))return bb>=1 end}) +aa.registerAnimation("fadeText",{start=function(ab)ab.chars={}for i=1,#ab.args[2]do +ab.chars[i]={char=ab.args[2]:sub(i,i),visible=false}end end,update=function(ab,bb) +local cb=""for db,_c in ipairs(ab.chars)do +if math.random()=1 end}) +aa.registerAnimation("scrollText",{start=function(ab)ab.width=ab.element.get("width") +ab.targetText=ab.args[2]ab.element.set(ab.args[1],"")end,update=function(ab,bb)local cb=math.floor( +ab.width* (1 -bb)) +local db=string.rep(" ",cb) +ab.element.set(ab.args[1],db..ab.targetText)return bb>=1 end})local _b={hooks={}}function _b.hooks.dispatchEvent(ab,bb,...) +if bb=="timer"then local cb=ab.get("animation")if +cb then cb:event(bb,...)end end end +function _b.setup(ab) +VisualElementBaseDispatchEvent=ab.dispatchEvent +ab.defineProperty(ab,"animation",{default=nil,type="table"})ab.listenTo(ab,"timer")end +function _b:animate()local ab=aa.new(self)self.set("animation",ab)return ab end;return{VisualElement=_b} end +project["plugins/state.lua"] = function(...) local d=require("propertySystem") +local _a=require("errorManager")local aa={} +function aa.setup(ba) +ba.defineProperty(ba,"states",{default={},type="table"}) +ba.defineProperty(ba,"computedStates",{default={},type="table"}) +ba.defineProperty(ba,"stateUpdate",{default={key="",value=nil,oldValue=nil},type="table"})end +function aa:initializeState(ba,ca,da,_b,ab)local bb=self.get("states") +if bb[ba]then _a.error("State '".. +ba.."' already exists")return self end +if _b then local cb=ab or +("states/"..self.get("name").."_"..ba..".state") +if fs.exists(cb)then +local db=fs.open(cb,"r") +bb[ba]={value=textutils.unserialize(db.readAll()),persist=true,file=cb}db.close()else +bb[ba]={value=ca,persist=true,file=cb,canTriggerRender=da}end else bb[ba]={value=ca,canTriggerRender=da}end;return self end +function aa:setState(ba,ca)local da=self.get("states") +if not da[ba]then error("State '".. +ba.."' not initialized")end;local _b=da[ba].value;da[ba].value=ca +if da[ba].persist then +local ab=fs.getDir(da[ba].file)if not fs.exists(ab)then fs.makeDir(ab)end +local bb=fs.open(da[ba].file,"w")bb.write(textutils.serialize(ca)) +bb.close()end;if da[ba].canTriggerRender then self:updateRender()end +self.set("stateUpdate",{key=ba,value=ca,oldValue=_b})return self end +function aa:getState(ba)local ca=self.get("states") +if not ca[ba]then _a.error("State '".. +ba.."' not initialized")end;return ca[ba].value end +function aa:computed(ba,ca)local da=self.get("computedStates") +da[ba]=setmetatable({},{__call=function() +return ca(self)end})return self end +function aa:shareState(ba,...)local ca=self:getState(ba) +for da,_b in ipairs({...})do if +_b.get("states")[ba]then +_a.error("Cannot share state '"..ba.."': Target element already has this state")return self end +_b:initializeState(ba,ca) +self:observe("stateUpdate",function(ab,bb) +if bb.key==ba then _b:setState(ba,bb.value)end end)end;return self end +function aa:onStateChange(ba,ca)if not self.get("states")[ba]then +_a.error("Cannot observe state '"..ba.. +"': State not initialized")return self end +self:observe("stateUpdate",function(da,_b)if +_b.key==ba then ca(da,_b.value,_b.oldValue)end end)return self end;return{BaseElement=aa} end +project["plugins/benchmark.lua"] = function(...) local ca=require("log")local da=setmetatable({},{__mode="k"})local function _b()return +{methods={}}end +local function ab(_c,ac)local bc=_c[ac] +if not da[_c]then da[_c]=_b()end +if not da[_c].methods[ac]then +da[_c].methods[ac]={calls=0,totalTime=0,minTime=math.huge,maxTime=0,lastTime=0,startTime=0,path={},methodName=ac,originalMethod=bc}end +_c[ac]=function(cc,...)cc:startProfile(ac)local dc=bc(cc,...) +cc:endProfile(ac)return dc end end;local bb={} +function bb:startProfile(_c)local ac=da[self]if not ac then ac=_b()da[self]=ac end;if not +ac.methods[_c]then +ac.methods[_c]={calls=0,totalTime=0,minTime=math.huge,maxTime=0,lastTime=0,startTime=0,path={},methodName=_c}end +local bc=ac.methods[_c]bc.startTime=os.clock()*1000;bc.path={}local cc=self;while cc do +table.insert(bc.path,1, +cc.get("name")or cc.get("id"))cc=cc.parent end;return self end +function bb:endProfile(_c)local ac=da[self] +if not ac or not ac.methods[_c]then return self end;local bc=ac.methods[_c]local cc=os.clock()*1000 +local dc=cc-bc.startTime;bc.calls=bc.calls+1;bc.totalTime=bc.totalTime+dc +bc.minTime=math.min(bc.minTime,dc)bc.maxTime=math.max(bc.maxTime,dc)bc.lastTime=dc;return self end +function bb:benchmark(_c)if not self[_c]then +ca.error("Method ".._c.." does not exist")return self end;da[self]=_b() +da[self].methodName=_c;da[self].isRunning=true;ab(self,_c)return self end +function bb:logBenchmark(_c)local ac=da[self] +if not ac or not ac.methods[_c]then return self end;local bc=ac.methods[_c] +if bc then local cc= +bc.calls>0 and(bc.totalTime/bc.calls)or 0 +ca.info(string.format( +"Benchmark results for %s.%s: ".. +"Path: %s ".."Calls: %d ".. +"Average time: %.2fms ".."Min time: %.2fms ".."Max time: %.2fms ".. +"Last time: %.2fms ".."Total time: %.2fms",table.concat(bc.path,"."),bc.methodName,table.concat(bc.path,"/"),bc.calls,cc, +bc.minTime~=math.huge and bc.minTime or 0,bc.maxTime,bc.lastTime,bc.totalTime))end;return self end +function bb:stopBenchmark(_c)local ac=da[self] +if not ac or not ac.methods[_c]then return self end;local bc=ac.methods[_c]if bc and bc.originalMethod then +self[_c]=bc.originalMethod end;ac.methods[_c]=nil;if +not next(ac.methods)then da[self]=nil end;return self end +function bb:getBenchmarkStats(_c)local ac=da[self] +if not ac or not ac.methods[_c]then return nil end;local bc=ac.methods[_c]return +{averageTime=bc.totalTime/bc.calls,totalTime=bc.totalTime,calls=bc.calls,minTime=bc.minTime,maxTime=bc.maxTime,lastTime=bc.lastTime}end;local cb={} +function cb:benchmarkContainer(_c)self:benchmark(_c) +for ac,bc in +pairs(self.get("children"))do bc:benchmark(_c)if bc:isType("Container")then +bc:benchmarkContainer(_c)end end;return self end +function cb:logContainerBenchmarks(_c,ac)ac=ac or 0;local bc=string.rep(" ",ac)local cc=0;local dc={} +for ad,bd in +pairs(self.get("children"))do local cd=da[bd] +if cd and cd.methods[_c]then local dd=cd.methods[_c] +cc=cc+dd.totalTime +table.insert(dc,{element=bd,type=bd.get("type"),calls=dd.calls,totalTime=dd.totalTime,avgTime=dd.totalTime/dd.calls})end end;local _d=da[self] +if _d and _d.methods[_c]then local ad=_d.methods[_c] +local bd=ad.totalTime-cc;local cd=bd/ad.calls +ca.info(string.format("%sBenchmark %s (%s): ".."%.2fms/call (Self: %.2fms/call) ".. +"[Total: %dms, Calls: %d]",bc,self.get("type"),_c, +ad.totalTime/ad.calls,cd,ad.totalTime,ad.calls)) +if#dc>0 then +for dd,__a in ipairs(dc)do +if __a.element:isType("Container")then __a.element:logContainerBenchmarks(_c, +ac+1)else +ca.info(string.format("%s> %s: %.2fms/call [Total: %dms, Calls: %d]", +bc.." ",__a.type,__a.avgTime,__a.totalTime,__a.calls))end end end end;return self end +function cb:stopContainerBenchmark(_c) +for ac,bc in pairs(self.get("children"))do if bc:isType("Container")then +bc:stopContainerBenchmark(_c)else bc:stopBenchmark(_c)end end;self:stopBenchmark(_c)return self end +local db={start=function(_c,ac)ac=ac or{}local bc=_b()bc.name=_c;bc.startTime=os.clock()*1000 +bc.custom=true;da[_c]=bc end,stop=function(_c) +local ac=da[_c]if not ac or not ac.custom then return end +local bc=os.clock()*1000;local cc=bc-ac.startTime;ac.calls=ac.calls+1 +ac.totalTime=ac.totalTime+cc;ac.minTime=math.min(ac.minTime,cc) +ac.maxTime=math.max(ac.maxTime,cc)ac.lastTime=cc +ca.info(string.format("Custom Benchmark '%s': ".. +"Calls: %d ".."Average time: %.2fms ".. +"Min time: %.2fms ".. +"Max time: %.2fms ".."Last time: %.2fms ".."Total time: %.2fms",_c,ac.calls, +ac.totalTime/ac.calls,ac.minTime,ac.maxTime,ac.lastTime,ac.totalTime))end,getStats=function(_c) +local ac=da[_c]if not ac then return nil end;return +{averageTime=ac.totalTime/ac.calls,totalTime=ac.totalTime,calls=ac.calls,minTime=ac.minTime,maxTime=ac.maxTime,lastTime=ac.lastTime}end,clear=function(_c)da[_c]= +nil end,clearAll=function() +for _c,ac in pairs(da)do if ac.custom then da[_c]=nil end end end}return{BaseElement=bb,Container=cb,API=db} end +project["plugins/pluginTemplate.lua"] = function(...) local b={hooks={init={}}}function b.setup(c) +c.defineProperty(c,"testProp",{default=5,type="number"})end;function b.hooks.init(c)end +function b:testFunc()end;return{VisualElement=b} end project["errorManager.lua"] = function(...) local d=require("log") local _a={tracebackEnabled=true,header="Basalt Error"}local function aa(ba,ca)term.setTextColor(ca)print(ba) term.setTextColor(colors.white)end function _a.error(ba) -term.setBackgroundColor(colors.black)term.clear()term.setCursorPos(1,1) +if _a.errorHandled then error()end;term.setBackgroundColor(colors.black) +term.clear()term.setCursorPos(1,1) aa(_a.header..":",colors.red)print()local ca=2;local da;while true do local db=debug.getinfo(ca,"Sl") if not db then break end;da=db;ca=ca+1 end;local _b=da or debug.getinfo(2,"Sl")local ab=_b.source:sub(2) @@ -300,21 +670,883 @@ if db then local _c=""local ac=1 repeat _c=db.readLine()if ac==tonumber(bb)then aa("\149Line "..bb,colors.cyan) aa(_c,colors.lightGray)break end;ac=ac+1 until not _c;db.close()end end;term.setBackgroundColor(colors.black) -d.error(ba)error()end;return _a end -project["libraries/utils.lua"] = function(...) local d,_a=math.floor,string.len;local aa={} -function aa.getCenteredPosition(ba,ca,da)local _b=_a(ba)local ab=d( -(ca-_b+1)/2 +0.5)local bb=d(da/2 +0.5)return ab,bb end -function aa.deepCopy(ba)if type(ba)~="table"then return ba end;local ca={}for da,_b in pairs(ba)do -ca[aa.deepCopy(da)]=aa.deepCopy(_b)end;return ca end;return aa end -project["libraries/expect.lua"] = function(...) local c=require("errorManager") -local function d(_a,aa,ba)local ca=type(aa) -if ba=="element"then if ca=="table"and -aa.get("type")~=nil then return true end end -if ba=="color"then -if ca=="number"and aa>=1 and aa<=32768 then return true end;if ca=="string"and colors[aa]then return true end end;if ca~=ba then c.header="Basalt Type Error" -c.error(string.format("Bad argument #%d: expected %s, got %s",_a,ba,ca))end;return true end;return d end -project["libraries/colorHex.lua"] = function(...) local b={}for i=0,15 do b[2 ^i]=("%x"):format(i) -b[("%x"):format(i)]=2 ^i end;return b end +d.error(ba)_a.errorHandled=true;error()end;return _a end +project["elements/Table.lua"] = function(...) local d=require("elements/VisualElement") +local _a=require("libraries/colorHex")local aa=setmetatable({},d)aa.__index=aa +aa.defineProperty(aa,"columns",{default={},type="table"}) +aa.defineProperty(aa,"data",{default={},type="table",canTriggerRender=true}) +aa.defineProperty(aa,"selectedRow",{default=nil,type="number",canTriggerRender=true}) +aa.defineProperty(aa,"headerColor",{default=colors.blue,type="number"}) +aa.defineProperty(aa,"selectedColor",{default=colors.lightBlue,type="number"}) +aa.defineProperty(aa,"gridColor",{default=colors.gray,type="number"}) +aa.defineProperty(aa,"sortColumn",{default=nil,type="number"}) +aa.defineProperty(aa,"sortDirection",{default="asc",type="string"}) +aa.defineProperty(aa,"scrollOffset",{default=0,type="number",canTriggerRender=true})aa.listenTo(aa,"mouse_click") +aa.listenTo(aa,"mouse_scroll") +function aa.new()local ba=setmetatable({},aa):__init() +ba.set("width",30)ba.set("height",10)ba.set("z",5)return ba end +function aa:init(ba,ca)d.init(self,ba,ca)self.set("type","Table")return self end +function aa:setColumns(ba)self.set("columns",ba)return self end;function aa:setData(ba)self.set("data",ba)return self end +function aa:sortData(ba) +local ca=self.get("data")local da=self.get("sortDirection") +table.sort(ca,function(_b,ab)if da=="asc"then return +_b[ba]ab[ba]end end)self.set("data",ca)return self end +function aa:mouse_click(ba,ca,da) +if not d.mouse_click(self,ba,ca,da)then return false end;local _b,ab=self:getRelativePosition(ca,da) +if ab==1 then local bb=1 +for cb,db in +ipairs(self.get("columns"))do +if _b>=bb and _b1 then local bb=ab-2 +self.get("scrollOffset")if bb>=0 and bb<# +self.get("data")then +self.set("selectedRow",bb+1)end end;return true end +function aa:mouse_scroll(ba,ca,da)local _b=self.get("data")local ab=self.get("height") +local bb=ab-2;local cb=math.max(0,#_b-bb+1) +local db=math.min(cb,math.max(0, +self.get("scrollOffset")+ba))self.set("scrollOffset",db)return true end +function aa:render()d.render(self)local ba=self.get("columns") +local ca=self.get("data")local da=self.get("selectedRow") +local _b=self.get("sortColumn")local ab=self.get("scrollOffset")local bb=self.get("height") +local cb=1 +for _c,ac in ipairs(ba)do local bc=ac.name;if _c==_b then +bc=bc.. (self.get("sortDirection")=="asc"and +"\30"or"\31")end +self:textFg(cb,1,bc:sub(1,ac.width),self.get("headerColor"))cb=cb+ac.width end;local db=bb-2 +for y=2,bb do local _c=y-2 +ab;local ac=ca[_c+1] +if ac and(_c+1)<=#ca then cb=1 +local bc= +(_c+1)==da and self.get("selectedColor")or self.get("background") +for cc,dc in ipairs(ba)do local _d=ac[cc]or"" +local ad=_d..string.rep(" ",dc.width-#_d) +self:blit(cb,y,ad,string.rep(_a[self.get("foreground")],dc.width),string.rep(_a[bc],dc.width))cb=cb+dc.width end else +self:blit(1,y,string.rep(" ",self.get("width")),string.rep(_a[self.get("foreground")],self.get("width")),string.rep(_a[self.get("background")],self.get("width")))end end +if#ca>bb-2 then local _c=bb-2 +local ac=math.max(1,math.floor(_c* (bb-2)/#ca))local bc=#ca- (bb-2)+1;local cc=ab/bc +local dc=2 +math.floor(cc* (_c-ac))if ab>=bc then dc=bb-ac end;for y=2,bb do +self:blit(self.get("width"),y,"\127",_a[colors.gray],_a[colors.gray])end;for y=dc,math.min(bb,dc+ac-1)do +self:blit(self.get("width"),y,"\127",_a[colors.white],_a[colors.white])end end end;return aa end +project["elements/BaseFrame.lua"] = function(...) local _a=require("elementManager") +local aa=_a.getElement("Container")local ba=require("render")local ca=setmetatable({},aa)ca.__index=ca +ca.defineProperty(ca,"term",{default= +nil,type="table",setter=function(da,_b) +if _b==nil or _b.setCursorPos==nil then return _b end;da._render=ba.new(_b)da._renderUpdate=true;local ab,bb=_b.getSize() +da.set("width",ab)da.set("height",bb)return _b end}) +function ca.new()local da=setmetatable({},ca):__init() +da.set("term",term.current())da.set("background",colors.lightGray)return da end +function ca:init(da,_b)aa.init(self,da,_b)self.set("type","BaseFrame")end;function ca:multiBlit(da,_b,ab,bb,cb,db,_c) +self._render:multiBlit(da,_b,ab,bb,cb,db,_c)end;function ca:textFg(da,_b,ab,bb) +self._render:textFg(da,_b,ab,bb)end;function ca:textBg(da,_b,ab,bb) +self._render:textBg(da,_b,ab,bb)end;function ca:blit(da,_b,ab,bb,cb) +self._render:blit(da,_b,ab,bb,cb)end;function ca:setCursor(da,_b,ab)local bb=self.get("term") +self._render:setCursor(da,_b,ab)end;function ca:render() +if(self._renderUpdate)then if +self._render~=nil then aa.render(self)self._render:render() +self._renderUpdate=false end end end +return ca end +project["elements/ProgressBar.lua"] = function(...) local c=require("elements/VisualElement") +local d=setmetatable({},c)d.__index=d +d.defineProperty(d,"progress",{default=0,type="number",canTriggerRender=true}) +d.defineProperty(d,"showPercentage",{default=false,type="boolean"}) +d.defineProperty(d,"progressColor",{default=colors.lime,type="number"}) +function d.new()local _a=setmetatable({},d):__init() +_a.set("width",10)_a.set("height",1)return _a end +function d:init(_a,aa)c.init(self,_a,aa)self.set("type","ProgressBar")end +function d:render()c.render(self)local _a=self.get("width") +local aa=math.min(100,math.max(0,self.get("progress")))local ba=math.floor((_a*aa)/100) +self:textBg(1,1,string.rep(" ",ba),self.get("progressColor"))if self.get("showPercentage")then local ca=tostring(aa).."%"local da=math.floor( +(_a-#ca)/2)+1 +self:textFg(da,1,ca,self.get("foreground"))end end;return d end +project["elements/Menu.lua"] = function(...) local _a=require("elements/VisualElement") +local aa=require("elements/List")local ba=require("libraries/colorHex") +local ca=setmetatable({},aa)ca.__index=ca +ca.defineProperty(ca,"separatorColor",{default=colors.gray,type="number"})function ca.new()local da=setmetatable({},ca):__init() +da.set("width",30)da.set("height",1) +da.set("background",colors.gray)return da end +function ca:init(da,_b) +aa.init(self,da,_b)self.set("type","Menu")return self end +function ca:setItems(da)local _b={}local ab=0 +for bb,cb in ipairs(da)do +if cb.separator then +table.insert(_b,{text=cb.text or"|",selectable=false})ab=ab+1 else local db=" "..cb.text.." "cb.text=db +table.insert(_b,cb)ab=ab+#db end end;self.set("width",ab)return aa.setItems(self,_b)end +function ca:render()_a.render(self)local da=1 +for _b,ab in ipairs(self.get("items"))do local bb=_b== +self.get("selectedIndex") +local cb=ab.selectable==false and +self.get("separatorColor")or(bb and +(ab.selectedForeground or self.get("foreground"))or +(ab.foreground or self.get("foreground"))) +local db= +bb and(ab.selectedBackground or self.get("selectedColor"))or(ab.background or self.get("background")) +self:blit(da,1,ab.text,string.rep(ba[cb],#ab.text),string.rep(ba[db],#ab.text))da=da+#ab.text end end +function ca:mouse_click(da,_b,ab) +if not _a.mouse_click(self,da,_b,ab)then return false end +if(self.get("selectable")==false)then return false end +local bb=select(1,self:getRelativePosition(_b,ab))local cb=1 +for db,_c in ipairs(self.get("items"))do +if +bb>=cb and bbdd then dd=c_a end +if(b_a==bb)then __a=__a+dd+dc;dd=1;cd=cd+1 +bd[cd]={offset=__a}else table.insert(bd[cd],b_a)end end elseif(_d)then local cd=1;local dd=1 +local __a=cc=="row"and bc.get("width")or bc.get("height")local a_a=0;local b_a=1 +for c_a,d_a in pairs(ad)do +if(bd[b_a]==nil)then bd[b_a]={offset=1}end +if d_a:getType()=="lineBreak"then dd=dd+cd+dc;a_a=0;cd=1;b_a=b_a+1 +bd[b_a]={offset=dd}else +local _aa=cc=="row"and d_a.get("width")or d_a.get("height") +if(_aa+a_a<=__a)then table.insert(bd[b_a],d_a) +a_a=a_a+_aa+dc else dd=dd+cd+dc +cd=cc=="row"and d_a.get("height")or d_a.get("width")b_a=b_a+1;a_a=_aa+dc;bd[b_a]={offset=dd,d_a}end +local aaa=cc=="row"and d_a.get("height")or d_a.get("width")if aaa>cd then cd=aaa end end end end;return bd end +local function db(bc,cc,dc,_d)local ad=bc.get("width")local bd=dc* (#cc-1)local cd=0;for c_a,d_a in ipairs(cc)do +if d_a~=bb then bd=bd+ +d_a.get("width")cd=cd+d_a.get("flexGrow")end end;local dd=ad-bd;local __a= +cd>0 and(dd/cd)or 0;local a_a=0;local b_a=1 +for c_a,d_a in ipairs(cc)do +if d_a~=bb then +local _aa=d_a.get("width") +if d_a.get("flexGrow")>0 then if c_a==#cc then local aaa=dd-a_a;_aa=_aa+aaa else local aaa=math.floor(__a* +d_a.get("flexGrow"))_aa=_aa+aaa +a_a=a_a+aaa end end;d_a.set("x",b_a)d_a.set("y",cc.offset or 1) +d_a.set("width",_aa)b_a=b_a+_aa+dc end end +if _d=="flex-end"then local c_a=ad- (b_a-dc-1)for d_a,_aa in ipairs(cc)do +_aa.set("x",_aa.get("x")+c_a)end elseif _d=="flex-center"or _d=="center"then local c_a=math.floor(( +ad- (b_a-dc-1))/2)for d_a,_aa in ipairs(cc)do _aa.set("x", +_aa.get("x")+c_a)end end end +local function _c(bc,cc,dc,_d)local ad=bc.get("height")local bd=dc* (#cc-1)local cd=0;for c_a,d_a in ipairs(cc)do +if d_a~=bb then bd=bd+ +d_a.get("height")cd=cd+d_a.get("flexGrow")end end;local dd=ad-bd;local __a= +cd>0 and(dd/cd)or 0;local a_a=0;local b_a=1 +for c_a,d_a in ipairs(cc)do +if d_a~=bb then +local _aa=d_a.get("height") +if d_a.get("flexGrow")>0 then if c_a==#cc then local aaa=dd-a_a;_aa=_aa+aaa else local aaa=math.floor(__a* +d_a.get("flexGrow"))_aa=_aa+aaa +a_a=a_a+aaa end end;d_a.set("x",cc.offset or 1)d_a.set("y",b_a) +d_a.set("height",_aa)b_a=b_a+_aa+dc end end +if _d=="flex-end"then local c_a=ad- (b_a-dc-1)for d_a,_aa in ipairs(cc)do +_aa.set("y",_aa.get("y")+c_a)end elseif _d=="flex-center"or _d=="center"then local c_a=math.floor(( +ad- (b_a-dc-1))/2)for d_a,_aa in ipairs(cc)do _aa.set("y", +_aa.get("y")+c_a)end end end +local function ac(bc,cc,dc,_d,ad)local bd=cb(bc,cc,dc,ad) +if cc=="row"then +for cd,dd in pairs(bd)do db(bc,dd,dc,_d)end else for cd,dd in pairs(bd)do _c(bc,dd,dc,_d)end end;bc.set("flexUpdateLayout",false)end +function ab.new()local bc=setmetatable({},ab):__init() +bc.set("width",12)bc.set("height",6) +bc.set("background",colors.blue)bc.set("z",10) +bc:observe("width",function() +bc.set("flexUpdateLayout",true)end) +bc:observe("height",function()bc.set("flexUpdateLayout",true)end)return bc end +function ab:init(bc,cc)_b.init(self,bc,cc)self.set("type","Flexbox")end +function ab:addChild(bc)_b.addChild(self,bc) +if(bc~=bb)then +bc:instanceProperty("flexGrow",{default=0,type="number"}) +bc:instanceProperty("flexShrink",{default=0,type="number"}) +bc:instanceProperty("flexBasis",{default=0,type="number"})end;self.set("flexUpdateLayout",true)return self end +function ab:removeChild(bc)_b.removeChild(self,bc) +if(bc~=bb)then bc.setFlexGrow=nil;bc.setFlexShrink= +nil;bc.setFlexBasis=nil;bc.getFlexGrow=nil;bc.getFlexShrink=nil;bc.getFlexBasis= +nil;bc.set("flexGrow",nil) +bc.set("flexShrink",nil)bc.set("flexBasis",nil)end;self.set("flexUpdateLayout",true)return self end;function ab:addLineBreak()self:addChild(bb)return self end +function ab:render() +if +(self.get("flexUpdateLayout"))then +ac(self,self.get("flexDirection"),self.get("flexSpacing"),self.get("flexJustifyContent"),self.get("flexWrap"))end;_b.render(self)end;return ab end +project["elements/Container.lua"] = function(...) local _b=require("elementManager") +local ab=_b.getElement("VisualElement")local bb=require("libraries/expect") +local cb=require("libraries/utils").split;local db=math.max;local _c=setmetatable({},ab)_c.__index=_c +_c.defineProperty(_c,"children",{default={},type="table"}) +_c.defineProperty(_c,"childrenSorted",{default=true,type="boolean"}) +_c.defineProperty(_c,"childrenEventsSorted",{default=true,type="boolean"}) +_c.defineProperty(_c,"childrenEvents",{default={},type="table"}) +_c.defineProperty(_c,"eventListenerCount",{default={},type="table"}) +_c.defineProperty(_c,"focusedChild",{default=nil,type="table",setter=function(dc,_d,ad)local bd=dc._values.focusedChild +if _d==bd then return _d end +if bd then +if bd:isType("Container")then bd.set("focusedChild",nil,true)end;bd.set("focused",false,true)end +if _d and not ad then _d.set("focused",true,true)if dc.parent then +dc.parent:setFocusedChild(dc)end end;return _d end}) +_c.defineProperty(_c,"visibleChildren",{default={},type="table"}) +_c.defineProperty(_c,"visibleChildrenEvents",{default={},type="table"}) +function _c:isChildVisible(dc)local _d,ad=dc.get("x"),dc.get("y") +local bd,cd=dc.get("width"),dc.get("height")local dd,__a=self.get("width"),self.get("height") +return +_d<=dd and ad<=__a and _d+bd>0 and ad+cd>0 end +for dc,_d in pairs(_b:getElementList())do +local ad=dc:sub(1,1):upper()..dc:sub(2) +if ad~="BaseFrame"then +_c["add"..ad]=function(bd,...)bb(1,bd,"table") +local cd=bd.basalt.create(dc,...)bd:addChild(cd)cd:postInit()return cd end +_c["addDelayed"..ad]=function(bd,cd)bb(1,bd,"table") +local dd=bd.basalt.create(dc,cd,true,bd)return dd end end end +function _c.new()local dc=setmetatable({},_c):__init()return dc end +function _c:init(dc,_d)ab.init(self,dc,_d)self.set("type","Container")end +function _c:addChild(dc) +if dc==self then error("Cannot add container to itself")end;table.insert(self._values.children,dc) +dc.parent=self;self.set("childrenSorted",false) +self:registerChildrenEvents(dc)return self end +local function ac(dc,_d)local ad={} +for bd,cd in ipairs(_d)do if dc:isChildVisible(cd)and cd.get("visible")then +table.insert(ad,cd)end end +for i=2,#ad do local bd=ad[i]local cd=bd.get("z")local dd=i-1 +while dd>0 do +local __a=ad[dd].get("z")if __a>cd then ad[dd+1]=ad[dd]dd=dd-1 else break end end;ad[dd+1]=bd end;return ad end +function _c:clear()self.set("children",{}) +self.set("childrenEvents",{})self.set("visibleChildren",{}) +self.set("visibleChildrenEvents",{})self.set("childrenSorted",true) +self.set("childrenEventsSorted",true)end +function _c:sortChildren() +self.set("visibleChildren",ac(self,self._values.children))self.set("childrenSorted",true)end +function _c:sortChildrenEvents(dc)if self._values.childrenEvents[dc]then +self._values.visibleChildrenEvents[dc]=ac(self,self._values.childrenEvents[dc])end +self.set("childrenEventsSorted",true)end +function _c:registerChildrenEvents(dc)if(dc._registeredEvents==nil)then return end +for _d in +pairs(dc._registeredEvents)do self:registerChildEvent(dc,_d)end end +function _c:registerChildEvent(dc,_d) +if not self._values.childrenEvents[_d]then +self._values.childrenEvents[_d]={}self._values.eventListenerCount[_d]=0;if self.parent then +self.parent:registerChildEvent(self,_d)end end;for ad,bd in ipairs(self._values.childrenEvents[_d])do +if bd==dc then return end end +self.set("childrenEventsSorted",false) +table.insert(self._values.childrenEvents[_d],dc)self._values.eventListenerCount[_d]= +self._values.eventListenerCount[_d]+1 end +function _c:removeChildrenEvents(dc)if(dc._registeredEvents==nil)then return end +for _d in +pairs(dc._registeredEvents)do self:unregisterChildEvent(dc,_d)end end +function _c:unregisterChildEvent(dc,_d) +if self._values.childrenEvents[_d]then +for ad,bd in +ipairs(self._values.childrenEvents[_d])do +if bd==dc then +table.remove(self._values.childrenEvents[_d],ad)self._values.eventListenerCount[_d]= +self._values.eventListenerCount[_d]-1 +if +self._values.eventListenerCount[_d]<=0 then +self._values.childrenEvents[_d]=nil;self._values.eventListenerCount[_d]=nil;if self.parent then +self.parent:unregisterChildEvent(self,_d)end end;break end end end end +function _c:removeChild(dc) +for _d,ad in ipairs(self._values.children)do if ad==dc then +table.remove(self._values.children,_d)dc.parent=nil;break end end;self:removeChildrenEvents(dc)return self end +function _c:getChild(dc) +if type(dc)=="string"then local _d=cb(dc,"/") +for ad,bd in +pairs(self._values.children)do if bd.get("name")==_d[1]then +if#_d==1 then return bd else if(bd:isType("Container"))then return +bd:find(table.concat(_d,"/",2))end end end end end;return nil end +local function bc(dc,_d,...)local ad={...}if _d:find("mouse_")then local bd,cd,dd=... +local __a,a_a=dc:getRelativePosition(cd,dd)ad={bd,__a,a_a}end +return ad end +local function cc(dc,_d,ad,...) +local bd=_d and dc.get("visibleChildrenEvents")or dc.get("childrenEvents") +if bd[ad]then local cd=bd[ad]for i=#cd,1,-1 do local dd=cd[i] +if(dd:dispatchEvent(ad,...))then return true,dd end end end;return false end +function _c:handleEvent(dc,...)ab.handleEvent(self,dc,...)local _d=bc(self,dc,...)return +cc(self,false,dc,table.unpack(_d))end +function _c:mouse_click(dc,_d,ad) +if ab.mouse_click(self,dc,_d,ad)then +local bd=bc(self,"mouse_click",dc,_d,ad) +local cd,dd=cc(self,true,"mouse_click",table.unpack(bd)) +if(cd)then self.set("focusedChild",dd)return true end;self.set("focusedChild",nil)end end +function _c:mouse_up(dc,_d,ad) +if ab.mouse_up(self,dc,_d,ad)then local bd=bc(self,"mouse_up",dc,_d,ad) +local cd,dd=cc(self,true,"mouse_up",table.unpack(bd))if(cd)then return true end end end;function _c:key(dc)if self.get("focusedChild")then return +self.get("focusedChild"):dispatchEvent("key",dc)end +return true end +function _c:char(dc)if +self.get("focusedChild")then +return self.get("focusedChild"):dispatchEvent("char",dc)end;return true end;function _c:key_up(dc) +if self.get("focusedChild")then return +self.get("focusedChild"):dispatchEvent("key_up",dc)end;return true end +function _c:multiBlit(dc,_d,ad,bd,cd,dd,__a) +local a_a,b_a=self.get("width"),self.get("height")ad=dc<1 and math.min(ad+dc-1,a_a)or +math.min(ad,math.max(0,a_a-dc+1))bd=_d<1 and math.min( +bd+_d-1,b_a)or +math.min(bd,math.max(0,b_a-_d+1))if ad<=0 or +bd<=0 then return end +ab.multiBlit(self,math.max(1,dc),math.max(1,_d),ad,bd,cd,dd,__a)end +function _c:textFg(dc,_d,ad,bd)local cd,dd=self.get("width"),self.get("height")if +_d<1 or _d>dd then return end;local __a=dc<1 and(2 -dc)or 1 +local a_a=math.min(#ad-__a+1, +cd-math.max(1,dc)+1)if a_a<=0 then return end +ab.textFg(self,math.max(1,dc),math.max(1,_d),ad:sub(__a,__a+a_a-1),bd)end +function _c:blit(dc,_d,ad,bd,cd)local dd,__a=self.get("width"),self.get("height")if +_d<1 or _d>__a then return end;local a_a=dc<1 and(2 -dc)or 1 +local b_a=math.min(#ad-a_a+1, +dd-math.max(1,dc)+1) +local c_a=math.min(#bd-a_a+1,dd-math.max(1,dc)+1) +local d_a=math.min(#cd-a_a+1,dd-math.max(1,dc)+1)if b_a<=0 then return end;local _aa=ad:sub(a_a,a_a+b_a-1)local aaa=bd:sub(a_a, +a_a+c_a-1) +local baa=cd:sub(a_a,a_a+d_a-1) +ab.blit(self,math.max(1,dc),math.max(1,_d),_aa,aaa,baa)end +function _c:render()ab.render(self)if not self.get("childrenSorted")then +self:sortChildren()end +if +not self.get("childrenEventsSorted")then for dc in pairs(self._values.childrenEvents)do +self:sortChildrenEvents(dc)end end +for dc,_d in ipairs(self.get("visibleChildren"))do if _d==self then +self.basalt.LOGGER.error("CIRCULAR REFERENCE DETECTED!")return end;_d:render()end end;return _c end +project["elements/Slider.lua"] = function(...) local c=require("elements/VisualElement") +local d=setmetatable({},c)d.__index=d +d.defineProperty(d,"step",{default=1,type="number",canTriggerRender=true}) +d.defineProperty(d,"max",{default=100,type="number"}) +d.defineProperty(d,"horizontal",{default=true,type="boolean",canTriggerRender=true}) +d.defineProperty(d,"barColor",{default=colors.gray,type="number",canTriggerRender=true}) +d.defineProperty(d,"sliderColor",{default=colors.blue,type="number",canTriggerRender=true})d.listenTo(d,"mouse_click") +d.listenTo(d,"mouse_drag")d.listenTo(d,"mouse_up")function d.new() +local _a=setmetatable({},d):__init()_a.set("width",8)_a.set("height",1) +_a.set("backgroundEnabled",false)return _a end;function d:init(_a,aa) +c.init(self,_a,aa)self.set("type","Slider")end +function d:getValue() +local _a=self.get("step")local aa=self.get("max") +local ba= +self.get("horizontal")and self.get("width")or self.get("height")return math.floor((_a-1)* (aa/ (ba-1)))end +function d:mouse_click(_a,aa,ba) +if _a==1 and self:isInBounds(aa,ba)then +local ca,da=self:getRelativePosition(aa,ba) +local _b=self.get("horizontal")and ca or da;local ab=self.get("horizontal")and self.get("width")or +self.get("height") +self.set("step",math.min(ab,math.max(1,_b)))self:updateRender()return true end end;d.mouse_drag=d.mouse_click +function d:mouse_scroll(_a,aa,ba) +if self:isInBounds(aa,ba)then +local ca=self.get("step")local da=self.get("horizontal")and self.get("width")or +self.get("height") +self.set("step",math.min(da,math.max(1, +ca+_a)))self:updateRender()return true end end +function d:render()c.render(self)local _a=self.get("width") +local aa=self.get("height")local ba=self.get("horizontal")local ca=self.get("step")local da= +ba and"\140"or"│" +local _b=string.rep(da,ba and _a or aa) +if ba then self:textFg(1,1,_b,self.get("barColor")) +self:textBg(ca,1," ",self.get("sliderColor"))else +for y=1,aa do self:textFg(1,y,da,self.get("barColor"))end +self:textFg(1,ca,"\140",self.get("sliderColor"))end end;return d end +project["elements/BaseElement.lua"] = function(...) local d=require("propertySystem") +local _a=require("/libraries/utils").uuid;local aa=setmetatable({},d)aa.__index=aa;aa._events={} +aa.defineProperty(aa,"type",{default={"BaseElement"},type="string",setter=function(ba,ca)if +type(ca)=="string"then table.insert(ba._values.type,1,ca)return +ba._values.type end;return ca end,getter=function(ba,ca,da)if +da~=nil and da<1 then return ba._values.type end;return ba._values.type[ +da or 1]end}) +aa.defineProperty(aa,"id",{default="",type="string",readonly=true}) +aa.defineProperty(aa,"name",{default="",type="string"}) +aa.defineProperty(aa,"eventCallbacks",{default={},type="table"})function aa.listenTo(ba,ca)if not ba._events then ba._events={}end +ba._events[ca]=true end;function aa.new() +local ba=setmetatable({},aa):__init()return ba end +function aa:init(ba,ca)self._props=ba +self._values.id=_a()self.basalt=ca;self._registeredEvents={} +if aa._events then +for da in pairs(aa._events)do +self._registeredEvents[da]=true +local _b="on".. +da:gsub("_(%l)",function(ab)return ab:upper()end):gsub("^%l",string.upper) +self[_b]=function(ab,...)ab:registerCallback(da,...)return ab end end end;return self end +function aa:postInit()if(self._props)then +for ba,ca in pairs(self._props)do self.set(ba,ca)end end;self._props=nil;return self end;function aa:isType(ba) +for ca,da in ipairs(self._values.type)do if da==ba then return true end end;return false end +function aa:listenEvent(ba,ca)ca= +ca~=false +if +ca~= (self._registeredEvents[ba]or false)then +if ca then self._registeredEvents[ba]=true;if self.parent then +self.parent:registerChildEvent(self,ba)end else self._registeredEvents[ba]=nil +if +self.parent then self.parent:unregisterChildEvent(self,ba)end end end;return self end +function aa:registerCallback(ba,ca)if not self._registeredEvents[ba]then +self:listenEvent(ba,true)end +if +not self._values.eventCallbacks[ba]then self._values.eventCallbacks[ba]={}end +table.insert(self._values.eventCallbacks[ba],ca)return self end +function aa:fireEvent(ba,...) +if self._values.eventCallbacks[ba]then for ca,da in +ipairs(self._values.eventCallbacks[ba])do local _b=da(self,...)return _b end end;return self end;function aa:dispatchEvent(ba,...)if self[ba]then return self[ba](self,...)end;return +self:handleEvent(ba,...)end;function aa:handleEvent(ba,...)return +false end +function aa:getBaseFrame()if self.parent then return +self.parent:getBaseFrame()end;return self end;function aa:destroy()end +function aa:updateRender()if(self.parent)then +self.parent:updateRender()else self._renderUpdate=true end end;return aa end +project["elements/Checkbox.lua"] = function(...) local c=require("elements/VisualElement") +local d=setmetatable({},c)d.__index=d +d.defineProperty(d,"checked",{default=false,type="boolean",canTriggerRender=true}) +d.defineProperty(d,"text",{default="",type="string",canTriggerRender=true}) +d.defineProperty(d,"symbol",{default="x",type="string"})d.listenTo(d,"mouse_click") +function d.new() +local _a=setmetatable({},d):__init()_a.set("width",1)_a.set("height",1)return _a end +function d:init(_a,aa)c.init(self,_a,aa)self.set("type","Checkbox")end +function d:mouse_click(_a,aa,ba) +if c.mouse_click(self,_a,aa,ba)then +self.set("checked",not self.get("checked")) +self:fireEvent("change",self.get("checked"))return true end end +function d:render()c.render(self)local _a= +self.get("checked")and self.get("symbol")or" " +self:textFg(1,1,"[".._a.."]",self.get("foreground"))local aa=self.get("text")if#aa>0 then +self:textFg(4,1,aa,self.get("foreground"))end end;return d end +project["elements/Input.lua"] = function(...) local d=require("elements/VisualElement") +local _a=require("libraries/colorHex")local aa=setmetatable({},d)aa.__index=aa +aa.defineProperty(aa,"text",{default="",type="string",canTriggerRender=true}) +aa.defineProperty(aa,"cursorPos",{default=1,type="number"}) +aa.defineProperty(aa,"viewOffset",{default=0,type="number",canTriggerRender=true}) +aa.defineProperty(aa,"maxLength",{default=nil,type="number"}) +aa.defineProperty(aa,"placeholder",{default="asd",type="string"}) +aa.defineProperty(aa,"placeholderColor",{default=colors.gray,type="number"}) +aa.defineProperty(aa,"focusedColor",{default=colors.blue,type="number"}) +aa.defineProperty(aa,"pattern",{default=nil,type="string"})aa.listenTo(aa,"mouse_click")aa.listenTo(aa,"key") +aa.listenTo(aa,"char")function aa.new()local ba=setmetatable({},aa):__init() +ba.set("width",8)ba.set("z",3)return ba end;function aa:init(ba,ca) +d.init(self,ba,ca)self.set("type","Input")end +function aa:char(ba)if +not self.get("focused")then return end +local ca=self.get("text")local da=self.get("cursorPos")local _b=self.get("maxLength") +local ab=self.get("pattern")if _b and#ca>=_b then return end +if ab and not ba:match(ab)then return end +self.set("text",ca:sub(1,da-1)..ba..ca:sub(da))self.set("cursorPos",da+1)self:updateRender() +self:updateViewport()end +function aa:key(ba)if not self.get("focused")then return end +local ca=self.get("cursorPos")local da=self.get("text")local _b=self.get("viewOffset") +local ab=self.get("width") +if ba==keys.left then if ca>1 then self.set("cursorPos",ca-1) +if ca-1 <=_b then self.set("viewOffset",math.max(0, +ca-2))end end elseif ba==keys.right then if ca<=#da then self.set("cursorPos", +ca+1)if ca-_b>=ab then +self.set("viewOffset",ca-ab+1)end end elseif +ba==keys.backspace then if ca>1 then +self.set("text",da:sub(1,ca-2)..da:sub(ca))self.set("cursorPos",ca-1)self:updateRender() +self:updateViewport()end end +local bb=self.get("cursorPos")-self.get("viewOffset")self:setCursor(bb,1,true)end +function aa:focus()d.focus(self)self:updateRender()end +function aa:blur()d.blur(self)self:updateRender()end +function aa:mouse_click(ba,ca,da) +if d.mouse_click(self,ba,ca,da)then +local _b,ab=self:getRelativePosition(ca,da)local bb=self.get("text") +self:setCursor(math.min(_b,#bb+1),ab,true) +self:set("cursorPos",_b+self.get("viewOffset"))return true end end +function aa:updateViewport()local ba=self.get("width") +local ca=self.get("cursorPos")local da=self.get("viewOffset") +local _b=#self.get("text") +if ca-da>ba then self.set("viewOffset",ca-ba)elseif ca<=da then self.set("viewOffset",math.max(0, +ca-1))end;if da>_b-ba then +self.set("viewOffset",math.max(0,_b-ba))end end +function aa:render()local ba=self.get("text")local ca=self.get("viewOffset") +local da=self.get("width")local _b=self.get("placeholder") +local ab=self.get("focusedColor")local bb=self.get("focused") +local cb,db=self.get("width"),self.get("height") +self:multiBlit(1,1,cb,db," ",_a[self.get("foreground")],_a[bb and ab or +self.get("background")])if +#ba==0 and#_b~=0 and self.get("focused")==false then +self:textFg(1,1,_b:sub(1,cb),self.get("placeholderColor"))return end +local _c=ba:sub(ca+1,ca+cb)self:textFg(1,1,_c,self.get("foreground"))end;return aa end +project["elements/VisualElement.lua"] = function(...) local ba=require("elementManager") +local ca=ba.getElement("BaseElement")local da=require("libraries/colorHex") +local _b=setmetatable({},ca)_b.__index=_b +_b.defineProperty(_b,"x",{default=1,type="number",canTriggerRender=true}) +_b.defineProperty(_b,"y",{default=1,type="number",canTriggerRender=true}) +_b.defineProperty(_b,"z",{default=1,type="number",canTriggerRender=true,setter=function(cb,db) +if cb.parent then cb.parent:sortChildren()end;return db end}) +_b.defineProperty(_b,"width",{default=1,type="number",canTriggerRender=true}) +_b.defineProperty(_b,"height",{default=1,type="number",canTriggerRender=true}) +_b.defineProperty(_b,"background",{default=colors.black,type="number",canTriggerRender=true}) +_b.defineProperty(_b,"foreground",{default=colors.white,type="number",canTriggerRender=true}) +_b.defineProperty(_b,"clicked",{default=false,type="boolean"}) +_b.defineProperty(_b,"backgroundEnabled",{default=true,type="boolean",canTriggerRender=true}) +_b.defineProperty(_b,"focused",{default=false,type="boolean",setter=function(cb,db,_c)local ac=cb.get("focused") +if db==ac then return db end;if db then cb:focus()else cb:blur()end;if not _c and cb.parent then +if db then +cb.parent:setFocusedChild(cb)else cb.parent:setFocusedChild(nil)end end;return db end}) +_b.defineProperty(_b,"visible",{default=true,type="boolean",canTriggerRender=true,setter=function(cb,db) +if(cb.parent~=nil)then +cb.parent.set("childrenSorted",false)cb.parent.set("childrenEventsSorted",false)end;return db end})_b.combineProperties(_b,"position","x","y") +_b.combineProperties(_b,"size","width","height") +_b.combineProperties(_b,"color","foreground","background")_b.listenTo(_b,"focus")_b.listenTo(_b,"blur") +local ab,bb=math.max,math.min +function _b.new()local cb=setmetatable({},_b):__init()return cb end;function _b:init(cb,db)ca.init(self,cb,db) +self.set("type","VisualElement")end;function _b:multiBlit(cb,db,_c,ac,bc,cc,dc) +cb=cb+self.get("x")-1;db=db+self.get("y")-1 +self.parent:multiBlit(cb,db,_c,ac,bc,cc,dc)end;function _b:textFg(cb,db,_c,ac)cb=cb+ +self.get("x")-1;db=db+self.get("y")-1 +self.parent:textFg(cb,db,_c,ac)end;function _b:textBg(cb,db,_c,ac)cb=cb+ +self.get("x")-1;db=db+self.get("y")-1 +self.parent:textBg(cb,db,_c,ac)end;function _b:blit(cb,db,_c,ac,bc)cb=cb+ +self.get("x")-1;db=db+self.get("y")-1 +self.parent:blit(cb,db,_c,ac,bc)end +function _b:isInBounds(cb,db) +local _c,ac=self.get("x"),self.get("y")local bc,cc=self.get("width"),self.get("height")return +cb>=_c and cb<= +_c+bc-1 and db>=ac and db<=ac+cc-1 end +function _b:mouse_click(cb,db,_c)if self:isInBounds(db,_c)then self.set("clicked",true) +self:fireEvent("mouse_click",cb,self:getRelativePosition(db,_c))return true end;return +false end +function _b:mouse_up(cb,db,_c)self.set("clicked",false)if self:isInBounds(db,_c)then +self:fireEvent("mouse_up",cb,db,_c)return true end +self:fireEvent("mouse_release",cb,self:getRelativePosition(db,_c))end;function _b:mouse_release()self.set("clicked",false)end;function _b:focus() +self:fireEvent("focus")end;function _b:blur()self:fireEvent("blur") +self:setCursor(1,1,false)end +function _b:getAbsolutePosition(cb,db) +local _c,ac=self.get("x"),self.get("y")if(cb~=nil)then _c=_c+cb-1 end;if(db~=nil)then ac=ac+db-1 end +local bc=self.parent;while bc do local cc,dc=bc.get("x"),bc.get("y")_c=_c+cc-1;ac=ac+dc-1 +bc=bc.parent end;return _c,ac end +function _b:getRelativePosition(cb,db)if(cb==nil)or(db==nil)then +cb,db=self.get("x"),self.get("y")end;local _c,ac=1,1;if self.parent then +_c,ac=self.parent:getRelativePosition()end;local bc=self.get("x") +local cc=self.get("y")return cb- (bc-1)- (_c-1),db- (cc-1)- (ac-1)end +function _b:setCursor(cb,db,_c) +if self.parent then local ac,bc=self:getAbsolutePosition(cb,db) +ac=ab(self.get("x"),bb(ac, +self.get("width")+self.get("x")-1))return self.parent:setCursor(ac,bc,_c)end end +function _b:render() +if(not self.get("backgroundEnabled"))then return end;local cb,db=self.get("width"),self.get("height") +self:multiBlit(1,1,cb,db," ",da[self.get("foreground")],da[self.get("background")])end;return _b end +project["elements/List.lua"] = function(...) local c=require("elements/VisualElement") +local d=setmetatable({},c)d.__index=d +d.defineProperty(d,"items",{default={},type="table",canTriggerRender=true}) +d.defineProperty(d,"selectedIndex",{default=0,type="number",canTriggerRender=true}) +d.defineProperty(d,"selectable",{default=true,type="boolean"}) +d.defineProperty(d,"offset",{default=0,type="number",canTriggerRender=true}) +d.defineProperty(d,"selectedColor",{default=colors.blue,type="number"})d.listenTo(d,"mouse_click") +d.listenTo(d,"mouse_scroll")function d.new()local _a=setmetatable({},d):__init() +_a.set("width",16)_a.set("height",8) +_a.set("background",colors.gray)return _a end;function d:init(_a,aa) +c.init(self,_a,aa)self.set("type","List")end +function d:addItem(_a) +local aa=self.get("items")table.insert(aa,_a)self:updateRender()return self end;function d:removeItem(_a)local aa=self.get("items")table.remove(aa,_a) +self:updateRender()return self end;function d:clear() +self.set("items",{})self.set("selectedIndex",0)self:updateRender() +return self end +function d:mouse_click(_a,aa,ba) +if _a==1 and +self:isInBounds(aa,ba)and self.get("selectable")then +local ca,da=self:getRelativePosition(aa,ba)local _b=da+self.get("offset") +local ab=self.get("items") +if _b<=#ab then local bb=ab[_b]self.set("selectedIndex",_b) +if +type(bb)=="table"and bb.callback then bb.callback(self)end;self:fireEvent("select",_b,bb)self:updateRender()return +true end end;return false end +function d:mouse_scroll(_a,aa,ba) +if self:isInBounds(aa,ba)then local ca=self.get("offset") +local da=math.max(0,# +self.get("items")-self.get("height"))ca=math.min(da,math.max(0,ca+_a)) +self.set("offset",ca)return true end end +function d:onSelect(_a)self:registerCallback("select",_a)return self end +function d:render()c.render(self)local _a=self.get("items") +local aa=self.get("height")local ba=self.get("offset") +local ca=self.get("selectedIndex")local da=self.get("width") +for i=1,aa do local _b=i+ba;local ab=_a[_b] +if ab then +if type(ab)=="table"and +ab.separator then local bb=(ab.text or"-"):sub(1,1) +local cb=string.rep(bb,da)local db=ab.foreground or self.get("foreground")local _c= +ab.background or self.get("background") +self:textBg(1,i,string.rep(" ",da),_c)self:textFg(1,i,cb,db)else +local bb=type(ab)=="table"and ab.text or ab;local cb=_b==ca +local db=cb and +(ab.selectedBackground or self.get("selectedColor"))or +(ab.background or self.get("background")) +local _c=cb and(ab.selectedForeground or colors.white)or(ab.foreground or +self.get("foreground"))self:textBg(1,i,string.rep(" ",da),db) +self:textFg(1,i,bb,_c)end end end end;return d end +project["elements/Tree.lua"] = function(...) local _a=require("elements/VisualElement") +local aa=require("libraries/colorHex")local ba=setmetatable({},_a)ba.__index=ba +ba.defineProperty(ba,"nodes",{default={},type="table",canTriggerRender=true}) +ba.defineProperty(ba,"selectedNode",{default=nil,type="table",canTriggerRender=true}) +ba.defineProperty(ba,"expandedNodes",{default={},type="table",canTriggerRender=true}) +ba.defineProperty(ba,"scrollOffset",{default=0,type="number",canTriggerRender=true}) +ba.defineProperty(ba,"nodeColor",{default=colors.white,type="number"}) +ba.defineProperty(ba,"selectedColor",{default=colors.lightBlue,type="number"})ba.listenTo(ba,"mouse_click") +ba.listenTo(ba,"mouse_scroll") +function ba.new()local da=setmetatable({},ba):__init() +da.set("width",30)da.set("height",10)da.set("z",5)return da end +function ba:init(da,_b)_a.init(self,da,_b)self.set("type","Tree")return self end +function ba:setNodes(da)self.set("nodes",da)if#da>0 then +self.get("expandedNodes")[da[1]]=true end;return self end;function ba:expandNode(da)self.get("expandedNodes")[da]=true +self:updateRender()return self end +function ba:collapseNode(da)self.get("expandedNodes")[da]= +nil;self:updateRender()return self end;function ba:toggleNode(da)if self.get("expandedNodes")[da]then +self:collapseNode(da)else self:expandNode(da)end +return self end +local function ca(da,_b,ab,bb)bb=bb or{}ab= +ab or 0;for cb,db in ipairs(da)do table.insert(bb,{node=db,level=ab}) +if +_b[db]and db.children then ca(db.children,_b,ab+1,bb)end end;return bb end +function ba:mouse_click(da,_b,ab) +if not _a.mouse_click(self,da,_b,ab)then return false end;local bb,cb=self:getRelativePosition(_b,ab) +local db=ca(self.get("nodes"),self.get("expandedNodes"))local _c=cb+self.get("scrollOffset") +if db[_c]then local ac=db[_c] +local bc=ac.node +if bb<=ac.level*2 +2 then self:toggleNode(bc)end;self.set("selectedNode",bc) +self:fireEvent("node_select",bc)end;return true end +function ba:onSelect(da)self:registerCallback("node_select",da)return self end +function ba:mouse_scroll(da) +local _b=ca(self.get("nodes"),self.get("expandedNodes")) +local ab=math.max(0,#_b-self.get("height")) +local bb=math.min(ab,math.max(0,self.get("scrollOffset")+da))self.set("scrollOffset",bb)return true end +function ba:render()_a.render(self) +local da=ca(self.get("nodes"),self.get("expandedNodes"))local _b=self.get("height")local ab=self.get("selectedNode") +local bb=self.get("expandedNodes")local cb=self.get("scrollOffset") +for y=1,_b do local db=da[y+cb] +if db then local _c=db.node +local ac=db.level;local bc=string.rep(" ",ac)local cc=" "if +_c.children and#_c.children>0 then cc=bb[_c]and"\31"or"\16"end +local dc= +_c==ab and self.get("selectedColor")or self.get("background") +local _d=bc..cc.." ".. (_c.text or"Node") +self:blit(1,y,_d..string.rep(" ",self.get("width")-#_d),string.rep(aa[self.get("nodeColor")],self.get("width")),string.rep(aa[dc],self.get("width")))else +self:blit(1,y,string.rep(" ",self.get("width")),string.rep(aa[self.get("foreground")],self.get("width")),string.rep(aa[self.get("background")],self.get("width")))end end end;return ba end +project["elements/Dropdown.lua"] = function(...) local _a=require("elements/VisualElement") +local aa=require("elements/List")local ba=require("libraries/colorHex") +local ca=setmetatable({},aa)ca.__index=ca +ca.defineProperty(ca,"isOpen",{default=false,type="boolean",canTriggerRender=true}) +ca.defineProperty(ca,"dropdownHeight",{default=5,type="number"}) +ca.defineProperty(ca,"selectedText",{default="",type="string"}) +ca.defineProperty(ca,"dropSymbol",{default="\31",type="string"}) +function ca.new()local da=setmetatable({},ca):__init() +da.set("width",16)da.set("height",1)da.set("z",8)return da end +function ca:init(da,_b)aa.init(self,da,_b)self.set("type","Dropdown")end +function ca:mouse_click(da,_b,ab) +if not _a.mouse_click(self,da,_b,ab)then return false end;local bb,cb=self:getRelativePosition(_b,ab) +if cb==1 then self.set("isOpen",not +self.get("isOpen"))if +not self.get("isOpen")then self.set("height",1)else +self.set("height",1 +math.min(self.get("dropdownHeight"),# +self.get("items")))end +return true elseif self.get("isOpen")and cb>1 then +local db=cb-1 +self.get("offset")local _c=self.get("items") +if db<=#_c then local ac=_c[db]if type(ac)=="table"and +ac.separator then return false end +self.set("selectedIndex",db)self.set("isOpen",false)self.set("height",1) +if type(ac)== +"table"and ac.callback then ac.callback(self)end;self:fireEvent("select",db,ac)return true end end;return false end +function ca:render()_a.render(self)local da=self.get("selectedText") +if +#da==0 and self.get("selectedIndex")>0 then +local _b=self.get("items")[self.get("selectedIndex")] +da=type(_b)=="table"and _b.text or tostring(_b)end +self:blit(1,1,da.. + +string.rep(" ",self.get("width")-#da-1).. (self.get("isOpen")and"\31"or"\17"),string.rep(ba[self.get("foreground")],self.get("width")),string.rep(ba[self.get("background")],self.get("width"))) +if self.get("isOpen")then local _b=self.get("items") +local ab=self.get("offset")local bb=self.get("selectedIndex")local cb=self.get("width") +for i=2,self.get("height") +do local db=i-1 +ab;local _c=_b[db] +if _c then +if type(_c)=="table"and _c.separator then local ac=( +_c.text or"-"):sub(1,1) +local bc=string.rep(ac,cb)local cc=_c.foreground or self.get("foreground")local dc= +_c.background or self.get("background") +self:textBg(1,i,string.rep(" ",cb),dc)self:textFg(1,i,bc,cc)else local ac=type(_c)=="table"and _c.text or +tostring(_c)local bc=db==bb +local cc=bc and(_c.selectedBackground or +self.get("selectedColor"))or(_c.background or +self.get("background")) +local dc=bc and(_c.selectedForeground or colors.white)or(_c.foreground or +self.get("foreground"))self:textBg(1,i,string.rep(" ",cb),cc) +self:textFg(1,i,ac,dc)end end end end end;return ca end +project["render.lua"] = function(...) local c={}c.__index=c;local d=require("libraries/colorHex") +function c.new(_a) +local aa=setmetatable({},c)aa.terminal=_a;aa.width,aa.height=_a.getSize() +aa.buffer={text={},fg={},bg={},dirtyRects={}} +for y=1,aa.height do aa.buffer.text[y]=string.rep(" ",aa.width) +aa.buffer.fg[y]=string.rep("0",aa.width)aa.buffer.bg[y]=string.rep("f",aa.width)end;return aa end;function c:addDirtyRect(_a,aa,ba,ca) +table.insert(self.buffer.dirtyRects,{x=_a,y=aa,width=ba,height=ca})end +function c:blit(_a,aa,ba,ca,da)if aa<1 or +aa>self.height then return self end;if(#ba~=#ca or#ba~=#da)then +error("Text, fg, and bg must be the same length")end +self.buffer.text[aa]=self.buffer.text[aa]:sub(1, +_a-1)..ba.. +self.buffer.text[aa]:sub(_a+#ba) +self.buffer.fg[aa]= +self.buffer.fg[aa]:sub(1,_a-1)..ca..self.buffer.fg[aa]:sub(_a+#ca) +self.buffer.bg[aa]= +self.buffer.bg[aa]:sub(1,_a-1)..da..self.buffer.bg[aa]:sub(_a+#da)self:addDirtyRect(_a,aa,#ba,1)return self end +function c:multiBlit(_a,aa,ba,ca,da,_b,ab)if aa<1 or aa>self.height then return self end;if( +#da~=#_b or#da~=#ab)then +error("Text, fg, and bg must be the same length")end;da=da:rep(ba) +_b=_b:rep(ba)ab=ab:rep(ba) +for dy=0,ca-1 do local bb=aa+dy +if bb>=1 and bb<=self.height then +self.buffer.text[bb]=self.buffer.text[bb]:sub(1, +_a-1)..da.. +self.buffer.text[bb]:sub(_a+#da) +self.buffer.fg[bb]= +self.buffer.fg[bb]:sub(1,_a-1).._b..self.buffer.fg[bb]:sub(_a+#_b) +self.buffer.bg[bb]= +self.buffer.bg[bb]:sub(1,_a-1)..ab..self.buffer.bg[bb]:sub(_a+#ab)end end;self:addDirtyRect(_a,aa,ba,ca)return self end +function c:textFg(_a,aa,ba,ca)if aa<1 or aa>self.height then return self end +ca=d[ca]or"0" +self.buffer.text[aa]= +self.buffer.text[aa]:sub(1,_a-1).. +ba..self.buffer.text[aa]:sub(_a+#ba) +self.buffer.fg[aa]= +self.buffer.fg[aa]:sub(1,_a-1).. +ca:rep(#ba)..self.buffer.fg[aa]:sub(_a+#ba)self:addDirtyRect(_a,aa,#ba,1)return self end +function c:textBg(_a,aa,ba,ca)if aa<1 or aa>self.height then return self end +ca=d[ca]or"f" +self.buffer.text[aa]= +self.buffer.text[aa]:sub(1,_a-1).. +ba..self.buffer.text[aa]:sub(_a+#ba) +self.buffer.bg[aa]= +self.buffer.bg[aa]:sub(1,_a-1).. +ca:rep(#ba)..self.buffer.bg[aa]:sub(_a+#ba)self:addDirtyRect(_a,aa,#ba,1)return self end +function c:text(_a,aa,ba)if aa<1 or aa>self.height then return self end +self.buffer.text[aa]=self.buffer.text[aa]:sub(1, +_a-1)..ba.. +self.buffer.text[aa]:sub(_a+#ba)self:addDirtyRect(_a,aa,#ba,1)return self end +function c:fg(_a,aa,ba)if aa<1 or aa>self.height then return self end +self.buffer.fg[aa]=self.buffer.fg[aa]:sub(1, +_a-1)..ba.. +self.buffer.fg[aa]:sub(_a+#ba)self:addDirtyRect(_a,aa,#ba,1)return self end +function c:bg(_a,aa,ba)if aa<1 or aa>self.height then return self end +self.buffer.bg[aa]=self.buffer.bg[aa]:sub(1, +_a-1)..ba.. +self.buffer.bg[aa]:sub(_a+#ba)self:addDirtyRect(_a,aa,#ba,1)return self end +function c:blit(_a,aa,ba,ca,da)if aa<1 or aa>self.height then return self end;if(#ba~=#ca or +#ba~=#da)then +error("Text, fg, and bg must be the same length")end +self.buffer.text[aa]=self.buffer.text[aa]:sub(1, +_a-1)..ba.. +self.buffer.text[aa]:sub(_a+#ba) +self.buffer.fg[aa]= +self.buffer.fg[aa]:sub(1,_a-1)..ca..self.buffer.fg[aa]:sub(_a+#ca) +self.buffer.bg[aa]= +self.buffer.bg[aa]:sub(1,_a-1)..da..self.buffer.bg[aa]:sub(_a+#da)self:addDirtyRect(_a,aa,#ba,1)return self end +function c:clear(_a)local aa=d[_a]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(aa,self.width)self:addDirtyRect(1,y,self.width,1)end;return self end +function c:render()local _a={} +for aa,ba in ipairs(self.buffer.dirtyRects)do local ca=false;for da,_b in ipairs(_a)do +if +self:rectOverlaps(ba,_b)then self:mergeRects(_b,ba)ca=true;break end end;if not ca then +table.insert(_a,ba)end end +for aa,ba in ipairs(_a)do +for y=ba.y,ba.y+ba.height-1 do +if y>=1 and y<=self.height then +self.terminal.setCursorPos(ba.x,y) +self.terminal.blit(self.buffer.text[y]:sub(ba.x,ba.x+ba.width-1),self.buffer.fg[y]:sub(ba.x, +ba.x+ba.width-1),self.buffer.bg[y]:sub(ba.x, +ba.x+ba.width-1))end end end;self.buffer.dirtyRects={}if self.blink then +self.terminal.setCursorPos(self.xCursor,self.yCursor)self.terminal.setCursorBlink(true)else +self.terminal.setCursorBlink(false)end;return +self end +function c:rectOverlaps(_a,aa)return +not( +_a.x+_a.width<=aa.x or aa.x+aa.width<=_a.x or _a.y+_a.height<=aa.y or +aa.y+aa.height<=_a.y)end +function c:mergeRects(_a,aa)local ba=math.min(_a.x,aa.x) +local ca=math.min(_a.y,aa.y) +local da=math.max(_a.x+_a.width,aa.x+aa.width) +local _b=math.max(_a.y+_a.height,aa.y+aa.height)_a.x=ba;_a.y=ca;_a.width=da-ba;_a.height=_b-ca end +function c:setCursor(_a,aa,ba)self.terminal.setCursorPos(_a,aa) +self.terminal.setCursorBlink(ba)self.xCursor=_a;self.yCursor=aa;self.blink=ba;return self end +function c:clearArea(_a,aa,ba,ca,da)local _b=d[da]or"f" +for dy=0,ca-1 do local ab=aa+dy +if ab>=1 and ab<=self.height then +local bb=string.rep(" ",ba)local cb=string.rep(_b,ba)self:blit(_a,ab,bb,"0",_b)end end;return self end;function c:getSize()return self.width,self.height end;return c end project["log.lua"] = function(...) local aa={}aa._logs={}aa._enabled=true;aa._logToFile=true aa._logFile="basalt.log"fs.delete(aa._logFile) aa.LEVEL={DEBUG=1,INFO=2,WARN=3,ERROR=4} @@ -333,68 +1565,4 @@ table.insert(aa._logs,{time=bb,level=ab,message=cc})end;function aa.debug(...)_b _b(aa.LEVEL.INFO,...)end function aa.warn(...)_b(aa.LEVEL.WARN,...)end;function aa.error(...)_b(aa.LEVEL.ERROR,...)end aa.info("Logger initialized")return aa end -project["main.lua"] = function(...) local da=require("elementManager") -local _b=require("libraries/expect")local ab=require("errorManager")local bb={}bb.traceback=true -bb._events={}bb._schedule={}bb._plugins={}bb.LOGGER=require("log")local cb=nil -local db=false -function bb.create(bc,cc)if(cc==nil)then cc=da.generateId()end -local dc=da.getElement(bc).new(cc,bb)local _d,ad=pcall(require,"main")if not _d then ab(false,ad)end -return dc end -function bb.createFrame()local bc=bb.create("BaseFrame")cb=bc;return bc end;function bb.getElementManager()return da end;function bb.getMainFrame()if(cb==nil)then -cb=bb.createFrame()end;return cb end;function bb.setActiveFrame(bc) -cb=bc;return false end;function bb.scheduleUpdate(bc) -table.insert(bb._schedule,bc)return#bb._schedule end;function bb.removeSchedule(bc)bb._schedule[bc]= -nil end -local function _c(bc,...) -if(bc=="terminate")then bb.stop()end -if bc:find("mouse")then if cb then cb:handleEvent(bc,...)end end -if bc:find("key")then if cb then cb:handleEvent(bc,...)end end;if bb._events[bc]then -for cc,dc in ipairs(bb._events[bc])do dc(...)end end end;local function ac()if(cb)then cb:render()end end;function bb.update() -for bc,cc in -pairs(bb._schedule)do if type(cc)=="function"then cc()end end end;function bb.stop()term.clear() -term.setCursorPos(1,1)db=false end -function bb.run(bc)db=bc -if(bc==nil)then db=true end -local function cc()ac()while db do _c(os.pullEventRaw())end end -while db do local dc,_d=pcall(cc)if not(dc)then ab.header="Basalt Runtime Error" -ab.error(_d)end end end;bb.autoUpdate=bb.run;return bb end -project["propertySystem.lua"] = function(...) local d=require("libraries/utils").deepCopy -local _a=require("libraries/expect")local aa={}aa.__index=aa;aa._properties={} -function aa.defineProperty(ba,ca,da)if -not rawget(ba,'_properties')then ba._properties={}end -ba._properties[ca]={type=da.type,default=da.default,canTriggerRender=da.canTriggerRender,getter=da.getter,setter=da.setter}local _b=ca:sub(1,1):upper()..ca:sub(2) -ba[ -"get".._b]=function(ab)_a(1,ab,"element")local bb=ab._values[ca]return da.getter and -da.getter(bb)or bb end -ba["set".._b]=function(ab,bb)_a(1,ab,"element")_a(2,bb,da.type)if da.setter then -bb=da.setter(ab,bb)end;ab:_updateProperty(ca,bb)return ab end end -function aa:__init()self._values={}self._observers={} -self.set=function(ab,bb) -local cb=self._values[ab]self._values[ab]=bb;if(self._properties[ab].setter)then -bb=self._properties[ab].setter(self,bb)end;if -cb~=bb and self._observers[ab]then -for db,_c in ipairs(self._observers[ab])do _c(self,bb,cb)end end end -self.get=function(ab)return self._values[ab]end;local ba={}local ca=getmetatable(self).__index -while ca do if -rawget(ca,'_properties')then -for ab,bb in pairs(ca._properties)do if not ba[ab]then ba[ab]=bb end end end;ca=getmetatable(ca)and -rawget(getmetatable(ca),'__index')end;self._properties=ba;local da=getmetatable(self)local _b=da.__index -setmetatable(self,{__index=function(ab,bb)if -self._properties[bb]then return self._values[bb]end;if -type(_b)=="function"then return _b(ab,bb)else return _b[bb]end end,__newindex=function(ab,bb,cb) -if -self._properties[bb]then if self._properties[bb].setter then -cb=self._properties[bb].setter(self,cb)end -self:_updateProperty(bb,cb)else rawset(ab,bb,cb)end end,__tostring=function(ab)return -string.format("Object: %s (id: %s)",ab._values.type,ab.id)end}) -for ab,bb in pairs(ba)do if self._values[ab]==nil then -if type(bb.default)=="table"then -self._values[ab]=d(bb.default)else self._values[ab]=bb.default end end end;return self end -function aa:_updateProperty(ba,ca)local da=self._values[ba] -if da~=ca then self._values[ba]=ca;if -self._properties[ba].canTriggerRender then self:updateRender()end;if -self._observers[ba]then -for _b,ab in ipairs(self._observers[ba])do ab(self,ca,da)end end end end -function aa:observe(ba,ca) -self._observers[ba]=self._observers[ba]or{}table.insert(self._observers[ba],ca)return self end;return aa end return project["main.lua"] \ No newline at end of file