diff --git a/apps/Appstore.lua b/apps/Appstore.lua new file mode 100644 index 0000000..f36dda8 --- /dev/null +++ b/apps/Appstore.lua @@ -0,0 +1,414 @@ +requireInjector(getfenv(1)) + +local Config = require('config') +local SHA1 = require('sha1') +local UI = require('ui') +local Util = require('util') + +-- scrap this entire file. don't muck with standard apis + +local REGISTRY_DIR = 'usr/.registry' + + +-- FIX SOMEDAY +function os.registerApp(app, key) + + app.key = SHA1.sha1(key) + Util.writeTable(fs.combine(REGISTRY_DIR, app.key), app) + os.queueEvent('os_register_app') +end + +function os.unregisterApp(key) + + local filename = fs.combine(REGISTRY_DIR, SHA1.sha1(key)) + if fs.exists(filename) then + fs.delete(filename) + os.queueEvent('os_register_app') + end +end + + +local sandboxEnv = Util.shallowCopy(getfenv(1)) +setmetatable(sandboxEnv, { __index = _G }) + +multishell.setTitle(multishell.getCurrent(), 'App Store') +UI:configure('Appstore', ...) + +local APP_DIR = 'usr/apps' + +local sources = { + + { text = "STD Default", + event = 'source', + url = "http://pastebin.com/raw/zVws7eLq" }, --stock +--[[ + { text = "Discover", + event = 'source', + generateName = true, + url = "http://pastebin.com/raw/9bXfCz6M" }, --owned by dannysmc95 + + { text = "Opus", + event = 'source', + url = "http://pastebin.com/raw/ajQ91Rmn" }, +]] +} + +shell.setDir(APP_DIR) + +function downloadApp(app) + local h + + if type(app.url) == "table" then + h = contextualGet(app.url[1]) + else + h = http.get(app.url) + end + + if h then + local contents = h.readAll() + h:close() + return contents + end +end + +function runApp(app, checkExists, ...) + + local path, fn + local args = { ... } + + if checkExists and fs.exists(fs.combine(APP_DIR, app.name)) then + path = fs.combine(APP_DIR, app.name) + else + local program = downloadApp(app) + + fn = function() + + if not program then + error('Failed to download') + end + + local fn = loadstring(program, app.name) + + if not fn then + error('Failed to download') + end + + setfenv(fn, sandboxEnv) + fn(unpack(args)) + end + end + + multishell.openTab({ + title = app.name, + env = sandboxEnv, + path = path, + fn = fn, + focused = true, + }) + + return true, 'Running program' +end + +local installApp = function(app) + + local program = downloadApp(app) + if not program then + return false, "Failed to download" + end + + local fullPath = fs.combine(APP_DIR, app.name) + Util.writeFile(fullPath, program) + return true, 'Installed as ' .. fullPath +end + +local viewApp = function(app) + + local program = downloadApp(app) + if not program then + return false, "Failed to download" + end + + Util.writeFile('/.source', program) + shell.openForegroundTab('edit /.source') + fs.delete('/.source') + return true +end + +local getSourceListing = function(source) + local contents = http.get(source.url) + if contents then + + local fn = loadstring(contents.readAll(), source.text) + contents.close() + + local env = { std = { } } + setmetatable(env, { __index = _G }) + setfenv(fn, env) + fn() + + if env.contextualGet then + contextualGet = env.contextualGet + end + + source.storeURLs = env.std.storeURLs + source.storeCatagoryNames = env.std.storeCatagoryNames + + if source.storeURLs and source.storeCatagoryNames then + for k,v in pairs(source.storeURLs) do + if source.generateName then + v.name = v.title:match('(%w+)') + if not v.name or #v.name == 0 then + v.name = tostring(k) + else + v.name = v.name:lower() + end + else + v.name = k + end + v.categoryName = source.storeCatagoryNames[v.catagory] + v.ltitle = v.title:lower() + end + end + end +end + +local appPage = UI.Page({ + backgroundColor = UI.ViewportWindow.defaults.backgroundColor, + menuBar = UI.MenuBar({ + showBackButton = not pocket, + buttons = { + { text = 'Install', event = 'install' }, + { text = 'Run', event = 'run' }, + { text = 'View', event = 'view' }, + { text = 'Remove', event = 'uninstall', name = 'removeButton' }, + }, + }), + container = UI.Window({ + x = 2, + y = 3, + height = UI.term.height - 3, + width = UI.term.width - 2, + viewport = UI.ViewportWindow(), + }), + notification = UI.Notification(), + accelerators = { + q = 'back', + backspace = 'back', + }, +}) + +function appPage.container.viewport:draw() + local app = self.parent.parent.app + local str = string.format( + 'By: %s\nCategory: %s\nFile name: %s\n\n%s', + app.creator, app.categoryName, app.name, app.description) + + self:clear() + local y = self:wrappedWrite(1, 1, app.title, self.width, nil, colors.yellow) + self.height = self:wrappedWrite(1, y, str, self.width) + + if appPage.notification.enabled then + appPage.notification:draw() + end +end + +function appPage:enable(source, app) + self.source = source + self.app = app + UI.Page.enable(self) + + self.container.viewport:setScrollPosition(0) + if fs.exists(fs.combine(APP_DIR, app.name)) then + self.menuBar.removeButton:enable('Remove') + else + self.menuBar.removeButton:disable('Remove') + end +end + +function appPage:eventHandler(event) + if event.type == 'back' then + UI:setPreviousPage() + + elseif event.type == 'run' then + self.notification:info('Running program', 3) + self:sync() + runApp(self.app, true) + + elseif event.type == 'view' then + self.notification:info('Downloading program', 3) + self:sync() + viewApp(self.app) + + elseif event.type == 'uninstall' then + if self.app.runOnly then + s,m = runApp(self.app, false, 'uninstall') + else + fs.delete(fs.combine(APP_DIR, self.app.name)) + self.notification:success("Uninstalled " .. self.app.name, 3) + self:focusFirst(self) + self.menuBar.removeButton:disable('Remove') + self.menuBar:draw() + + os.unregisterApp(self.app.creator .. '.' .. self.app.name) + end + + elseif event.type == 'install' then + self.notification:info("Installing", 3) + self:sync() + local s, m + if self.app.runOnly then + s,m = runApp(self.app, false) + else + s,m = installApp(self.app) + end + if s then + self.notification:success(m, 3) + + if not self.app.runOnly then + self.menuBar.removeButton:enable('Remove') + self.menuBar:draw() + + local category = 'Apps' + if self.app.catagoryName == 'Game' then + category = 'Games' + end + + os.registerApp({ + run = fs.combine(APP_DIR, self.app.name), + title = self.app.title, + category = category, + icon = self.app.icon, + }, self.app.creator .. '.' .. self.app.name) + end + else + self.notification:error(m, 3) + end + else + return UI.Page.eventHandler(self, event) + end + return true +end + +local categoryPage = UI.Page({ + menuBar = UI.MenuBar({ + buttons = { + { text = 'Catalog', event = 'dropdown', dropdown = 'sourceMenu' }, + { text = 'Category', event = 'dropdown', dropdown = 'categoryMenu' }, + }, + }), + sourceMenu = UI.DropMenu({ + buttons = sources, + }), + grid = UI.ScrollingGrid({ + y = 2, + height = UI.term.height - 2, + columns = { + { heading = 'Title', key = 'title' }, + }, + sortColumn = 'title', + autospace = true, + }), + statusBar = UI.StatusBar(), + accelerators = { + l = 'lua', + q = 'quit', + }, +}) + +function categoryPage:setCategory(source, name, index) + self.grid.values = { } + for k,v in pairs(source.storeURLs) do + if index == 0 or index == v.catagory then + table.insert(self.grid.values, v) + end + end + self.statusBar:setStatus(string.format('%s: %s', source.text, name)) + self.grid:update() + self.grid:setIndex(1) +end + +function categoryPage:setSource(source) + + if not source.categoryMenu then + + self.statusBar:setStatus('Loading...') + self.statusBar:draw() + self:sync() + + getSourceListing(source) + + if not source.storeURLs then + error('Unable to download application list') + end + + local buttons = { } + for k,v in Util.spairs(source.storeCatagoryNames, + function(a, b) return a:lower() < b:lower() end) do + + if v ~= 'Operating System' then + table.insert(buttons, { + text = v, + event = 'category', + index = k, + }) + end + end + + source.categoryMenu = UI.DropMenu({ + y = 2, + x = 1, + buttons = buttons, + }) + source.index, source.name = Util.first(source.storeCatagoryNames) + + categoryPage:add({ + categoryMenu = source.categoryMenu + }) + end + + self.source = source + self.categoryMenu = source.categoryMenu + categoryPage:setCategory(source, source.name, source.index) +end + +function categoryPage.grid:sortCompare(a, b) + return a.ltitle < b.ltitle +end + +function categoryPage.grid:getRowTextColor(row, selected) + if fs.exists(fs.combine(APP_DIR, row.name)) then + return colors.orange + end + return UI.Grid:getRowTextColor(row, selected) +end + +function categoryPage:eventHandler(event) + + if event.type == 'grid_select' or event.type == 'select' then + UI:setPage(appPage, self.source, self.grid:getSelected()) + + elseif event.type == 'category' then + self:setCategory(self.source, event.button.text, event.button.index) + self:setFocus(self.grid) + self:draw() + + elseif event.type == 'source' then + self:setFocus(self.grid) + self:setSource(event.button) + self:draw() + + elseif event.type == 'quit' then + UI:exitPullEvents() + + else + return UI.Page.eventHandler(self, event) + end + return true +end + +print("Retrieving catalog list") +categoryPage:setSource(sources[1]) + +UI:setPage(categoryPage) +UI:pullEvents() +UI.term:reset() diff --git a/apps/Events.lua b/apps/Events.lua new file mode 100644 index 0000000..178e96d --- /dev/null +++ b/apps/Events.lua @@ -0,0 +1,132 @@ +requireInjector(getfenv(1)) + +local Event = require('event') +local UI = require('ui') +local Util = require('util') + +multishell.setTitle(multishell.getCurrent(), 'Events') +UI:configure('Events', ...) + +local page = UI.Page({ + menuBar = UI.MenuBar({ + buttons = { + { text = 'Filter', event = 'filter' }, + { text = 'Reset', event = 'reset' }, + { text = 'Pause ', event = 'toggle', name = 'pauseButton' }, + }, + }), + grid = UI.Grid({ + y = 2, + columns = { + { heading = 'Event', key = 'event' }, + { key = 'p1' }, + { key = 'p2' }, + { key = 'p3' }, + { key = 'p4' }, + { key = 'p5' }, + }, + autospace = true, + }), + accelerators = { + f = 'filter', + p = 'toggle', + r = 'reset', + c = 'clear', + q = 'quit', + }, + filtered = { }, +}) + +function page:eventHandler(event) + + if event.type == 'filter' then + local entry = self.grid:getSelected() + self.filtered[entry.event] = true + + elseif event.type == 'toggle' then + self.paused = not self.paused + if self.paused then + self.menuBar.pauseButton.text = 'Resume' + else + self.menuBar.pauseButton.text = 'Pause ' + end + self.menuBar:draw() + + elseif event.type == 'grid_select' then + multishell.openTab({ path = 'sys/apps/Lua.lua', args = { event.selected }, focused = true }) + + elseif event.type == 'reset' then + self.filtered = { } + self.grid:setValues({ }) + self.grid:draw() + if self.paused then + self:emit({ type = 'toggle' }) + end + + elseif event.type == 'clear' then + self.grid:setValues({ }) + self.grid:draw() + + elseif event.type == 'quit' then + Event.exitPullEvents() + + elseif event.type == 'focus_change' then + if event.focused == self.grid then + if not self.paused then + self:emit({ type = 'toggle' }) + end + end + + else + return UI.Page.eventHandler(self, event) + end + return true +end + +function page.grid:getDisplayValues(row) + row = Util.shallowCopy(row) + + local function tovalue(s) + if type(s) == 'table' then + return 'table' + end + return s + end + + for k,v in pairs(row) do + row[k] = tovalue(v) + end + + return row +end + +function page.grid:draw() + self:adjustWidth() + UI.Grid.draw(self) +end + +Event.addRoutine(function() + + while true do + local e = { os.pullEvent() } + if not page.paused and not page.filtered[e[1]] then + table.insert(page.grid.values, 1, { + event = e[1], + p1 = e[2], + p2 = e[3], + p3 = e[4], + p4 = e[5], + p5 = e[6], + }) + if #page.grid.values > page.grid.height - 1 then + table.remove(page.grid.values, #page.grid.values) + end + page.grid:update() + page.grid:draw() + page:sync() + end + end +end) + +UI:setPage(page) +UI:pullEvents() diff --git a/apps/Peripherals.lua b/apps/Peripherals.lua new file mode 100644 index 0000000..b0f9df4 --- /dev/null +++ b/apps/Peripherals.lua @@ -0,0 +1,218 @@ +requireInjector(getfenv(1)) + +local Event = require('event') +local UI = require('ui') +local Util = require('util') + +multishell.setTitle(multishell.getCurrent(), 'Devices') + +--[[ -- PeripheralsPage -- ]] -- +local peripheralsPage = UI.Page { + grid = UI.ScrollingGrid { + columns = { + { heading = 'Type', key = 'type' }, + { heading = 'Side', key = 'side' }, + }, + sortColumn = 'type', + height = UI.term.height - 1, + autospace = true, + }, + statusBar = UI.StatusBar { + status = 'Select peripheral' + }, + accelerators = { + q = 'quit', + }, +} + +function peripheralsPage.grid:draw() + local sides = peripheral.getNames() + + Util.clear(self.values) + for _,side in pairs(sides) do + table.insert(self.values, { + type = peripheral.getType(side), + side = side + }) + end + self:update() + self:adjustWidth() + UI.Grid.draw(self) +end + +function peripheralsPage:updatePeripherals() + if UI:getCurrentPage() == self then + self.grid:draw() + self:sync() + end +end + +function peripheralsPage:eventHandler(event) + if event.type == 'quit' then + Event.exitPullEvents() + + elseif event.type == 'grid_select' then + UI:setPage('methods', event.selected) + + end + return UI.Page.eventHandler(self, event) +end + +--[[ -- MethodsPage -- ]] -- +local methodsPage = UI.Page { + grid = UI.ScrollingGrid { + columns = { + { heading = 'Name', key = 'name', width = UI.term.width } + }, + sortColumn = 'name', + height = 7, + }, + viewportConsole = UI.ViewportWindow { + y = 8, + height = UI.term.height - 8, + backgroundColor = colors.brown, + }, + statusBar = UI.StatusBar { + status = 'q to return', + }, + accelerators = { + q = 'back', + backspace = 'back', + }, +} + +function methodsPage:enable(p) + + self.peripheral = p or self.peripheral + + local p = peripheral.wrap(self.peripheral.side) + if p.getDocs then + self.grid.values = { } + for k,v in pairs(p.getDocs()) do + table.insert(self.grid.values, { + name = k, + doc = v, + }) + end + elseif not p.getAdvancedMethodsData then + self.grid.values = { } + for name,f in pairs(p) do + table.insert(self.grid.values, { + name = name, + noext = true, + }) + end + else + self.grid.values = p.getAdvancedMethodsData() + for name,f in pairs(self.grid.values) do + f.name = name + end + end + + self.viewportConsole.offy = 0 + + self.grid:update() + self.grid:setIndex(1) + + self.statusBar:setStatus(self.peripheral.type) + UI.Page.enable(self) +end + +function methodsPage:eventHandler(event) + if event.type == 'back' then + UI:setPage(peripheralsPage) + return true + elseif event.type == 'grid_focus_row' then + self.viewportConsole.offy = 0 + self.viewportConsole:draw() + end + return UI.Page.eventHandler(self, event) +end + +function methodsPage.viewportConsole:draw() + local c = self + local method = methodsPage.grid:getSelected() + + c:clear() + c:setCursorPos(1, 1) + + if method.noext then + c.cursorY = 2 + c:print('No extended Information') + return 2 + end + + if method.doc then + c:print(method.doc, nil, colors.yellow) + c.ymax = c.cursorY + 1 + return + end + + if method.description then + c:print(method.description) + end + + c.cursorY = c.cursorY + 2 + c.cursorX = 1 + + if method.returnTypes ~= '()' then + c:print(method.returnTypes .. ' ', nil, colors.yellow) + end + c:print(method.name, nil, colors.black) + c:print('(') + + local maxArgLen = 1 + + for k,arg in ipairs(method.args) do + if #arg.description > 0 then + maxArgLen = math.max(#arg.name, maxArgLen) + end + local argName = arg.name + local fg = colors.green + if arg.optional then + argName = string.format('[%s]', arg.name) + fg = colors.orange + end + c:print(argName, nil, fg) + if k < #method.args then + c:print(', ') + end + end + c:print(')') + + c.cursorY = c.cursorY + 1 + + if #method.args > 0 then + for _,arg in ipairs(method.args) do + if #arg.description > 0 then + c.cursorY = c.cursorY + 1 + c.cursorX = 1 + local fg = colors.green + if arg.optional then + fg = colors.orange + end + c:print(arg.name .. ': ', nil, fg) + c.cursorX = maxArgLen + 3 + c:print(arg.description, nil, nil, maxArgLen + 3) + end + end + end + + c.ymax = c.cursorY + 1 +end + +Event.on('peripheral', function() + peripheralsPage:updatePeripherals() +end) + +Event.on('peripheral_detach', function() + peripheralsPage:updatePeripherals() +end) + +UI:setPage(peripheralsPage) + +UI:setPages({ + methods = methodsPage, +}) + +UI:pullEvents() diff --git a/apps/Script.lua b/apps/Script.lua new file mode 100644 index 0000000..c90545e --- /dev/null +++ b/apps/Script.lua @@ -0,0 +1,563 @@ +requireInjector(getfenv(1)) + +local Config = require('config') +local Event = require('event') +local Socket = require('socket') +local UI = require('ui') +local Util = require('util') + +local GROUPS_PATH = 'usr/groups' +local SCRIPTS_PATH = 'usr/etc/scripts' + +multishell.setTitle(multishell.getCurrent(), 'Script') +UI:configure('script', ...) + +local config = { + showGroups = false, + variables = [[{ + COMPUTER_ID = os.getComputerID(), + }]], +} + +Config.load('script', config) + +local width = math.floor(UI.term.width / 2) - 1 +if UI.term.width % 2 ~= 0 then + width = width + 1 +end + +function processVariables(script) + + local fn = loadstring('return ' .. config.variables) + if fn then + local variables = fn() + + for k,v in pairs(variables) do + local token = string.format('{%s}', k) + script = script:gsub(token, v) + end + end + return script +end + +function invokeScript(computer, scriptName) + + local script = Util.readFile(scriptName) + if not script then + print('Unable to read script file') + end + + local socket = Socket.connect(computer.id, 161) + if not socket then + print('Unable to connect to ' .. computer.id) + return + end + + script = processVariables(script) + + Util.print('Running %s on %s', scriptName, computer.label) + socket:write({ type = 'script', args = script }) + --[[ + local response = socket:read(2) + + if response and response.result then + if type(response.result) == 'table' then + print(textutils.serialize(response.result)) + else + print(tostring(response.result)) + end + else + printError('No response') + end + --]] + + socket:close() +end + +function runScript(computerOrGroup, scriptName) + if computerOrGroup.id then + invokeScript(computerOrGroup, scriptName) + else + local list = computerOrGroup.list + if computerOrGroup.path then + list = Util.readTable(computerOrGroup.path) + end + if list then + for _,computer in pairs(list) do + invokeScript(computer, scriptName) + end + end + end +end + +local function getActiveComputers(t) + t = t or { } + Util.clear(t) + for k,computer in pairs(_G.network) do + if computer.active then + t[k] = computer + end + end + return t +end + +local function getTurtleList() + local turtles = { + label = 'Turtles', + list = { }, + } + for k,computer in pairs(getActiveComputers()) do + if computer.fuel then + turtles.list[k] = computer + end + end + return turtles +end + +local args = { ... } +if #args == 2 then + local key = args[1] + local script = args[2] + local target + if tonumber(key) then + target = _G.network[tonumber(key)] + elseif key == 'All' then + target = { + list = Util.shallowCopy(getActiveComputers()), + } + elseif key == 'Localhost' then + target = { id = os.getComputerID() } + elseif key == 'Turtles' then + target = getTurtleList() + else + target = Util.readTable(fs.combine(GROUPS_PATH, key)) + end + + if not target then + error('Syntax: Script