diff options
Diffstat (limited to '.mbs/bin')
| -rw-r--r-- | .mbs/bin/clear.lua | 4 | ||||
| -rw-r--r-- | .mbs/bin/help.lua | 19 | ||||
| -rw-r--r-- | .mbs/bin/lua.lua | 416 | ||||
| -rw-r--r-- | .mbs/bin/mbs.lua | 260 | ||||
| -rw-r--r-- | .mbs/bin/shell.lua | 702 |
5 files changed, 1401 insertions, 0 deletions
diff --git a/.mbs/bin/clear.lua b/.mbs/bin/clear.lua new file mode 100644 index 0000000..3a8e8d7 --- /dev/null +++ b/.mbs/bin/clear.lua @@ -0,0 +1,4 @@ +local _, y = term.getCursorPos() + +term.scroll(y - 1) +term.setCursorPos(1, 1)
\ No newline at end of file diff --git a/.mbs/bin/help.lua b/.mbs/bin/help.lua new file mode 100644 index 0000000..f92630f --- /dev/null +++ b/.mbs/bin/help.lua @@ -0,0 +1,19 @@ +local topic = ... or "intro" + +if topic == "index" then + print("Help topics availiable:") + textutils.pagedTabulate(help.topics()) +else + local file_name = help.lookup(topic) + if not file_name then error("No help available", 0) end + + local file = fs.open(file_name, "r") + -- Shouldn't happen, but nice to handle anyway + if not file then error("No help available", 0) end + + local contents = file.readAll() + file.close() + + local _, height = term.getCursorPos() + textutils.pagedPrint(contents, height - 3) +end
\ No newline at end of file diff --git a/.mbs/bin/lua.lua b/.mbs/bin/lua.lua new file mode 100644 index 0000000..eed7aa9 --- /dev/null +++ b/.mbs/bin/lua.lua @@ -0,0 +1,416 @@ +if select('#', ...) > 0 then + print("This is an interactive Lua prompt.") + print("To run a lua program, just type its name.") + return +end + +local input_colour, output_colour, text_colour, keyword_colour, comment_colour, string_colour = + colours.green, colours.cyan, term.getTextColour(), colours.yellow, colours.grey, colours.red +local number_colour, extra_colour, object_colour = + colours.magenta, colours.grey, colours.lightGrey + +local keywords = { + ["and"] = keyword_colour, ["break"] = keyword_colour, ["do"] = keyword_colour, + ["else"] = keyword_colour, ["elseif"] = keyword_colour, ["end"] = keyword_colour, + ["false"] = object_colour, ["for"] = keyword_colour, ["function"] = keyword_colour, + ["if"] = keyword_colour, ["in"] = keyword_colour, ["local"] = keyword_colour, + ["nil"] = object_colour, ["not"] = keyword_colour, ["or"] = keyword_colour, + ["repeat"] = keyword_colour, ["return"] = keyword_colour, ["then"] = keyword_colour, + ["true"] = object_colour, ["until"] = keyword_colour, ["while"] = keyword_colour, +} + +local tokens = { + { "^%s+", text_colour }, + + -- Identifiers and keywords + { "^[%a_][%w_]*", function(match) return keywords[match] or text_colour end }, + + -- TODO: Exponents + hex, partial strings and comments + + { "^%-%-%[%[.-%]%]", comment_colour }, + { "^%-%-.*", comment_colour }, + + { [[^".-[^\]"]], string_colour }, -- Complete strings + { [[^"[^"]*"?]], string_colour }, -- Incomplete strings + { [[^'.-[^\]']], string_colour }, -- Complete strings + { [[^'[^"]*'?]], string_colour }, -- Incomplete strings + { "^%[%[.-%]%]", string_colour }, + + { "^0x[a-fA-F0-9]*", number_colour }, -- Hexadecimal + + { "^%d+%.%d*e[-+]?%d*", number_colour }, -- 23.4e+2 + { "^%d+%.%d*", number_colour }, -- 23.2 + { "^%d+e[-+]?%d*", number_colour }, -- 23e+2 + { "^%d+", number_colour }, -- 23 + + { "^%.%d*e[-+]?%d*", number_colour }, -- .23e+2 + { "^%.%d*", number_colour }, -- .23 + + { "^[^%w_]", text_colour }, -- Consume some unknown input +} + +--- A basic highlighting function: +local function highlight(line, start) + local find, type = string.find, type + for i = 1, #tokens do + local token = tokens[i] + local pat_start, pat_finish = find(line, token[1], start) + if pat_finish then + if type(token[2]) == "function" then + return pat_finish, token[2](line:sub(pat_start, pat_finish)) + else + return pat_finish, token[2] + end + end + end + + return #line, text_colour +end + +local function write_with(colour, text) + term.setTextColour(colour) + write(text) +end + +local function pretty_sort(a, b) + local ta, tb = type(a), type(b) + + if ta == "string" then return tb ~= "string" or a < b + elseif tb == "string" then return false + end + + if ta == "number" then return tb ~= "number" or a < b end + return false +end + +local debug_info = type(debug) == "table" and type(debug.getinfo) == "function" and debug.getinfo +local debug_local = type(debug) == "table" and type(debug.getlocal) == "function" and debug.getlocal +local function pretty_function(fn) + local info = debug_info and debug_info(fn, "Su") + + -- Include function source position if available + local name + if info and info.short_src and info.linedefined and info.linedefined >= 1 then + name = "function<" .. info.short_src .. ":" .. info.linedefined .. ">" + else + name = tostring(fn) + end + + -- Include arguments if a Lua function and if available. Lua will report "C" + -- functions as variadic. + if info and info.what == "Lua" and info.nparams and debug_local then + local args = {} + for i = 1, info.nparams do args[i] = debug_local(fn, i) or "?" end + if info.isvararg then args[#args + 1] = "..." end + name = name .. "(" .. table.concat(args, ", ") .. ")" + end + + return name +end + +local function pretty_size(obj, tracking, limit) + local obj_type = type(obj) + if obj_type == "string" then return #string.format("%q", obj):gsub("\\\n", "\\n") + elseif obj_type == "function" then return #pretty_function(obj) + elseif obj_type ~= "table" or tracking[obj] then return #tostring(obj) end + + local count = 2 + tracking[obj] = true + for k, v in pairs(obj) do + count = count + pretty_size(k, tracking, limit) + pretty_size(v, tracking, limit) + if count >= limit then break end + end + tracking[obj] = nil + return count +end + +local function pretty_impl(obj, tracking, width, height, indent, tuple_length) + local obj_type = type(obj) + if obj_type == "string" then + local formatted = string.format("%q", obj):gsub("\\\n", "\\n") + + -- Strings are limited to the size of the current buffer with a bit of padding + local limit = math.max(8, math.floor(width * height * 0.8)) + if #formatted > limit then + write_with(string_colour, formatted:sub(1, limit - 3)) + write_with(extra_colour, "...") + else + write_with(string_colour, formatted) + end + return + elseif obj_type == "number" then + return write_with(number_colour, tostring(obj)) + elseif obj_type == "function" then + return write_with(object_colour, pretty_function(obj)) + elseif obj_type ~= "table" or tracking[obj] then + return write_with(object_colour, tostring(obj)) + elseif (getmetatable(obj) or {}).__tostring then + return write_with(text_colour, tostring(obj)) + end + + local open, close = "{", "}" + if tuple_length then open, close = "(", ")" end + + if (tuple_length == nil or tuple_length == 0) and next(obj) == nil then + return write_with(text_colour, open .. close) + elseif width <= 7 then + write_with(text_colour, open) write_with(extra_colour, " ... ") write_with(text_colour, close) + return + end + + local should_newline = false + local length = tuple_length or #obj + + -- Compute the "size" of this object and how many children it has. + local size, children, keys, kn = 2, 0, {}, 0 + for k, v in pairs(obj) do + if type(k) == "number" and k >= 1 and k <= length and k % 1 == 0 then + local vs = pretty_size(v, tracking, width) + size = size + vs + 2 + children = children + 1 + else + kn = kn + 1 + keys[kn] = k + + local vs, ks = pretty_size(v, tracking, width), pretty_size(k, tracking, width) + size = size + vs + ks + 2 + children = children + 2 + end + + -- Some aribtrary scale factor to stop long lines filling too much of the + -- screen + if size >= width * 0.6 then should_newline = true end + end + + -- If we want to have multiple lines, but don't fit in one then abort! + if should_newline and height <= 1 then + write_with(text_colour, open) write_with(extra_colour, " ... ") write_with(text_colour, close) + return + end + + -- Make sure our keys are in some sort of sensible order + table.sort(keys, pretty_sort) + + local next_newline, sub_indent, child_width, child_height + if should_newline then + next_newline, sub_indent = ",\n", indent .. " " + + -- We split our height over multiple items. A future improvement could be to + -- give more "height" to complex elements (such as tables) + height = height - 2 + child_width, child_height = width - 2, math.ceil(height / children) + + -- If there's more children then we have space then + if children > height then children = height - 2 end + else + next_newline, sub_indent = ", ", "" + + -- Like multi-line elements, we share the width across multiple children + width = width - 2 + child_width, child_height = math.ceil(width / children), 1 + end + + write_with(text_colour, open .. (should_newline and "\n" or " ")) + + tracking[obj] = true + local seen = {} + local first = true + for k = 1, length do + if not first then write_with(text_colour, next_newline) else first = false end + write_with(text_colour, sub_indent) + + seen[k] = true + pretty_impl(obj[k], tracking, child_width, child_height, sub_indent) + + children = children - 1 + if children < 0 then + if not first then write_with(text_colour, next_newline) else first = false end + write_with(extra_colour, sub_indent .. "...") + break + end + end + + for i = 1, kn do + local k, v = keys[i], obj[keys[i]] + if not seen[k] then + if not first then write_with(text_colour, next_newline) else first = false end + write_with(text_colour, sub_indent) + + if type(k) == "string" and not keywords[k] and string.match( k, "^[%a_][%a%d_]*$" ) then + write_with(text_colour, k .. " = ") + pretty_impl(v, tracking, child_width, child_height, sub_indent) + else + write_with(text_colour, "[") + pretty_impl(k, tracking, child_width, child_height, sub_indent) + write_with(text_colour, "] = ") + pretty_impl(v, tracking, child_width, child_height, sub_indent) + end + + children = children - 1 + if children < 0 then + if not first then write_with(text_colour, next_newline) else first = false end + write_with(extra_colour, sub_indent .. "...") + break + end + end + end + tracking[obj] = nil + + write_with(text_colour, (should_newline and "\n" .. indent or " ") .. (tuple_length and ")" or "}")) +end + +local function pretty(t, n) + local width, height = term.getSize() + local fit_height = settings.get("mbs.lua.pretty_height", true) + if type(fit_height) == "number" then height = fit_height + elseif fit_height == false then height = 1/0 end + return pretty_impl(t, {}, width, height - 2, "", n) +end + +local running = true +local history = {} +local counter = 1 +local output = {} + +local environment = setmetatable({ + exit = setmetatable({}, { + __tostring = function() return "Call exit() to exit" end, + __call = function() running = false end, + }), + + _noTail = function(...) return ... end, + + out = output, +}, { __index = _ENV }) + +local autocomplete = nil +if not settings or settings.get("lua.autocomplete") then + autocomplete = function(line) + local start = line:find("[a-zA-Z0-9_%.:]+$") + if start then + line = line:sub(start) + end + if #line > 0 then + return textutils.complete(line, environment) + end + end +end + +local history_file = settings.get("mbs.lua.history_file", ".lua_history") +if history_file and fs.exists(history_file) then + local handle = fs.open(history_file, "r") + if handle then + for line in handle.readLine do history[#history + 1] = line end + handle.close() + end +end + +local function set_output(out, length) + environment._ = out + environment['_' .. counter] = out + output[counter] = out + + term.setTextColour(output_colour) + write("out[" .. counter .. "]: ") + term.setTextColour(text_colour) + + if type(out) == "table" then + print(pretty(out, length)) + else + print(pretty(out)) + end +end + +--- Handle the result of the function +local function handle(force_print, success, ...) + if success then + local len = select('#', ...) + if len == 0 then + if force_print then + set_output(nil) + end + elseif len == 1 then + set_output(...) + else + set_output({...}, len) + end + else + printError(...) + end +end + +if type(package) == "table" and type(package.path) == "string" then + -- Attempt to determine the shell directory with leading and trailing slashes + local dir = shell.dir() + if dir:sub(1, 1) ~= "/" then dir = "/" .. dir end + if dir:sub(#dir, #dir) ~= "/" then dir = dir .. "/" end + + -- Strip the default "current program" package path + local strip_path = "?;?.lua;?/init.lua;" + local path = package.path + if path:sub(1, #strip_path) == strip_path then path = path:sub(#strip_path + 1) end + + -- And append the current directory to the package path + package.path = dir .. "?;" .. dir .. "?.lua;" .. dir .. "?/init.lua;" .. path +end + +while running do + term.setTextColour(input_colour) + term.write("in [" .. counter .. "]: ") + term.setTextColour(text_colour) + + local line + if readline and readline.read and settings.get("mbs.lua.highlight") then + line = readline.read { + history = history, + complete = autocomplete, + highlight = highlight, + } + else + line = read(nil, history, autocomplete) + end + if not line then break end + + if line:find("%S") then + if line ~= history[#history] then + -- Add item to history + history[#history + 1] = line + + -- Remove extra items from history + local max = tonumber(settings.get("mbs.lua.history_max", 1e4)) or 1e4 + while #history > max do table.remove(history, 1) end + + -- Write history file + local history_file = settings.get("mbs.lua.history_file", ".lua_history") + if history_file then + local handle = fs.open(history_file, "w") + if handle then + for i = 1, #history do handle.writeLine(history[i]) end + handle.close() + end + end + end + + local force_print = true + local func, e = load("return " .. line, "=lua", "t", environment) + if not func then + func, e = load(line, "=lua", "t", environment) + force_print = false + else + local wrapped_func = load("return _noTail(" .. line .. ")", "=lua", "t", environment) + if wrapped_func then func = wrapped_func end + end + + if func then + if settings.get("mbs.lua.traceback", true) then + handle(force_print, stack_trace.xpcall_with(func)) + else + handle(force_print, pcall(func)) + end + else + printError(e) + end + + counter = counter + 1 + end +end
\ No newline at end of file diff --git a/.mbs/bin/mbs.lua b/.mbs/bin/mbs.lua new file mode 100644 index 0000000..67fada6 --- /dev/null +++ b/.mbs/bin/mbs.lua @@ -0,0 +1,260 @@ +local arg = table.pack(...) +local root_dir = ".mbs" +local rom_dir = "rom/.mbs" +local install_dir = fs.exists(root_dir) and root_dir or rom_dir +local repo_url = "https://raw.githubusercontent.com/SquidDev-CC/mbs/master/" + +--- Write a string with the given colour to the terminal +local function write_coloured(colour, text) + local old = term.getTextColour() + term.setTextColour(colour) + io.write(text) + term.setTextColour(old) +end + +--- Print usage for this program +local commands = { "install", "modules", "module", "download" } +local function print_usage() + local name = fs.getName(shell.getRunningProgram()):gsub("%.lua$", "") + write_coloured(colours.cyan, name .. " modules ") io.write("Print the status of all modules\n") + write_coloured(colours.cyan, name .. " module ") io.write("Print information about a given module\n") + write_coloured(colours.cyan, name .. " install ") io.write("Download all modules and create a startup file\n") + write_coloured(colours.cyan, name .. " download ") io.write("Download all modules WITHOUT creating a startup file\n") +end + +--- Attempt to load a module from the given path, returning the module or false +-- and an error message. +local function load_module(path) + if fs.isDir(path) then return false, "Invalid module (is directory)" end + + local fn, err = loadfile(path, _ENV) + if not fn then return false, "Invalid module (" .. err .. ")" end + + local ok, res = pcall(fn) + if not ok then return false, "Invalid module (" .. res .. ")" end + + if type(res) ~= "table" or type(res.description) ~= "string" or type(res.enabled) ~= "function" then + return false, "Malformed module" + end + + return res +end + +--- Setup all modules +local function setup_module(module) + for _, setting in ipairs(module.settings) do + if settings.get(setting.name) == nil then + settings.set(setting.name, setting.default) + end + end +end + +--- Download a set of files +local function download_files(files) + if #files == 0 then return end + + local urls = {} + for _, file in ipairs(files) do + local url = repo_url .. file + http.request(url) + urls[url] = file + end + + while true do + local event, url, arg1 = os.pullEvent() + if event == "http_success" and urls[url] then + local handle = fs.open(fs.combine(root_dir, urls[url]), "w") + handle.write(arg1.readAll()) + handle.close() + arg1.close() + + urls[url] = nil + if next(urls) == nil then return end + elseif event == "http_failure" and urls[url] then + error("Could not download " .. urls[url], 0) + end + end +end + +--- read completion helper, completes text using the given options +local function complete_multi(text, options, add_spaces) + local results = {} + for n = 1, #options do + local option = options[n] + if #option + (add_spaces and 1 or 0) > #text and option:sub(1, #text) == text then + local result = option:sub(#text + 1) + if add_spaces then + results[#results + 1] = result .. " " + else + results[#results + 1] = result + end + end + end + return results +end + +--- Append an object to a list if it is not already contained within +local function add_unique(list, x) + for i = 1, #list do if list[i] == x then return end end + list[#list + 1] = x +end + +local function load_all_modules() + -- Load all modules and update them. + local module_dir = fs.combine(root_dir, "modules") + local modules = fs.isDir(module_dir) and fs.list(module_dir) or {} + + -- Add the default modules if not already there. + for _, module in ipairs { "lua.lua", "pager.lua", "readline.lua", "shell.lua" } do + add_unique(modules, module) + end + + local files = {} + for i = 1, #modules do files[i] = "modules/" .. modules[i] end + download_files(files) + + -- Scan for dependencies in enabled modules, downloading them as well + local deps = {} + for i = 1, #files do + local module = load_module(fs.combine(root_dir, files[i])) + if module then + setup_module(module) + if module.enabled() then + for _, dep in ipairs(module.dependencies) do deps[#deps + 1] = dep end + end + end + end + download_files(deps) +end + +if arg.n == 0 then + printError("Expected some command") + print_usage() + error() +elseif arg[1] == "download" then + load_all_modules() +elseif arg[1] == "install" then + load_all_modules() + + -- Move the existing startup file. We have to read the whole thing, + -- as otherwise we'd end up copying inside ourselves. + if fs.exists("startup") and not fs.isDir("startup") then + write_coloured(colours.cyan, "Moving your existing startup file to startup/30_startup.lua.\n") + + local handle = fs.open("startup", "r") + local contents = handle.readAll() + handle.close() + fs.delete("startup") + + handle = fs.open("startup/30_startup.lua", "w") + handle.write(contents) + handle.close() + end + + -- Also move the startup.lua file afterwards + if fs.exists("startup.lua") and not fs.isDir("startup.lua") then + write_coloured(colours.cyan, "Moving your existing startup.lua file to startup/31_startup.lua.\n") + fs.move("startup.lua", "startup/31_startup.lua") + end + + if fs.exists("startup/99_mbs.lua") then + write_coloured(colours.cyan, "Deleting the old startup/99_mbs.lua file. We now run before other startup files.\n") + fs.delete("startup/99_mbs.lua") + end + + -- We'll run at the first possible position to ensure + local handle = fs.open("startup/00_mbs.lua", "w") + handle.writeLine(("assert(loadfile(%q, _ENV))('startup')"):format(shell.getRunningProgram())) + handle.close() + + write_coloured(colours.green, "Installed! ") + io.write("Please reboot to apply changes.\n") +elseif arg[1] == "startup" then + -- Gather a list of all modules + local module_dir = fs.combine(install_dir, "modules") + local files = fs.isDir(module_dir) and fs.list(module_dir) or {} + + -- Load those modules and determine which are enabled. + local enabled = {} + local module_names = {} + for _, file in ipairs(files) do + local module = load_module(fs.combine(module_dir, file)) + if module then + setup_module(module) + module_names[#module_names + 1] = file:gsub("%.lua$", "") + if module.enabled() then enabled[#enabled + 1] = module end + end + end + + shell.setCompletionFunction(shell.getRunningProgram(), function(_, index, text, previous) + if index == 1 then + return complete_multi(text, commands, true) + elseif index == 2 and previous[#previous] == "module" then + return complete_multi(text, module_names, false) + end + end) + + -- Setup those modules + for _, module in ipairs(enabled) do + if type(module.setup) == "function" then module.setup(install_dir) end + end + + -- And run the startup hook if needed + for _, module in ipairs(enabled) do + if type(module.startup) == "function" then module.startup(install_dir) end + end + +elseif arg[1] == "modules" then + local module_dir = fs.combine(install_dir, "modules") + local files = fs.isDir(module_dir) and fs.list(module_dir) or {} + local found_any = false + + for _, file in ipairs(files) do + local res, err = load_module(fs.combine(module_dir, file)) + write_coloured(colours.cyan, file:gsub("%.lua$", "") .. " ") + if res then + write(res.description) + if res.enabled() then + write_coloured(colours.green, " (enabled)") + else + write_coloured(colours.red, " (disabled)") + end + found_any = true + else + write_coloured(colours.red, err) + end + + io.write("\n") + end + + if not found_any then error("No modules found. Maybe try running the `install` command?", 0) end +elseif arg[1] == "module" then + if not arg[2] then error("Expected module name", 0) end + local module, err = load_module(fs.combine(install_dir, fs.combine("modules", arg[2] .. ".lua"))) + if not module then error(err, 0) end + + io.write(module.description) + if module.enabled() then + write_coloured(colours.green, " (enabled)") + else + write_coloured(colours.red, " (disabled)") + end + io.write("\n\n") + + for _, setting in ipairs(module.settings) do + local value = settings.get(setting.name) + write_coloured(colours.cyan, setting.name) + io.write(" " .. setting.description .. " (") + write_coloured(colours.yellow, textutils.serialise(value)) + if value ~= setting.default then + io.write(", default is \n") + write_coloured(colours.yellow, textutils.serialise(setting.default)) + end + + io.write(")\n") + end +else + printError("Unknown command") + print_usage() + error() +end diff --git a/.mbs/bin/shell.lua b/.mbs/bin/shell.lua new file mode 100644 index 0000000..6dd7d9c --- /dev/null +++ b/.mbs/bin/shell.lua @@ -0,0 +1,702 @@ + +local multishell = multishell +local parentShell = shell + +if multishell then + multishell.setTitle(multishell.getCurrent(), "shell") +end + +local bExit = false +local sDir = (parentShell and parentShell.dir()) or "" +local sPath = (parentShell and parentShell.path()) or ".:/rom/programs" +local tAliases = (parentShell and parentShell.aliases()) or {} +local tCompletionInfo = (parentShell and parentShell.getCompletionInfo()) or {} +local tProgramStack = {} +local history = parentShell and type(parentShell.history) == "function" and parentShell.history() +local fWrapper = nil + +local shell = {} +local function createShellEnv(sDir) + local tEnv = {} + tEnv["shell"] = shell + tEnv["multishell"] = multishell + + if fWrapper then + if read then tEnv.read = fWrapper(read) end + if readline and readline.read then tEnv.readline = { read = fWrapper(readline.read) } end + end + + local package = {} + package.loaded = { + _G = _G, + bit32 = bit32, + coroutine = coroutine, + math = math, + package = package, + string = string, + table = table, + } + package.path = settings.get('mbs.shell.require_path') or + "?;?.lua;?/init.lua;/rom/modules/main/?;/rom/modules/main/?.lua;/rom/modules/main/?/init.lua" + if turtle then + package.path = package.path..";/rom/modules/turtle/?;/rom/modules/turtle/?.lua;/rom/modules/turtle/?/init.lua" + elseif command then + package.path = package.path..";/rom/modules/command/?;/rom/modules/command/?.lua;/rom/modules/command/?/init.lua" + end + package.config = "/\n;\n?\n!\n-" + package.preload = {} + package.loaders = { + function(name) + if package.preload[name] then + return package.preload[name] + else + return nil, "no field package.preload['" .. name .. "']" + end + end, + function(name) + local fname = string.gsub(name, "%.", "/") + local sError = "" + for pattern in string.gmatch(package.path, "[^;]+") do + local sPath = string.gsub(pattern, "%?", fname) + if sPath:sub(1,1) ~= "/" then + sPath = fs.combine(sDir, sPath) + end + if fs.exists(sPath) and not fs.isDir(sPath) then + local fnFile, sError = loadfile(sPath, tEnv) + if fnFile then + return fnFile, sPath + else + return nil, sError + end + else + if #sError > 0 then + sError = sError .. "\n " + end + sError = sError .. "no file '" .. sPath .. "'" + end + end + return nil, sError + end + } + + local sentinel = {} + local function require(name) + if type(name) ~= "string" then + error("bad argument #1 (expected string, got " .. type(name) .. ")", 2) + end + if package.loaded[name] == sentinel then + error("loop or previous error loading module '" .. name .. "'", 0) + end + if package.loaded[name] then + return package.loaded[name] + end + + local sError = "module '" .. name .. "' not found:" + for _, searcher in ipairs(package.loaders) do + local loader = table.pack(searcher(name)) + if loader[1] then + package.loaded[name] = sentinel + local result = loader[1](name, table.unpack(loader, 2, loader.n)) + if result == nil then result = true end + + package.loaded[name] = result + return result + else + sError = sError .. "\n " .. loader[2] + end + end + error(sError, 2) + end + + tEnv["package"] = package + tEnv["require"] = require + + return tEnv +end + +-- Colours +local promptColour, textColour, bgColour +if term.isColour() then + promptColour = colours.yellow + textColour = colours.white + bgColour = colours.black +else + promptColour = colours.white + textColour = colours.white + bgColour = colours.black +end + +local function run(_sCommand, ...) + local sPath = shell.resolveProgram(_sCommand) + if sPath ~= nil then + tProgramStack[#tProgramStack + 1] = sPath + if multishell then + local sTitle = fs.getName(sPath) + if sTitle:sub(-4) == ".lua" then + sTitle = sTitle:sub(1,-5) + end + multishell.setTitle(multishell.getCurrent(), sTitle) + end + local sDir = fs.getDir(sPath) + local tEnv = setmetatable(createShellEnv(sDir), { __index = _G }) + + if settings.get("mbs.shell.strict_globals", false) then + -- load (in bios.lua) will attempt to set _ENV on our environment, which + -- throws an error with this protection enabled. Thus we set it here first. + tEnv._ENV = tEnv + getmetatable(tEnv).__newindex = function(_, name) + error("Attempt to create global " .. tostring(name) .. "\n If this is intended then you probably want to use _G." .. tostring(name), 2) + end + end + + local ok + local fnFile, err = loadfile(sPath, tEnv) + if fnFile then + if settings.get("mbs.shell.traceback", true) then + local tArgs = table.pack(...) + ok, err = stack_trace.xpcall_with(function() return fnFile(table.unpack(tArgs, 1, tArgs.n)) end) + else + ok, err = pcall(fnFile, ...) + end + + if not ok then + ok = false + if err and err ~= "" then printError(err) end + end + else + ok = false + if err and err ~= "" then printError(err) end + end + + tProgramStack[#tProgramStack] = nil + if multishell then + if #tProgramStack > 0 then + local sTitle = fs.getName(tProgramStack[#tProgramStack]) + if sTitle:sub(-4) == ".lua" then + sTitle = sTitle:sub(1,-5) + end + multishell.setTitle(multishell.getCurrent(), sTitle) + else + multishell.setTitle(multishell.getCurrent(), "shell") + end + end + return ok + else + printError("No such program") + return false + end +end + +local function tokenise(...) + local sLine = table.concat({ ... }, " ") + local tWords = {} + local bQuoted = false + for match in string.gmatch(sLine .. "\"", "(.-)\"") do + if bQuoted then + table.insert(tWords, match) + else + for m in string.gmatch(match, "[^ \t]+") do + table.insert(tWords, m) + end + end + bQuoted = not bQuoted + end + return tWords +end + +-- Install shell API +function shell.run(...) + local tWords = tokenise(...) + local sCommand = tWords[1] + if sCommand then + return run(sCommand, table.unpack(tWords, 2)) + end + return false +end + +function shell.exit() + bExit = true +end + +function shell.dir() + return sDir +end + +function shell.setDir(_sDir) + if type(_sDir) ~= "string" then + error("bad argument #1 (expected string, got " .. type(_sDir) .. ")", 2) + end + if not fs.isDir(_sDir) then + error("Not a directory", 2) + end + sDir = _sDir +end + +function shell.path() + return sPath +end + +function shell.setPath(_sPath) + if type(_sPath) ~= "string" then + error("bad argument #1 (expected string, got " .. type(_sPath) .. ")", 2) + end + sPath = _sPath +end + +function shell.resolve(_sPath) + if type(_sPath) ~= "string" then + error("bad argument #1 (expected string, got " .. type(_sPath) .. ")", 2) + end + local sStartChar = string.sub(_sPath, 1, 1) + if sStartChar == "/" or sStartChar == "\\" then + return fs.combine("", _sPath) + else + return fs.combine(sDir, _sPath) + end +end + +local function pathWithExtension(_sPath, _sExt) + local nLen = #sPath + local sEndChar = string.sub(_sPath, nLen, nLen) + -- Remove any trailing slashes so we can add an extension to the path safely + if sEndChar == "/" or sEndChar == "\\" then + _sPath = string.sub(_sPath, 1, nLen - 1) + end + return _sPath .. "." .. _sExt +end + +function shell.resolveProgram(_sCommand) + if type(_sCommand) ~= "string" then + error("bad argument #1 (expected string, got " .. type(_sCommand) .. ")", 2) + end + -- Substitute aliases firsts + if tAliases[_sCommand] ~= nil then + _sCommand = tAliases[_sCommand] + end + + -- If the path is a global path, use it directly + local sStartChar = string.sub(_sCommand, 1, 1) + if _sCommand:find("/") or _sCommand:find("\\") then + local sPath = shell.resolve(_sCommand) + if fs.exists(sPath) and not fs.isDir(sPath) then + return sPath + else + local sPathLua = pathWithExtension(sPath, "lua") + if fs.exists(sPathLua) and not fs.isDir(sPathLua) then + return sPathLua + end + end + return nil + end + + -- Otherwise, look on the path variable + for sPath in string.gmatch(sPath, "[^:]+") do + sPath = fs.combine(shell.resolve(sPath), _sCommand) + if fs.exists(sPath) and not fs.isDir(sPath) then + return sPath + else + local sPathLua = pathWithExtension(sPath, "lua") + if fs.exists(sPathLua) and not fs.isDir(sPathLua) then + return sPathLua + end + end + end + + -- Not found + return nil +end + +function shell.programs(_bIncludeHidden) + local tItems = {} + + -- Add programs from the path + for sPath in string.gmatch(sPath, "[^:]+") do + sPath = shell.resolve(sPath) + if fs.isDir(sPath) then + local tList = fs.list(sPath) + for n=1,#tList do + local sFile = tList[n] + if not fs.isDir(fs.combine(sPath, sFile)) and + (_bIncludeHidden or string.sub(sFile, 1, 1) ~= ".") then + if #sFile > 4 and sFile:sub(-4) == ".lua" then + sFile = sFile:sub(1,-5) + end + tItems[sFile] = true + end + end + end + end + + -- Sort and return + local tItemList = {} + for sItem in pairs(tItems) do + table.insert(tItemList, sItem) + end + table.sort(tItemList) + return tItemList +end + +local function completeProgram(sLine) + if #sLine > 0 and (sLine:find("/") or sLine:find("\\")) then + -- Add programs from the root + return fs.complete(sLine, sDir, true, false) + + else + local tResults = {} + local tSeen = {} + + -- Add aliases + for sAlias in pairs(tAliases) do + if #sAlias > #sLine and string.sub(sAlias, 1, #sLine) == sLine then + local sResult = string.sub(sAlias, #sLine + 1) + if not tSeen[sResult] then + table.insert(tResults, sResult) + tSeen[sResult] = true + end + end + end + + -- Add all subdirectories. We don't include files as they will be added in the block below + local tDirs = fs.complete(sLine, sDir, false, false) + for i = 1, #tDirs do + local sResult = tDirs[i] + if not tSeen[sResult] then + table.insert(tResults, sResult) + tSeen[sResult] = true + end + end + + -- Add programs from the path + local tPrograms = shell.programs() + for n=1,#tPrograms do + local sProgram = tPrograms[n] + if #sProgram > #sLine and string.sub(sProgram, 1, #sLine) == sLine then + local sResult = string.sub(sProgram, #sLine + 1) + if not tSeen[sResult] then + table.insert(tResults, sResult) + tSeen[sResult] = true + end + end + end + + -- Sort and return + table.sort(tResults) + return tResults + end +end + +local function completeProgramArgument(sProgram, nArgument, sPart, tPreviousParts) + local tInfo = tCompletionInfo[sProgram] + if tInfo then + return tInfo.fnComplete(shell, nArgument, sPart, tPreviousParts) + end + return nil +end + +function shell.complete(sLine) + if type(sLine) ~= "string" then + error("bad argument #1 (expected string, got " .. type(sLine) .. ")", 2) + end + if #sLine > 0 then + local tWords = tokenise(sLine) + local nIndex = #tWords + if string.sub(sLine, #sLine, #sLine) == " " then + nIndex = nIndex + 1 + end + if nIndex == 1 then + local sBit = tWords[1] or "" + local sPath = shell.resolveProgram(sBit) + if tCompletionInfo[sPath] then + return { " " } + else + local tResults = completeProgram(sBit) + for n=1,#tResults do + local sResult = tResults[n] + local sPath = shell.resolveProgram(sBit .. sResult) + if tCompletionInfo[sPath] then + tResults[n] = sResult .. " " + end + end + return tResults + end + + elseif nIndex > 1 then + local sPath = shell.resolveProgram(tWords[1]) + local sPart = tWords[nIndex] or "" + local tPreviousParts = tWords + tPreviousParts[nIndex] = nil + return completeProgramArgument(sPath , nIndex - 1, sPart, tPreviousParts) + + end + end + return nil +end + +function shell.completeProgram(sProgram) + if type(sProgram) ~= "string" then + error("bad argument #1 (expected string, got " .. type(sProgram) .. ")", 2) + end + return completeProgram(sProgram) +end + +function shell.setCompletionFunction(sProgram, fnComplete) + if type(sProgram) ~= "string" then + error("bad argument #1 (expected string, got " .. type(sProgram) .. ")", 2) + end + if type(fnComplete) ~= "function" then + error("bad argument #2 (expected function, got " .. type(fnComplete) .. ")", 2) + end + tCompletionInfo[sProgram] = { + fnComplete = fnComplete + } +end + +function shell.getCompletionInfo() + return tCompletionInfo +end + +function shell.getRunningProgram() + if #tProgramStack > 0 then + return tProgramStack[#tProgramStack] + end + return nil +end + +function shell.setAlias(_sCommand, _sProgram) + if type(_sCommand) ~= "string" then + error("bad argument #1 (expected string, got " .. type(_sCommand) .. ")", 2) + end + if type(_sProgram) ~= "string" then + error("bad argument #2 (expected string, got " .. type(_sProgram) .. ")", 2) + end + tAliases[_sCommand] = _sProgram +end + +function shell.clearAlias(_sCommand) + if type(_sCommand) ~= "string" then + error("bad argument #1 (expected string, got " .. type(_sCommand) .. ")", 2) + end + tAliases[_sCommand] = nil +end + +function shell.aliases() + -- Copy aliases + local tCopy = {} + for sAlias, sCommand in pairs(tAliases) do + tCopy[sAlias] = sCommand + end + return tCopy +end + +function shell.history() + -- Read commands and execute them + if not history then + history = {} + + local history_file = settings.get("mbs.shell.history_file", ".shell_history") + if history_file and fs.exists(history_file) then + local handle = fs.open(history_file, "r") + if handle then + for line in handle.readLine do history[#history + 1] = line end + handle.close() + end + end + end + + return history +end + +if multishell then + function shell.openTab(...) + local tWords = tokenise(...) + local sCommand = tWords[1] + if sCommand then + local sPath = shell.resolveProgram(sCommand) + if sPath == "rom/programs/shell.lua" then + return multishell.launch(createShellEnv("rom/programs"), sPath, table.unpack(tWords, 2)) + elseif sPath ~= nil then + return multishell.launch(createShellEnv("rom/programs"), "rom/programs/shell.lua", sCommand, table.unpack(tWords, 2)) + else + printError("No such program") + end + end + end + + function shell.switchTab(nID) + if type(nID) ~= "number" then + error("bad argument #1 (expected number, got " .. type(nID) .. ")", 2) + end + multishell.setFocus(nID) + end +end + +local tArgs = { ... } +if #tArgs > 0 then + -- "shell x y z": Run the program specified on the commandline + shell.run(...) + return +end + +-- "shell": Run the shell REPL +local parent = term.current() +local redirect = scroll_window.create(parent) + +local function get_first_startup() + if fs.exists("startup.lua") then return "startup.lua" end + if fs.isDir("startup") then + local first = fs.list("startup")[1] + if first then return fs.combine("startup", first) end + end + + return nil +end + +--- Create a wrapper for various read functions, allowing the user to scroll +-- when typing. +local scroll_offset = nil +fWrapper = function(fn) + return function(...) + -- Set the scroll_offset to 0 to allow scrolling + scroll_offset = 0 + + local ok, res = pcall(fn, ...) + + -- And set to nil again to disable + if scroll_offset ~= 0 then redirect.draw(0) end + scroll_offset = nil + + if not ok then error(res, 0) end + return res + end +end + +local worker = coroutine.create(function() + + -- Print the header + term.redirect(redirect) + term.setCursorPos(1, 1) + term.setBackgroundColor(bgColour) + term.setTextColour(promptColour) + print(os.version() .. " (+MBS)") + term.setTextColour(textColour) + + if parentShell == nil then + -- If we've no parent shell. run the startup script. It's pretty unlikely, + -- but some mad people might be using it! + shell.run("/rom/startup.lua") + elseif parentShell.getRunningProgram() == get_first_startup() then + -- If we're currently in the first startup file, then run all the others. + local current = parentShell.getRunningProgram() + + -- Run /startup or /startup.lua + local root_startup = shell.resolveProgram("startup") + if root_startup and root_startup ~= current then shell.run("/" .. root_startup) end + + -- Run startup/* + if fs.isDir("startup") then + for _, file in ipairs(fs.list("startup")) do + local sub_startup = fs.combine("startup", file) + if sub_startup ~= current and not fs.isDir(sub_startup) then + shell.run("/" .. sub_startup) + end + end + end + end + + -- The main interaction loop + local history = shell.history() + local wrapped_read = fWrapper(read) + while not bExit do + local scrollback = tonumber(settings.get("mbs.shell.scroll_max", 1e3)) + if scrollback then redirect.setMaxScrollback(scrollback) end + + term.setBackgroundColor(bgColour) + term.setTextColour(promptColour) + if term.getCursorPos() ~= 1 then print() end + write(shell.dir() .. "> ") + term.setTextColour(textColour) + + local line + if settings.get("shell.autocomplete") then + line = wrapped_read(nil, history, shell.complete) + else + line = wrapped_read(nil, history) + end + + if not line then break end + + if line:match("%S") and history[#history] ~= line then + -- Add item to history + history[#history + 1] = line + + -- Remove extra items from history + local max = tonumber(settings.get("mbs.shell.history_max", 1e4)) or 1e4 + while #history > max do table.remove(history, 1) end + + -- Write history file + local history_file = settings.get("mbs.shell.history_file", ".shell_history") + if history_file then + local handle = fs.open(history_file, "w") + if handle then + for i = 1, #history do handle.writeLine(history[i]) end + handle.close() + end + end + end + + local _, y = term.getCursorPos() + redirect.setCursorThreshold(y) + + local ok = shell.run(line) + + term.redirect(redirect) + redirect.endPrivateMode(not ok) + redirect.draw(0) + end + + term.redirect(parent) +end) + +local ok, filter = coroutine.resume(worker) + +-- We run the main worker inside a coroutine, catching any potential scroll +-- events. +while coroutine.status(worker) ~= "dead" do + local event = table.pack(coroutine.yield()) + local e = event[1] + + -- Run the main REPL worker + if filter == nil or e == filter or e == "terminate" then + ok, filter = coroutine.resume(worker, table.unpack(event, 1, event.n)) + end + + -- Resize the terminal if required + if e == "term_resize" then + redirect.updateSize() + redirect.draw(scroll_offset or 0, true) + end + + -- If we're in some interactive function, allow scrolling the input + if scroll_offset then + local change = 0 + if e == "mouse_scroll" then + change = event[2] + elseif e == "key" and event[2] == keys.pageDown then + change = 10 + elseif e == "key" and event[2] == keys.pageUp then + change = -10 + elseif e == "key" or e == "paste" then + -- Reset offset if another key is pressed + change = -scroll_offset + end + + if change ~= 0 and term.current() == redirect and not redirect.isPrivateMode() then + scroll_offset = scroll_offset + change + if scroll_offset > 0 then scroll_offset = 0 end + if scroll_offset < -redirect.getTotalHeight() then scroll_offset = -redirect.getTotalHeight() end + redirect.draw(scroll_offset) + end + end +end + +if not ok then error(filter, 0) end
\ No newline at end of file |
