local files = {} files["dlib/options.lua"] = "local M = {}\ \ local CONFIG_FILE = \"dracore.conf\"\ local OLD_CONDUITS_FILE = \"conduits.cfg\"\ local OLD_SETTINGS_FILE = \"settings.cfg\"\ \ local function readSerialisedFile(path)\ if not fs.exists(path) then return nil end\ local f = fs.open(path, \"r\")\ local data = f.readAll()\ f.close()\ local ok, result = pcall(textutils.unserialise, data)\ if ok and type(result) == \"table\" then return result end\ return nil\ end\ \ local function loadConfig()\ local combined = readSerialisedFile(CONFIG_FILE)\ if combined then return combined end\ return {\ conduits = readSerialisedFile(OLD_CONDUITS_FILE) or {},\ settings = readSerialisedFile(OLD_SETTINGS_FILE) or {},\ }\ end\ \ local function saveConfig(config)\ local f = fs.open(CONFIG_FILE, \"w\")\ f.write(textutils.serialise(config))\ f.close()\ end\ \ local function sanitizeColor(value, default)\ if type(value) == \"number\" then return value end\ return default\ end\ \ local function sanitizeChoice(value, allowed, default)\ for _, v in ipairs(allowed) do\ if value == v then return value end\ end\ return default\ end\ \ local function sanitizeNumber(value, default, min, max)\ local n = tonumber(value)\ if not n then n = default end\ if min then n = math.max(min, n) end\ if max then n = math.min(max, n) end\ return n\ end\ \ local function sanitizeConduit(c)\ return {\ name = type(c.name) == \"string\" and c.name or \"Conduit\",\ color = sanitizeColor(c.color, colors.white),\ meterName = type(c.meterName) == \"string\" and c.meterName or nil,\ flowGateName = type(c.flowGateName) == \"string\" and c.flowGateName or nil,\ direction = (c.direction == \"out\") and \"out\" or \"in\",\ }\ end\ \ local function buildConduits(configData)\ local list = {}\ for _, c in ipairs(configData.conduits or {}) do\ table.insert(list, sanitizeConduit(c))\ end\ return list\ end\ \ local DEFAULT_THEME = {\ topBar = { bar = colors.gray, text = colors.cyan },\ window = { frame = colors.lightGray, buttonFill = colors.green, buttonText = colors.black },\ storage = { outline = nil, fill = nil, shape = nil },\ main = { bg = colors.black },\ }\ \ local function buildTheme(saved)\ return {\ topBar = {\ bar = sanitizeColor(saved.topBar and saved.topBar.bar, DEFAULT_THEME.topBar.bar),\ text = sanitizeColor(saved.topBar and saved.topBar.text, DEFAULT_THEME.topBar.text),\ },\ window = {\ frame = sanitizeColor(saved.window and saved.window.frame, DEFAULT_THEME.window.frame),\ buttonFill = sanitizeColor(saved.window and saved.window.buttonFill, DEFAULT_THEME.window.buttonFill),\ buttonText = sanitizeColor(saved.window and saved.window.buttonText, DEFAULT_THEME.window.buttonText),\ },\ storage = {\ outline = saved.storage and sanitizeColor(saved.storage.outline, nil),\ fill = saved.storage and sanitizeColor(saved.storage.fill, nil),\ shape = saved.storage and sanitizeChoice(saved.storage.shape, { \"circle\", \"square\", \"battery\" }, nil),\ },\ main = {\ bg = sanitizeColor(saved.main and saved.main.bg, DEFAULT_THEME.main.bg),\ },\ }\ end\ \ local function slotCountIndex(slotCountSteps, count)\ local best, bestDiff = 1, math.huge\ for i, v in ipairs(slotCountSteps) do\ if v == count then return i end\ local diff = math.abs(v - count)\ if diff < bestDiff then best, bestDiff = i, diff end\ end\ return best\ end\ \ local function buildConduitDisplay(saved, slotCountSteps, maxSlotsPerSide)\ return {\ activeCount = slotCountSteps[slotCountIndex(slotCountSteps, sanitizeNumber(saved.activeCount, 5, 0, maxSlotsPerSide))],\ spacing = sanitizeNumber(saved.spacing, 1, 1, 4),\ i = type(saved.i) == \"table\" and saved.i or {},\ o = type(saved.o) == \"table\" and saved.o or {},\ }\ end\ \ function M.loadInitialState(slotCountSteps, maxSlotsPerSide)\ local configData = loadConfig()\ local savedSettings = configData.settings or {}\ return {\ configData = configData,\ textScale = sanitizeNumber(savedSettings.textScale, 0.5, 0.5, 5),\ refreshInterval = sanitizeNumber(savedSettings.refreshInterval, 1, 0.5, 5),\ theme = buildTheme(savedSettings.theme or {}),\ conduits = buildConduits(configData),\ conduitDisplay = buildConduitDisplay(savedSettings.conduitDisplay or {}, slotCountSteps, maxSlotsPerSide),\ }\ end\ \ function M.saveConduits(configData, conduits)\ configData.conduits = conduits\ saveConfig(configData)\ end\ \ function M.saveSettings(configData, settings)\ configData.settings = settings\ saveConfig(configData)\ end\ \ function M.attach(App)\ \ local state = App.state\ local Render = App.Render\ local energy = App.energy\ local screens = App.screens\ local addButton = App.addButton\ local CONTENT_BG = App.CONTENT_BG\ \ local cancelTextEntry = App.cancelTextEntry\ local startTextEntry = App.startTextEntry\ local closeOptions = App.closeOptions\ \ local saveConduitsNow = App.saveConduitsNow\ local saveSettingsNow = App.saveSettingsNow\ local findConduitByMeter = App.findConduitByMeter\ local findConduitByFlowGate = App.findConduitByFlowGate\ local withoutClaimed = App.withoutClaimed\ local clearConduitFromDisplay = App.clearConduitFromDisplay\ \ local formatEnergy = App.formatEnergy\ local detectStyle = App.detectStyle\ local setupBuffers = App.setupBuffers\ \ local drawTallButton = Render.drawTallButton\ local drawButton = Render.drawButton\ local drawThinRect = Render.drawThinRect\ local drawThinTopEdge = Render.drawThinTopEdge\ local drawThinBottomEdge = Render.drawThinBottomEdge\ \ local function drawListButton(ctx, win, geom, y, fillColor, drawContent, action)\ local x1, x2 = 2, geom.ow - 1\ win.setCursorPos(x1, y)\ win.setBackgroundColor(fillColor)\ win.write(string.rep(\" \", math.max(0, x2 - x1 + 1)))\ if drawContent then drawContent(x1, y) end\ win.setBackgroundColor(CONTENT_BG)\ if action then\ addButton(ctx, geom.ox + x1 - 1, geom.oy + y - 1, geom.ox + x2 - 1, geom.oy + y - 1, action)\ end\ end\ \ local function drawStepper(ctx, win, geom, row, label, get, set, step, lo, hi, fmt)\ local valueText = fmt(get())\ local line = (\"[-] %s: %s [+]\"):format(label, valueText)\ local col = math.max(1, math.floor((geom.ow - #line) / 2) + 1)\ win.setCursorPos(col, row)\ win.setTextColor(colors.white)\ win.write(line)\ \ local minusX = col + 1\ local plusX = col + #line - 2\ \ addButton(ctx, geom.ox + minusX - 1, geom.oy + row - 1, geom.ox + minusX - 1, geom.oy + row - 1, function()\ set(math.max(lo, get() - step))\ end)\ addButton(ctx, geom.ox + plusX - 1, geom.oy + row - 1, geom.ox + plusX - 1, geom.oy + row - 1, function()\ set(math.min(hi, get() + step))\ end)\ end\ \ local GRID_MIN_WIDTH = 13\ local GRID_MAX_HEIGHT = 3\ \ local function drawButtonGrid(ctx, win, geom, items, startRow, endRow, pageState)\ if #items == 0 or endRow < startRow then return end\ \ local availW = geom.ow - 2\ local availH = endRow - startRow + 1\ if availW < 1 then return end\ \ local cols = math.max(1, math.floor(availW / GRID_MIN_WIDTH))\ cols = math.min(cols, #items)\ local btnW = math.floor(availW / cols)\ \ local rowsNeeded = math.ceil(#items / cols)\ local gapRows = math.max(0, rowsNeeded - 1)\ local usePagination = (rowsNeeded + gapRows) > availH\ \ local btnH, gridRows, perPage\ if usePagination then\ btnH = 1\ local rowsAvail = math.max(1, availH - 1)\ gridRows = math.max(1, math.floor((rowsAvail + 1) / 2))\ perPage = cols * gridRows\ else\ local availForButtons = math.max(rowsNeeded, availH - gapRows)\ btnH = math.max(1, math.min(GRID_MAX_HEIGHT, math.floor(availForButtons / rowsNeeded)))\ gridRows = rowsNeeded\ perPage = #items\ end\ \ local totalPages = math.max(1, math.ceil(#items / perPage))\ pageState.page = math.min(math.max(pageState.page or 1, 1), totalPages)\ local pageStart = (pageState.page - 1) * perPage + 1\ local pageEnd = math.min(#items, pageStart + perPage - 1)\ \ local x0 = 2\ local row, col = startRow, 0\ for i = pageStart, pageEnd do\ local item = items[i]\ local x = x0 + col * btnW\ drawTallButton(ctx, win, geom, x, row, btnW - 1, btnH, item.label,\ item.fill or state.theme.window.buttonFill,\ item.textColor or state.theme.window.buttonText,\ item.action)\ \ col = col + 1\ if col >= cols then\ col = 0\ row = row + btnH + 1\ end\ end\ \ if usePagination then\ local navRow = startRow + gridRows + (gridRows - 1)\ if pageState.page > 1 then\ drawButton(ctx, win, geom, x0, navRow, 9, \"< Prev\", state.theme.window.buttonFill, state.theme.window.buttonText, function()\ pageState.page = pageState.page - 1\ end)\ end\ if pageState.page < totalPages then\ local w = 9\ drawButton(ctx, win, geom, geom.ow - 1 - w, navRow, w, \"Next >\", state.theme.window.buttonFill, state.theme.window.buttonText, function()\ pageState.page = pageState.page + 1\ end)\ end\ local pageText = (\"Page %d/%d\"):format(pageState.page, totalPages)\ local pcol = math.max(1, math.floor((geom.ow - #pageText) / 2) + 1)\ win.setCursorPos(pcol, navRow)\ win.setTextColor(colors.lightGray)\ win.write(pageText)\ end\ end\ \ local function ensureOptionsWindow(ctx, monW, monH)\ local geom = ctx.optionsGeom\ if ctx.optionsWin and geom and geom.monW == monW and geom.monH == monH then\ return ctx.optionsContent, geom\ end\ \ local ow = math.min(monW, math.max(30, math.floor(monW * 0.9)))\ local oh = math.min(monH - 1, math.max(22, math.floor(monH * 0.85)))\ oh = math.max(oh, 3)\ local ox = math.floor((monW - ow) / 2) + 1\ local oy = math.max(2, math.floor((monH - oh) / 2) + 1)\ \ local parent = ctx.buffer\ ctx.optionsWin = window.create(parent, ox, oy, ow, oh, false)\ \ local iw, ih = math.max(1, ow - 2), math.max(1, oh - 2)\ ctx.optionsContent = window.create(ctx.optionsWin, 2, 2, iw, ih, false)\ \ ctx.optionsGeom = {\ monW = monW, monH = monH,\ ox = ox + 1, oy = oy + 1,\ ow = iw, oh = ih,\ outerOw = ow, outerOh = oh,\ }\ return ctx.optionsContent, ctx.optionsGeom\ end\ \ local function hideOptionsWindow(ctx)\ if ctx.optionsContent then\ ctx.optionsContent.setVisible(false)\ end\ if ctx.optionsWin then\ ctx.optionsWin.setVisible(false)\ end\ end\ \ local ALL_COLORS = {\ colors.white, colors.orange, colors.magenta, colors.lightBlue,\ colors.yellow, colors.lime, colors.pink, colors.gray,\ colors.lightGray, colors.cyan, colors.purple, colors.blue,\ colors.brown, colors.green, colors.red, colors.black,\ }\ \ local function drawColorGrid(ctx, win, geom, x, y, onPick, gridW, gridH, borderColor, flankColor, panelW)\ gridW, gridH = gridW or 2, gridH or 8\ local cellW = 3\ borderColor = borderColor or colors.gray\ \ local boxLeft, boxRight = x - 1, x + (gridW * cellW)\ local boxTop, boxBottom = y - 1, y + gridH\ \ win.setBackgroundColor(borderColor)\ for row = boxTop, boxBottom do\ win.setCursorPos(boxLeft, row)\ win.write(string.rep(\" \", boxRight - boxLeft + 1))\ end\ \ local boxW = boxRight - boxLeft + 1\ local hasFlank = flankColor and panelW\ \ if hasFlank then\ drawThinTopEdge(win, boxLeft, boxTop, boxW, colors.gray, borderColor, true, true)\ drawThinBottomEdge(win, boxLeft, boxBottom, boxW, colors.gray, borderColor, true, true)\ else\ drawThinRect(win, boxLeft, boxTop, boxW, boxBottom - boxTop + 1, colors.gray, borderColor)\ end\ \ if hasFlank then\ win.setBackgroundColor(flankColor)\ for row = y, y + gridH - 1 do\ if boxLeft - 2 >= 2 then\ win.setCursorPos(2, row)\ win.write(string.rep(\" \", boxLeft - 2))\ end\ if boxRight + 1 <= panelW + 1 then\ win.setCursorPos(boxRight + 1, row)\ win.write(string.rep(\" \", panelW + 1 - boxRight))\ end\ end\ \ -- Flank offshoots get their own top/bottom edge instead of a\ -- vertical line closing off the box, so the border reads as one continuous run.\ if boxLeft - 2 >= 2 then\ drawThinTopEdge(win, 2, y, boxLeft - 2, colors.gray, flankColor)\ drawThinBottomEdge(win, 2, y + gridH - 1, boxLeft - 2, colors.gray, flankColor)\ end\ if boxRight + 1 <= panelW + 1 then\ drawThinTopEdge(win, boxRight + 1, y, panelW + 1 - boxRight, colors.gray, flankColor)\ drawThinBottomEdge(win, boxRight + 1, y + gridH - 1, panelW + 1 - boxRight, colors.gray, flankColor)\ end\ end\ \ for i, col in ipairs(ALL_COLORS) do\ local row = math.floor((i - 1) / gridW)\ local colIdx = (i - 1) % gridW\ local px, py = x + (colIdx * cellW), y + row\ \ win.setBackgroundColor(col)\ win.setCursorPos(px, py)\ win.write(string.rep(\" \", cellW))\ \ addButton(ctx, geom.ox + px - 1, geom.oy + py - 1, geom.ox + px + cellW - 2, geom.oy + py - 1, function()\ onPick(col)\ end)\ end\ \ win.setBackgroundColor(CONTENT_BG)\ return y + gridH\ end\ \ local function adjustForButtonContrast(c)\ local bf = state.theme.window.buttonFill\ if c == colors.red and bf == colors.red then return colors.orange end\ if c == colors.blue and bf == colors.blue then return colors.lightBlue end\ return c\ end\ \ local function drawColorBar(win, x, y, width, fgColor, bgColor)\ if width < 1 then return end\ local char, fg, bg = Render.teletextChar(false, false, true, true, false, false, fgColor, bgColor)\ win.blit(string.rep(char, width), string.rep(colors.toBlit(fg), width), string.rep(colors.toBlit(bg), width))\ end\ \ local pickListPage = { page = 1 }\ \ local function startConduitWizard()\ state.wizard = { step = \"meter\" }\ state.conduitView = \"list\"\ pickListPage.page = 1\ end\ \ local function cancelWizard()\ state.wizard = nil\ end\ \ local function finishWizard()\ local w = state.wizard\ table.insert(state.conduits, {\ name = w.name,\ color = w.color,\ meterName = w.meterName,\ flowGateName = w.flowGateName,\ direction = \"in\",\ slot = nil,\ })\ saveConduitsNow()\ state.wizard = nil\ state.conduitView = \"list\"\ end\ \ local function openConduits()\ if App.textEntry then cancelTextEntry() end\ state.wizard = nil\ state.conduitView = \"list\"\ state.conduitDetailIndex = nil\ state.conduitListPage.page = 1\ pickListPage.page = 1\ state.settingsView = \"conduits\"\ end\ \ local function drawSecondaryAction(ctx, win, geom, rightLabel, onRight)\ local rightText = \"[\" .. rightLabel .. \"]\"\ local rightCol = geom.ow - #rightText - 1\ win.setCursorPos(rightCol, 3)\ win.setBackgroundColor(colors.black)\ win.setTextColor(colors.cyan)\ win.write(rightText)\ local rightEnd = rightCol + #rightText - 1\ addButton(ctx, geom.ox + rightCol - 1, geom.oy + 3 - 1, geom.ox + rightEnd - 1, geom.oy + 3 - 1, onRight)\ win.setBackgroundColor(CONTENT_BG)\ end\ \ local function drawPickList(ctx, win, geom, items, startRow, endRow, onPick, emptyText, annotate)\ if #items == 0 then\ local col = math.max(1, math.floor((geom.ow - #emptyText) / 2) + 1)\ win.setCursorPos(col, startRow)\ win.setTextColor(colors.lightGray)\ win.write(emptyText)\ return\ end\ \ local availH = endRow - startRow + 1\ local perPageFit = math.max(1, math.floor((availH + 1) / 2))\ local usePagination = #items > perPageFit\ \ local perPage = perPageFit\ if usePagination then\ local navAvailH = availH - 1\ perPage = math.max(1, math.floor((navAvailH + 1) / 2))\ end\ \ local totalPages = math.max(1, math.ceil(#items / perPage))\ pickListPage.page = math.min(math.max(pickListPage.page or 1, 1), totalPages)\ local pageStart = (pickListPage.page - 1) * perPage + 1\ local pageEnd = math.min(#items, pageStart + perPage - 1)\ \ local row = startRow\ for i = pageStart, pageEnd do\ local name = items[i]\ local suffix = annotate and annotate(name)\ drawListButton(ctx, win, geom, row, state.theme.window.buttonFill, function(x1, y)\ win.setCursorPos(x1 + 1, y)\ win.setTextColor(state.theme.window.buttonText)\ local prefix = string.char(16) .. \" \" .. name\ win.write(prefix)\ if suffix then\ win.setTextColor(colors.lightGray)\ win.write(\" \" .. suffix)\ end\ end, function()\ onPick(name)\ end)\ \ row = row + 2\ end\ \ if usePagination then\ local navRow = endRow\ if pickListPage.page > 1 then\ drawButton(ctx, win, geom, 2, navRow, 9, \"< Prev\", state.theme.window.buttonFill, state.theme.window.buttonText, function()\ pickListPage.page = pickListPage.page - 1\ end)\ end\ if pickListPage.page < totalPages then\ local w = 9\ drawButton(ctx, win, geom, geom.ow - 1 - w, navRow, w, \"Next >\", state.theme.window.buttonFill, state.theme.window.buttonText, function()\ pickListPage.page = pickListPage.page + 1\ end)\ end\ local pageText = (\"Page %d/%d\"):format(pickListPage.page, totalPages)\ local pcol = math.max(1, math.floor((geom.ow - #pageText) / 2) + 1)\ win.setCursorPos(pcol, navRow)\ win.setTextColor(colors.lightGray)\ win.write(pageText)\ end\ end\ \ local function gateIndicatorColor(c, rowBg)\ if not c.flowGateName then return rowBg end\ local status = energy.getFlowGateStatus(c.flowGateName)\ if not status or not status.enabled then\ return rowBg\ end\ if status.limit == 0 then\ return colors.red\ end\ return colors.blue\ end\ \ local function drawConduitRow(ctx, win, geom, row, i, c)\ c.direction = c.direction or \"in\"\ local rate = energy.getMeterRate(c.meterName)\ local rateText = rate and (formatEnergy(rate) .. \"/t\") or \"offline\"\ local rowBg = (c.color == colors.gray) and colors.lightGray or colors.gray\ \ drawListButton(ctx, win, geom, row, rowBg, function(x1, y)\ local isOut = c.direction == \"out\"\ local dirColor = isOut and colors.red or colors.blue\ local dirLabel = isOut and \"OUT\" or \"IN \"\ \ win.setCursorPos(x1 + 1, y)\ win.setTextColor(dirColor)\ win.write(string.char(7) .. dirLabel)\ \ local nameX = x1 + 1 + 4 + 1\ local gateColor = gateIndicatorColor(c, rowBg)\ local rateCol = (geom.ow - 1) - #rateText\ \ local name = c.name\ local nameWidth = math.max(1, rateCol - 2 - nameX)\ if #name > nameWidth then\ name = name:sub(1, math.max(1, nameWidth - 1)) .. \".\"\ end\ win.setCursorPos(nameX, y)\ win.setTextColor(c.color)\ win.write(name)\ \ local barX = nameX + #name\ local barWidth = math.max(0, rateCol - barX)\ drawColorBar(win, barX, y, barWidth, c.color, rowBg)\ \ local rateColStart = rateCol - 2\ win.setCursorPos(rateColStart, y)\ win.setTextColor(gateColor)\ win.write(string.char(7) .. \" \")\ \ win.setCursorPos(rateCol, y)\ win.setTextColor(dirColor)\ win.write(rateText)\ end, function()\ state.conduitDetailIndex = i\ state.conduitView = \"detail\"\ end)\ end\ \ local CONDUIT_SORT_COLUMNS = {\ { key = \"direction\", label = \"In/Out\", x = 2, w = 9 },\ { key = \"name\", label = \"Name\", x = 12, w = 12 },\ { key = \"rate\", label = \"Rate\", x = nil, w = 9 },\ }\ \ local function sortConduits()\ local sort = state.conduitSort\ if not sort.key then return end\ table.sort(state.conduits, function(a, b)\ local av, bv\ if sort.key == \"direction\" then\ av, bv = (a.direction or \"in\"), (b.direction or \"in\")\ elseif sort.key == \"rate\" then\ av = energy.getMeterRate(a.meterName) or -math.huge\ bv = energy.getMeterRate(b.meterName) or -math.huge\ else\ av, bv = a.name:lower(), b.name:lower()\ end\ if av == bv then return false end\ if sort.asc then return av < bv else return av > bv end\ end)\ end\ \ local function drawConduitHeader(ctx, win, geom, row)\ for _, colDef in ipairs(CONDUIT_SORT_COLUMNS) do\ local x = colDef.x or (geom.ow - colDef.w)\ local active = state.conduitSort.key == colDef.key\ local arrow = active and (state.conduitSort.asc and \" v\" or \" ^\") or \"\"\ drawButton(ctx, win, geom, x, row, colDef.w, colDef.label .. arrow,\ active and colors.cyan or colors.black,\ active and colors.black or colors.lightGray,\ function()\ if state.conduitSort.key == colDef.key then\ state.conduitSort.asc = not state.conduitSort.asc\ else\ state.conduitSort.key = colDef.key\ state.conduitSort.asc = true\ end\ sortConduits()\ end)\ end\ end\ \ local function conduitPaging(availRows)\ local rowsPerItem = 2\ local perPage = math.max(1, math.floor((availRows + 1) / rowsPerItem))\ local totalPages = math.max(1, math.ceil(#state.conduits / perPage))\ local page = math.min(math.max(state.conduitListPage.page or 1, 1), totalPages)\ local pageStart = (page - 1) * perPage + 1\ local pageEnd = math.min(#state.conduits, pageStart + perPage - 1)\ return pageStart, pageEnd, totalPages, page\ end\ \ local function drawConduitNavRow(ctx, win, geom, row, height, totalPages, page)\ local pageState = state.conduitListPage\ local newLabel = \"+ New Conduit\"\ local newW = #newLabel + 2\ local midX = math.max(1, math.floor((geom.ow - newW) / 2) + 1)\ \ drawTallButton(ctx, win, geom, midX, row, newW, height, newLabel, state.theme.window.buttonFill, state.theme.window.buttonText, startConduitWizard)\ \ if totalPages > 1 then\ local navW = 9\ if page > 1 then\ drawTallButton(ctx, win, geom, 2, row, navW, height, \"< Prev\", state.theme.window.buttonFill, state.theme.window.buttonText, function()\ pageState.page = page - 1\ end)\ end\ if page < totalPages then\ drawTallButton(ctx, win, geom, geom.ow - 1 - navW, row, navW, height, \"Next >\", state.theme.window.buttonFill, state.theme.window.buttonText, function()\ pageState.page = page + 1\ end)\ end\ end\ end\ \ local function drawConduitsListView(ctx, win, geom)\ drawConduitHeader(ctx, win, geom, 3)\ \ local navHeight = (ctx.isMonitor and geom.oh >= 16) and 2 or 1\ local navRow = geom.oh - navHeight + 1\ local listStartRow, listEndRow = 5, navRow - 2\ \ if #state.conduits == 0 then\ local text = \"No conduits yet\"\ local col = math.max(1, math.floor((geom.ow - #text) / 2) + 1)\ win.setCursorPos(col, listStartRow)\ win.setTextColor(colors.lightGray)\ win.write(text)\ drawConduitNavRow(ctx, win, geom, navRow, navHeight, 1, 1)\ return\ end\ \ local availRows = listEndRow - listStartRow + 1\ local pageStart, pageEnd, totalPages, page = conduitPaging(availRows)\ \ local row = listStartRow\ for i = pageStart, pageEnd do\ drawConduitRow(ctx, win, geom, row, i, state.conduits[i])\ row = row + 2\ end\ \ drawConduitNavRow(ctx, win, geom, navRow, navHeight, totalPages, page)\ end\ \ local function findDisplaySlot(c)\ local side = (c.direction == \"out\") and \"o\" or \"i\"\ for k, name in pairs(state.conduitDisplay[side]) do\ if name == c.name then return side, k end\ end\ return side, nil\ end\ \ local function drawConduitDetailView(ctx, win, geom)\ local idx = state.conduitDetailIndex\ local c = state.conduits[idx]\ if not c then\ state.conduitView = \"list\"\ return\ end\ c.direction = c.direction or \"in\"\ \ local row = 1\ local rate = energy.getMeterRate(c.meterName)\ local rateText = rate and (formatEnergy(rate) .. \"/t\") or \"offline\"\ win.setCursorPos(2, row)\ win.setTextColor(c.color)\ win.write(string.char(7) .. \" \" .. c.name)\ win.setTextColor(colors.lightGray)\ win.write(\" \" .. c.meterName .. \" \")\ win.setTextColor(colors.white)\ win.write(rateText)\ row = row + 2\ \ local fullW = geom.ow - 2\ \ local isOut = c.direction == \"out\"\ local btnGap = 1\ local btnW = math.floor((fullW - 2 * btnGap) / 3)\ local bx = 2\ \ drawTallButton(ctx, win, geom, bx, row, btnW, 2, \"Dir: \" .. (isOut and \"OUT\" or \"IN\"),\ adjustForButtonContrast(isOut and colors.red or colors.blue), colors.white, function()\ c.direction = isOut and \"in\" or \"out\"\ saveConduitsNow()\ end)\ bx = bx + btnW + btnGap\ \ local side, slotIndex = findDisplaySlot(c)\ drawTallButton(ctx, win, geom, bx, row, btnW, 2, \"Slot: \" .. (slotIndex and (side:upper() .. slotIndex) or \"None\"),\ colors.gray, colors.white, function()\ state.conduitView = \"slotPicker\"\ end)\ bx = bx + btnW + btnGap\ \ drawTallButton(ctx, win, geom, bx, row, fullW - (bx - 2), 2, \"Rename\", colors.cyan, colors.black, function()\ startTextEntry(\"Conduit Name\", c.name, false, function(value)\ c.name = value\ saveConduitsNow()\ end)\ end)\ row = row + 3\ \ local gridBoxW = 8 * 3 + 2\ local gridX = 2 + math.floor((fullW - gridBoxW) / 2) + 1\ local labelText = \"Color\"\ win.setCursorPos(2 + math.floor((fullW - #labelText) / 2), row)\ win.setTextColor(colors.white)\ win.write(labelText)\ row = row + 2\ row = drawColorGrid(ctx, win, geom, gridX, row, function(swatch)\ c.color = swatch\ saveConduitsNow()\ end, 8, 2, c.color, c.color, fullW)\ row = row + 2\ \ if c.flowGateName then\ local status = energy.getFlowGateStatus(c.flowGateName)\ local statusText, isStopped, isUnlimited, isLimited = \"disconnected\", false, false, false\ if status then\ if not status.enabled then\ statusText, isUnlimited = \"Unlimited\", true\ elseif not status.limit or status.limit == 0 then\ statusText, isStopped = \"Stopped\", true\ else\ statusText, isLimited = \"Limit \" .. formatEnergy(status.limit) .. \"/t\", true\ end\ end\ \ -- Only the 2 actions that AREN'T the current mode are shown, to\ -- fit gate status and its controls on a single row.\ local actionDefs = {\ { full = \"Stop\", short = \"Stop\", fill = colors.red, active = isStopped, action = function()\ energy.setFlowLimitByName(c.flowGateName, 0)\ end },\ { full = \"Unlimited\", short = \"Unltd\", fill = colors.lime, active = isUnlimited, action = function()\ energy.clearFlowLimitByName(c.flowGateName)\ end },\ { full = \"Set Limit\", short = \"Limit\", fill = colors.cyan, active = isLimited, action = function()\ startTextEntry(\"Limit (RF/t)\", \"\", true, function(value)\ energy.setFlowLimitByName(c.flowGateName, value)\ end)\ end },\ }\ local actions = {}\ for _, a in ipairs(actionDefs) do\ if not a.active then table.insert(actions, a) end\ end\ \ local labelText = \"Gate: \" .. c.flowGateName .. \" (\" .. statusText .. \")\"\ \ local function layout(useFull)\ local w = useFull and 11 or 8\ local totalBtnW = (#actions * w) + math.max(0, #actions - 1)\ local startX = 2 + fullW - totalBtnW\ return w, startX, math.max(0, startX - 2 - 1)\ end\ \ local btnW, btnStartX, maxLabelW = layout(true)\ local useFull = maxLabelW >= math.min(#labelText, 14)\ if not useFull then\ btnW, btnStartX, maxLabelW = layout(false)\ end\ \ if #labelText > maxLabelW then labelText = labelText:sub(1, maxLabelW) end\ win.setCursorPos(2, row)\ win.setTextColor(colors.lightGray)\ win.write(labelText)\ \ local bx = btnStartX\ for _, a in ipairs(actions) do\ drawButton(ctx, win, geom, bx, row, btnW, useFull and a.full or a.short, a.fill, colors.white, a.action)\ bx = bx + btnW + 1\ end\ row = row + 2\ \ drawButton(ctx, win, geom, 2, row, fullW, \"Unlink Gate\", colors.orange, colors.black, function()\ energy.clearFlowLimitByName(c.flowGateName)\ c.flowGateName = nil\ saveConduitsNow()\ end)\ row = row + 1\ else\ drawButton(ctx, win, geom, 2, row, fullW, \"Link Flow Gate\", colors.cyan, colors.black, function()\ state.conduitView = \"linkGate\"\ pickListPage.page = 1\ end)\ row = row + 2\ end\ \ drawButton(ctx, win, geom, 2, row, fullW, \"Delete Conduit\", colors.red, colors.white, function()\ if c.flowGateName then\ energy.clearFlowLimitByName(c.flowGateName)\ end\ table.remove(state.conduits, idx)\ saveConduitsNow()\ state.conduitView = \"list\"\ state.conduitDetailIndex = nil\ end)\ end\ \ local function drawLinkGateStep(ctx, win, geom)\ local title = \"Link a flow gate\"\ local col = math.max(1, math.floor((geom.ow - #title) / 2) + 1)\ win.setCursorPos(col, 4)\ win.setTextColor(colors.yellow)\ win.write(title)\ \ local c = state.conduits[state.conduitDetailIndex]\ \ drawPickList(ctx, win, geom, energy.getFlowGateNames(), 6, geom.oh - 1, function(name)\ if c then\ local prevOwner = findConduitByFlowGate(name)\ if prevOwner and prevOwner ~= c then\ energy.clearFlowLimitByName(prevOwner.flowGateName)\ prevOwner.flowGateName = nil\ end\ c.flowGateName = name\ saveConduitsNow()\ end\ state.conduitView = \"detail\"\ end, \"No flow gates found\", function(name)\ local owner = findConduitByFlowGate(name)\ if owner and owner ~= c then\ return \"(in use by \" .. owner.name .. \")\"\ end\ return nil\ end)\ end\ \ local function drawConduitSlotStep(ctx, win, geom)\ local title = \"Pick a display slot\"\ local col = math.max(1, math.floor((geom.ow - #title) / 2) + 1)\ win.setCursorPos(col, 4)\ win.setTextColor(colors.yellow)\ win.write(title)\ \ local c = state.conduits[state.conduitDetailIndex]\ if not c then\ state.conduitView = \"detail\"\ return\ end\ \ local side, oldIndex = findDisplaySlot(c)\ local count = state.conduitDisplay.activeCount\ local items = { \"None\" }\ for k = 1, count do\ if k ~= oldIndex then\ table.insert(items, side:upper() .. k)\ end\ end\ \ drawPickList(ctx, win, geom, items, 6, geom.oh - 1, function(label)\ if label == \"None\" then\ clearConduitFromDisplay(c.name)\ else\ local k = tonumber(label:sub(2))\ local occupant = state.conduitDisplay[side][k]\ state.conduitDisplay[side][k] = c.name\ if oldIndex then\ state.conduitDisplay[side][oldIndex] = occupant\ elseif occupant then\ clearConduitFromDisplay(occupant)\ end\ end\ saveSettingsNow()\ state.conduitView = \"detail\"\ end, \"No display slots available\", function(label)\ if label == \"None\" then\ return not oldIndex and \"(current)\" or nil\ end\ local k = tonumber(label:sub(2))\ local occupant = state.conduitDisplay[side][k]\ if occupant then return \"(swap with \" .. occupant .. \")\" end\ return nil\ end)\ end\ \ local function drawWizardMeterStep(ctx, win, geom)\ local title = \"Pick a meter\"\ local col = math.max(1, math.floor((geom.ow - #title) / 2) + 1)\ win.setCursorPos(col, 4)\ win.setTextColor(colors.yellow)\ win.write(title)\ \ local available = withoutClaimed(energy.getMeterNames(), findConduitByMeter)\ \ drawPickList(ctx, win, geom, available, 6, geom.oh - 1, function(name)\ state.wizard.meterName = name\ pickListPage.page = 1\ startTextEntry(\"Conduit Name\", \"\", false, function(value)\ state.wizard.name = value\ state.wizard.step = \"color\"\ end, function()\ state.wizard = nil\ end)\ end, \"No unused energy meters found\")\ end\ \ local function drawWizardColorStep(ctx, win, geom)\ local title = \"Pick a color\"\ local col = math.max(1, math.floor((geom.ow - #title) / 2) + 1)\ win.setCursorPos(col, 4)\ win.setTextColor(colors.yellow)\ win.write(title)\ \ local gridX = math.max(2, geom.ow - (2 * 3) - 2)\ drawColorGrid(ctx, win, geom, gridX, 6, function(c)\ state.wizard.color = c\ state.wizard.step = \"gate\"\ pickListPage.page = 1\ end)\ end\ \ local function drawWizardGateStep(ctx, win, geom)\ drawSecondaryAction(ctx, win, geom, \"Skip\", finishWizard)\ \ local title = \"Link a flow gate? (optional)\"\ local col = math.max(1, math.floor((geom.ow - #title) / 2) + 1)\ win.setCursorPos(col, 4)\ win.setTextColor(colors.yellow)\ win.write(title)\ \ local available = withoutClaimed(energy.getFlowGateNames(), findConduitByFlowGate)\ \ drawPickList(ctx, win, geom, available, 6, geom.oh - 1, function(name)\ state.wizard.flowGateName = name\ finishWizard()\ end, \"No unused flow gates found\")\ end\ \ local function drawTextEntry(ctx, win, geom)\ local row = 4\ local promptText = App.textEntry.prompt .. \":\"\ win.setCursorPos(2, row)\ win.setTextColor(colors.yellow)\ win.write(promptText)\ \ local cancelLabel = \"[X]\"\ drawButton(ctx, win, geom, geom.ow - #cancelLabel, row, #cancelLabel, cancelLabel, colors.red, colors.white, cancelTextEntry)\ row = row + 2\ \ local fieldWidth = math.max(10, math.min(geom.ow - 4, 40))\ local fieldX = math.max(2, math.floor((geom.ow - fieldWidth) / 2) + 1)\ local shown = \"> \" .. App.textEntry.value .. \"_\"\ if #shown > fieldWidth then\ shown = shown:sub(#shown - fieldWidth + 1)\ end\ win.setCursorPos(fieldX, row)\ win.setBackgroundColor(colors.black)\ win.write(string.rep(\" \", fieldWidth))\ win.setCursorPos(fieldX, row)\ win.setTextColor(colors.white)\ win.write(shown)\ win.setBackgroundColor(CONTENT_BG)\ row = row + 1\ \ if App.textEntry.numeric then\ win.setCursorPos(2, row)\ win.setTextColor(colors.lightGray)\ win.write(\"(numbers only)\")\ end\ row = row + 1\ \ local keyboardRow = math.max(row, math.floor(geom.oh / 2) + 1)\ Render.Keyboard.draw(ctx, win, geom, keyboardRow)\ end\ \ local function drawTabRow(ctx, win, geom, row, x0, items, isActive, onSelect)\ local x = x0\ for _, item in ipairs(items) do\ local label = \" \" .. item.label .. \" \"\ local w = #label\ if x + w > geom.ow then\ x = x0\ row = row + 2\ end\ local active = isActive(item)\ drawButton(ctx, win, geom, x, row, w, item.label,\ active and colors.cyan or colors.black,\ active and colors.black or colors.lightGray,\ function() onSelect(item) end)\ x = x + w + 1\ end\ return row + 2\ end\ \ local THEME_PROPS = {\ { label = \"Top Bar Color\", get = function() return state.theme.topBar.bar end, set = function(c) state.theme.topBar.bar = c end },\ { label = \"Top Bar Text\", get = function() return state.theme.topBar.text end, set = function(c) state.theme.topBar.text = c end },\ { label = \"Window Frame\", get = function() return state.theme.window.frame end, set = function(c) state.theme.window.frame = c end },\ { label = \"Button Color\", get = function() return state.theme.window.buttonFill end, set = function(c) state.theme.window.buttonFill = c end },\ { label = \"Button Text\", get = function() return state.theme.window.buttonText end, set = function(c) state.theme.window.buttonText = c end },\ {\ label = \"Storage Outline\",\ get = function() return state.theme.storage.outline or detectStyle().outline end,\ set = function(c) state.theme.storage.outline = c end,\ resettable = true,\ reset = function() state.theme.storage.outline = nil end,\ },\ {\ label = \"Storage Fill\",\ get = function() return state.theme.storage.fill or detectStyle().fill end,\ set = function(c) state.theme.storage.fill = c end,\ resettable = true,\ reset = function() state.theme.storage.fill = nil end,\ },\ { label = \"Background\", get = function() return state.theme.main.bg end, set = function(c) state.theme.main.bg = c end },\ }\ \ local LIGHT_BACKGROUNDS = {\ [colors.white] = true, [colors.yellow] = true, [colors.lime] = true,\ [colors.lightBlue] = true, [colors.lightGray] = true, [colors.cyan] = true,\ [colors.pink] = true, [colors.orange] = true,\ }\ \ local function textColorFor(bg)\ return LIGHT_BACKGROUNDS[bg] and colors.black or colors.white\ end\ \ local function drawThemeRow(ctx, win, geom, y, prop, active, onSelect)\ local bg = prop.get()\ local fg = textColorFor(bg)\ drawListButton(ctx, win, geom, y, bg, function(x1, yy)\ win.setCursorPos(x1 + 1, yy)\ win.setTextColor(fg)\ local prefix = active and (string.char(16) .. \" \") or \" \"\ win.write(prefix .. prop.label)\ end, onSelect)\ end\ \ local function drawThemeMenu(ctx, win, geom)\ state.themeIndex = math.min(math.max(state.themeIndex or 1, 1), #THEME_PROPS)\ \ local row = 2\ for i, prop in ipairs(THEME_PROPS) do\ if row > geom.oh then break end\ drawThemeRow(ctx, win, geom, row, prop, state.themeIndex == i, function()\ state.themeIndex = i\ end)\ row = row + 1\ end\ row = row + 1\ \ local prop = THEME_PROPS[state.themeIndex]\ if not prop or row > geom.oh then return end\ \ win.setCursorPos(2, row)\ win.setTextColor(colors.white)\ win.write(prop.label .. \": \")\ win.setBackgroundColor(prop.get())\ win.write(\" \")\ win.setBackgroundColor(CONTENT_BG)\ row = row + 2\ \ local fullW = geom.ow - 2\ local gridBoxW = 8 * 3 + 2\ local gridX = 2 + math.floor((fullW - gridBoxW) / 2) + 1\ row = drawColorGrid(ctx, win, geom, gridX, row, function(c)\ prop.set(c)\ saveSettingsNow()\ end, 8, 2)\ row = row + 2\ \ if prop.resettable and row <= geom.oh then\ drawButton(ctx, win, geom, 2, row, geom.ow - 2, \"Reset to Auto\", colors.orange, colors.black, function()\ prop.reset()\ saveSettingsNow()\ end)\ end\ end\ \ local SHAPE_OPTIONS = {\ { value = nil, label = \"Auto\" },\ { value = \"circle\", label = \"Circle\" },\ { value = \"square\", label = \"Box\" },\ { value = \"battery\", label = \"Battery\" },\ }\ \ local function drawRenderingMenu(ctx, win, geom)\ local title = \"Rendering\"\ local col = math.max(1, math.floor((geom.ow - #title) / 2) + 1)\ win.setCursorPos(col, 4)\ win.setTextColor(colors.yellow)\ win.write(title)\ \ drawStepper(ctx, win, geom, 6, \"Text Scale\",\ function() return state.textScale end,\ function(v)\ state.textScale = v\ for _, s in ipairs(screens) do\ if s.isMonitor then s.target.setTextScale(v) end\ end\ setupBuffers()\ saveSettingsNow()\ end,\ 0.5, 0.5, 5, function(v) return string.format(\"%.1f\", v) end)\ \ drawStepper(ctx, win, geom, 8, \"Update Speed\",\ function() return state.refreshInterval end,\ function(v)\ state.refreshInterval = v\ saveSettingsNow()\ end,\ 0.5, 0.5, 5, function(v) return string.format(\"%.1fs\", v) end)\ \ local current = state.theme.storage.shape\ local currentLabel = \"Auto\"\ for _, opt in ipairs(SHAPE_OPTIONS) do\ if opt.value == current then currentLabel = opt.label end\ end\ win.setCursorPos(2, 10)\ win.setTextColor(colors.white)\ win.write(\"Shape: \")\ win.setTextColor(colors.cyan)\ win.write(currentLabel)\ \ drawTabRow(ctx, win, geom, 12, 2, SHAPE_OPTIONS,\ function(opt) return opt.value == current end,\ function(opt)\ state.theme.storage.shape = opt.value\ saveSettingsNow()\ end)\ end\ \ local SETTINGS_ITEMS = {\ { label = \"Theme\", action = function() state.settingsView = \"theme\" end },\ { label = \"Rendering\", action = function() state.settingsView = \"rendering\" end },\ { label = \"Conduits\", action = openConduits },\ {\ label = \"Conduit\\nDisplay\",\ action = function()\ state.editingConduitDisplay = true\ closeOptions()\ end,\ },\ }\ \ local function drawSettingsGrid(ctx, win, geom)\ local items = {}\ for _, entry in ipairs(SETTINGS_ITEMS) do\ table.insert(items, { label = entry.label, action = entry.action })\ end\ drawButtonGrid(ctx, win, geom, items, 4, geom.oh - 1, state.settingsGridPage)\ end\ \ local function drawSettingsTab(ctx, win, geom)\ if state.settingsView == \"theme\" then\ drawThemeMenu(ctx, win, geom)\ elseif state.settingsView == \"rendering\" then\ drawRenderingMenu(ctx, win, geom)\ else\ drawSettingsGrid(ctx, win, geom)\ end\ end\ \ local function optionsBackAction()\ if App.textEntry then return nil end\ \ if state.settingsView == \"theme\" or state.settingsView == \"rendering\" then\ return \"Back\", function() state.settingsView = \"grid\" end\ end\ \ if state.settingsView ~= \"conduits\" then return nil end\ \ if state.wizard then\ if state.wizard.step == \"meter\" then\ return \"Cancel\", cancelWizard\ elseif state.wizard.step == \"color\" then\ return \"Back\", function() state.wizard.step = \"meter\" end\ elseif state.wizard.step == \"gate\" then\ return \"Back\", function() state.wizard.step = \"color\" end\ end\ elseif state.conduitView == \"detail\" then\ return \"Back\", function()\ state.conduitView = \"list\"\ state.conduitDetailIndex = nil\ end\ elseif state.conduitView == \"linkGate\" then\ return \"Back\", function() state.conduitView = \"detail\" end\ elseif state.conduitView == \"slotPicker\" then\ return \"Back\", function() state.conduitView = \"detail\" end\ end\ return \"Back\", function() state.settingsView = \"grid\" end\ end\ \ local function optionsWindowTitle()\ if state.settingsView == \"conduits\" then return \"Conduits\" end\ return \"Options\"\ end\ \ local BACK_PLACEHOLDER = \"[< Back]\"\ \ local function drawOptionsScreen(ctx, monW, monH)\ local win, geom = ensureOptionsWindow(ctx, monW, monH)\ local outer = ctx.optionsWin\ \ outer.setVisible(false)\ outer.setBackgroundColor(colors.black)\ outer.clear()\ Render.drawThinBorder(outer, geom.outerOw, geom.outerOh, state.theme.window.frame)\ \ local outerOx, outerOy = geom.ox - 1, geom.oy - 1\ local frame = state.theme.window.frame\ \ local backLabel, backAction = optionsBackAction()\ local backText = backLabel and (\"[< \" .. backLabel .. \"]\") or BACK_PLACEHOLDER\ outer.setCursorPos(2, 1)\ outer.setBackgroundColor(frame)\ outer.setTextColor(backLabel and colors.white or frame)\ outer.write(backText)\ if backLabel then\ addButton(ctx, outerOx + 2 - 1, outerOy, outerOx + 2 + #backText - 2, outerOy, backAction)\ end\ \ local titleText = \" \" .. optionsWindowTitle() .. \" \"\ local titleX = 2 + #backText + 1\ outer.setCursorPos(titleX, 1)\ outer.setBackgroundColor(frame)\ outer.setTextColor(colors.white)\ outer.write(titleText)\ \ local closeLabel = \"[X]\"\ local closeX = geom.outerOw - #closeLabel - 1\ outer.setCursorPos(closeX, 1)\ outer.setBackgroundColor(colors.red)\ outer.setTextColor(colors.white)\ outer.write(closeLabel)\ addButton(ctx, outerOx + closeX - 1, outerOy, outerOx + closeX + #closeLabel - 2, outerOy, closeOptions)\ \ win.setVisible(false)\ win.setBackgroundColor(CONTENT_BG)\ win.setTextColor(colors.white)\ win.clear()\ \ if App.textEntry then\ drawTextEntry(ctx, win, geom)\ elseif state.settingsView == \"conduits\" then\ if state.wizard then\ if state.wizard.step == \"meter\" then\ drawWizardMeterStep(ctx, win, geom)\ elseif state.wizard.step == \"color\" then\ drawWizardColorStep(ctx, win, geom)\ elseif state.wizard.step == \"gate\" then\ drawWizardGateStep(ctx, win, geom)\ end\ elseif state.conduitView == \"detail\" then\ drawConduitDetailView(ctx, win, geom)\ elseif state.conduitView == \"linkGate\" then\ drawLinkGateStep(ctx, win, geom)\ elseif state.conduitView == \"slotPicker\" then\ drawConduitSlotStep(ctx, win, geom)\ else\ drawConduitsListView(ctx, win, geom)\ end\ else\ drawSettingsTab(ctx, win, geom)\ end\ \ if state.settingsView == \"grid\" and not App.textEntry then\ local versionText = \"v\" .. App.VERSION .. \" \"\ local versionX = math.max(1, geom.outerOw - #versionText)\ outer.setCursorPos(versionX, geom.outerOh)\ outer.setBackgroundColor(frame)\ outer.setTextColor(colors.black)\ outer.write(versionText)\ addButton(ctx, outerOx + versionX - 1, outerOy + geom.outerOh - 1,\ outerOx + versionX + #versionText - 2, outerOy + geom.outerOh - 1, App.startDemoMode)\ end\ \ win.setVisible(true)\ outer.setVisible(true)\ end\ \ return {\ drawOptionsScreen = drawOptionsScreen,\ hideOptionsWindow = hideOptionsWindow,\ }\ \ end\ \ return M" files["dlib/render.lua"] = "return function(App)\ \ local CONTENT_BG = App.CONTENT_BG\ local addButton = App.addButton\ \ -- Codes 128-159 encode 5 of a 2x3 subpixel cell's 6 cells directly;\ -- bottom-right \"on\" is only reachable by inverting fg/bg and the other 5 bits.\ local function teletextChar(tl, tr, ml, mr, bl, br, fgColor, bgColor)\ local num = (tl and 1 or 0) + (tr and 2 or 0) + (ml and 4 or 0)\ + (mr and 8 or 0) + (bl and 16 or 0)\ if br then\ return string.char(128 + (31 - num)), bgColor, fgColor\ else\ return string.char(128 + num), fgColor, bgColor\ end\ end\ \ local Canvas = {}\ Canvas.__index = Canvas\ \ function Canvas.new(cols, rows)\ local self = setmetatable({}, Canvas)\ self.cols = cols\ self.rows = rows\ self.width = cols * 2\ self.height = rows * 3\ self.bg = colors.black\ self.pixels = {}\ for y = 1, self.height do\ self.pixels[y] = {}\ end\ self:clear(self.bg)\ return self\ end\ \ function Canvas:clear(color)\ self.bg = color\ for y = 1, self.height do\ local row = self.pixels[y]\ for x = 1, self.width do\ row[x] = color\ end\ end\ end\ \ function Canvas:setPixel(x, y, color)\ x = math.floor(x + 0.5)\ y = math.floor(y + 0.5)\ if x < 1 or x > self.width or y < 1 or y > self.height then return end\ self.pixels[y][x] = color\ end\ \ function Canvas:setPriority(color)\ self.priorityColor = color\ end\ \ function Canvas:getPixel(x, y)\ x = math.floor(x + 0.5)\ y = math.floor(y + 0.5)\ if x < 1 or x > self.width or y < 1 or y > self.height then return nil end\ return self.pixels[y][x]\ end\ \ -- Leftmost/rightmost painted character column on a row, or nil, nil if\ -- untouched - lets callers (e.g. wire drawing) know what a shape will cover.\ function Canvas:rowPixelBounds(charRow)\ if charRow < 1 or charRow > self.rows then return nil, nil end\ local topRow = self.pixels[charRow * 3 - 2]\ local midRow = self.pixels[charRow * 3 - 1]\ local botRow = self.pixels[charRow * 3]\ local bg = self.bg\ local left, right\ \ for col = 1, self.cols do\ local xl, xr = col * 2 - 1, col * 2\ if topRow[xl] ~= bg or topRow[xr] ~= bg or midRow[xl] ~= bg or midRow[xr] ~= bg\ or botRow[xl] ~= bg or botRow[xr] ~= bg then\ if not left then left = col end\ right = col\ end\ end\ \ return left, right\ end\ \ function Canvas:cellFullyPainted(charRow, col)\ if charRow < 1 or charRow > self.rows then return false end\ local topRow = self.pixels[charRow * 3 - 2]\ local midRow = self.pixels[charRow * 3 - 1]\ local botRow = self.pixels[charRow * 3]\ local bg = self.bg\ local xl, xr = col * 2 - 1, col * 2\ return topRow[xl] ~= bg and topRow[xr] ~= bg and midRow[xl] ~= bg and midRow[xr] ~= bg\ and botRow[xl] ~= bg and botRow[xr] ~= bg\ end\ \ -- A cell can only show 2 colours; when 3 are present, priorityColor\ -- (the outline) always wins, and the other slot goes to whichever remains most frequent.\ local function resolveCell(c1, c2, c3, c4, c5, c6, priorityColor)\ local counts, order = {}, {}\ for _, c in ipairs({ c1, c2, c3, c4, c5, c6 }) do\ if not counts[c] then order[#order + 1] = c end\ counts[c] = (counts[c] or 0) + 1\ end\ \ local fg\ if priorityColor and counts[priorityColor] then\ fg = priorityColor\ else\ local bestN = -1\ for _, c in ipairs(order) do\ if counts[c] > bestN then fg, bestN = c, counts[c] end\ end\ end\ \ local bg, bestN = fg, -1\ for _, c in ipairs(order) do\ if c ~= fg and counts[c] > bestN then bg, bestN = c, counts[c] end\ end\ \ return fg, bg\ end\ \ -- transparent: skip untouched cells entirely instead of blitting bg, so\ -- whatever's on `target` underneath shows through. bgOverride: color for cells left partially unpainted.\ function Canvas:render(target, rowOffset, transparent, bgOverride)\ target = target or term\ rowOffset = rowOffset or 0\ local toBlit = colors.toBlit\ \ for row = 1, self.rows do\ local topRow = self.pixels[row * 3 - 2]\ local midRow = self.pixels[row * 3 - 1]\ local botRow = self.pixels[row * 3]\ local text, fgs, bgs = {}, {}, {}\ \ for col = 1, self.cols do\ local xl, xr = col * 2 - 1, col * 2\ local tl, tr = topRow[xl], topRow[xr]\ local ml, mr = midRow[xl], midRow[xr]\ local bl, br = botRow[xl], botRow[xr]\ \ if transparent and tl == self.bg and tr == self.bg and ml == self.bg\ and mr == self.bg and bl == self.bg and br == self.bg then\ text[col] = false\ else\ local fg, bg = resolveCell(tl, tr, ml, mr, bl, br, self.priorityColor)\ \ if bg == self.bg and bgOverride then\ local override = bgOverride(row, col)\ if override then bg = override end\ end\ \ local char, textColor, bgColor\ if fg == bg then\ char, textColor, bgColor = \" \", fg, bg\ else\ char, textColor, bgColor = teletextChar(\ tl == fg, tr == fg, ml == fg, mr == fg, bl == fg, br == fg,\ fg, bg)\ end\ \ text[col] = char\ fgs[col] = toBlit(textColor)\ bgs[col] = toBlit(bgColor)\ end\ end\ \ if not transparent then\ target.setCursorPos(1, rowOffset + row)\ target.blit(table.concat(text), table.concat(fgs), table.concat(bgs))\ else\ local col = 1\ while col <= self.cols do\ if text[col] == false then\ col = col + 1\ else\ local startCol = col\ local t, f, b = {}, {}, {}\ while col <= self.cols and text[col] ~= false do\ t[#t + 1] = text[col]\ f[#f + 1] = fgs[col]\ b[#b + 1] = bgs[col]\ col = col + 1\ end\ target.setCursorPos(startCol, rowOffset + row)\ target.blit(table.concat(t), table.concat(f), table.concat(b))\ end\ end\ end\ end\ end\ \ local function ringThicknessFor(percentage)\ if percentage <= 80 then\ return 2\ end\ return 2 + (percentage - 80) / 20 * 1.5\ end\ \ -- percentage is by area: fill radius grows to the ring's inner edge, scaled by sqrt(pct).\ local function drawCircle(canvas, x, y, radius, color1, percentage, color2)\ percentage = math.max(0, math.min(100, percentage or 0))\ \ local thickness = ringThicknessFor(percentage)\ local ringInner = math.max(radius - thickness, 0)\ local fillRadius = ringInner * math.sqrt(percentage / 100)\ local fillRadiusSq = fillRadius * fillRadius\ \ canvas:setPriority(color1)\ \ local minX = math.max(1, math.floor(x - radius - 1))\ local maxX = math.min(canvas.width, math.ceil(x + radius + 1))\ local minY = math.max(1, math.floor(y - radius - 1))\ local maxY = math.min(canvas.height, math.ceil(y + radius + 1))\ \ if percentage > 0 then\ for py = minY, maxY do\ local dy = py - y\ for px = minX, maxX do\ local dx = px - x\ if dx * dx + dy * dy <= fillRadiusSq then\ canvas:setPixel(px, py, color2)\ end\ end\ end\ end\ \ for py = minY, maxY do\ local dy = py - y\ for px = minX, maxX do\ local dx = px - x\ local dist = math.sqrt(dx * dx + dy * dy)\ if dist <= radius and dist > radius - thickness then\ canvas:setPixel(px, py, color1)\ end\ end\ end\ end\ \ local BLOCK = 2\ \ -- Fill is a grid of BLOCKxBLOCK cubes anchored bottom-left, lit one at a\ -- time; the far row/column absorbs any remainder so it always reaches the border.\ local function drawSquare(canvas, x, y, size, color1, percentage, color2)\ percentage = math.max(0, math.min(100, percentage or 0))\ canvas:setPriority(color1)\ \ local left, right = math.floor(x - size), math.ceil(x + size)\ local top, bottom = math.floor(y - size), math.ceil(y + size)\ \ for py = top, bottom do\ for px = left, right do\ local onEdge = (px - left < BLOCK) or (right - px < BLOCK)\ or (py - top < BLOCK) or (bottom - py < BLOCK)\ if onEdge then\ canvas:setPixel(px, py, color1)\ end\ end\ end\ \ if percentage <= 0 then return end\ \ local iLeft, iRight = left + BLOCK, right - BLOCK\ local iTop, iBottom = top + BLOCK, bottom - BLOCK\ local innerW, innerH = iRight - iLeft + 1, iBottom - iTop + 1\ \ local cols = math.max(1, math.floor(innerW / BLOCK))\ local colSpans = {}\ for c = 1, cols do\ local l = iLeft + (c - 1) * BLOCK\ local r = l + BLOCK - 1\ if c == cols then r = iRight end\ colSpans[c] = { left = l, right = r }\ end\ \ local rows = math.max(1, math.floor(innerH / BLOCK))\ local rowSpans = {}\ for r = 1, rows do\ local b = iBottom - (r - 1) * BLOCK\ local t = b - BLOCK + 1\ if r == rows then t = iTop end\ rowSpans[r] = { top = t, bottom = b }\ end\ \ local totalCubes = cols * rows\ local litCubes = math.floor(totalCubes * percentage / 100 + 0.5)\ \ local count = 0\ for r = 1, rows do\ local rs = rowSpans[r]\ for c = 1, cols do\ count = count + 1\ if count > litCubes then return end\ local cs = colSpans[c]\ for py = rs.top, rs.bottom do\ for px = cs.left, cs.right do\ canvas:setPixel(px, py, color2)\ end\ end\ end\ end\ end\ \ local function drawBattery(canvas, x, y, size, color1, percentage, color2)\ percentage = math.max(0, math.min(100, percentage or 0))\ canvas:setPriority(color1)\ \ local bodyHeight = size * 3.2\ local left, right = math.floor(x - size), math.ceil(x + size)\ local bTop = math.floor(y - bodyHeight / 2)\ local bBottom = math.ceil(y + bodyHeight / 2)\ local bLeft, bRight = left, right\ \ local nubHalfW = size * 0.35\ local nubHeight = size * 0.35\ local nLeft = math.floor(x - nubHalfW)\ local nRight = math.ceil(x + nubHalfW)\ local nTop = math.floor((y - bodyHeight / 2) - nubHeight)\ \ for py = bTop, bBottom do\ for px = bLeft, bRight do\ local onEdge = (px - bLeft < BLOCK) or (bRight - px < BLOCK)\ or (py - bTop < BLOCK) or (bBottom - py < BLOCK)\ if onEdge then\ local underNub = (py - bTop < BLOCK) and px >= nLeft and px <= nRight\ if not underNub then\ canvas:setPixel(px, py, color1)\ end\ end\ end\ end\ \ for py = nTop, bTop + BLOCK - 1 do\ for px = nLeft, nRight do\ local onEdge = (px - nLeft < BLOCK) or (nRight - px < BLOCK) or (py - nTop < BLOCK)\ if onEdge then\ canvas:setPixel(px, py, color1)\ end\ end\ end\ \ if percentage <= 0 then return end\ \ -- Fill runs bottom-up through body, neck and nub as one continuous\ -- rectangle stack, weighted by area so the narrow nub can't dominate the range.\ local iLeft, iRight = bLeft + BLOCK, bRight - BLOCK\ local iTop, iBottom = bTop + BLOCK, bBottom - BLOCK\ local nInnerLeft, nInnerRight = nLeft + BLOCK, nRight - BLOCK\ local nInnerTop = nTop + BLOCK\ \ local segments = {\ { left = iLeft, right = iRight, top = iTop, bottom = iBottom },\ { left = nInnerLeft, right = nInnerRight, top = bTop, bottom = iTop - 1 },\ { left = nInnerLeft, right = nInnerRight, top = nInnerTop, bottom = bTop - 1 },\ }\ \ local totalArea = 0\ for _, seg in ipairs(segments) do\ local w = math.max(0, seg.right - seg.left + 1)\ local h = math.max(0, seg.bottom - seg.top + 1)\ totalArea = totalArea + w * h\ end\ \ local remaining = totalArea * percentage / 100\ for _, seg in ipairs(segments) do\ local w = seg.right - seg.left + 1\ local h = seg.bottom - seg.top + 1\ local area = w * h\ if remaining > 0 and area > 0 then\ local fillH = math.min(h, remaining / w)\ local fillTop = seg.bottom - fillH + 1\ for py = math.floor(fillTop), seg.bottom do\ for px = seg.left, seg.right do\ canvas:setPixel(px, py, color2)\ end\ end\ remaining = remaining - fillH * w\ end\ end\ end\ \ local function drawThinRect(target, x, y, w, h, borderColor, bg)\ local toBlit = colors.toBlit\ \ local function put(px, py, tl, tr, ml, mr, bl, br)\ local ch, fg, bgc = teletextChar(tl, tr, ml, mr, bl, br, borderColor, bg)\ target.setCursorPos(px, py)\ target.blit(ch, toBlit(fg), toBlit(bgc))\ end\ \ for i = 0, w - 1 do\ local px = x + i\ if i == 0 then\ put(px, y, true, true, true, true, true, false)\ elseif i == w - 1 then\ put(px, y, true, true, true, true, false, true)\ else\ put(px, y, true, true, true, true, false, false)\ end\ end\ \ if h > 1 then\ for i = 0, w - 1 do\ local px = x + i\ if i == 0 then\ put(px, y + h - 1, true, false, true, false, true, true)\ elseif i == w - 1 then\ put(px, y + h - 1, false, true, false, true, true, true)\ else\ put(px, y + h - 1, false, false, false, false, true, true)\ end\ end\ end\ \ for i = 1, h - 2 do\ local py = y + i\ put(x, py, true, false, true, false, true, false)\ put(x + w - 1, py, false, true, false, true, false, true)\ end\ end\ \ local function drawThinTopEdge(target, x, y, w, borderColor, bg, cornerLeft, cornerRight)\ local ch, fg, bgc = teletextChar(true, true, false, false, false, false, borderColor, bg)\ target.setCursorPos(x, y)\ target.blit(string.rep(ch, w), string.rep(colors.toBlit(fg), w), string.rep(colors.toBlit(bgc), w))\ \ if cornerLeft then\ local lch, lfg, lbg = teletextChar(true, true, true, false, true, false, borderColor, bg)\ target.setCursorPos(x, y)\ target.blit(lch, colors.toBlit(lfg), colors.toBlit(lbg))\ end\ if cornerRight and w > 1 then\ local rch, rfg, rbg = teletextChar(true, true, false, true, false, true, borderColor, bg)\ target.setCursorPos(x + w - 1, y)\ target.blit(rch, colors.toBlit(rfg), colors.toBlit(rbg))\ end\ end\ \ local function drawThinBottomEdge(target, x, y, w, borderColor, bg, cornerLeft, cornerRight)\ local ch, fg, bgc = teletextChar(false, false, false, false, true, true, borderColor, bg)\ target.setCursorPos(x, y)\ target.blit(string.rep(ch, w), string.rep(colors.toBlit(fg), w), string.rep(colors.toBlit(bgc), w))\ \ if cornerLeft then\ local lch, lfg, lbg = teletextChar(true, false, true, false, true, true, borderColor, bg)\ target.setCursorPos(x, y)\ target.blit(lch, colors.toBlit(lfg), colors.toBlit(lbg))\ end\ if cornerRight and w > 1 then\ local rch, rfg, rbg = teletextChar(false, true, false, true, true, true, borderColor, bg)\ target.setCursorPos(x + w - 1, y)\ target.blit(rch, colors.toBlit(rfg), colors.toBlit(rbg))\ end\ end\ \ local function drawThinBorder(target, w, h, borderColor)\ drawThinRect(target, 1, 1, w, h, borderColor, colors.black)\ end\ \ local function drawTallButton(ctx, win, geom, x, y, width, height, label, fillColor, textColor, action)\ width = math.max(1, width)\ height = math.max(1, height)\ \ for ry = 0, height - 1 do\ win.setCursorPos(x, y + ry)\ win.setBackgroundColor(fillColor)\ win.write(string.rep(\" \", width))\ end\ \ local lines = {}\ for line in (label or \"\"):gmatch(\"[^\\n]+\") do\ table.insert(lines, line)\ end\ if #lines == 0 then lines = { \"\" } end\ \ local firstRow = y + math.max(0, math.floor((height - #lines) / 2))\ win.setTextColor(textColor)\ for i, line in ipairs(lines) do\ local text = line\ if #text > width then text = text:sub(1, width) end\ local padLeft = math.floor((width - #text) / 2)\ local labelRow = firstRow + i - 1\ if labelRow >= y and labelRow <= y + height - 1 then\ win.setCursorPos(x + padLeft, labelRow)\ win.write(text)\ end\ end\ win.setBackgroundColor(CONTENT_BG)\ \ if action then\ addButton(ctx, geom.ox + x - 1, geom.oy + y - 1, geom.ox + x + width - 2, geom.oy + y + height - 2, action)\ end\ \ return x + width\ end\ \ local function drawButton(ctx, win, geom, x, y, width, label, fillColor, textColor, action)\ return drawTallButton(ctx, win, geom, x, y, width, 1, label, fillColor, textColor, action)\ end\ \ local function keyButton(ctx, win, geom, x, y, w, h, label, action, fill)\ return drawTallButton(ctx, win, geom, x, y, w, h, label, fill or colors.lightGray, colors.white, action)\ end\ \ local confirmTextEntry = App.confirmTextEntry\ \ local KEYBOARD_ROWS = {\ { \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\", \"0\" },\ { \"q\", \"w\", \"e\", \"r\", \"t\", \"y\", \"u\", \"i\", \"o\", \"p\" },\ { \"a\", \"s\", \"d\", \"f\", \"g\", \"h\", \"j\", \"k\", \"l\" },\ { \"z\", \"x\", \"c\", \"v\", \"b\", \"n\", \"m\", \"-\" },\ }\ \ local function drawOnscreenKeyboard(ctx, win, geom, startRow)\ local availW = geom.ow - 2\ local btnW = math.max(2, math.floor(availW / 10))\ local btnH = 2\ if startRow + (#KEYBOARD_ROWS * btnH) + 1 > geom.oh then\ btnH = 1\ end\ \ local row = startRow\ local maxRow = geom.oh\ for rIdx, keyRow in ipairs(KEYBOARD_ROWS) do\ if row > maxRow then break end\ local totalW = #keyRow * btnW\ local x0 = math.max(1, math.floor((geom.ow - totalW) / 2) + 1)\ for cIdx, k in ipairs(keyRow) do\ local x = x0 + (cIdx - 1) * btnW\ local isEven = (rIdx + cIdx) % 2 == 0\ local fill = isEven and colors.gray or colors.lightGray\ keyButton(ctx, win, geom, x, row, btnW - 1, btnH, k, function()\ App.textEntry.value = App.textEntry.value .. k\ end, fill)\ end\ row = row + btnH\ end\ \ if row <= maxRow then\ local controls = {\ { label = \"UNDO\", w = 8, fill = colors.red, action = function()\ App.textEntry.value = App.textEntry.value:sub(1, -2)\ end },\ { label = \"SPACE\", w = math.max(10, geom.ow - 22), fill = colors.blue, action = function()\ App.textEntry.value = App.textEntry.value .. \" \"\ end },\ { label = \"ACCEPT\", w = 10, fill = colors.green, action = confirmTextEntry },\ }\ local totalW = 0\ for _, c in ipairs(controls) do totalW = totalW + c.w end\ local x = math.max(1, math.floor((geom.ow - totalW) / 2) + 1)\ for _, c in ipairs(controls) do\ drawTallButton(ctx, win, geom, x, row, c.w, 1, c.label, c.fill, colors.white, c.action)\ x = x + c.w\ end\ row = row + 1\ end\ \ return row\ end\ \ local Keyboard = {\ ROWS = KEYBOARD_ROWS,\ draw = drawOnscreenKeyboard,\ }\ \ return {\ Canvas = Canvas,\ drawCircle = drawCircle,\ drawSquare = drawSquare,\ drawBattery = drawBattery,\ teletextChar = teletextChar,\ \ drawTallButton = drawTallButton,\ drawButton = drawButton,\ keyButton = keyButton,\ drawThinBorder = drawThinBorder,\ drawThinRect = drawThinRect,\ drawThinTopEdge = drawThinTopEdge,\ drawThinBottomEdge = drawThinBottomEdge,\ \ Keyboard = Keyboard,\ }\ \ end" files["dlib/main.lua"] = "return function(App)\ \ local state = App.state\ local energy = App.energy\ local Render = App.Render\ local addButton = App.addButton\ local closeOptions = App.closeOptions\ local CONTENT_BG = App.CONTENT_BG\ \ local saveSettingsNow = App.saveSettingsNow\ local getConduitByName = App.getConduitByName\ local clearConduitFromDisplay = App.clearConduitFromDisplay\ \ local compactEnergy = App.compactEnergy\ local formatTime = App.formatTime\ local detectStyle = App.detectStyle\ local SLOT_COUNT_STEPS = App.SLOT_COUNT_STEPS\ local MAX_SLOTS_PER_SIDE = App.MAX_SLOTS_PER_SIDE\ \ -- Teletext glyph 128+1+2+4+8: top+middle sub-rows lit, for a divider\ -- 2/3 of a cell tall instead of a full character line.\ local TOP_LINE_CHAR_THICK = string.char(128 + 1 + 2 + 4 + 8)\ \ local function drawTopBar(ctx, w)\ local target = ctx.buffer\ local theme = state.theme.topBar\ target.setBackgroundColor(colors.black)\ target.setTextColor(theme.bar)\ target.setCursorPos(1, 1)\ target.write(string.rep(TOP_LINE_CHAR_THICK, w))\ \ local optionsLabel = state.demoMode and \"[Exit Demo]\" or state.editingConduitDisplay and \"[Done]\" or \"[Options]\"\ local labelX = (w - 1) - #optionsLabel + 1\ \ local info = energy.getStorageInfo()\ local shownPercentage = state.demoMode and state.demoPercentage or info.percentage\ local capacityText = string.format(\" [%s/%s] [%.2f%%]\",\ compactEnergy(info.stored), compactEnergy(info.max), shownPercentage)\ local maxCapacityWidth = math.max(0, labelX - 1 - 2)\ if #capacityText > maxCapacityWidth then\ capacityText = capacityText:sub(1, maxCapacityWidth)\ end\ \ target.setCursorPos(2, 1)\ target.setBackgroundColor(theme.bar)\ target.setTextColor(theme.text)\ target.write(capacityText)\ \ target.setCursorPos(labelX, 1)\ target.setBackgroundColor(state.showOptions and theme.text or theme.bar)\ target.setTextColor(state.showOptions and colors.black or theme.text)\ target.write(optionsLabel)\ \ addButton(ctx, labelX, 1, labelX + #optionsLabel - 1, 1, function()\ if state.demoMode then\ state.demoMode = false\ elseif state.editingConduitDisplay then\ state.editingConduitDisplay = false\ state.editingSlot = nil\ elseif state.showOptions then\ closeOptions()\ else\ state.showOptions = true\ end\ end)\ end\ \ local SLOT_HEIGHT = 3\ local SPACING_STEPS = { 1, 2, 3, 4 }\ \ local function fittedIncrement(spacing, ringsPerSide, canvasRows)\ local increment = SLOT_HEIGHT + spacing\ if ringsPerSide <= 0 then return increment end\ local available = math.floor(canvasRows / 2) - 1\ while increment > SLOT_HEIGHT and ringsPerSide * increment > available do\ increment = increment - 1\ end\ return increment\ end\ \ -- Ring N's row offset from center and its sign (-1 above, +1 below, 0\ -- for the center slot itself); k alternates above/below as it counts up.\ local function slotRingSign(k)\ if k <= 1 then return 0, 0 end\ local ring = math.floor(k / 2)\ local sign = (k % 2 == 0) and -1 or 1\ return ring, sign\ end\ \ local function rowInBounds(row, top, rows)\ return row >= top and row <= top + rows - 1\ end\ \ -- Fills [colA, colB] solid with `color` on `row`. wireBg (if given)\ -- records every painted column, for the shape's bgOverride pass later.\ local function drawHBar(target, colA, colB, row, color, wireBg)\ local left = math.min(colA, colB)\ local width = math.abs(colB - colA) + 1\ target.setCursorPos(left, row)\ target.setBackgroundColor(color)\ target.write(string.rep(\" \", width))\ \ if wireBg then\ local rowGrid = wireBg[row]\ if not rowGrid then\ rowGrid = {}\ wireBg[row] = rowGrid\ end\ for col = left, left + width - 1 do\ rowGrid[col] = color\ end\ end\ end\ \ local function writeLabel(target, x, row, width, text, bg, fg)\ if #text > width then text = text:sub(1, width) end\ target.setCursorPos(x, row)\ target.setBackgroundColor(bg)\ target.setTextColor(fg)\ target.write(text .. string.rep(\" \", width - #text))\ end\ \ local CONNECTOR_STEP = 2\ \ local function outerWidth(ring)\ return 2 * ring + 1\ end\ \ -- Writes `text` inside [colA, colB], padded toward the block-facing\ -- edge (dir=1 -> pad right, dir=-1 -> pad left).\ local function writeInset(target, colA, colB, row, text, bg, fg, dir)\ local left = math.min(colA, colB)\ local width = math.abs(colB - colA) + 1\ if #text > width then text = text:sub(1, width) end\ local padded\ if dir == -1 then\ padded = string.rep(\" \", width - #text) .. text\ else\ padded = text .. string.rep(\" \", width - #text)\ end\ target.setCursorPos(left, row)\ target.setBackgroundColor(bg)\ target.setTextColor(fg)\ target.write(padded)\ end\ \ local function shapeHalfExtent(shape, size)\ return (shape == \"battery\") and (size * 1.6) or size\ end\ \ local function shapeHalfWidthAt(shape, size, dySub)\ local halfExtent = shapeHalfExtent(shape, size)\ local boundedDy = math.max(-halfExtent, math.min(halfExtent, dySub))\ if shape == \"circle\" then\ return math.sqrt(math.max(0, size * size - boundedDy * boundedDy))\ end\ return size\ end\ \ local function rowCenterSubY(canvasRowOffset, row)\ local canvasRow = row - canvasRowOffset\ return (canvasRow - 1) * 3 + 2\ end\ \ local function subXToColumn(subX)\ return math.floor(subX / 2 + 0.5)\ end\ \ local function geometricEdgeAt(shapeGeom, canvasRowOffset, row, side)\ local dySub = rowCenterSubY(canvasRowOffset, row) - shapeGeom.cy\ local halfWidth = shapeHalfWidthAt(shapeGeom.shape, shapeGeom.size, dySub)\ local subX = (side == \"i\") and (shapeGeom.cx - halfWidth) or (shapeGeom.cx + halfWidth)\ return subXToColumn(subX)\ end\ \ -- Where a wire reaching for the shape stops: reads the shape's own\ -- rendered canvas when this row has anything painted, so it can never\ -- mismatch the shape's real edge; falls back to geometry only past its reach.\ local function edgeAt(canvas, shapeGeom, canvasRowOffset, row, side)\ local canvasRow = row - canvasRowOffset\ if canvasRow >= 1 and canvasRow <= canvas.rows then\ local left, right = canvas:rowPixelBounds(canvasRow)\ local col = (side == \"i\") and left or right\ if col then return col end\ end\ return geometricEdgeAt(shapeGeom, canvasRowOffset, row, side)\ end\ \ local function drawWireSegment(target, canvas, shapeGeom, canvasRowOffset, row, side, fromX, dir, desiredLen, color, wireBg)\ local edge = edgeAt(canvas, shapeGeom, canvasRowOffset, row, side)\ local desiredEndX = fromX + dir * (desiredLen - 1)\ local endX = (dir == 1) and math.min(desiredEndX, edge) or math.max(desiredEndX, edge)\ drawHBar(target, fromX, endX, row, color, wireBg)\ return endX, endX == edge\ end\ \ -- An off-center conduit's wire: flat run on blockRow, then (if it\ -- didn't already hit blockRow's edge) a bend onto bendRow, flat to its\ -- edge. Returns the bend's [left, right] text span and direction, or nil if there's no room.\ local function drawConnector(target, canvas, shapeGeom, canvasRowOffset, blockX, side, blockRow, bendRow, ring, color, wireBg)\ local dir = (side == \"i\") and 1 or -1\ local blockEndX, touchedBlock = drawWireSegment(target, canvas, shapeGeom, canvasRowOffset, blockRow, side,\ blockX, dir, CONNECTOR_STEP * ring, color, wireBg)\ \ if touchedBlock then return nil, nil, nil end\ \ local bendEdge = edgeAt(canvas, shapeGeom, canvasRowOffset, bendRow, side)\ if (dir == 1 and bendEdge < blockEndX) or (dir == -1 and bendEdge > blockEndX) then\ return nil, nil, nil\ end\ drawHBar(target, blockEndX, bendEdge, bendRow, color, wireBg)\ \ local textEndX = bendEdge - dir\ if (dir == 1 and textEndX < blockEndX) or (dir == -1 and textEndX > blockEndX) then\ return nil, nil, nil\ end\ \ return math.min(blockEndX, textEndX), math.max(blockEndX, textEndX), dir\ end\ \ -- Center conduit's connector: no bend, name+rate typed directly into\ -- the wire, which continues under the shape (see wireBg) up to its own edge.\ local function drawCenterConnector(target, blockX, lineCoreX, textLimitX, row, color, label, wireBg)\ local dir = (lineCoreX >= blockX) and 1 or -1\ local totalWidth = math.abs(lineCoreX - blockX) + 1\ local leadIn = math.min(CONNECTOR_STEP, totalWidth)\ local leadEndX = blockX + dir * (leadIn - 1)\ \ drawHBar(target, blockX, leadEndX, row, color, wireBg)\ \ local remaining = totalWidth - leadIn\ if remaining > 0 then\ local labelStartX = leadEndX + dir\ local textRoom\ if dir == 1 then\ textRoom = math.max(0, textLimitX - labelStartX + 1)\ else\ textRoom = math.max(0, labelStartX - textLimitX + 1)\ end\ local consumed = math.min(#label, remaining, textRoom)\ if consumed > 0 then\ writeInset(target, labelStartX, labelStartX + dir * (consumed - 1), row,\ label, color, state.theme.main.bg)\ end\ \ local afterLabelX = labelStartX + dir * consumed\ if remaining - consumed > 0 then\ drawHBar(target, afterLabelX, lineCoreX, row, color, wireBg)\ end\ end\ end\ \ -- Ring 1 keeps the full step distance (shape's still near its widest\ -- there); rings 2+ get progressively less as the shape's taper eats the gap.\ local function ringRowDistance(ring, increment)\ local reduction = 0\ if ring == 2 then\ reduction = 1\ elseif ring >= 3 then\ reduction = 2\ end\ return ring * increment - reduction\ end\ \ -- Rule of thumb (not derived from SLOT_HEIGHT/ringRowDistance), tuned\ -- by eye against how the widget actually looks at various sizes.\ local function demoCountFitsHeight(h, count)\ return (h - 8) > (count * 2)\ end\ \ local function clampCountToFit(h, desiredCount)\ if desiredCount <= 0 then return 0 end\ local count = SLOT_COUNT_STEPS[1]\ for i = #SLOT_COUNT_STEPS, 1, -1 do\ local step = SLOT_COUNT_STEPS[i]\ if step <= desiredCount and demoCountFitsHeight(h, step) then\ count = step\ break\ end\ end\ return count\ end\ \ -- Draws every conduit slot's block/wire/label and returns wireBg (a\ -- {[row]={[col]=color}} grid) so the shape's later transparent render\ -- can blend its boundary cells into whatever wire runs underneath them.\ local function drawConduitSlots(ctx, target, w, h, canvasTopRow, canvasRows, editMode, canvas, shapeGeom)\ local wireBg = {}\ local desiredCount = state.demoMode and MAX_SLOTS_PER_SIDE or state.conduitDisplay.activeCount\ local count = clampCountToFit(h, desiredCount)\ if count <= 0 and not editMode then return wireBg end\ \ local centerRow = canvasTopRow + math.floor(canvasRows / 2)\ local ringsPerSide = math.floor(count / 2)\ local increment = fittedIncrement(state.conduitDisplay.spacing, ringsPerSide, canvasRows)\ local canvasRowOffset = canvasTopRow - 1\ \ for _, side in ipairs({ \"i\", \"o\" }) do\ for k = 1, count do\ local ring, sign = slotRingSign(k)\ local blockRow = centerRow + sign * ringRowDistance(ring, increment)\ local bendRow = (ring > 0) and (blockRow - sign) or nil\ local rateRow = (ring > 0) and (bendRow - sign) or nil\ \ if rowInBounds(blockRow, canvasTopRow, canvasRows) then\ local blockWidth = outerWidth(ring)\ local x = (side == \"i\") and 1 or (w - blockWidth + 1)\ local blockEdgeX = (side == \"i\") and (x + blockWidth - 1) or x\ \ local c = state.demoMode and state.demoConduits[side][k]\ or getConduitByName(state.conduitDisplay[side][k])\ local bendOK = ring > 0 and rowInBounds(bendRow, canvasTopRow, canvasRows)\ local rateOK = ring > 0 and rowInBounds(rateRow, canvasTopRow, canvasRows)\ \ if c then\ local rate = energy.getMeterRate(c.meterName)\ local rateText = rate and (compactEnergy(rate) .. \"/t\") or \"off\"\ local displayName = editMode and (side:upper() .. k .. \": \" .. c.name) or c.name\ \ drawHBar(target, x, x + blockWidth - 1, blockRow, c.color)\ \ if ring == 0 then\ local lineCoreX = edgeAt(canvas, shapeGeom, canvasRowOffset, blockRow, side)\ local dir = (side == \"i\") and 1 or -1\ if (dir == 1 and lineCoreX >= blockEdgeX) or (dir == -1 and lineCoreX <= blockEdgeX) then\ local textLimitX = lineCoreX - dir\ drawCenterConnector(target, blockEdgeX, lineCoreX, textLimitX, blockRow, c.color,\ displayName .. \" \" .. rateText, wireBg)\ end\ elseif bendOK then\ local bendLeft, bendRight, dir = drawConnector(target, canvas, shapeGeom,\ canvasRowOffset, blockEdgeX, side, blockRow, bendRow, ring, c.color, wireBg)\ \ if bendLeft then\ local left, right = bendLeft, bendRight\ if rateOK then\ local rateEdge = edgeAt(canvas, shapeGeom, canvasRowOffset, rateRow, side)\ local rateLimit = rateEdge - dir\ if dir == 1 then\ right = math.min(right, rateLimit)\ else\ left = math.max(left, rateLimit)\ end\ end\ if left <= right then\ writeInset(target, left, right, bendRow, displayName,\ c.color, state.theme.main.bg, dir)\ if rateOK then\ writeInset(target, left, right, rateRow, rateText,\ state.theme.main.bg, c.color, dir)\ end\ end\ end\ end\ elseif editMode then\ writeLabel(target, x, blockRow, blockWidth, (blockWidth >= 4) and (\"+\" .. side:upper() .. k) or \"+\",\ colors.gray, colors.lightGray)\ end\ \ if editMode then\ local top, bottom = blockRow, blockRow\ if bendOK then top, bottom = math.min(top, bendRow), math.max(bottom, bendRow) end\ if rateOK then top, bottom = math.min(top, rateRow), math.max(bottom, rateRow) end\ local hitW = math.max(blockWidth, 6)\ local hitX = (side == \"i\") and x or (x + blockWidth - hitW)\ addButton(ctx, hitX, top, hitX + hitW - 1, bottom, function()\ state.editingSlot = { side = side, index = k, swap = c ~= nil }\ end)\ elseif c and not state.demoMode then\ local top, bottom = blockRow, blockRow\ if bendOK then top, bottom = math.min(top, bendRow), math.max(bottom, bendRow) end\ if rateOK then top, bottom = math.min(top, rateRow), math.max(bottom, rateRow) end\ local hitW = math.max(blockWidth, 6)\ local hitX = (side == \"i\") and x or (x + blockWidth - hitW)\ addButton(ctx, hitX, top, hitX + hitW - 1, bottom, function()\ for idx, conduit in ipairs(state.conduits) do\ if conduit == c then\ state.conduitDetailIndex = idx\ break\ end\ end\ state.settingsView = \"conduits\"\ state.conduitView = \"detail\"\ state.showOptions = true\ end)\ end\ end\ end\ end\ \ return wireBg\ end\ \ local function drawOptionButtonRow(ctx, target, w, row, label, values, current, onSelect)\ local labelText = label .. \": \"\ local buttonTexts = {}\ local totalWidth = #labelText\ for i, v in ipairs(values) do\ local text = \" \" .. tostring(v) .. \" \"\ buttonTexts[i] = text\ totalWidth = totalWidth + #text + 1\ end\ totalWidth = totalWidth - 1\ \ local col = math.max(1, math.floor((w - totalWidth) / 2) + 1)\ \ target.setCursorPos(1, row)\ target.setBackgroundColor(state.theme.main.bg)\ target.clearLine()\ target.setCursorPos(col, row)\ target.setTextColor(colors.white)\ target.write(labelText)\ \ local x = col + #labelText\ for i, v in ipairs(values) do\ local text = buttonTexts[i]\ local selected = (v == current)\ target.setCursorPos(x, row)\ target.setBackgroundColor(selected and state.theme.window.buttonFill or colors.gray)\ target.setTextColor(selected and state.theme.window.buttonText or colors.lightGray)\ target.write(text)\ addButton(ctx, x, row, x + #text - 1, row, function() onSelect(v) end)\ x = x + #text + 1\ end\ end\ \ local function drawSlotPicker(ctx, w, h)\ local slot = state.editingSlot\ local wantDir = (slot.side == \"i\") and \"in\" or \"out\"\ local frame = state.theme.window.frame\ local btnBg = state.theme.topBar.bar\ local btnText = state.theme.topBar.text\ \ -- Modal: drop every hitbox already registered this frame (from the\ -- screen underneath) so a tap here can't fall through onto one of those.\ for i = #ctx.buttons, 1, -1 do\ ctx.buttons[i] = nil\ end\ \ local matches = {}\ for _, c in ipairs(state.conduits) do\ if (c.direction or \"in\") == wantDir then table.insert(matches, c) end\ end\ \ local ow = math.min(w, math.max(30, math.floor(w * 0.7)))\ local oh = math.min(h - 2, math.max(15, math.floor(h * 0.85)))\ local ox = math.floor((w - ow) / 2) + 1\ local oy = math.max(2, math.floor((h - oh) / 2) + 1)\ \ local outer = window.create(ctx.buffer, ox, oy, ow, oh, false)\ outer.setBackgroundColor(colors.black)\ outer.clear()\ Render.drawThinBorder(outer, ow, oh, frame)\ \ local titleText = \" Pick a conduit for \" .. slot.side:upper() .. slot.index .. \" \"\ if #titleText > ow - 4 then titleText = titleText:sub(1, ow - 4) end\ local titleX = math.max(2, math.floor((ow - #titleText) / 2) + 1)\ outer.setCursorPos(titleX, 1)\ outer.setBackgroundColor(frame)\ outer.setTextColor(colors.white)\ outer.write(titleText)\ \ local closeLabel = \"[X]\"\ local closeX = ow - #closeLabel - 1\ outer.setCursorPos(closeX, 1)\ outer.setBackgroundColor(colors.red)\ outer.setTextColor(colors.white)\ outer.write(closeLabel)\ addButton(ctx, ox + closeX - 1, oy, ox + closeX + #closeLabel - 2, oy, function()\ state.editingSlot = nil\ end)\ \ local iw, ih = ow - 2, oh - 2\ local win = window.create(outer, 2, 2, iw, ih, false)\ win.setBackgroundColor(CONTENT_BG)\ win.setTextColor(colors.white)\ win.clear()\ \ local row = 1\ local clearLabel = \" Clear / Empty \"\ local clearCol = math.max(1, math.floor((iw - #clearLabel) / 2) + 1)\ win.setCursorPos(clearCol, row)\ win.setBackgroundColor(btnBg)\ win.setTextColor(btnText)\ win.write(clearLabel)\ addButton(ctx, ox + clearCol, oy + row, ox + clearCol + #clearLabel - 1, oy + row, function()\ state.conduitDisplay[slot.side][slot.index] = nil\ saveSettingsNow()\ state.editingSlot = nil\ end)\ win.setBackgroundColor(CONTENT_BG)\ row = row + 2\ \ local listTop = row\ local ROWS_PER_ITEM = 3\ local roomWithoutNav = ih - listTop + 1\ local needsPaging = #matches > math.floor(roomWithoutNav / ROWS_PER_ITEM)\ local roomForList = needsPaging and (roomWithoutNav - 2) or roomWithoutNav\ local perPage = math.max(1, math.floor(roomForList / ROWS_PER_ITEM))\ \ -- Page state lives on ctx (per-screen), not on the shared slot\ -- table - otherwise whichever screen redraws last clamps the page for every screen.\ local slotKey = slot.side .. slot.index\ if ctx.slotPickerFor ~= slotKey then\ ctx.slotPickerFor = slotKey\ ctx.slotPickerPage = 1\ end\ \ local numPages = math.max(1, math.ceil(#matches / perPage))\ ctx.slotPickerPage = math.max(1, math.min(ctx.slotPickerPage or 1, numPages))\ local startIdx = (ctx.slotPickerPage - 1) * perPage + 1\ local endIdx = math.min(#matches, startIdx + perPage - 1)\ \ for i = startIdx, endIdx do\ local c = matches[i]\ \ local maxLabel = math.max(1, iw - 6)\ local nameLine = c.name\ if #nameLine > maxLabel then nameLine = nameLine:sub(1, maxLabel) end\ local meterLine = c.meterName\ if #meterLine > maxLabel then meterLine = meterLine:sub(1, maxLabel) end\ \ local rowW = math.min(iw - 2, math.max(#nameLine + 4, #meterLine + 4, 12))\ local rowCol = math.max(1, math.floor((iw - rowW) / 2) + 1)\ \ win.setBackgroundColor(btnBg)\ win.setCursorPos(rowCol, row)\ win.write(string.rep(\" \", rowW))\ win.setCursorPos(rowCol, row + 1)\ win.write(string.rep(\" \", rowW))\ \ win.setCursorPos(rowCol + 1, row)\ win.setTextColor(c.color)\ win.write(string.char(7))\ win.setTextColor(btnText)\ win.write(\" \" .. nameLine)\ \ win.setCursorPos(rowCol + 2, row + 1)\ win.setTextColor(colors.lightGray)\ win.write(meterLine)\ win.setBackgroundColor(CONTENT_BG)\ \ addButton(ctx, ox + rowCol, oy + row, ox + rowCol + rowW - 1, oy + row + 1, function()\ clearConduitFromDisplay(c.name)\ state.conduitDisplay[slot.side][slot.index] = c.name\ saveSettingsNow()\ state.editingSlot = nil\ end)\ row = row + ROWS_PER_ITEM\ end\ \ if needsPaging then\ local navRow = ih\ local prevLabel = \"< Prev\"\ local pageLabel = \"Page \" .. ctx.slotPickerPage .. \"/\" .. numPages\ local nextLabel = \"Next >\"\ local totalNavWidth = #prevLabel + 2 + #pageLabel + 2 + #nextLabel\ local navCol = math.max(1, math.floor((iw - totalNavWidth) / 2) + 1)\ \ win.setBackgroundColor(CONTENT_BG)\ win.setCursorPos(1, navRow)\ win.clearLine()\ \ local x = navCol\ if ctx.slotPickerPage > 1 then\ win.setCursorPos(x, navRow)\ win.setBackgroundColor(btnBg)\ win.setTextColor(btnText)\ win.write(prevLabel)\ addButton(ctx, ox + x, oy + navRow, ox + x + #prevLabel - 1, oy + navRow, function()\ ctx.slotPickerPage = ctx.slotPickerPage - 1\ end)\ end\ x = x + #prevLabel + 2\ \ win.setCursorPos(x, navRow)\ win.setBackgroundColor(CONTENT_BG)\ win.setTextColor(colors.lightGray)\ win.write(pageLabel)\ x = x + #pageLabel + 2\ \ if ctx.slotPickerPage < numPages then\ win.setCursorPos(x, navRow)\ win.setBackgroundColor(btnBg)\ win.setTextColor(btnText)\ win.write(nextLabel)\ addButton(ctx, ox + x, oy + navRow, ox + x + #nextLabel - 1, oy + navRow, function()\ ctx.slotPickerPage = ctx.slotPickerPage + 1\ end)\ end\ end\ \ outer.setVisible(true)\ win.setVisible(true)\ end\ \ local function drawSlotSwapPicker(ctx, w, h)\ local slot = state.editingSlot\ local frame = state.theme.window.frame\ local btnBg = state.theme.topBar.bar\ local btnText = state.theme.topBar.text\ \ for i = #ctx.buttons, 1, -1 do\ ctx.buttons[i] = nil\ end\ \ local count = state.demoMode and MAX_SLOTS_PER_SIDE or state.conduitDisplay.activeCount\ local others = {}\ for k = 1, count do\ if k ~= slot.index then table.insert(others, k) end\ end\ \ local ow = math.min(w, math.max(26, math.floor(w * 0.6)))\ local oh = math.min(h - 2, math.max(13, math.floor(h * 0.7)))\ local ox = math.floor((w - ow) / 2) + 1\ local oy = math.max(2, math.floor((h - oh) / 2) + 1)\ \ local outer = window.create(ctx.buffer, ox, oy, ow, oh, false)\ outer.setBackgroundColor(colors.black)\ outer.clear()\ Render.drawThinBorder(outer, ow, oh, frame)\ \ local titleText = \" Swap \" .. slot.side:upper() .. slot.index .. \" with... \"\ if #titleText > ow - 4 then titleText = titleText:sub(1, ow - 4) end\ local titleX = math.max(2, math.floor((ow - #titleText) / 2) + 1)\ outer.setCursorPos(titleX, 1)\ outer.setBackgroundColor(frame)\ outer.setTextColor(colors.white)\ outer.write(titleText)\ \ local closeLabel = \"[X]\"\ local closeX = ow - #closeLabel - 1\ outer.setCursorPos(closeX, 1)\ outer.setBackgroundColor(colors.red)\ outer.setTextColor(colors.white)\ outer.write(closeLabel)\ addButton(ctx, ox + closeX - 1, oy, ox + closeX + #closeLabel - 2, oy, function()\ state.editingSlot = nil\ end)\ \ local iw, ih = ow - 2, oh - 2\ local win = window.create(outer, 2, 2, iw, ih, false)\ win.setBackgroundColor(CONTENT_BG)\ win.setTextColor(colors.white)\ win.clear()\ \ local row = 1\ local ROWS_PER_ITEM = 2\ local roomWithoutNav = ih - row + 1\ local needsPaging = #others > math.floor(roomWithoutNav / ROWS_PER_ITEM)\ local roomForList = needsPaging and (roomWithoutNav - 2) or roomWithoutNav\ local perPage = math.max(1, math.floor(roomForList / ROWS_PER_ITEM))\ \ local slotKey = slot.side .. slot.index\ if ctx.slotSwapFor ~= slotKey then\ ctx.slotSwapFor = slotKey\ ctx.slotSwapPage = 1\ end\ \ local numPages = math.max(1, math.ceil(#others / perPage))\ ctx.slotSwapPage = math.max(1, math.min(ctx.slotSwapPage or 1, numPages))\ local startIdx = (ctx.slotSwapPage - 1) * perPage + 1\ local endIdx = math.min(#others, startIdx + perPage - 1)\ \ for i = startIdx, endIdx do\ local k = others[i]\ local otherName = state.conduitDisplay[slot.side][k]\ local otherC = getConduitByName(otherName)\ local label = slot.side:upper() .. k .. \": \" .. (otherC and otherC.name or \"empty\")\ local maxLabel = math.max(1, iw - 4)\ if #label > maxLabel then label = label:sub(1, maxLabel) end\ \ local rowW = math.min(iw - 2, math.max(#label + 4, 12))\ local rowCol = math.max(1, math.floor((iw - rowW) / 2) + 1)\ \ win.setBackgroundColor(btnBg)\ win.setCursorPos(rowCol, row)\ win.write(string.rep(\" \", rowW))\ win.setCursorPos(rowCol + 1, row)\ win.setTextColor(otherC and otherC.color or btnText)\ win.write(otherC and string.char(7) or \" \")\ win.setTextColor(btnText)\ win.write(\" \" .. label)\ win.setBackgroundColor(CONTENT_BG)\ \ addButton(ctx, ox + rowCol, oy + row, ox + rowCol + rowW - 1, oy + row, function()\ local slotSide = state.conduitDisplay[slot.side]\ slotSide[slot.index], slotSide[k] = slotSide[k], slotSide[slot.index]\ saveSettingsNow()\ state.editingSlot = nil\ end)\ row = row + ROWS_PER_ITEM\ end\ \ if needsPaging then\ local navRow = ih\ local prevLabel = \"< Prev\"\ local pageLabel = \"Page \" .. ctx.slotSwapPage .. \"/\" .. numPages\ local nextLabel = \"Next >\"\ local totalNavWidth = #prevLabel + 2 + #pageLabel + 2 + #nextLabel\ local navCol = math.max(1, math.floor((iw - totalNavWidth) / 2) + 1)\ \ win.setBackgroundColor(CONTENT_BG)\ win.setCursorPos(1, navRow)\ win.clearLine()\ \ local x = navCol\ if ctx.slotSwapPage > 1 then\ win.setCursorPos(x, navRow)\ win.setBackgroundColor(btnBg)\ win.setTextColor(btnText)\ win.write(prevLabel)\ addButton(ctx, ox + x, oy + navRow, ox + x + #prevLabel - 1, oy + navRow, function()\ ctx.slotSwapPage = ctx.slotSwapPage - 1\ end)\ end\ x = x + #prevLabel + 2\ \ win.setCursorPos(x, navRow)\ win.setBackgroundColor(CONTENT_BG)\ win.setTextColor(colors.lightGray)\ win.write(pageLabel)\ x = x + #pageLabel + 2\ \ if ctx.slotSwapPage < numPages then\ win.setCursorPos(x, navRow)\ win.setBackgroundColor(btnBg)\ win.setTextColor(btnText)\ win.write(nextLabel)\ addButton(ctx, ox + x, oy + navRow, ox + x + #nextLabel - 1, oy + navRow, function()\ ctx.slotSwapPage = ctx.slotSwapPage + 1\ end)\ end\ end\ \ outer.setVisible(true)\ win.setVisible(true)\ end\ \ local STAT_ROWS = 2\ \ local function drawWidgetScreen(ctx, w, h)\ local target = ctx.buffer\ \ local style = detectStyle()\ local outline = state.theme.storage.outline or style.outline\ local fill = state.theme.storage.fill or style.fill\ local shape = state.theme.storage.shape or style.shape\ local info = energy.getStorageInfo()\ local throughput = energy.getStorageThroughput()\ \ if state.demoMode then\ info = { stored = info.stored, max = info.max, percentage = state.demoPercentage }\ end\ \ local canvasRows = math.max(1, h - 1 - STAT_ROWS)\ local canvas = Render.Canvas.new(w, canvasRows)\ canvas:clear(state.theme.main.bg)\ \ local targetDiameter = canvas.height * 0.8\ local size = (shape == \"battery\") and (targetDiameter / 3.2) or (targetDiameter / 2)\ size = math.min(size, canvas.width * 0.45)\ \ local cx, cy = canvas.width / 2, canvas.height / 2\ \ local shapeGeom = { shape = shape, cx = cx, cy = cy, size = size }\ \ if shape == \"circle\" then\ Render.drawCircle(canvas, cx, cy, size, outline, info.percentage, fill)\ elseif shape == \"battery\" then\ Render.drawBattery(canvas, cx, cy, size, outline, info.percentage, fill)\ else\ Render.drawSquare(canvas, cx, cy, size, outline, info.percentage, fill)\ end\ \ target.setBackgroundColor(state.theme.main.bg)\ for row = 2, 1 + canvasRows do\ target.setCursorPos(1, row)\ target.clearLine()\ end\ \ local wireBg = drawConduitSlots(ctx, target, w, h, 2, canvasRows, state.editingConduitDisplay, canvas, shapeGeom)\ \ -- Shape renders transparent, on top of the wires: only its own\ -- painted pixels overwrite them, and bgOverride resolves any\ -- partially-painted boundary cell to the wire color behind it.\ canvas:render(target, 1, true, function(canvasRow, col)\ local row = canvasRow + 1\ local rowGrid = wireBg[row]\ return rowGrid and rowGrid[col]\ end)\ \ if state.editingConduitDisplay then\ local baseRow = 1 + canvasRows\ local cd = state.conduitDisplay\ drawOptionButtonRow(ctx, target, w, baseRow + 1, \"Slots\", SLOT_COUNT_STEPS, cd.activeCount,\ function(v)\ cd.activeCount = v\ saveSettingsNow()\ end)\ drawOptionButtonRow(ctx, target, w, baseRow + 2, \"Spacing\", SPACING_STEPS, cd.spacing,\ function(v)\ cd.spacing = v\ saveSettingsNow()\ end)\ \ if state.editingSlot then\ if state.editingSlot.swap then\ drawSlotSwapPicker(ctx, w, h)\ else\ drawSlotPicker(ctx, w, h)\ end\ end\ return\ end\ \ local function centered(row, text, fg)\ local col = math.max(1, math.floor((w - #text) / 2) + 1)\ target.setCursorPos(col, row)\ target.setBackgroundColor(state.theme.main.bg)\ target.setTextColor(fg or colors.white)\ target.clearLine()\ target.write(text)\ end\ \ local function centeredSegments(row, segments)\ local total = 0\ for _, seg in ipairs(segments) do total = total + #seg.text end\ target.setBackgroundColor(state.theme.main.bg)\ target.setCursorPos(1, row)\ target.clearLine()\ local col = math.max(1, math.floor((w - total) / 2) + 1)\ for _, seg in ipairs(segments) do\ target.setCursorPos(col, row)\ target.setTextColor(seg.fg or colors.white)\ target.write(seg.text)\ col = col + #seg.text\ end\ end\ \ local baseRow = 1 + canvasRows\ local net = throughput.net\ local netColor = net > 0 and colors.lime or (net < 0 and colors.red or colors.lightGray)\ local sign = net > 0 and \"+\" or (net < 0 and \"-\" or \"\")\ centeredSegments(baseRow + 1, {\ { text = \"IN: \" },\ { text = compactEnergy(throughput.input) .. \"/t \" },\ { text = sign .. compactEnergy(math.abs(net)) .. \"/t \", fg = netColor },\ { text = \"OUT: \" },\ { text = compactEnergy(throughput.output) .. \"/t\" },\ })\ \ local etaLabel, etaSeconds\ if net > 0 then\ etaLabel = \"Full in\"\ etaSeconds = (info.max - info.stored) / (net * 20)\ elseif net < 0 then\ etaLabel = \"Empty in\"\ etaSeconds = info.stored / (-net * 20)\ else\ etaLabel = \"Stable\"\ etaSeconds = math.huge\ end\ local etaText = etaSeconds == math.huge and etaLabel or (etaLabel .. \": \" .. formatTime(etaSeconds))\ centered(baseRow + 2, etaText, colors.lightGray)\ \ local inColors, outColors = {}, {}\ local totalIn, totalOut = 0, 0\ for _, c in ipairs(state.conduits) do\ local rate = energy.getMeterRate(c.meterName) or 0\ if rate > 0 then\ if c.direction == \"out\" then\ outColors[c.color] = (outColors[c.color] or 0) + rate\ totalOut = totalOut + rate\ else\ inColors[c.color] = (inColors[c.color] or 0) + rate\ totalIn = totalIn + rate\ end\ end\ end\ \ local BAR_CHAR = string.char(140)\ local function drawProportionalBar(x, width, amounts, total, dir)\ if width <= 0 or total <= 0 then return end\ \ local sorted = {}\ for color, amt in pairs(amounts) do\ table.insert(sorted, { color = color, amt = amt })\ end\ table.sort(sorted, function(a, b)\ if a.amt == b.amt then return a.color < b.color end\ return a.amt > b.amt\ end)\ \ -- Largest-remainder rounding: each share gets its exact fraction\ -- of what's left, rounded, so small shares still round up to 1 column when there's room.\ local remainingWidth, remainingTotal = width, total\ local cx = (dir == \"left\") and (x + width - 1) or x\ for _, item in ipairs(sorted) do\ if remainingTotal <= 0 or remainingWidth <= 0 then break end\ local cellWidth = math.min(remainingWidth, math.floor(item.amt / remainingTotal * remainingWidth + 0.5))\ if cellWidth > 0 then\ local startX = (dir == \"left\") and (cx - cellWidth + 1) or cx\ target.setCursorPos(startX, baseRow + 2)\ target.setBackgroundColor(state.theme.main.bg)\ target.setTextColor(item.color)\ target.write(string.rep(BAR_CHAR, cellWidth))\ cx = cx + ((dir == \"left\") and -cellWidth or cellWidth)\ remainingWidth = remainingWidth - cellWidth\ end\ remainingTotal = remainingTotal - item.amt\ end\ end\ \ local etaCol = math.max(1, math.floor((w - #etaText) / 2) + 1)\ local rightBarX = etaCol + #etaText + 1\ drawProportionalBar(2, etaCol - 3, inColors, totalIn, \"right\")\ drawProportionalBar(rightBarX, w - rightBarX, outColors, totalOut, \"left\")\ end\ \ return {\ drawTopBar = drawTopBar,\ drawWidgetScreen = drawWidgetScreen,\ }\ \ end" files["dlib/energy.lua"] = "local energy = {}\ \ local stores = {}\ local meters = {}\ local flowGates = {}\ \ local history = {\ stored = 0,\ maxStored = 0,\ lastTime = os.clock(),\ rate = 0,\ }\ \ -- Polled once per energy.update(), not live per call, so the displayed\ -- rate only ever changes on the Rendering menu's Update Speed timer.\ local meterRates = {}\ \ local function wrapDraconicStorage(name)\ return {\ poll = function()\ local ok, stored, max, input, output = pcall(function()\ return\ peripheral.call(name, \"getEnergyStored\") or 0,\ peripheral.call(name, \"getMaxEnergyStored\") or 0,\ peripheral.call(name, \"getInputPerTick\") or 0,\ peripheral.call(name, \"getOutputPerTick\") or 0\ end)\ if not ok then return nil end\ return { stored = stored, max = max, input = input, output = output }\ end,\ }\ end\ \ local function wrapInductionPort(name)\ return {\ poll = function()\ local ok, storedNow, needed = pcall(function()\ return\ peripheral.call(name, \"getEnergy\") or 0,\ peripheral.call(name, \"getEnergyNeeded\") or 0\ end)\ if not ok then return nil end\ return { stored = storedNow, max = storedNow + needed, input = 0, output = 0 }\ end,\ }\ end\ \ function energy.init()\ stores = {}\ meters = {}\ flowGates = {}\ meterRates = {}\ \ for _, name in ipairs(peripheral.getNames()) do\ local pType = peripheral.getType(name)\ \ if pType == \"draconic_rf_storage\" then\ table.insert(stores, wrapDraconicStorage(name))\ elseif pType == \"inductionPort\" then\ table.insert(stores, wrapInductionPort(name))\ elseif pType == \"energymeter\" then\ table.insert(meters, { name = name, peripheral = peripheral.wrap(name) })\ elseif pType == \"flow_gate\" then\ table.insert(flowGates, { name = name, peripheral = peripheral.wrap(name) })\ end\ end\ \ energy.update()\ end\ \ function energy.update()\ local totalStored, totalMax = 0, 0\ \ for _, store in ipairs(stores) do\ local snapshot = store.poll()\ if snapshot then\ totalStored = totalStored + snapshot.stored\ totalMax = totalMax + snapshot.max\ end\ end\ \ for _, m in ipairs(meters) do\ local ok, rate = pcall(m.peripheral.getEnergyRate)\ meterRates[m.name] = ok and (rate or 0) or nil\ end\ \ local now = os.clock()\ local dt = now - history.lastTime\ \ -- Only `rate` needs dt>0 to divide by; stored/maxStored must update\ -- every call or a same-tick double poll would drop a fresh reading.\ if dt > 0 then\ history.rate = (totalStored - history.stored) / (dt * 20)\ history.lastTime = now\ end\ history.stored = totalStored\ history.maxStored = totalMax\ end\ \ function energy.getStorageInfo()\ return {\ stored = history.stored,\ max = history.maxStored,\ percentage = history.maxStored > 0 and (history.stored / history.maxStored) * 100 or 0,\ }\ end\ \ function energy.getMeteredRate()\ local totalRate = 0\ for _, meter in ipairs(meters) do\ totalRate = totalRate + (meterRates[meter.name] or 0)\ end\ return totalRate\ end\ \ function energy.getStorageThroughput()\ local input, output = 0, 0\ for _, store in ipairs(stores) do\ local snapshot = store.poll()\ if snapshot then\ input = input + snapshot.input\ output = output + snapshot.output\ end\ end\ return {\ input = input,\ output = output,\ net = input - output,\ }\ end\ \ local function applyFlowOverride(gate, limit)\ local p = gate.peripheral\ return pcall(function()\ if not p.getOverrideEnabled() then\ p.setOverrideEnabled(true)\ end\ p.setFlowOverride(limit)\ end)\ end\ \ function energy.setFlowLimit(limit, index)\ if index then\ if flowGates[index] then applyFlowOverride(flowGates[index], limit) end\ else\ for _, gate in ipairs(flowGates) do\ applyFlowOverride(gate, limit)\ end\ end\ end\ \ local function findMeter(name)\ for _, m in ipairs(meters) do\ if m.name == name then return m end\ end\ return nil\ end\ \ local function findGate(name)\ for _, g in ipairs(flowGates) do\ if g.name == name then return g end\ end\ return nil\ end\ \ function energy.getMeterNames()\ local names = {}\ for _, m in ipairs(meters) do\ table.insert(names, m.name)\ end\ return names\ end\ \ function energy.getFlowGateNames()\ local names = {}\ for _, g in ipairs(flowGates) do\ table.insert(names, g.name)\ end\ return names\ end\ \ function energy.getMeterRate(name)\ if not findMeter(name) then return nil end\ return meterRates[name]\ end\ \ function energy.getFlowGateStatus(name)\ local g = findGate(name)\ if not g then return nil end\ local p = g.peripheral\ local okE, enabled = pcall(p.getOverrideEnabled)\ local okL, limit = pcall(p.getFlowOverride)\ local okF, flow = pcall(p.getFlow)\ return {\ enabled = okE and enabled or false,\ limit = okL and limit or nil,\ flow = okF and flow or nil,\ }\ end\ \ function energy.setFlowLimitByName(name, limit)\ local g = findGate(name)\ if not g then return false end\ return applyFlowOverride(g, limit)\ end\ \ function energy.clearFlowLimitByName(name)\ local g = findGate(name)\ if not g then return false end\ local ok = pcall(g.peripheral.setOverrideEnabled, false)\ return ok\ end\ \ function energy.runLoop()\ energy.init()\ local timer = os.startTimer(0.05)\ \ while true do\ local event, p1 = os.pullEvent()\ if event == \"timer\" and p1 == timer then\ energy.update()\ timer = os.startTimer(0.05)\ elseif event == \"peripheral\" or event == \"peripheral_detach\" then\ energy.init()\ end\ end\ end\ \ return energy" files["dracore.lua"] = "local VERSION = \"0.42\"\ \ local App = {}\ App.VERSION = VERSION\ \ local CONTENT_BG = colors.black\ App.CONTENT_BG = CONTENT_BG\ \ local SLOT_COUNT_STEPS = { 5, 7, 9 }\ local MAX_SLOTS_PER_SIDE = 9\ App.SLOT_COUNT_STEPS = SLOT_COUNT_STEPS\ App.MAX_SLOTS_PER_SIDE = MAX_SLOTS_PER_SIDE\ \ local function formatEnergy(n)\ if n >= 10^18 then return string.format(\"%.1f ERF\", n / 10^18) end\ if n >= 10^15 then return string.format(\"%.1f PRF\", n / 10^15) end\ if n >= 10^12 then return string.format(\"%.1f TRF\", n / 10^12) end\ if n >= 10^9 then return string.format(\"%.1f GRF\", n / 10^9) end\ if n >= 10^6 then return string.format(\"%.1f MRF\", n / 10^6) end\ if n >= 10^3 then return string.format(\"%.1f kRF\", n / 10^3) end\ return n .. \" RF\"\ end\ \ local function compactEnergy(n)\ local sign = n < 0 and \"-\" or \"\"\ n = math.abs(n)\ if n >= 10^18 then return sign .. string.format(\"%.1fErf\", n / 10^18) end\ if n >= 10^15 then return sign .. string.format(\"%.1fPrf\", n / 10^15) end\ if n >= 10^12 then return sign .. string.format(\"%.1fTrf\", n / 10^12) end\ if n >= 10^9 then return sign .. string.format(\"%.1fGrf\", n / 10^9) end\ if n >= 10^6 then return sign .. string.format(\"%.1fMrf\", n / 10^6) end\ if n >= 10^3 then return sign .. string.format(\"%.1fkrf\", n / 10^3) end\ return sign .. n .. \"rf\"\ end\ \ local function formatTime(seconds)\ if seconds <= 0 or seconds == math.huge then return \"Infinite\" end\ local units = {\ {label = \"years\", sec = 31536000},\ {label = \"months\", sec = 2592000},\ {label = \"days\", sec = 86400},\ {label = \"hours\", sec = 3600},\ {label = \"minutes\", sec = 60}\ }\ local parts = {}\ for _, u in ipairs(units) do\ if seconds >= u.sec then\ local val = math.floor(seconds / u.sec)\ table.insert(parts, val .. \" \" .. u.label)\ seconds = seconds % u.sec\ end\ if #parts == 2 then break end\ end\ return #parts > 0 and table.concat(parts, \" \") or \"< 1 minute\"\ end\ \ App.formatEnergy = formatEnergy\ App.compactEnergy = compactEnergy\ App.formatTime = formatTime\ \ local SHAPE_STYLES = {\ draconic_rf_storage = {\ shape = \"circle\",\ outline = colors.red,\ fill = colors.orange,\ },\ inductionPort = {\ shape = \"square\",\ outline = colors.white,\ fill = colors.lime,\ },\ }\ \ local function detectStyle()\ local sawInduction = false\ for _, name in ipairs(peripheral.getNames()) do\ local pType = peripheral.getType(name)\ if pType == \"draconic_rf_storage\" then\ return SHAPE_STYLES.draconic_rf_storage\ elseif pType == \"inductionPort\" then\ sawInduction = true\ end\ end\ if sawInduction then\ return SHAPE_STYLES.inductionPort\ end\ return SHAPE_STYLES.draconic_rf_storage\ end\ \ App.SHAPE_STYLES = SHAPE_STYLES\ App.detectStyle = detectStyle\ \ local OptionsLib = dofile(\"dlib/options.lua\")\ local loaded = OptionsLib.loadInitialState(SLOT_COUNT_STEPS, MAX_SLOTS_PER_SIDE)\ \ local state = {\ showOptions = false,\ textScale = loaded.textScale,\ refreshInterval = loaded.refreshInterval,\ theme = loaded.theme,\ \ settingsView = \"grid\",\ settingsGridPage = { page = 1 },\ themeIndex = 1,\ \ conduits = loaded.conduits,\ conduitView = \"list\",\ conduitDetailIndex = nil,\ conduitListPage = { page = 1 },\ conduitSort = { key = nil, asc = true },\ wizard = nil,\ \ editingConduitDisplay = false,\ editingSlot = nil,\ conduitDisplay = loaded.conduitDisplay,\ \ demoMode = false,\ demoPercentage = 50,\ demoConduits = nil,\ }\ \ local function saveConduitsNow()\ OptionsLib.saveConduits(loaded.configData, state.conduits)\ end\ \ local function saveSettingsNow()\ OptionsLib.saveSettings(loaded.configData, {\ textScale = state.textScale,\ refreshInterval = state.refreshInterval,\ theme = state.theme,\ conduitDisplay = state.conduitDisplay,\ })\ end\ \ local function getConduitByName(name)\ if not name then return nil end\ for _, c in ipairs(state.conduits) do\ if c.name == name then return c end\ end\ return nil\ end\ \ local function findConduitByMeter(name)\ for _, c in ipairs(state.conduits) do\ if c.meterName == name then return c end\ end\ return nil\ end\ \ local function findConduitByFlowGate(name)\ for _, c in ipairs(state.conduits) do\ if c.flowGateName == name then return c end\ end\ return nil\ end\ \ local function withoutClaimed(names, ownerOf, exceptConduit)\ local out = {}\ for _, name in ipairs(names) do\ local owner = ownerOf(name)\ if not owner or owner == exceptConduit then\ table.insert(out, name)\ end\ end\ return out\ end\ \ local function clearConduitFromDisplay(name)\ for _, side in ipairs({ \"i\", \"o\" }) do\ for idx, assigned in pairs(state.conduitDisplay[side]) do\ if assigned == name then\ state.conduitDisplay[side][idx] = nil\ end\ end\ end\ end\ App.state = state\ App.saveConduitsNow = saveConduitsNow\ App.saveSettingsNow = saveSettingsNow\ App.getConduitByName = getConduitByName\ App.findConduitByMeter = findConduitByMeter\ App.findConduitByFlowGate = findConduitByFlowGate\ App.withoutClaimed = withoutClaimed\ App.clearConduitFromDisplay = clearConduitFromDisplay\ \ local screens = {}\ App.screens = screens\ \ local function wrapScreens()\ local list = {\ { target = term, name = \"term\", isMonitor = false, buttons = {} },\ }\ for _, name in ipairs(peripheral.getNames()) do\ if peripheral.getType(name) == \"monitor\" then\ table.insert(list, {\ target = peripheral.wrap(name),\ name = name,\ isMonitor = true,\ buttons = {},\ })\ end\ end\ for i = #screens, 1, -1 do screens[i] = nil end\ for i, s in ipairs(list) do screens[i] = s end\ end\ \ local function setupBuffers()\ for _, s in ipairs(screens) do\ local w, h = s.target.getSize()\ -- term itself is a redirect proxy, not a real terminal object;\ -- term.current() is the one window.create needs as a parent.\ local parent = s.isMonitor and s.target or term.current()\ s.buffer = window.create(parent, 1, 1, w, h, false)\ s.optionsWin, s.optionsContent, s.optionsGeom = nil, nil, nil\ end\ end\ App.setupBuffers = setupBuffers\ \ local function addButton(ctx, x1, y1, x2, y2, action)\ if not action then return end\ table.insert(ctx.buttons, { x1 = x1, y1 = y1, x2 = x2, y2 = y2, action = action })\ end\ App.addButton = addButton\ \ local function handleTouch(ctx, x, y)\ for _, b in ipairs(ctx.buttons) do\ if x >= b.x1 and x <= b.x2 and y >= b.y1 and y <= b.y2 then\ b.action()\ return\ end\ end\ end\ \ App.textEntry = nil\ \ local function startTextEntry(prompt, initial, numeric, onConfirm, onCancel)\ App.textEntry = {\ prompt = prompt,\ value = initial or \"\",\ numeric = numeric,\ onConfirm = onConfirm,\ onCancel = onCancel,\ }\ end\ App.startTextEntry = startTextEntry\ \ local function cancelTextEntry()\ if not App.textEntry then return end\ local cb = App.textEntry.onCancel\ App.textEntry = nil\ if cb then cb() end\ end\ App.cancelTextEntry = cancelTextEntry\ \ local function confirmTextEntry()\ if not App.textEntry then return end\ local raw = App.textEntry.value\ \ if App.textEntry.numeric then\ local n = tonumber(raw)\ if not n then return end\ local cb = App.textEntry.onConfirm\ App.textEntry = nil\ if cb then cb(n) end\ else\ if raw == \"\" then return end\ local cb = App.textEntry.onConfirm\ App.textEntry = nil\ if cb then cb(raw) end\ end\ end\ App.confirmTextEntry = confirmTextEntry\ \ local function closeOptions()\ if App.textEntry then cancelTextEntry() end\ state.showOptions = false\ end\ App.closeOptions = closeOptions\ \ local DEMO_COLORS = {\ colors.white, colors.orange, colors.magenta, colors.lightBlue, colors.yellow,\ colors.lime, colors.pink, colors.gray, colors.lightGray, colors.cyan,\ colors.purple, colors.blue, colors.brown, colors.green, colors.red,\ }\ \ local function startDemoMode()\ local demo = { i = {}, o = {} }\ for k = 1, MAX_SLOTS_PER_SIDE do\ demo.i[k] = { name = \"Conduit \" .. k, color = DEMO_COLORS[math.random(#DEMO_COLORS)], meterName = nil }\ demo.o[k] = { name = \"Conduit \" .. k, color = DEMO_COLORS[math.random(#DEMO_COLORS)], meterName = nil }\ end\ state.demoConduits = demo\ state.demoPercentage = App.energy.getStorageInfo().percentage\ state.demoMode = true\ state.showOptions = false\ end\ App.startDemoMode = startDemoMode\ \ local Render = dofile(\"dlib/render.lua\")(App)\ App.Render = Render\ \ local energy = dofile(\"dlib/energy.lua\")\ App.energy = energy\ \ local Options = OptionsLib.attach(App)\ local Main = dofile(\"dlib/main.lua\")(App)\ \ local function redrawScreen(ctx)\ ctx.buttons = {}\ local w, h = ctx.target.getSize()\ \ ctx.buffer.setVisible(false)\ \ if not state.showOptions then\ Options.hideOptionsWindow(ctx)\ end\ \ Main.drawTopBar(ctx, w)\ \ if not state.showOptions then\ Main.drawWidgetScreen(ctx, w, h)\ end\ \ if state.showOptions then\ Options.drawOptionsScreen(ctx, w, h)\ end\ \ ctx.buffer.setVisible(true)\ end\ \ local function redraw()\ for _, ctx in ipairs(screens) do\ redrawScreen(ctx)\ end\ end\ \ local function main()\ wrapScreens()\ for _, s in ipairs(screens) do\ if s.isMonitor then s.target.setTextScale(state.textScale) end\ end\ setupBuffers()\ energy.init()\ \ local dirty = true\ local lastPoll = os.clock()\ \ local function pollIfDue()\ local now = os.clock()\ if now - lastPoll >= state.refreshInterval then\ energy.update()\ lastPoll = now\ return true\ end\ return false\ end\ \ local function inputLoop()\ while true do\ local event, a, b, c = os.pullEvent()\ \ if event == \"monitor_touch\" then\ for _, s in ipairs(screens) do\ if s.isMonitor and s.name == a then\ handleTouch(s, b, c)\ dirty = true\ break\ end\ end\ \ elseif event == \"mouse_click\" and a == 1 then\ for _, s in ipairs(screens) do\ if not s.isMonitor then\ handleTouch(s, b, c)\ dirty = true\ break\ end\ end\ \ elseif event == \"peripheral\" or event == \"peripheral_detach\" then\ energy.init()\ lastPoll = os.clock()\ wrapScreens()\ for _, s in ipairs(screens) do\ if s.isMonitor then s.target.setTextScale(state.textScale) end\ end\ setupBuffers()\ dirty = true\ \ elseif event == \"char\" and App.textEntry then\ App.textEntry.value = App.textEntry.value .. a\ dirty = true\ \ elseif event == \"key\" and App.textEntry then\ if a == keys.backspace then\ App.textEntry.value = App.textEntry.value:sub(1, -2)\ dirty = true\ elseif a == keys.enter or a == keys.numPadEnter then\ confirmTextEntry()\ dirty = true\ elseif a == keys.escape then\ cancelTextEntry()\ dirty = true\ end\ \ elseif event == \"key\" and state.demoMode then\ if a == keys.left then\ state.demoPercentage = math.max(0, state.demoPercentage - 1)\ dirty = true\ elseif a == keys.right then\ state.demoPercentage = math.min(100, state.demoPercentage + 1)\ dirty = true\ elseif a == keys.down then\ state.demoPercentage = math.max(0, state.demoPercentage - 0.01)\ dirty = true\ elseif a == keys.up then\ state.demoPercentage = math.min(100, state.demoPercentage + 0.01)\ dirty = true\ end\ end\ end\ end\ \ -- Two coroutines instead of one loop, so a slow multi-monitor\ -- redraw can never leave touch/key input queued behind it.\ local function renderLoop()\ while true do\ if pollIfDue() then dirty = true end\ if dirty then\ redraw()\ dirty = false\ end\ sleep(0.05)\ end\ end\ \ redraw()\ parallel.waitForAny(inputLoop, renderLoop)\ end\ \ local ok, err = pcall(main)\ \ term.setBackgroundColor(colors.black)\ term.setTextColor(colors.white)\ term.clear()\ term.setCursorPos(1, 1)\ \ if not ok then\ error(err, 0)\ end" local VERSION = "0.42" local SYS_NAME = "dracore" local MAIN_EXEC = "dracore.lua" local CONF_OPTS = { "dracore.conf", } local term = term local monitor = peripheral.find("monitor") local w, h = term.getSize() if monitor then monitor.setTextScale(1) end local multiTerm = {} local targets = {term} if monitor then table.insert(targets, monitor) end local functions = {"write", "scroll", "setCursorPos", "setCursorBlink", "setBackgroundColor", "setTextColor", "clear", "clearLine", "setTextScale"} for _, name in ipairs(functions) do multiTerm[name] = function(...) for _, t in ipairs(targets) do if t[name] then t[name](...) end end end end multiTerm.getSize = function() return term.getSize() end local term = multiTerm local function parseVersion(v) local parts = {} for num in string.gmatch(v or "", "%d+") do table.insert(parts, tonumber(num)) end return parts end local function compareVersions(v1, v2) local p1, p2 = parseVersion(v1), parseVersion(v2) for i = 1, math.max(#p1, #p2) do local n1, n2 = p1[i] or 0, p2[i] or 0 if n1 < n2 then return -1 end if n1 > n2 then return 1 end end return 0 end local installedVer = nil local mode = "INSTALL" if MAIN_EXEC and MAIN_EXEC ~= "" and fs.exists(MAIN_EXEC) then local f = fs.open(MAIN_EXEC, "r") if f then local content = f.readAll() or "" f.close() installedVer = content:match('local%s+VERSION%s*=%s*["\']([^"\']+)["\']') or content:match('VERSION%s*=%s*["\']([^"\']+)["\']') end end if installedVer then local cmp = compareVersions(installedVer, VERSION) if cmp > 0 then mode = "DOWNGRADE" elseif cmp < 0 then mode = "UPDATE" else mode = "REINSTALL" end else for path in pairs(files) do if fs.exists(path) then mode = "UPDATE" break end end end local function drawBackground() term.setBackgroundColor(colors.blue) term.clear() term.setCursorPos(1, 1) term.setTextColor(colors.white) term.write(SYS_NAME .. " " .. VERSION .. " Setup") term.setCursorPos(1, 2) term.write(string.rep("=", w)) term.setCursorPos(1, h) term.setBackgroundColor(colors.lightGray) term.setTextColor(colors.black) term.clearLine() term.write(" ENTER=Continue F3=Exit") end local function drawWindow(x, y, width, height, title) term.setBackgroundColor(colors.black) for i = 1, height do term.setCursorPos(x + 1, y + i) term.write(string.rep(" ", width)) end term.setBackgroundColor(colors.lightGray) term.setTextColor(colors.black) for i = 0, height - 1 do term.setCursorPos(x, y + i) term.write(string.rep(" ", width)) end term.setCursorPos(x, y); term.write(string.rep("-", width)) term.setCursorPos(x, y + height - 1); term.write(string.rep("-", width)) if title then term.setCursorPos(x + (width / 2) - (#title / 2) - 1, y) term.write(" " .. title .. " ") end end local function getSettings() local installStartup = true local resetList = {} for _, v in ipairs(CONF_OPTS) do if fs.exists(v) then resetList[v] = false end end while true do drawBackground() term.setBackgroundColor(colors.blue) term.setTextColor(colors.white) term.setCursorPos(5, 4) term.write("Configure " .. SYS_NAME .. " installation.") term.setCursorPos(5, 5) term.write("Your user data will not be affected.") local optCount = 1 for k, v in pairs(resetList) do optCount = optCount + 1 end local winH = optCount + 9 drawWindow(6, 7, 42, winH, "Configuration") local optX = 8 term.setCursorPos(optX, 8) term.setTextColor(colors.black) term.write("Installed: " .. (installedVer or "None") .. " | Target: " .. VERSION) term.setCursorPos(optX, 9) if mode == "DOWNGRADE" then term.setTextColor(colors.red) term.write("WARNING: Downgrade detected!") elseif mode == "UPDATE" then term.setTextColor(colors.black) term.write("Note: Update available") elseif mode == "REINSTALL" then term.setTextColor(colors.black) term.write("Note: Reinstalling current version") else term.setTextColor(colors.black) term.write("Note: Fresh installation") end local optY = 11 term.setCursorPos(optX, optY) term.setTextColor(colors.black) term.write(installStartup and "[X] Install/Update startup.lua" or "[ ] Skip startup.lua") local i = 1 local renderMap = {} for file, reset in pairs(resetList) do term.setCursorPos(optX, optY + i) term.write(reset and "[X] Reset " .. file or "[ ] Reset " .. file) renderMap[optY + i] = file i = i + 1 end local btnY = optY + i + 1 term.setBackgroundColor(colors.gray) term.setTextColor(colors.white) term.setCursorPos(optX, btnY); term.write(" " .. mode .. " ") term.setCursorPos(optX + 15, btnY); term.write(" CANCEL ") local ev = {os.pullEvent()} local event = ev[1] if event == "key" then local p1 = ev[2] if p1 == keys.enter then return true, installStartup, resetList elseif p1 == keys.f3 then return false end elseif event == "mouse_click" then local mx, my = ev[3], ev[4] if my == optY and mx >= optX and mx <= optX + 30 then installStartup = not installStartup elseif renderMap[my] then local fname = renderMap[my] resetList[fname] = not resetList[fname] elseif my == btnY then if mx >= optX and mx <= optX + 10 then return true, installStartup, resetList elseif mx >= optX + 15 and mx <= optX + 25 then return false end end end end end local proceed, installStartup, resetList = getSettings() if not proceed then term.setBackgroundColor(colors.black); term.clear(); term.setCursorPos(1,1) print("Setup cancelled."); return end for file, reset in pairs(resetList) do if reset and fs.exists(file) then fs.delete(file) end end drawBackground() drawWindow(8, 8, 36, 6, mode .. " IN PROGRESS") local total = 0 for _ in pairs(files) do total = total + 1 end if installStartup then total = total + 1 end local count = 0 for path, data in pairs(files) do count = count + 1 term.setCursorPos(10, 10) term.write("Writing: " .. path:sub(1, 20) .. "...") local progress = math.floor((count / total) * 30) term.setCursorPos(10, 12) term.setBackgroundColor(colors.gray); term.write(string.rep(" ", 30)) term.setCursorPos(10, 12) term.setBackgroundColor(colors.black); term.write(string.rep(" ", progress)) local dir = path:match("(.+)/") if dir and not fs.exists(dir) then fs.makeDir(dir) end local f = fs.open(path, "w") f.write(data) f.close() sleep(0.05) end if installStartup and MAIN_EXEC then count = count + 1 term.setCursorPos(10, 10) term.write("Writing: startup.lua...") local startupData = string.format([[ local target = %q local function getFiles() local files = {"shell"} for _, f in ipairs(fs.list("/")) do if not fs.isDir(f) and f ~= "startup.lua" and f ~= "shell" then table.insert(files, f) end end return files end local function setBar(y, text, w) term.setCursorPos(1, y) term.setBackgroundColor(colors.blue) term.setTextColor(colors.white) term.clearLine() term.setCursorPos(math.floor((w - #text) / 2) + 1, y) term.write(text) end local function save(new) local lines = {} local f = fs.open("startup.lua", "r") for line in function() return f.readLine() end do table.insert(lines, line:find("^local target") and 'local target = "'..new..'"' or line) end f.close() local f = fs.open("startup.lua", "w") for _, l in ipairs(lines) do f.write(l .. "\n") end f.close() os.reboot() end local w, h = term.getSize() local isCol = term.isColor() term.clear() term.setTextColor(colors.yellow) local lx = w - 14 term.setCursorPos(lx+2, 1) term.write(".______.") term.setCursorPos(lx, 2) term.write(" / \\") term.setCursorPos(lx, 3) term.write("|energy * |") term.setTextColor(colors.green) term.setCursorPos(lx, 4) term.write("____________") term.setTextColor(colors.lime) term.setCursorPos(lx, 5) term.write("EPA POL PREV") term.setTextColor(colors.white) term.setCursorPos(1, 1) term.write(os.version() .. " (C) 2026") term.setCursorPos(1, 2) term.write("ID: " .. os.getComputerID()) term.setCursorPos(1, 5) term.write("Proccessor: CraftCPU " .. (isCol and "64-Bit" or "32-Bit")) term.setCursorPos(1, 6) term.write("Arch: Lua 5.1 VM") term.setCursorPos(1, h) term.write("Press Q for BIOS") local step = isCol and 64 or 32 for i = 0, (isCol and 1024 or 512), step do term.setCursorPos(1, 8) term.write("Memory Test: " .. i .. "K OK") local t = os.startTimer(0.01) while true do local e, k = os.pullEvent() if e == "key" and k == keys.q then inMenu = true break elseif e == "timer" and k == t then break end end if inMenu then break end end if not fs.exists(target) and target ~= "shell" then inMenu = true end if inMenu then local files, sel = getFiles(), 1 while true do term.setBackgroundColor(colors.lightGray) term.clear() setBar(1, "CMOS SETUP UTILITY", w) setBar(h-1, "L-Click: Boot | R-Click/S: Default", w) setBar(h, "Arrows: Select | Enter: Boot", w) local regions = {} for i, f in ipairs(files) do local txt = " " .. f .. (f == target and " (default) " or " ") term.setCursorPos(3, 2+i) if i == sel then term.setBackgroundColor(colors.red) term.setTextColor(colors.white) else term.setBackgroundColor(colors.lightGray) term.setTextColor(colors.blue) end term.write(txt) regions[i] = {y=2+i, x1=3, x2=3+#txt} end local e, p1, p2, p3 = os.pullEvent() if e == "key" then if p1 == keys.up then sel = (sel > 1) and sel - 1 or #files elseif p1 == keys.down then sel = (sel < #files) and sel + 1 or 1 elseif p1 == keys.enter then target = files[sel] break elseif p1 == keys.s then save(files[sel]) end elseif e == "mouse_click" or e == "monitor_touch" then for i, r in ipairs(regions) do if p3 == r.y and p2 >= r.x1 and p2 <= r.x2 then if p1 == 2 then save(files[i]) elseif i == sel then target = files[i] goto boot else sel = i end end end end end end ::boot:: term.setBackgroundColor(colors.black) term.setTextColor(colors.white) term.clear() term.setCursorPos(1, 1) if fs.exists(target) or target == "shell" then shell.run(target) else print("Main executable not found: " .. target) end ]], MAIN_EXEC) local f = fs.open("startup.lua", "w") f.write(startupData) f.close() end drawBackground() drawWindow(10, 8, 32, 6, "Success") term.setCursorPos(12, 10); term.write(SYS_NAME .. " installed.") term.setCursorPos(12, 12); term.write("Press any key to reboot.") os.pullEvent("key") os.reboot()