-- lib/ui.lua — Shared UI helpers for manager & client -- Usage: local ui = dofile("lib/ui.lua") -- -- Set ui.draw to the current render target (window/monitor) -- before calling any drawing functions. local ui = {} ------------------------------------------------- -- Drawing target — set this before drawing ------------------------------------------------- ui.draw = nil ------------------------------------------------- -- Drawing primitives ------------------------------------------------- function ui.monWrite(x, y, text, fg, bg) ui.draw.setCursorPos(x, y) if fg then ui.draw.setTextColor(fg) end if bg then ui.draw.setBackgroundColor(bg) end ui.draw.write(text) end function ui.monFill(y, color) local w, _ = ui.draw.getSize() ui.draw.setCursorPos(1, y) ui.draw.setBackgroundColor(color) ui.draw.write(string.rep(" ", w)) end function ui.monCenter(y, text, fg, bg) local w, _ = ui.draw.getSize() local x = math.floor((w - #text) / 2) + 1 ui.monWrite(x, y, text, fg, bg) end function ui.monBar(x, y, width, ratio, barColor, bgColor) local filled = math.floor(ratio * width) ui.draw.setCursorPos(x, y) ui.draw.setBackgroundColor(barColor) ui.draw.write(string.rep(" ", filled)) ui.draw.setBackgroundColor(bgColor) ui.draw.write(string.rep(" ", width - filled)) end function ui.drawButton(x, y, text, fg, bg, padLeft, padRight) padLeft = padLeft or 1 padRight = padRight or 1 local full = string.rep(" ", padLeft) .. text .. string.rep(" ", padRight) ui.monWrite(x, y, full, fg, bg) return x, y, x + #full - 1, y end ------------------------------------------------- -- Touch zone management ------------------------------------------------- --- Append a clickable zone to a pending-zone list. -- @param zones table — the pending zone array to append to function ui.addZone(zones, x1, y1, x2, y2, action, data) table.insert(zones, { x1 = x1, y1 = y1, x2 = x2, y2 = y2, action = action, data = data, }) end --- Find which zone was hit. -- @param zones table — the active zone array to search function ui.hitTest(zones, x, y) for _, zone in ipairs(zones) do if x >= zone.x1 and x <= zone.x2 and y >= zone.y1 and y <= zone.y2 then return zone.action, zone.data end end return nil, nil end ------------------------------------------------- -- Monitor setup ------------------------------------------------- --- Wrap and configure the main inventory monitor. -- @param side string — preferred peripheral side/name -- @param excludeSide string — side to skip (e.g. smelter monitor) -- @return mon, monName or nil, nil function ui.setupMonitor(side, excludeSide) local mon = peripheral.wrap(side) local monName if mon and mon.setTextScale then monName = side else mon = nil end 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 end if not mon then return nil, nil end mon.setTextScale(0.5) mon.clear() return mon, monName end --- Wrap and configure the smelter monitor. -- @param side string — preferred peripheral side/name -- @param excludeName string — main monitor name to skip -- @return smelterMon, smelterMonName or nil, nil function ui.setupSmelterMonitor(side, excludeName) local smelterMon = peripheral.wrap(side) local smelterMonName if smelterMon and smelterMon.setTextScale then smelterMonName = side else smelterMon = nil end if not smelterMon then for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == "monitor" and name ~= excludeName then smelterMon = peripheral.wrap(name) smelterMonName = name break end end end if not smelterMon then return nil, nil end smelterMon.setTextScale(0.5) smelterMon.clear() return smelterMon, smelterMonName end ------------------------------------------------- -- Item filtering ------------------------------------------------- --- Filter an item list by search query. -- @param itemList table — array of { name, total } -- @param searchQuery string — filter text (case-insensitive, plain match) -- @return filtered table — matching subset function ui.getFilteredItems(itemList, searchQuery) local filtered = {} for _, item in ipairs(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 ------------------------------------------------- -- Crafting recipe helpers ------------------------------------------------- --- Count ingredients needed for one craft. function ui.getRecipeIngredients(recipe) local ingredients = {} for _, item in ipairs(recipe.grid) do if item then ingredients[item] = (ingredients[item] or 0) + 1 end end return ingredients end --- Check if a recipe can be crafted with current stock. -- @param recipe table — recipe with .grid -- @param getTotal function(itemName) -> number — returns stock count function ui.canCraftRecipe(recipe, getTotal) local ingredients = ui.getRecipeIngredients(recipe) for itemName, needed in pairs(ingredients) do if (getTotal(itemName) or 0) < needed then return false end end return true end --- How many batches can be crafted. -- @param recipe table — recipe with .grid -- @param getTotal function(itemName) -> number function ui.maxCraftBatches(recipe, getTotal) local ingredients = ui.getRecipeIngredients(recipe) local minBatches = math.huge for itemName, needed in pairs(ingredients) do local batches = math.floor((getTotal(itemName) or 0) / needed) if batches < minBatches then minBatches = batches end end if minBatches == math.huge then return 0 end return minBatches end --- Get list of ingredients where stock < needed. -- @param recipe table — recipe with .grid -- @param getTotal function(itemName) -> number function ui.getMissingIngredients(recipe, getTotal) local ingredients = ui.getRecipeIngredients(recipe) local missing = {} for itemName, needed in pairs(ingredients) do local have = getTotal(itemName) or 0 if have < needed then table.insert(missing, { name = itemName, have = have, need = needed }) end end return missing end return ui