_G.requireInjector(_ENV) local Config = require('config') local Event = require('event') local Socket = require('socket') local sync = require('sync').sync local UI = require('ui') local Util = require('util') local colors = _G.colors local device = _G.device local os = _G.os local socket local SHIELD_SLOT = 2 local config = Config.load('miloRemote', { }) local page = UI.Page { dummy = UI.Window { x = 1, ex = -13, y = 1, height = 1, infoBar = UI.StatusBar { backgroundColor = colors.lightGray, }, }, refresh = UI.Button { y = 1, x = -12, event = 'refresh', text = 'Refresh', }, setupButton = UI.Button { y = 1, x = -3, event = 'setup', text = '\206', help = 'Configuration', }, grid = UI.Grid { y = 2, ey = -2, columns = { { heading = ' Qty', key = 'count' , width = 4, justify = 'right' }, { heading = 'Name', key = 'displayName' }, }, values = { }, sortColumn = 'displayName', help = '^(s)tack, ^(a)ll' }, statusBar = UI.Window { y = -1, filter = UI.TextEntry { x = 1, ex = -9, limit = 50, shadowText = 'filter', backgroundColor = colors.cyan, backgroundFocusColor = colors.cyan, accelerators = { [ 'enter' ] = 'eject', }, }, amount = UI.TextEntry { x = -8, ex = -4, limit = 3, shadowText = '1', shadowTextColor = colors.gray, backgroundColor = colors.black, backgroundFocusColor = colors.black, accelerators = { [ 'enter' ] = 'eject_specified', }, help = 'Specify an amount to send', }, display = UI.Button { x = -3, event = 'toggle_display', value = 0, text = 'A', help = 'Toggle display mode', }, }, accelerators = { r = 'refresh', [ 'control-r' ] = 'refresh', [ 'control-e' ] = 'eject', [ 'control-s' ] = 'eject_stack', [ 'control-a' ] = 'eject_all', q = 'quit', }, setup = UI.SlideOut { backgroundColor = colors.cyan, titleBar = UI.TitleBar { title = 'Remote Setup', }, form = UI.Form { x = 2, ex = -2, y = 2, ey = -1, values = config, [1] = UI.TextEntry { formLabel = 'Server', formKey = 'server', help = 'ID for the server', shadowText = 'Milo server ID', limit = 6, validate = 'numeric', required = true, }, [2] = UI.TextEntry { formLabel = 'User Name', formKey = 'user', help = 'User name for bound manipulator', shadowText = 'User name', limit = 50, required = true, }, [3] = UI.TextEntry { formLabel = 'Return Slot', formKey = 'slot', help = 'Use a slot for sending to storage', shadowText = 'Inventory slot #', limit = 5, validate = 'numeric', required = true, }, [4] = UI.Checkbox { formLabel = 'Shield Slot', formKey = 'useShield', help = 'or use the shield slot for sending' }, }, statusBar = UI.StatusBar { backgroundColor = colors.cyan, }, }, displayMode = 0, items = { }, } local function filterItems(t, filter, displayMode) if filter or displayMode > 0 then local r = { } if filter then filter = filter:lower() end for _,v in pairs(t) do if not filter or string.find(v.lname, filter, 1, true) then if not displayMode or displayMode == 0 or displayMode == 1 and v.count > 0 or displayMode == 2 and v.has_recipe then table.insert(r, v) end end end return r end return t end function page:setStatus(status) self.dummy.infoBar:setStatus(status) self:sync() end function page:sendRequest(data) local response if not config.server then self:setStatus('Invalid configuration') Event.onTimeout(2, function() self:setStatus('') end) return end sync(self, function() local msg for _ = 1, 2 do if not socket or not socket.connected then self:setStatus('connecting ...') socket, msg = Socket.connect(config.server, 4242) if socket then socket:write(config.user) local r = socket:read(2) if r and not r.msg then self:setStatus('connected ...') else msg = r and r.msg or 'Timed out' socket:close() socket = nil break end end end if socket then if socket:write(data) then response = socket:read(2) if response then if response.msg then self:setStatus(response.msg) response = nil end Event.onTimeout(2, function() self:setStatus('') end) return end end socket:close() end end self:setStatus(msg or 'Failed to connect') Event.onTimeout(2, function() self:setStatus('') end) end) return response end function page.grid:getRowTextColor(row, selected) if row.is_craftable then return colors.yellow end if row.has_recipe then return colors.cyan end return UI.Grid:getRowTextColor(row, selected) end function page.grid:getDisplayValues(row) row = Util.shallowCopy(row) row.count = row.count > 0 and Util.toBytes(row.count) or '' return row end function page:transfer(item, count) local response = self:sendRequest({ request = 'transfer', item = item, count = count }) if response then item.count = response.current - response.count self.grid:draw() if response.craft > 0 then self:setStatus(response.craft .. ' crafting ...') elseif response.craft + response.count < response.requested then self:setStatus((response.craft + response.count) .. ' available ...') end end end function page.setup:eventHandler(event) if event.type == 'focus_change' then self.statusBar:setStatus(event.focused.help) end return UI.SlideOut.eventHandler(self, event) end function page:eventHandler(event) if event.type == 'quit' then UI:exitPullEvents() elseif event.type == 'setup' then self.setup:show() elseif event.type == 'form_complete' then Config.update('miloRemote', config) self.setup:hide() self:refresh() self.grid:draw() elseif event.type == 'form_cancel' then self.setup:hide() elseif event.type == 'focus_change' then self.dummy.infoBar:setStatus(event.focused.help) elseif event.type == 'eject' or event.type == 'grid_select' then local item = self.grid:getSelected() if item then self:setStatus('requesting 1 ...') self:transfer(item, 1) end elseif event.type == 'eject_stack' then local item = self.grid:getSelected() if item then self:setStatus('requesting stack ...') self:transfer(item, 'stack') end elseif event.type == 'eject_all' then local item = self.grid:getSelected() if item then self:setStatus('requesting all ...') self:transfer(item, 'all') end elseif event.type == 'eject_specified' then local item = self.grid:getSelected() local count = tonumber(self.statusBar.amount.value) if item and count then self.statusBar.amount:reset() self:setFocus(self.statusBar.filter) self:setStatus('requesting ' .. count .. ' ...') self:transfer(item, count) else self:setStatus('nope ...') end elseif event.type == 'refresh' then self:setFocus(self.statusBar.filter) self:setStatus('updating ...') self:refresh() self.grid:draw() elseif event.type == 'toggle_display' then local values = { [0] = 'A', [1] = 'I', [2] = 'C', } event.button.value = (event.button.value + 1) % 3 self.displayMode = event.button.value event.button.text = values[event.button.value] event.button:draw() self:applyFilter() self.grid:draw() elseif event.type == 'text_change' and event.element == self.statusBar.filter then self.filter = event.text if #self.filter == 0 then self.filter = nil end self:applyFilter() self.grid:draw() else UI.Page.eventHandler(self, event) end return true end function page:enable() self:setFocus(self.statusBar.filter) UI.Page.enable(self) if not config.server then self.setup:show() end Event.onTimeout(.1, function() self:refresh() self.grid:draw() self:sync() end) end function page:refresh() local items = self:sendRequest({ request = 'list' }) if items then self.items = items self:applyFilter() end end function page:applyFilter() local t = filterItems(self.items, self.filter, self.displayMode) self.grid:setValues(t) end Event.addRoutine(function() while true do os.sleep(1.5) local neural = device.neuralInterface local inv = config.useShield and 'getEquipment' or 'getInventory' if not neural or not neural[inv] then _G._debug('missing Introspection module') elseif config.server then local method = neural[inv] local item = method and method().getItemMeta(config.useShield and SHIELD_SLOT or config.slot) if item then local slotNo = config.useShield and 'shield' or config.slot local response = page:sendRequest({ request = 'deposit', slot = slotNo, count = item.count, key = table.concat({ item.name, item.damage, item.nbtHash }, ':') }) if response then local ritem = page.items[response.key] if ritem then ritem.count = response.current + item.count end page.grid:draw() page:sync() end end end end end) UI:setPage(page) UI:pullEvents() if socket then socket:close() end