From a75607f890cdfcf8daf02a83d17f0b515973155a Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sun, 28 Aug 2022 19:14:53 +0000 Subject: [PATCH] add scroll bar --- main.lua | 318 +++----------------------------------------- views/main.lua | 354 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 370 insertions(+), 302 deletions(-) create mode 100644 views/main.lua diff --git a/main.lua b/main.lua index 5420662..f683025 100644 --- a/main.lua +++ b/main.lua @@ -1,21 +1,11 @@ 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(...) +function dbg(...) if dbg_file then local out = io.open(dbg_file, "a") if not out then return end @@ -49,15 +39,6 @@ local function is_inventory(peripheral_name) 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 @@ -76,298 +57,31 @@ local function list_inventories() 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 main_view = require("views.main") 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 = {} - } + main_view:prepare(inventories, "minecraft:barrel_4") + local update_interval = 0.1 local update_timer = os.startTimer(update_interval) while true do ---@diagnostic disable-next-line: missing-parameter - ui.event = {os.pullEvent()} + local event = {os.pullEvent()} + ui.event = event - 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) + if event[1] == "timer" and event[2] == update_timer then + update_timer = os.startTimer(update_interval) else - main_view:display_items_with_search(left_store, main_area, is_left_active) + local start = os.clock() + main_view:on_event(table.unpack(event)) + if os.clock() - start > update_interval then + os.cancelTimer(update_timer) + update_timer = os.startTimer(update_interval) + end end + + main_view:draw() end end diff --git a/views/main.lua b/views/main.lua new file mode 100644 index 0000000..14e970d --- /dev/null +++ b/views/main.lua @@ -0,0 +1,354 @@ +local term_stack = require("term-stack") +local ensure_width = require("cc.strings").ensure_width +local ui = require("ui") +local main_view = {} + +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 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 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 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 function vline(x, y, height) + for i=1, height do + term.setCursorPos(x, y+i-1) + term.write("|") + end +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 + +function main_view:prepare(inventories, result_inventory) + self.event = {} + + self.is_left_active = true + + self.main_area = rect(1, 1, term.getSize()) + self.split_x = math.floor(self.main_area.w*0.5) + self.left_area, self.right_area = vsplit(self.main_area, self.split_x) + + self.result_inventory = result_inventory + self.inventories = inventories + remove_value(self.inventories, result_inventory) + self.items = list_items(self.inventories) + + local all_item_counts = get_item_counts(self.items) + self.left_store = { + all_items = all_item_counts, + filtered_names = self:list_filtered_names(all_item_counts, ""), + search_bar = "", + selected_idx = 1, + scroll = 0 + } + self.right_store = { + all_items = {}, + filtered_names = {}, + search_bar = "", + selected_idx = 1, + scroll = 0 + } +end + +function main_view:list_filtered_names(all_items, name_filter) + 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) + + return names +end + +function main_view:display_item_list(store, area, active) + term_stack.push_cursor(area.x, area.y) + + local filtered_names = store.filtered_names + local shown_count = #filtered_names + + local max_count = 0 + for i=1, math.min(area.h, shown_count) do + local name = filtered_names[i + store.scroll] + local count = store.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=1, math.min(area.h, shown_count) do + local i = row + store.scroll + local name = filtered_names[i] + local count = store.all_items[name] + local item_info = self.items[name] + local count_width = math.floor(math.log(count, 10) + 1) + if i == store.selected_idx and active 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, area.w-max_count_width-1)) + end + + if shown_count > area.h then + local scroll_height = math.max(1, area.h/shown_count*area.h) + local y = 1+store.scroll/shown_count*area.h + paintutils.drawLine(area.w, y, area.w, y+scroll_height-1, colors.lightGray) + end + + term_stack.pop_cursor() +end + +function main_view:move_item(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:update_selected_item(store, area, idx) + local filtered_count = #store.filtered_names + store.selected_idx = clamp(idx, 1, filtered_count) + + local margin = 2 + local item_list_height = area.h-1 + if store.selected_idx > store.scroll+item_list_height-margin then + store.scroll = math.min(store.selected_idx-item_list_height+margin, filtered_count-item_list_height) + elseif store.selected_idx <= store.scroll+margin then + store.scroll = math.max(store.selected_idx-1-margin, 0) + end +end + +function main_view:select_item(store, area, item_name) + for i, name in ipairs(store.filtered_names) do + if name == item_name then + self:update_selected_item(store, area, i) + return + end + end +end + +function main_view:refresh_filtered_names(store, area) + store.filtered_names = main_view:list_filtered_names(store.all_items, store.search_bar or "") + self:update_selected_item(store, area, store.selected_idx) +end + +function main_view:display_items_with_search(store, area, active) + 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_filtered_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_idx = math.min(store.selected_idx + 1, #store.filtered_names) + elseif key == keys.up or (main_view.ctrl_down and key == keys.k) then + store.selected_idx = math.max(store.selected_idx - 1, 1) + end + + local margin = 2 + local item_list_height = area.h-1 + if store.selected_idx > store.scroll+item_list_height-margin then + store.scroll = math.min(store.selected_idx-item_list_height+margin, #store.filtered_names - item_list_height) + elseif store.selected_idx <= store.scroll+margin then + store.scroll = math.max(store.selected_idx-1-margin, 0) + end + end + end + + term.setBackgroundColor(colors.black) + main_view:display_item_list(store, rect(1, 2, area.w, area.h-1), active) + + term_stack.pop_cursor() +end + +function main_view:has_selected_items() + for _, count in pairs(main_view.right_store.all_items) do + if count > 0 then + return true + end + end + + return false +end + +function main_view:draw() + term.setBackgroundColor(colors.black) + term.clear() + + local has_selected_items = self:has_selected_items() + if has_selected_items then + term.setTextColor(colors.lightGray) + vline(self.split_x, self.main_area.y, self.main_area.h) + + main_view:display_items_with_search(self.left_store, self.left_area, self.is_left_active) + main_view:display_items_with_search(self.right_store, self.right_area, not self.is_left_active) + else + main_view:display_items_with_search(self.left_store, self.main_area, true) + end +end + +function main_view:on_event(event, ...) + local has_selected_items = self:has_selected_items() + if not has_selected_items and not self.is_left_active then + self.is_left_active = true + end + + if event == "key" then + self:on_key(...) + elseif event == "key_up" then + self:on_key_up(...) + end +end + +function main_view:on_key(key) + local left_selected = self.left_store.filtered_names[self.left_store.selected_idx] + local right_selected = self.right_store.filtered_names[self.right_store.selected_idx] + + if key == keys.rightCtrl or key == keys.leftCtrl then + self.ctrl_down = true + elseif key == keys.rightAlt or key == keys.leftAlt then + self.alt_down = true + elseif key == keys.tab then + self.is_left_active = not self.is_left_active + elseif key == keys.f5 then + self.items = list_items(self.inventories) + self.left_store.all_items = get_item_counts(self.items) + self.right_store.all_items = {} + self:refresh_filtered_names(self.left_store, self.left_area) + self:refresh_filtered_names(self.right_store, self.right_area) + end + + if self.ctrl_down then + local has_selected_items = self:has_selected_items() + if key == keys.space then + if has_selected_items then + for name, count in pairs(self.right_store.all_items) do + local transferred = self:move_item(name, count, self.result_inventory) + self.left_store.all_items[name] = self.left_store.all_items[name] - transferred + end + self.right_store.all_items = {} + self:refresh_filtered_names(self.left_store, self.left_area) + self:refresh_filtered_names(self.right_store, self.right_area) + else + if left_selected then + local item = left_selected + local stack_size = self.items[item].stack_size + local transferred_count = self:move_item(item, stack_size, self.result_inventory) + self.left_store.all_items[item] = self.left_store.all_items[item] - transferred_count + self:refresh_filtered_names(self.left_store, self.left_area) + self:select_item(self.left_store, self.left_area, item) + end + end + end + + if self.is_left_active then + if key == keys.l and left_selected then + local item = left_selected + local stack_size = self.items[item].stack_size + local transferred = math.min(self.left_store.all_items[item], stack_size) + self.left_store.all_items[item] = self.left_store.all_items[item] - transferred + self.right_store.all_items[item] = (self.right_store.all_items[item] or 0) + transferred + self:refresh_filtered_names(self.left_store, self.left_area) + self:refresh_filtered_names(self.right_store, self.right_area) + self:select_item(self.left_store, self.left_area, item) + end + else + if key == keys.h and right_selected then + local item = right_selected + local stack_size = self.items[item].stack_size + local transferred = math.min(self.right_store.all_items[item], stack_size) + self.left_store.all_items[item] = self.left_store.all_items[item] + transferred + self.right_store.all_items[item] = self.right_store.all_items[item] - transferred + self:refresh_filtered_names(self.left_store, self.left_area) + self:refresh_filtered_names(self.right_store, self.right_area) + self:select_item(self.right_store, self.right_area, item) + end + end + end +end + +function main_view:on_key_up(key) + if key == keys.rightCtrl or key == keys.leftCtrl then + self.ctrl_down = false + elseif key == keys.rightAlt or key == keys.leftAlt then + self.alt_down = false + end +end + +return main_view