diff --git a/.gitignore b/.gitignore index 7b8cb54..45803c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ lib/* -cfg/* \ No newline at end of file +cfg/* +.DS_Store +.vscode \ No newline at end of file diff --git a/allium.lua b/allium.lua index 22711ad..3362969 100644 --- a/allium.lua +++ b/allium.lua @@ -1,35 +1,51 @@ -- Allium by hugeblank -- Dependency Loading -local raisin, color, semver, mojson = require("lib.raisin"), require("lib.color"), require("lib.semver"), require("lib.mojson") +local raisin, color, semver, mojson, json = require("lib.raisin"), require("lib.color"), require("lib.semver"), require("lib.mojson"), require("lib.json") -- Internal definitions -local allium, plugins, group = {}, {}, {thread = raisin.group(1) , command = raisin.group(2)} +local allium, plugins, group = {}, {}, {thread = raisin.group(1) , command = raisin.group(2)} -local function print(noline, ...) -- Magical function that takes in a table and changes the text color/writes at the same time - local words = {...} - if type(noline) ~= "boolean" then - table.insert(words, 1, noline) - noline = false - end - local text_color = term.getTextColor() - for i = 1, #words do - if type(words[i]) == "number" then - term.setTextColor(words[i]) - elseif type(words[i]) == "table" then - print(unpack(words[i])) - else - write(tostring(words[i])) +-- Executing path +local path = "/" +for str in string.gmatch(shell.getRunningProgram(), ".+[/]") do + path = path..str +end + +-- Defining custom print +local nprint = _G.print +local function print(prefix, wcText, ...) -- Magical function that takes in a table and changes the text color/writes at the same time + local color = term.getTextColor() + local function writeColor(cdata) + for i = 1, #cdata do + if type(cdata[i]) == "string" then + write(cdata[i]) + else + term.setTextColor(cdata[i]) + end end + term.setTextColor(color) end - if not noline then - write("\n") + writeColor(prefix) + if wcText then + writeColor({...}) + nprint() + else + nprint(...) end - term.setTextColor(text_color) +end + +local function getData(name) -- Extract data on user from data command + local suc, data = commands.exec("data get entity "..name) + if not suc then return suc, data end + data = data[1]:sub(data[1]:find("{"), -1) + local data = mojson.parseList(data) + if not data then return false end + return data end local function deep_copy(table) -- Recursively copy a module - out = {} + local out = {} for name, func in pairs(table) do if type(func) == "table" then out[name] = deep_copy(func) @@ -45,66 +61,26 @@ local function assert(condition, message, level) end local cli = { - info = {true, "[", colors.lime, "I", colors.white, "] "}, - warn = {true, "[", colors.yellow, "W", colors.white, "] "}, - error = {true, "[", colors.red, "E", colors.white, "] "} + info = {"[", colors.lime, "I", colors.white, "] "}, + warn = {"[", colors.yellow, "W", colors.white, "] "}, + error = {"[", colors.red, "E", colors.white, "] "} } -local config +local config, up = ... do -- Configuration parsing - local file, default, rule = fs.open("cfg/allium.lson", "r"), {import_timeout = 5, label = "<&r&dAll&5&h[[Hugeblank was here. Hi.]]&i[[https://www.youtube.com/watch?v=hjGZLnja1o8]]i&r&dum&r> "} - local function verify_cfg(input, default, index) - for f_k, f_v in pairs(input) do -- input key, value - for t_k, t_v in pairs(default) do -- standard key, value - if type(f_v) == "table" and type(t_v) == "table" then - if not verify_cfg(f_v, t_v, f_k..".") then - return false - end - elseif f_k == t_k and type(f_v) ~= type(t_v) then - printError("Invalid config option "..(index or "")..f_k.." (expected "..type(t_v)..", got "..type(f_v)..")") - return false - end - end - end - return true - end - local function fill_missing(file, default) - local out = {} - for k, v in pairs(default) do - if type(v) == "table" then - out[k] = fill_missing(file[k], v) - else - if file[k] == nil then - out[k] = v - else - out[k] = file[k] - end - end - end - return out - end - if not file then -- Could not read file - printError("Could not read config") - return - end - local output = textutils.unserialise(file.readAll()) - if not output then -- Config file in invalid format - printError("Could not parse config") + if type(config) ~= "table" then + printError("Invalid input configuration, make sure you're using the provided init file.") return end - allium.version, rule = semver.parse(output.version) + local ver, rule = semver.parse(config.version) + allium.version = ver if not allium.version then -- Invalid Allium version printError("Could not parse Allium's version (breaks SemVer rule #"..rule..")") return end - output.version = nil - if verify_cfg(output, default) then -- Invalid configuration option (skips missing ones) - config = fill_missing(output, default) - else - return - end end +local main -- Main terminal window Allium is outputting to do -- Allium image setup <3 local image = { " 2a2", @@ -131,12 +107,12 @@ do -- Allium image setup <3 term.setCursorPos(x-7, cy+1) end local win = window.create(term.current(), 1, 1, x-9, y, true) -- Create a window to prevent text from writing over the image - term.redirect(win) -- Redirect the terminal + main = term.redirect(win) -- Redirect the terminal term.setCursorPos(1, 1) term.setBackgroundColor(colors.black) -- Reset terminal and cursor term.setTextColor(colors.white) - print(cli.info, "Loading ", colors.magenta, "All", colors.purple, "i", colors.magenta, "um") - print(cli.info, "Initializing API") + print(cli.info, true, "Loading ", colors.magenta, "All", colors.purple, "i", colors.magenta, "um") + print(cli.info, true, "Initializing API") end allium.assert = assert @@ -146,20 +122,32 @@ allium.sanitize = function(name) return name:lower():gsub(" ", "_"):gsub("[^a-z0-9_]", "") end +-- Logging wrapper functions +allium.log = function(...) + print(cli.info, false, ...) +end + +allium.warn = function(...) + print(cli.warn, false, ...) +end + allium.tell = function(name, message, alt_name) assert(type(name) == "string", "Invalid argument #1 (expected string, got "..type(name)..")") - assert(type(message) == "string" or type(message) == "table", "Invalid argument #2 (expected string or table, got "..type(message)..")") - local out + assert(type(message) == "string" or type(message) == "table", "Invalid argument #2 (expected string or table, got "..type(message)..")") + local out = {} if type(message) == "table" then - _, out = commands.tellraw(name, color.format(table.concat(message, "\\n"))) + for i = 1, #message do + _, out[#out+1] = commands.async.tellraw(name, json.encode(color.format(message[i]))) + end else - message = message:gsub("\n", "\\n") - _, out = commands.tellraw(name, color.format((function(alt_name) if alt_name == true then return "" elseif alt_name then return alt_name.."&r" else return config.label.."&r" end end)(alt_name)..message)) - end - return textutils.serialise(out) + _, out = commands.tellraw(name, json.encode(color.format((function(alt_name) if alt_name == true then return "" elseif alt_name then return alt_name.."&r" else return config.label.."&r" end end)(alt_name)..message))) + end + return out end allium.execute = function(name, command) + assert(type(name) == "string", "Invalid argument #1 (string expected, got "..type(name)..")") + assert(type(command) == "string", "Invalid argument #2 (string expected, got "..type(command)..")") os.queueEvent("chat_capture", command, "execute", name) end @@ -180,10 +168,9 @@ allium.getPlayers = function() end allium.getPosition = function(name) - local suc, data = commands.exec("data get entity "..name) - if not suc then return false, data end - data = data[1]:sub(data[1]:find("{"), -1) - local data = mojson.parseList(data) + assert(type(name) == "string", "Invalid argument #1 (string expected, got "..type(name)..")") + local data = getData(name) + assert(data, "Failed to get data on user ".. name) return { position = data.Pos, rotation = data.Rotation, @@ -248,7 +235,8 @@ allium.register = function(p_name, version, fullname) assert(plugins[real_name] == nil, "Invalid argument #1 (plugin exists under name "..real_name..")") local version, rule = semver.parse(version) assert(type(version) == "table", "Invalid argument #2 (malformed SemVer, breaks rule "..(rule or "")..")") - plugins[real_name] = {commands = {}, name = fullname or p_name, version = version} + local loaded = {} + plugins[real_name] = {commands = {}, loaded = loaded, name = fullname or p_name, version = version} local funcs, this = {}, plugins[real_name] funcs.command = function(c_name, command, info) -- name: name | command: executing function | info: help information @@ -266,13 +254,50 @@ allium.register = function(p_name, version, fullname) funcs.thread = function(thread) -- Add a thread that repeatedly iterates assert(type(thread) == "function", "Invalid argument #1 (function expected, got "..type(thread)..")") - return raisin.thread(thread, 0, group.thread) + local wrapper = function() + local s, e = pcall(thread) + if not s then + allium.warn("Thread in "..real_name.." | "..e) + end + end + return raisin.thread(wrapper, 0, group.thread) end + funcs.loadConfig = function(default) + assert(type(default) == "table", "Invalid argument #1 (table expected, got "..type(default)..")") + local file = path.."/cfg/"..real_name..".lson" + if not fs.exists(file) then + local setting = fs.open(file,"w") + setting.write(textutils.serialise(default)) + setting.close() + return default + end + local setting = fs.open(file, "r") + local config = setting.readAll() + setting.close() + config = textutils.unserialise(config) + if type(config) ~= "table" then + return default + end + local checkForKeys + checkForKeys = function(default, test) + for key, value in pairs(default) do + if not test[key] then + test[key] = value + elseif type(test[key]) == "table" then + checkForKeys(value, test[key]) + end + end + end + checkForKeys(default, config) + return config + end + + funcs.getPersistence = function(name) assert(type(name) ~= "nil", "Invalid argument #1 (expected anything but nil, got "..type(name)..")") - if fs.exists("cfg/persistence.lson") then - local fper = fs.open("cfg/persistence.lson", "r") + if fs.exists(path.."cfg/persistence.lson") then + local fper = fs.open(path.."cfg/persistence.lson", "r") local tpersist = textutils.unserialize(fper.readAll()) fper.close() if not tpersist[real_name] then @@ -293,7 +318,7 @@ allium.register = function(p_name, version, fullname) end if type(name) == "string" then tpersist[real_name][name] = data - local fpers = fs.open("cfg/persistence.lson", "w") + local fpers = fs.open(path.."cfg/persistence.lson", "w") if not fpers then return false end @@ -314,28 +339,26 @@ allium.register = function(p_name, version, fullname) funcs.import = function(p_name) -- request the API from a specific plugin assert(type(p_name) == "string", "Invalid argument #1 (string expected, got "..type(p_name)..")") p_name = allium.sanitize(p_name) + assert(p_name == real_name, real_name.." attempted to load self. What made you think you could do this?") local timer = os.startTimer(config.import_timeout or 5) - repeat - local e = {os.pullEvent()} - until (e[1] == "timer" and e[2] == timer) or (plugins[p_name] and plugins[p_name].module) - if not plugins[p_name] and plugins[p_name].module then + parallel.waitForAny(function() + repeat + local e = {os.pullEvent()} + until (e[1] == "timer" and e[2] == timer) or (plugins[p_name] and plugins[p_name].module) + end, function() + repeat + sleep() + until plugins[p_name].module + end) + if not plugins[p_name].module then return false end - for being_loaded, loaded_plugins in pairs(loaded) do -- Plugin being loaded, plugins that the plugin being loaded has loaded - if being_loaded == p_name then - for i = 1, #loaded_plugins do - if loaded_plugins[i] == real_name then - return false - end - end - break + for i = 1, #plugins[p_name].loaded do + if plugins[p_name].loaded[i] == real_name then + error("Cannot import "..p_name.."Circular dependencies with "..real_name.." and "..plugins[p_name].loaded[i]) end end - if loaded[real_name] then - loaded[real_name][#loaded[real_name]+1] = p_name - else - loaded[real_name] = {p_name} - end + loaded[#loaded+1] = p_name return deep_copy(plugins[p_name].module) end @@ -343,6 +366,7 @@ allium.register = function(p_name, version, fullname) end allium.verify = function(param) -- Verification code ripped from DepMan instance + assert(type(param) == "string", "Invalid argument #1 (string expected, got "..type(param)..")") local function convert(str) -- Use the semver API to convert. Provide a detailed error if conversion fails if type(str) ~= "string" then error("Could not convert "..tostring(str)) @@ -404,7 +428,7 @@ for _, side in pairs(peripheral.getNames()) do -- Finding the chat module if allium.side then break end end if not allium.side then - print(cli.warn, "Allium could not find chat module") + allium.warn("Allium could not find chat module") end -- Packaging the Allium API @@ -413,24 +437,24 @@ if not package.preload["allium"] then return allium end else - print(cli.error, "Another instance of Allium is already running") + print(cli.error, false, "Another instance of Allium is already running") return end do -- Plugin loading process - print(cli.info, "Loading plugins") + allium.log("Loading plugins") local loader_group = raisin.group(1) local function scopeDown(dir) for _, plugin in pairs(fs.list(dir)) do if (not fs.isDir(dir.."/"..plugin)) and plugin:find(".lua") then local file, err = loadfile(dir.."/"..plugin, _ENV) if not file then - print(cli.error, err) + print(cli.error, false, err) else local thread = function() local suc, err = pcall(file) if not suc then - print(cli.error, err) + print(cli.error, false, err) end end raisin.thread(thread, 0, loader_group) @@ -440,9 +464,8 @@ do -- Plugin loading process end end end - local dir = shell.dir() - if fs.exists(dir.."/plugins") then - scopeDown(dir.."/plugins") + if fs.exists(fs.combine(path, "/plugins")) then + scopeDown(fs.combine(path, "/plugins")) end raisin.manager.runGroup(loader_group) end @@ -470,7 +493,7 @@ local interpreter = function() -- Main command interpretation thread while true do local _, message, _, name, uuid = os.pullEvent("chat_capture") -- Pull chat messages if message:find("!") == 1 then -- Are they for allium? - args = {} + local args = {} for k in message:gmatch("%S+") do -- Put all arguments spaced out into a table args[#args+1] = k end @@ -549,20 +572,20 @@ local interpreter = function() -- Main command interpretation thread if stat == false then -- It crashed... allium.tell(name, { "&4!"..cmd_exec.command.." crashed! This is likely not your fault, but the developer's. Please contact the developer of &a"..cmd_exec.plugin.."&4. Error:", - "&c&h[[Click here to place error into chat prompt, so you may copy it if needed for an issue report]]&s[["..err.."]]"..err.."&r" + "&c&h(Click here to place error into chat prompt, so you may copy it if needed for an issue report)&s("..err..")"..err.."&r" }) - print(cli.warn, cmd.." | "..err) + allium.warn(cmd.." | "..err) end end raisin.thread(exec_command, 0, group.command) else -- This isn't even a valid command... - allium.tell(name, "&6Invalid Command, use &c&g[[!allium:help]]!help&r&6 for assistance.") --bleh! + allium.tell(name, "&6Invalid Command, use &c&g(!allium:help)!help&r&6 for assistance.") --bleh! end end end end -local scanner = function() -- Login/out scanner thread +local player_scanner = function() -- Login/out scanner thread local online = {} while true do local cur_players = allium.getPlayers() @@ -584,21 +607,109 @@ local scanner = function() -- Login/out scanner thread end end else - print(cli.warn, "Could not list online players, skipping tick.") + allium.warn("Could not list online players, skipping tick.") end end end +local update_interaction = function() -- Update UI scanning and handling thread + local common = { + run = {} + } + common.refresh = function() + local done = term.redirect(main) + local x, y = term.getSize() + common.bY = y-1 + if #common.run > 0 then + common.bX = x-6 + term.setCursorPos(x-6, y-1) + term.blit("TRS \24", "888f8", "14efb") + else + common.bX = x-5 + term.setCursorPos(x-5, y-1) + term.blit("TRS", "888", "14e") + end + term.setBackgroundColor(colors.black) -- Reset terminal and cursor + term.setTextColor(colors.white) + term.redirect(done) + end + parallel.waitForAll(function() -- Update checker on initialize + if config.updates.notify.dependencies then + local suc, deps = up.check.dependencies() + local suffixer + if type(deps) == "table" and #deps > 0 then + if #deps == 1 then + suffixer = {"Utility ", " is "} + else + suffixer = {"Utilities: ", " are "} + end + allium.log(suffixer[1]..table.concat(deps, ", ")..suffixer[2].."ready to be updated") + common.run[#common.run+1] = {up.run.dependencies} + elseif not suc then + print(cli.error, true, "Error in checking for dependency updates: "..deps) + end + end + if config.updates.notify.allium then + local sha = up.check.allium() + if sha ~= config.sha then + allium.log("Allium is ready to be updated") + common.run[#common.run+1] = {up.run.allium, sha} + elseif not sha then + allium.warn("Failed to scan for allium updates") + end + end + if config.updates.notify.plugins then + -- Things will also be here + end + common.refresh() + end, function() -- User Interface + common.refresh() + while true do + local e = {os.pullEvent("mouse_click")} + table.remove(e, 1) + if table.remove(e, 1) == 1 then + local x = table.remove(e, 1) + if table.remove(e, 1) == common.bY then + if x-common.bX == 0 then -- Terminate + allium.log("Exiting Allium...") + raisin.manager.halt() + elseif x-common.bX == 1 then -- Reboot + allium.log("Rebooting...") + sleep(1) + os.reboot() + elseif x-common.bX == 2 then -- Shutdown + allium.log("Shutting down...") + sleep(1) + os.shutdown() + elseif x-common.bX == 4 and #common.run > 0 then -- Update + allium.log("Downloading updates...") + for i = 1, #common.run do + local s, err = pcall(table.unpack(common.run[i])) + if not s then + print(cli.error, true, "Failed to execute an update: "..err) + end + end + allium.log("Rebooting to apply updates...") + sleep(1) + os.reboot() + end + end + end + end + end) +end + raisin.thread(interpreter, 0) -raisin.thread(scanner, 1) +raisin.thread(player_scanner, 1) +raisin.thread(update_interaction, 1) -if not fs.exists("cfg/persistence.lson") then --In the situation that this is a first installation, let's do some setup - local fpers = fs.open("cfg/persistence.lson", "w") +if not fs.exists(path.."cfg/persistence.lson") then --In the situation that this is a first installation, let's do some setup + local fpers = fs.open(path.."cfg/persistence.lson", "w") fpers.write("{}") fpers.close() end -print(cli.info, "Allium started.") +allium.log("Allium started.") allium.tell("@a", "&eHello World!") raisin.manager.run() diff --git a/plugins/allium-stem.lua b/plugins/allium-stem.lua index ad5b8e9..97b748b 100644 --- a/plugins/allium-stem.lua +++ b/plugins/allium-stem.lua @@ -1,141 +1,154 @@ local allium = require("allium") -local stem = allium.register("allium", "0.4.1", "Allium Stem") -local addDetails -do -- Just a block for organization of command parsing stuffs - local function infill(variant, execute) - local out = {} - if variant == "username" then - local players = allium.getPlayers() - for i = 1, #players do - out[#out+1] = " &6-&r &g[["..execute..players[i].." ]]&h[[Click to add user "..players[i].."]]&a"..players[i] - end - elseif variant == "plugin" then - local list = allium.getInfo() - for plugin in pairs(list) do - out[#out+1] = " &6-&r &g[["..execute..plugin.." ]]&h[[Click to add plugin "..plugin.."]]&a"..plugin - end - elseif variant == "command" then - local list = allium.getInfo() - for plugin, v in pairs(list) do - for command in pairs(v) do - local rawcmd = command - local command = plugin..":"..command - out[#out+1] = " &6-&r &g[["..execute..command.." ]]&h[[Click to add command !"..rawcmd.."]]&a!"..command +local stem = allium.register("allium", "0.6.0", "Allium Stem") + +local help = function(name, args, data) + local cmds_per, page = 8, 1 -- Turn this into a per-user persistence preference + local info = {} + local out_str = "" + local addDetails + local next_command = "!allium:help " + + do -- Just a block for organization of command parsing stuffs + local function infill(variant, execute) + local out = {} + if type(variant) == "string" then + if variant == "username" then + local players = allium.getPlayers() + for i = 1, #players do + out[#out+1] = " &6-&r &g("..execute..players[i].." )&h(Click to add user "..players[i]..")&a"..players[i] + end + elseif variant == "plugin" then + local list = allium.getInfo() + for plugin in pairs(list) do + out[#out+1] = " &6-&r &g("..execute..plugin.." )&h(Click to add plugin "..plugin..")&a"..plugin + end + elseif variant == "command" then + local list = allium.getInfo() + for plugin, v in pairs(list) do + for command in pairs(v) do + local rawcmd = command + local command = plugin..":"..command + out[#out+1] = " &6-&r &g("..execute..command.." )&h(Click to add command !"..rawcmd..")&a!"..command + end + end + elseif variant:sub(1, -2) == "position_" then + local position = allium.getPosition(name).position + if variant:sub(-1, -1) == "x" then + out[#out+1] = " &6-&r &g("..execute..position[1].." )&h(Click to add your x position)&a"..position[1] + elseif variant:sub(-1, -1) == "y" then + out[#out+1] = " &6-&r &g("..execute..position[2].." )&h(Click to add your y position)&a"..position[2] + elseif variant:sub(-1, -1) == "z" then + out[#out+1] = " &6-&r &g("..execute..position[3].." )&h(Click to add your z position)&a"..position[3] + end + end + elseif type(variant) == "function" or type(variant) == "table" then + local result = {} + if type(variant) == "function" then + result = variant() + else + result = variant + end + for i = 1, #result do + if type(result[i]) == "string" and not result[i]:find("=") then + out[#out+1] = " &6-&r &g("..execute..result[i].." )&h(Click to add "..result[i]..")&a"..result[i] + end end end - elseif type(variant) == "function" or type(variant) == "table" then - local result = {} - if type(variant) == "function" then - result = variant() + return out + end + + local function parse(label, data, execute) + local prefix, postfix, execution, hover, information = "&6 - &r<&a", "&r>:", "", "" + if type(data[1]) == "string" then + information = data[1] else - result = variant + information = "&oNo information found" + end + if data.optional == true then + prefix, postfix = "&6 - &r[&a", "&r]:" end - for i = 1, #result do - if type(result[i]) == "string" and not result[i]:find("=") then - out[#out+1] = " &6-&r &g[["..execute..result[i].." ]]&h[[Click to add "..result[i].."]]&a"..result[i] + if data.clickable == true or data.clickable == nil then + hover, execution = "Click to add this parameter", execute..label + if type(data.infill) == "string" then + execution = execution.."=" end end - end - return out - end - - local function parse(label, data, execute) - local prefix, postfix, execution, hover, information = "&6 - &r<&a", "&r>:", "", "" - if type(data[1]) == "string" then - information = data[1] - else - information = "&oNo information found" - end - if data.optional == true then - prefix, postfix = "&6 - &r[&a", "&r]:" - end - if data.clickable == true or data.clickable == nil then - hover, execution = "Click to add this parameter", execute..label - if type(data.infill) == "string" then - execution = execution.."=" + if data.default and tostring(data.default) then -- Overrides infill. default and infill shouldn't even be used in the same place anyways. + execution = execute..label.."=\"\""..data.default.."\"\"" -- Quoting a quote so it gets placed in chat properly end + local meta = "" + if #execution ~= 0 then + meta = meta.."&g("..execution..")" + end + if #hover ~= 0 then + meta = meta.."&h("..hover..")" + end + return prefix..meta..label..postfix.." "..information end - if data.default and tostring(data.default) then -- Overrides infill. default and infill shouldn't even be used in the same place anyways. - execution = execute..label.."=\"\""..data.default.."\"\"" -- Quoting a quote so it gets placed in chat properly - end - local meta = "" - if #execution ~= 0 then - meta = meta.."&g[["..execution.."]]" - end - if #hover ~= 0 then - meta = meta.."&h[["..hover.."]]" - end - return prefix..meta..label..postfix.." "..information - end - addDetails = function(info, args, execute, command) - if not args[1] then - -- We're at the end of parsing and should render possible fields - local out = {} - if info then - for k, v in pairs(info) do - if type(v) == "table" then - out[#out+1] = parse(k, v, execute) + addDetails = function(info, args, execute, command) + if not args[1] then --or info then + -- We're at the end of parsing and should render possible fields + local out = {} + if info then + for k, v in pairs(info) do + if type(v) == "table" then + out[#out+1] = parse(k, v, execute) + end end end + if #out == 0 or not info then + out = {" &6-&a No more parameters to add!", " &6-&a Click &r&c&h(Click to run command)&g("..command..")here&r&a to run the command", " &6- &eOR&a click on the first line to add the command to the chat input."} + end + return out, command + else -- Otherwise things are going totally as planned and we should just recurse onwards + local param_data = {} + local is_tag = args[1]:find("=") + if is_tag then + param_data.param = args[1]:sub(1, is_tag-1) + param_data.tag = args[1]:sub(is_tag+1, -1) + table.remove(args, 1) + else + param_data.param = table.remove(args, 1) end - if #out == 0 or not info then - out = {" &6-&a No more parameters to add!", " &6-&a Click &r&c&h[[Add command to chat input]]&s[["..command.."]]here&r&a, or the red command to add it to your chat input."} - end - return out, command - else -- Otherwise things are going totally as planned and we should just recurse onwards - local param_data = {} - local is_tag = args[1]:find("=") - if is_tag then - param_data.param = args[1]:sub(1, is_tag-1) - param_data.tag = args[1]:sub(is_tag+1, -1) - table.remove(args, 1) - else - param_data.param = table.remove(args, 1) - end - if is_tag and #param_data.tag == 0 then - -- If the parameter is an infill thing, and doesn't have a value attached to it: - if not (info[param_data.param] and info[param_data.param].infill) then - return "Missing infill information" + if is_tag and #param_data.tag == 0 then + -- If the parameter is an infill thing, and doesn't have a value attached to it: + if not (info[param_data.param] and info[param_data.param].infill) then + return "Missing infill information" + end + return infill(info[param_data.param].infill, execute..param_data.param.."="), command + elseif param_data.tag then + execute = execute..param_data.param.."="..param_data.tag.." " + command = command..param_data.tag.." " + else + execute = execute..param_data.param.." " + command = command..param_data.param.." " end - return infill(info[param_data.param].infill, execute..param_data.param.."="), command - elseif param_data.tag then - execute = execute..param_data.param.."="..param_data.tag.." " - command = command..param_data.tag.." " - else - execute = execute..param_data.param.." " - command = command..param_data.param.." " + return addDetails(info[param_data.param], args, execute, command) end - return addDetails(info[param_data.param], args, execute, command) end end -end - -local help = function(name, args, data) - local cmds_per, page = 7, 1 -- Turn this into a per-user persistence preference - local info = {} - local out_str = "" - local next_command = "!allium:help " local function run() for i = (cmds_per*(page-1))+1, (cmds_per*page) do if info[i] then - out_str = out_str..info[i].."\\n" + out_str = out_str..info[i].."\n" end end - if out_str == "" or page <= 0 then + if #out_str == 0 or page <= 0 then data.error("Page does not exist.") return end - out_str = "&2===================&r &dAll&5i&r&dum&e Help Menu&r &2===================&r\\n"..out_str + out_str = "&2===================&r &dAll&5i&r&dum&e Help Menu&r &2===================&r\n"..out_str local template = #(" << "..page.."/"..math.ceil(#info/cmds_per).." >> ") local sides = 32-template - out_str = out_str.."&2"..string.rep("=", sides).."&r &6&l&h[[Previous Page]]&g[["..next_command..(page-1).."]]<<&r&c&l "..page.."/"..math.ceil(#info/cmds_per).." &r&6&l&h[[Next Page]]&g[["..next_command..(page+1).."]]>>&r &2"..string.rep("=", sides).."&r" + out_str = out_str.."&2"..string.rep("=", sides).."&r &6&l&h(Previous Page)&g("..next_command..(page-1)..")<<&r&c&l "..page.."/"..math.ceil(#info/cmds_per).." &r&6&l&h(Next Page)&g("..next_command..(page+1)..")>>&r &2"..string.rep("=", sides).."&r" allium.tell(name, out_str, true) end - if tonumber(args[#args]) then - page = tonumber(args[#args]) + local pagenum = tonumber(args[#args]) + if pagenum and pagenum == math.ceil(pagenum) then + page = pagenum args[#args] = nil end @@ -148,7 +161,7 @@ local help = function(name, args, data) else entry.usage = "" end - info[#info+1] = "&c&g[[!allium:help "..p_name..":"..cmd_name.."]]&h[[Click to begin autofill]]!"..p_name..":"..cmd_name.."&r: "..entry.info[1] + info[#info+1] = "&c&g(!allium:help "..p_name..":"..cmd_name..")&h(Click to begin autofill)!"..p_name..":"..cmd_name.."&r: "..entry.info[1] end end return run() @@ -169,7 +182,7 @@ local help = function(name, args, data) data.error(cmd..": "..infill_info) return end - info[#info+1] = "&c&s[["..infill_text.."]]&h[[Click to add to chat input]]"..infill_text + info[#info+1] = "&c&s("..infill_text..")&h(Click to add to chat input)"..infill_text.."&r" for i = 1, #infill_info do info[#info+1] = infill_info[i] end @@ -191,7 +204,7 @@ local help = function(name, args, data) else entry.usage = "" end - info[#info+1] = "&c&g[[!allium:help "..args[1]..":"..cmd_name.."]]&h[[Click to begin autofill]]!"..cmd_name.."&r: "..entry.info[1] + info[#info+1] = "&c&g(!allium:help "..args[1]..":"..cmd_name..")&h(Click to begin autofill)!"..cmd_name.."&r: "..entry.info[1] end return run() else @@ -204,12 +217,12 @@ end local credits = function(name) allium.tell(name, { - "&dAll&5i&dum &av"..tostring(allium.version).."&r was cultivated with love by &a&h[[Check out his repo!]]&i[[https://github.com/hugeblank]]hugeblank&r.", - "Documentation on Allium can be found here: &9&h[[Read up on Allium!]]&ihttps://github.com/hugeblank/allium-wiki&r.", - "Contribute and report issues to Allium here: &9&h[[Check out where Allium is grown!]]&ihttps://github.com/hugeblank/allium&r.", + "&dAll&5i&dum &av"..tostring(allium.version).."&r was cultivated with love by &a&h(Check out his profile!)&i(https://github.com/hugeblank)hugeblank&r.", + "Documentation on Allium can be found here: &9&h(Read up on Allium!)&ihttps://github.com/hugeblank/allium-wiki&r.", + "Contribute and report issues to Allium here: &9&h(Check out where Allium is grown!)&ihttps://github.com/hugeblank/allium&r.", "&6Other Contributors:", - "&a - &rCommand formatting API by &1&h[[Check out his profile!]]&i[[https://github.com/roger109z]]roger109z&r.", - "&a - &rJSON parsing library by &d&h[[Check out their profile!]]&i[[https://github.com/rxi]]rxi&r." + "&a - &rCommand formatting API by &1&h(Check out his profile!)&i(https://github.com/roger109z)roger109z&r.", + "&a - &rJSON parsing library by &d&h(Check out their profile!)&i(https://github.com/rxi)rxi&r." }, true) end @@ -218,9 +231,9 @@ local plugins = function(name) local str = "" local plugins = allium.getInfo() for p_name in pairs(plugins) do - local p_str = "&h[["..p_name.." v"..tostring(allium.getVersion(p_name)).."]]" + local p_str = "&h("..p_name.." v"..tostring(allium.getVersion(p_name))..")" if plugins[p_name]["credits"] then - p_str = p_str.."&g[[!"..p_name..":credits]]" + p_str = p_str.."&g(!"..p_name..":credits)" end pluginlist[#pluginlist+1] = p_str..allium.getName(p_name) end diff --git a/startup.lua b/startup.lua index a5c371c..f212986 100644 --- a/startup.lua +++ b/startup.lua @@ -1,100 +1,212 @@ -shell.openTab("shell") - -- Allium version -local allium_version = "0.8.1" +-- x.x.x-pr = unstable, potential breaking changes +local allium_version = "0.9.0" + +local path = "/" +local firstrun = false +for str in string.gmatch(shell.getRunningProgram(), ".+[/]") do + path = path..str +end if not commands then -- Attempt to prevent user from running this on non-command comps printError("Allium must be run on a command computer") return end -local config = {} +--[[ + DEFAULT ALLIUM CONFIGS ### DO NOT CHANGE THESE ### + Configurations can be changed in /cfg/allium.lson +]] +local default = { + label = "<&r&dAll&5&h(Kilroy wuz here.)&i(https://www.youtube.com/watch?v=XqZsoesa55w\\&t=15s)i&r&dum&r> ", -- The label the loader uses + import_timeout = 5, -- The maximum amount of time it takes to wait for a plugin dependency to provide its module. + updates = { -- Various update configurations. + notify = { -- Configurations to trigger notifications when parts of Allium are ready for an update + dependencies = true, -- Notify when dependencies need updating + plugins = true, -- Notify when plugins need updating + allium = true -- Notify when allium needs updating + }, + repo = { -- Repo specific information for Allium in case you want to use a fork + user = "hugeblank", -- User to check updates from + branch = "master", -- Branch/Tag to check updates from + name = "Allium" -- Name of repo to check updates from + } + } +} -do -- Configuration parsing - local file, default = fs.open("cfg/allium.lson", "r"), {updates = {deps = true, allium = true}} - local function verify_cfg(input, default, index) - for f_k, f_v in pairs(input) do -- input key, value - for t_k, t_v in pairs(default) do -- standard key, value - if type(f_v) == "table" and type(t_v) == "table" then - if not verify_cfg(f_v, t_v, f_k..".") then - return false - end - elseif f_k == t_k and type(f_v) ~= type(t_v) then - printError("Invalid config option "..(index or "")..f_k.." (expected "..type(t_v)..", got "..type(f_v)..")") - return false - end - end - end - return true - end - local function fill_missing(file, default) - local out = {} - if not file then file = {} end - for k, v in pairs(default) do - if type(v) == "table" then - out[k] = fill_missing(file[k], v) - else - if type(file[k]) == "nil" then - out[k] = v - else - out[k] = file[k] - end - end - end - for k, v in pairs(file) do - if out[k] == nil then - out[k] = v +--load settings from file +local loadSettings = function(file, default) + assert(type(file) == "string", "file must be a string") + if not fs.exists(file) then + firstrun = true + local setting = fs.open(file,"w") + setting.write(textutils.serialise(default)) + setting.close() + return default + end + local setting = fs.open(file, "r") + local config = setting.readAll() + setting.close() + config = textutils.unserialise(config) + if type(config) ~= "table" then + return default + end + local checkForKeys + checkForKeys = function(default, test) + for key, value in pairs(default) do + if type(test[key]) ~= type(value) then + test[key] = value + elseif type(test[key]) == "table" then + checkForKeys(value, test[key]) end end - return out end - local output = {} - if file then -- Could not read file - output = textutils.unserialise(file.readAll()) or {} - file.close() - end - if verify_cfg(output, default) then -- Make sure none of the config opts are invalid (skips missing ones) - config = fill_missing(output, default) -- Fill in the remaining options that are missing - if config.version ~= allium_version then - config.version = allium_version - file = fs.open("cfg/allium.lson", "w") - if file then - file.write(textutils.serialise(config)) - file.close() + checkForKeys(default, config) + return config +end + +local config, up = loadSettings(fs.combine(path, "cfg/allium.lson"), default), {} +local depman +up.check = {} +up.run = {} + +if config.updates.notify.dependencies then + local depget = http.get("https://raw.githubusercontent.com/hugeblank/allium-depman/master/instance.lua") + if depget then + local contents = depget.readAll() + depget.close() + local depargs = { -- Depman args minus the task which can be inserted into the first index + path, + "https://raw.githubusercontent.com/hugeblank/allium-depman/master/listing.lson", + path.."/cfg/dependencies.lson", + path.."/lib", + allium_version + } + depman = function(task) + local out = {} + local temp = _G.print -- The good ol' switcheroo + _G.print = function(...) out = {...} end + local result = pcall(load(contents, "Depman", nil, _ENV), task, table.unpack(depargs)) + _G.print = temp + return result, table.unpack(out) + end + up.check.dependencies = function() + local suc, out = depman("scan") + if suc then + return suc, textutils.unserialise(out) + else + return suc, out end end - else - return - end + up.run.dependencies = function() + return depman("upgrade") + end + end end --- Checking Allium/Plugin updates -if config.updates.allium then - if fs.exists("cfg/repolist.csh") then -- Checking for a repolist shell executable - -- Update all plugins and programs on the repolist - for line in io.lines("cfg/repolist.csh") do - shell.run(line) + +-- First run installation of utilities +if firstrun then + print("Welcome to Allium! Doing some first-run setup and then we'll be on our way.") + fs.delete(fs.combine(path, "/lib")) + if depman then + depman("upgrade") + end +end +local github, json = require("lib.nap")("https://api.github.com"), require("lib.json") + +if config.updates.notify.allium then + up.check.allium = function() + local repo = config.updates.repo + local jsonresponse = github.repos[repo.user][repo.name].commits[repo.branch]({ + method = "GET" + }) + if jsonresponse then + local out = jsonresponse.readAll() + jsonresponse.close() + return json.decode(out).sha + else + return false, "No response from github" + end + end + up.run.allium = function(sha) + local repo = config.updates.repo + local null = function() end + os.run({ + term = { + write=null, + setCursorPos=null, + getCursorPos=function() return 1, 1 end + }, + print = null, + write = null, + shell = { + getRunningProgram = function() return path.."/lib/gget.lua" end + } + }, + fs.combine(path, "/lib/gget.lua"), + repo.user, + repo.name, + repo.branch + ) + local file = fs.open(fs.combine(path, "/cfg/version.lson"), "w") + if file then + file.write(textutils.serialise({sha = sha})) + -- Not adding version because we're outdated now. We've been replaced. + file.close() + else + printError("Could not write to file. Is the disk full?") + return end end end --- Filling Dependencies -if config.updates.deps then - -- Allium DepMan Instance: https://pastebin.com/nRgBd3b6 - print("Updating Dependencies...") - local didrun = false - parallel.waitForAll(function() - didrun = shell.run("pastebin run nRgBd3b6 upgrade https://pastebin.com/raw/fisfxn76 /cfg/deps.lson /lib "..allium_version) - end, - function() - multishell.setTitle(multishell.getCurrent(), "depman") - end) - if not didrun then - printError("Could not update dependencies") +if config.updates.notify.plugins then + -- Things will be here +end + +-- Final firstrun stuff +if firstrun then + print("Finalizing installation") + local sha, file = config.updates.check.allium(), fs.open(fs.combine(path, "/cfg/version.lson"), "w") + if file then + file.write(textutils.serialise({sha = sha, version = allium_version})) + file.close() + else + printError("Could not write to file. Is the disk full?") return end end +local r_file = fs.open(fs.combine(path, "/cfg/version.lson"), "r") +if r_file then + local v_data = textutils.unserialise(r_file.readAll()) + r_file.close() + if v_data then + config.version, config.sha = v_data.version, v_data.sha + else + printError("Could not parse version data, did you mess with ./cfg/version.lson?") + print("If you're updating from a prior version, delete allium.config, reboot, and you should be good.") + return + end + if not config.version then + local w_file = fs.open(fs.combine(path, "/cfg/version.lson"), "w") + if w_file then -- Reapply version because it was removed by the last version + v_data.version = allium_version + config.version = allium_version + w_file.write(textutils.serialise(v_data)) + w_file.close() + else + printError("Could not write to file. Is the disk full?") + return + end + end +else + printError("Could not read version data, did you delete ./cfg/version.lson?") + print("If you're updating from a prior version, delete allium.config, reboot, and you should be good.") + return +end + -- Clearing the screen term.setBackgroundColor(colors.black) term.setTextColor(colors.white) @@ -102,21 +214,20 @@ term.clear() term.setCursorPos(1, 1) -- Running Allium -shell.run("allium.lua") +multishell.setTitle(multishell.getCurrent(), "Allium") +local s, e = pcall(os.run, _ENV, path.."allium.lua", config, up) +if not s then + printError(e) +end -- Removing all captures for _, side in pairs(peripheral.getNames()) do -- Finding the chat module if peripheral.getMethods(side) then for _, method in pairs(peripheral.getMethods(side)) do if method == "uncapture" then - peripheral.call(side, "uncapture", ".") + peripheral.call(side, "uncapture") break end end end -end - --- Rebooting or exiting -print("Rebooting in 5 seconds") -print("Press any key to cancel") -parallel.waitForAny(function() repeat until os.pullEvent("char") end, function() sleep(5) os.reboot() end) \ No newline at end of file +end \ No newline at end of file