From d4a3b1cce90046ca0af2db720a9fc71278aab5d6 Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Sun, 22 Mar 2026 16:56:50 -0400 Subject: [PATCH] Add dashboard rendering and touch handlers for inventory management - Implemented main dashboard UI with item display, status bar, and pagination. - Added touch zones for item ordering, quantity selection, and search functionality. - Created smelter dashboard with tabs for status, smelting, crafting, and missing recipes. - Integrated keyboard input for search queries and item management. - Enhanced drawing functions for better UI representation and interaction. --- manager/display.lua | 1836 ++++++++++++++++++++------------------- manager/display.lua.bak | 998 +++++++++++++++++++++ 2 files changed, 1949 insertions(+), 885 deletions(-) create mode 100644 manager/display.lua.bak diff --git a/manager/display.lua b/manager/display.lua index 48527e0..2cfe8ab 100644 --- a/manager/display.lua +++ b/manager/display.lua @@ -1,12 +1,16 @@ --- manager/display.lua — Dashboard rendering and touch handlers +-- manager/display.lua — Dashboard rendering using Opus UI framework -- Usage: local display = dofile("manager/display.lua")(ctx) +-- +-- Requires Opus UI framework (opus.ui, opus.event) +-- Falls back to raw monitor drawing if Opus is not available. return function(ctx) +local UI = require('opus.ui') + local cfg = ctx.cfg local state = ctx.state local log = ctx.log -local ui = ctx.ui local ops = ctx.ops local cache = state.cache @@ -15,7 +19,7 @@ local activity = state.activity local D = {} ------------------------------------------------- --- Monitor handles (set during init) +-- Monitor handles ------------------------------------------------- D.mon = nil @@ -23,976 +27,1038 @@ D.monName = nil D.smelterMon = nil D.smelterMonName = nil -function D.setupMonitor() - D.mon, D.monName = ui.setupMonitor(cfg.MONITOR_SIDE, cfg.SMELTER_MONITOR_SIDE) - return D.mon ~= nil -end - -function D.setupSmelterMonitor() - D.smelterMon, D.smelterMonName = ui.setupSmelterMonitor(cfg.SMELTER_MONITOR_SIDE, D.monName) - return D.smelterMon ~= nil -end +-- Opus UI devices and pages +local mainDevice = nil +local smelterDevice = nil +local mainPage = nil +local smelterPage = nil ------------------------------------------------- --- Main dashboard UI state +-- Local UI state ------------------------------------------------- local selectedAmount = 1 local amountOptions = {1, 4, 8, 16, 32, 64} - -local touchZones = {} -local pendingZones = {} - -local currentPage = 1 -local totalPages = 1 local searchQuery = "" -local showKeyboard = false - -local kbRows = { - {"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"}, -} - -------------------------------------------------- --- Smelter dashboard UI state -------------------------------------------------- - local smelterView = "status" -local smelterPage = 1 -local smelterTotalPages = 1 -local smelterTouchZones = {} -local smelterPendingZones = {} ------------------------------------------------- --- Drawing helpers (delegated to shared ui module) +-- Helpers ------------------------------------------------- -local draw = nil - -local function setDrawTarget(target) - draw = target - ui.draw = target -end - -local function monWrite(x, y, text, fg, bg) ui.monWrite(x, y, text, fg, bg) end -local function monFill(y, color) ui.monFill(y, color) end -local function monCenter(y, text, fg, bg) ui.monCenter(y, text, fg, bg) end -local function monBar(x, y, w, r, bc, bgc) ui.monBar(x, y, w, r, bc, bgc) end -local function drawButton(x, y, t, fg, bg, pl, pr) return ui.drawButton(x, y, t, fg, bg, pl, pr) end - -local function addZone(x1, y1, x2, y2, action, data) - ui.addZone(pendingZones, x1, y1, x2, y2, action, data) -end - -local function hitTest(x, y) - return ui.hitTest(touchZones, x, y) -end - -local function addSmelterZone(x1, y1, x2, y2, action, data) - ui.addZone(smelterPendingZones, x1, y1, x2, y2, action, data) -end - -local function smelterHitTest(x, y) - return ui.hitTest(smelterTouchZones, x, y) +local function shortName(fullName) + local s = fullName:gsub("^minecraft:", ""):gsub("_", " ") + return s:sub(1,1):upper() .. s:sub(2) end local function getFilteredItems() state.ensureItemList() - return ui.getFilteredItems(cache.itemList, searchQuery) + local filtered = {} + for _, item in ipairs(cache.itemList) do + if searchQuery == "" then + table.insert(filtered, item) + else + local lower = item.name:lower():gsub("minecraft:", ""):gsub("_", " ") + if lower:find(searchQuery:lower(), 1, true) then + table.insert(filtered, item) + end + end + end + return filtered +end + +local function getActivityString() + local parts = {} + if activity.sorting then table.insert(parts, "SORTING") end + if activity.dispensing then table.insert(parts, "DISPENSING") end + if activity.smelting then table.insert(parts, "SMELTING") end + if activity.scanning then table.insert(parts, "SCANNING") end + if activity.defragging then table.insert(parts, "DEFRAG") end + if activity.composting then table.insert(parts, "COMPOST") end + if #parts > 0 then + return table.concat(parts, " | ") + end + return "" +end + +local function getBottomMessage() + if activity.dispensing then return "DISPENSING..." + elseif activity.smelting then return "SMELTING..." + elseif activity.sorting then return "SORTING BARREL..." + elseif activity.defragging then return "DEFRAGMENTING..." + elseif activity.composting then return "COMPOSTING..." + end + return "Tap item to order" +end + +local function getStorageBarColor() + if cache.usedRatio > 0.9 then return colors.red + elseif cache.usedRatio > 0.7 then return colors.orange + elseif cache.usedRatio > 0.5 then return colors.yellow + end + return colors.lime +end + +local function getStockBarColor(_, ratio) + if ratio < 0.25 then return colors.red + elseif ratio < 0.5 then return colors.orange + end + return colors.lime end ------------------------------------------------- --- Main dashboard drawing +-- Monitor setup ------------------------------------------------- -function D.drawDashboard() - if not D.mon then return end - - local w, h = D.mon.getSize() - pendingZones = {} - - setDrawTarget(window.create(D.mon, 1, 1, w, h, false)) - draw.setBackgroundColor(colors.black) - draw.clear() - - -- Title bar - monFill(1, colors.blue) - monCenter(1, " ** INVENTORY MANAGER ** ", colors.white, colors.blue) - - -- Status bar - monFill(2, colors.gray) - local statusParts = {} - table.insert(statusParts, string.format(" Chests: %d", cache.chestCount)) - table.insert(statusParts, cache.dropperOk and "Dropper: OK" or "Dropper: --") - table.insert(statusParts, cache.barrelOk and "Barrel: OK" or "Barrel: --") - if cache.furnaceCount and cache.furnaceCount > 0 then - table.insert(statusParts, string.format("Furnaces: %d", cache.furnaceCount)) +local function findMonitor(side, excludeSide) + local mon = peripheral.wrap(side) + local monName + if mon and mon.setTextScale then + monName = side + else + mon = nil end - - local actParts = {} - if activity.sorting then table.insert(actParts, "SORTING") end - if activity.dispensing then table.insert(actParts, "DISPENSING") end - if activity.smelting then table.insert(actParts, "SMELTING") end - if activity.scanning then table.insert(actParts, "SCANNING") end - if activity.defragging then table.insert(actParts, "DEFRAG") end - if activity.composting then table.insert(actParts, "COMPOST") end - - monWrite(2, 2, table.concat(statusParts, " | "), colors.white, colors.gray) - - if #actParts > 0 then - local actStr = " " .. table.concat(actParts, " | ") .. " " - monWrite(w - #actStr, 2, actStr, colors.white, colors.orange) - end - - -- Divider - monFill(3, colors.lightBlue) - monCenter(3, string.rep("-", math.min(w - 4, 60)), colors.cyan, colors.lightBlue) - - -- Storage capacity - monFill(4, colors.black) - local capLabel = string.format(" Storage: %d/%d slots (%d free)", - cache.usedSlots, cache.totalSlots, cache.freeSlots) - monWrite(2, 4, capLabel, colors.lightGray, colors.black) - - local barStart = #capLabel + 4 - local barWidth = w - barStart - 2 - if barWidth > 4 then - local barColor = colors.lime - if cache.usedRatio > 0.9 then barColor = colors.red - elseif cache.usedRatio > 0.7 then barColor = colors.orange - elseif cache.usedRatio > 0.5 then barColor = colors.yellow + if not mon then + for _, name in ipairs(peripheral.getNames()) do + if peripheral.getType(name) == "monitor" and name ~= excludeSide then + mon = peripheral.wrap(name) + monName = name + break + end end - monBar(barStart, 4, barWidth, cache.usedRatio, barColor, colors.gray) - local pctStr = string.format(" %d%% ", math.floor(cache.usedRatio * 100)) - local pctX = barStart + math.floor(barWidth / 2) - math.floor(#pctStr / 2) - monWrite(pctX, 4, pctStr, colors.white, barColor) end + return mon, monName +end - -- Amount selector (row 5) - monFill(5, colors.black) - monWrite(2, 5, "Qty:", colors.lightGray, colors.black) +function D.setupMonitor() + D.mon, D.monName = findMonitor(cfg.MONITOR_SIDE, cfg.SMELTER_MONITOR_SIDE) + if not D.mon then return false end + + mainDevice = UI.Device({ + device = D.mon, + textScale = 0.5, + }) + return true +end + +function D.setupSmelterMonitor() + D.smelterMon, D.smelterMonName = findMonitor(cfg.SMELTER_MONITOR_SIDE, D.monName) + if not D.smelterMon then return false end + + smelterDevice = UI.Device({ + device = D.smelterMon, + textScale = 0.5, + }) + return true +end + +------------------------------------------------- +-- Build main dashboard page +------------------------------------------------- + +local function buildMainPage() + if not mainDevice then return end + + mainPage = UI.Page { + backgroundColor = colors.black, + textColor = colors.white, + + -- Title bar (row 1) + titleBar = UI.Window { + x = 1, y = 1, ex = -1, height = 1, + backgroundColor = colors.blue, + draw = function(self) + self:clear(colors.blue) + self:centeredWrite(1, " ** INVENTORY MANAGER ** ", colors.blue, colors.white) + end, + }, + + -- Status bar (row 2) + statusRow = UI.Window { + x = 1, y = 2, ex = -1, height = 1, + backgroundColor = colors.gray, + draw = function(self) + self:clear(colors.gray) + local parts = {} + table.insert(parts, string.format(" Chests: %d", cache.chestCount)) + table.insert(parts, cache.dropperOk and "Dropper: OK" or "Dropper: --") + table.insert(parts, cache.barrelOk and "Barrel: OK" or "Barrel: --") + if cache.furnaceCount and cache.furnaceCount > 0 then + table.insert(parts, string.format("Furnaces: %d", cache.furnaceCount)) + end + self:write(2, 1, table.concat(parts, " | "), colors.gray, colors.white) + + local actStr = getActivityString() + if #actStr > 0 then + actStr = " " .. actStr .. " " + self:write(self.width - #actStr + 1, 1, actStr, colors.orange, colors.white) + end + end, + }, + + -- Divider (row 3) + divider = UI.Window { + x = 1, y = 3, ex = -1, height = 1, + backgroundColor = colors.lightBlue, + draw = function(self) + self:clear(colors.lightBlue) + local dash = string.rep("-", math.min(self.width - 4, 60)) + self:centeredWrite(1, dash, colors.lightBlue, colors.cyan) + end, + }, + + -- Storage label + bar (row 4) + storageRow = UI.Window { + x = 1, y = 4, ex = -1, height = 1, + backgroundColor = colors.black, + draw = function(self) + self:clear(colors.black) + local label = string.format(" Storage: %d/%d slots (%d free)", + cache.usedSlots, cache.totalSlots, cache.freeSlots) + self:write(2, 1, label, colors.black, colors.lightGray) + + local barStart = #label + 4 + local barWidth = self.width - barStart - 2 + if barWidth > 4 then + local ratio = cache.usedRatio or 0 + local filled = math.floor(ratio * barWidth) + local barColor = getStorageBarColor() + + -- Draw the bar + if filled > 0 then + self:write(barStart, 1, string.rep(" ", filled), barColor) + end + if barWidth - filled > 0 then + self:write(barStart + filled, 1, + string.rep(" ", barWidth - filled), colors.gray) + end + + -- Overlay percentage text + local pctStr = string.format(" %d%% ", math.floor(ratio * 100)) + local pctX = barStart + math.floor(barWidth / 2) - math.floor(#pctStr / 2) + for ci = 1, #pctStr do + local cx = pctX + ci - 1 + if cx >= barStart and cx < barStart + barWidth then + local bg = (cx - barStart) < filled and barColor or colors.gray + self:write(cx, 1, pctStr:sub(ci, ci), bg, colors.white) + end + end + end + end, + }, + + -- Amount selector row (row 5) + amountRow = UI.Window { + x = 1, y = 5, ex = -1, height = 1, + backgroundColor = colors.black, + draw = function(self) + self:clear(colors.black) + self:write(2, 1, "Qty:", colors.black, colors.lightGray) + -- Amount buttons are drawn as children + self:drawChildren() + end, + }, + + -- Search + refresh row (row 6) + searchRow = UI.Window { + x = 1, y = 6, ex = -1, height = 1, + backgroundColor = colors.black, + }, + + searchEntry = UI.TextEntry { + x = 3, y = 6, + ex = '45%', + shadowText = "search...", + backgroundColor = colors.black, + backgroundFocusColor = colors.gray, + textColor = colors.white, + shadowTextColor = colors.gray, + limit = 30, + }, + + refreshBtn = UI.Button { + y = 5, ex = -2, + text = "Refresh", + backgroundColor = colors.green, + backgroundFocusColor = colors.lime, + textColor = colors.white, + event = 'do_scan', + }, + + -- Item grid (rows 7 to h-3) + itemGrid = UI.ScrollingGrid { + x = 1, y = 7, ex = -1, ey = -4, + disableHeader = false, + headerHeight = 1, + headerBackgroundColor = colors.gray, + headerTextColor = colors.lightGray, + backgroundColor = colors.black, + alternateRowColor = colors.gray, + backgroundSelectedColor = colors.blue, + unfocusedBackgroundSelectedColor = colors.blue, + textColor = colors.white, + focusIndicator = '>', + sortColumn = 'total', + inverseSort = true, + columns = { + { heading = '#', key = 'idx', width = 3 }, + { heading = 'Item', key = 'short' }, + { heading = 'Qty', key = 'qty', width = 6, textColor = colors.yellow }, + { heading = 'Stock', key = 'ratio', width = 12, + barColumn = true, + barColor = getStockBarColor, + barEmptyColor = colors.gray, + }, + }, + values = {}, + + getDisplayValues = function(_, row) + return { + idx = tostring(row.idx or ''), + short = row.short or '', + qty = tostring(row.total or 0), + ratio = row.ratio or 0, + } + end, + }, + + -- Alert / status area + alertBar = UI.Window { + x = 1, ey = -3, ex = -1, height = 1, + backgroundColor = colors.black, + draw = function(self) + self:clear(colors.black) + if #state.activeAlerts > 0 then + local alertIdx = math.floor(os.epoch("utc") / 2000) % #state.activeAlerts + 1 + local a = state.activeAlerts[alertIdx] + local msg = string.format(" LOW STOCK: %s (%d/%d) ", a.label, a.current, a.min) + self:centeredWrite(1, msg, colors.red, colors.white) + elseif state.statusTimer > 0 and #state.statusMessage > 0 then + self:centeredWrite(1, state.statusMessage, colors.black, state.statusColor) + end + end, + }, + + -- Footer + footerBar = UI.Window { + x = 1, ey = -2, ex = -1, height = 1, + backgroundColor = colors.gray, + draw = function(self) + self:clear(colors.gray) + state.ensureItemList() + local footerLeft = string.format(" Total: %d items | %d types ", + cache.grandTotal, #cache.itemList) + self:write(2, 1, footerLeft, colors.gray, colors.white) + + if searchQuery ~= "" then + local filteredItems = getFilteredItems() + local filterNote = string.format("| Showing %d ", #filteredItems) + self:write(2 + #footerLeft + 1, 1, filterNote, colors.gray, colors.yellow) + end + + local timeStr = textutils.formatTime(os.time(), true) + self:write(self.width - #timeStr - 1, 1, timeStr, colors.gray, colors.lightGray) + end, + }, + + -- Bottom accent + bottomBar = UI.Window { + x = 1, ey = -1, ex = -1, height = 1, + backgroundColor = colors.blue, + draw = function(self) + self:clear(colors.blue) + self:centeredWrite(1, " " .. getBottomMessage() .. " ", colors.blue, colors.lightBlue) + end, + }, + + -- Notification overlay + notification = UI.Notification { + anchor = 'bottom', + }, + + eventHandler = function(self, event) + if event.type == 'text_change' then + searchQuery = event.text or "" + D.refreshItemGrid() + self.alertBar:draw() + self.footerBar:draw() + self.bottomBar:draw() + self:sync() + return true + + elseif event.type == 'grid_select' then + local row = event.selected + if row and row.name then + local short = shortName(row.name) + state.statusMessage = string.format("Ordering %s x%d...", short, selectedAmount) + state.statusColor = colors.cyan + state.statusTimer = 10 + activity.dispensing = true + state.needsRedraw = true + ops.orderItem(row.name, selectedAmount) + end + return true + + elseif event.type == 'amount_select' then + selectedAmount = event.amount + D.updateAmountButtons() + self:sync() + return true + + elseif event.type == 'do_scan' then + state.statusMessage = "Refreshing..." + state.statusColor = colors.cyan + state.statusTimer = 3 + state.needsRedraw = true + return true + end + + return UI.Page.eventHandler(self, event) + end, + } + + -- Add amount buttons as children of amountRow local btnX = 7 for _, amt in ipairs(amountOptions) do - local label = tostring(amt) - local bg = (amt == selectedAmount) and colors.cyan or colors.gray - local fg = (amt == selectedAmount) and colors.white or colors.lightGray - local x1, y1, x2, y2 = drawButton(btnX, 5, label, fg, bg) - addZone(x1, y1, x2, y2, "amount", amt) - btnX = x2 + 2 + local uid = 'amt_' .. amt + mainPage.amountRow[uid] = UI.Button { + x = btnX, y = 1, + text = tostring(amt), + backgroundColor = (amt == selectedAmount) and colors.cyan or colors.gray, + backgroundFocusColor = colors.cyan, + textColor = (amt == selectedAmount) and colors.white or colors.lightGray, + textFocusColor = colors.white, + event = 'amount_select', + amount = amt, + } + btnX = btnX + #tostring(amt) + 4 end - local refreshBg = activity.scanning and colors.yellow or colors.green - local refreshFg = activity.scanning and colors.black or colors.white - local refreshTxt = activity.scanning and "Scanning" or "Refresh" - local scanX = w - #refreshTxt - 3 - local sx1, sy1, sx2, sy2 = drawButton(scanX, 5, refreshTxt, refreshFg, refreshBg, 1, 1) - addZone(sx1, sy1, sx2, sy2, "scan", nil) + -- Attach to device + mainDevice.currentPage = mainPage + mainPage.parent = mainDevice + mainPage:setParent() +end - -- Search bar + Pagination (row 6) - monFill(6, colors.black) - - local kbLabel = showKeyboard and " X " or " ? " - local kbBg = showKeyboard and colors.red or colors.purple - monWrite(2, 6, kbLabel, colors.white, kbBg) - addZone(2, 6, 4, 6, "kb_toggle", nil) - - local queryDisplay = searchQuery - if showKeyboard then - queryDisplay = queryDisplay .. "|" - elseif queryDisplay == "" then - queryDisplay = "search..." +function D.updateAmountButtons() + if not mainPage then return end + for _, amt in ipairs(amountOptions) do + local btn = mainPage.amountRow['amt_' .. amt] + if btn then + btn.backgroundColor = (amt == selectedAmount) and colors.cyan or colors.gray + btn.textColor = (amt == selectedAmount) and colors.white or colors.lightGray + btn:draw() + end end - local fieldW = math.floor(w * 0.4) - if fieldW < 10 then fieldW = 10 end - local displayText = queryDisplay:sub(1, fieldW) - displayText = displayText .. string.rep("_", math.max(0, fieldW - #displayText)) - monWrite(6, 6, displayText, - (searchQuery == "" and not showKeyboard) and colors.gray or colors.white, - colors.black) - addZone(6, 6, 5 + fieldW, 6, "kb_toggle", nil) +end +function D.refreshItemGrid() + if not mainPage then return end local filteredItems = getFilteredItems() - local maxRows = h - 10 - if maxRows < 1 then maxRows = 1 end - totalPages = math.max(1, math.ceil(#filteredItems / maxRows)) - if currentPage > totalPages then currentPage = totalPages end - if currentPage < 1 then currentPage = 1 end - - local pageStr = string.format("Pg %d/%d", currentPage, totalPages) - local navW = 3 + 1 + #pageStr + 1 + 3 - local navX = w - navW - - if currentPage > 1 then - monWrite(navX, 6, " < ", colors.white, colors.gray) - addZone(navX, 6, navX + 2, 6, "page_prev", nil) - else - monWrite(navX, 6, " < ", colors.lightGray, colors.black) - end - - monWrite(navX + 4, 6, pageStr, colors.lightGray, colors.black) - - local nextX = navX + 4 + #pageStr + 1 - if currentPage < totalPages then - monWrite(nextX, 6, " > ", colors.white, colors.gray) - addZone(nextX, 6, nextX + 2, 6, "page_next", nil) - else - monWrite(nextX, 6, " > ", colors.lightGray, colors.black) - end - - -- Column headers (row 7) - local row = 7 - monFill(row, colors.gray) - monWrite(2, row, "#", colors.lightGray, colors.gray) - monWrite(5, row, "Item", colors.lightGray, colors.gray) - monWrite(w - 22, row, "Qty", colors.lightGray, colors.gray) - monWrite(w - 14, row, "Stock", colors.lightGray, colors.gray) - monWrite(w - 1, row, ">", colors.lightGray, colors.gray) - row = row + 1 - - -- Item rows + -- Find max count for ratio calc local maxCount = 0 for _, item in ipairs(filteredItems) do if item.total > maxCount then maxCount = item.total end end if maxCount == 0 then maxCount = 1 end - local startIdx = (currentPage - 1) * maxRows + 1 - local endIdx = math.min(startIdx + maxRows - 1, #filteredItems) - - if #filteredItems == 0 then - monFill(8, colors.black) - monFill(9, colors.black) - if searchQuery ~= "" then - monCenter(9, "No items match \"" .. searchQuery .. "\"", colors.gray, colors.black) - else - monCenter(9, "No items in storage", colors.gray, colors.black) - end - row = 10 - else - for i = startIdx, endIdx do - local item = filteredItems[i] - local y = row - local short = item.name:gsub("^minecraft:", ""):gsub("_", " ") - short = short:sub(1,1):upper() .. short:sub(2) - - local maxNameLen = w - 30 - if #short > maxNameLen then - short = short:sub(1, maxNameLen - 2) .. ".." - end - - local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray - monFill(y, rowBg) - - monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg) - monWrite(5, y, short, colors.white, rowBg) - monWrite(w - 22, y, tostring(item.total), colors.yellow, rowBg) - - local ratio = item.total / maxCount - local barColor = colors.lime - if ratio < 0.25 then barColor = colors.red - elseif ratio < 0.5 then barColor = colors.orange - end - monBar(w - 14, y, 12, ratio, barColor, rowBg == colors.gray and colors.lightGray or colors.gray) - - monWrite(w - 1, y, ">", colors.orange, rowBg) - addZone(1, y, w, y, "order", item.name) - - row = row + 1 - end + local gridValues = {} + for i, item in ipairs(filteredItems) do + table.insert(gridValues, { + idx = i, + name = item.name, + short = shortName(item.name), + total = item.total, + qty = tostring(item.total), + ratio = item.total / maxCount, + }) end - local lastItemRow = h - 3 - while row <= lastItemRow do - monFill(row, colors.black) - row = row + 1 - end + mainPage.itemGrid:setValues(gridValues) +end - if showKeyboard then - local keyW = 3 - local keyGap = 1 +------------------------------------------------- +-- Build smelter dashboard page +------------------------------------------------- - local kbDefs = { - { keys = kbRows[1], specials = {{ label = " Bksp ", action = "kb_bksp", bg = colors.red }} }, - { keys = kbRows[2], specials = {{ label = " Done ", action = "kb_done", bg = colors.green }} }, - { keys = kbRows[3], specials = { - { label = " Space ", action = "kb_space", bg = colors.lightGray }, - { label = " Clr ", action = "kb_clear", bg = colors.orange }, - }}, - } +local function buildSmelterPage() + if not smelterDevice then return end - for rowIdx, def in ipairs(kbDefs) do - local y = h - 3 + rowIdx - monFill(y, colors.black) + smelterPage = UI.Page { + backgroundColor = colors.black, + textColor = colors.white, - local keysW = #def.keys * keyW + math.max(0, #def.keys - 1) * keyGap - local specialsW = 0 - for _, sp in ipairs(def.specials) do - specialsW = specialsW + keyGap + #sp.label - end - local rowW = keysW + specialsW - local x = math.floor((w - rowW) / 2) + 1 + -- Title bar + titleBar = UI.Window { + x = 1, y = 1, ex = -1, height = 1, + backgroundColor = colors.purple, + draw = function(self) + self:clear(colors.purple) + self:centeredWrite(1, " ** SMELTER DASHBOARD ** ", colors.purple, colors.white) + end, + }, - for ki, key in ipairs(def.keys) do - monWrite(x, y, " " .. key .. " ", colors.white, colors.gray) - addZone(x, y, x + keyW - 1, y, "kb_key", key:lower()) - x = x + keyW - if ki < #def.keys then x = x + keyGap end - end + -- Status bar with pause toggle + statusRow = UI.Window { + x = 1, y = 2, ex = -1, height = 1, + backgroundColor = colors.gray, + draw = function(self) + self:clear(colors.gray) + local activeCount = 0 + for _, fs in ipairs(cache.furnaceStatus or {}) do + if fs.active then activeCount = activeCount + 1 end + end + local statusStr = string.format(" Furnaces: %d Active: %d", + cache.furnaceCount or 0, activeCount) + self:write(2, 1, statusStr, colors.gray, colors.white) - for _, sp in ipairs(def.specials) do - x = x + keyGap - monWrite(x, y, sp.label, colors.white, sp.bg) - addZone(x, y, x + #sp.label - 1, y, sp.action, nil) - x = x + #sp.label - end - end - else - -- Status message - monFill(h - 2, colors.black) - if #state.activeAlerts > 0 then - local alertIdx = math.floor(os.epoch("utc") / 2000) % #state.activeAlerts + 1 - local a = state.activeAlerts[alertIdx] - local alertMsg = string.format(" LOW STOCK: %s (%d/%d) ", a.label, a.current, a.min) - monCenter(h - 2, alertMsg, colors.white, colors.red) - elseif state.statusTimer > 0 and #state.statusMessage > 0 then - monCenter(h - 2, state.statusMessage, state.statusColor, colors.black) - end + local pauseLabel = state.smeltingPaused and " PAUSED " or " ACTIVE " + local pauseBg = state.smeltingPaused and colors.red or colors.lime + local pauseFg = state.smeltingPaused and colors.white or colors.black + self:write(self.width - #pauseLabel + 1, 1, pauseLabel, pauseBg, pauseFg) + end, - -- Footer - state.ensureItemList() - monFill(h - 1, colors.gray) - local footerLeft = string.format(" Total: %d items | %d types ", - cache.grandTotal, #cache.itemList) - monWrite(2, h - 1, footerLeft, colors.white, colors.gray) + eventHandler = function(self, event) + if event.type == 'mouse_click' then + local pauseLabel = state.smeltingPaused and " PAUSED " or " ACTIVE " + local pauseStart = self.width - #pauseLabel + 1 + if event.x >= pauseStart then + state.smeltingPaused = not state.smeltingPaused + log.debug("UI", "Smelting %s", + state.smeltingPaused and "PAUSED" or "RESUMED") + ops.saveDisabledRecipes() + state.smelterNeedsRedraw = true + state.needsRedraw = true + return true + end + end + return UI.Window.eventHandler(self, event) + end, + }, - if searchQuery ~= "" then - local filterNote = string.format("| Showing %d ", #filteredItems) - monWrite(2 + #footerLeft + 1, h - 1, filterNote, colors.yellow, colors.gray) - end + -- Divider + divider = UI.Window { + x = 1, y = 3, ex = -1, height = 1, + backgroundColor = colors.magenta, + draw = function(self) + self:clear(colors.magenta) + local dash = string.rep("-", math.min(self.width - 4, 60)) + self:centeredWrite(1, dash, colors.magenta, colors.pink) + end, + }, - local timeStr = textutils.formatTime(os.time(), true) - monWrite(w - #timeStr - 1, h - 1, timeStr, colors.lightGray, colors.gray) + -- Tabs container + tabs = UI.Tabs { + x = 1, y = 4, ex = -1, ey = -3, + barBackgroundColor = colors.black, + selectedBackgroundColor = colors.purple, + unselectedBackgroundColor = colors.gray, + + -- Status tab + statusTab = UI.Tab { + index = 1, + title = "Status", + noFill = true, + backgroundColor = colors.black, + + grid = UI.ScrollingGrid { + y = 1, ey = -1, + disableHeader = false, + headerBackgroundColor = colors.gray, + headerTextColor = colors.lightGray, + backgroundColor = colors.black, + alternateRowColor = colors.gray, + textColor = colors.white, + sortColumn = 'idx', + columns = { + { heading = '#', key = 'idx', width = 2 }, + { heading = 'T', key = 'ftype', width = 1 }, + { heading = 'Input', key = 'input' }, + { heading = 'Output', key = 'output', width = 12 }, + { heading = 'Fuel', key = 'fuel', width = 10 }, + { heading = 'State', key = 'fstate', width = 5 }, + }, + values = {}, + }, + }, + + -- Smelt recipe tab + smeltTab = UI.Tab { + index = 2, + title = "Smelt", + noFill = true, + backgroundColor = colors.black, + + enableAllBtn = UI.Button { + x = -18, y = 0, + text = "All On", + backgroundColor = colors.green, + backgroundFocusColor = colors.lime, + textColor = colors.white, + event = 'enable_all', + }, + + disableAllBtn = UI.Button { + x = -9, y = 0, + text = "All Off", + backgroundColor = colors.red, + backgroundFocusColor = colors.orange, + textColor = colors.white, + event = 'disable_all', + }, + + grid = UI.ScrollingGrid { + y = 1, ey = -1, + disableHeader = false, + headerBackgroundColor = colors.gray, + headerTextColor = colors.lightGray, + backgroundColor = colors.black, + alternateRowColor = colors.gray, + textColor = colors.white, + sortColumn = 'inputShort', + columns = { + { heading = 'Input', key = 'inputShort' }, + { heading = 'Output', key = 'resultShort', width = 12 }, + { heading = 'Type', key = 'types', width = 3 }, + { heading = 'Stock', key = 'inStorage', width = 6 }, + { heading = 'On?', key = 'toggleLabel', width = 4 }, + }, + values = {}, + + getRowTextColor = function(self, row, selected) + if selected then return colors.white end + -- Color the toggle column indicator + return colors.white + end, + }, + }, + + -- Craft tab + craftTab = UI.Tab { + index = 3, + title = "Craft", + noFill = true, + backgroundColor = colors.black, + + turtleStatus = UI.Window { + x = -14, y = 0, width = 14, height = 1, + draw = function(self) + local turtleOk = ctx.craftTurtleName + and peripheral.isPresent(ctx.craftTurtleName) + local label = turtleOk and " Turtle OK " or " No Turtle " + local bg = turtleOk and colors.lime or colors.red + local fg = turtleOk and colors.black or colors.white + self:clear(colors.black) + self:write(1, 1, label, bg, fg) + end, + }, + + grid = UI.ScrollingGrid { + y = 1, ey = -1, + disableHeader = false, + headerBackgroundColor = colors.gray, + headerTextColor = colors.lightGray, + backgroundColor = colors.black, + alternateRowColor = colors.gray, + textColor = colors.white, + sortColumn = 'short', + columns = { + { heading = '#', key = 'dispIdx', width = 3 }, + { heading = 'Output', key = 'short' }, + { heading = 'Yield', key = 'yield', width = 5 }, + { heading = 'Can Make', key = 'batches', width = 8 }, + { heading = 'Go', key = 'goLabel', width = 6 }, + }, + values = {}, + }, + }, + + -- Missing tab + missingTab = UI.Tab { + index = 4, + title = "Missing", + noFill = true, + backgroundColor = colors.black, + + grid = UI.ScrollingGrid { + y = 1, ey = -1, + disableHeader = false, + headerBackgroundColor = colors.gray, + headerTextColor = colors.lightGray, + backgroundColor = colors.black, + alternateRowColor = colors.gray, + textColor = colors.white, + sortColumn = 'short', + columns = { + { heading = '#', key = 'dispIdx', width = 3 }, + { heading = 'Output', key = 'short' }, + { heading = 'Missing (have/need)', key = 'summary' }, + }, + values = {}, + + getRowTextColor = function(self, row, selected) + if selected then return colors.white end + return colors.red + end, + }, + }, + }, + + -- Footer info bar + smelterFooter = UI.Window { + x = 1, ey = -2, ex = -1, height = 1, + backgroundColor = colors.gray, + draw = function(self) + self:clear(colors.gray) + if smelterView == "status" or smelterView == "smelt" then + local enabledCount = 0 + local totalRecipes = 0 + for _ in pairs(cfg.SMELTABLE) do totalRecipes = totalRecipes + 1 end + for inputName in pairs(cfg.SMELTABLE) do + if not state.disabledRecipes[inputName] then + enabledCount = enabledCount + 1 + end + end + local info = string.format(" Smelt: %d/%d enabled", enabledCount, totalRecipes) + self:write(2, 1, info, colors.gray, colors.white) + if activity.smelting then + self:write(2 + #info + 2, 1, " SMELTING... ", colors.orange, colors.white) + end + elseif smelterView == "craft" then + self:write(2, 1, " Select recipe to craft", colors.gray, colors.white) + if activity.crafting then + self:write(26, 1, " CRAFTING... ", colors.orange, colors.white) + end + elseif smelterView == "missing" then + local availCount = 0 + for _, recipe in ipairs(cfg.CRAFTABLE) do + if ops.canCraftRecipe(recipe) then availCount = availCount + 1 end + end + local info = string.format(" Available: %d/%d recipes", + availCount, #cfg.CRAFTABLE) + self:write(2, 1, info, colors.gray, colors.white) + end + end, + }, -- Bottom accent - monFill(h, colors.blue) - local bottomMsg = " Tap item to order " - if activity.dispensing then - bottomMsg = " DISPENSING... " - elseif activity.smelting then - bottomMsg = " SMELTING... " - elseif activity.sorting then - bottomMsg = " SORTING BARREL... " - elseif activity.defragging then - bottomMsg = " DEFRAGMENTING... " - elseif activity.composting then - bottomMsg = " COMPOSTING... " - end - monCenter(h, bottomMsg, colors.lightBlue, colors.blue) - end + bottomBar = UI.Window { + x = 1, ey = -1, ex = -1, height = 1, + backgroundColor = colors.purple, + draw = function(self) + self:clear(colors.purple) + local msg = " Smelt recipe manager " + if smelterView == "status" then + msg = activity.smelting and " SMELTING... " or " Furnace status " + elseif smelterView == "smelt" then + msg = " Tap recipe to toggle " + elseif smelterView == "craft" then + msg = activity.crafting and " CRAFTING... " or " Tap to craft " + elseif smelterView == "missing" then + msg = " Missing ingredients " + end + self:centeredWrite(1, msg, colors.purple, colors.pink) + end, + }, - draw.setVisible(true) - touchZones = pendingZones + notification = UI.Notification { + anchor = 'bottom', + }, + + eventHandler = function(self, event) + if event.type == 'tab_change' then + local tabMap = { 'status', 'smelt', 'craft', 'missing' } + if event.current then + smelterView = tabMap[event.current] or smelterView + end + D.refreshSmelterData() + self.smelterFooter:draw() + self.bottomBar:draw() + -- fall through to default handler for tab switching + + elseif event.type == 'enable_all' then + state.disabledRecipes = {} + log.debug("UI", "All recipes enabled") + ops.saveDisabledRecipes() + state.smelterNeedsRedraw = true + return true + + elseif event.type == 'disable_all' then + for inputName in pairs(cfg.SMELTABLE) do + state.disabledRecipes[inputName] = true + end + log.debug("UI", "All recipes disabled") + ops.saveDisabledRecipes() + state.smelterNeedsRedraw = true + return true + + elseif event.type == 'grid_select' then + if smelterView == "smelt" and event.selected then + local inputName = event.selected.inputName + if inputName then + if state.disabledRecipes[inputName] then + state.disabledRecipes[inputName] = nil + else + state.disabledRecipes[inputName] = true + end + local short = shortName(inputName) + log.debug("UI", "Recipe %s: %s", short, + state.disabledRecipes[inputName] and "OFF" or "ON") + ops.saveDisabledRecipes() + state.smelterNeedsRedraw = true + end + return true + + elseif smelterView == "craft" and event.selected then + local recipeIdx = event.selected.idx + local recipe = cfg.CRAFTABLE[recipeIdx] + if recipe then + local turtleOk = ctx.craftTurtleName + and peripheral.isPresent(ctx.craftTurtleName) + if not turtleOk then + self.notification:error("No crafting turtle!") + return true + end + local short = shortName(recipe.output) + log.info("CRAFT", "Craft: %s (#%d)", short, recipeIdx) + local ok, err = ops.craftItem(recipeIdx) + if ok then + self.notification:success("Crafted " .. short .. " x" .. recipe.count) + state.statusMessage = "Crafted " .. short + state.statusColor = colors.lime + else + self.notification:error("Failed: " .. (err or "unknown")) + state.statusMessage = "Craft failed" + state.statusColor = colors.red + end + state.statusTimer = 5 + state.needsRedraw = true + state.smelterNeedsRedraw = true + end + return true + end + end + + return UI.Page.eventHandler(self, event) + end, + } + + -- Make status row clickable for pause toggle + smelterPage.statusRow.focus = function() end + + -- Attach to device + smelterDevice.currentPage = smelterPage + smelterPage.parent = smelterDevice + smelterPage:setParent() end ------------------------------------------------- --- Smelter dashboard drawing +-- Data refresh functions ------------------------------------------------- -function D.drawSmelterDashboard() - if not D.smelterMon then return end +function D.refreshSmelterData() + if not smelterPage then return end - local w, h = D.smelterMon.getSize() - smelterPendingZones = {} - - setDrawTarget(window.create(D.smelterMon, 1, 1, w, h, false)) - draw.setBackgroundColor(colors.black) - draw.clear() - - -- Title bar - monFill(1, colors.purple) - monCenter(1, " ** SMELTER DASHBOARD ** ", colors.white, colors.purple) - - -- Status bar - monFill(2, colors.gray) - local activeCount = 0 - for _, fs in ipairs(cache.furnaceStatus or {}) do - if fs.active then activeCount = activeCount + 1 end - end - local statusStr = string.format(" Furnaces: %d Active: %d", - cache.furnaceCount or 0, activeCount) - monWrite(2, 2, statusStr, colors.white, colors.gray) - - local pauseLabel = state.smeltingPaused and " PAUSED " or " ACTIVE " - local pauseBg = state.smeltingPaused and colors.red or colors.lime - local pauseFg = state.smeltingPaused and colors.white or colors.black - monWrite(w - #pauseLabel, 2, pauseLabel, pauseFg, pauseBg) - addSmelterZone(w - #pauseLabel, 2, w - 1, 2, "toggle_pause", nil) - - -- Divider - monFill(3, colors.magenta) - monCenter(3, string.rep("-", math.min(w - 4, 60)), colors.pink, colors.magenta) - - -- Tab row - monFill(4, colors.black) - local tabStatusBg = smelterView == "status" and colors.purple or colors.gray - local tabSmeltBg = smelterView == "smelt" and colors.purple or colors.gray - local tabCraftBg = smelterView == "craft" and colors.purple or colors.gray - local tabMissingBg = smelterView == "missing" and colors.purple or colors.gray - local bx1, by1, bx2, by2 - bx1, by1, bx2, by2 = drawButton(2, 4, "Status", colors.white, tabStatusBg) - addSmelterZone(bx1, by1, bx2, by2, "tab", "status") - bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "Smelt", colors.white, tabSmeltBg) - addSmelterZone(bx1, by1, bx2, by2, "tab", "smelt") - bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "Craft", colors.white, tabCraftBg) - addSmelterZone(bx1, by1, bx2, by2, "tab", "craft") - bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "Missing", colors.white, tabMissingBg) - addSmelterZone(bx1, by1, bx2, by2, "tab", "missing") - - local craftAvailCount = nil - - if smelterView == "status" then - -- Furnace Status View - monFill(5, colors.gray) - local outCol = math.floor(w * 0.40) - local fuelCol = math.floor(w * 0.65) - local statCol = w - 6 - monWrite(2, 5, "#", colors.lightGray, colors.gray) - monWrite(4, 5, "T", colors.lightGray, colors.gray) - monWrite(6, 5, "Input", colors.lightGray, colors.gray) - monWrite(outCol, 5, "Output", colors.lightGray, colors.gray) - monWrite(fuelCol, 5, "Fuel", colors.lightGray, colors.gray) - monWrite(statCol, 5, "State", colors.lightGray, colors.gray) - - local furnaceList = cache.furnaceStatus or {} - local maxRows = h - 8 - if maxRows < 1 then maxRows = 1 end - smelterTotalPages = math.max(1, math.ceil(#furnaceList / maxRows)) - if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end - if smelterPage < 1 then smelterPage = 1 end - - local startIdx = (smelterPage - 1) * maxRows + 1 - local endIdx = math.min(startIdx + maxRows - 1, #furnaceList) - - local row = 6 - if #furnaceList == 0 then - monFill(7, colors.black) - monCenter(7, "No furnaces found on network", colors.gray, colors.black) - row = 8 - else - for i = startIdx, endIdx do - local fs = furnaceList[i] - local y = row - local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray - monFill(y, rowBg) - - monWrite(2, y, string.format("%d", i), colors.lightBlue, rowBg) - - local typeAbbr = "F" - local typeColor = colors.orange - if fs.type == "minecraft:smoker" then - typeAbbr = "S" - typeColor = colors.green - elseif fs.type == "minecraft:blast_furnace" then - typeAbbr = "B" - typeColor = colors.cyan - end - monWrite(4, y, typeAbbr, typeColor, rowBg) - - if fs.input then - local inName = fs.input.name:gsub("^minecraft:", ""):gsub("_", " ") - local maxIn = outCol - 8 - if #inName > maxIn then inName = inName:sub(1, maxIn - 2) .. ".." end - monWrite(6, y, inName, colors.white, rowBg) - monWrite(outCol - 4, y, "x" .. fs.input.count, colors.yellow, rowBg) - else - monWrite(6, y, "(empty)", colors.lightGray, rowBg) - end - - if fs.output then - local outName = fs.output.name:gsub("^minecraft:", ""):gsub("_", " ") - local maxOut = fuelCol - outCol - 5 - if #outName > maxOut then outName = outName:sub(1, maxOut - 2) .. ".." end - monWrite(outCol, y, outName, colors.white, rowBg) - monWrite(fuelCol - 4, y, "x" .. fs.output.count, colors.yellow, rowBg) - else - monWrite(outCol, y, "-", colors.lightGray, rowBg) - end - - if fs.fuel then - local fuelName = fs.fuel.name:gsub("^minecraft:", ""):gsub("_", " ") - local maxFuel = statCol - fuelCol - 4 - if #fuelName > maxFuel then fuelName = fuelName:sub(1, maxFuel - 2) .. ".." end - monWrite(fuelCol, y, fuelName, colors.white, rowBg) - monWrite(statCol - 4, y, "x" .. fs.fuel.count, colors.yellow, rowBg) - else - monWrite(fuelCol, y, "-", colors.lightGray, rowBg) - end - - if state.smeltingPaused then - monWrite(statCol, y, "PAUSE", colors.red, rowBg) - elseif fs.active then - monWrite(statCol, y, " COOK", colors.lime, rowBg) - elseif fs.input and not fs.fuel then - monWrite(statCol, y, "FUEL?", colors.orange, rowBg) - else - monWrite(statCol, y, " IDLE", colors.lightGray, rowBg) - end - - row = row + 1 - end + -- Status tab + local furnaceList = cache.furnaceStatus or {} + local statusValues = {} + for i, fs in ipairs(furnaceList) do + local typeAbbr = "F" + if fs.type == "minecraft:smoker" then typeAbbr = "S" + elseif fs.type == "minecraft:blast_furnace" then typeAbbr = "B" end - while row <= h - 2 do monFill(row, colors.black); row = row + 1 end + local inputStr = "(empty)" + if fs.input then + local n = shortName(fs.input.name) + inputStr = n .. " x" .. fs.input.count + end - elseif smelterView == "smelt" then - -- Smelt Recipe Manager View - local recipeList = {} - for inputName, recipe in pairs(cfg.SMELTABLE) do - local short = inputName:gsub("^minecraft:", ""):gsub("_", " ") - short = short:sub(1,1):upper() .. short:sub(2) - local resultShort = recipe.result:gsub("^minecraft:", ""):gsub("_", " ") - resultShort = resultShort:sub(1,1):upper() .. resultShort:sub(2) - local types = "" - if recipe.furnaceSet["minecraft:furnace"] then types = types .. "F" end - if recipe.furnaceSet["minecraft:smoker"] then types = types .. "S" end - if recipe.furnaceSet["minecraft:blast_furnace"] then types = types .. "B" end - local enabled = not state.disabledRecipes[inputName] - local inStorage = 0 - if cache.catalogue[inputName] then - for _, s in ipairs(cache.catalogue[inputName]) do - inStorage = inStorage + s.total - end + local outputStr = "-" + if fs.output then + local n = shortName(fs.output.name) + outputStr = n .. " x" .. fs.output.count + end + + local fuelStr = "-" + if fs.fuel then + local n = shortName(fs.fuel.name) + fuelStr = n .. " x" .. fs.fuel.count + end + + local stateStr = " IDLE" + if state.smeltingPaused then stateStr = "PAUSE" + elseif fs.active then stateStr = " COOK" + elseif fs.input and not fs.fuel then stateStr = "FUEL?" + end + + table.insert(statusValues, { + idx = tostring(i), + ftype = typeAbbr, + input = inputStr, + output = outputStr, + fuel = fuelStr, + fstate = stateStr, + }) + end + smelterPage.tabs.statusTab.grid:setValues(statusValues) + + -- Smelt tab + local recipeList = {} + for inputName, recipe in pairs(cfg.SMELTABLE) do + local short = shortName(inputName) + local resultShort = shortName(recipe.result) + local types = "" + if recipe.furnaceSet["minecraft:furnace"] then types = types .. "F" end + if recipe.furnaceSet["minecraft:smoker"] then types = types .. "S" end + if recipe.furnaceSet["minecraft:blast_furnace"] then types = types .. "B" end + local enabled = not state.disabledRecipes[inputName] + local inStorage = 0 + if cache.catalogue[inputName] then + for _, s in ipairs(cache.catalogue[inputName]) do + inStorage = inStorage + s.total end - table.insert(recipeList, { - inputName = inputName, - inputShort = short, - resultShort = resultShort, - types = types, - enabled = enabled, - inStorage = inStorage, + end + table.insert(recipeList, { + inputName = inputName, + inputShort = short, + resultShort = resultShort, + types = types, + inStorage = tostring(inStorage), + toggleLabel = enabled and " ON " or " OFF", + enabled = enabled, + }) + end + table.sort(recipeList, function(a, b) return a.inputShort < b.inputShort end) + smelterPage.tabs.smeltTab.grid:setValues(recipeList) + + -- Craft tab + local availList = {} + for idx, recipe in ipairs(cfg.CRAFTABLE) do + if ops.canCraftRecipe(recipe) then + local short = shortName(recipe.output) + local batches = ops.maxCraftBatches(recipe) + table.insert(availList, { + idx = idx, + dispIdx = tostring(#availList + 1), + short = short, + yield = "x" .. recipe.count, + batches = "x" .. batches, + goLabel = " MAKE ", }) end - table.sort(recipeList, function(a, b) return a.inputShort < b.inputShort end) - - local arrowCol = math.floor(w * 0.30) - local typeCol = math.floor(w * 0.60) - local stockCol = math.floor(w * 0.72) - local toggleCol = w - 5 - - monFill(5, colors.gray) - monWrite(2, 5, "Input", colors.lightGray, colors.gray) - monWrite(arrowCol, 5, "Output", colors.lightGray, colors.gray) - monWrite(typeCol, 5, "Type", colors.lightGray, colors.gray) - monWrite(stockCol, 5, "Stock", colors.lightGray, colors.gray) - monWrite(toggleCol, 5, "On?", colors.lightGray, colors.gray) - - local bulkX = w - 22 - bx1, by1, bx2, by2 = drawButton(bulkX, 4, "All On", colors.white, colors.green) - addSmelterZone(bx1, by1, bx2, by2, "enable_all", nil) - bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "All Off", colors.white, colors.red) - addSmelterZone(bx1, by1, bx2, by2, "disable_all", nil) - - local maxRows = h - 8 - if maxRows < 1 then maxRows = 1 end - smelterTotalPages = math.max(1, math.ceil(#recipeList / maxRows)) - if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end - if smelterPage < 1 then smelterPage = 1 end - - local startIdx = (smelterPage - 1) * maxRows + 1 - local endIdx = math.min(startIdx + maxRows - 1, #recipeList) - - local row = 6 - for i = startIdx, endIdx do - local r = recipeList[i] - local y = row - local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray - monFill(y, rowBg) - - local maxInputLen = arrowCol - 3 - local inputDisplay = r.inputShort - if #inputDisplay > maxInputLen then - inputDisplay = inputDisplay:sub(1, maxInputLen - 2) .. ".." - end - monWrite(2, y, inputDisplay, colors.white, rowBg) - - local maxOutLen = typeCol - arrowCol - 2 - local outDisplay = r.resultShort - if #outDisplay > maxOutLen then - outDisplay = outDisplay:sub(1, maxOutLen - 2) .. ".." - end - monWrite(arrowCol, y, outDisplay, colors.lightBlue, rowBg) - - monWrite(typeCol, y, r.types, colors.orange, rowBg) - monWrite(stockCol, y, tostring(r.inStorage), colors.yellow, rowBg) - - if r.enabled then - monWrite(toggleCol, y, " ON ", colors.white, colors.green) - else - monWrite(toggleCol, y, " OFF", colors.white, colors.red) - end - addSmelterZone(1, y, w, y, "toggle_recipe", r.inputName) - - row = row + 1 - end - - while row <= h - 2 do monFill(row, colors.black); row = row + 1 end - - elseif smelterView == "craft" then - -- Available Crafting Recipes - local turtleOk = ctx.craftTurtleName and peripheral.isPresent(ctx.craftTurtleName) - local tLabel = turtleOk and " Turtle OK " or " No Turtle " - local tBg = turtleOk and colors.lime or colors.red - local tFg = turtleOk and colors.black or colors.white - monWrite(w - #tLabel, 4, tLabel, tFg, tBg) - - local availList = {} - for idx, recipe in ipairs(cfg.CRAFTABLE) do - if ops.canCraftRecipe(recipe) then - local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ") - short = short:sub(1,1):upper() .. short:sub(2) - local batches = ops.maxCraftBatches(recipe) - table.insert(availList, { - idx = idx, - short = short, - count = recipe.count, - batches = batches, - }) - end - end - craftAvailCount = #availList - - monFill(5, colors.gray) - local makeCol = w - 6 - monWrite(2, 5, "#", colors.lightGray, colors.gray) - monWrite(4, 5, "Output", colors.lightGray, colors.gray) - monWrite(math.floor(w * 0.45), 5, "Yield", colors.lightGray, colors.gray) - monWrite(math.floor(w * 0.60), 5, "Can Make", colors.lightGray, colors.gray) - monWrite(makeCol, 5, "Go", colors.lightGray, colors.gray) - - local maxRows = h - 8 - if maxRows < 1 then maxRows = 1 end - smelterTotalPages = math.max(1, math.ceil(#availList / maxRows)) - if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end - if smelterPage < 1 then smelterPage = 1 end - - local startIdx = (smelterPage - 1) * maxRows + 1 - local endIdx = math.min(startIdx + maxRows - 1, #availList) - - local row = 6 - if #availList == 0 then - monFill(7, colors.black) - monCenter(7, "No recipes available to craft", colors.gray, colors.black) - row = 8 - else - for i = startIdx, endIdx do - local r = availList[i] - local y = row - local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray - monFill(y, rowBg) - - monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg) - - local maxNameLen = math.floor(w * 0.40) - local nameDisplay = r.short - if #nameDisplay > maxNameLen then - nameDisplay = nameDisplay:sub(1, maxNameLen - 2) .. ".." - end - monWrite(4, y, nameDisplay, colors.white, rowBg) - - monWrite(math.floor(w * 0.45), y, "x" .. r.count, colors.yellow, rowBg) - monWrite(math.floor(w * 0.60), y, - string.format("x%d", r.batches), colors.lime, rowBg) - - if turtleOk then - monWrite(makeCol, y, " MAKE ", colors.white, colors.green) - addSmelterZone(makeCol, y, makeCol + 5, y, "craft", r.idx) - else - monWrite(makeCol, y, " ---- ", colors.gray, colors.black) - end - - row = row + 1 - end - end - - while row <= h - 2 do monFill(row, colors.black); row = row + 1 end - - elseif smelterView == "missing" then - -- Unavailable Crafting Recipes - local missList = {} - for idx, recipe in ipairs(cfg.CRAFTABLE) do - if not ops.canCraftRecipe(recipe) then - local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ") - short = short:sub(1,1):upper() .. short:sub(2) - local missing = ops.getMissingIngredients(recipe) - local parts = {} - for _, m in ipairs(missing) do - local mShort = m.name:gsub("^minecraft:", ""):gsub("_", " ") - table.insert(parts, string.format("%s %d/%d", mShort, m.have, m.need)) - end - table.insert(missList, { - idx = idx, - short = short, - count = recipe.count, - summary = table.concat(parts, ", "), - }) - end - end - craftAvailCount = #cfg.CRAFTABLE - #missList - - monFill(5, colors.gray) - monWrite(2, 5, "#", colors.lightGray, colors.gray) - monWrite(4, 5, "Output", colors.lightGray, colors.gray) - monWrite(math.floor(w * 0.35), 5, "Missing (have/need)", colors.lightGray, colors.gray) - - local maxRows = h - 8 - if maxRows < 1 then maxRows = 1 end - smelterTotalPages = math.max(1, math.ceil(#missList / maxRows)) - if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end - if smelterPage < 1 then smelterPage = 1 end - - local startIdx = (smelterPage - 1) * maxRows + 1 - local endIdx = math.min(startIdx + maxRows - 1, #missList) - - local row = 6 - if #missList == 0 then - monFill(7, colors.black) - monCenter(7, "All recipes can be crafted!", colors.lime, colors.black) - row = 8 - else - for i = startIdx, endIdx do - local r = missList[i] - local y = row - local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray - monFill(y, rowBg) - - monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg) - - local nameCol = math.floor(w * 0.35) - 5 - local nameDisplay = r.short .. " x" .. r.count - if #nameDisplay > nameCol then - nameDisplay = nameDisplay:sub(1, nameCol - 2) .. ".." - end - monWrite(4, y, nameDisplay, colors.white, rowBg) - - local missCol = math.floor(w * 0.35) - local missW = w - missCol - 1 - local summaryDisplay = r.summary - if #summaryDisplay > missW then - summaryDisplay = summaryDisplay:sub(1, missW - 2) .. ".." - end - monWrite(missCol, y, summaryDisplay, colors.red, rowBg) - - row = row + 1 - end - end - - while row <= h - 2 do monFill(row, colors.black); row = row + 1 end end + smelterPage.tabs.craftTab.grid:setValues(availList) - -- Pagination (h - 1) - monFill(h - 1, colors.gray) - local pageStr = string.format("Pg %d/%d", smelterPage, smelterTotalPages) - monCenter(h - 1, pageStr, colors.white, colors.gray) - - if smelterPage > 1 then - monWrite(2, h - 1, " < ", colors.white, colors.lightGray) - addSmelterZone(2, h - 1, 4, h - 1, "page_prev", nil) - end - if smelterPage < smelterTotalPages then - monWrite(w - 3, h - 1, " > ", colors.white, colors.lightGray) - addSmelterZone(w - 3, h - 1, w - 1, h - 1, "page_next", nil) - end - - -- Bottom accent - monFill(h, colors.purple) - local bottomMsg = "" - if smelterView == "status" or smelterView == "smelt" then - local enabledCount = 0 - local totalRecipes = 0 - for _ in pairs(cfg.SMELTABLE) do totalRecipes = totalRecipes + 1 end - for inputName in pairs(cfg.SMELTABLE) do - if not state.disabledRecipes[inputName] then enabledCount = enabledCount + 1 end + -- Missing tab + local missList = {} + for idx, recipe in ipairs(cfg.CRAFTABLE) do + if not ops.canCraftRecipe(recipe) then + local short = shortName(recipe.output) + local missing = ops.getMissingIngredients(recipe) + local parts = {} + for _, m in ipairs(missing) do + local mShort = shortName(m.name) + table.insert(parts, string.format("%s %d/%d", mShort, m.have, m.need)) + end + table.insert(missList, { + idx = idx, + dispIdx = tostring(#missList + 1), + short = short .. " x" .. recipe.count, + summary = table.concat(parts, ", "), + }) end - bottomMsg = string.format(" Smelt: %d/%d enabled ", enabledCount, totalRecipes) - if activity.smelting then bottomMsg = " SMELTING... " end - elseif smelterView == "craft" then - bottomMsg = " Tap MAKE to craft " - if activity.crafting then bottomMsg = " CRAFTING... " end - elseif smelterView == "missing" then - local availC = craftAvailCount or 0 - bottomMsg = string.format(" Available: %d/%d recipes ", availC, #cfg.CRAFTABLE) end - monCenter(h, bottomMsg, colors.pink, colors.purple) - - draw.setVisible(true) - smelterTouchZones = smelterPendingZones + smelterPage.tabs.missingTab.grid:setValues(missList) end ------------------------------------------------- --- Touch handlers +-- Draw functions (called by inventoryManager tasks) +------------------------------------------------- + +function D.drawDashboard() + if not mainPage then + if mainDevice then + buildMainPage() + end + if not mainPage then return end + end + + -- Update dynamic data + D.refreshItemGrid() + + -- Update refresh button state + if activity.scanning then + mainPage.refreshBtn.text = "Scanning" + mainPage.refreshBtn.backgroundColor = colors.yellow + mainPage.refreshBtn.textColor = colors.black + else + mainPage.refreshBtn.text = "Refresh" + mainPage.refreshBtn.backgroundColor = colors.green + mainPage.refreshBtn.textColor = colors.white + end + + -- Draw everything + mainPage:draw() + mainDevice:sync() +end + +function D.drawSmelterDashboard() + if not smelterPage then + if smelterDevice then + buildSmelterPage() + end + if not smelterPage then return end + end + + D.refreshSmelterData() + + smelterPage:draw() + smelterDevice:sync() +end + +------------------------------------------------- +-- Touch handlers (compatibility bridge) +-- Route monitor_touch events through Opus UI ------------------------------------------------- function D.handleTouch(x, y) - local action, data = hitTest(x, y) - if not action then - log.debug("TOUCH", "No zone hit") - return - end + if not mainPage or not mainDevice then return end - if action == "amount" then - selectedAmount = data - log.debug("UI", "Amount set to %s", data) - state.needsRedraw = true + local clickEvent = mainPage:pointToChild(x, y) + if clickEvent and clickEvent.element then + clickEvent.type = 'mouse_click' + clickEvent.key = 'mouse_click' + clickEvent.button = 1 + clickEvent.ie = { code = 'mouse_click', x = clickEvent.x, y = clickEvent.y } - elseif action == "order" then - local itemName = data - if itemName then - local short = itemName:gsub("^minecraft:", ""):gsub("_", " ") - state.statusMessage = string.format("Ordering %s x%d...", short, selectedAmount) - state.statusColor = colors.cyan - state.statusTimer = 10 - activity.dispensing = true - state.needsRedraw = true - ops.orderItem(itemName, selectedAmount) + if clickEvent.element.focus then + mainPage:setFocus(clickEvent.element) end - - elseif action == "scan" then - state.statusMessage = "Refreshing..." - state.statusColor = colors.cyan - state.statusTimer = 3 - state.needsRedraw = true - log.debug("UI", "Manual refresh") - - elseif action == "kb_toggle" then - showKeyboard = not showKeyboard - log.debug("UI", "Keyboard %s", showKeyboard and "open" or "closed") - state.needsRedraw = true - - elseif action == "kb_key" then - if #searchQuery < 30 then - searchQuery = searchQuery .. data - end - currentPage = 1 - state.needsRedraw = true - - elseif action == "kb_bksp" then - if #searchQuery > 0 then - searchQuery = searchQuery:sub(1, -2) - end - currentPage = 1 - state.needsRedraw = true - - elseif action == "kb_space" then - if #searchQuery < 30 then - searchQuery = searchQuery .. " " - end - currentPage = 1 - state.needsRedraw = true - - elseif action == "kb_done" then - showKeyboard = false - log.debug("UI", "Keyboard closed") - state.needsRedraw = true - - elseif action == "kb_clear" then - searchQuery = "" - currentPage = 1 - log.debug("UI", "Search cleared") - state.needsRedraw = true - - elseif action == "page_prev" then - if currentPage > 1 then - currentPage = currentPage - 1 - log.debug("UI", "Page %d", currentPage) - end - state.needsRedraw = true - - elseif action == "page_next" then - if currentPage < totalPages then - currentPage = currentPage + 1 - log.debug("UI", "Page %d", currentPage) - end - state.needsRedraw = true + clickEvent.element:emit(clickEvent) + mainPage:sync() end end function D.handleSmelterTouch(x, y) - local action, data = smelterHitTest(x, y) - if not action then return end + if not smelterPage or not smelterDevice then return end - if action == "tab" then - smelterView = data - smelterPage = 1 - log.debug("UI", "Tab: %s", data) - state.smelterNeedsRedraw = true + local clickEvent = smelterPage:pointToChild(x, y) + if clickEvent and clickEvent.element then + clickEvent.type = 'mouse_click' + clickEvent.key = 'mouse_click' + clickEvent.button = 1 + clickEvent.ie = { code = 'mouse_click', x = clickEvent.x, y = clickEvent.y } - elseif action == "toggle_pause" then - state.smeltingPaused = not state.smeltingPaused - log.debug("UI", "Smelting %s", state.smeltingPaused and "PAUSED" or "RESUMED") - ops.saveDisabledRecipes() - state.smelterNeedsRedraw = true - state.needsRedraw = true - - elseif action == "toggle_recipe" then - if state.disabledRecipes[data] then - state.disabledRecipes[data] = nil - else - state.disabledRecipes[data] = true - end - local short = data:gsub("^minecraft:", ""):gsub("_", " ") - log.debug("UI", "Recipe %s: %s", short, state.disabledRecipes[data] and "OFF" or "ON") - ops.saveDisabledRecipes() - state.smelterNeedsRedraw = true - - elseif action == "enable_all" then - state.disabledRecipes = {} - log.debug("UI", "All recipes enabled") - ops.saveDisabledRecipes() - state.smelterNeedsRedraw = true - - elseif action == "disable_all" then - for inputName in pairs(cfg.SMELTABLE) do - state.disabledRecipes[inputName] = true - end - log.debug("UI", "All recipes disabled") - ops.saveDisabledRecipes() - state.smelterNeedsRedraw = true - - elseif action == "page_prev" then - if smelterPage > 1 then - smelterPage = smelterPage - 1 - end - state.smelterNeedsRedraw = true - - elseif action == "page_next" then - if smelterPage < smelterTotalPages then - smelterPage = smelterPage + 1 - end - state.smelterNeedsRedraw = true - - elseif action == "craft" then - local recipeIdx = data - local recipe = cfg.CRAFTABLE[recipeIdx] - if recipe then - local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ") - log.info("CRAFT", "Craft request: %s (#%d)", short, recipeIdx) - local ok, err = ops.craftItem(recipeIdx) - if ok then - state.statusMessage = "Crafted " .. short .. " x" .. recipe.count - state.statusColor = colors.lime - else - state.statusMessage = "Craft failed: " .. (err or "unknown") - state.statusColor = colors.red - end - state.statusTimer = 5 - state.needsRedraw = true - state.smelterNeedsRedraw = true + if clickEvent.element.focus then + smelterPage:setFocus(clickEvent.element) end + clickEvent.element:emit(clickEvent) + smelterPage:sync() end end return D -end +end \ No newline at end of file diff --git a/manager/display.lua.bak b/manager/display.lua.bak new file mode 100644 index 0000000..48527e0 --- /dev/null +++ b/manager/display.lua.bak @@ -0,0 +1,998 @@ +-- manager/display.lua — Dashboard rendering and touch handlers +-- Usage: local display = dofile("manager/display.lua")(ctx) + +return function(ctx) + +local cfg = ctx.cfg +local state = ctx.state +local log = ctx.log +local ui = ctx.ui +local ops = ctx.ops + +local cache = state.cache +local activity = state.activity + +local D = {} + +------------------------------------------------- +-- Monitor handles (set during init) +------------------------------------------------- + +D.mon = nil +D.monName = nil +D.smelterMon = nil +D.smelterMonName = nil + +function D.setupMonitor() + D.mon, D.monName = ui.setupMonitor(cfg.MONITOR_SIDE, cfg.SMELTER_MONITOR_SIDE) + return D.mon ~= nil +end + +function D.setupSmelterMonitor() + D.smelterMon, D.smelterMonName = ui.setupSmelterMonitor(cfg.SMELTER_MONITOR_SIDE, D.monName) + return D.smelterMon ~= nil +end + +------------------------------------------------- +-- Main dashboard UI state +------------------------------------------------- + +local selectedAmount = 1 +local amountOptions = {1, 4, 8, 16, 32, 64} + +local touchZones = {} +local pendingZones = {} + +local currentPage = 1 +local totalPages = 1 +local searchQuery = "" +local showKeyboard = false + +local kbRows = { + {"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"}, +} + +------------------------------------------------- +-- Smelter dashboard UI state +------------------------------------------------- + +local smelterView = "status" +local smelterPage = 1 +local smelterTotalPages = 1 +local smelterTouchZones = {} +local smelterPendingZones = {} + +------------------------------------------------- +-- Drawing helpers (delegated to shared ui module) +------------------------------------------------- + +local draw = nil + +local function setDrawTarget(target) + draw = target + ui.draw = target +end + +local function monWrite(x, y, text, fg, bg) ui.monWrite(x, y, text, fg, bg) end +local function monFill(y, color) ui.monFill(y, color) end +local function monCenter(y, text, fg, bg) ui.monCenter(y, text, fg, bg) end +local function monBar(x, y, w, r, bc, bgc) ui.monBar(x, y, w, r, bc, bgc) end +local function drawButton(x, y, t, fg, bg, pl, pr) return ui.drawButton(x, y, t, fg, bg, pl, pr) end + +local function addZone(x1, y1, x2, y2, action, data) + ui.addZone(pendingZones, x1, y1, x2, y2, action, data) +end + +local function hitTest(x, y) + return ui.hitTest(touchZones, x, y) +end + +local function addSmelterZone(x1, y1, x2, y2, action, data) + ui.addZone(smelterPendingZones, x1, y1, x2, y2, action, data) +end + +local function smelterHitTest(x, y) + return ui.hitTest(smelterTouchZones, x, y) +end + +local function getFilteredItems() + state.ensureItemList() + return ui.getFilteredItems(cache.itemList, searchQuery) +end + +------------------------------------------------- +-- Main dashboard drawing +------------------------------------------------- + +function D.drawDashboard() + if not D.mon then return end + + local w, h = D.mon.getSize() + pendingZones = {} + + setDrawTarget(window.create(D.mon, 1, 1, w, h, false)) + draw.setBackgroundColor(colors.black) + draw.clear() + + -- Title bar + monFill(1, colors.blue) + monCenter(1, " ** INVENTORY MANAGER ** ", colors.white, colors.blue) + + -- Status bar + monFill(2, colors.gray) + local statusParts = {} + table.insert(statusParts, string.format(" Chests: %d", cache.chestCount)) + table.insert(statusParts, cache.dropperOk and "Dropper: OK" or "Dropper: --") + table.insert(statusParts, cache.barrelOk and "Barrel: OK" or "Barrel: --") + if cache.furnaceCount and cache.furnaceCount > 0 then + table.insert(statusParts, string.format("Furnaces: %d", cache.furnaceCount)) + end + + local actParts = {} + if activity.sorting then table.insert(actParts, "SORTING") end + if activity.dispensing then table.insert(actParts, "DISPENSING") end + if activity.smelting then table.insert(actParts, "SMELTING") end + if activity.scanning then table.insert(actParts, "SCANNING") end + if activity.defragging then table.insert(actParts, "DEFRAG") end + if activity.composting then table.insert(actParts, "COMPOST") end + + monWrite(2, 2, table.concat(statusParts, " | "), colors.white, colors.gray) + + if #actParts > 0 then + local actStr = " " .. table.concat(actParts, " | ") .. " " + monWrite(w - #actStr, 2, actStr, colors.white, colors.orange) + end + + -- Divider + monFill(3, colors.lightBlue) + monCenter(3, string.rep("-", math.min(w - 4, 60)), colors.cyan, colors.lightBlue) + + -- Storage capacity + monFill(4, colors.black) + local capLabel = string.format(" Storage: %d/%d slots (%d free)", + cache.usedSlots, cache.totalSlots, cache.freeSlots) + monWrite(2, 4, capLabel, colors.lightGray, colors.black) + + local barStart = #capLabel + 4 + local barWidth = w - barStart - 2 + if barWidth > 4 then + local barColor = colors.lime + if cache.usedRatio > 0.9 then barColor = colors.red + elseif cache.usedRatio > 0.7 then barColor = colors.orange + elseif cache.usedRatio > 0.5 then barColor = colors.yellow + end + monBar(barStart, 4, barWidth, cache.usedRatio, barColor, colors.gray) + local pctStr = string.format(" %d%% ", math.floor(cache.usedRatio * 100)) + local pctX = barStart + math.floor(barWidth / 2) - math.floor(#pctStr / 2) + monWrite(pctX, 4, pctStr, colors.white, barColor) + end + + -- Amount selector (row 5) + monFill(5, colors.black) + monWrite(2, 5, "Qty:", colors.lightGray, colors.black) + local btnX = 7 + for _, amt in ipairs(amountOptions) do + local label = tostring(amt) + local bg = (amt == selectedAmount) and colors.cyan or colors.gray + local fg = (amt == selectedAmount) and colors.white or colors.lightGray + local x1, y1, x2, y2 = drawButton(btnX, 5, label, fg, bg) + addZone(x1, y1, x2, y2, "amount", amt) + btnX = x2 + 2 + end + + local refreshBg = activity.scanning and colors.yellow or colors.green + local refreshFg = activity.scanning and colors.black or colors.white + local refreshTxt = activity.scanning and "Scanning" or "Refresh" + local scanX = w - #refreshTxt - 3 + local sx1, sy1, sx2, sy2 = drawButton(scanX, 5, refreshTxt, refreshFg, refreshBg, 1, 1) + addZone(sx1, sy1, sx2, sy2, "scan", nil) + + -- Search bar + Pagination (row 6) + monFill(6, colors.black) + + local kbLabel = showKeyboard and " X " or " ? " + local kbBg = showKeyboard and colors.red or colors.purple + monWrite(2, 6, kbLabel, colors.white, kbBg) + addZone(2, 6, 4, 6, "kb_toggle", nil) + + local queryDisplay = searchQuery + if showKeyboard then + queryDisplay = queryDisplay .. "|" + elseif queryDisplay == "" then + queryDisplay = "search..." + end + local fieldW = math.floor(w * 0.4) + if fieldW < 10 then fieldW = 10 end + local displayText = queryDisplay:sub(1, fieldW) + displayText = displayText .. string.rep("_", math.max(0, fieldW - #displayText)) + monWrite(6, 6, displayText, + (searchQuery == "" and not showKeyboard) and colors.gray or colors.white, + colors.black) + addZone(6, 6, 5 + fieldW, 6, "kb_toggle", nil) + + local filteredItems = getFilteredItems() + + local maxRows = h - 10 + if maxRows < 1 then maxRows = 1 end + totalPages = math.max(1, math.ceil(#filteredItems / maxRows)) + if currentPage > totalPages then currentPage = totalPages end + if currentPage < 1 then currentPage = 1 end + + local pageStr = string.format("Pg %d/%d", currentPage, totalPages) + local navW = 3 + 1 + #pageStr + 1 + 3 + local navX = w - navW + + if currentPage > 1 then + monWrite(navX, 6, " < ", colors.white, colors.gray) + addZone(navX, 6, navX + 2, 6, "page_prev", nil) + else + monWrite(navX, 6, " < ", colors.lightGray, colors.black) + end + + monWrite(navX + 4, 6, pageStr, colors.lightGray, colors.black) + + local nextX = navX + 4 + #pageStr + 1 + if currentPage < totalPages then + monWrite(nextX, 6, " > ", colors.white, colors.gray) + addZone(nextX, 6, nextX + 2, 6, "page_next", nil) + else + monWrite(nextX, 6, " > ", colors.lightGray, colors.black) + end + + -- Column headers (row 7) + local row = 7 + monFill(row, colors.gray) + monWrite(2, row, "#", colors.lightGray, colors.gray) + monWrite(5, row, "Item", colors.lightGray, colors.gray) + monWrite(w - 22, row, "Qty", colors.lightGray, colors.gray) + monWrite(w - 14, row, "Stock", colors.lightGray, colors.gray) + monWrite(w - 1, row, ">", colors.lightGray, colors.gray) + row = row + 1 + + -- Item rows + local maxCount = 0 + for _, item in ipairs(filteredItems) do + if item.total > maxCount then maxCount = item.total end + end + if maxCount == 0 then maxCount = 1 end + + local startIdx = (currentPage - 1) * maxRows + 1 + local endIdx = math.min(startIdx + maxRows - 1, #filteredItems) + + if #filteredItems == 0 then + monFill(8, colors.black) + monFill(9, colors.black) + if searchQuery ~= "" then + monCenter(9, "No items match \"" .. searchQuery .. "\"", colors.gray, colors.black) + else + monCenter(9, "No items in storage", colors.gray, colors.black) + end + row = 10 + else + for i = startIdx, endIdx do + local item = filteredItems[i] + local y = row + local short = item.name:gsub("^minecraft:", ""):gsub("_", " ") + short = short:sub(1,1):upper() .. short:sub(2) + + local maxNameLen = w - 30 + if #short > maxNameLen then + short = short:sub(1, maxNameLen - 2) .. ".." + end + + local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray + monFill(y, rowBg) + + monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg) + monWrite(5, y, short, colors.white, rowBg) + monWrite(w - 22, y, tostring(item.total), colors.yellow, rowBg) + + local ratio = item.total / maxCount + local barColor = colors.lime + if ratio < 0.25 then barColor = colors.red + elseif ratio < 0.5 then barColor = colors.orange + end + monBar(w - 14, y, 12, ratio, barColor, rowBg == colors.gray and colors.lightGray or colors.gray) + + monWrite(w - 1, y, ">", colors.orange, rowBg) + addZone(1, y, w, y, "order", item.name) + + row = row + 1 + end + end + + local lastItemRow = h - 3 + while row <= lastItemRow do + monFill(row, colors.black) + row = row + 1 + end + + if showKeyboard then + local keyW = 3 + local keyGap = 1 + + local kbDefs = { + { keys = kbRows[1], specials = {{ label = " Bksp ", action = "kb_bksp", bg = colors.red }} }, + { keys = kbRows[2], specials = {{ label = " Done ", action = "kb_done", bg = colors.green }} }, + { keys = kbRows[3], specials = { + { label = " Space ", action = "kb_space", bg = colors.lightGray }, + { label = " Clr ", action = "kb_clear", bg = colors.orange }, + }}, + } + + for rowIdx, def in ipairs(kbDefs) do + local y = h - 3 + rowIdx + monFill(y, colors.black) + + local keysW = #def.keys * keyW + math.max(0, #def.keys - 1) * keyGap + local specialsW = 0 + for _, sp in ipairs(def.specials) do + specialsW = specialsW + keyGap + #sp.label + end + local rowW = keysW + specialsW + local x = math.floor((w - rowW) / 2) + 1 + + for ki, key in ipairs(def.keys) do + monWrite(x, y, " " .. key .. " ", colors.white, colors.gray) + addZone(x, y, x + keyW - 1, y, "kb_key", key:lower()) + x = x + keyW + if ki < #def.keys then x = x + keyGap end + end + + for _, sp in ipairs(def.specials) do + x = x + keyGap + monWrite(x, y, sp.label, colors.white, sp.bg) + addZone(x, y, x + #sp.label - 1, y, sp.action, nil) + x = x + #sp.label + end + end + else + -- Status message + monFill(h - 2, colors.black) + if #state.activeAlerts > 0 then + local alertIdx = math.floor(os.epoch("utc") / 2000) % #state.activeAlerts + 1 + local a = state.activeAlerts[alertIdx] + local alertMsg = string.format(" LOW STOCK: %s (%d/%d) ", a.label, a.current, a.min) + monCenter(h - 2, alertMsg, colors.white, colors.red) + elseif state.statusTimer > 0 and #state.statusMessage > 0 then + monCenter(h - 2, state.statusMessage, state.statusColor, colors.black) + end + + -- Footer + state.ensureItemList() + monFill(h - 1, colors.gray) + local footerLeft = string.format(" Total: %d items | %d types ", + cache.grandTotal, #cache.itemList) + monWrite(2, h - 1, footerLeft, colors.white, colors.gray) + + if searchQuery ~= "" then + local filterNote = string.format("| Showing %d ", #filteredItems) + monWrite(2 + #footerLeft + 1, h - 1, filterNote, colors.yellow, colors.gray) + end + + local timeStr = textutils.formatTime(os.time(), true) + monWrite(w - #timeStr - 1, h - 1, timeStr, colors.lightGray, colors.gray) + + -- Bottom accent + monFill(h, colors.blue) + local bottomMsg = " Tap item to order " + if activity.dispensing then + bottomMsg = " DISPENSING... " + elseif activity.smelting then + bottomMsg = " SMELTING... " + elseif activity.sorting then + bottomMsg = " SORTING BARREL... " + elseif activity.defragging then + bottomMsg = " DEFRAGMENTING... " + elseif activity.composting then + bottomMsg = " COMPOSTING... " + end + monCenter(h, bottomMsg, colors.lightBlue, colors.blue) + end + + draw.setVisible(true) + touchZones = pendingZones +end + +------------------------------------------------- +-- Smelter dashboard drawing +------------------------------------------------- + +function D.drawSmelterDashboard() + if not D.smelterMon then return end + + local w, h = D.smelterMon.getSize() + smelterPendingZones = {} + + setDrawTarget(window.create(D.smelterMon, 1, 1, w, h, false)) + draw.setBackgroundColor(colors.black) + draw.clear() + + -- Title bar + monFill(1, colors.purple) + monCenter(1, " ** SMELTER DASHBOARD ** ", colors.white, colors.purple) + + -- Status bar + monFill(2, colors.gray) + local activeCount = 0 + for _, fs in ipairs(cache.furnaceStatus or {}) do + if fs.active then activeCount = activeCount + 1 end + end + local statusStr = string.format(" Furnaces: %d Active: %d", + cache.furnaceCount or 0, activeCount) + monWrite(2, 2, statusStr, colors.white, colors.gray) + + local pauseLabel = state.smeltingPaused and " PAUSED " or " ACTIVE " + local pauseBg = state.smeltingPaused and colors.red or colors.lime + local pauseFg = state.smeltingPaused and colors.white or colors.black + monWrite(w - #pauseLabel, 2, pauseLabel, pauseFg, pauseBg) + addSmelterZone(w - #pauseLabel, 2, w - 1, 2, "toggle_pause", nil) + + -- Divider + monFill(3, colors.magenta) + monCenter(3, string.rep("-", math.min(w - 4, 60)), colors.pink, colors.magenta) + + -- Tab row + monFill(4, colors.black) + local tabStatusBg = smelterView == "status" and colors.purple or colors.gray + local tabSmeltBg = smelterView == "smelt" and colors.purple or colors.gray + local tabCraftBg = smelterView == "craft" and colors.purple or colors.gray + local tabMissingBg = smelterView == "missing" and colors.purple or colors.gray + local bx1, by1, bx2, by2 + bx1, by1, bx2, by2 = drawButton(2, 4, "Status", colors.white, tabStatusBg) + addSmelterZone(bx1, by1, bx2, by2, "tab", "status") + bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "Smelt", colors.white, tabSmeltBg) + addSmelterZone(bx1, by1, bx2, by2, "tab", "smelt") + bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "Craft", colors.white, tabCraftBg) + addSmelterZone(bx1, by1, bx2, by2, "tab", "craft") + bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "Missing", colors.white, tabMissingBg) + addSmelterZone(bx1, by1, bx2, by2, "tab", "missing") + + local craftAvailCount = nil + + if smelterView == "status" then + -- Furnace Status View + monFill(5, colors.gray) + local outCol = math.floor(w * 0.40) + local fuelCol = math.floor(w * 0.65) + local statCol = w - 6 + monWrite(2, 5, "#", colors.lightGray, colors.gray) + monWrite(4, 5, "T", colors.lightGray, colors.gray) + monWrite(6, 5, "Input", colors.lightGray, colors.gray) + monWrite(outCol, 5, "Output", colors.lightGray, colors.gray) + monWrite(fuelCol, 5, "Fuel", colors.lightGray, colors.gray) + monWrite(statCol, 5, "State", colors.lightGray, colors.gray) + + local furnaceList = cache.furnaceStatus or {} + local maxRows = h - 8 + if maxRows < 1 then maxRows = 1 end + smelterTotalPages = math.max(1, math.ceil(#furnaceList / maxRows)) + if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end + if smelterPage < 1 then smelterPage = 1 end + + local startIdx = (smelterPage - 1) * maxRows + 1 + local endIdx = math.min(startIdx + maxRows - 1, #furnaceList) + + local row = 6 + if #furnaceList == 0 then + monFill(7, colors.black) + monCenter(7, "No furnaces found on network", colors.gray, colors.black) + row = 8 + else + for i = startIdx, endIdx do + local fs = furnaceList[i] + local y = row + local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray + monFill(y, rowBg) + + monWrite(2, y, string.format("%d", i), colors.lightBlue, rowBg) + + local typeAbbr = "F" + local typeColor = colors.orange + if fs.type == "minecraft:smoker" then + typeAbbr = "S" + typeColor = colors.green + elseif fs.type == "minecraft:blast_furnace" then + typeAbbr = "B" + typeColor = colors.cyan + end + monWrite(4, y, typeAbbr, typeColor, rowBg) + + if fs.input then + local inName = fs.input.name:gsub("^minecraft:", ""):gsub("_", " ") + local maxIn = outCol - 8 + if #inName > maxIn then inName = inName:sub(1, maxIn - 2) .. ".." end + monWrite(6, y, inName, colors.white, rowBg) + monWrite(outCol - 4, y, "x" .. fs.input.count, colors.yellow, rowBg) + else + monWrite(6, y, "(empty)", colors.lightGray, rowBg) + end + + if fs.output then + local outName = fs.output.name:gsub("^minecraft:", ""):gsub("_", " ") + local maxOut = fuelCol - outCol - 5 + if #outName > maxOut then outName = outName:sub(1, maxOut - 2) .. ".." end + monWrite(outCol, y, outName, colors.white, rowBg) + monWrite(fuelCol - 4, y, "x" .. fs.output.count, colors.yellow, rowBg) + else + monWrite(outCol, y, "-", colors.lightGray, rowBg) + end + + if fs.fuel then + local fuelName = fs.fuel.name:gsub("^minecraft:", ""):gsub("_", " ") + local maxFuel = statCol - fuelCol - 4 + if #fuelName > maxFuel then fuelName = fuelName:sub(1, maxFuel - 2) .. ".." end + monWrite(fuelCol, y, fuelName, colors.white, rowBg) + monWrite(statCol - 4, y, "x" .. fs.fuel.count, colors.yellow, rowBg) + else + monWrite(fuelCol, y, "-", colors.lightGray, rowBg) + end + + if state.smeltingPaused then + monWrite(statCol, y, "PAUSE", colors.red, rowBg) + elseif fs.active then + monWrite(statCol, y, " COOK", colors.lime, rowBg) + elseif fs.input and not fs.fuel then + monWrite(statCol, y, "FUEL?", colors.orange, rowBg) + else + monWrite(statCol, y, " IDLE", colors.lightGray, rowBg) + end + + row = row + 1 + end + end + + while row <= h - 2 do monFill(row, colors.black); row = row + 1 end + + elseif smelterView == "smelt" then + -- Smelt Recipe Manager View + local recipeList = {} + for inputName, recipe in pairs(cfg.SMELTABLE) do + local short = inputName:gsub("^minecraft:", ""):gsub("_", " ") + short = short:sub(1,1):upper() .. short:sub(2) + local resultShort = recipe.result:gsub("^minecraft:", ""):gsub("_", " ") + resultShort = resultShort:sub(1,1):upper() .. resultShort:sub(2) + local types = "" + if recipe.furnaceSet["minecraft:furnace"] then types = types .. "F" end + if recipe.furnaceSet["minecraft:smoker"] then types = types .. "S" end + if recipe.furnaceSet["minecraft:blast_furnace"] then types = types .. "B" end + local enabled = not state.disabledRecipes[inputName] + local inStorage = 0 + if cache.catalogue[inputName] then + for _, s in ipairs(cache.catalogue[inputName]) do + inStorage = inStorage + s.total + end + end + table.insert(recipeList, { + inputName = inputName, + inputShort = short, + resultShort = resultShort, + types = types, + enabled = enabled, + inStorage = inStorage, + }) + end + table.sort(recipeList, function(a, b) return a.inputShort < b.inputShort end) + + local arrowCol = math.floor(w * 0.30) + local typeCol = math.floor(w * 0.60) + local stockCol = math.floor(w * 0.72) + local toggleCol = w - 5 + + monFill(5, colors.gray) + monWrite(2, 5, "Input", colors.lightGray, colors.gray) + monWrite(arrowCol, 5, "Output", colors.lightGray, colors.gray) + monWrite(typeCol, 5, "Type", colors.lightGray, colors.gray) + monWrite(stockCol, 5, "Stock", colors.lightGray, colors.gray) + monWrite(toggleCol, 5, "On?", colors.lightGray, colors.gray) + + local bulkX = w - 22 + bx1, by1, bx2, by2 = drawButton(bulkX, 4, "All On", colors.white, colors.green) + addSmelterZone(bx1, by1, bx2, by2, "enable_all", nil) + bx1, by1, bx2, by2 = drawButton(bx2 + 2, 4, "All Off", colors.white, colors.red) + addSmelterZone(bx1, by1, bx2, by2, "disable_all", nil) + + local maxRows = h - 8 + if maxRows < 1 then maxRows = 1 end + smelterTotalPages = math.max(1, math.ceil(#recipeList / maxRows)) + if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end + if smelterPage < 1 then smelterPage = 1 end + + local startIdx = (smelterPage - 1) * maxRows + 1 + local endIdx = math.min(startIdx + maxRows - 1, #recipeList) + + local row = 6 + for i = startIdx, endIdx do + local r = recipeList[i] + local y = row + local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray + monFill(y, rowBg) + + local maxInputLen = arrowCol - 3 + local inputDisplay = r.inputShort + if #inputDisplay > maxInputLen then + inputDisplay = inputDisplay:sub(1, maxInputLen - 2) .. ".." + end + monWrite(2, y, inputDisplay, colors.white, rowBg) + + local maxOutLen = typeCol - arrowCol - 2 + local outDisplay = r.resultShort + if #outDisplay > maxOutLen then + outDisplay = outDisplay:sub(1, maxOutLen - 2) .. ".." + end + monWrite(arrowCol, y, outDisplay, colors.lightBlue, rowBg) + + monWrite(typeCol, y, r.types, colors.orange, rowBg) + monWrite(stockCol, y, tostring(r.inStorage), colors.yellow, rowBg) + + if r.enabled then + monWrite(toggleCol, y, " ON ", colors.white, colors.green) + else + monWrite(toggleCol, y, " OFF", colors.white, colors.red) + end + addSmelterZone(1, y, w, y, "toggle_recipe", r.inputName) + + row = row + 1 + end + + while row <= h - 2 do monFill(row, colors.black); row = row + 1 end + + elseif smelterView == "craft" then + -- Available Crafting Recipes + local turtleOk = ctx.craftTurtleName and peripheral.isPresent(ctx.craftTurtleName) + local tLabel = turtleOk and " Turtle OK " or " No Turtle " + local tBg = turtleOk and colors.lime or colors.red + local tFg = turtleOk and colors.black or colors.white + monWrite(w - #tLabel, 4, tLabel, tFg, tBg) + + local availList = {} + for idx, recipe in ipairs(cfg.CRAFTABLE) do + if ops.canCraftRecipe(recipe) then + local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ") + short = short:sub(1,1):upper() .. short:sub(2) + local batches = ops.maxCraftBatches(recipe) + table.insert(availList, { + idx = idx, + short = short, + count = recipe.count, + batches = batches, + }) + end + end + craftAvailCount = #availList + + monFill(5, colors.gray) + local makeCol = w - 6 + monWrite(2, 5, "#", colors.lightGray, colors.gray) + monWrite(4, 5, "Output", colors.lightGray, colors.gray) + monWrite(math.floor(w * 0.45), 5, "Yield", colors.lightGray, colors.gray) + monWrite(math.floor(w * 0.60), 5, "Can Make", colors.lightGray, colors.gray) + monWrite(makeCol, 5, "Go", colors.lightGray, colors.gray) + + local maxRows = h - 8 + if maxRows < 1 then maxRows = 1 end + smelterTotalPages = math.max(1, math.ceil(#availList / maxRows)) + if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end + if smelterPage < 1 then smelterPage = 1 end + + local startIdx = (smelterPage - 1) * maxRows + 1 + local endIdx = math.min(startIdx + maxRows - 1, #availList) + + local row = 6 + if #availList == 0 then + monFill(7, colors.black) + monCenter(7, "No recipes available to craft", colors.gray, colors.black) + row = 8 + else + for i = startIdx, endIdx do + local r = availList[i] + local y = row + local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray + monFill(y, rowBg) + + monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg) + + local maxNameLen = math.floor(w * 0.40) + local nameDisplay = r.short + if #nameDisplay > maxNameLen then + nameDisplay = nameDisplay:sub(1, maxNameLen - 2) .. ".." + end + monWrite(4, y, nameDisplay, colors.white, rowBg) + + monWrite(math.floor(w * 0.45), y, "x" .. r.count, colors.yellow, rowBg) + monWrite(math.floor(w * 0.60), y, + string.format("x%d", r.batches), colors.lime, rowBg) + + if turtleOk then + monWrite(makeCol, y, " MAKE ", colors.white, colors.green) + addSmelterZone(makeCol, y, makeCol + 5, y, "craft", r.idx) + else + monWrite(makeCol, y, " ---- ", colors.gray, colors.black) + end + + row = row + 1 + end + end + + while row <= h - 2 do monFill(row, colors.black); row = row + 1 end + + elseif smelterView == "missing" then + -- Unavailable Crafting Recipes + local missList = {} + for idx, recipe in ipairs(cfg.CRAFTABLE) do + if not ops.canCraftRecipe(recipe) then + local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ") + short = short:sub(1,1):upper() .. short:sub(2) + local missing = ops.getMissingIngredients(recipe) + local parts = {} + for _, m in ipairs(missing) do + local mShort = m.name:gsub("^minecraft:", ""):gsub("_", " ") + table.insert(parts, string.format("%s %d/%d", mShort, m.have, m.need)) + end + table.insert(missList, { + idx = idx, + short = short, + count = recipe.count, + summary = table.concat(parts, ", "), + }) + end + end + craftAvailCount = #cfg.CRAFTABLE - #missList + + monFill(5, colors.gray) + monWrite(2, 5, "#", colors.lightGray, colors.gray) + monWrite(4, 5, "Output", colors.lightGray, colors.gray) + monWrite(math.floor(w * 0.35), 5, "Missing (have/need)", colors.lightGray, colors.gray) + + local maxRows = h - 8 + if maxRows < 1 then maxRows = 1 end + smelterTotalPages = math.max(1, math.ceil(#missList / maxRows)) + if smelterPage > smelterTotalPages then smelterPage = smelterTotalPages end + if smelterPage < 1 then smelterPage = 1 end + + local startIdx = (smelterPage - 1) * maxRows + 1 + local endIdx = math.min(startIdx + maxRows - 1, #missList) + + local row = 6 + if #missList == 0 then + monFill(7, colors.black) + monCenter(7, "All recipes can be crafted!", colors.lime, colors.black) + row = 8 + else + for i = startIdx, endIdx do + local r = missList[i] + local y = row + local rowBg = ((i - startIdx) % 2 == 0) and colors.black or colors.gray + monFill(y, rowBg) + + monWrite(2, y, string.format("%2d", i), colors.lightBlue, rowBg) + + local nameCol = math.floor(w * 0.35) - 5 + local nameDisplay = r.short .. " x" .. r.count + if #nameDisplay > nameCol then + nameDisplay = nameDisplay:sub(1, nameCol - 2) .. ".." + end + monWrite(4, y, nameDisplay, colors.white, rowBg) + + local missCol = math.floor(w * 0.35) + local missW = w - missCol - 1 + local summaryDisplay = r.summary + if #summaryDisplay > missW then + summaryDisplay = summaryDisplay:sub(1, missW - 2) .. ".." + end + monWrite(missCol, y, summaryDisplay, colors.red, rowBg) + + row = row + 1 + end + end + + while row <= h - 2 do monFill(row, colors.black); row = row + 1 end + end + + -- Pagination (h - 1) + monFill(h - 1, colors.gray) + local pageStr = string.format("Pg %d/%d", smelterPage, smelterTotalPages) + monCenter(h - 1, pageStr, colors.white, colors.gray) + + if smelterPage > 1 then + monWrite(2, h - 1, " < ", colors.white, colors.lightGray) + addSmelterZone(2, h - 1, 4, h - 1, "page_prev", nil) + end + if smelterPage < smelterTotalPages then + monWrite(w - 3, h - 1, " > ", colors.white, colors.lightGray) + addSmelterZone(w - 3, h - 1, w - 1, h - 1, "page_next", nil) + end + + -- Bottom accent + monFill(h, colors.purple) + local bottomMsg = "" + if smelterView == "status" or smelterView == "smelt" then + local enabledCount = 0 + local totalRecipes = 0 + for _ in pairs(cfg.SMELTABLE) do totalRecipes = totalRecipes + 1 end + for inputName in pairs(cfg.SMELTABLE) do + if not state.disabledRecipes[inputName] then enabledCount = enabledCount + 1 end + end + bottomMsg = string.format(" Smelt: %d/%d enabled ", enabledCount, totalRecipes) + if activity.smelting then bottomMsg = " SMELTING... " end + elseif smelterView == "craft" then + bottomMsg = " Tap MAKE to craft " + if activity.crafting then bottomMsg = " CRAFTING... " end + elseif smelterView == "missing" then + local availC = craftAvailCount or 0 + bottomMsg = string.format(" Available: %d/%d recipes ", availC, #cfg.CRAFTABLE) + end + monCenter(h, bottomMsg, colors.pink, colors.purple) + + draw.setVisible(true) + smelterTouchZones = smelterPendingZones +end + +------------------------------------------------- +-- Touch handlers +------------------------------------------------- + +function D.handleTouch(x, y) + local action, data = hitTest(x, y) + if not action then + log.debug("TOUCH", "No zone hit") + return + end + + if action == "amount" then + selectedAmount = data + log.debug("UI", "Amount set to %s", data) + state.needsRedraw = true + + elseif action == "order" then + local itemName = data + if itemName then + local short = itemName:gsub("^minecraft:", ""):gsub("_", " ") + state.statusMessage = string.format("Ordering %s x%d...", short, selectedAmount) + state.statusColor = colors.cyan + state.statusTimer = 10 + activity.dispensing = true + state.needsRedraw = true + ops.orderItem(itemName, selectedAmount) + end + + elseif action == "scan" then + state.statusMessage = "Refreshing..." + state.statusColor = colors.cyan + state.statusTimer = 3 + state.needsRedraw = true + log.debug("UI", "Manual refresh") + + elseif action == "kb_toggle" then + showKeyboard = not showKeyboard + log.debug("UI", "Keyboard %s", showKeyboard and "open" or "closed") + state.needsRedraw = true + + elseif action == "kb_key" then + if #searchQuery < 30 then + searchQuery = searchQuery .. data + end + currentPage = 1 + state.needsRedraw = true + + elseif action == "kb_bksp" then + if #searchQuery > 0 then + searchQuery = searchQuery:sub(1, -2) + end + currentPage = 1 + state.needsRedraw = true + + elseif action == "kb_space" then + if #searchQuery < 30 then + searchQuery = searchQuery .. " " + end + currentPage = 1 + state.needsRedraw = true + + elseif action == "kb_done" then + showKeyboard = false + log.debug("UI", "Keyboard closed") + state.needsRedraw = true + + elseif action == "kb_clear" then + searchQuery = "" + currentPage = 1 + log.debug("UI", "Search cleared") + state.needsRedraw = true + + elseif action == "page_prev" then + if currentPage > 1 then + currentPage = currentPage - 1 + log.debug("UI", "Page %d", currentPage) + end + state.needsRedraw = true + + elseif action == "page_next" then + if currentPage < totalPages then + currentPage = currentPage + 1 + log.debug("UI", "Page %d", currentPage) + end + state.needsRedraw = true + end +end + +function D.handleSmelterTouch(x, y) + local action, data = smelterHitTest(x, y) + if not action then return end + + if action == "tab" then + smelterView = data + smelterPage = 1 + log.debug("UI", "Tab: %s", data) + state.smelterNeedsRedraw = true + + elseif action == "toggle_pause" then + state.smeltingPaused = not state.smeltingPaused + log.debug("UI", "Smelting %s", state.smeltingPaused and "PAUSED" or "RESUMED") + ops.saveDisabledRecipes() + state.smelterNeedsRedraw = true + state.needsRedraw = true + + elseif action == "toggle_recipe" then + if state.disabledRecipes[data] then + state.disabledRecipes[data] = nil + else + state.disabledRecipes[data] = true + end + local short = data:gsub("^minecraft:", ""):gsub("_", " ") + log.debug("UI", "Recipe %s: %s", short, state.disabledRecipes[data] and "OFF" or "ON") + ops.saveDisabledRecipes() + state.smelterNeedsRedraw = true + + elseif action == "enable_all" then + state.disabledRecipes = {} + log.debug("UI", "All recipes enabled") + ops.saveDisabledRecipes() + state.smelterNeedsRedraw = true + + elseif action == "disable_all" then + for inputName in pairs(cfg.SMELTABLE) do + state.disabledRecipes[inputName] = true + end + log.debug("UI", "All recipes disabled") + ops.saveDisabledRecipes() + state.smelterNeedsRedraw = true + + elseif action == "page_prev" then + if smelterPage > 1 then + smelterPage = smelterPage - 1 + end + state.smelterNeedsRedraw = true + + elseif action == "page_next" then + if smelterPage < smelterTotalPages then + smelterPage = smelterPage + 1 + end + state.smelterNeedsRedraw = true + + elseif action == "craft" then + local recipeIdx = data + local recipe = cfg.CRAFTABLE[recipeIdx] + if recipe then + local short = recipe.output:gsub("^minecraft:", ""):gsub("_", " ") + log.info("CRAFT", "Craft request: %s (#%d)", short, recipeIdx) + local ok, err = ops.craftItem(recipeIdx) + if ok then + state.statusMessage = "Crafted " .. short .. " x" .. recipe.count + state.statusColor = colors.lime + else + state.statusMessage = "Craft failed: " .. (err or "unknown") + state.statusColor = colors.red + end + state.statusTimer = 5 + state.needsRedraw = true + state.smelterNeedsRedraw = true + end + end +end + +return D + +end