aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Lee <alee14498@gmail.com>2019-07-19 14:10:39 -0400
committerAndrew Lee <alee14498@gmail.com>2019-07-19 14:10:39 -0400
commitfe08446d84e0aa939780ad013f0545778703da6d (patch)
treef45358ae6470dc894c41abcb278c4898c7ef1b18
parent21446806b264d8fd6694b1495f0c51bf24d26b35 (diff)
downloadbits-UI-fe08446d84e0aa939780ad013f0545778703da6d.tar.gz
bits-UI-fe08446d84e0aa939780ad013f0545778703da6d.tar.bz2
bits-UI-fe08446d84e0aa939780ad013f0545778703da6d.zip
MBS as default shell
-rw-r--r--.gitignore4
-rw-r--r--.mbs/bin/clear.lua4
-rw-r--r--.mbs/bin/help.lua19
-rw-r--r--.mbs/bin/lua.lua416
-rw-r--r--.mbs/bin/mbs.lua260
-rw-r--r--.mbs/bin/shell.lua702
-rw-r--r--.mbs/lib/blit_window.lua297
-rw-r--r--.mbs/lib/readline.lua506
-rw-r--r--.mbs/lib/scroll_window.lua459
-rw-r--r--.mbs/lib/stack_trace.lua88
-rw-r--r--.mbs/modules/lua.lua58
-rw-r--r--.mbs/modules/pager.lua48
-rw-r--r--.mbs/modules/readline.lua66
-rw-r--r--.mbs/modules/shell.lua81
-rw-r--r--README.md1
-rw-r--r--boot/bubl.cfg3
-rw-r--r--startup1
-rw-r--r--startup.lua30
18 files changed, 3019 insertions, 24 deletions
diff --git a/.gitignore b/.gitignore
index b9cdadb..b3a6928 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,6 @@ home/
etc/
.err-logs/
kst/
-.enchat \ No newline at end of file
+.enchat
+.shell_history
+.lua_history \ No newline at end of file
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
diff --git a/.mbs/lib/blit_window.lua b/.mbs/lib/blit_window.lua
new file mode 100644
index 0000000..39cd843
--- /dev/null
+++ b/.mbs/lib/blit_window.lua
@@ -0,0 +1,297 @@
+local colour_lookup = {}
+for i = 0, 16 do
+ colour_lookup[2 ^ i] = string.format("%x", i)
+end
+
+function create(original)
+ if not original then original = term.current() end
+
+ local text = {}
+ local text_colour = {}
+ local back_colour = {}
+ local palette = {}
+
+ local cursor_x, cursor_y = 1, 1
+
+ local cursor_blink = false
+ local cur_text_colour = "0"
+ local cur_back_colour = "f"
+
+ local sizeX, sizeY = original.getSize()
+ local color = original.isColor()
+
+ local bubble = true
+
+ local redirect = {}
+
+ if original.getPaletteColour then
+ for i = 0, 15 do
+ local c = 2 ^ i
+ palette[c] = { original.getPaletteColour(c) }
+ end
+ end
+
+ function redirect.write(writeText)
+ writeText = tostring(writeText)
+ if bubble then original.write(writeText) end
+
+ local pos = cursor_x
+
+ -- If we're off the screen then just emulate a write
+ if cursor_y > sizeY or cursor_y < 1 then
+ cursor_x = pos + #writeText
+ return
+ end
+
+ if pos + #writeText <= 1 or pos > sizeX then
+ -- If we're too far off the left then skip.
+ cursor_x = pos + #writeText
+ return
+ elseif pos < 1 then
+ -- Adjust text to fit on screen starting at one.
+ writeText = string.sub(writeText, math.abs(cursor_x) + 2)
+ pos = 1
+ end
+
+ local lineText = text[cursor_y]
+ local lineColor = text_colour[cursor_y]
+ local lineBack = back_colour[cursor_y]
+ local preStop = pos - 1
+ local preStart = math.min(1, preStop)
+ local postStart = pos + string.len(writeText)
+ local postStop = sizeX
+ local sub, rep = string.sub, string.rep
+
+ text[cursor_y] = sub(lineText, preStart, preStop)..writeText..sub(lineText, postStart, postStop)
+ text_colour[cursor_y] = sub(lineColor, preStart, preStop)..rep(cur_text_colour, #writeText)..sub(lineColor, postStart, postStop)
+ back_colour[cursor_y] = sub(lineBack, preStart, preStop)..rep(cur_back_colour, #writeText)..sub(lineBack, postStart, postStop)
+ cursor_x = pos + string.len(writeText)
+ end
+
+ function redirect.blit(writeText, writeFore, writeBack)
+ if type(writeText) ~= "string" then error("bad argument #1 (expected string, got " .. type(writeText) .. ")", 2) end
+ if type(writeFore) ~= "string" then error("bad argument #2 (expected string, got " .. type(writeFore) .. ")", 2) end
+ if type(writeBack) ~= "string" then error("bad argument #3 (expected string, got " .. type(writeBack) .. ")", 2) end
+ if #writeFore ~= #writeText or #writeBack ~= #writeText then error("Arguments must be the same length", 2) end
+
+ if bubble then original.blit(writeText, writeFore, writeBack) end
+ local pos = cursor_x
+
+ -- If we're off the screen then just emulate a write
+ if cursor_y > sizeY or cursor_y < 1 then
+ cursor_x = pos + #writeText
+ return
+ end
+
+ if pos + #writeText <= 1 then
+ --skip entirely.
+ cursor_x = pos + #writeText
+ return
+ elseif pos < 1 then
+ --adjust text to fit on screen starting at one.
+ writeText = string.sub(writeText, math.abs(cursor_x) + 2)
+ writeFore = string.sub(writeFore, math.abs(cursor_x) + 2)
+ writeBack = string.sub(writeBack, math.abs(cursor_x) + 2)
+ cursor_x = 1
+ elseif pos > sizeX then
+ --if we're off the edge to the right, skip entirely.
+ cursor_x = pos + #writeText
+ return
+ end
+
+ local lineText = text[cursor_y]
+ local lineColor = text_colour[cursor_y]
+ local lineBack = back_colour[cursor_y]
+ local preStop = cursor_x - 1
+ local preStart = math.min(1, preStop)
+ local postStart = cursor_x + string.len(writeText)
+ local postStop = sizeX
+ local sub = string.sub
+
+ text[cursor_y] = sub(lineText, preStart, preStop)..writeText..sub(lineText, postStart, postStop)
+ text_colour[cursor_y] = sub(lineColor, preStart, preStop)..writeFore..sub(lineColor, postStart, postStop)
+ back_colour[cursor_y] = sub(lineBack, preStart, preStop)..writeBack..sub(lineBack, postStart, postStop)
+ cursor_x = pos + string.len(writeText)
+ end
+
+ function redirect.clear()
+ for i = 1, sizeY do
+ text[i] = string.rep(" ", sizeX)
+ text_colour[i] = string.rep(cur_text_colour, sizeX)
+ back_colour[i] = string.rep(cur_back_colour, sizeX)
+ end
+
+ if bubble then return original.clear() end
+ end
+
+ function redirect.clearLine()
+ -- If we're off the screen then just emulate a clearLine
+ if cursor_y > sizeY or cursor_y < 1 then
+ return
+ end
+
+ text[cursor_y] = string.rep(" ", sizeX)
+ text_colour[cursor_y] = string.rep(cur_text_colour, sizeX)
+ back_colour[cursor_y] = string.rep(cur_back_colour, sizeX)
+
+ if bubble then return original.clearLine() end
+ end
+
+ function redirect.getCursorPos()
+ return cursor_x, cursor_y
+ end
+
+ function redirect.setCursorPos(x, y)
+ if type(x) ~= "number" then error("bad argument #1 (expected number, got " .. type(x) .. ")", 2) end
+ if type(y) ~= "number" then error("bad argument #2 (expected number, got " .. type(y) .. ")", 2) end
+
+ cursor_x = math.floor(x)
+ cursor_y = math.floor(y)
+ if bubble then return original.setCursorPos(x, y) end
+ end
+
+ function redirect.setCursorBlink(b)
+ cursor_blink = b
+ if bubble then return original.setCursorBlink(b) end
+ end
+
+ function redirect.getSize()
+ return sizeX, sizeY
+ end
+
+ function redirect.scroll(n)
+ if type(n) ~= "number" then error("bad argument #1 (expected number, got " .. type(n) .. ")", 2) end
+
+ local empty_text = string.rep(" ", sizeX)
+ local empty_text_colour = string.rep(cur_text_colour, sizeX)
+ local empty_back_colour = string.rep(cur_back_colour, sizeX)
+ if n > 0 then
+ for i = 1, sizeY do
+ text[i] = text[i + n] or empty_text
+ text_colour[i] = text_colour[i + n] or empty_text_colour
+ back_colour[i] = back_colour[i + n] or empty_back_colour
+ end
+ elseif n < 0 then
+ for i = sizeY, 1, -1 do
+ text[i] = text[i + n] or empty_text
+ text_colour[i] = text_colour[i + n] or empty_text_colour
+ back_colour[i] = back_colour[i + n] or empty_back_colour
+ end
+ end
+
+ if bubble then return original.scroll(n) end
+ end
+
+ function redirect.setTextColour(clr)
+ if type(clr) ~= "number" then error("bad argument #1 (expected number, got " .. type(clr) .. ")", 2) end
+ cur_text_colour = colour_lookup[clr] or error("Invalid colour (got " .. clr .. ")" , 2)
+ if bubble then return original.setTextColour(clr) end
+ end
+ redirect.setTextColor = redirect.setTextColour
+
+ function redirect.setBackgroundColour(clr)
+ if type(clr) ~= "number" then error("bad argument #1 (expected number, got " .. type(clr) .. ")", 2) end
+ cur_back_colour = colour_lookup[clr] or error("Invalid colour (got " .. clr .. ")" , 2)
+ if bubble then return original.setBackgroundColour(clr) end
+ end
+ redirect.setBackgroundColor = redirect.setBackgroundColour
+
+ function redirect.isColour()
+ return color == true
+ end
+ redirect.isColor = redirect.isColour
+
+ function redirect.getTextColour()
+ return 2 ^ tonumber(cur_text_colour, 16)
+ end
+ redirect.getTextColor = redirect.getTextColour
+
+ function redirect.getBackgroundColour()
+ return 2 ^ tonumber(cur_back_colour, 16)
+ end
+ redirect.getBackgroundColor = redirect.getBackgroundColour
+
+ if original.getPaletteColour then
+ function redirect.setPaletteColour(colour, r, g, b)
+ local palcol = palette[colour]
+ if not palcol then error("Invalid colour (got " .. tostring(colour) .. ")", 2) end
+ if type(r) == "number" and g == nil and b == nil then
+ palcol[1], palcol[2], palcol[3] = colours.rgb8(r)
+ else
+ if type(r) ~= "number" then error("bad argument #2 (expected number, got " .. type(r) .. ")", 2) end
+ if type(g) ~= "number" then error("bad argument #3 (expected number, got " .. type(g) .. ")", 2) end
+ if type(b) ~= "number" then error("bad argument #4 (expected number, got " .. type(b) .. ")", 2) end
+
+ palcol[1], palcol[2], palcol[3] = r, g, b
+ end
+
+ if bubble then return original.setPaletteColour(colour, r, g, b) end
+ end
+ redirect.setPaletteColor = redirect.setPaletteColour
+
+ function redirect.getPaletteColour(colour)
+ local palcol = palette[colour]
+ if not palcol then error("Invalid colour (got " .. tostring(colour) .. ")", 2) end
+ return palcol[1], palcol[2], palcol[3]
+ end
+ redirect.getPaletteColor = redirect.getPaletteColour
+ end
+
+ function redirect.draw(target)
+ if not target then target = original end
+
+ if target.getPaletteColour then
+ for colour, pal in pairs(palette) do
+ target.setPaletteColour(colour, pal[1], pal[2], pal[3])
+ end
+ end
+
+ for i=1, sizeY do
+ target.setCursorPos(1,i)
+ target.blit(text[i], text_colour[i], back_colour[i])
+ end
+
+ target.setCursorPos(cursor_x, cursor_y)
+ target.setTextColour(2 ^ tonumber(cur_text_colour, 16))
+ target.setBackgroundColor(2 ^ tonumber(cur_back_colour, 16))
+ target.setCursorBlink(cursor_blink)
+ end
+
+ function redirect.bubble(b)
+ bubble = b
+ end
+
+ function redirect.updateSize()
+ local new_x, new_y = original.getSize()
+ if new_x == sizeX and new_y == sizeY then return end
+
+ -- For any existing lines, trim them
+ for y = 1, sizeY do
+ if new_x < sizeX then
+ text[y] = text[y]:sub(1, new_x)
+ text_colour[y] = text_colour[y]:sub(1, new_x)
+ back_colour[y] = back_colour[y]:sub(1, new_x)
+ elseif new_x > sizeX then
+ text[y] = text[y] .. (" "):rep(new_x - sizeX)
+ text_colour[y] = text_colour[y] .. (cur_text_colour):rep(new_x - sizeX)
+ back_colour[y] = back_colour[y] .. (cur_back_colour):rep(new_x - sizeX)
+ end
+ end
+
+ -- Add any new lines we might need.
+ local text_line = (" "):rep(new_x)
+ local fore_line = (cur_text_colour):rep(new_x)
+ local back_line = (cur_back_colour):rep(new_x)
+ for y = sizeY + 1, new_y do
+ text[y] = text_line
+ text_colour[y] = fore_line
+ back_colour[y] = back_line
+ end
+
+ sizeX = new_x
+ sizeY = new_y
+ end
+
+ redirect.clear()
+ return redirect
+end \ No newline at end of file
diff --git a/.mbs/lib/readline.lua b/.mbs/lib/readline.lua
new file mode 100644
index 0000000..ba322a6
--- /dev/null
+++ b/.mbs/lib/readline.lua
@@ -0,0 +1,506 @@
+
+--- A mapping of colours, to aid reading settings.
+local colour_table = { }
+
+for k, v in pairs(colours) do
+ if type(v) == "number" then colour_table[k] = v end
+end
+
+for k, v in pairs(colors) do
+ if type(v) == "number" then colour_table[k] = v end
+end
+
+--- Keys which are used in combinations involving the "meta" key.
+local meta_keys = "ulcdbf"
+
+--- Clamp a [value] within a range
+local function clamp(value, min, max)
+ if value < min then return min end
+ if value > max then return max end
+ return value
+end
+
+--- Verify a [tbl] has a [key] of nil or the given [type]
+local type = type
+local function check_key(tbl, key, ty)
+ local actual_type = type(tbl[key])
+ if actual_type ~= "nil" and actual_type ~= ty then
+ error(("bad key %s (expected %s, got %s)"):format(key, ty, actual_type), 3)
+ end
+end
+
+function read(opts)
+ if opts == nil then
+ opts = {}
+ elseif type(opts) ~= "table" then
+ error("bad argument #1 (expected table, got " .. type(opts) .. ")", 2)
+ end
+
+ check_key(opts, "replace_char", "string") -- Character to show instead
+ check_key(opts, "history", "table") -- List of previous items
+ check_key(opts, "complete", "function") -- Completion function
+ check_key(opts, "default", "string") -- Initial string
+ check_key(opts, "complete_fg", "number") -- Foreground for completion
+ check_key(opts, "complete_bg", "number") -- Background for completion
+
+ -- Highlight function: (line: string, start: number) -> (end: number, colour:colour)
+ check_key(opts, "highlight", "function")
+
+ local w = term.getSize()
+ local sx = term.getCursorPos()
+
+ local sLine = opts.default or ""
+ local nPos, nScroll = #sLine, 0
+ local tKillRing, nKillRing = {}, 0
+
+ local nHistoryPos
+ local tDown = {}
+ local nMod = 0
+ local replace_char = opts.replace_char and opts.replace_char:sub(1, 1)
+ local complete_fg = opts.complete_fg or colour_table[settings.get("mbs.readline.complete_fg")] or -1
+ local complete_bg = opts.complete_bg or colour_table[settings.get("mbs.readline.complete_bg")] or -1
+
+ local tCompletions
+ local nCompletion
+ local function recomplete()
+ if opts.complete and nPos == #sLine then
+ tCompletions = opts.complete(sLine)
+ if tCompletions and #tCompletions > 0 then
+ nCompletion = 1
+ else
+ nCompletion = nil
+ end
+ else
+ tCompletions = nil
+ nCompletion = nil
+ end
+ end
+
+ local function uncomplete()
+ tCompletions = nil
+ nCompletion = nil
+ end
+
+ local function updateModifier()
+ nMod = 0
+ if tDown[keys.leftCtrl] or tDown[keys.rightCtrl] then nMod = nMod + 1 end
+ if tDown[keys.leftAlt] or tDown[keys.rightAlt] then nMod = nMod + 2 end
+ end
+
+ local function nextWord()
+ -- Attempt to find the position of the next word
+ local nOffset = sLine:find("%w%W", nPos + 1)
+ if nOffset then return nOffset else return #sLine end
+ end
+
+ local function prevWord()
+ -- Attempt to find the position of the previous word
+ local nOffset = 1
+ while nOffset <= #sLine do
+ local nNext = sLine:find("%W%w", nOffset)
+ if nNext and nNext < nPos then
+ nOffset = nNext + 1
+ else
+ break
+ end
+ end
+ return nOffset - 1
+ end
+
+ local function redraw(_bClear)
+ local cursor_pos = nPos - nScroll
+ if sx + cursor_pos >= w then
+ -- We've moved beyond the RHS, ensure we're on the edge.
+ nScroll = sx + nPos - w
+ elseif cursor_pos < 0 then
+ -- We've moved beyond the LHS, ensure we're on the edge.
+ nScroll = nPos
+ end
+
+ local _, cy = term.getCursorPos()
+ term.setCursorPos(sx, cy)
+ local sReplace = (_bClear and " ") or replace_char
+
+ if opts.highlight and not _bClear then
+ -- We've a highlighting function: step through each line of input
+ local old_col = term.getTextColor()
+ local hl_pos, hl_max, hl_col = 1, #sLine, old_col
+ while hl_pos <= hl_max do
+ local next_pos, next_col = opts.highlight(sLine, hl_pos)
+ if next_pos < hl_pos then error("Highlighting function consumed no input") end
+
+ if next_pos >= nScroll + 1 then
+ if next_col ~= hl_col then term.setTextColor(next_col) hl_col = next_col end
+ if sReplace then
+ term.write(string.rep(sReplace, next_pos - math.max(nScroll + 1, hl_pos) + 1))
+ else
+ term.write(string.sub(sLine, math.max(nScroll + 1, hl_pos), next_pos))
+ end
+ end
+
+ hl_pos = next_pos + 1
+ end
+ term.setTextColor(old_col)
+ else
+ -- If we've no highlighting function, we can go the "fast" path.
+ if sReplace then
+ term.write(string.rep(sReplace, math.max(#sLine - nScroll, 0)))
+ else
+ term.write(string.sub(sLine, nScroll + 1))
+ end
+ end
+
+ if nCompletion then
+ local sCompletion = tCompletions[ nCompletion ]
+ local oldText, oldBg
+ if not _bClear then
+ oldText = term.getTextColor()
+ oldBg = term.getBackgroundColor()
+ if complete_fg > -1 then term.setTextColor(complete_fg) end
+ if complete_bg > -1 then term.setBackgroundColor(complete_bg) end
+ end
+ if sReplace then
+ term.write(string.rep(sReplace, #sCompletion))
+ else
+ term.write(sCompletion)
+ end
+ if not _bClear then
+ term.setTextColor(oldText)
+ term.setBackgroundColor(oldBg)
+ end
+ end
+
+ term.setCursorPos(sx + nPos - nScroll, cy)
+ end
+
+ local function nsub(start, fin)
+ if start < 1 or fin < start then return "" end
+ return sLine:sub(start, fin)
+ end
+
+ local function clear()
+ redraw(true)
+ end
+
+ local function kill(text)
+ if #text == "" then return end
+ nKillRing = nKillRing + 1
+ tKillRing[nKillRing] = text
+ end
+
+ local function acceptCompletion()
+ if nCompletion then
+ -- Clear
+ clear()
+
+ -- Find the common prefix of all the other suggestions which start with the same letter as the current one
+ local sCompletion = tCompletions[ nCompletion ]
+ sLine = sLine .. sCompletion
+ nPos = #sLine
+
+ -- Redraw
+ recomplete()
+ redraw()
+ end
+ end
+
+ term.setCursorBlink(true)
+ recomplete()
+ redraw()
+ while true do
+ local sEvent, param, param1, param2 = os.pullEvent()
+ if sEvent == "char" and (nMod == 0 or nMod == 3 or (nMod == 2 and not meta_keys:find(param, 1, true))) then
+ -- Typed key
+ -- Alt+X will queue a char event, so we limit ourselves to cases where
+ -- no modifier is used, or Ctrl+Alt are (equivalent to AltGr), or the Alt
+ -- key is used and we have no known combination.
+ clear()
+ sLine = string.sub(sLine, 1, nPos) .. param .. string.sub(sLine, nPos + 1)
+ nPos = nPos + 1
+ recomplete()
+ redraw()
+ elseif sEvent == "paste" then
+ -- Pasted text
+ clear()
+ sLine = string.sub(sLine, 1, nPos) .. param .. string.sub(sLine, nPos + 1)
+ nPos = nPos + #param
+ recomplete()
+ redraw()
+ elseif sEvent == "key" then
+ -- All keybindigns within the read loop.
+ -- IMPORTANT: Please update the meta_keys variable up top. Ideally we'd
+ -- make each function operate on a state, and run outside the loop, but
+ -- this will do for now.
+ if param == keys.leftCtrl or param == keys.rightCtrl or param == keys.leftAlt or param == keys.rightAlt then
+ tDown[param] = true
+ updateModifier()
+ elseif param == keys.enter then
+ -- Enter
+ if nCompletion then
+ clear()
+ uncomplete()
+ redraw()
+ end
+ break
+
+ -- Moving through text/completions
+ elseif nMod == 1 and param == keys.d then
+ -- End of stream, abort
+ if nCompletion then
+ clear()
+ uncomplete()
+ redraw()
+ end
+ sLine = nil
+ nPos = 0
+ break
+ elseif (nMod == 0 and param == keys.left) or (nMod == 1 and param == keys.b) then
+ -- Left
+ if nPos > 0 then
+ clear()
+ nPos = nPos - 1
+ recomplete()
+ redraw()
+ end
+ elseif (nMod == 0 and param == keys.right) or (nMod == 1 and param == keys.f) then
+ -- Right
+ if nPos < #sLine then
+ -- Move right
+ clear()
+ nPos = nPos + 1
+ recomplete()
+ redraw()
+ else
+ -- Accept autocomplete
+ acceptCompletion()
+ end
+ elseif nMod == 2 and param == keys.b then
+ -- Word left
+ local nNewPos = prevWord()
+ if nNewPos ~= nPos then
+ clear()
+ nPos = nNewPos
+ recomplete()
+ redraw()
+ end
+ elseif nMod == 2 and param == keys.f then
+ -- Word right
+ local nNewPos = nextWord()
+ if nNewPos ~= nPos then
+ clear()
+ nPos = nNewPos
+ recomplete()
+ redraw()
+ end
+ elseif (nMod == 0 and (param == keys.up or param == keys.down))
+ or (nMod == 1 and (param == keys.p or param == keys.n)) then
+ -- Up or down
+ if nCompletion then
+ -- Cycle completions
+ clear()
+ if param == keys.up or param == keys.p then
+ nCompletion = nCompletion - 1
+ if nCompletion < 1 then
+ nCompletion = #tCompletions
+ end
+ elseif param == keys.down or param == keys.n then
+ nCompletion = nCompletion + 1
+ if nCompletion > #tCompletions then
+ nCompletion = 1
+ end
+ end
+ redraw()
+ elseif opts.history then
+ -- Cycle history
+ clear()
+ if param == keys.up or param == keys.p then
+ -- Up
+ if nHistoryPos == nil then
+ if #opts.history > 0 then
+ nHistoryPos = #opts.history
+ end
+ elseif nHistoryPos > 1 then
+ nHistoryPos = nHistoryPos - 1
+ end
+ elseif param == keys.down or param == keys.n then
+ -- Down
+ if nHistoryPos == #opts.history then
+ nHistoryPos = nil
+ elseif nHistoryPos ~= nil then
+ nHistoryPos = nHistoryPos + 1
+ end
+ end
+ if nHistoryPos then
+ sLine = opts.history[nHistoryPos]
+ nPos, nScroll = #sLine, 0
+ else
+ sLine = ""
+ nPos, nScroll = 0, 0
+ end
+ uncomplete()
+ redraw()
+ end
+ elseif (nMod == 0 and param == keys.home)
+ or (nMod == 1 and param == keys.a) then
+ -- Home
+ if nPos > 0 then
+ clear()
+ nPos = 0
+ recomplete()
+ redraw()
+ end
+ elseif (nMod == 0 and param == keys["end"])
+ or (nMod == 1 and param == keys.e) then
+ -- End
+ if nPos < #sLine then
+ clear()
+ nPos = #sLine
+ recomplete()
+ redraw()
+ end
+ -- Changing text
+ elseif nMod == 1 and param == keys.t then
+ -- Transpose char
+ local prev, cur
+ if nPos == #sLine then prev, cur = nPos - 1, nPos
+ elseif nPos == 0 then prev, cur = 1, 2
+ else prev, cur = nPos, nPos + 1
+ end
+
+ sLine = nsub(1, prev - 1) .. nsub(cur, cur) .. nsub(prev, prev) .. nsub(cur + 1, #sLine)
+ nPos = math.min(#sLine, cur)
+
+ -- We need the clear to remove the completion
+ clear(); recomplete(); redraw()
+ elseif nMod == 2 and param == keys.u then
+ -- Upcase word
+ if nPos < #sLine then
+ local nNext = nextWord()
+ sLine = nsub(1, nPos) .. nsub(nPos + 1, nNext):upper() .. nsub(nNext + 1, #sLine)
+ nPos = nNext
+ clear(); recomplete(); redraw()
+ end
+ elseif nMod == 2 and param == keys.l then
+ -- Lowercase word
+ if nPos < #sLine then
+ local nNext = nextWord()
+ sLine = nsub(1, nPos) .. nsub(nPos + 1, nNext):lower() .. nsub(nNext + 1, #sLine)
+ nPos = nNext
+ clear(); recomplete(); redraw()
+ end
+ elseif nMod == 2 and param == keys.c then
+ -- Capitalize word
+ if nPos < #sLine then
+ local nNext = nextWord()
+ sLine = nsub(1, nPos) .. nsub(nPos + 1, nPos + 1):upper()
+ .. nsub(nPos + 2, nNext):lower() .. nsub(nNext + 1, #sLine)
+ nPos = nNext
+ clear(); recomplete(); redraw()
+ end
+
+ -- Killing text
+ elseif nMod == 0 and param == keys.backspace then
+ -- Backspace
+ if nPos > 0 then
+ clear()
+ sLine = string.sub(sLine, 1, nPos - 1) .. string.sub(sLine, nPos + 1)
+ nPos = nPos - 1
+ if nScroll > 0 then nScroll = nScroll - 1 end
+ recomplete()
+ redraw()
+ end
+ elseif nMod == 0 and param == keys.delete then
+ -- Delete
+ if nPos < #sLine then
+ clear()
+ sLine = string.sub(sLine, 1, nPos) .. string.sub(sLine, nPos + 2)
+ recomplete()
+ redraw()
+ end
+ elseif nMod == 1 and param == keys.u then
+ -- Delete from cursor to beginning of line
+ if nPos > 0 then
+ clear()
+ kill(sLine:sub(1, nPos))
+ sLine = sLine:sub(nPos + 1)
+ nPos = 0
+ recomplete(); redraw()
+ end
+ elseif nMod == 1 and param == keys.k then
+ -- Delete from cursor to end of line
+ if nPos < #sLine then
+ clear()
+ kill(sLine:sub(nPos + 1))
+ sLine = sLine:sub(1, nPos)
+ nPos = #sLine
+ recomplete(); redraw()
+ end
+ elseif nMod == 2 and param == keys.d then
+ -- Delete from cursor to end of next word
+ if nPos < #sLine then
+ local nNext = nextWord()
+ if nNext ~= nPos then
+ clear()
+ kill(sLine:sub(nPos + 1, nNext))
+ sLine = sLine:sub(1, nPos) .. sLine:sub(nNext + 1)
+ recomplete(); redraw()
+ end
+ end
+ elseif nMod == 1 and param == keys.w then
+ -- Delete from cursor to beginning of previous word
+ if nPos > 0 then
+ local nPrev = prevWord(nPos)
+ if nPrev ~= nPos then
+ clear()
+ kill(sLine:sub(nPrev + 1, nPos))
+ sLine = sLine:sub(1, nPrev) .. sLine:sub(nPos + 1)
+ nPos = nPrev
+ recomplete(); redraw()
+ end
+ end
+ elseif nMod == 1 and param == keys.y then
+ local insert = tKillRing[nKillRing]
+ if insert then
+ clear()
+ sLine = sLine:sub(1, nPos) .. insert .. sLine:sub(nPos + 1)
+ nPos = nPos + #insert
+ recomplete(); redraw()
+ end
+ -- Misc
+ elseif nMod == 0 and param == keys.tab then
+ -- Tab (accept autocomplete)
+ acceptCompletion()
+ end
+ elseif sEvent == "key_up" then
+ -- Update the status of the modifier flag
+ if param == keys.leftCtrl or param == keys.rightCtrl
+ or param == keys.leftAlt or param == keys.rightAlt then
+ tDown[param] = false
+ updateModifier()
+ end
+ elseif sEvent == "mouse_click" or sEvent == "mouse_drag" and param == 1 then
+ local _, cy = term.getCursorPos()
+ if param2 == cy then
+ -- We first clamp the x position with in the start and end points
+ -- to ensure we don't scroll beyond the visible region.
+ local x = clamp(param1, sx, w)
+
+ -- Then ensure we don't scroll beyond the current line
+ nPos = clamp(nScroll + x - sx, 0, #sLine)
+
+ redraw()
+ end
+ elseif sEvent == "term_resize" then
+ -- Terminal resized
+ w = term.getSize()
+ redraw()
+ end
+ end
+
+ local _, cy = term.getCursorPos()
+ term.setCursorBlink(false)
+ term.setCursorPos(w + 1, cy)
+ print()
+
+ return sLine
+end \ No newline at end of file
diff --git a/.mbs/lib/scroll_window.lua b/.mbs/lib/scroll_window.lua
new file mode 100644
index 0000000..75a6fb1
--- /dev/null
+++ b/.mbs/lib/scroll_window.lua
@@ -0,0 +1,459 @@
+local type = type
+
+local colour_lookup = {}
+for i = 0, 16 do
+ colour_lookup[2 ^ i] = string.format("%x", i)
+end
+
+function create(original)
+ if not original then original = term.current() end
+
+ local text = {}
+ local text_colour = {}
+ local back_colour = {}
+ local palette = {}
+
+ local cursor_x, cursor_y = 1, 1
+
+ local scroll_offset = 0
+ local scroll_cursor_y = cursor_y
+
+ local cursor_blink = false
+ local cur_text_colour = "0"
+ local cur_back_colour = "f"
+
+ local sizeX, sizeY = original.getSize()
+ local color = original.isColor()
+
+ local max_scrollback = 100
+ local bubble, delegate = true, nil
+
+ local cursor_threshold = 0
+
+ local redirect = {}
+
+ if original.getPaletteColour then
+ for i = 0, 15 do
+ local c = 2 ^ i
+ palette[c] = { original.getPaletteColour(c) }
+ end
+ end
+
+ local function trim()
+ if max_scrollback > -1 then
+ while scroll_offset > max_scrollback do
+ table.remove(text, 1)
+ table.remove(text_colour, 1)
+ table.remove(back_colour, 1)
+ scroll_offset = scroll_offset - 1
+ end
+ end
+ scroll_cursor_y = scroll_offset + cursor_y
+ end
+
+ function redirect.write(writeText)
+ if delegate then return delegate.write(writeText) end
+
+ writeText = tostring(writeText)
+ if bubble then original.write(writeText) end
+
+ local pos = cursor_x
+
+ -- If we're off the screen then just emulate a write
+ if cursor_y > sizeY or cursor_y < 1 then
+ cursor_x = pos + #writeText
+ return
+ end
+
+ if pos + #writeText <= 1 or pos > sizeX then
+ -- If we're too far off the left then skip.
+ cursor_x = pos + #writeText
+ return
+ elseif pos < 1 then
+ -- Adjust text to fit on screen starting at one.
+ writeText = string.sub(writeText, -pos + 2)
+ pos = 1
+ end
+
+ local lineText = text[scroll_cursor_y]
+ local lineColor = text_colour[scroll_cursor_y]
+ local lineBack = back_colour[scroll_cursor_y]
+ local preStop = pos - 1
+ local preStart = math.min(1, preStop)
+ local postStart = pos + string.len(writeText)
+ local postStop = sizeX
+ local sub, rep = string.sub, string.rep
+
+ text[scroll_cursor_y] = sub(lineText, preStart, preStop)..writeText..sub(lineText, postStart, postStop)
+ text_colour[scroll_cursor_y] = sub(lineColor, preStart, preStop)..rep(cur_text_colour, #writeText)..sub(lineColor, postStart, postStop)
+ back_colour[scroll_cursor_y] = sub(lineBack, preStart, preStop)..rep(cur_back_colour, #writeText)..sub(lineBack, postStart, postStop)
+ cursor_x = pos + string.len(writeText)
+ end
+
+ function redirect.blit(writeText, writeFore, writeBack)
+ if delegate then return delegate.blit(writeText, writeFore, writeBack) end
+
+ if type(writeText) ~= "string" then error("bad argument #1 (expected string, got " .. type(writeText) .. ")", 2) end
+ if type(writeFore) ~= "string" then error("bad argument #2 (expected string, got " .. type(writeFore) .. ")", 2) end
+ if type(writeBack) ~= "string" then error("bad argument #3 (expected string, got " .. type(writeBack) .. ")", 2) end
+ if #writeFore ~= #writeText or #writeBack ~= #writeText then error("Arguments must be the same length", 2) end
+
+ if bubble then original.blit(writeText, writeFore, writeBack) end
+
+ local pos = cursor_x
+
+ -- If we're off the screen then just emulate a write
+ if cursor_y > sizeY or cursor_y < 1 then
+ cursor_x = pos + #writeText
+ return
+ end
+
+ if pos + #writeText <= 1 then
+ --skip entirely.
+ cursor_x = pos + #writeText
+ return
+ elseif pos < 1 then
+ --adjust text to fit on screen starting at one.
+ writeText = string.sub(writeText, math.abs(cursor_x) + 2)
+ writeFore = string.sub(writeFore, math.abs(cursor_x) + 2)
+ writeBack = string.sub(writeBack, math.abs(cursor_x) + 2)
+ cursor_x = 1
+ elseif pos > sizeX then
+ --if we're off the edge to the right, skip entirely.
+ cursor_x = pos + #writeText
+ return
+ else
+ writeText = writeText
+ end
+
+ local lineText = text[scroll_cursor_y]
+ local lineColor = text_colour[scroll_cursor_y]
+ local lineBack = back_colour[scroll_cursor_y]
+ local preStop = cursor_x - 1
+ local preStart = math.min(1, preStop)
+ local postStart = cursor_x + string.len(writeText)
+ local postStop = sizeX
+ local sub = string.sub
+
+ text[scroll_cursor_y] = sub(lineText, preStart, preStop)..writeText..sub(lineText, postStart, postStop)
+ text_colour[scroll_cursor_y] = sub(lineColor, preStart, preStop)..writeFore..sub(lineColor, postStart, postStop)
+ back_colour[scroll_cursor_y] = sub(lineBack, preStart, preStop)..writeBack..sub(lineBack, postStart, postStop)
+ cursor_x = pos + #writeText
+ end
+
+ function redirect.clear()
+ if delegate then return delegate.clear() end
+
+ if cursor_threshold > 0 then
+ return redirect.beginPrivateMode().clear()
+ end
+
+ local text_line = (" "):rep(sizeX)
+ local fore_line = (cur_text_colour):rep(sizeX)
+ local back_line = (cur_back_colour):rep(sizeX)
+
+ for i = scroll_offset + 1, sizeY + scroll_offset do
+ text[i] = text_line
+ text_colour[i] = fore_line
+ back_colour[i] = back_line
+ end
+
+ if bubble then return original.clear() end
+ end
+ function redirect.clearLine()
+ if delegate then return delegate.clearLine() end
+
+ -- If we're off the screen then just emulate a clearLine
+ if cursor_y > sizeY or cursor_y < 1 then
+ return
+ end
+
+ text[scroll_cursor_y] = string.rep(" ", sizeX)
+ text_colour[scroll_cursor_y] = string.rep(cur_text_colour, sizeX)
+ back_colour[scroll_cursor_y] = string.rep(cur_back_colour, sizeX)
+
+ if bubble then return original.clearLine() end
+ end
+
+ function redirect.getCursorPos()
+ if delegate then return delegate.getCursorPos() end
+ return cursor_x, cursor_y
+ end
+
+ function redirect.setCursorPos(x, y)
+ if delegate then return delegate.setCursorPos(x, y) end
+
+ if type(x) ~= "number" then error("bad argument #1 (expected number, got " .. type(x) .. ")", 2) end
+ if type(y) ~= "number" then error("bad argument #2 (expected number, got " .. type(y) .. ")", 2) end
+
+ local new_y = math.floor(y)
+ if new_y >= 1 and new_y < cursor_threshold then
+ -- If we're writing within a protected region then start a private buffer
+ return redirect.beginPrivateMode().setCursorPos(x, y)
+ end
+
+ cursor_x = math.floor(x)
+ cursor_y = new_y
+ scroll_cursor_y = new_y + scroll_offset
+
+ if bubble then return original.setCursorPos(x, y) end
+ end
+
+ function redirect.setCursorBlink(b)
+ if delegate then return delegate.setCursorBlink(b) end
+
+ if type(b) ~= "boolean" then error("bad argument #1 (expected boolean, got " .. type(b) .. ")", 2) end
+
+ cursor_blink = b
+ if bubble then return original.setCursorBlink(b) end
+ end
+
+ function redirect.getSize()
+ if delegate then return delegate.getSize() end
+
+ return sizeX, sizeY
+ end
+
+ function redirect.scroll(n)
+ if delegate then return delegate.scroll(n) end
+
+ if type(n) ~= "number" then error("bad argument #1 (expected number, got " .. type(n) .. ")", 2) end
+
+ if n > 0 then
+ scroll_offset = scroll_offset + n
+ for i = sizeY + scroll_offset - n + 1, sizeY + scroll_offset do
+ text[i] = string.rep(" ", sizeX)
+ text_colour[i] = string.rep(cur_text_colour, sizeX)
+ back_colour[i] = string.rep(cur_back_colour, sizeX)
+ end
+
+ trim()
+ elseif n < 0 then
+ for i = sizeY + scroll_cursor_y, math.abs(n) + 1 + scroll_cursor_y, -1 do
+ if text[i + n] then
+ text[i] = text[i + n]
+ text_colour[i] = text_colour[i + n]
+ back_colour[i] = back_colour[i + n]
+ end
+ end
+
+ for i = scroll_cursor_y, math.abs(n) + scroll_cursor_y do
+ text[i] = string.rep(" ", sizeX)
+ text_colour[i] = string.rep(cur_text_colour, sizeX)
+ back_colour[i] = string.rep(cur_back_colour, sizeX)
+ end
+ end
+
+ cursor_threshold = cursor_threshold - n
+
+ if bubble then return original.scroll(n) end
+ end
+
+ function redirect.setTextColour(clr)
+ if delegate then return delegate.setTextColour(clr) end
+
+ if type(clr) ~= "number" then error("bad argument #1 (expected number, got " .. type(clr) .. ")", 2) end
+ cur_text_colour = colour_lookup[clr] or error("Invalid colour (got " .. clr .. ")" , 2)
+ if bubble then return original.setTextColour(clr) end
+ end
+ redirect.setTextColor = redirect.setTextColour
+
+ function redirect.setBackgroundColour(clr)
+ if delegate then return delegate.setBackgroundColour(clr) end
+
+ if type(clr) ~= "number" then error("bad argument #1 (expected number, got " .. type(clr) .. ")", 2) end
+ cur_back_colour = colour_lookup[clr] or error("Invalid colour (got " .. clr .. ")" , 2)
+ if bubble then return original.setBackgroundColour(clr) end
+ end
+ redirect.setBackgroundColor = redirect.setBackgroundColour
+
+ function redirect.isColour()
+ if delegate then return delegate.isColour() end
+ return color == true
+ end
+ redirect.isColor = redirect.isColour
+
+ function redirect.getTextColour()
+ if delegate then return delegate.getTextColour() end
+ return 2 ^ tonumber(cur_text_colour, 16)
+ end
+ redirect.getTextColor = redirect.getTextColour
+
+ function redirect.getBackgroundColour()
+ if delegate then return delegate.getBackgroundColour() end
+ return 2 ^ tonumber(cur_back_colour, 16)
+ end
+ redirect.getBackgroundColor = redirect.getBackgroundColour
+
+ if original.getPaletteColour then
+ function redirect.setPaletteColour(colour, r, g, b)
+ if delegate then return delegate.setPaletteColour(colour, r, g, b) end
+
+ local palcol = palette[colour]
+ if not palcol then error("Invalid colour (got " .. tostring(colour) .. ")", 2) end
+ if type(r) == "number" and g == nil and b == nil then
+ palcol[1], palcol[2], palcol[3] = colours.rgb8(r)
+ else
+ if type(r) ~= "number" then error("bad argument #2 (expected number, got " .. type(r) .. ")", 2) end
+ if type(g) ~= "number" then error("bad argument #3 (expected number, got " .. type(g) .. ")", 2) end
+ if type(b) ~= "number" then error("bad argument #4 (expected number, got " .. type(b) .. ")", 2) end
+
+ palcol[1], palcol[2], palcol[3] = r, g, b
+ end
+
+ if bubble then return original.setPaletteColour(colour, r, g, b) end
+ end
+ redirect.setPaletteColor = redirect.setPaletteColour
+
+ function redirect.getPaletteColour(colour)
+ if delegate then return delegate.getPaletteColour(colour) end
+
+ local palcol = palette[colour]
+ if not palcol then error("Invalid colour (got " .. tostring(colour) .. ")", 2) end
+ return palcol[1], palcol[2], palcol[3]
+ end
+ redirect.getPaletteColor = redirect.getPaletteColour
+ end
+
+ function redirect.draw(offset, clear)
+ if delegate then return end
+
+ if original.getPaletteColour then
+ for colour, pal in pairs(palette) do
+ original.setPaletteColour(colour, pal[1], pal[2], pal[3])
+ end
+ end
+
+ original.setTextColour(2 ^ tonumber(cur_text_colour, 16))
+ original.setBackgroundColor(2 ^ tonumber(cur_back_colour, 16))
+ if clear then original.clear() end
+
+ local original = original
+ local scroll_offset = scroll_offset + (offset or 0)
+ for i = 1, sizeY do
+ original.setCursorPos(1,i)
+ local yOffset = scroll_offset + i
+ original.blit(text[yOffset], text_colour[yOffset], back_colour[yOffset])
+ end
+
+ original.setCursorPos(cursor_x, cursor_y - offset)
+ original.setCursorBlink(cursor_blink)
+ end
+
+ function redirect.bubble(b)
+ bubble = b
+ end
+
+ function redirect.setCursorThreshold(y)
+ cursor_threshold = y
+ end
+
+ function redirect.endPrivateMode(redraw)
+ if delegate then
+ local old_delegate = delegate
+ delegate = nil
+ redirect.draw(0)
+
+ -- If we should redraw the old buffer then blit it to the canvas
+ if redraw then
+ if cursor_threshold > 0 then
+ redirect.scroll(cursor_threshold)
+ end
+
+ old_delegate.draw(redirect)
+ end
+ end
+ end
+
+ function redirect.beginPrivateMode()
+ if not delegate then
+ delegate = blit_window.create(original)
+
+ for y = 1, sizeY do
+ delegate.setCursorPos(1, y)
+ delegate.blit(text[y + scroll_offset], text_colour[y + scroll_offset], back_colour[y + scroll_offset])
+ end
+
+ delegate.setCursorPos(cursor_x, cursor_y)
+ delegate.setCursorBlink(cursor_blink)
+ delegate.setTextColour(2 ^ tonumber(cur_text_colour, 16))
+ delegate.setBackgroundColor(2 ^ tonumber(cur_back_colour, 16))
+
+ if original.getPaletteColour then
+ for i = 0, 15 do
+ local palcol = palette[2 ^ i]
+ delegate.setPaletteColour(2 ^ i, palcol[1], palcol[2], palcol[3])
+ end
+ end
+ end
+
+ return delegate
+ end
+
+ function redirect.isPrivateMode()
+ return delegate ~= nil
+ end
+
+ function redirect.getTotalHeight() return scroll_offset end
+
+ function redirect.setMaxScrollback(n)
+ local old_scrollback = max_scrollback
+ max_scrollback = n
+
+ if old_scrollback > max_scrollback then trim() end
+ end
+
+ function redirect.updateSize()
+ -- Update the delegate window.
+ if delegate then delegate.updateSize() end
+
+ -- If nothing has changed then just skip.
+ local new_x, new_y = original.getSize()
+ if new_x == sizeX and new_y == sizeY then return end
+
+ -- If we have an insufficient number of lines then add some in.
+ local total_height = #text
+
+ -- For any existing lines, trim them
+ for y = 1, total_height do
+ if new_x < sizeX then
+ text[y] = text[y]:sub(1, new_x)
+ text_colour[y] = text_colour[y]:sub(1, new_x)
+ back_colour[y] = back_colour[y]:sub(1, new_x)
+ elseif new_x > sizeX then
+ text[y] = text[y] .. (" "):rep(new_x - sizeX)
+ text_colour[y] = text_colour[y] .. (cur_text_colour):rep(new_x - sizeX)
+ back_colour[y] = back_colour[y] .. (cur_back_colour):rep(new_x - sizeX)
+ end
+ end
+
+ if new_y > sizeY then
+ -- Append any new lines we might need.
+ local text_line = (" "):rep(new_x)
+ local fore_line = (cur_text_colour):rep(new_x)
+ local back_line = (cur_back_colour):rep(new_x)
+ for y = total_height + 1, new_y do
+ text[y] = text_line
+ text_colour[y] = fore_line
+ back_colour[y] = back_line
+ end
+ elseif new_y < sizeY then
+ -- Move the cursor "up" the screen, as we're going to scroll the rest of
+ -- the terminal up.
+ -- Note, this is a little ugly (we lose the top of the screen even if we)
+ -- don't need to, but it's the best we can do for now.
+ cursor_y = cursor_y - sizeY + new_y
+ end
+
+ sizeX = new_x
+ sizeY = new_y
+
+ -- Update the scroll offset. For now we just go back to the bottom
+ scroll_offset = #text - sizeY
+ scroll_cursor_y = scroll_offset + cursor_y
+ trim()
+ end
+
+ redirect.clear()
+ return redirect
+end \ No newline at end of file
diff --git a/.mbs/lib/stack_trace.lua b/.mbs/lib/stack_trace.lua
new file mode 100644
index 0000000..7674813
--- /dev/null
+++ b/.mbs/lib/stack_trace.lua
@@ -0,0 +1,88 @@
+local type = type
+local debug_traceback = type(debug) == "table" and type(debug.traceback) == "function" and debug.traceback
+
+local function traceback(x)
+ -- Attempt to detect error() and error("xyz", 0).
+ -- This probably means they're erroring the program intentionally and so we
+ -- shouldn't display anything.
+ if x == nil or (type(x) == "string" and not x:find(":%d+:")) then
+ return x
+ end
+
+ if debug_traceback then
+ -- The parens are important, as they prevent a tail call occuring, meaning
+ -- the stack level is preserved. This ensures the code behaves identically
+ -- on LuaJ and PUC Lua.
+ return (debug_traceback(tostring(x), 2))
+ else
+ local level = 3
+ local out = { tostring(x), "stack traceback:" }
+ while true do
+ local _, msg = pcall(error, "", level)
+ if msg == "" then break end
+
+ out[#out + 1] = " " .. msg
+ level = level + 1
+ end
+
+ return table.concat(out, "\n")
+ end
+end
+
+local function trim_traceback(target, marker)
+ local ttarget, tmarker = {}, {}
+ for line in target:gmatch("([^\n]*)\n?") do ttarget[#ttarget + 1] = line end
+ for line in marker:gmatch("([^\n]*)\n?") do tmarker[#tmarker + 1] = line end
+
+ -- Trim identical suffixes
+ local t_len, m_len = #ttarget, #tmarker
+ while t_len >= 3 and ttarget[t_len] == tmarker[m_len] do
+ table.remove(ttarget, t_len)
+ t_len, m_len = t_len - 1, m_len - 1
+ end
+
+ -- Trim elements from this file and xpcall invocations
+ while t_len >= 1 and ttarget[t_len]:find("^\tstack_trace%.lua:%d+:") or
+ ttarget[t_len] == "\t[C]: in function 'xpcall'" or ttarget[t_len] == " xpcall: " do
+ table.remove(ttarget, t_len)
+ t_len = t_len - 1
+ end
+
+ return ttarget
+end
+
+--- Run a function with
+local function xpcall_with(fn)
+ -- So this is rather grim: we need to get the full traceback and current one and remove
+ -- the common prefix
+ local trace
+ local res = table.pack(xpcall(fn, traceback)) if not res[1] then trace = traceback("trace.lua:1:") end
+ local ok, err = res[1], res[2]
+
+ if not ok and err ~= nil then
+ trace = trim_traceback(err, trace)
+
+ -- Find the position where the stack traceback actually starts
+ local trace_starts
+ for i = #trace, 1, -1 do
+ if trace[i] == "stack traceback:" then trace_starts = i; break end
+ end
+
+ -- If this traceback is more than 15 elements long, keep the first 9, last 5
+ -- and put an ellipsis between the rest
+ local max = 15
+ if trace_starts and #trace - trace_starts > max then
+ local keep_starts = trace_starts + 10
+ for i = #trace - trace_starts - max, 0, -1 do table.remove(trace, keep_starts + i) end
+ table.insert(trace, keep_starts, " ...")
+ end
+
+ return false, table.concat(trace, "\n")
+ end
+
+ return table.unpack(res, 1, res.n)
+end
+
+_ENV.traceback = traceback
+_ENV.trim_traceback = trim_traceback
+_ENV.xpcall_with = xpcall_with \ No newline at end of file
diff --git a/.mbs/modules/lua.lua b/.mbs/modules/lua.lua
new file mode 100644
index 0000000..d9453e6
--- /dev/null
+++ b/.mbs/modules/lua.lua
@@ -0,0 +1,58 @@
+local function lib_load(path, name)
+ if not _G[name] then
+ os.loadAPI(fs.combine(path, "lib/" .. name .. ".lua"))
+ if not _G[name] then _G[name] = _G[name .. ".lua"] end
+ end
+end
+
+return {
+ description = "Replaces the Lua REPL with an advanced version.",
+
+ dependencies = {
+ "bin/lua.lua",
+ "lib/stack_trace.lua"
+ },
+
+ -- When updating the defaults, one should also update bin/lua.lua
+ settings = {
+ {
+ name = "mbs.lua.enabled",
+ description = "Whether the extended Lua REPL is enabled.",
+ default = true,
+ },
+ {
+ name = "mbs.lua.history_file",
+ description = "The file to save history to. Set to false to disable.",
+ default = ".lua_history",
+ },
+ {
+ name = "mbs.lua.history_max",
+ description = "The maximum size of the history file",
+ default = 1e4,
+ },
+ {
+ name = "mbs.lua.traceback",
+ description = "Show an error traceback when an input errors",
+ default = true,
+ },
+ {
+ name= "mbs.lua.pretty_height",
+ description = "The height to fit the pretty-printer output to. Set to "
+ .. "false to disable, true to use the terminal height or a number for a constant height.",
+ default = true,
+ },
+ {
+ name= "mbs.lua.highlight",
+ description = "Whether to apply syntax highlighting to the REPL's input.",
+ default = true,
+ },
+ },
+
+ enabled = function() return settings.get("mbs.lua.enabled") end,
+
+ setup = function(path)
+ lib_load(path, "stack_trace")
+
+ shell.setAlias("lua", "/" .. fs.combine(path, "bin/lua.lua"))
+ end
+} \ No newline at end of file
diff --git a/.mbs/modules/pager.lua b/.mbs/modules/pager.lua
new file mode 100644
index 0000000..7c25afb
--- /dev/null
+++ b/.mbs/modules/pager.lua
@@ -0,0 +1,48 @@
+return {
+ description = "Replaces the textutils pagers with something akin to less",
+
+ dependencies = {
+ "bin/help.lua"
+ },
+
+ settings = {
+ {
+ name = "mbs.pager.enabled",
+ description = "Whether the alternative pager is enabled.",
+ default = true,
+ },
+ {
+ name = "mbs.pager.mode",
+ description = "The mode for the alternative pager.",
+ default = "default",
+ }
+ },
+
+ enabled = function() return settings.get("mbs.pager.enabled") end,
+
+ setup = function(path)
+ shell.setAlias("help", "/" .. fs.combine(path, "bin/help.lua"))
+ shell.setCompletionFunction(fs.combine(path, "bin/help.lua"), function(shell, index, text, previous)
+ if index == 1 then return help.completeTopic(text) end
+ end)
+
+ local native_pprint, native_ptabulate = textutils.pagedPrint, textutils.pagedTabulate
+ textutils.pagedPrint = function(text, free_lines)
+ local mode = settings.get("mbs.pager.mode")
+ if mode == "none" then
+ return io.write(text .. "\n")
+ else
+ return native_pprint(text, free_lines)
+ end
+ end
+
+ textutils.pagedTabulate = function(...)
+ local mode = settings.get("mbs.pager.mode")
+ if mode == "none" then
+ return textutils.tabulate(...)
+ else
+ return native_ptabulate(...)
+ end
+ end
+ end
+} \ No newline at end of file
diff --git a/.mbs/modules/readline.lua b/.mbs/modules/readline.lua
new file mode 100644
index 0000000..4dea9eb
--- /dev/null
+++ b/.mbs/modules/readline.lua
@@ -0,0 +1,66 @@
+--- Additional readline
+
+local function lib_load(path, name)
+ if not _G[name] then
+ os.loadAPI(fs.combine(path, "lib/" .. name .. ".lua"))
+ if not _G[name] then _G[name] = _G[name .. ".lua"] end
+ end
+end
+
+return {
+ description =
+ "This module extends the default read function, adding keybindings similar to " ..
+ "those provided by Emacs or GNU readline as well as additional configuration options.",
+
+ dependencies = {
+ "lib/readline.lua",
+ },
+
+ settings = {
+ {
+ name = "mbs.readline.enabled",
+ description = "Whether the readline module is enabled.",
+ default = true,
+ },
+ {
+ name = "mbs.readline.complete_bg",
+ description = "The background colour for completions.",
+ default = "none",
+ },
+ {
+ name = "mbs.readline.complete_fg",
+ description = "The foreground colour for completions.",
+ default = "grey",
+ }
+ },
+
+ enabled = function() return settings.get("mbs.readline.enabled") end,
+
+ setup = function(path)
+ lib_load(path, "readline")
+
+ -- Replace the default read function
+ _G.read = function(replace_char, history, complete, default)
+ if replace_char ~= nil and type(replace_char) ~= "string" then
+ error("bad argument #1 (expected string, got " .. type(replace_char) .. ")", 2)
+ end
+ if history ~= nil and type(history) ~= "table" then
+ error("bad argument #2 (expected table, got " .. type(history) .. ")", 2)
+ end
+ if complete ~= nil and type(complete) ~= "function" then
+ error("bad argument #3 (expected function, got " .. type(complete) .. ")", 2)
+ end
+ if default ~= nil and type(default) ~= "string" then
+ error("bad argument #4 (expected string, got " .. type(default) .. ")", 2)
+ end
+
+ return readline.read {
+ replace_char = replace_char,
+ history = history,
+ complete = complete,
+ default = default,
+ }
+ end
+
+ end,
+} \ No newline at end of file
diff --git a/.mbs/modules/shell.lua b/.mbs/modules/shell.lua
new file mode 100644
index 0000000..035322d
--- /dev/null
+++ b/.mbs/modules/shell.lua
@@ -0,0 +1,81 @@
+local function lib_load(path, name)
+ if not _G[name] then
+ os.loadAPI(fs.combine(path, "lib/" .. name .. ".lua"))
+ if not _G[name] then _G[name] = _G[name .. ".lua"] end
+ end
+end
+
+return {
+ description = "Replaces the shell with an advanced version.",
+
+ dependencies = {
+ "bin/clear.lua",
+ "bin/shell.lua",
+ "lib/blit_window.lua",
+ "lib/scroll_window.lua",
+ "lib/stack_trace.lua",
+ },
+
+ -- When updating the defaults, one should also update bin/shell.lua
+ settings = {
+ {
+ name = "mbs.shell.enabled",
+ description = "Whether the extended shell is enabled.",
+ default = true,
+ },
+ {
+ name = "mbs.shell.history_file",
+ description = "The file to save history to. Set to false to disable.",
+ default = ".shell_history",
+ },
+ {
+ name = "mbs.shell.history_max",
+ description = "The maximum size of the history file",
+ default = 1e4,
+ },
+ {
+ name = "mbs.shell.scroll_max",
+ description = "The maximum size of the scrollback",
+ default = 1e3,
+ },
+ {
+ name = "mbs.shell.traceback",
+ description = "Show an error traceback when a program errors",
+ default = true,
+ },
+ {
+ name = "mbs.shell.require_path",
+ description = "The path from that require will use by default. Set to false to use the CraftOS default.",
+ default = false,
+ },
+ {
+ name = "mbs.shell.strict_globals",
+ description = "When set to true the shell will throw errors when programs attempt to define new globals in their environment. If you really want globals then you should use _G instead.",
+ default = false,
+ },
+ },
+
+ enabled = function() return settings.get("mbs.shell.enabled") end,
+
+ setup = function(path)
+ lib_load(path, "scroll_window")
+ lib_load(path, "blit_window")
+ lib_load(path, "stack_trace")
+
+ shell.setAlias("shell", "/" .. fs.combine(path, "bin/shell.lua"))
+ shell.setAlias("clear", "/" .. fs.combine(path, "bin/clear.lua"))
+
+ shell.setCompletionFunction(fs.combine(path, "bin/shell.lua"), function(shell, index, text, previous)
+ if index == 1 then return shell.completeProgram(text) end
+ end)
+ end,
+
+ startup = function(path)
+ local fn, err = loadfile(fs.combine(path, "bin/shell.lua"), _ENV)
+ if not fn then error(err) end
+
+ fn()
+
+ shell.exit()
+ end,
+} \ No newline at end of file
diff --git a/README.md b/README.md
index bc0a5ea..ba508fd 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ The programs/apis I used for bits-UI.
- FireWolf by GravityScore and 1lann
- Mouse File Browser by Stiepen irc(Kilobyte), Cruor and BigSHinyToys
- Enchat 3 by LDDestroier
+- Mildly Better Shell by SquidDev
- fLib by NDFJay
- Sha256 by Anavrins
- JSON API by ElvishJerricco
diff --git a/boot/bubl.cfg b/boot/bubl.cfg
deleted file mode 100644
index 0c3582e..0000000
--- a/boot/bubl.cfg
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "bitsui": "/system/boot.lua"
-} \ No newline at end of file
diff --git a/startup b/startup
deleted file mode 100644
index 452ac9a..0000000
--- a/startup
+++ /dev/null
@@ -1 +0,0 @@
-shell.run("startup.lua") -- It redirects to the startup lua file if it's running a older version of CraftOS.
diff --git a/startup.lua b/startup.lua
index 424850e..8456680 100644
--- a/startup.lua
+++ b/startup.lua
@@ -34,7 +34,7 @@ function bootloader()
term.setCursorPos(1,6)
print("3. Recovery Mode\n")
term.setCursorPos(1,7)
- print("4. Boot CraftOS\n")
+ print("4. Boot CraftOS with MBS\n")
term.setCursorPos(1,9)
term.write("> ")
end
@@ -84,8 +84,10 @@ function bootloaderInput()
shell.run("/system/recovery/main.lua")
elseif input == "4" then
clear()
+ sleep(1)
+ assert(loadfile("/.mbs/bin/mbs.lua", _ENV))('startup')
term.setTextColor(16)
- print(os.version())
+ print(os.version() .. " (+MBS)")
term.setCursorPos(1,2)
term.setTextColor(1)
else
@@ -99,22 +101,12 @@ function bootloaderInput()
end
clear()
print("Welcome to BUBL!")
-if fs.exists(bublcfg) then
- sleep(1)
- if fs.exists("/.git") then
- devMode = true
- else
- devMode = false
- end
- clear()
- bootloader()
- bootloaderInput()
+sleep(1)
+if fs.exists("/.git") then
+ devMode = true
else
- clear()
- term.setTextColor(colors.red)
- print("[ERROR] System cannot find bubl.cfg...")
- term.setCursorPos(1,2)
- print("System halted...")
- sleep(2)
- os.shutdown()
+ devMode = false
end
+clear()
+bootloader()
+bootloaderInput() \ No newline at end of file