commit 628381ef6182480a9372f3684b6f0694a7d98cca Author: Rokas Puzonas Date: Sun Aug 28 14:49:08 2022 +0000 initial commit diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..1ae2aa2 --- /dev/null +++ b/.luarc.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", + "Lua.diagnostics.disable": [ + "undefined-field" + ], + "Lua.workspace.library": { + "../cc-lsp-config": true + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..db0f47b --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) +Copyright © 2022 Rokas Puzonas + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e318b4e --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Storage system diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..5420662 --- /dev/null +++ b/main.lua @@ -0,0 +1,374 @@ +local ui = require("ui") +local ensure_width = require("cc.strings").ensure_width +local term_stack = require("term-stack") + +local LEFT_KEY = {keys.left , {"ctrl", keys.h}} +local DOWN_KEY = {keys.down , {"ctrl", keys.j}} +local UP_KEY = {keys.up , {"ctrl", keys.k}} +local RIGHT_KEY = {keys.right, {"ctrl", keys.l}} +local REQUEST_KEY = {"ctrl", keys.space} +local HELP_KEY = keys.f1 +local STATISTICS_KEY = keys.f2 +local REFRESH_KEY = keys.f5 + +local dbg_file = fs.combine(shell.dir(), "logs.txt") +if dbg_file then + fs.delete(dbg_file) +end +local function dbg(...) + if dbg_file then + local out = io.open(dbg_file, "a") + if not out then return end + for i = 1, select("#", ...) do + local value = select(i, ...) + if type(value) == "table" then + out:write(textutils.serialise(value)) + else + out:write(tostring(value)) + out:write(" ") + end + end + out:write("\n") + out:close() + else + local pretty = require("cc.pretty") + for i = 1, select("#", ...) do + local value = select(i, ...) + pretty.pretty_print(value) + end + end +end + +local function is_inventory(peripheral_name) + local types = {peripheral.getType(peripheral_name)} + for _, t in ipairs(types) do + if t == "inventory" then + return true + end + end + return false +end + +local function remove_value(array, value) + for i, v in ipairs(array) do + if v == value then + table.remove(array, i) + return + end + end +end + +local function has_in_array(value, array) + for _, v in ipairs(array) do + if v == value then + return true + end + end +end + +local function list_inventories() + local inventories = {} + for _, name in ipairs(peripheral.getNames()) do + if is_inventory(name) and not has_in_array(name, { "top", "left", "right", "back", "bottom", "front" }) then + table.insert(inventories, name) + end + end + return inventories +end + +local function list_items(inventories) + local items = {} + for _, name in ipairs(inventories) do + for slot, item in pairs(peripheral.call(name, "list")) do + items[item.name] = items[item.name] or {} + table.insert(items[item.name], { + count = item.count, + slot = slot, + peripheral = name + }) + end + end + + for name, item_collection in pairs(items) do + local item = item_collection[1] + local detail = peripheral.call(item.peripheral, "getItemDetail", item.slot) + items[name].displayName = detail.displayName + items[name].stack_size = detail.maxCount + + item_collection.count = 0 + for _, slot in ipairs(item_collection) do + item_collection.count = item_collection.count + slot.count + end + end + + return items +end + +local function get_item_counts(items) + local item_counts = {} + for name, item_collection in pairs(items) do + item_counts[name] = item_collection.count + end + return item_counts +end + +local function vline(x, y, height) + for i=1, height do + term.setCursorPos(x, y+i-1) + term.write("|") + end +end + +local function clamp(x, min, max) + return math.min(math.max(x, min), max) +end + +local function rect(x, y, w, h) + return { x = x, y = y, w = w, h = h or w } +end + +local function vsplit(area, x) + local left_area = rect(area.x, area.y, x - area.x, area.h) + local right_area = rect(area.x + left_area.w+1, area.y, area.w - left_area.w-1, area.h) + return left_area, right_area +end + +local main_view = { event = {} } + +function main_view:list_shown_names(all_items, name_filter, max_names) + name_filter = name_filter:lower() + + local names = {} + for name, count in pairs(all_items) do + local displayName = self.items[name].displayName:lower() + if displayName:find(name_filter) and count > 0 then + table.insert(names, name) + end + end + + table.sort(names, function(a, b) return all_items[a] > all_items[b] end) + + if #names > max_names then + local names_subset = {} + for i=1, max_names do + table.insert(names_subset, names[i]) + end + names = names_subset + end + + return names +end + +function main_view:display_item_list(x, y, w, all_items, shown_names, highlight_row) + term_stack.push_cursor(x, y) + + local max_count = 0 + for _, name in pairs(shown_names) do + local count = all_items[name] + max_count = math.max(max_count, count) + end + local max_count_width = math.floor(math.log(max_count, 10) + 1) + + for row, name in pairs(shown_names) do + local count = all_items[name] + local item_info = self.items[name] + local count_width = math.floor(math.log(count, 10) + 1) + if row == highlight_row then + term.setTextColor(colors.yellow) + else + term.setTextColor(colors.white) + end + term.setCursorPos((max_count_width+1 - count_width), row) + term.write(count) + term.setCursorPos(max_count_width+2, row) + term.write(ensure_width(item_info.displayName, w-max_count_width-1)) + end + + term_stack.pop_cursor() +end + +function main_view:move_items(item_name, amount, destination) + local item_collection = self.items[item_name] + + local moved_amount = 0 + while moved_amount < amount and item_collection.count > 0 do + local slot_details = item_collection[#item_collection] + + local needed_amount = amount - moved_amount + local transferred_amount = peripheral.call(slot_details.peripheral, "pushItems", destination, slot_details.slot, needed_amount) + moved_amount = moved_amount + transferred_amount + slot_details.count = slot_details.count - transferred_amount + item_collection.count = item_collection.count - transferred_amount + if slot_details.count == 0 then + table.remove(item_collection) + end + end + + return moved_amount +end + +function main_view:refresh_shown_names(store, area) + store.shown_names = main_view:list_shown_names(store.all_items, store.search_bar or "", area.h-1) + store.selected_row = clamp(store.selected_row or 1, 1, #store.shown_names) +end + +function main_view:display_items_with_search(store, area, active) + store.search_bar = store.search_bar or "" + store.selected_row = store.selected_row or 0 + store.all_items = store.all_items or {} + store.shown_names = store.shown_names or main_view:list_shown_names(store.all_items, store.search_bar, area.h-1) + + term_stack.push_cursor(area.x, area.y) + + local new_search_bar = ui:textbox(area.w, active, "Search", store.search_bar) + if new_search_bar ~= store.search_bar then + store.search_bar = new_search_bar + self:refresh_shown_names(store, area) + end + + if active then + if ui.event[1] == "key" then + local key = ui.event[2] + if key == keys.down or (main_view.ctrl_down and key == keys.j) then + store.selected_row = math.min(store.selected_row + 1, #store.shown_names) + elseif key == keys.up or (main_view.ctrl_down and key == keys.k) then + store.selected_row = math.max(store.selected_row - 1, 1) + end + end + end + + term.setBackgroundColor(colors.black) + main_view:display_item_list(1, 2, area.w, store.all_items, store.shown_names, store.selected_row) + + term_stack.pop_cursor() +end + +local function main() + main_view.result_inventory = "minecraft:barrel_4" + + local inventories = list_inventories() + remove_value(inventories, main_view.result_inventory) + main_view.items = list_items(inventories) + + local update_interval = 0.05 + + local main_area = rect(1, 1, term.getSize()) + local split_x = math.floor(main_area.w*0.5) + local left_area, right_area = vsplit(main_area, split_x) + + local is_left_active = true + local left_store = { + all_items = get_item_counts(main_view.items) + } + local right_store = { + all_items = {} + } + + local update_timer = os.startTimer(update_interval) + while true do + ---@diagnostic disable-next-line: missing-parameter + ui.event = {os.pullEvent()} + + local has_selected_items = false + for _, count in pairs(right_store.all_items) do + if count > 0 then + has_selected_items = true + break + end + end + if not has_selected_items and not is_left_active then + is_left_active = true + end + + local event = ui.event[1] + if event == "timer" then + local timer = ui.event[2] + if timer == update_timer then + update_timer = os.startTimer(update_interval) + end + elseif event == "key" then + local key = ui.event[2] + if key == keys.rightCtrl or key == keys.leftCtrl then + main_view.ctrl_down = true + elseif key == keys.tab then + is_left_active = not is_left_active + elseif key == keys.f5 then + main_view.items = list_items(inventories) + left_store.all_items = get_item_counts(main_view.items) + right_store.all_items = {} + main_view:refresh_shown_names(left_store, left_area) + main_view:refresh_shown_names(right_store, right_area) + + update_timer = os.startTimer(update_interval) + end + + if main_view.ctrl_down then + if key == keys.space then + if has_selected_items then + for name, count in pairs(right_store.all_items) do + local transferred_count = main_view:move_items(name, count, main_view.result_inventory) + left_store.all_items[name] = left_store.all_items[name] - transferred_count + end + right_store.all_items = {} + main_view:refresh_shown_names(left_store, left_area) + main_view:refresh_shown_names(right_store, right_area) + else + local target_item = left_store.shown_names[left_store.selected_row] + if target_item then + local item_collection = main_view.items[target_item] + local transferred_count = main_view:move_items(target_item, item_collection.stack_size, main_view.result_inventory) + left_store.all_items[target_item] = left_store.all_items[target_item] - transferred_count + main_view:refresh_shown_names(left_store, left_area) + end + end + end + + if is_left_active then + if key == keys.l then + local target_item = left_store.shown_names[left_store.selected_row] + if target_item then + local stack_size = main_view.items[target_item].stack_size + local transferred_count = math.min(left_store.all_items[target_item], stack_size) + left_store.all_items[target_item] = left_store.all_items[target_item] - transferred_count + right_store.all_items[target_item] = (right_store.all_items[target_item] or 0) + transferred_count + main_view:refresh_shown_names(left_store, left_area) + main_view:refresh_shown_names(right_store, right_area) + end + end + else + if key == keys.h then + local target_item = right_store.shown_names[right_store.selected_row] + if target_item then + local stack_size = main_view.items[target_item].stack_size + local transferred_count = math.min(right_store.all_items[target_item], stack_size) + left_store.all_items[target_item] = left_store.all_items[target_item] + transferred_count + right_store.all_items[target_item] = right_store.all_items[target_item] - transferred_count + main_view:refresh_shown_names(left_store, left_area) + main_view:refresh_shown_names(right_store, right_area) + end + end + end + end + elseif event == "key_up" then + local key = ui.event[2] + if key == keys.rightCtrl or key == keys.leftCtrl then + main_view.ctrl_down = false + end + end + + term.setBackgroundColor(colors.black) + term.clear() + + if has_selected_items then + term.setTextColor(colors.lightGray) + vline(split_x, main_area.y, main_area.h) + + main_view:display_items_with_search(left_store, left_area, is_left_active) + main_view:display_items_with_search(right_store, right_area, not is_left_active) + else + main_view:display_items_with_search(left_store, main_area, is_left_active) + end + end +end + +main(...) diff --git a/term-stack.lua b/term-stack.lua new file mode 100644 index 0000000..a4d96b0 --- /dev/null +++ b/term-stack.lua @@ -0,0 +1,97 @@ +local expectlib = require("cc.expect") +local expect, field = expectlib.expect, expectlib.field +local term_stack = {} + +local cursor_stack = { } +local setCursorPos = term.setCursorPos +local getCursorPos = term.getCursorPos +local cursor_ox = 0 +local cursor_oy = 0 +function term_stack.push_cursor(x, y) + expect(1, x, "number", "nil") + expect(2, y, "number", "nil") + + local dx = ((x or 1) - 1) + local dy = ((y or 1) - 1) + + table.insert(cursor_stack, { dx, dy }) + cursor_ox = cursor_ox + dx + cursor_oy = cursor_oy + dy + setCursorPos(cursor_ox+1, cursor_oy+1) +end + +function term_stack.pop_cursor() + local frame = table.remove(cursor_stack) + cursor_ox = cursor_ox - frame[1] + cursor_oy = cursor_oy - frame[2] + setCursorPos(cursor_ox+1, cursor_oy+1) +end + +function term.setCursorPos(x, y) + expect(1, x, "number") + expect(2, y, "number") + + setCursorPos(x + cursor_ox, y + cursor_oy) +end + +function term.getCursorPos() + local x, y = getCursorPos() + return x - cursor_ox, y - cursor_oy +end + + + +local size_stack = {} +local getSize = term.getSize +local current_w, current_h = getSize() +function term_stack.push_size(w, h) + expect(1, w, "number") + expect(2, h, "number") + + table.insert(size_stack, { current_w, current_h }) + current_w = w + current_h = h + return w, h +end + +function term_stack.pop_size() + local frame = table.remove(cursor_stack) + current_w = frame[1] + current_h = frame[2] +end + +function term.getSize() + return current_w, current_h +end + + + +function term_stack.push_rect(x, y, w, h) + if type(x) == "table" then + local rect = x + field(rect, "x", "number") + field(rect, "y", "number") + field(rect, "w", "number") + field(rect, "h", "number") + + term_stack.push_cursor(rect.x, rect.y) + term_stack.push_size(rect.w, rect.h) + return rect.w, rect.h + else + expect(1, x, "number") + expect(2, y, "number") + expect(3, w, "number") + expect(4, h, "number") + + term_stack.push_cursor(x, y) + term_stack.push_size(w, h) + return w, h + end +end + +function term_stack.pop_rect() + term_stack.pop_cursor() + term_stack.pop_size() +end + +return term_stack diff --git a/ui.lua b/ui.lua new file mode 100644 index 0000000..892095c --- /dev/null +++ b/ui.lua @@ -0,0 +1,61 @@ +local ui = {} + +function ui:button(x, y, text) + local active = false + local hot = false + if self.event[1] == "mouse_up" then + local mx = self.event[3] + local my = self.event[4] + if mx >= x and x < mx + #text and my == y then + active = true + end + elseif self.event[1] == "mouse_click" then + local mx = self.event[3] + local my = self.event[4] + if mx >= x and x < mx + #text and my == y then + hot = true + end + end + + if hot then + term.setBackgroundColor(colors.lightGray) + else + term.setBackgroundColor(colors.gray) + end + term.write(text) + + return active +end + +function ui:textbox(width, active, placeholder, text) + if active then + if self.event[1] == "char" and #text < width-1 then + text = text .. self.event[2] + elseif self.event[1] == "key" and self.event[2] == keys.backspace then + text = text:sub(1, -2) + end + end + + local x, y = term.getCursorPos() + paintutils.drawLine(x, y, x+width-1, y, colors.gray) + term.setCursorPos(x, y) + + term.setTextColor(colors.white) + if active then + term.write(text) + if os.clock() % 1 < 0.5 then + term.write("|") + end + else + if text == "" then + term.setTextColor(colors.lightGray) + term.write(placeholder) + else + term.write(text) + end + end + + return text +end + +return ui