diff --git a/Basalt/objects/Program.lua b/Basalt/objects/Program.lua index 28bfc1e..76f4594 100644 --- a/Basalt/objects/Program.lua +++ b/Basalt/objects/Program.lua @@ -1,84 +1,808 @@ -local processes = {} -local process = {} -local processId = 0 +local Object = require("Object") +local tHex = require("tHex") +local process = require("process") +local xmlValue = require("utils").getValueFromXML -local newPackage = dofile("rom/modules/main/cc/require.lua").make +local sub = string.sub -function process:new(path, window, newEnv, ...) - local args = {...} - local newP = setmetatable({ path = path }, { __index = self }) - newP.window = window - window.current = term.current - window.redirect = term.redirect - newP.processId = processId - if(type(path)=="string")then - newP.coroutine = coroutine.create(function() - local pPath = shell.resolveProgram(path) - local env = setmetatable(newEnv, {__index=_ENV}) - env.shell = shell - env.basaltProgram=true - env.require, env.package = newPackage(env, fs.getDir(pPath)) - if(fs.exists(pPath))then - local file = fs.open(pPath, "r") - local content = file.readAll() - file.close() - local program = load(content, path, "bt", env) - if(program~=nil)then - return program(table.unpack(args)) +return function(name, parent) + local base = Object(name) + local objectType = "Program" + base:setZIndex(5) + local object + local cachedPath + local enviroment = {} + + local function createBasaltWindow(x, y, width, height, self) + local xCursor, yCursor = 1, 1 + local bgColor, fgColor = colors.black, colors.white + local cursorBlink = false + local visible = false + + local cacheT = {} + local cacheBG = {} + local cacheFG = {} + + local tPalette = {} + + local emptySpaceLine + local emptyColorLines = {} + + for i = 0, 15 do + local c = 2 ^ i + tPalette[c] = { parent:getBasaltInstance().getBaseTerm().getPaletteColour(c) } + end + + local function createEmptyLines() + emptySpaceLine = (" "):rep(width) + for n = 0, 15 do + local nColor = 2 ^ n + local sHex = tHex[nColor] + emptyColorLines[nColor] = sHex:rep(width) end end - end) - elseif(type(path)=="function")then - newP.coroutine = coroutine.create(function() - path(table.unpack(args)) - end) - else - return - end - processes[processId] = newP - processId = processId + 1 - return newP -end -function process:resume(event, ...) - local cur = term.current() - term.redirect(self.window) - if(self.filter~=nil)then - if(event~=self.filter)then return end - self.filter=nil - end - local ok, result = coroutine.resume(self.coroutine, event, ...) - - if ok then - self.filter = result - else - printError(result) - end - term.redirect(cur) - return ok, result -end - -function process:isDead() - if (self.coroutine ~= nil) then - if (coroutine.status(self.coroutine) == "dead") then - table.remove(processes, self.processId) - return true + local function recreateWindowArray() + createEmptyLines() + local emptyText = emptySpaceLine + local emptyFG = emptyColorLines[colors.white] + local emptyBG = emptyColorLines[colors.black] + for n = 1, height do + cacheT[n] = sub(cacheT[n] == nil and emptyText or cacheT[n] .. emptyText:sub(1, width - cacheT[n]:len()), 1, width) + cacheFG[n] = sub(cacheFG[n] == nil and emptyFG or cacheFG[n] .. emptyFG:sub(1, width - cacheFG[n]:len()), 1, width) + cacheBG[n] = sub(cacheBG[n] == nil and emptyBG or cacheBG[n] .. emptyBG:sub(1, width - cacheBG[n]:len()), 1, width) + end + base.updateDraw(base) end - else - return true + recreateWindowArray() + + local function updateCursor() + if xCursor >= 1 and yCursor >= 1 and xCursor <= width and yCursor <= height then + --parentTerminal.setCursorPos(xCursor + x - 1, yCursor + y - 1) + else + --parentTerminal.setCursorPos(0, 0) + end + --parentTerminal.setTextColor(fgColor) + end + + local function internalBlit(sText, sTextColor, sBackgroundColor) + -- copy pasti strikes again (cc: window.lua) + local nStart = xCursor + local nEnd = nStart + #sText - 1 + if yCursor >= 1 and yCursor <= height then + if nStart <= width and nEnd >= 1 then + -- Modify line + if nStart == 1 and nEnd == width then + cacheT[yCursor] = sText + cacheFG[yCursor] = sTextColor + cacheBG[yCursor] = sBackgroundColor + else + local sClippedText, sClippedTextColor, sClippedBackgroundColor + if nStart < 1 then + local nClipStart = 1 - nStart + 1 + local nClipEnd = width - nStart + 1 + sClippedText = sub(sText, nClipStart, nClipEnd) + sClippedTextColor = sub(sTextColor, nClipStart, nClipEnd) + sClippedBackgroundColor = sub(sBackgroundColor, nClipStart, nClipEnd) + elseif nEnd > width then + local nClipEnd = width - nStart + 1 + sClippedText = sub(sText, 1, nClipEnd) + sClippedTextColor = sub(sTextColor, 1, nClipEnd) + sClippedBackgroundColor = sub(sBackgroundColor, 1, nClipEnd) + else + sClippedText = sText + sClippedTextColor = sTextColor + sClippedBackgroundColor = sBackgroundColor + end + + local sOldText = cacheT[yCursor] + local sOldTextColor = cacheFG[yCursor] + local sOldBackgroundColor = cacheBG[yCursor] + local sNewText, sNewTextColor, sNewBackgroundColor + if nStart > 1 then + local nOldEnd = nStart - 1 + sNewText = sub(sOldText, 1, nOldEnd) .. sClippedText + sNewTextColor = sub(sOldTextColor, 1, nOldEnd) .. sClippedTextColor + sNewBackgroundColor = sub(sOldBackgroundColor, 1, nOldEnd) .. sClippedBackgroundColor + else + sNewText = sClippedText + sNewTextColor = sClippedTextColor + sNewBackgroundColor = sClippedBackgroundColor + end + if nEnd < width then + local nOldStart = nEnd + 1 + sNewText = sNewText .. sub(sOldText, nOldStart, width) + sNewTextColor = sNewTextColor .. sub(sOldTextColor, nOldStart, width) + sNewBackgroundColor = sNewBackgroundColor .. sub(sOldBackgroundColor, nOldStart, width) + end + + cacheT[yCursor] = sNewText + cacheFG[yCursor] = sNewTextColor + cacheBG[yCursor] = sNewBackgroundColor + end + object:updateDraw() + end + xCursor = nEnd + 1 + if (visible) then + updateCursor() + end + end + end + + local function setText(_x, _y, text) + if (text ~= nil) then + local gText = cacheT[_y] + if (gText ~= nil) then + cacheT[_y] = sub(gText:sub(1, _x - 1) .. text .. gText:sub(_x + (text:len()), width), 1, width) + end + end + object:updateDraw() + end + + local function setBG(_x, _y, colorStr) + if (colorStr ~= nil) then + local gBG = cacheBG[_y] + if (gBG ~= nil) then + cacheBG[_y] = sub(gBG:sub(1, _x - 1) .. colorStr .. gBG:sub(_x + (colorStr:len()), width), 1, width) + end + end + object:updateDraw() + end + + local function setFG(_x, _y, colorStr) + if (colorStr ~= nil) then + local gFG = cacheFG[_y] + if (gFG ~= nil) then + cacheFG[_y] = sub(gFG:sub(1, _x - 1) .. colorStr .. gFG:sub(_x + (colorStr:len()), width), 1, width) + end + end + object:updateDraw() + end + + local setTextColor = function(color) + if type(color) ~= "number" then + error("bad argument #1 (expected number, got " .. type(color) .. ")", 2) + elseif tHex[color] == nil then + error("Invalid color (got " .. color .. ")", 2) + end + fgColor = color + end + + local setBackgroundColor = function(color) + if type(color) ~= "number" then + error("bad argument #1 (expected number, got " .. type(color) .. ")", 2) + elseif tHex[color] == nil then + error("Invalid color (got " .. color .. ")", 2) + end + bgColor = color + end + + local setPaletteColor = function(colour, r, g, b) + -- have to work on + if type(colour) ~= "number" then + error("bad argument #1 (expected number, got " .. type(colour) .. ")", 2) + end + + if tHex[colour] == nil then + error("Invalid color (got " .. colour .. ")", 2) + end + + local tCol + if type(r) == "number" and g == nil and b == nil then + tCol = { colours.rgb8(r) } + tPalette[colour] = tCol + else + if type(r) ~= "number" then + error("bad argument #2 (expected number, got " .. type(r) .. ")", 2) + end + if type(g) ~= "number" then + error("bad argument #3 (expected number, got " .. type(g) .. ")", 2) + end + if type(b) ~= "number" then + error("bad argument #4 (expected number, got " .. type(b) .. ")", 2) + end + + tCol = tPalette[colour] + tCol[1] = r + tCol[2] = g + tCol[3] = b + end + end + + local getPaletteColor = function(colour) + if type(colour) ~= "number" then + error("bad argument #1 (expected number, got " .. type(colour) .. ")", 2) + end + if tHex[colour] == nil then + error("Invalid color (got " .. colour .. ")", 2) + end + local tCol = tPalette[colour] + return tCol[1], tCol[2], tCol[3] + end + + local basaltwindow = { + setCursorPos = function(_x, _y) + if type(_x) ~= "number" then + error("bad argument #1 (expected number, got " .. type(_x) .. ")", 2) + end + if type(_y) ~= "number" then + error("bad argument #2 (expected number, got " .. type(_y) .. ")", 2) + end + xCursor = math.floor(_x) + yCursor = math.floor(_y) + if (visible) then + updateCursor() + end + end; + + getCursorPos = function() + return xCursor, yCursor + end; + + setCursorBlink = function(blink) + if type(blink) ~= "boolean" then + error("bad argument #1 (expected boolean, got " .. type(blink) .. ")", 2) + end + cursorBlink = blink + end; + + getCursorBlink = function() + return cursorBlink + end; + + + getPaletteColor = getPaletteColor, + getPaletteColour = getPaletteColor, + + setBackgroundColor = setBackgroundColor, + setBackgroundColour = setBackgroundColor, + + setTextColor = setTextColor, + setTextColour = setTextColor, + + setPaletteColor = setPaletteColor, + setPaletteColour = setPaletteColor, + + getBackgroundColor = function() + return bgColor + end; + getBackgroundColour = function() + return bgColor + end; + + getSize = function() + return width, height + end; + + getTextColor = function() + return fgColor + end; + getTextColour = function() + return fgColor + end; + + basalt_resize = function(_width, _height) + width, height = _width, _height + recreateWindowArray() + end; + + basalt_reposition = function(_x, _y) + x, y = _x, _y + end; + + basalt_setVisible = function(vis) + visible = vis + end; + + drawBackgroundBox = function(_x, _y, _width, _height, bgCol) + for n = 1, _height do + setBG(_x, _y + (n - 1), tHex[bgCol]:rep(_width)) + end + end; + drawForegroundBox = function(_x, _y, _width, _height, fgCol) + for n = 1, _height do + setFG(_x, _y + (n - 1), tHex[fgCol]:rep(_width)) + end + end; + drawTextBox = function(_x, _y, _width, _height, symbol) + for n = 1, _height do + setText(_x, _y + (n - 1), symbol:rep(_width)) + end + end; + + writeText = function(_x, _y, text, bgCol, fgCol) + bgCol = bgCol or bgColor + fgCol = fgCol or fgColor + setText(x, _y, text) + setBG(_x, _y, tHex[bgCol]:rep(text:len())) + setFG(_x, _y, tHex[fgCol]:rep(text:len())) + end; + + basalt_update = function() + if (parent ~= nil) then + for n = 1, height do + parent:setText(x, y + (n - 1), cacheT[n]) + parent:setBG(x, y + (n - 1), cacheBG[n]) + parent:setFG(x, y + (n - 1), cacheFG[n]) + end + end + end; + + scroll = function(offset) + if type(offset) ~= "number" then + error("bad argument #1 (expected number, got " .. type(offset) .. ")", 2) + end + if offset ~= 0 then + local sEmptyText = emptySpaceLine + local sEmptyTextColor = emptyColorLines[fgColor] + local sEmptyBackgroundColor = emptyColorLines[bgColor] + for newY = 1, height do + local y = newY + offset + if y >= 1 and y <= height then + cacheT[newY] = cacheT[y] + cacheBG[newY] = cacheBG[y] + cacheFG[newY] = cacheFG[y] + else + cacheT[newY] = sEmptyText + cacheFG[newY] = sEmptyTextColor + cacheBG[newY] = sEmptyBackgroundColor + end + end + end + if (visible) then + updateCursor() + end + end; + + + isColor = function() + return parent:getBasaltInstance().getBaseTerm().isColor() + end; + + isColour = function() + return parent:getBasaltInstance().getBaseTerm().isColor() + end; + + write = function(text) + text = tostring(text) + if (visible) then + internalBlit(text, tHex[fgColor]:rep(text:len()), tHex[bgColor]:rep(text:len())) + end + end; + + clearLine = function() + if (visible) then + setText(1, yCursor, (" "):rep(width)) + setBG(1, yCursor, tHex[bgColor]:rep(width)) + setFG(1, yCursor, tHex[fgColor]:rep(width)) + end + if (visible) then + updateCursor() + end + end; + + clear = function() + for n = 1, height do + setText(1, n, (" "):rep(width)) + setBG(1, n, tHex[bgColor]:rep(width)) + setFG(1, n, tHex[fgColor]:rep(width)) + end + if (visible) then + updateCursor() + end + end; + + blit = function(text, fgcol, bgcol) + if type(text) ~= "string" then + error("bad argument #1 (expected string, got " .. type(text) .. ")", 2) + end + if type(fgcol) ~= "string" then + error("bad argument #2 (expected string, got " .. type(fgcol) .. ")", 2) + end + if type(bgcol) ~= "string" then + error("bad argument #3 (expected string, got " .. type(bgcol) .. ")", 2) + end + if #fgcol ~= #text or #bgcol ~= #text then + error("Arguments must be the same length", 2) + end + if (visible) then + internalBlit(text, fgcol, bgcol) + end + end + + + } + + return basaltwindow end - return false -end -function process:getStatus() - if (self.coroutine ~= nil) then - return coroutine.status(self.coroutine) + base.width = 30 + base.height = 12 + local pWindow = createBasaltWindow(1, 1, base.width, base.height) + local curProcess + local paused = false + local queuedEvent = {} + + local function updateCursor(self) + local xCur, yCur = pWindow.getCursorPos() + local obx, oby = self:getAnchorPosition() + local w,h = self:getSize() + if (obx + xCur - 1 >= 1 and obx + xCur - 1 <= obx + w - 1 and yCur + oby - 1 >= 1 and yCur + oby - 1 <= oby + h - 1) then + self.parent:setCursor(self:isFocused() and pWindow.getCursorBlink(), obx + xCur - 1, yCur + oby - 1, pWindow.getTextColor()) + end end - return nil -end -function process:start() - coroutine.resume(self.coroutine) -end + local function resumeProcess(self, event, ...) + local ok, result = curProcess:resume(event, ...) + if (ok==false)and(result~=nil)and(result~="Terminated")then + local val = self:sendEvent("program_error", result) + if(val~=false)then + error("Basalt Program - "..result) + end + end + + if(curProcess:getStatus()=="dead")then + self:sendEvent("program_done") + end + end -return process + local function mouseEvent(self, event, p1, x, y) + if (curProcess == nil) then + return false + end + if not (curProcess:isDead()) then + if not (paused) then + local absX, absY = self:getAbsolutePosition(self:getAnchorPosition(nil, nil, true)) + resumeProcess(self, event, p1, x-absX+1, y-absY+1) + updateCursor(self) + end + end + end + + local function keyEvent(self, event, key, isHolding) + if (curProcess == nil) then + return false + end + if not (curProcess:isDead()) then + if not (paused) then + if (self.draw) then + resumeProcess(self, event, key, isHolding) + updateCursor(self) + end + end + end + end + + object = { + getType = function(self) + return objectType + end; + + show = function(self) + base.show(self) + pWindow.setBackgroundColor(self.bgColor) + pWindow.setTextColor(self.fgColor) + pWindow.basalt_setVisible(true) + return self + end; + + hide = function(self) + base.hide(self) + pWindow.basalt_setVisible(false) + return self + end; + + setPosition = function(self, x, y, rel) + base.setPosition(self, x, y, rel) + pWindow.basalt_reposition(self:getAnchorPosition()) + return self + end; + + setValuesByXMLData = function(self, data) + base.setValuesByXMLData(self, data) + if(xmlValue("path", data)~=nil)then cachedPath = xmlValue("path", data) end + if(xmlValue("execute", data)~=nil)then if(xmlValue("execute", data))then if(cachedPath~=nil)then self:execute(cachedPath) end end end + end, + + getBasaltWindow = function() + return pWindow + end; + + getBasaltProcess = function() + return curProcess + end; + + setSize = function(self, width, height, rel) + base.setSize(self, width, height, rel) + pWindow.basalt_resize(self:getWidth(), self:getHeight()) + return self + end; + + getStatus = function(self) + if (curProcess ~= nil) then + return curProcess:getStatus() + end + return "inactive" + end; + + setEnviroment = function(self, env) + enviroment = env or {} + return self + end, + + execute = function(self, path, ...) + cachedPath = path or cachedPath + curProcess = process:new(cachedPath, pWindow, enviroment, ...) + pWindow.setBackgroundColor(colors.black) + pWindow.setTextColor(colors.white) + pWindow.clear() + pWindow.setCursorPos(1, 1) + pWindow.setBackgroundColor(self.bgColor) + pWindow.setTextColor(self.fgColor) + pWindow.basalt_setVisible(true) + + resumeProcess(self) + paused = false + if(self.parent~=nil)then + self.parent:addEvent("mouse_click", self) + self.parent:addEvent("mouse_up", self) + self.parent:addEvent("mouse_drag", self) + self.parent:addEvent("mouse_scroll", self) + self.parent:addEvent("key", self) + self.parent:addEvent("key_up", self) + self.parent:addEvent("char", self) + self.parent:addEvent("other_event", self) + end + return self + end; + + stop = function(self) + if (curProcess ~= nil) then + if not (curProcess:isDead()) then + resumeProcess(self, "terminate") + if (curProcess:isDead()) then + if (self.parent ~= nil) then + self.parent:setCursor(false) + end + end + end + end + self.parent:removeEvents(self) + return self + end; + + pause = function(self, p) + paused = p or (not paused) + if (curProcess ~= nil) then + if not (curProcess:isDead()) then + if not (paused) then + self:injectEvents(queuedEvent) + queuedEvent = {} + end + end + end + return self + end; + + isPaused = function(self) + return paused + end; + + injectEvent = function(self, event, p1, p2, p3, p4, ign) + if (curProcess ~= nil) then + if not (curProcess:isDead()) then + if (paused == false) or (ign) then + resumeProcess(self, event, p1, p2, p3, p4) + else + table.insert(queuedEvent, { event = event, args = { p1, p2, p3, p4 } }) + end + end + end + return self + end; + + getQueuedEvents = function(self) + return queuedEvent + end; + + updateQueuedEvents = function(self, events) + queuedEvent = events or queuedEvent + return self + end; + + injectEvents = function(self, events) + if (curProcess ~= nil) then + if not (curProcess:isDead()) then + for _, value in pairs(events) do + resumeProcess(self, value.event, table.unpack(value.args)) + end + end + end + return self + end; + + mouseHandler = function(self, button, x, y) + if (base.mouseHandler(self, button, x, y)) then + mouseEvent(self, "mouse_click", button, x, y) + return true + end + return false + end, + + mouseUpHandler = function(self, button, x, y) + if (base.mouseUpHandler(self, button, x, y)) then + mouseEvent(self, "mouse_up", button, x, y) + return true + end + return false + end, + + scrollHandler = function(self, dir, x, y) + if (base.scrollHandler(self, dir, x, y)) then + mouseEvent(self, "mouse_scroll", dir, x, y) + return true + end + return false + end, + + dragHandler = function(self, button, x, y) + if (base.dragHandler(self, button, x, y)) then + mouseEvent(self, "mouse_drag", button, x, y) + return true + end + return false + end, + + keyHandler = function(self, key, isHolding) + if(base.keyHandler(self, key, isHolding))then + keyEvent(self, "key", key, isHolding) + return true + end + return false + end, + + keyUpHandler = function(self, key) + if(base.keyUpHandler(self, key))then + keyEvent(self, "key_up", key) + return true + end + return false + end, + + charHandler = function(self, char) + if(base.charHandler(self, char))then + keyEvent(self, "char", char) + return true + end + return false + end, + + getFocusHandler = function(self) + base.getFocusHandler(self) + if (curProcess ~= nil) then + if not (curProcess:isDead()) then + if not (paused) then + if (self.parent ~= nil) then + local xCur, yCur = pWindow.getCursorPos() + local obx, oby = self:getAnchorPosition() + local w,h = self:getSize() + if (obx + xCur - 1 >= 1 and obx + xCur - 1 <= obx + w - 1 and yCur + oby - 1 >= 1 and yCur + oby - 1 <= oby + h - 1) then + self.parent:setCursor(pWindow.getCursorBlink(), obx + xCur - 1, yCur + oby - 1, pWindow.getTextColor()) + end + end + end + end + end + end, + + loseFocusHandler = function(self) + base.loseFocusHandler(self) + if (curProcess ~= nil) then + if not (curProcess:isDead()) then + if (self.parent ~= nil) then + self.parent:setCursor(false) + end + end + end + end, + + customEventHandler = function(self, event, ...) + base.customEventHandler(self, event, ...) + if (curProcess == nil) then + return + end + if(event=="basalt_resize")then + local w, h = pWindow.getSize() + local pW, pH = self:getSize() + if(w~=pW)or(h~=pH)then + pWindow.basalt_resize(pW, pH) + if not (curProcess:isDead()) then + resumeProcess(self, "term_resize") + end + end + pWindow.basalt_reposition(self:getAnchorPosition()) + + end + end, + + eventHandler = function(self, event, p1, p2, p3, p4) + base.eventHandler(self, event, p1, p2, p3, p4) + if (curProcess == nil) then + return + end + if not (curProcess:isDead()) then + if not (paused) then + if(event ~= "terminate") then + resumeProcess(self, event, p1, p2, p3, p4) + end + if (self:isFocused()) then + local obx, oby = self:getAnchorPosition() + local xCur, yCur = pWindow.getCursorPos() + if (self.parent ~= nil) then + local w,h = self:getSize() + if (obx + xCur - 1 >= 1 and obx + xCur - 1 <= obx + w - 1 and yCur + oby - 1 >= 1 and yCur + oby - 1 <= oby + h - 1) then + self.parent:setCursor(pWindow.getCursorBlink(), obx + xCur - 1, yCur + oby - 1, pWindow.getTextColor()) + end + end + + if (event == "terminate") then + resumeProcess(self, event) + self.parent:setCursor(false) + return true + end + end + else + table.insert(queuedEvent, { event = event, args = { p1, p2, p3, p4 } }) + end + end + end, + + draw = function(self) + if (base.draw(self)) then + if (self.parent ~= nil) then + local obx, oby = self:getAnchorPosition() + local xCur, yCur = pWindow.getCursorPos() + local w,h = self:getSize() + pWindow.basalt_reposition(obx, oby) + pWindow.basalt_update() + if (obx + xCur - 1 >= 1 and obx + xCur - 1 <= obx + w - 1 and yCur + oby - 1 >= 1 and yCur + oby - 1 <= oby + h - 1) then + self.parent:setCursor(self:isFocused() and pWindow.getCursorBlink(), obx + xCur - 1, yCur + oby - 1, pWindow.getTextColor()) + end + end + end + end, + + onError = function(self, ...) + for _,v in pairs(table.pack(...))do + if(type(v)=="function")then + self:registerEvent("program_error", v) + end + end + if(self.parent~=nil)then + self.parent:addEvent("other_event", self) + end + return self + end, + + onDone = function(self, ...) + for _,v in pairs(table.pack(...))do + if(type(v)=="function")then + self:registerEvent("program_done", v) + end + end + if(self.parent~=nil)then + self.parent:addEvent("other_event", self) + end + return self + end, + + init = function(self) + if(base.init(self))then + self.bgColor = self.parent:getTheme("ProgramBG") + end + end, + + } + + return setmetatable(object, base) +end