diff --git a/sys/apis/config.lua b/sys/apis/config.lua index 52ce0c0..13ab441 100644 --- a/sys/apis/config.lua +++ b/sys/apis/config.lua @@ -15,8 +15,13 @@ function Config.load(fname, data) if not fs.exists(filename) then Util.writeTable(filename, data) else - Util.merge(data, Util.readTable(filename) or { }) + local contents = Util.readTable(filename) or + error('Configuration file is corrupt:' .. filename) + + Util.merge(data, contents) end + + return data end function Config.loadWithCheck(fname, data) @@ -33,7 +38,7 @@ function Config.loadWithCheck(fname, data) shell.run('edit ' .. filename) end - Config.load(fname, data) + return Config.load(fname, data) end function Config.update(fname, data) diff --git a/sys/apis/event.lua b/sys/apis/event.lua index fda4946..2a66542 100644 --- a/sys/apis/event.lua +++ b/sys/apis/event.lua @@ -1,4 +1,5 @@ -local os = _G.os +local os = _G.os +local table = _G.table local Event = { uid = 1, -- unique id for handlers @@ -6,8 +7,29 @@ local Event = { types = { }, -- event handlers timers = { }, -- named timers terminate = false, + free = { }, } +-- Use a pool of coroutines for event handlers +local function createCoroutine(h) + local co = table.remove(Event.free) + if not co then + co = coroutine.create(function(_, ...) + local args = { ... } + while true do + h.fn(table.unpack(args)) + h.co = nil + table.insert(Event.free, co) + args = { coroutine.yield() } + h = table.remove(args, 1) + h.co = co + end + end) + end + h.primeCo = true -- TODO: fix... + return co +end + local Routine = { } function Routine:isDead() @@ -24,18 +46,20 @@ function Routine:terminate() end function Routine:resume(event, ...) - --if coroutine.status(self.co) == 'running' then - --return - --end - if not self.co then error('Cannot resume a dead routine') end if not self.filter or self.filter == event or event == "terminate" then - local s, m = coroutine.resume(self.co, event, ...) - - if coroutine.status(self.co) == 'dead' then + local s, m + if self.primeCo then + -- Only need self passed when using a coroutine from the pool + s, m = coroutine.resume(self.co, self, event, ...) + self.primeCo = nil + else + s, m = coroutine.resume(self.co, event, ...) + end + if self:isDead() then self.co = nil self.filter = nil Event.routines[self.uid] = nil @@ -83,8 +107,14 @@ end function Event.off(h) if h and h.event then for _,event in pairs(h.event) do + local handler = Event.types[event][h.uid] + if handler then + handler:terminate() + end Event.types[event][h.uid] = nil end + elseif h and h.co then + h:terminate() end end @@ -107,7 +137,12 @@ local function addTimer(interval, recurring, fn) end function Event.onInterval(interval, fn) - return addTimer(interval, true, fn) + return Event.addRoutine(function() + while true do + os.sleep(interval) + fn() + end + end) end function Event.onTimeout(timeout, fn) @@ -136,6 +171,18 @@ function Event.waitForEvent(event, timeout) until e[1] == 'timer' and e[2] == timerId end +-- Set a handler for the terminate event. Within the function, return +-- true or false to indicate whether the event should be propagated to +-- all sub-threads +function Event.onTerminate(fn) + Event.termFn = fn +end + +function Event.termFn() + Event.terminate = true + return true -- propagate +end + function Event.addRoutine(fn) local r = setmetatable({ co = coroutine.create(fn), @@ -171,7 +218,7 @@ local function processHandlers(event) for _,h in pairs(handlers) do if not h.co then -- callbacks are single threaded (only 1 co per handler) - h.co = coroutine.create(h.fn) + h.co = createCoroutine(h) Event.routines[h.uid] = h end end @@ -204,11 +251,16 @@ end function Event.pullEvent(eventType) while true do local e = { os.pullEventRaw() } + local propagate = true -- don't like this... - Event.terminate = Event.terminate or e[1] == 'terminate' + if e[1] == 'terminate' then + propagate = Event.termFn() + end - processHandlers(e[1]) - processRoutines(table.unpack(e)) + if propagate then + processHandlers(e[1]) + processRoutines(table.unpack(e)) + end if Event.terminate then return { 'terminate' } diff --git a/sys/apis/fs/netfs.lua b/sys/apis/fs/netfs.lua index 58089a4..d7eb2eb 100644 --- a/sys/apis/fs/netfs.lua +++ b/sys/apis/fs/netfs.lua @@ -1,5 +1,5 @@ local Socket = require('socket') -local synchronized = require('sync') +local synchronized = require('sync').sync local fs = _G.fs diff --git a/sys/apis/git.lua b/sys/apis/git.lua index 67f27b3..77c398c 100644 --- a/sys/apis/git.lua +++ b/sys/apis/git.lua @@ -1,27 +1,51 @@ local json = require('json') local Util = require('util') +-- Limit queries to once per minecraft day +-- TODO: will not work if time is stopped + local TREE_URL = 'https://api.github.com/repos/%s/%s/git/trees/%s?recursive=1' local FILE_URL = 'https://raw.githubusercontent.com/%s/%s/%s/%s' - local git = { } -function git.list(repository) +local fs = _G.fs +local os = _G.os +if not _G.GIT then + _G.GIT = { } +end + +function git.list(repository) local t = Util.split(repository, '(.-)/') - local user = t[1] - local repo = t[2] - local branch = t[3] or 'master' + local user = table.remove(t, 1) + local repo = table.remove(t, 1) + local branch = table.remove(t, 1) or 'master' + local path - local dataUrl = string.format(TREE_URL, user, repo, branch) - local contents = Util.download(dataUrl) - - if not contents then - error('Invalid repository') + if not Util.empty(t) then + path = table.concat(t, '/') .. '/' end - local data = json.decode(contents) + local cacheKey = table.concat({ user, repo, branch }, '-') + local fname = fs.combine('.git', cacheKey) + + local function getContents() + if fs.exists(fname) then + local contents = Util.readTable(fname) + if contents and contents.data == os.day() then + return contents.data + end + fs.delete(fname) + end + local dataUrl = string.format(TREE_URL, user, repo, branch) + local contents = Util.download(dataUrl) + if contents then + return json.decode(contents) + end + end + + local data = getContents() or error('Invalid repository') if data.message and data.message:find("API rate limit exceeded") then error("Out of API calls, try again later") @@ -31,15 +55,26 @@ function git.list(repository) error("Invalid repository") end - local list = { } + if not fs.exists(fname) then + Util.writeTable('.git/' .. cacheKey, { day = os.day(), data = data }) + end + local list = { } for _,v in pairs(data.tree) do if v.type == "blob" then v.path = v.path:gsub("%s","%%20") - list[v.path] = { - url = string.format(FILE_URL, user, repo, branch, v.path), - size = v.size, - } + if not path then + list[v.path] = { + url = string.format(FILE_URL, user, repo, branch, v.path), + size = v.size, + } + elseif Util.startsWith(v.path, path) then + local p = string.sub(v.path, #path) + list[p] = { + url = string.format(FILE_URL, user, repo, branch, path .. p), + size = v.size, + } + end end end diff --git a/sys/apis/json.lua b/sys/apis/json.lua index 6a1a921..64f8825 100644 --- a/sys/apis/json.lua +++ b/sys/apis/json.lua @@ -68,7 +68,6 @@ local function encodeCommon(val, pretty, tabLevel, tTracking) str = str .. encodeCommon(v, pretty, tabLevel, tTracking) end) else -debug(val) arrEncoding(val, "{", "}", pairs, function(k,v) assert(type(k) == "string", "JSON object keys must be strings", 2) str = str .. encodeCommon(k, pretty, tabLevel, tTracking) diff --git a/sys/apis/packages.lua b/sys/apis/packages.lua new file mode 100644 index 0000000..ae7375a --- /dev/null +++ b/sys/apis/packages.lua @@ -0,0 +1,60 @@ +local Util = require('util') + +local fs = _G.fs +local textutils = _G.textutils + +local PACKAGE_DIR = 'packages' + +local Packages = { } + +function Packages:installed() + self.cache = { } + + if fs.exists(PACKAGE_DIR) then + for _, dir in pairs(fs.list(PACKAGE_DIR)) do + local path = fs.combine(fs.combine(PACKAGE_DIR, dir), '.package') + self.cache[dir] = Util.readTable(path) + end + end + + return self.cache +end + +function Packages:list() + if self.packageList then + return self.packageList + end + self.packageList = Util.readTable('usr/config/packages') or { } + + return self.packageList +end + +function Packages:isInstalled(package) + return self:installed()[package] +end + +function Packages:getManifest(package) + local fname = 'packages/' .. package .. '/.package' + if fs.exists(fname) then + local c = Util.readTable(fname) + if c then + c.repository = c.repository:gsub('{{OPUS_BRANCH}}', _G.OPUS_BRANCH) + return c + end + end + local list = self:list() + local url = list and list[package] + + if url then + local c = Util.httpGet(url) + if c then + c = textutils.unserialize(c) + if c then + c.repository = c.repository:gsub('{{OPUS_BRANCH}}', _G.OPUS_BRANCH) + return c + end + end + end +end + +return Packages diff --git a/sys/apis/peripheral.lua b/sys/apis/peripheral.lua index 0d527fb..33a1df1 100644 --- a/sys/apis/peripheral.lua +++ b/sys/apis/peripheral.lua @@ -28,9 +28,9 @@ function Peripheral.addDevice(deviceList, side) end if ptype == 'modem' then - if Peripheral.call(name, 'isWireless') then - ptype = 'wireless_modem' - else + if not Peripheral.call(name, 'isWireless') then +-- ptype = 'wireless_modem' +-- else ptype = 'wired_modem' end end @@ -55,17 +55,21 @@ function Peripheral.addDevice(deviceList, side) end -- this can randomly fail - pcall(function() deviceList[name] = Peripheral.wrap(side) end) + if not deviceList[name] then + pcall(function() + deviceList[name] = Peripheral.wrap(side) + end) - if deviceList[name] then - Util.merge(deviceList[name], { - name = name, - type = ptype, - side = side, - }) - - return deviceList[name] + if deviceList[name] then + Util.merge(deviceList[name], { + name = name, + type = ptype, + side = side, + }) + end end + + return deviceList[name] end function Peripheral.getBySide(side) diff --git a/sys/apis/point.lua b/sys/apis/point.lua index 1bd68ba..1d83eab 100644 --- a/sys/apis/point.lua +++ b/sys/apis/point.lua @@ -48,6 +48,13 @@ function Point.copy(pt) return { x = pt.x, y = pt.y, z = pt.z } end +function Point.round(pt) + pt.x = Util.round(pt.x) + pt.y = Util.round(pt.y) + pt.z = Util.round(pt.z) + return pt +end + function Point.same(pta, ptb) return pta.x == ptb.x and pta.y == ptb.y and @@ -144,14 +151,21 @@ function Point.calculateMoves(pta, ptb, distance) end end - if ptb.heading then - if heading ~= ptb.heading then - moves = moves + Point.calculateTurns(heading, ptb.heading) - heading = ptb.heading - end + if not ptb.heading then + return moves, heading, moves end - return moves, heading + -- calc turns as slightly less than moves + local weighted = moves + if heading ~= ptb.heading then + local turns = Point.calculateTurns(heading, ptb.heading) + moves = moves + turns + local wturns = { [0] = 0, [1] = .9, [2] = 1.9 } + weighted = weighted + wturns[turns] + heading = ptb.heading + end + + return moves, heading, weighted end -- given a set of points, find the one taking the least moves @@ -164,7 +178,7 @@ function Point.closest(reference, pts) for _,pt in pairs(pts) do local distance = Point.turtleDistance(reference, pt) if distance < lm then - local m = Point.calculateMoves(reference, pt, distance) + local _, _, m = Point.calculateMoves(reference, pt, distance) if m < lm then lpt = pt lm = m diff --git a/sys/apis/socket.lua b/sys/apis/socket.lua index df47a79..7926234 100644 --- a/sys/apis/socket.lua +++ b/sys/apis/socket.lua @@ -81,7 +81,8 @@ local function loopback(port, sport, msg) end local function newSocket(isLoopback) - for i = 16384, 32767 do + for _ = 16384, 32767 do + local i = math.random(16384, 32767) if not device.wireless_modem.isOpen(i) then local socket = { shost = os.getComputerID(), @@ -128,6 +129,7 @@ function Socket.connect(host, port) local e, id, sport, dport, msg = os.pullEvent() if e == 'modem_message' and sport == socket.sport and + type(msg) == 'table' and msg.dhost == socket.shost then os.cancelTimer(timerId) @@ -171,7 +173,7 @@ local function trusted(msg, port) local data = Crypto.decrypt(msg.t or '', pubKey) --local sharedKey = modexp(pubKey, exchange.secretKey, public.primeMod) - return data.ts and tonumber(data.ts) and math.abs(os.time() - data.ts) < 1 + return data.ts and tonumber(data.ts) and math.abs(os.time() - data.ts) < 24 end end @@ -184,6 +186,7 @@ function Socket.server(port) if sport == port and msg and + type(msg) == 'table' and msg.dhost == os.getComputerID() and msg.type == 'OPEN' then diff --git a/sys/apis/sound.lua b/sys/apis/sound.lua new file mode 100644 index 0000000..b4dab46 --- /dev/null +++ b/sys/apis/sound.lua @@ -0,0 +1,18 @@ +local peripheral = _G.peripheral + +local Sound = { + _volume = 1, +} + +function Sound.play(sound, vol) + local speaker = peripheral.find('speaker') + if speaker then + speaker.playSound('minecraft:' .. sound, vol or Sound._volume) + end +end + +function Sound.setVolume(volume) + Sound._volume = math.max(0, math.min(volume, 1)) +end + +return Sound diff --git a/sys/apis/sync.lua b/sys/apis/sync.lua index e0b841e..7987cb4 100644 --- a/sys/apis/sync.lua +++ b/sys/apis/sync.lua @@ -1,26 +1,61 @@ -local syncLocks = { } +local Sync = { + syncLocks = { } +} local os = _G.os -return function(obj, fn) +function Sync.sync(obj, fn) local key = tostring(obj) - if syncLocks[key] then + if Sync.syncLocks[key] then local cos = tostring(coroutine.running()) - table.insert(syncLocks[key], cos) + table.insert(Sync.syncLocks[key], cos) repeat local _, co = os.pullEvent('sync_lock') until co == cos else - syncLocks[key] = { } + Sync.syncLocks[key] = { } end local s, m = pcall(fn) - local co = table.remove(syncLocks[key], 1) + local co = table.remove(Sync.syncLocks[key], 1) if co then os.queueEvent('sync_lock', co) else - syncLocks[key] = nil + Sync.syncLocks[key] = nil end if not s then error(m) end end + +function Sync.lock(obj) + local key = tostring(obj) + if Sync.syncLocks[key] then + local cos = tostring(coroutine.running()) + table.insert(Sync.syncLocks[key], cos) + repeat + local _, co = os.pullEvent('sync_lock') + until co == cos + else + Sync.syncLocks[key] = { } + end +end + +function Sync.release(obj) + local key = tostring(obj) + if not Sync.syncLocks[key] then + error('Sync.release: Lock was not obtained', 2) + end + local co = table.remove(Sync.syncLocks[key], 1) + if co then + os.queueEvent('sync_lock', co) + else + Sync.syncLocks[key] = nil + end +end + +function Sync.isLocked(obj) + local key = tostring(obj) + return not not Sync.syncLocks[key] +end + +return Sync diff --git a/sys/apis/ui.lua b/sys/apis/ui.lua index d49501f..dffb370 100644 --- a/sys/apis/ui.lua +++ b/sys/apis/ui.lua @@ -3,6 +3,7 @@ local class = require('class') local Event = require('event') local Input = require('input') local Peripheral = require('peripheral') +local Sound = require('sound') local Transition = require('ui.transition') local Util = require('util') @@ -306,6 +307,9 @@ function Manager:setDefaultDevice(dev) end function Manager:addPage(name, page) + if not self.pages then + self.pages = { } + end self.pages[name] = page end @@ -940,6 +944,7 @@ function UI.Device:postInit() end function UI.Device:resize() + self.device.setTextScale(self.textScale) self.width, self.height = self.device.getSize() self.lines = { } self.canvas:resize(self.width, self.height) @@ -1055,6 +1060,17 @@ function UI.StringBuffer:insert(s, width) end end +function UI.StringBuffer:insertRight(s, width) + local len = #tostring(s or '') + if len > width then + s = _sub(s, 1, width) + end + if len < width then + table.insert(self.buffer, _rep(' ', width - len)) + end + table.insert(self.buffer, s) +end + function UI.StringBuffer:get(sep) return Util.widthify(table.concat(self.buffer, sep or ''), self.bufSize) end @@ -1205,7 +1221,7 @@ function UI.Page:focusNext() end function UI.Page:setFocus(child) - if not child.focus then + if not child or not child.focus then return end @@ -1218,7 +1234,8 @@ function UI.Page:setFocus(child) self.focused = child if not child.focused then child.focused = true - self:emit({ type = 'focus_change', focused = child }) + child:emit({ type = 'focus_change', focused = child }) + --self:emit({ type = 'focus_change', focused = child }) end child:focus() @@ -1250,6 +1267,7 @@ UI.Grid.defaults = { backgroundSelectedColor = colors.gray, headerBackgroundColor = colors.cyan, headerTextColor = colors.white, + headerSortColor = colors.yellow, unfocusedTextSelectedColor = colors.white, unfocusedBackgroundSelectedColor = colors.gray, focusIndicator = '>', @@ -1407,6 +1425,18 @@ function UI.Grid:getSelected() end end +function UI.Grid:setSelected(name, value) + if self.sorted then + for k,v in pairs(self.sorted) do + if self.values[v][name] == value then + self:setIndex(k) + return + end + end + end + self:setIndex(1) +end + function UI.Grid:focus() self:drawRows() end @@ -1459,7 +1489,7 @@ function UI.Grid:update() end function UI.Grid:drawHeadings() - local sb = UI.StringBuffer(self.width) + local x = 1 for _,col in ipairs(self.columns) do local ind = ' ' if col.key == self.sortColumn then @@ -1469,9 +1499,13 @@ function UI.Grid:drawHeadings() ind = self.sortIndicator end end - sb:insert(ind .. col.heading, col.cw + 1) + self:write(x, + 1, + Util.widthify(ind .. col.heading, col.cw + 1), + self.headerBackgroundColor, + col.key == self.sortColumn and self.headerSortColor or self.headerTextColor) + x = x + col.cw + 1 end - self:write(1, 1, sb:get(), self.headerBackgroundColor, self.headerTextColor) end function UI.Grid:sortCompare(a, b) @@ -1508,7 +1542,11 @@ function UI.Grid:drawRows() end for _,col in pairs(self.columns) do - sb:insert(ind .. safeValue(row[col.key] or ''), col.cw + 1) + if col.justify == 'right' then + sb:insertRight(ind .. safeValue(row[col.key] or ''), col.cw + 1) + else + sb:insert(ind .. safeValue(row[col.key] or ''), col.cw + 1) + end ind = ' ' end @@ -1603,13 +1641,12 @@ function UI.Grid:eventHandler(event) local col = 2 for _,c in ipairs(self.columns) do if event.x < col + c.cw then - if self.sortColumn == c.key then - self:setInverseSort(not self.inverseSort) - else - self.sortColumn = c.key - self:setInverseSort(false) - end - self:draw() + self:emit({ + type = 'grid_sort', + sortColumn = c.key, + inverseSort = self.sortColumn == c.key and not self.inverseSort, + element = self, + }) break end col = col + c.cw + 1 @@ -1632,6 +1669,10 @@ function UI.Grid:eventHandler(event) end return false + elseif event.type == 'grid_sort' then + self.sortColumn = event.sortColumn + self:setInverseSort(event.inverseSort) + self:draw() elseif event.type == 'scroll_down' then self:setIndex(self.index + 1) elseif event.type == 'scroll_up' then @@ -1854,6 +1895,8 @@ function UI.TitleBar:draw() sb:center(string.format(' %s ', self.title)) if self.previousPage or self.event then sb:insert(-1, self.closeInd) + else + sb:insert(-2, self.frameChar) end self:write(1, 1, sb:get()) end @@ -2286,56 +2329,52 @@ function UI.Wizard:add(pages) end end -function UI.Wizard:enable() +function UI.Wizard:getPage(index) + return Util.find(self.pages, 'index', index) +end + +function UI.Wizard:enable(...) self.enabled = true - for _,child in ipairs(self.children) do - if not child.index then - child:enable() - elseif child.index == 1 then - child:enable() + self.index = 1 + local initial = self:getPage(1) + for _,child in pairs(self.children) do + if child == initial or not child.index then + child:enable(...) else child:disable() end end - self:emit({ type = 'enable_view', next = Util.find(self.pages, 'index', 1) }) + self:emit({ type = 'enable_view', next = initial }) end -function UI.Wizard:nextView() - local currentView = Util.find(self.pages, 'enabled', true) - local nextView = Util.find(self.pages, 'index', currentView.index + 1) - - if nextView then - self:emit({ type = 'enable_view', view = nextView }) - self:addTransition('slideLeft') - currentView:disable() - nextView:enable() - end -end - -function UI.Wizard:prevView() - local currentView = Util.find(self.pages, 'enabled', true) - local nextView = Util.find(self.pages, 'index', currentView.index - 1) - - if nextView then - self:emit({ type = 'enable_view', view = nextView }) - self:addTransition('slideRight') - currentView:disable() - nextView:enable() - end +function UI.Wizard:isViewValid() + local currentView = self:getPage(self.index) + return not currentView.validate and true or currentView:validate() end function UI.Wizard:eventHandler(event) if event.type == 'nextView' then - local currentView = Util.find(self.pages, 'enabled', true) - local nextView = Util.find(self.pages, 'index', currentView.index + 1) - currentView:emit({ type = 'enable_view', next = nextView, current = currentView }) + local currentView = self:getPage(self.index) + if self:isViewValid() then + self.index = self.index + 1 + local nextView = self:getPage(self.index) + currentView:emit({ type = 'enable_view', next = nextView, current = currentView }) + end elseif event.type == 'previousView' then - local currentView = Util.find(self.pages, 'enabled', true) - local nextView = Util.find(self.pages, 'index', currentView.index - 1) - currentView:emit({ type = 'enable_view', prev = nextView, current = currentView }) + local currentView = self:getPage(self.index) + local nextView = self:getPage(self.index - 1) + if nextView then + self.index = self.index - 1 + currentView:emit({ type = 'enable_view', prev = nextView, current = currentView }) + end return true + elseif event.type == 'wizard_complete' then + if self:isViewValid() then + self:emit({ type = 'accept' }) + end + elseif event.type == 'enable_view' then if event.current then if event.next then @@ -2347,18 +2386,20 @@ function UI.Wizard:eventHandler(event) end local current = event.next or event.prev - if Util.find(self.pages, 'index', current.index - 1) then + if not current then error('property "index" is required on wizard pages') end + + if self:getPage(self.index - 1) then self.previousButton:enable() else self.previousButton:disable() end - if Util.find(self.pages, 'index', current.index + 1) then + if self:getPage(self.index + 1) then self.nextButton.text = 'Next >' self.nextButton.event = 'nextView' else self.nextButton.text = 'Accept' - self.nextButton.event = 'accept' + self.nextButton.event = 'wizard_complete' end -- a new current view current:enable() @@ -2381,12 +2422,12 @@ function UI.SlideOut:enable() self.enabled = false end -function UI.SlideOut:show() +function UI.SlideOut:show(...) self:addTransition('expandUp') self.canvas:setVisible(true) self.enabled = true for _,child in pairs(self.children) do - child:enable() + child:enable(...) end self:draw() self:capture(self) @@ -2494,6 +2535,7 @@ end function UI.Notification:error(value, timeout) self.backgroundColor = colors.red + Sound.play('entity.villager.no', .5) self:display(value, timeout) end @@ -2544,9 +2586,10 @@ UI.Throttle = class(UI.Window) UI.Throttle.defaults = { UIElement = 'Throttle', backgroundColor = colors.gray, - height = 6, + bordercolor = colors.cyan, + height = 4, width = 10, - timeout = .095, + timeout = .075, ctr = 0, image = { ' //) (O )~@ &~&-( ?Q ', @@ -2562,6 +2605,7 @@ function UI.Throttle:setParent() end function UI.Throttle:enable() + self.c = os.clock() self.enabled = false end @@ -2570,27 +2614,26 @@ function UI.Throttle:disable() self.enabled = false self.canvas:removeLayer() self.canvas = nil - self.c = nil + self.ctr = 0 end end function UI.Throttle:update() local cc = os.clock() - if not self.c then - self.c = cc - elseif cc > self.c + self.timeout then + if cc > self.c + self.timeout then os.sleep(0) self.c = os.clock() self.enabled = true if not self.canvas then - self.canvas = self:addLayer(self.backgroundColor, colors.cyan) + self.canvas = self:addLayer(self.backgroundColor, self.borderColor) self.canvas:setVisible(true) - self:clear(colors.cyan) + self:clear(self.borderColor) end local image = self.image[self.ctr + 1] local width = self.width - 2 for i = 0, #self.image do - self:write(2, i + 2, image:sub(width * i + 1, width * i + width), colors.black, colors.white) + self:write(2, i + 1, image:sub(width * i + 1, width * i + width), + self.backgroundColor, self.textColor) end self.ctr = (self.ctr + 1) % #self.image @@ -2913,6 +2956,9 @@ UI.Chooser.defaults = { choices = { }, nochoice = 'Select', backgroundFocusColor = colors.lightGray, + textInactiveColor = colors.gray, + leftIndicator = '<', + rightIndicator = '>', height = 1, } function UI.Chooser:setParent() @@ -2933,14 +2979,15 @@ function UI.Chooser:draw() if self.focused then bg = self.backgroundFocusColor end + local fg = self.inactive and self.textInactiveColor or self.textColor local choice = Util.find(self.choices, 'value', self.value) local value = self.nochoice if choice then value = choice.name end - self:write(1, 1, '<', bg, colors.black) - self:write(2, 1, ' ' .. Util.widthify(value, self.width-4) .. ' ', bg) - self:write(self.width, 1, '>', bg, colors.black) + self:write(1, 1, self.leftIndicator, self.backgroundColor, colors.black) + self:write(2, 1, ' ' .. Util.widthify(tostring(value), self.width-4) .. ' ', bg, fg) + self:write(self.width, 1, self.rightIndicator, self.backgroundColor, colors.black) end function UI.Chooser:focus() @@ -2951,22 +2998,26 @@ function UI.Chooser:eventHandler(event) if event.type == 'key' then if event.key == 'right' or event.key == 'space' then local _,k = Util.find(self.choices, 'value', self.value) + local choice if k and k < #self.choices then - self.value = self.choices[k+1].value + choice = self.choices[k+1] else - self.value = self.choices[1].value + choice = self.choices[1] end - self:emit({ type = 'choice_change', value = self.value }) + self.value = choice.value + self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice }) self:draw() return true elseif event.key == 'left' then local _,k = Util.find(self.choices, 'value', self.value) + local choice if k and k > 1 then - self.value = self.choices[k-1].value + choice = self.choices[k-1] else - self.value = self.choices[#self.choices].value + choice = self.choices[#self.choices] end - self:emit({ type = 'choice_change', value = self.value }) + self.value = choice.value + self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice }) self:draw() return true end @@ -2981,6 +3032,57 @@ function UI.Chooser:eventHandler(event) end end +--[[-- Chooser --]]-- +UI.Checkbox = class(UI.Window) +UI.Checkbox.defaults = { + UIElement = 'Checkbox', + nochoice = 'Select', + checkedIndicator = 'X', + leftMarker = '[', + rightMarker = ']', + value = false, + textColor = colors.white, + backgroundColor = colors.black, + backgroundFocusColor = colors.lightGray, + height = 1, + width = 3, + accelerators = { + space = 'checkbox_toggle', + mouse_click = 'checkbox_toggle', + } +} +function UI.Checkbox:draw() + local bg = self.backgroundColor + if self.focused then + bg = self.backgroundFocusColor + end + if type(self.value) == 'string' then + self.value = nil -- TODO: fix form + end + local text = string.format('[%s]', not self.value and ' ' or self.checkedIndicator) + self:write(1, 1, text, bg) + self:write(1, 1, self.leftMarker, self.backgroundColor, self.textColor) + self:write(2, 1, not self.value and ' ' or self.checkedIndicator, bg) + self:write(3, 1, self.rightMarker, self.backgroundColor, self.textColor) +end + +function UI.Checkbox:focus() + self:draw() +end + +function UI.Checkbox:reset() + self.value = false +end + +function UI.Checkbox:eventHandler(event) + if event.type == 'checkbox_toggle' then + self.value = not self.value + self:emit({ type = 'checkbox_change', checked = self.value, element = self }) + self:draw() + return true + end +end + --[[-- Text --]]-- UI.Text = class(UI.Window) UI.Text.defaults = { @@ -3180,16 +3282,18 @@ function UI.Form:createForm() end end - table.insert(self.children, UI.Button { - y = -self.margin, x = -12 - self.margin, - text = 'Ok', - event = 'form_ok', - }) - table.insert(self.children, UI.Button { - y = -self.margin, x = -7 - self.margin, - text = 'Cancel', - event = 'form_cancel', - }) + if not self.manualControls then + table.insert(self.children, UI.Button { + y = -self.margin, x = -12 - self.margin, + text = 'Ok', + event = 'form_ok', + }) + table.insert(self.children, UI.Button { + y = -self.margin, x = -7 - self.margin, + text = 'Cancel', + event = 'form_cancel', + }) + end end function UI.Form:validateField(field) @@ -3198,25 +3302,47 @@ function UI.Form:validateField(field) return false, 'Field is required' end end + if field.validate == 'numeric' then + if #tostring(field.value) > 0 then + if not tonumber(field.value) then + return false, 'Invalid number' + end + end + end + return true +end + +function UI.Form:save() + for _,child in pairs(self.children) do + if child.formKey then + local s, m = self:validateField(child) + if not s then + self:setFocus(child) + self:emit({ type = 'form_invalid', message = m, field = child }) + return false + end + end + end + for _,child in pairs(self.children) do + if child.formKey then + if (child.pruneEmpty and type(child.value) == 'string' and #child.value == 0) or + (child.pruneEmpty and type(child.value) == 'boolean' and not child.value) then + self.values[child.formKey] = nil + elseif child.validate == 'numeric' then + self.values[child.formKey] = tonumber(child.value) + else + self.values[child.formKey] = child.value + end + end + end + return true end function UI.Form:eventHandler(event) if event.type == 'form_ok' then - for _,child in pairs(self.children) do - if child.formKey then - local s, m = self:validateField(child) - if not s then - self:setFocus(child) - self:emit({ type = 'form_invalid', message = m, field = child }) - return false - end - end - end - for _,child in pairs(self.children) do - if child.formKey then - self.values[child.formKey] = child.value - end + if not self:save() then + return false end self:emit({ type = self.event, UIElement = self }) else diff --git a/sys/apis/ui/canvas.lua b/sys/apis/ui/canvas.lua index baa011c..ea45ecf 100644 --- a/sys/apis/ui/canvas.lua +++ b/sys/apis/ui/canvas.lua @@ -137,7 +137,7 @@ function Canvas:writeBlit(x, y, text, bg, fg) if bg then bg = _sub(bg, 2 - x) end - if bg then + if fg then fg = _sub(fg, 2 - x) end width = width + x - 1 @@ -149,7 +149,7 @@ function Canvas:writeBlit(x, y, text, bg, fg) if bg then bg = _sub(bg, 1, self.width - x + 1) end - if bg then + if fg then fg = _sub(fg, 1, self.width - x + 1) end width = #text diff --git a/sys/apis/util.lua b/sys/apis/util.lua index 29fd5e1..40f3cb2 100644 --- a/sys/apis/util.lua +++ b/sys/apis/util.lua @@ -172,6 +172,20 @@ function Util.deepMerge(obj, args) end end +-- remove table entries if passed function returns false +function Util.prune(t, fn) + for _,k in pairs(Util.keys(t)) do + local v = t[k] + if type(v) == 'table' then + t[k] = Util.prune(v, fn) + end + if not fn(t[k]) then + t[k] = nil + end + end + return t +end + function Util.transpose(t) local tt = { } for k,v in pairs(t) do @@ -207,6 +221,7 @@ function Util.findAll(t, name, value) end function Util.shallowCopy(t) + if not t then error('Util.shallowCopy: invalid table', 2) end local t2 = { } for k,v in pairs(t) do t2[k] = v @@ -342,6 +357,7 @@ function Util.readFile(fname) end function Util.writeFile(fname, data) + if not fname or not data then error('Util.writeFile: invalid parameters', 2) end local file = io.open(fname, "w") if not file then error('Unable to open ' .. fname, 2) @@ -415,7 +431,7 @@ end function Util.download(url, filename) local contents, msg = Util.httpGet(url) if not contents then - error(string.format('Failed to download %s\n%s', url, msg)) + error(string.format('Failed to download %s\n%s', url, msg), 2) end if filename then @@ -475,6 +491,7 @@ function Util.insertString(str, istr, pos) end function Util.split(str, pattern) + if not str then error('Util.split: Invalid parameters', 2) end pattern = pattern or "(.-)\n" local t = {} local function helper(line) table.insert(t, line) return "" end @@ -491,7 +508,7 @@ function Util.matches(str, pattern) return t end -function Util.startsWidth(s, match) +function Util.startsWith(s, match) return string.sub(s, 1, #match) == match end @@ -653,7 +670,6 @@ function Util.getOptions(options, args, ignoreInvalid) end end return true, Util.size(rawOptions) - end return Util diff --git a/sys/apps/Network.lua b/sys/apps/Network.lua index e6d50c4..0fceb8c 100644 --- a/sys/apps/Network.lua +++ b/sys/apps/Network.lua @@ -1,5 +1,6 @@ _G.requireInjector(_ENV) +local Config = require('config') local Event = require('event') local Socket = require('socket') local UI = require('ui') @@ -20,6 +21,9 @@ local gridColumns = { { heading = 'Status', key = 'status' }, } +local trusted = Util.readTable('usr/.known_hosts') +local config = Config.load('network', { }) + if UI.term.width >= 30 then table.insert(gridColumns, { heading = 'Fuel', key = 'fuel', width = 5 }) table.insert(gridColumns, { heading = 'Uptime', key = 'uptime' }) @@ -39,7 +43,16 @@ local page = UI.Page { { text = 'Establish', event = 'trust' }, { text = 'Remove', event = 'untrust' }, } }, - { text = 'Help', event = 'help' }, + { text = 'Help', event = 'help', noCheck = true }, + { + text = '\206', + x = -3, + dropdown = { + { text = 'Show all', event = 'show_all', noCheck = true }, + UI.MenuBar.spacer, + { text = 'Show trusted', event = 'show_trusted', noCheck = true }, + }, + }, }, }, grid = UI.ScrollingGrid { @@ -143,6 +156,16 @@ This only needs to be done once. q = 'cancel', } }) + + elseif event.type == 'show_all' then + config.showTrusted = false + self.grid:setValues(network) + Config.update('network', config) + + elseif event.type == 'show_trusted' then + config.showTrusted = true + Config.update('network', config) + elseif event.type == 'quit' then Event.exitPullEvents() end @@ -155,7 +178,7 @@ function page.menuBar:getActive(menuItem) local trustList = Util.readTable('usr/.known_hosts') or { } return t and trustList[t.id] end - return not not t + return menuItem.noCheck or not not t end function page.grid:getRowTextColor(row, selected) @@ -184,7 +207,17 @@ function page.grid:getDisplayValues(row) end Event.onInterval(1, function() - page.grid:update() + local t = { } + if config.showTrusted then + for k,v in pairs(network) do + if trusted[k] then + t[k] = v + end + end + page.grid:setValues(t) + else + page.grid:update() + end page.grid:draw() page:sync() end) diff --git a/sys/apps/Overview.lua b/sys/apps/Overview.lua index 1d1961d..14740fc 100644 --- a/sys/apps/Overview.lua +++ b/sys/apps/Overview.lua @@ -1,14 +1,15 @@ _G.requireInjector(_ENV) -local class = require('class') -local Config = require('config') -local Event = require('event') -local FileUI = require('ui.fileui') -local NFT = require('nft') -local SHA1 = require('sha1') -local Tween = require('ui.tween') -local UI = require('ui') -local Util = require('util') +local class = require('class') +local Config = require('config') +local Event = require('event') +local FileUI = require('ui.fileui') +local NFT = require('nft') +local Packages = require('packages') +local SHA1 = require('sha1') +local Tween = require('ui.tween') +local UI = require('ui') +local Util = require('util') local colors = _G.colors local fs = _G.fs @@ -32,25 +33,28 @@ local config = { Config.load('Overview', config) local applications = { } +local extSupport = Util.getVersion() >= 1.76 local function loadApplications() - local requirements = { - turtle = function() return turtle end, - advancedTurtle = function() return turtle and term.isColor() end, - advanced = function() return term.isColor() end, - pocket = function() return pocket end, - advancedPocket = function() return pocket and term.isColor() end, - advancedComputer = function() return not turtle and not pocket and term.isColor() end, + turtle = not not turtle, + advancedTurtle = turtle and term.isColor(), + advanced = term.isColor(), + pocket = not not pocket, + advancedPocket = pocket and term.isColor(), + advancedComputer = not turtle and not pocket and term.isColor(), } applications = Util.readTable('sys/etc/app.db') - if fs.exists('usr/etc/apps') then - local dbs = fs.list('usr/etc/apps') - for _, db in pairs(dbs) do - local apps = Util.readTable('usr/etc/apps/' .. db) or { } - Util.merge(applications, apps) + for dir in pairs(Packages:installed()) do + local path = fs.combine('packages/' .. dir, 'etc/apps') + if fs.exists(path) then + local dbs = fs.list(path) + for _, db in pairs(dbs) do + local apps = Util.readTable(fs.combine(path, db)) or { } + Util.merge(applications, apps) + end end end @@ -72,13 +76,10 @@ local function loadApplications() end if a.requires then - local fn = requirements[a.requires] - if fn and not fn() then - return false - end + return requirements[a.requires] end - return true -- Util.startsWidth(a.run, 'http') or shell.resolveProgram(a.run) + return true -- Util.startsWith(a.run, 'http') or shell.resolveProgram(a.run) end) end @@ -117,7 +118,7 @@ local function parseIcon(iconText) icon = NFT.parse(iconText) if icon then if icon.height > 3 or icon.width > 8 then - error('Invalid size') + error('Must be an NFT image - 3 rows, 8 cols max') end end return icon @@ -174,6 +175,10 @@ local page = UI.Page { }, } +if extSupport then + page.container.backgroundColor = colors.black +end + UI.Icon = class(UI.Window) UI.Icon.defaults = { UIElement = 'Icon', @@ -194,7 +199,6 @@ function UI.Icon:eventHandler(event) end function page.container:setCategory(categoryName, animate) - -- reset the viewport window self.children = { } self.offy = 0 @@ -231,7 +235,10 @@ function page.container:setCategory(categoryName, animate) for _,program in ipairs(filtered) do local icon - if program.icon then + if extSupport and program.iconExt then + icon = parseIcon(program.iconExt) + end + if not icon and program.icon then icon = parseIcon(program.icon) end if not icon then @@ -344,7 +351,6 @@ function page:resize() end function page:eventHandler(event) - if event.type == 'tab_select' then self.container:setCategory(event.button.text, true) self.container:draw() @@ -455,7 +461,10 @@ function editor:enable(app) self.form:setValues(app) local icon - if app.icon then + if extSupport and app.iconExt then + icon = parseIcon(app.iconExt) + end + if not icon and app.icon then icon = parseIcon(app.icon) end self.form.image:setImage(icon) @@ -479,7 +488,6 @@ function editor:updateApplications(app) end function editor:eventHandler(event) - if event.type == 'form_cancel' or event.type == 'cancel' then UI:setPreviousPage() @@ -501,13 +509,17 @@ function editor:eventHandler(event) local s, m = pcall(function() local iconLines = Util.readFile(fileName) if not iconLines then - error('Unable to load file') + error('Must be an NFT image - 3 rows, 8 cols max') end local icon, m = parseIcon(iconLines) if not icon then error(m) end - self.form.values.icon = iconLines + if extSupport then + self.form.values.iconExt = iconLines + else + self.form.values.icon = iconLines + end self.form.image:setImage(icon) self.form.image:draw() end) diff --git a/sys/apps/PackageManager.lua b/sys/apps/PackageManager.lua new file mode 100644 index 0000000..fd05cf0 --- /dev/null +++ b/sys/apps/PackageManager.lua @@ -0,0 +1,158 @@ +_G.requireInjector(_ENV) + +local Ansi = require('ansi') +local Packages = require('packages') +local UI = require('ui') + +local colors = _G.colors +local shell = _ENV.shell +local term = _G.term + +UI:configure('PackageManager', ...) + +local page = UI.Page { + grid = UI.ScrollingGrid { + y = 2, ey = 7, x = 2, ex = -6, + values = { }, + columns = { + { heading = 'Package', key = 'name' }, + }, + sortColumn = 'name', + autospace = true, + help = 'Select a package', + }, + add = UI.Button { + x = -4, y = 4, + text = '+', + event = 'action', + help = 'Install or update', + }, + remove = UI.Button { + x = -4, y = 6, + text = '-', + event = 'action', + operation = 'uninstall', + operationText = 'Remove', + help = 'Remove', + }, + description = UI.TextArea { + x = 2, y = 9, ey = -2, + --backgroundColor = colors.white, + }, + statusBar = UI.StatusBar { }, + action = UI.SlideOut { + backgroundColor = colors.cyan, + titleBar = UI.TitleBar { + event = 'hide-action', + }, + button = UI.Button { + ex = -4, y = 4, width = 7, + text = 'Begin', event = 'begin', + }, + output = UI.Embedded { + y = 6, ey = -2, x = 2, ex = -2, + }, + statusBar = UI.StatusBar { + backgroundColor = colors.cyan, + }, + }, +} + +function page.grid:getRowTextColor(row, selected) + if row.installed then + return colors.yellow + end + return UI.Grid.getRowTextColor(self, row, selected) +end + +function page.action:show() + UI.SlideOut.show(self) + self.output:draw() + self.output.win.redraw() +end + +function page:run(operation, name) + local oterm = term.redirect(self.action.output.win) + self.action.output:clear() + local cmd = string.format('package %s %s', operation, name) + --for _ = 1, 3 do + -- print(cmd .. '\n') + -- os.sleep(1) + --end + term.setCursorPos(1, 1) + term.clear() + term.setTextColor(colors.yellow) + print(cmd .. '\n') + term.setTextColor(colors.white) + shell.run(cmd) + term.redirect(oterm) + self.action.output:draw() +end + +function page:updateSelection(selected) + self.add.operation = selected.installed and 'update' or 'install' + self.add.operationText = selected.installed and 'Update' or 'Install' +end + +function page:eventHandler(event) + if event.type == 'focus_change' then + self.statusBar:setStatus(event.focused.help) + + elseif event.type == 'grid_focus_row' then + local manifest = event.selected.manifest + + self.description.value = string.format('%s%s\n\n%s%s', + Ansi.yellow, manifest.title, + Ansi.white, manifest.description) + self.description:draw() + self:updateSelection(event.selected) + + elseif event.type == 'action' then + local selected = self.grid:getSelected() + if selected then + self.operation = event.button.operation + self.action.button.text = event.button.operationText + self.action.titleBar.title = selected.manifest.title + self.action.button.text = 'Begin' + self.action.button.event = 'begin' + self.action:show() + end + + elseif event.type == 'hide-action' then + self.action:hide() + + elseif event.type == 'begin' then + local selected = self.grid:getSelected() + self:run(self.operation, selected.name) + selected.installed = Packages:isInstalled(selected.name) + + self:updateSelection(selected) + self.action.button.text = 'Done' + self.action.button.event = 'hide-action' + self.action.button:draw() + + elseif event.type == 'quit' then + UI:exitPullEvents() + end + UI.Page.eventHandler(self, event) +end + +for k in pairs(Packages:list()) do + local manifest = Packages:getManifest(k) + if not manifest then + manifest = { + invalid = true, + description = 'Unable to download manifest', + title = '', + } + end + table.insert(page.grid.values, { + installed = not not Packages:isInstalled(k), + name = k, + manifest = manifest, + }) +end +page.grid:update() + +UI:setPage(page) +UI:pullEvents() diff --git a/sys/apps/System.lua b/sys/apps/System.lua index 10e4816..ce2a2b1 100644 --- a/sys/apps/System.lua +++ b/sys/apps/System.lua @@ -141,61 +141,63 @@ local systemPage = UI.Page { } if turtle then - local Home = require('turtle.home') + pcall(function() + local Home = require('turtle.home') +-- TODO: dont rely on turtle.home + local values = { } + Config.load('gps', values.home and { values.home } or { }) - local values = { } - Config.load('gps', values.home and { values.home } or { }) - - systemPage.tabs:add({ - gpsTab = UI.Window { - tabTitle = 'GPS', - labelText = UI.Text { - x = 3, y = 2, - value = 'On restart, return to this location' - }, - grid = UI.Grid { - x = 3, ex = -3, y = 4, - height = 2, - values = values, - inactive = true, - columns = { - { heading = 'x', key = 'x' }, - { heading = 'y', key = 'y' }, - { heading = 'z', key = 'z' }, + systemPage.tabs:add({ + gpsTab = UI.Window { + tabTitle = 'GPS', + labelText = UI.Text { + x = 3, y = 2, + value = 'On restart, return to this location' + }, + grid = UI.Grid { + x = 3, ex = -3, y = 4, + height = 2, + values = values, + inactive = true, + columns = { + { heading = 'x', key = 'x' }, + { heading = 'y', key = 'y' }, + { heading = 'z', key = 'z' }, + }, + }, + button1 = UI.Button { + x = 3, y = 7, + text = 'Set home', + event = 'gps_set', + }, + button2 = UI.Button { + ex = -3, y = 7, width = 7, + text = 'Clear', + event = 'gps_clear', }, }, - button1 = UI.Button { - x = 3, y = 7, - text = 'Set home', - event = 'gps_set', - }, - button2 = UI.Button { - ex = -3, y = 7, width = 7, - text = 'Clear', - event = 'gps_clear', - }, - }, - }) - function systemPage.tabs.gpsTab:eventHandler(event) - if event.type == 'gps_set' then - systemPage.notification:info('Determining location', 10) - systemPage:sync() - if Home.set() then - Config.load('gps', values) - self.grid:setValues(values.home and { values.home } or { }) + }) + function systemPage.tabs.gpsTab:eventHandler(event) + if event.type == 'gps_set' then + systemPage.notification:info('Determining location', 10) + systemPage:sync() + if Home.set() then + Config.load('gps', values) + self.grid:setValues(values.home and { values.home } or { }) + self.grid:draw() + systemPage.notification:success('Location set') + else + systemPage.notification:error('Unable to determine location') + end + return true + elseif event.type == 'gps_clear' then + fs.delete('usr/config/gps') + self.grid:setValues({ }) self.grid:draw() - systemPage.notification:success('Location set') - else - systemPage.notification:error('Unable to determine location') + return true end - return true - elseif event.type == 'gps_clear' then - fs.delete('usr/config/gps') - self.grid:setValues({ }) - self.grid:draw() - return true end - end + end) end if settings then diff --git a/sys/apps/package.lua b/sys/apps/package.lua new file mode 100644 index 0000000..1f0d212 --- /dev/null +++ b/sys/apps/package.lua @@ -0,0 +1,97 @@ +_G.requireInjector(_ENV) + +local Git = require('git') +local Packages = require('packages') +local Util = require('util') + +local fs = _G.fs +local term = _G.term + +local args = { ... } +local action = table.remove(args, 1) + +local function Syntax(msg) + _G.printError(msg) + print('\nSyntax: Package list | install [name] ... | update [name] | uninstall [name]') + error(0) +end + +local function progress(max) + -- modified from: https://pastebin.com/W5ZkVYSi (apemanzilla) + local _, y = term.getCursorPos() + local wide, _ = term.getSize() + term.setCursorPos(1, y) + term.write("[") + term.setCursorPos(wide - 6, y) + term.write("]") + local done = 0 + return function() + done = done + 1 + local value = done / max + term.setCursorPos(2,y) + term.write(("="):rep(math.floor(value * (wide - 8)))) + local percent = math.floor(value * 100) .. "%" + term.setCursorPos(wide - percent:len(),y) + term.write(percent) + end +end + +local function install(name) + local manifest = Packages:getManifest(name) or error('Invalid package') + local packageDir = fs.combine('packages', name) + local method = args[2] or 'local' + if method == 'remote' then + Util.writeTable(packageDir .. '/.install', { + mount = string.format('%s gitfs %s', packageDir, manifest.repository), + }) + Util.writeTable(fs.combine(packageDir, '.package'), manifest) + else + local list = Git.list(manifest.repository) + local showProgress = progress(Util.size(list)) + for path, entry in pairs(list) do + Util.download(entry.url, fs.combine(packageDir, path)) + showProgress() + end + end + return +end + +if action == 'list' then + for k in pairs(Packages:list()) do + Util.print('[%s] %s', Packages:isInstalled(k) and 'x' or ' ', k) + end + return +end + +if action == 'install' then + local name = args[1] or Syntax('Invalid package') + if Packages:isInstalled(name) then + error('Package is already installed') + end + install(name) + print('installation complete') + return +end + +if action == 'update' then + local name = args[1] or Syntax('Invalid package') + if not Packages:isInstalled(name) then + error('Package is not installed') + end + install(name) + print('update complete') + return +end + +if action == 'uninstall' then + local name = args[1] or Syntax('Invalid package') + if not Packages:isInstalled(name) then + error('Package is not installed') + end + local packageDir = fs.combine('packages', name) + fs.delete(packageDir) + print('removed: ' .. packageDir) + return +end + +Syntax('Invalid command') diff --git a/sys/apps/shell b/sys/apps/shell index 124d9fc..31ce233 100644 --- a/sys/apps/shell +++ b/sys/apps/shell @@ -365,7 +365,7 @@ local term = _G.term local textutils = _G.textutils local terminal = term.current() -Terminal.scrollable(terminal, 100) +--Terminal.scrollable(terminal, 100) terminal.noAutoScroll = true local config = { @@ -568,10 +568,10 @@ local function shellRead(history) local ie = Input:translate(event, p1, p2, p3) if ie then if ie.code == 'scroll_up' then - terminal.scrollUp() + --terminal.scrollUp() elseif ie.code == 'scroll_down' then - terminal.scrollDown() + --terminal.scrollDown() elseif ie.code == 'terminate' then bExit = true diff --git a/sys/apps/telnet.lua b/sys/apps/telnet.lua index 0684f8f..25c6c05 100644 --- a/sys/apps/telnet.lua +++ b/sys/apps/telnet.lua @@ -10,7 +10,7 @@ local os = _G.os local read = _G.read local term = _G.term -local options, args = Util.args({ ... }) +local args = { ... } local remoteId = tonumber(table.remove(args, 1) or '') if not remoteId then @@ -19,11 +19,11 @@ if not remoteId then end if not remoteId then - error('Syntax: telnet [-title TITLE] ID [PROGRAM]') + error('Syntax: telnet ID [PROGRAM] [ARGS]') end -if options.title and multishell then - multishell.setTitle(multishell.getCurrent(), options.title) +if multishell then + multishell.setTitle(multishell.getCurrent(), 'Telnet ' .. remoteId) end local socket, msg = Socket.connect(remoteId, 23) diff --git a/sys/apps/vnc.lua b/sys/apps/vnc.lua index 6ea3852..5b17700 100644 --- a/sys/apps/vnc.lua +++ b/sys/apps/vnc.lua @@ -1,12 +1,13 @@ _G.requireInjector(_ENV) -local Event = require('event') -local Socket = require('socket') -local Terminal = require('terminal') -local Util = require('util') +local Event = require('event') +local Socket = require('socket') +local Terminal = require('terminal') +local Util = require('util') local colors = _G.colors local multishell = _ENV.multishell +local os = _G.os local term = _G.term local remoteId @@ -26,75 +27,104 @@ if multishell then multishell.setTitle(multishell.getCurrent(), 'VNC-' .. remoteId) end -print('connecting...') -local socket, msg = Socket.connect(remoteId, 5900) +local function connect() + local socket, msg = Socket.connect(remoteId, 5900) -if not socket then - error(msg) -end + if not socket then + return false, msg + end -local function writeTermInfo() - local w, h = term.getSize() - socket:write({ - type = 'termInfo', - width = w, - height = h, - isColor = term.isColor(), + local function writeTermInfo() + local w, h = term.getSize() + socket:write({ + type = 'termInfo', + width = w, + height = h, + isColor = term.isColor(), + }) + end + + writeTermInfo() + + local ct = Util.shallowCopy(term.current()) + + if not ct.isColor() then + Terminal.toGrayscale(ct) + end + + ct.clear() + ct.setCursorPos(1, 1) + + Event.addRoutine(function() + while true do + local data = socket:read() + if not data then + break + end + for _,v in ipairs(data) do + ct[v.f](unpack(v.args)) + end + end + end) + + local filter = Util.transpose({ + 'char', 'paste', 'key', 'key_up', + 'mouse_scroll', 'mouse_click', 'mouse_drag', 'mouse_up', }) -end -writeTermInfo() - -local ct = Util.shallowCopy(term.current()) - -if not ct.isColor() then - Terminal.toGrayscale(ct) -end - -Event.addRoutine(function() while true do - local data = socket:read() - if not data then + local e = Event.pullEvent() + local event = e[1] + + if not socket.connected then break end - for _,v in ipairs(data) do - ct[v.f](unpack(v.args)) + + if filter[event] then + socket:write({ + type = 'shellRemote', + event = e, + }) + elseif event == 'term_resize' then + writeTermInfo() + elseif event == 'terminate' then + socket:close() + ct.setBackgroundColor(colors.black) + ct.clear() + ct.setCursorPos(1, 1) + return true end end -end) - -ct.clear() -ct.setCursorPos(1, 1) - -local filter = Util.transpose({ - 'char', 'paste', 'key', 'key_up', - 'mouse_scroll', 'mouse_click', 'mouse_drag', 'mouse_up', -}) + return false, "Connection Lost" +end while true do - local e = Event.pullEvent() - local event = e[1] + term.clear() + term.setCursorPos(1, 1) - if not socket.connected then - print() - print('Connection lost') - print('Press enter to exit') - _G.read() + print('connecting...') + local s, m = connect() + if s then break end - if filter[event] then - socket:write({ - type = 'shellRemote', - event = e, - }) - elseif event == 'term_resize' then - writeTermInfo() - elseif event == 'terminate' then - socket:close() - ct.setBackgroundColor(colors.black) - ct.clear() - ct.setCursorPos(1, 1) - break + term.setBackgroundColor(colors.black) + term.setTextColor(colors.white) + term.clear() + term.setCursorPos(1, 1) + print(m) + print('\nPress any key to exit') + print('\nRetrying in ... ') + local x, y = term.getCursorPos() + for i = 5, 1, -1 do + local timerId = os.startTimer(1) + term.setCursorPos(x, y) + term.write(i) + repeat + local e, id = os.pullEvent() + if e == 'char' or e == 'key' then + return + end + until e == 'timer' and id == timerId end end diff --git a/sys/boot/opus.boot b/sys/boot/opus.boot index 32c7010..8c5c413 100644 --- a/sys/boot/opus.boot +++ b/sys/boot/opus.boot @@ -11,7 +11,7 @@ for k,v in pairs(_ENV) do sandboxEnv[k] = v end -_G.debug = function() end +_G._debug = function() end local function makeEnv() local env = setmetatable({ }, { __index = _G }) diff --git a/sys/etc/app.db b/sys/etc/app.db index 2990a65..353b20b 100644 --- a/sys/etc/app.db +++ b/sys/etc/app.db @@ -1,10 +1,21 @@ { + [ "0a999012ffb87b3edac99adbdfc498b12831a1e2" ] = { + title = "Packages", + category = "System", + run = "PackageManager.lua", + iconExt = "\030c\0317\151\131\131\131\0307\031c\148\ +\030c\0317\151\131\0310\143\0317\131\0307\031c\148\ +\0307\031c\138\030f\0317\151\131\131\131", + }, [ "53ebc572b4a44802ba114729f07bdaaf5409a9d7" ] = { + title = "Network", category = "Apps", icon = "\0304 \030 \ \030f \0304 \0307 \030 \031 \031f)\ \030f \0304 \0307 \030 \031f)", - title = "Network", + iconExt = "\030 \031f \0305\031f\140\030f\0315\137\144\ +\030 \031f\030f\0314\131\131\0304\031f\148\030 \0305\155\150\149\ +\030 \031f\030f\0310\147\0300\031f\141\0304\149\0307\0318\149\030 ", run = "Network.lua", }, c7116629a6a855cb774d9c7c8ad822fd83c71fb5 = { @@ -13,6 +24,9 @@ icon = "\0304\031f \030f\0310o..\0304\031f \ \0304\031f \030f\0310.o.\0304\031f \ \0304\031f - ", + iconExt = "\0307\031f\135\0300\0317\159\0307\0310\144\031f\139\ +\0300\0317\131\0307\0310\147\0300\0317\156\131\ +\130\143\143\129", run = "rom/programs/reboot", }, fb91e24fa52d8d2b32937bf04d843f730319a902 = { @@ -21,6 +35,9 @@ icon = "\0301\03171\03180\030 \031 \ \0301\03181\030 \031 \ \0301\03170\03180\03171\0307\031f>", + iconExt = "\031f\128\0313\152\131\131\132\031f\128\ +\0313\139\159\129\0303\031f\159\129\139\ +\031f\128\0313\136\0303\031f\143\143\030f\0313\134\031f\128", run = "http://pastebin.com/raw/UzGHLbNC", }, c47ae15370cfe1ed2781eedc1dc2547d12d9e972 = { @@ -29,6 +46,9 @@ icon = " \031f?\031 \ \031f?\031 \ \031f?", + iconExt = "\0300\031f\129\030f\0310\131\0300\031f\148\030f\0310\148\ +\030 \031 \0300\031f\131\030f\0310\142\129\ +\030 \031 \0300\031f\131\030f\128", run = "Help.lua", }, b0832074630eb731d7fbe8074de48a90cd9bb220 = { @@ -37,46 +57,77 @@ icon = "\030f \ \030f\0310lua>\031 \ \030f ", + iconExt = "\0300\031f\151\030f\128\0300\159\159\159\030f\0310\144\0304\031f\159\030f\128\ +\0300\031f\149\030f\128\0300\149\149\151\145\030f\128\0314\153\ +\130\131\130\131\130\131\0314\130\031f\128", run = "sys/apps/Lua.lua", }, - df485c871329671f46570634d63216761441bcd6 = { - title = "Devices", - category = "System", - icon = "\0304 \030 \ -\030f \0304 \0307 \030 \031 \031f_\ -\030f \0304 \0307 \030 \031f/", - run = "Devices.lua", - }, bc0792d8dc81e8aa30b987246a5ce97c40cd6833 = { title = "System", category = "System", icon = " \0307\031f| \ \0307\031f---o\030 \031 \ \0307\031f| ", + iconExt = "\0318\138\0308\031f\130\0318\128\031f\129\030f\0318\133\ +\0318\143\0308\128\0317\143\0318\128\030f\143\ +\0318\138\135\143\139\133", run = "System.lua", }, - c5497bca58468ae64aed6c0fd921109217988db3 = { - title = "Events", - category = "System", - icon = "\0304\031f \030 \0311e\ -\030f\031f \0304 \030 \0311ee\031f \ -\030f\031f \0304 \030 \0311e\031f ", - run = "Events.lua", - }, [ "2a4d562b1d9a9c90bdede6fac8ce4f7402462b86" ] = { title = "Tasks", category = "System", icon = "\030f\031f \0315/\ \030f\031f \0315/\\/ \ \030f\0315/\031f ", + iconExt = "\031f\128\128\0305\159\030f\128\0305\159\030f\0315\134\031f\128\ +\031f\128\0315\152\129\137\0305\031f\158\139\030f\0317 \ +\0315\134\031f\128\128\128\128\0305\154\030f\0317 ", run = "Tasks.lua", }, + [ "a0365977708b7387ee9ce2c13e5820e6e11732cb" ] = { + title = "Pain", + category = "Apps", + icon = "\030 \031f\0307\031f\159\030 \159\030 \ +\030 \031f\0308\031f\135\0307\0318\144\140\030f\0317\159\143\031c\139\0302\135\030f\0312\157\ +\030 \031f\030f\0318\143\133\0312\136\0302\031f\159\159\143\131\030f\0312\132", + run = "http://pastebin.com/raw/wJQ7jav0", + }, + [ "48d6857f6b2869d031f463b13aa34df47e18c548" ] = { + title = "Breakout", + category = "Games", + icon = "\0301\031f \0309 \030c \030b \030e \030c \0306 \ +\030 \031f \ +\030 \031f \0300 \0310 ", + iconExt = "\030 \031f\030f\0319\144\030d\031f\159\030b\159\030f\0311\144\031b\144\030c\031f\159\030f\0311\144\ +\030 \031f\030f\0311\130\031b\129\0319\130\031e\130\0310\144\031d\129\0316\129\ +\030 \031f\030f\0310\136\140\140\030 ", + run = "https://gist.github.com/LDDestroier/c7528d95bc0103545c2a/raw", + }, + [ "53a5d150062b1e03206b9e15854b81060e3c7552" ] = { + title = "Minesweeper", + category = "Games", + icon = "\030f\031f \03131\0308\031f \030f\031d2\ +\030f\031f \031d2\03131\0308\031f \030f\03131\ +\030f\03131\0308\031f \030f\03131\031e3", + run = "https://pastebin.com/raw/nsKrHTbN", + }, + [ "01c933b2a36ad8ed2d54089cb2903039046c1216" ] = { + title = "Enchat", + icon = "\030e\031f\151\030f\031e\156\0311\140\0314\140\0315\140\031d\140\031b\140\031a\132\ +\030f\0314\128\030e\031f\132\030f\031e\132\0318nchat\ +\030f\031e\138\141\0311\140\0314\140\0315\132\0317v\03183\031a\132", + category = "Apps", + run = "https://raw.githubusercontent.com/LDDestroier/enchat/master/enchat3.lua", + }, [ "6ce6c512ea433a7fc5c8841628e7696cd0ff7f2b" ] = { title = "Files", category = "Apps", icon = "\0300\0317==\031 \0307 \ \0300\0317====\ \0300\0317====", + iconExt = "\030 \031f\0300\031f\136\140\132\0308\130\030f\0318\144\ +\030 \031f\030f\0310\157\0300\031f\147\030f\0310\142\143\149\ +\030 \031f\0300\031f\136\140\132\140\030f\0310\149", run = "Files.lua", }, [ "7fddb7d8d1d60b1eeefa9af01082e0811d4b484d" ] = { @@ -85,12 +136,18 @@ icon = "\0304\031f \ \0304\031f \030f\0310zz\031 \ \0304\031f \030f ", + iconExt = "\030e\031f\135\030f\031e\148\030e\128\031f\151\139\ +\030e\031e\128\030f\031f\128\031e\143\031f\128\030e\031e\128\ +\031e\139\030e\031f\130\131\129\030f\031e\135", run = "/rom/programs/shutdown", }, bdc1fd5d3c0f3dcfd55d010426e61bf9451e680d = { title = "Shell", category = "Apps", - icon = "\030f\0314\151\131\131\131\131\ + icon = "\0304 \030 \ +\0304 \030f\0314> \0310_\031 \ +\0304 \030f \030 ", + iconExt = "\030f\0314\151\131\131\131\131\ \030f\0314\149\030f\0314> \0310_ \ \030f\0314\149\030f ", run = "shell", @@ -127,6 +184,9 @@ icon = "\030d \030 \030e \030 \ \030d \030 \ \030d ", + iconExt = "\030 \031f\0305\031f\151\030f\0315\135\131\0305\031f\146\ +\030 \031f\030f\0315\130\141\0305\031f\139\030f\0315\130\ +\030 \031f\0305\031f\146\143\030f\0315\158\031e\130", run = "/rom/programs/fun/worm", }, [ "9f46ca3ef617166776ef6014a58d4e66859caa62" ] = { @@ -135,6 +195,17 @@ icon = " \030f \ \030f \0307 \ \030f \0307 \0300 ", + iconExt = "\031f\128\0307\143\131\131\131\131\143\030f\128\ +\0307\031f\129\0317\128\0319\136\0309\031b\136\132\0307\0319\132\0317\128\031f\130\ +\0317\130\143\0307\128\128\128\128\030f\143\129", run = "/rom/programs/fun/dj", }, + [ "76b849f460640bc789c433894382fb5acbac42a2" ] = { + title = "Tron", + category = "Games", + iconExt = "\030 \031f\030b\031f\143\030f\128\128\030b\143\143\143\030f\128\128\ +\030 \031f\0309\031b\140\030b\031f\151\030f\031b\131\0307\148\0317\128\030b\151\030f\031b\131\148\ +\030 \031f\030f\031b\131\031f\128\031b\131\0317\131\031f\128\0317\131\031b\131\031f\128", + run = "https://raw.githubusercontent.com/LDDestroier/CC/master/tron.lua", + }, } diff --git a/sys/etc/ext.theme b/sys/etc/ext.theme index e09bfc6..492529e 100644 --- a/sys/etc/ext.theme +++ b/sys/etc/ext.theme @@ -8,6 +8,15 @@ Button = { --focusIndicator = '\183', }, + Checkbox = { + checkedIndicator = '\4', + leftMarker = '\124', + rightMarker = '\124', + }, + Chooser = { + leftIndicator = '\17', + rightIndicator = '\16', + }, Grid = { focusIndicator = '\183', inverseSortIndicator = '\24', diff --git a/sys/extensions/1.device.lua b/sys/extensions/1.device.lua index 2ec843a..003ae14 100644 --- a/sys/extensions/1.device.lua +++ b/sys/extensions/1.device.lua @@ -33,12 +33,25 @@ local keyboard = _G.device.keyboard local mouse = _G.device.mouse local os = _G.os +local drivers = { } + kernel.hook('peripheral', function(_, eventData) local side = eventData[1] if side then local dev = Peripheral.addDevice(device, side) if dev then - os.queueEvent('device_attach', dev.name) + if drivers[dev.type] then + local e = drivers[dev.type](dev) + if type(e) == 'table' then + for _, v in pairs(e) do + os.queueEvent('device_attach', v.name) + end + elseif e then + os.queueEvent('device_attach', e.name) + end + end + + os.queueEvent('device_attach', dev.name, dev) end end end) @@ -48,7 +61,12 @@ kernel.hook('peripheral_detach', function(_, eventData) if side then local dev = Util.find(device, 'side', side) if dev then - os.queueEvent('device_detach', dev.name) + os.queueEvent('device_detach', dev.name, dev) + if dev._children then + for _,v in pairs(dev._children) do + os.queueEvent('peripheral_detach', v.name) + end + end device[dev.name] = nil end end @@ -109,3 +127,60 @@ kernel.hook('monitor_touch', function(event, eventData) return true -- stop propagation end end) + +local function createDevice(name, devType, method, manipulator) + local dev = { + name = name, + side = name, + type = devType, + } + local methods = { + 'drop', 'getDocs', 'getItem', 'getItemMeta', 'getTransferLocations', + 'list', 'pullItems', 'pushItems', 'size', 'suck', + } + if manipulator[method] then + for _,k in pairs(methods) do + dev[k] = function(...) + return manipulator[method]()[k](...) + end + end + if not manipulator._children then + manipulator._children = { dev } + else + table.insert(manipulator._children, dev) + end + device[name] = dev + end +end + +drivers['manipulator'] = function(dev) + if dev.getName then + local name + pcall(function() + name = dev.getName() + end) + if name then + if dev.getInventory then + createDevice(name .. ':inventory', 'inventory', 'getInventory', dev) + end + if dev.getEquipment then + createDevice(name .. ':equipment', 'equipment', 'getEquipment', dev) + end + if dev.getEnder then + createDevice(name .. ':enderChest', 'enderChest', 'getEnder', dev) + end + + return dev._children + end + end +end + +-- initialize drivers +for _,v in pairs(device) do + if drivers[v.type] then + local s, m = pcall(drivers[v.type], v) + if not s and m then + _G.printError(m) + end + end +end diff --git a/sys/extensions/4.user.lua b/sys/extensions/4.user.lua index 7688c4c..662fc6f 100644 --- a/sys/extensions/4.user.lua +++ b/sys/extensions/4.user.lua @@ -11,10 +11,10 @@ end if not fs.exists('usr/autorun') then fs.makeDir('usr/autorun') end -if not fs.exists('usr/config/fstab') then - Util.writeFile('usr/config/fstab', - 'usr gitfs kepler155c/opus-apps/' .. _G.OPUS_BRANCH) -end +--if not fs.exists('usr/config/fstab') then +-- Util.writeFile('usr/config/fstab', +-- 'usr gitfs kepler155c/opus-apps/' .. _G.OPUS_BRANCH) +--end if not fs.exists('usr/config/shell') then Util.writeTable('usr/config/shell', { @@ -24,6 +24,17 @@ if not fs.exists('usr/config/shell') then }) end +if not fs.exists('usr/config/packages') then + local packages = { + [ 'develop-1.8' ] = 'https://pastebin.com/raw/WhEiNGZE', + [ 'master-1.8' ] = 'https://pastebin.com/raw/pexZpAxt', + } + + if packages[_G.OPUS_BRANCH] then + Util.download(packages[_G.OPUS_BRANCH], 'usr/config/packages') + end +end + local config = Util.readTable('usr/config/shell') if config.aliases then for k in pairs(shell.aliases()) do diff --git a/sys/extensions/5.network.lua b/sys/extensions/5.network.lua index b776653..3963ae2 100644 --- a/sys/extensions/5.network.lua +++ b/sys/extensions/5.network.lua @@ -1,5 +1,10 @@ -local kernel = _G.kernel -local os = _G.os +_G.requireInjector(_ENV) + +local Config = require('config') + +local device = _G.device +local kernel = _G.kernel +local os = _G.os _G.network = { } @@ -11,15 +16,44 @@ local function startNetwork() }) end +local function setModem(dev) + if not device.wireless_modem and dev.isWireless() then + local config = Config.load('os', { }) + if not config.wirelessModem or dev.name == config.wirelessModem then + device.wireless_modem = dev + os.queueEvent('device_attach', 'wireless_modem') + return dev + end + end +end + +-- create a psuedo-device named 'wireleess_modem' kernel.hook('device_attach', function(_, eventData) - if eventData[1] == 'wireless_modem' then - startNetwork() + local dev = device[eventData[1]] + if dev and dev.type == 'modem' then + if setModem(dev) then + startNetwork() + end end end) -if _G.device.wireless_modem then +kernel.hook('device_detach', function(_, eventData) + if device.wireless_modem and eventData[1] == device.wireless_modem.name then + device['wireless_modem'] = nil + os.queueEvent('device_detach', 'wireless_modem') + end +end) + +for _,dev in pairs(device) do + if dev.type == 'modem' then + if setModem(dev) then + break + end + end +end + +if device.wireless_modem then print('waiting for network...') startNetwork() os.pullEvent('network_up') end - diff --git a/sys/extensions/6.packages.lua b/sys/extensions/6.packages.lua new file mode 100644 index 0000000..8fb598a --- /dev/null +++ b/sys/extensions/6.packages.lua @@ -0,0 +1,45 @@ +_G.requireInjector(_ENV) + +local Packages = require('packages') +local Util = require('util') + +local shell = _ENV.shell +local fs = _G.fs + +local appPaths = Util.split(shell.path(), '(.-):') +local luaPaths = Util.split(_G.LUA_PATH, '(.-):') + +local function addPath(t, e) + local function hasEntry() + for _,v in ipairs(t) do + if v == e then + return true + end + end + end + if not hasEntry() then + table.insert(t, 1, e) + end +end + +-- dependency graph +-- https://github.com/mpeterv/depgraph/blob/master/src/depgraph/init.lua + +for name in pairs(Packages:installed()) do + local packageDir = fs.combine('packages', name) + if fs.exists(fs.combine(packageDir, '.install')) then + local install = Util.readTable(fs.combine(packageDir, '.install')) + if install and install.mount then + fs.mount(table.unpack(Util.matches(install.mount))) + end + end + + addPath(appPaths, packageDir) + local apiPath = fs.combine(fs.combine('packages', name), 'apis') + if fs.exists(apiPath) then + addPath(luaPaths, apiPath) + end +end + +shell.setPath(table.concat(appPaths, ':')) +_G.LUA_PATH = table.concat(luaPaths, ':') diff --git a/sys/extensions/6.tl3.lua b/sys/extensions/6.tl3.lua index fab4b90..d82930a 100644 --- a/sys/extensions/6.tl3.lua +++ b/sys/extensions/6.tl3.lua @@ -7,7 +7,7 @@ _G.requireInjector(_ENV) local Pathing = require('turtle.pathfind') local GPS = require('gps') local Point = require('point') -local synchronized = require('sync') +local synchronized = require('sync').sync local Util = require('util') local os = _G.os @@ -458,6 +458,7 @@ function turtle.back() end local function moveTowardsX(dx) + if not tonumber(dx) then error('moveTowardsX: Invalid arguments') end local direction = dx - turtle.point.x local move @@ -662,11 +663,11 @@ function turtle._goto(pt) local dx, dy, dz, dh = pt.x, pt.y, pt.z, pt.heading if not turtle.gotoSingleTurn(dx, dy, dz, dh) then if not gotoMultiTurn(dx, dy, dz) then - return false + return false, 'Failed to reach location' end end turtle.setHeading(dh) - return true + return pt end -- avoid lint errors @@ -738,6 +739,8 @@ function turtle.getSlot(indexOrId, slots) } end + -- inconsistent return value + -- null is returned if indexOrId is a string and no item is present return { qty = 0, -- deprecate count = 0, @@ -794,8 +797,12 @@ function turtle.getSummedInventory() end function turtle.has(item, count) - local slot = turtle.getSummedInventory()[item] - return slot and slot.count >= (count or 1) + if item:match('.*:%d') then + local slot = turtle.getSummedInventory()[item] + return slot and slot.count >= (count or 1) + end + local slot = turtle.getSlot(item) + return slot and slot.count > 0 end function turtle.getFilledSlots(startSlot) @@ -966,6 +973,18 @@ function turtle.addWorldBlock(pt) Pathing.addBlock(pt) end +local movementStrategy = turtle.pathfind + +function turtle.setMovementStrategy(strategy) + if strategy == 'pathing' then + movementStrategy = turtle.pathfind + elseif strategy == 'goto' then + movementStrategy = turtle._goto + else + error('Invalid movement strategy') + end +end + function turtle.faceAgainst(pt, options) -- 4 sided options = options or { } options.dest = { } @@ -980,7 +999,7 @@ function turtle.faceAgainst(pt, options) -- 4 sided }) end - return turtle.pathfind(Point.closest(turtle.point, options.dest), options) + return movementStrategy(Point.closest(turtle.point, options.dest), options) end -- move against this point @@ -1012,7 +1031,7 @@ function turtle.moveAgainst(pt, options) -- 6 sided }) end - return turtle.pathfind(Point.closest(turtle.point, options.dest), options) + return movementStrategy(Point.closest(turtle.point, options.dest), options) end local actionsAt = { @@ -1097,7 +1116,7 @@ local function _actionAt(action, pt, ...) direction = 'up' end - if turtle.pathfind(apt) then + if movementStrategy(apt) then return action[direction](...) end end diff --git a/sys/extensions/7.multishell.lua b/sys/extensions/7.multishell.lua index 0a4a81d..4da07e8 100644 --- a/sys/extensions/7.multishell.lua +++ b/sys/extensions/7.multishell.lua @@ -1,7 +1,8 @@ _G.requireInjector(_ENV) -local Config = require('config') -local Util = require('util') +local Config = require('config') +local Packages = require('packages') +local Util = require('util') local colors = _G.colors local fs = _G.fs @@ -32,6 +33,7 @@ local config = { backgroundColor = colors.gray, tabBarBackgroundColor = colors.gray, focusBackgroundColor = colors.gray, + errorColor = colors.black, }, color = { textColor = colors.lightGray, @@ -40,6 +42,7 @@ local config = { backgroundColor = colors.gray, tabBarBackgroundColor = colors.gray, focusBackgroundColor = colors.gray, + errorColor = colors.red, }, } Config.load('multishell', config) @@ -129,6 +132,7 @@ function multishell.openTab(tab) print('\nPress enter to close') routine.isDead = true routine.hidden = false + redrawMenu() while true do local e, code = os.pullEventRaw('key') if e == 'terminate' or e == 'key' and code == keys.enter then @@ -252,8 +256,9 @@ kernel.hook('multishell_redraw', function() tab.ex = tabX + tab.width tabX = tabX + tab.width if tab ~= currentTab then + local textColor = tab.isDead and _colors.errorColor or _colors.textColor write(tab.sx, tab.title:sub(1, tab.width - 1), - _colors.backgroundColor, _colors.textColor) + _colors.backgroundColor, textColor) end end end @@ -371,6 +376,10 @@ local function startup() end runDir('sys/autorun', shell.run) + for name in pairs(Packages:installed()) do + local packageDir = 'packages/' .. name .. '/autorun' + runDir(packageDir, shell.run) + end runDir('usr/autorun', shell.run) if not success then diff --git a/sys/kernel.lua b/sys/kernel.lua index 2f5b457..b686bd8 100644 --- a/sys/kernel.lua +++ b/sys/kernel.lua @@ -28,12 +28,18 @@ local focusedRoutineEvents = Util.transpose { 'paste', 'terminate', } -_G.debug = function(pattern, ...) +_G._debug = function(pattern, ...) local oldTerm = term.redirect(kernel.window) Util.print(pattern, ...) term.redirect(oldTerm) end +if not _G.debug then -- don't clobber lua debugger + function _G.debug(...) + _G._debug(...) + end +end + -- any function that runs in a kernel hook does not run in -- a separate coroutine or have a window. an error in a hook -- function will crash the system. diff --git a/sys/network/redserver.lua b/sys/network/redserver.lua index f4f9351..fda019c 100644 --- a/sys/network/redserver.lua +++ b/sys/network/redserver.lua @@ -83,11 +83,15 @@ Event.on('modem_message', function(_, _, dport, dhost, request) if dport == 80 and dhost == computerId and type(request) == 'table' then if request.method == 'GET' then local query + if not request.path or type(request.path) ~= 'string' then + return + end local path = request.path:gsub('%?(.*)', function(v) query = parseQuery(v) return '' end) if fs.isDir(path) then + -- TODO: more validation modem.transmit(request.replyPort, request.replyAddress, { statusCode = 200, contentType = 'table/directory', diff --git a/sys/network/snmp.lua b/sys/network/snmp.lua index d9fa82d..50297b1 100644 --- a/sys/network/snmp.lua +++ b/sys/network/snmp.lua @@ -150,11 +150,13 @@ local function sendInfo() end if device.neuralInterface then info.status = device.neuralInterface.status - if not info.status and device.neuralInterface.getMetaOwner then - info.status = 'health: ' .. - math.floor(device.neuralInterface.getMetaOwner().health / - device.neuralInterface.getMetaOwner().maxHealth * 100) - end + pcall(function() + if not info.status and device.neuralInterface.getMetaOwner then + info.status = 'health: ' .. + math.floor(device.neuralInterface.getMetaOwner().health / + device.neuralInterface.getMetaOwner().maxHealth * 100) + end + end) end device.wireless_modem.transmit(999, os.getComputerID(), info) end diff --git a/sys/network/transport.lua b/sys/network/transport.lua index 77342c2..2eeb801 100644 --- a/sys/network/transport.lua +++ b/sys/network/transport.lua @@ -33,7 +33,7 @@ function transport.read(socket) end function transport.write(socket, data) - --debug('>> ' .. Util.tostring({ type = 'DATA', seq = socket.wseq })) + --_debug('>> ' .. Util.tostring({ type = 'DATA', seq = socket.wseq })) socket.transmit(socket.dport, socket.dhost, data) --local timerId = os.startTimer(3) @@ -45,7 +45,7 @@ function transport.write(socket, data) end function transport.ping(socket) - --debug('>> ' .. Util.tostring({ type = 'DATA', seq = socket.wseq })) + --_debug('>> ' .. Util.tostring({ type = 'DATA', seq = socket.wseq })) if os.clock() - socket.activityTimer > 10 then socket.activityTimer = os.clock() socket.transmit(socket.dport, socket.dhost, { @@ -78,9 +78,12 @@ Event.on('modem_message', function(_, _, dport, dhost, msg, distance) local socket = transport.sockets[dport] if socket and socket.connected then - --if msg.type then debug('<< ' .. Util.tostring(msg)) end + --if msg.type then _debug('<< ' .. Util.tostring(msg)) end + if socket.co and coroutine.status(socket.co) == 'dead' then + _G._debug('socket coroutine dead') + socket:close() - if msg.type == 'DISC' then + elseif msg.type == 'DISC' then -- received disconnect from other end if socket.connected then os.queueEvent('transport_' .. socket.uid) @@ -108,9 +111,9 @@ Event.on('modem_message', function(_, _, dport, dhost, msg, distance) socket.activityTimer = os.clock() if msg.seq ~= socket.rseq then print('transport seq error - closing socket ' .. socket.sport) - debug(msg.data) - debug('current ' .. socket.rseq) - debug('expected ' .. msg.seq) + _debug(msg.data) + _debug('current ' .. socket.rseq) + _debug('expected ' .. msg.seq) -- socket:close() -- os.queueEvent('transport_' .. socket.uid) else @@ -122,7 +125,7 @@ Event.on('modem_message', function(_, _, dport, dhost, msg, distance) os.queueEvent('transport_' .. socket.uid) end - --debug('>> ' .. Util.tostring({ type = 'ACK', seq = msg.seq })) + --_debug('>> ' .. Util.tostring({ type = 'ACK', seq = msg.seq })) --socket.transmit(socket.dport, socket.dhost, { -- type = 'ACK', -- seq = msg.seq,