From 824685fc074bc84ce5700ceac4f661ee80d689b8 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Thu, 5 Dec 2019 00:18:55 -0500 Subject: [PATCH 01/90] speakerView fix --- milo/plugins/speakerView.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/milo/plugins/speakerView.lua b/milo/plugins/speakerView.lua index 2d79952..c5668dc 100644 --- a/milo/plugins/speakerView.lua +++ b/milo/plugins/speakerView.lua @@ -39,7 +39,7 @@ local wizardPage = UI.WizardPage { function wizardPage:setNode(node) self.form:setValues(node) if not node.volume then - self.form.volume = 1 + self.form.volume.value = 1 end end -- 2.49.1 From a2c8f0c65509c154ae75be976297648267425b5b Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Thu, 5 Dec 2019 11:37:31 -0700 Subject: [PATCH 02/90] cash alternative shell added --- cash/.package | 30 ++++++++++++++++++++++++++++++ cash/etc/apps.db | 7 +++++++ cash/etc/fstab | 1 + common/etc/fstab | 4 +++- 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 cash/.package create mode 100644 cash/etc/apps.db create mode 100644 cash/etc/fstab diff --git a/cash/.package b/cash/.package new file mode 100644 index 0000000..b7a2cec --- /dev/null +++ b/cash/.package @@ -0,0 +1,30 @@ +{ + title = 'ComputerCraft Advanced Shell', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/cash', + description = [[A Bourne-compatible shell for ComputerCraft. + +Features +Bash/sh-style command line +Tab completion (defaulting to file names where not supported) +Customizable prompts (including ANSI support) +Local & environment variables +Argument quoting +Multiple commands on one line with semicolons +Many built-in functions (including in-line Lua commands) +Arithmetic expansion +If, while, for statements +Function support +Shell scripting/shebangs +Background jobs +rc files +Restorable history +Partial CCKernel2 support +Full compatibility with CraftOS shell.lua]], + license = 'MIT', + install = [[ + require('opus.alternate').set('shell', 'packages/cash/cash.lua') + ]], + uninstall = [[ + require('opus.alternate').remove('shell', 'packages/cash/cash.lua') + ]], +} diff --git a/cash/etc/apps.db b/cash/etc/apps.db new file mode 100644 index 0000000..2b13ce3 --- /dev/null +++ b/cash/etc/apps.db @@ -0,0 +1,7 @@ +{ + [ "a3c2cb0e2228250d98c481128f6837ef93111d73" ] = { + title = "MBS", + category = "Apps", + run = ".mbs/bin/shell.lua", + }, +} diff --git a/cash/etc/fstab b/cash/etc/fstab new file mode 100644 index 0000000..37fb07c --- /dev/null +++ b/cash/etc/fstab @@ -0,0 +1 @@ +packages/cash/cash.lua urlfs https://raw.githubusercontent.com/MCJack123/cash/master/cash.lua \ No newline at end of file diff --git a/common/etc/fstab b/common/etc/fstab index eedfbf8..e636640 100644 --- a/common/etc/fstab +++ b/common/etc/fstab @@ -1,4 +1,6 @@ packages/common/ascii.lua urlfs http://pastebin.com/raw/u3kcnyjd packages/common/hexedit.lua urlfs https://pastebin.com/raw/Ds9ajsp4 packages/common/colors.lua urlfs https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/ignore/colors.lua -packages/common/cowsay.lua urlfs https://pastebin.com/raw/n00VQJsw \ No newline at end of file +packages/common/cowsay.lua urlfs https://pastebin.com/raw/n00VQJsw +packages/common/calc.lua urlfs https://pastebin.com/raw/nAinUn1h +packages/common/write.lua urlfs https://pastebin.com/raw/RSyhCjqv \ No newline at end of file -- 2.49.1 From e0960bd93073bbd9a53bcae9711429b008e0bc32 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Thu, 5 Dec 2019 11:39:55 -0700 Subject: [PATCH 03/90] cash alternative shell added --- packages.list | 1 + 1 file changed, 1 insertion(+) diff --git a/packages.list b/packages.list index 57b1ce5..092785d 100644 --- a/packages.list +++ b/packages.list @@ -2,6 +2,7 @@ [ 'common' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/common/.package', [ 'core' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/core/.package', [ 'builder' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/builder/.package', + [ 'cash' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/cash/.package', [ 'ccemux' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/ccemux/.package', [ 'farms' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/farms/.package', -- [ 'forestry' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/forestry/.package', -- 2.49.1 From 6831de37a3f2e566380f6ffee99047219232919e Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Thu, 5 Dec 2019 13:27:14 -0700 Subject: [PATCH 04/90] compression read missing --- lzwfs/lzwfs.lua | 9 ++++++++- packages.list | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lzwfs/lzwfs.lua b/lzwfs/lzwfs.lua index 46aa869..382bd7a 100644 --- a/lzwfs/lzwfs.lua +++ b/lzwfs/lzwfs.lua @@ -180,7 +180,14 @@ function fs.open(fname, flags) local ctr = 0 local lines return { - readLine = function() + read = function() + if not lines then + lines = decompress(f.readAll()) + end + ctr = ctr + 1 + return lines:sub(ctr, ctr) + end, + readLine = function() if not lines then lines = split(decompress(f.readAll())) end diff --git a/packages.list b/packages.list index 092785d..eaee2b8 100644 --- a/packages.list +++ b/packages.list @@ -1,9 +1,9 @@ { - [ 'common' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/common/.package', - [ 'core' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/core/.package', [ 'builder' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/builder/.package', [ 'cash' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/cash/.package', [ 'ccemux' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/ccemux/.package', + [ 'common' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/common/.package', + [ 'core' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/core/.package', [ 'farms' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/farms/.package', -- [ 'forestry' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/forestry/.package', [ 'games' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/games/.package', -- 2.49.1 From 7b45348710fc77cdbd70034e7b6bc8e7bb802d47 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Thu, 5 Dec 2019 15:34:20 -0700 Subject: [PATCH 05/90] oops --- cash/etc/apps.db | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cash/etc/apps.db b/cash/etc/apps.db index 2b13ce3..00e39c0 100644 --- a/cash/etc/apps.db +++ b/cash/etc/apps.db @@ -1,7 +1,7 @@ { - [ "a3c2cb0e2228250d98c481128f6837ef93111d73" ] = { - title = "MBS", + [ "16323efd4cb1f1f639d67b94161c2ef37a905517e" ] = { + title = "cash", category = "Apps", - run = ".mbs/bin/shell.lua", + run = "packages/cash/cash.lua", }, } -- 2.49.1 From a85e14e94d653a28adfa77f48edda9d5dd5152a4 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Sat, 7 Dec 2019 22:32:40 -0500 Subject: [PATCH 06/90] ores.lua: Load ore lists from config file and damage value support --- neural/ores.lua | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/neural/ores.lua b/neural/ores.lua index 373dd85..7c15018 100644 --- a/neural/ores.lua +++ b/neural/ores.lua @@ -5,6 +5,8 @@ -- Updated to use new(ish) canvas3d +local Config = require('opus.config') + local gps = _G.gps local keys = _G.keys local os = _G.os @@ -43,7 +45,7 @@ local function getPoint() end end -local targets = { +local targets = Config.load('ores', { ["minecraft:emerald_ore"] = { "minecraft:emerald_ore", 0 }, ["minecraft:diamond_ore"] = { "minecraft:diamond_ore", 0 }, ["minecraft:gold_ore"] = { "minecraft:gold_ore", 0 }, @@ -54,7 +56,8 @@ local targets = { ["minecraft:coal_ore"] = { "minecraft:coal_ore", 0 }, ["minecraft:quartz_ore"] = { "minecraft:quartz_ore", 0 }, ["minecraft:glowstone"] = { "minecraft:glowstone", 0 }, -} +}) + local projecting = { } local offset = getPoint() or showRequirements('GPS') local canvas = modules.canvas3d().create({ @@ -85,7 +88,7 @@ local function update() local blocks = { } for _, b in pairs(scanned) do - if targets[b.name] then + if targets[b.name..(b.metadata ~= 0 and ":"..b.metadata or "")] then -- track block's world position b.id = table.concat({ math.floor(pos.x + b.x), @@ -98,7 +101,8 @@ local function update() for _, b in pairs(blocks) do if not projecting[b.id] then projecting[b.id] = b - local target = targets[b.name] + + local target = targets[b.name..(b.metadata ~= 0 and ":"..b.metadata or "")] local x = b.x - math.floor(offset.x) + math.floor(pos.x) local y = b.y - math.floor(offset.y) + math.floor(pos.y) -- 2.49.1 From 14e6fd57a79945f9213a1889953df54b61f4f6ea Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Sun, 8 Dec 2019 13:50:22 -0700 Subject: [PATCH 07/90] cash tweaks --- cash/.package | 1 + cash/etc/apps.db | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cash/.package b/cash/.package index b7a2cec..93cb0fb 100644 --- a/cash/.package +++ b/cash/.package @@ -23,6 +23,7 @@ Full compatibility with CraftOS shell.lua]], license = 'MIT', install = [[ require('opus.alternate').set('shell', 'packages/cash/cash.lua') + require('opus.util').writeFile('.cashrc', 'set TERMINATE_QUIT=yes') ]], uninstall = [[ require('opus.alternate').remove('shell', 'packages/cash/cash.lua') diff --git a/cash/etc/apps.db b/cash/etc/apps.db index 00e39c0..d58d870 100644 --- a/cash/etc/apps.db +++ b/cash/etc/apps.db @@ -1,6 +1,6 @@ { [ "16323efd4cb1f1f639d67b94161c2ef37a905517e" ] = { - title = "cash", + title = "Cash", category = "Apps", run = "packages/cash/cash.lua", }, -- 2.49.1 From 83452a2be0e4a2c0cb874fe94ec72b9f4f87a4b1 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Sun, 15 Dec 2019 19:12:36 -0700 Subject: [PATCH 08/90] experimental terminal window manager --- ignore/twm.lua | 469 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 469 insertions(+) create mode 100644 ignore/twm.lua diff --git a/ignore/twm.lua b/ignore/twm.lua new file mode 100644 index 0000000..24f908a --- /dev/null +++ b/ignore/twm.lua @@ -0,0 +1,469 @@ +local Terminal = require('opus.terminal') +local trace = require('opus.trace') +local Util = require('opus.util') + +local colors = _G.colors +local os = _G.os +local peripheral = _G.peripheral +local printError = _G.printError +local shell = _ENV.shell +local term = _G.term +local window = _G.window + +local function syntax() + printError('Syntax:') + error('mwm [--config=filename] [monitor]') +end + +local args = Util.parse(...) +local UID = 0 +local multishell = { } +local processes = { } +local parentTerm = term.current() +local sessionFile = args.config or 'usr/config/mwm' +local monName = args[1] +local running +local parentMon + +local defaultEnv = Util.shallowCopy(_ENV) +defaultEnv.multishell = multishell + +if monName == 'terminal' then + parentMon = term.current() +elseif monName then + parentMon = peripheral.wrap(monName) or syntax() +else + parentMon = peripheral.find('monitor') or syntax() +end + +if parentMon.setTextScale then + parentMon.setTextScale(.5) +end + +local monDim, termDim = { }, { } +monDim.width, monDim.height = parentMon.getSize() +termDim.width, termDim.height = parentTerm.getSize() + +-- even though the monitor window is set to visible +-- the canvas is not (possibly change default in terminal.lua) + +-- canvas is not visible so that redraws +-- are done once in the event loop +local monitor = Terminal.window(parentMon, 1, 1, monDim.width, monDim.height, true) +monitor.setBackgroundColor(colors.gray) +monitor.clear() + +local function nextUID() + UID = UID + 1 + return UID +end + +local function xprun(env, path, ...) + setmetatable(env, { __index = _G }) + local fn, m = loadfile(path, env) + if fn then + return trace(fn, ...) + end + return fn, m +end + +local function write(win, x, y, text) + win.setCursorPos(x, y) + win.write(text) +end + +local function redraw() + --monitor.clear() + monitor.canvas:dirty() + --monitor.setBackgroundColor(colors.gray) + monitor.canvas:clear(colors.gray) + for k, process in ipairs(processes) do + process.container.canvas:dirty() + process:focus(k == #processes) + end +end + +local function getProcessAt(x, y) + for k = #processes, 1, -1 do + local process = processes[k] + if x >= process.x and + y >= process.y and + x <= process.x + process.width - 1 and + y <= process.y + process.height - 1 then + return k, process + end + end +end + +--[[ A runnable process ]]-- +local Process = { } + +function Process:new(args) + args.env = args.env or Util.shallowCopy(defaultEnv) + args.width = args.width or math.floor(termDim.width * .75) + args.height = args.height or math.floor(termDim.height * .75) + + -- TODO: randomize start position + local self = setmetatable({ + uid = nextUID(), + x = args.x or 1, + y = args.y or 1, + width = args.width, + height = args.height + 1, + path = args.path, + args = args.args or { }, + title = args.title or 'shell', + isMoving = false, + isResizing = false, + }, { __index = Process }) + + self:adjustDimensions() + if not args.x then + self.x = math.random(1, monDim.width - self.width + 1) + self.y = math.random(1, monDim.height - self.height + 1) + end + + self.container = Terminal.window(monitor, self.x, self.y, self.width, self.height, true) + self.window = window.create(self.container, 1, 2, args.width, args.height, true) + self.terminal = self.window + + self.container.canvas.parent = monitor.canvas + table.insert(monitor.canvas.layers, 1, self.container.canvas) + self.container.canvas:setVisible(true) + + --self.container.getSize = self.window.getSize + + self.co = coroutine.create(function() + local result, err + + if args.fn then + result, err = Util.runFunction(args.env, args.fn, table.unpack(self.args)) + elseif args.path then + result, err = xprun(args.env, args.path, table.unpack(self.args)) + end + + if not result and err and err ~= 'Terminated' then + printError('\n' .. tostring(err)) + os.pullEventRaw('terminate') + end + multishell.removeProcess(self) + end) + + self:focus(false) + + return self +end + +function Process:focus(focused) + if focused then + self.container.setBackgroundColor(colors.yellow) + else + self.container.setBackgroundColor(colors.lightGray) + end + self.container.setTextColor(colors.black) + write(self.container, 1, 1, string.rep(' ', self.width)) + write(self.container, 2, 1, self.title) + write(self.container, self.width - 1, 1, '*') + write(self.container, self.width - 3, 1, '\029') + + if focused then + self.window.restoreCursor() + end +end + +function Process:drawSizers() + self.container.setBackgroundColor(colors.black) + self.container.setTextColor(colors.yellow) + local str = string.format('%d x %d', self.width - 2, self.height - 3) + write(self.container, (self.width - #str) / 2, 1, str) +end + +function Process:adjustDimensions() + self.width = math.min(self.width, monDim.width) + self.height = math.min(self.height, monDim.height) + + self.x = math.max(1, self.x) + self.y = math.max(1, self.y) + self.x = math.min(self.x, monDim.width - self.width + 1) + self.y = math.min(self.y, monDim.height - self.height + 1) +end + +function Process:reposition() + self:adjustDimensions() + self.container.reposition(self.x, self.y, self.width, self.height) + self.container.setBackgroundColor(colors.black) + self.container.clear() + self.window.reposition(1, 2, self.width, self.height - 1) + if self.window ~= self.terminal then + if self.terminal.reposition then -- ?? + self.terminal.reposition(1, 1, self.width, self.height - 1) + end + end + redraw() +end + +function Process:click(x, y, rx, ry) + if y == 1 then -- title bar + if x == self.width - 1 then + self:resume('terminate') + elseif x == self.width - 3 then + self.isResizing = { x = rx, y = ry, h = self.height, w = self.width } + else + self.isMoving = { x = rx, y = ry, ox = self.x, oy = self.y } + end + + else + if self.isMoving then + self.isMoving = false + end + self:resume('mouse_click', 1, x, y - 1) + self:resume('mouse_up', 1, x, y - 1) + end +end + +function Process:resize(x, y) + self.height = y - self.isResizing.y + self.isResizing.h + self.width = x - self.isResizing.x + self.isResizing.w + + self:reposition() + self:resume('term_resize') + self:drawSizers() + multishell.saveSession(sessionFile) +end + +function Process:resume(event, ...) + if coroutine.status(self.co) == 'dead' then + return + end + + if not self.filter or self.filter == event or event == "terminate" then + --term.redirect(self.terminal) + local previousTerm = term.redirect(self.terminal) + + local previous = running + running = self -- stupid shell set title + local ok, result = coroutine.resume(self.co, event, ...) + running = previous + + self.terminal = term.current() + term.redirect(previousTerm) + + if ok then + self.filter = result + else + printError(result) + end + return ok, result + end +end + +--[[ Install a multishell manager for the monitor ]]-- +function multishell.getFocus() + return processes[#processes].uid +end + +function multishell.setFocus(uid) + local process = Util.find(processes, 'uid', uid) + + if process then + local lastFocused = processes[#processes] + if lastFocused ~= process then + + if lastFocused then + lastFocused:focus(false) + end + + Util.removeByValue(processes, process) + table.insert(processes, process) + + process.container.canvas:raise() + process:focus(true) + process.container.canvas:dirty() + end + return true + end + return false +end + +function multishell.getTitle(uid) + local process = Util.find(processes, 'uid', uid) + if process then + return process.title + end +end + +function multishell.setTitle(uid, title) + local process = Util.find(processes, 'uid', uid) + if process then + process.title = title or '' + process:focus(process == processes[#processes]) + end +end + +function multishell.getCurrent() + if running then + return running.uid + end +end + +function multishell.getCount() + return #processes +end + +function multishell.getTabs() + return processes +end + +function multishell.launch(env, file, ...) + return multishell.openTab({ + path = file, + env = env, + title = 'shell', + args = { ... }, + }) +end + +function multishell.openTab(tabInfo) + local process = Process:new(tabInfo) + + table.insert(processes, 1, process) + + --local previousTerm = term.current() + process:resume() + --term.redirect(previousTerm) + + multishell.saveSession(sessionFile) + + return process.uid +end + +function multishell.removeProcess(process) + Util.removeByValue(processes, process) + process.container.canvas:removeLayer() + + multishell.saveSession(sessionFile) + redraw() +end + +function multishell.saveSession(filename) + local t = { } + for _,process in ipairs(processes) do + if process.path then + table.insert(t, { + x = process.x, + y = process.y, + width = process.width, + height = process.height - 1, + path = process.path, + args = process.args, + }) + end + end + Util.writeTable(filename, t) +end + +function multishell.loadSession(filename) + local config = Util.readTable(filename) + if config then + for k = #config, 1, -1 do + multishell.openTab(config[k]) + end + end +end + +function multishell.stop() + multishell._stop = true +end + +function multishell.start() + while not multishell._stop do + + local event = { os.pullEventRaw() } + + if event[1] == 'terminate' then + local focused = processes[#processes] + if focused then + focused:resume('terminate') + if #processes == 0 then + break + end + end + + elseif event[1] == 'monitor_touch' or event[1] == 'mouse_click' then --or event[1] == 'mouse_up' then + local x, y = event[3], event[4] + + local key, process = getProcessAt(x, y) + if process then + if key ~= #processes then + multishell.setFocus(process.uid) + multishell.saveSession(sessionFile) + end + process:click(x - process.x + 1, y - process.y + 1, x, y) + end + + elseif event[1] == 'mouse_up' then + local focused = processes[#processes] + if focused and (focused.isResizing or focused.isMoving) then + multishell.saveSession(sessionFile) + if focused.isResizing then + focused:focus(true) + end + end + if focused then + focused.isResizing = nil + focused.isMoving = false + end + + elseif event[1] == 'mouse_drag' then + local focused = processes[#processes] + if focused then + if focused.isResizing then + focused:resize(event[3], event[4]) + + elseif focused.isMoving then + focused.x = event[3] - focused.isMoving.x + focused.isMoving.ox + focused.y = event[4] - focused.isMoving.y + focused.isMoving.oy + focused:reposition() + end + end + + elseif event[1] == 'char' or + event[1] == 'key' or + event[1] == 'key_up' or + event[1] == 'paste' then + + local focused = processes[#processes] + if focused then + focused:resume(table.unpack(event)) + end + + else + for _,process in pairs(Util.shallowCopy(processes)) do + process:resume(table.unpack(event)) + end + end + + monitor.canvas:render(parentMon) + + local focused = processes[#processes] + if focused then + focused.window.restoreCursor() + end + end +end + +multishell.loadSession(sessionFile) + +if #processes == 0 then + multishell.openTab({ + path = 'sys/apps/shell.lua', + title = 'shell', + }) +end + +processes[#processes]:focus(true) +multishell.start() + +term.redirect(parentTerm) +parentTerm.clear() +parentTerm.setCursorPos(1, 1) -- 2.49.1 From 79c8c4beaec6b35312795da0d31da3171a271af5 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Mon, 16 Dec 2019 18:02:25 -0700 Subject: [PATCH 09/90] builder help update --- builder/help/builder | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/builder/help/builder b/builder/help/builder index f668438..b105f97 100644 --- a/builder/help/builder +++ b/builder/help/builder @@ -27,7 +27,7 @@ Copy a schematic file to the turtle/command computer (see downloading instructio Building ======== Run: -> builder +> builder The first time you run the program, you must select a wrench. Place a wrench into the chest. Go to the supplies list and double-click the SelectAWrench item. Select the wrench and apply. @@ -47,9 +47,13 @@ Simply copy a schematic file into the computer's folder. Multiplayer ----------- -Option 1: Use wget if the schematic is available for download (unreliable). +Option 1: Pass the url of the schematic. -Option 2: Transferring via pastebin (reliable) +> builder + +Option 2: Use wget if the schematic is available for download (unreliable). + +Option 3: Transferring via pastebin (reliable) To create a base64 file from a command line, do: * linux / max: -- 2.49.1 From 3f19c94e0fa4c28dab162695c168171f7618d111 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Fri, 27 Dec 2019 01:18:09 -0500 Subject: [PATCH 10/90] GPS overhaul --- gps/gpsServer.lua | 7 +++---- neural/ores.lua | 11 ++--------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/gps/gpsServer.lua b/gps/gpsServer.lua index 15191e5..5c60926 100644 --- a/gps/gpsServer.lua +++ b/gps/gpsServer.lua @@ -6,7 +6,6 @@ local Event = require('opus.event') local colors = _G.colors local fs = _G.fs -local gps = _G.gps local os = _G.os local peripheral = _G.peripheral local read = _G.read @@ -200,7 +199,7 @@ local function server(mode) error('Modem is not activated or connected: ' .. k) end if mode == 'gps' then - modem.open(gps.CHANNEL_GPS) + modem.open(GPS.CHANNEL_GPS) elseif mode == 'snmp' then modem.open(999) end @@ -254,9 +253,9 @@ local function server(mode) Event.on('modem_message', function(_, side, channel, computerId, message, distance) if distance and modems[side] then - if mode == 'gps' and channel == gps.CHANNEL_GPS and message == "PING" then + if mode == 'gps' and channel == GPS.CHANNEL_GPS and message == "PING" then for _, modem in pairs(modems) do - modem.transmit(computerId, gps.CHANNEL_GPS, { modem.x, modem.y, modem.z }) + modem.transmit(computerId, GPS.CHANNEL_GPS, { modem.x, modem.y, modem.z }) end getPosition(computerId, modems[side], distance) end diff --git a/neural/ores.lua b/neural/ores.lua index 7c15018..9bbf2de 100644 --- a/neural/ores.lua +++ b/neural/ores.lua @@ -6,8 +6,8 @@ -- Updated to use new(ish) canvas3d local Config = require('opus.config') +local GPS = require("opus.gps") -local gps = _G.gps local keys = _G.keys local os = _G.os local parallel = _G.parallel @@ -35,14 +35,7 @@ end local BLOCK_SIZE = .5 local function getPoint() - local pt = { gps.locate() } - if pt[1] then - return { - x = pt[1], - y = pt[2], - z = pt[3], - } - end + return GPS.locate() end local targets = Config.load('ores', { -- 2.49.1 From 62951067aeaf98036c0ec5c94225ccc74d362604 Mon Sep 17 00:00:00 2001 From: LDDestroier Date: Sun, 19 Jan 2020 20:48:41 -0500 Subject: [PATCH 11/90] Updated list URL --- common/Appstore.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/Appstore.lua b/common/Appstore.lua index a94c93f..c489696 100644 --- a/common/Appstore.lua +++ b/common/Appstore.lua @@ -40,7 +40,7 @@ local sources = { { text = "STD Default", event = 'source', - url = "http://pastebin.com/raw/zVws7eLq" }, --stock + url = "https://github.com/LDDestroier/STD-GUI/raw/master/list.lua" }, --stock --[[ { text = "Discover", event = 'source', -- 2.49.1 From 48f142accc1ad233ebeee7152889ad772dd82285 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Wed, 29 Jan 2020 23:14:38 -0500 Subject: [PATCH 12/90] swshop helpfile and updates --- swshop/.package | 3 +++ swshop/Shoplogs.lua | 2 +- swshop/help/swshop | 25 +++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 swshop/help/swshop diff --git a/swshop/.package b/swshop/.package index efb46ed..afeb083 100644 --- a/swshop/.package +++ b/swshop/.package @@ -1,4 +1,7 @@ { + required = { + 'milo', + }, title = 'Switchcraft basic shop', repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/swshop', description = 'Modification of the k store by Lemmmy\nRun installPlugin.lua after install', diff --git a/swshop/Shoplogs.lua b/swshop/Shoplogs.lua index 2eb605e..5766b08 100644 --- a/swshop/Shoplogs.lua +++ b/swshop/Shoplogs.lua @@ -141,7 +141,7 @@ end function page.grid:getDisplayValues(row) row = Util.shallowCopy(row) local x = row.recipient - row.from = x and x:match('(%w+)@') or row.from + row.from = x and x:match('(.+)@') or row.from return row end diff --git a/swshop/help/swshop b/swshop/help/swshop new file mode 100644 index 0000000..5fc1804 --- /dev/null +++ b/swshop/help/swshop @@ -0,0 +1,25 @@ +An item shop software using Krist as its currency. + +Requirements +============ + * Advanced Turtle + * Chests (any type) + * Milo system + +Installation +============ +> pastebin run uzghlbnc +> package install swshop +> reboot + +Setup +===== +From the shell, run the 'installPlugin.lua' once to install the shop component into your Milo installation. + +From Milo, open the setup page and find the monitor you wish to use in the list. + +Choose the monitor's type to be 'Store Front', and configure your domain and your wallet's key. + +Usage +===== +Setup prices by right-clicking on the desired item and going on the 'Shops' tab. -- 2.49.1 From d971448c9a8c426df5713c04e5d8ef4ecd63e105 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Sat, 8 Feb 2020 00:01:36 -0500 Subject: [PATCH 13/90] Fix swshop Fixes a number transform issue which caused swshop to crash without giving the bought item. --- swshop/swshop.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swshop/swshop.lua b/swshop/swshop.lua index 391b6a8..0bb07ab 100644 --- a/swshop/swshop.lua +++ b/swshop/swshop.lua @@ -44,7 +44,7 @@ local function getItemDetails(item) t = textutils.unserialize(t) for key, v in pairs(t) do if v.name == item then - return key, v.price + return key, tonumber(v.price) end end end -- 2.49.1 From cb0c1da77d93aad58e1d0e4f58fbf118bb8f8072 Mon Sep 17 00:00:00 2001 From: bprice Date: Thu, 13 Feb 2020 22:55:12 -0700 Subject: [PATCH 14/90] fix Max Fuel for Creative mode --- miners/simpleMiner.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/miners/simpleMiner.lua b/miners/simpleMiner.lua index 4acd2b4..625f11d 100644 --- a/miners/simpleMiner.lua +++ b/miners/simpleMiner.lua @@ -35,7 +35,12 @@ local fortuneBlocks = { local MIN_FUEL = 7500 local LOW_FUEL = 1500 -local MAX_FUEL = turtle.getFuelLimit() + +if turtle.getFuelLimit() == 'unlimited' then + MAX_FUEL = 100000 +else + MAX_FUEL = turtle.getFuelLimit() +end local PROGRESS_FILE = 'usr/config/mining.progress' local TRASH_FILE = 'usr/config/mining.trash' @@ -380,8 +385,9 @@ local function mineable(action) if block.name == 'minecraft:chest' then collectDrops(action.suck) end - + print(MAX_FUEL) if turtle.getFuelLevel() < (MAX_FUEL - 1000) then + print(MAX_FUEL) if block.name == 'minecraft:lava' or block.name == 'minecraft:flowing_lava' then if turtle.select('minecraft:bucket:0') then if action.place() then -- 2.49.1 From e361e374918f7332f86e01859f0e136ba08a519e Mon Sep 17 00:00:00 2001 From: bprice Date: Thu, 13 Feb 2020 22:58:12 -0700 Subject: [PATCH 15/90] remove print() for last commit --- miners/simpleMiner.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/miners/simpleMiner.lua b/miners/simpleMiner.lua index 625f11d..84807d2 100644 --- a/miners/simpleMiner.lua +++ b/miners/simpleMiner.lua @@ -385,9 +385,7 @@ local function mineable(action) if block.name == 'minecraft:chest' then collectDrops(action.suck) end - print(MAX_FUEL) if turtle.getFuelLevel() < (MAX_FUEL - 1000) then - print(MAX_FUEL) if block.name == 'minecraft:lava' or block.name == 'minecraft:flowing_lava' then if turtle.select('minecraft:bucket:0') then if action.place() then -- 2.49.1 From 301f531a4a48ed528dede03957e7a4db5813688b Mon Sep 17 00:00:00 2001 From: Anavrins Date: Fri, 14 Feb 2020 01:13:43 -0500 Subject: [PATCH 16/90] Fix getFuelLimit and getFuelLevel --- turtle/autorun/6.tl3.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/turtle/autorun/6.tl3.lua b/turtle/autorun/6.tl3.lua index 51db8ec..d6a352b 100644 --- a/turtle/autorun/6.tl3.lua +++ b/turtle/autorun/6.tl3.lua @@ -391,8 +391,9 @@ end if type(turtle.getFuelLevel()) ~= 'number' then -- Support unlimited fuel function turtle.getFuelLevel() - return 100000 + return math.huge end + turtle.getFuelLimit = turtle.getFuelLevel end -- override to optionally specify a fuel -- 2.49.1 From afcbfd1b0427d92debf6f2cf2b4be29a5607c397 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Mon, 24 Feb 2020 21:10:30 -0500 Subject: [PATCH 17/90] Added a "Level Emitter" to Milo --- milo/plugins/emitterTask.lua | 29 +++++++++ milo/plugins/emitterView.lua | 109 ++++++++++++++++++++++++++++++++++ milo/plugins/item/infoTab.lua | 3 + 3 files changed, 141 insertions(+) create mode 100644 milo/plugins/emitterTask.lua create mode 100644 milo/plugins/emitterView.lua diff --git a/milo/plugins/emitterTask.lua b/milo/plugins/emitterTask.lua new file mode 100644 index 0000000..b5088dd --- /dev/null +++ b/milo/plugins/emitterTask.lua @@ -0,0 +1,29 @@ +local itemDB = require('core.itemDB') +local Milo = require('milo') + +local device = _G.device + +local EmitterTask = { + name = 'emitter', + priority = 5, +} + +local function filter(a) + return a.emitter +end + +function EmitterTask:cycle(context) + for node in context.storage:filterActive('emitter', filter) do + local config = node.emitter + local item = Milo:getItem(itemDB:splitKey(config.item)) + + config.signal = not not config.signal + if item and item.count >= config.amount then + device[node.name].setOutput(config.side, config.signal) + else + device[node.name].setOutput(config.side, not config.signal) + end + end +end + +Milo:registerTask(EmitterTask) diff --git a/milo/plugins/emitterView.lua b/milo/plugins/emitterView.lua new file mode 100644 index 0000000..f3c51d3 --- /dev/null +++ b/milo/plugins/emitterView.lua @@ -0,0 +1,109 @@ +local Milo = require('milo') +local UI = require('opus.ui') +local itemDB = require('core.itemDB') + +local colors = _G.colors +local device = _G.device +local context = Milo:getContext() + +local wizardPage = UI.WizardPage { + title = 'Level Emitter', + index = 2, + backgroundColor = colors.cyan, + [1] = UI.TextArea { + x = 2, y = 1, + height = 2, + textColor = colors.yellow, + value = 'Emit a redstone signal if an\nitem amount if over a threshold', + }, + form = UI.Form { + x = 1, ex = -1, y = 3, ey = -1, + manualControls = true, + + itemName = UI.TextEntry { + formLabel = 'Item', formKey = 'item', formIndex = 1, + help = 'Item to monitor', + required = true, + }, + side = UI.Chooser { + formLabel = 'Side', formKey = 'side', formIndex = 2, + width = 10, + choices = { + {name = 'Down', value = 'down'}, + {name = 'Up', value = 'up'}, + {name = 'North', value = 'north'}, + {name = 'South', value = 'south'}, + {name = 'West', value = 'west'}, + {name = 'East', value = 'east'}, + }, + required = true, + }, + amount = UI.TextEntry { + formLabel = 'Amount', formKey = 'amount', formIndex = 3, + width = 7, + transform = 'number', + help = 'Threshold value', + required = true, + }, + signal = UI.Checkbox { + formLabel = 'Signal', formKey = 'signal', formIndex = 4, + help = 'Enable redstone signal when over threshold', + }, + scanItem = UI.Button { + x = 15, y = 6, + text = 'Scan item', event = 'scan_turtle', + help = 'Scan an item from the turtle inventory', + }, + }, +} + +function wizardPage:setNode(node) + self.node = node + if not self.node.emitter then + self.node.emitter = { + signal = { value = true } + } + end + self.form:setValues(self.node.emitter) +end + +function wizardPage:validate() + return self.form:save() +end + +function wizardPage:isValidType(node) + local m = device[node.name] + return m and m.type == 'redstone_integrator' and { + name = 'Level Emitter', + value = 'emitter', + category = 'custom', + help = 'Emit redstone signals', + } +end + +function wizardPage:isValidFor(node) + return node.mtype == 'emitter' +end + +function wizardPage:enable() + Milo:pauseCrafting({ key = 'gridInUse', msg = 'Crafting paused' }) + UI.WizardPage.enable(self) +end +function wizardPage:disable() + Milo:resumeCrafting({ key = 'gridInUse' }) + UI.WizardPage.disable(self) +end + +function wizardPage:eventHandler(event) + if event.type == 'scan_turtle' then + local inventory = Milo:getTurtleInventory() + for _,item in pairs(inventory) do + self.form.itemName.value = itemDB:makeKey(item) + break + end + self:draw() + Milo:emptyInventory() + end +end + +UI:getPage('nodeWizard').wizard:add({ emiter = wizardPage }) diff --git a/milo/plugins/item/infoTab.lua b/milo/plugins/item/infoTab.lua index 7ae99c7..92bb37d 100644 --- a/milo/plugins/item/infoTab.lua +++ b/milo/plugins/item/infoTab.lua @@ -27,6 +27,9 @@ function infoTab:draw() value = value .. item.nbtHash .. '\n' end + value = value .. string.format('\n%sCount:%s %s', + Ansi.yellow, Ansi.reset, item.count) + value = value .. string.format('\n%sDamage:%s %s', Ansi.yellow, Ansi.reset, item.damage) -- 2.49.1 From 2dc74d80ea7a1bb7b06639805890eef4114fa390 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Mon, 23 Mar 2020 23:41:16 -0400 Subject: [PATCH 18/90] Milo: Bump max request amount to 9999 --- milo/MiloRemote.lua | 6 +++--- milo/core/listing.lua | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/milo/MiloRemote.lua b/milo/MiloRemote.lua index e85de5c..32cbd06 100644 --- a/milo/MiloRemote.lua +++ b/milo/MiloRemote.lua @@ -60,7 +60,7 @@ local page = UI.Page { statusBar = UI.Window { y = -1, filter = UI.TextEntry { - x = 1, ex = -12, + x = 1, ex = -13, limit = 50, shadowText = 'filter', backgroundColor = colors.cyan, @@ -73,8 +73,8 @@ local page = UI.Page { }, }, amount = UI.TextEntry { - x = -11, ex = -7, - limit = 3, + x = -12, ex = -7, + limit = 4, shadowText = '1', shadowTextColor = colors.gray, backgroundColor = colors.black, diff --git a/milo/core/listing.lua b/milo/core/listing.lua index 6a2d9c4..b19b4d2 100644 --- a/milo/core/listing.lua +++ b/milo/core/listing.lua @@ -50,7 +50,7 @@ local page = UI.Page { }, statusBar = UI.StatusBar { filter = UI.TextEntry { - x = 1, ex = -17, + x = 1, ex = -18, limit = 50, shadowText = 'filter', shadowTextColor = colors.gray, @@ -64,14 +64,14 @@ local page = UI.Page { }, }, storageStatus = UI.Text { - x = -16, ex = -9, + x = -17, ex = -10, textColor = colors.lime, backgroundColor = colors.cyan, value = '', }, amount = UI.TextEntry { - x = -8, ex = -4, - limit = 3, + x = -9, ex = -4, + limit = 4, shadowText = '1', shadowTextColor = colors.gray, backgroundColor = colors.black, -- 2.49.1 From 996b8c1cd378f4aa6e5bd115e685f88051f1a7f2 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Tue, 24 Mar 2020 00:09:51 -0400 Subject: [PATCH 19/90] Swshop: Configurable lamp side --- swshop/shopConfig.lua | 16 +++++++++++++++- swshop/swshop.lua | 8 ++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/swshop/shopConfig.lua b/swshop/shopConfig.lua index 958a86d..026ad16 100644 --- a/swshop/shopConfig.lua +++ b/swshop/shopConfig.lua @@ -33,8 +33,22 @@ local wizardPage = UI.WizardPage { limit = 64, }, [4] = UI.Chooser { + formLabel = 'RS Signal', formKey = 'rsSide', formIndex = 5, + width = 10, + nochoice = 'Top', + choices = { + {name = 'Bottom', value = 'bottom'}, + {name = 'Top', value = 'top'}, + {name = 'Back', value = 'back'}, + {name = 'Front', value = 'front'}, + {name = 'Right', value = 'right'}, + {name = 'Left', value = 'left'}, + }, + required = true, + }, + [5] = UI.Chooser { width = 9, - formIndex = 5, + formIndex = 6, formLabel = 'Font Size', formKey = 'textScale', nochoice = 'Small', choices = { diff --git a/swshop/swshop.lua b/swshop/swshop.lua index 0bb07ab..499c8f9 100644 --- a/swshop/swshop.lua +++ b/swshop/swshop.lua @@ -19,7 +19,7 @@ local textutils = _G.textutils local chat = device['plethora:chat'] local storage = Config.load('storage') -rs.setOutput('top', false) +Util.each(rs.getSides(), function(side) rs.setOutput(side, false) end) r.init(jua) w.init(jua) @@ -31,7 +31,7 @@ local privatekey = config.isPrivateKey and config.password or Krist.toKristWalle local address = Krist.makev2address(privatekey) jua.on("terminate", function() - rs.setOutput('top', false) + rs.setOutput(config.rsSide, false) jua.stop() _G.printError("Terminated") end) @@ -144,7 +144,7 @@ local function connect() assert(success, "Failed to get websocket URL") print("Connected to websocket.") - rs.setOutput('top', true) + rs.setOutput(config.rsSide, true) success = await(ws.subscribe, "ownTransactions", function(data) local transaction = data.transaction @@ -160,7 +160,7 @@ local s, m = pcall(function() end) end) -rs.setOutput('top', false) +rs.setOutput(config.rsSide, false) if not s then error(m, 2) end -- 2.49.1 From 408601f0d2f5cdccb5e5c34b531e19398e035e8f Mon Sep 17 00:00:00 2001 From: Anavrins Date: Tue, 24 Mar 2020 00:17:35 -0400 Subject: [PATCH 20/90] Swshop: whoops --- swshop/swshop.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/swshop/swshop.lua b/swshop/swshop.lua index 499c8f9..a90c20a 100644 --- a/swshop/swshop.lua +++ b/swshop/swshop.lua @@ -29,9 +29,10 @@ local node = ({ ... })[1] or error('Node name is required') local config = storage[node] local privatekey = config.isPrivateKey and config.password or Krist.toKristWalletFormat(config.password) local address = Krist.makev2address(privatekey) +local rsSide = config.rsSide or 'top' jua.on("terminate", function() - rs.setOutput(config.rsSide, false) + rs.setOutput(rsSide, false) jua.stop() _G.printError("Terminated") end) @@ -144,7 +145,7 @@ local function connect() assert(success, "Failed to get websocket URL") print("Connected to websocket.") - rs.setOutput(config.rsSide, true) + rs.setOutput(rsSide, true) success = await(ws.subscribe, "ownTransactions", function(data) local transaction = data.transaction @@ -160,7 +161,7 @@ local s, m = pcall(function() end) end) -rs.setOutput(config.rsSide, false) +rs.setOutput(rsSide, false) if not s then error(m, 2) end -- 2.49.1 From b08a0f225e225374f758e43189f6d838bbb68584 Mon Sep 17 00:00:00 2001 From: Drew Lemmy Date: Fri, 27 Mar 2020 17:09:04 +0000 Subject: [PATCH 21/90] Temporary patch for #20 A more comprehensive fix should be found in the future --- swshop/shopTab.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swshop/shopTab.lua b/swshop/shopTab.lua index 6bfabb5..169d6ee 100644 --- a/swshop/shopTab.lua +++ b/swshop/shopTab.lua @@ -25,7 +25,7 @@ local shopTab = UI.Tab { formLabel = 'Price', formKey = 'price', help = 'Per item cost', required = true, - transform = 'number', + validate = 'numeric', }, [3] = UI.TextEntry { limit = 64, -- 2.49.1 From 47e0a90116e9f1d9d38487ffae33512aac1aec02 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Tue, 21 Apr 2020 20:56:21 -0400 Subject: [PATCH 22/90] gpsServer: Reduce amount of peripheral calls Redraw screen every seconds instead of every packets received, yeah that was a bad idea. --- gps/gpsServer.lua | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/gps/gpsServer.lua b/gps/gpsServer.lua index 5c60926..58461d6 100644 --- a/gps/gpsServer.lua +++ b/gps/gpsServer.lua @@ -246,8 +246,6 @@ local function server(mode) computers[computerId] = nil page.grid.values = positions page.grid:update() - page.grid:draw() - page.grid:sync() end end @@ -267,22 +265,17 @@ local function server(mode) end) Event.onInterval(1, function() - local resync = false for _, detail in pairs(positions) do if os.clock() - detail.lastChanged > 10 then detail.changed = false - resync = true end if os.clock() - detail.timestamp > 60 and detail.alive then detail.alive = false detail.hbeat = false - resync = true end end - if resync then - page:draw() - page:sync() - end + page:draw() + page:sync() end) end @@ -304,6 +297,7 @@ elseif args[1] == 'snmp' then table.insert(page.grid.columns, { heading = 'Label', key = 'label', textColor = colors.cyan } ) + page.grid.sortColumn = 'label' page.grid:adjustWidth() server('snmp') -- 2.49.1 From 45769697396f2288020aa6f6001bd00dd8966d5f Mon Sep 17 00:00:00 2001 From: kepler155c Date: Tue, 21 Apr 2020 22:40:47 -0600 Subject: [PATCH 23/90] Ui enhancements 2.0 (#29) * canvas overhaul * editor 2.0 * more tweaks * more editor work * completions + refactor * cleanup + editor additions * cleanup + undo overhaul * editor recent/peripherals/redo + cleanup * editor path issues * cleanup * changes for deprecated ui methods - recolor milo - make turtle scripts run again - mob rancher improvements * can now use named colors --- builder/builder.lua | 15 +- ccemux/etc/apps.db | 12 + ccemux/system/ccemux.lua | 8 +- common/Appstore.lua | 121 +-- common/Devices.lua | 74 +- common/DiskCopy.lua | 7 +- common/Events.lua | 127 ++- common/Follow.lua | 3 +- common/SoundPlayer.lua | 2 +- common/Turtles.lua | 324 +++---- common/edit.lua | 1392 ++++++++++++++++++---------- common/etc/apps.db | 15 +- common/etc/scripts/goHome | 2 +- common/etc/scripts/moveTo | 29 - common/etc/scripts/moveTo.lua | 17 + common/etc/scripts/setHome | 2 +- common/etc/scripts/summon | 6 +- common/recorder.lua | 107 ++- examples/.package | 6 - examples/grid.lua | 44 - gps/gpsServer.lua | 2 +- ignore/Music.lua | 2 - ignore/passthrough.lua | 174 ++++ ignore/twm.lua | 62 +- lzwfs/system/lzwfs.lua | 13 +- milo/MiloLocal.lua | 2 +- milo/MiloRemote.lua | 12 +- milo/apis/fuzzyMatch.lua | 19 - milo/apis/massAdapter.lua | 53 -- milo/core/listing.lua | 10 +- milo/core/machines.lua | 17 +- milo/plugins/activityView.lua | 1 - milo/plugins/backupView.lua | 2 - milo/plugins/brewingStandView.lua | 1 - milo/plugins/emitterView.lua | 2 - milo/plugins/inputChestView.lua | 2 - milo/plugins/item.lua | 1 - milo/plugins/item/infoTab.lua | 5 +- milo/plugins/item/machinesTab.lua | 1 - milo/plugins/item/recipeTab.lua | 3 - milo/plugins/item/resetTab.lua | 2 +- milo/plugins/jobMonitor.lua | 1 - milo/plugins/massStorageView.lua | 43 - milo/plugins/remote/setup.lua | 23 +- milo/plugins/speakerView.lua | 1 - milo/plugins/statsView.lua | 7 +- milo/plugins/storageView.lua | 2 - milo/plugins/transferView.lua | 1 - milo/plugins/trashcanView.lua | 1 - miners/scanningMiner.lua | 3 +- monitor/mwm.lua | 5 +- neural/Equipment.lua | 142 +++ neural/Scanner.lua | 4 +- neural/Sensor.lua | 4 +- neural/etc/apps.db | 6 + neural/mobRancher.lua | 116 ++- recipeBook/recipeBook.lua | 15 +- screenSaver/autorun/saver.lua | 2 +- screenSaver/system/saver.lua | 50 +- secure/autorun/lock.lua | 4 +- secure/system/secure.lua | 15 +- swshop/Shoplogs.lua | 2 +- swshop/shopConfig.lua | 4 +- swshop/shopTab.lua | 2 +- turtle/{autorun => init}/6.tl3.lua | 0 65 files changed, 1842 insertions(+), 1310 deletions(-) create mode 100644 ccemux/etc/apps.db delete mode 100644 common/etc/scripts/moveTo create mode 100644 common/etc/scripts/moveTo.lua delete mode 100644 examples/.package delete mode 100644 examples/grid.lua create mode 100644 ignore/passthrough.lua delete mode 100644 milo/apis/fuzzyMatch.lua delete mode 100644 milo/apis/massAdapter.lua delete mode 100644 milo/plugins/massStorageView.lua create mode 100644 neural/Equipment.lua rename turtle/{autorun => init}/6.tl3.lua (100%) diff --git a/builder/builder.lua b/builder/builder.lua index 0c39cb8..c021169 100644 --- a/builder/builder.lua +++ b/builder/builder.lua @@ -236,10 +236,7 @@ function substitutionPage.info:draw() end self:clear() - self:setCursorPos(1, 1) - self:print(' Replace ' .. inName .. '\n') - --self:print(' ' .. sub.id .. ':' .. sub.dmg .. '\n', nil, colors.yellow) - self:print(' With ' .. outName) + self:print(' Replace ' .. inName .. '\n' .. ' With ' .. outName) end function substitutionPage:enable() @@ -536,7 +533,7 @@ local startPage = UI.Page { event = 'setStartLevel', cancelEvent = 'slide_hide', text = UI.Text { - x = 5, y = 1, width = 20, + x = 5, y = 1, width = 10, textColor = colors.gray, }, textEntry = UI.TextEntry { @@ -554,7 +551,7 @@ local startPage = UI.Page { event = 'setStartBlock', cancelEvent = 'slide_hide', text = UI.Text { - x = 2, y = 1, width = 20, + x = 2, y = 1, width = 13, textColor = colors.gray, }, textEntry = UI.TextEntry { @@ -727,8 +724,7 @@ function startPage:eventHandler(event) Builder:begin() elseif event.type == 'quit' then - UI.term:reset() - Event.exitPullEvents() + UI:quit() end return UI.Page.eventHandler(self, event) @@ -772,5 +768,4 @@ UI:setPages({ }) UI:setPage('start') - -UI:pullEvents() +UI:start() diff --git a/ccemux/etc/apps.db b/ccemux/etc/apps.db new file mode 100644 index 0000000..d94fb3d --- /dev/null +++ b/ccemux/etc/apps.db @@ -0,0 +1,12 @@ +{ + [ "87e89abb4c1c551fe08d355d097f18b8de78edca5f556997085681662fce8eed" ] = { + title = "Config", + category = "CCEmuX", + run = "emu config", + }, + [ "cec3a9b89b2e391393d0f68e4bc12a9fa6cf358b3cdf79496dc442d52b8dd528" ] = { + title = "Data", + category = "CCEmuX", + run = "emu data", + }, +} diff --git a/ccemux/system/ccemux.lua b/ccemux/system/ccemux.lua index 0948583..f05df3d 100644 --- a/ccemux/system/ccemux.lua +++ b/ccemux/system/ccemux.lua @@ -9,7 +9,7 @@ local tab = UI.Tab { tabTitle = 'CCEmuX', description = 'CCEmuX peripherals', form = UI.Form { - x = 2, ex = -2, y = 1, ey = 4, + x = 2, ex = -2, y = 2, ey = 5, values = { side = 'bottom', type = 'wireless_modem', @@ -28,7 +28,7 @@ local tab = UI.Tab { }, }, drive_id = UI.TextEntry { - x = 20, y = 3, + x = 19, y = 3, formKey = 'drive_id', shadowText = 'id', width = 5, @@ -36,13 +36,13 @@ local tab = UI.Tab { transform = 'number', }, add = UI.Button { - x = 28, y = 3, + x = -6, y = 3, width = 5, text = 'Add', event = 'form_ok', help = 'Add items to turtle to add to filter', }, }, grid = UI.Grid { - x = 3, ex = -3, y = 6, ey = -2, + x = 2, ex = -2, y = 7, ey = -2, columns = { { heading = 'Side', key = 'side', width = 8 }, { heading = 'Type', key = 'type' }, diff --git a/common/Appstore.lua b/common/Appstore.lua index c489696..7576f5f 100644 --- a/common/Appstore.lua +++ b/common/Appstore.lua @@ -8,10 +8,10 @@ local http = _G.http local multishell = _ENV.multishell local os = _G.os local shell = _ENV.shell +local colors = _G.colors local REGISTRY_DIR = 'usr/.registry' - -- FIX SOMEDAY local function registerApp(app, key) app.key = SHA.compute(key) @@ -27,7 +27,6 @@ local function unregisterApp(key) end end - local sandboxEnv = Util.shallowCopy(_ENV) setmetatable(sandboxEnv, { __index = _G }) @@ -36,21 +35,10 @@ UI:configure('Appstore', ...) local APP_DIR = 'usr/apps' -local sources = { - - { text = "STD Default", - event = 'source', - url = "https://github.com/LDDestroier/STD-GUI/raw/master/list.lua" }, --stock ---[[ - { text = "Discover", - event = 'source', - generateName = true, - url = "http://pastebin.com/raw/9bXfCz6M" }, --owned by dannysmc95 - - { text = "Opus", - event = 'source', - url = "http://pastebin.com/raw/ajQ91Rmn" }, -]] +local source = { + text = "STD Default", + event = 'source', + url = "https://github.com/LDDestroier/STD-GUI/raw/master/list.lua", } shell.setDir(APP_DIR) @@ -87,14 +75,14 @@ local function runApp(app, checkExists, ...) error('Failed to download') end - local fn = loadstring(program, app.name) + fn = _G.loadstring(program, app.name) if not fn then error('Failed to download') end - setfenv(fn, sandboxEnv) - fn(unpack(args)) + _G.setfenv(fn, sandboxEnv) + fn(table.unpack(args)) end end @@ -134,16 +122,16 @@ local viewApp = function(app) return true end -local getSourceListing = function(source) +local getSourceListing = function() local contents = http.get(source.url) if contents then - local fn = loadstring(contents.readAll(), source.text) + local fn = _G.loadstring(contents.readAll(), source.text) contents.close() local env = { std = { } } setmetatable(env, { __index = _G }) - setfenv(fn, env) + _G.setfenv(fn, env) fn() if env.contextualGet then @@ -172,9 +160,28 @@ local getSourceListing = function(source) end end +getSourceListing() + +if not source.storeURLs then + error('Unable to download application list') +end + +local buttons = { } +for k,v in Util.spairs(source.storeCatagoryNames, + function(a, b) return a:lower() < b:lower() end) do + + if v ~= 'Operating System' then + table.insert(buttons, { + text = v, + event = 'category', + index = k, + }) + end +end +source.index, source.name = Util.first(source.storeCatagoryNames) + local appPage = UI.Page { menuBar = UI.MenuBar { --- showBackButton = not pocket, buttons = { { text = '\027', event = 'back' }, { text = 'Install', event = 'install' }, @@ -204,16 +211,14 @@ function appPage.container.viewport:draw() Ansi.yellow .. app.description .. Ansi.reset) self:clear() - self:setCursorPos(1, 1) self:print(str) - self.ymax = self.cursorY if appPage.notification.enabled then appPage.notification:draw() end end -function appPage:enable(source, app) +function appPage:enable(app) self.source = source self.app = app UI.Page.enable(self) @@ -293,8 +298,7 @@ end local categoryPage = UI.Page { menuBar = UI.MenuBar { buttons = { - { text = 'Catalog', dropdown = sources }, - { text = 'Category', name = 'categoryButton', dropdown = { } }, + { text = 'Category', name = 'categoryButton', dropdown = buttons }, }, }, grid = UI.ScrollingGrid { @@ -309,62 +313,21 @@ local categoryPage = UI.Page { l = 'lua', [ 'control-q' ] = 'quit', }, + source = source, } -function categoryPage:setCategory(source, name, index) +function categoryPage:setCategory(name, index) self.grid.values = { } for _,v in pairs(source.storeURLs) do if index == 0 or index == v.catagory then table.insert(self.grid.values, v) end end - self.statusBar:setStatus(string.format('%s: %s', source.text, name)) + self.statusBar:setStatus(string.format('%s: %s', self.source.text or '', name)) self.grid:update() self.grid:setIndex(1) end -function categoryPage:setSource(source) - - if not source.categoryMenu then - - self.statusBar:setStatus('Loading...') - self.statusBar:draw() - self:sync() - - getSourceListing(source) - - if not source.storeURLs then - error('Unable to download application list') - end - - local buttons = { } - for k,v in Util.spairs(source.storeCatagoryNames, - function(a, b) return a:lower() < b:lower() end) do - - if v ~= 'Operating System' then - table.insert(buttons, { - text = v, - event = 'category', - index = k, - }) - end - end - - source.categoryMenu = UI.DropMenu({ - buttons = buttons, - }) - source.index, source.name = Util.first(source.storeCatagoryNames) - - categoryPage.menuBar.categoryButton:add({ - categoryMenu = source.categoryMenu - }) - end - - self.source = source - self.menuBar.categoryButton.dropmenu = source.categoryMenu - categoryPage:setCategory(source, source.name, source.index) -end - function categoryPage.grid:sortCompare(a, b) return a.ltitle < b.ltitle end @@ -377,12 +340,11 @@ function categoryPage.grid:getRowTextColor(row, selected) end function categoryPage:eventHandler(event) - if event.type == 'grid_select' or event.type == 'select' then - UI:setPage(appPage, self.source, self.grid:getSelected()) + UI:setPage(appPage, self.grid:getSelected()) elseif event.type == 'category' then - self:setCategory(self.source, event.button.text, event.button.index) + self:setCategory(event.button.text, event.button.index) self:setFocus(self.grid) self:draw() @@ -392,7 +354,7 @@ function categoryPage:eventHandler(event) self:draw() elseif event.type == 'quit' then - UI:exitPullEvents() + UI:quit() else return UI.Page.eventHandler(self, event) @@ -401,8 +363,7 @@ function categoryPage:eventHandler(event) end print("Retrieving catalog list") -categoryPage:setSource(sources[1]) +categoryPage:setCategory(source.name, source.index) UI:setPage(categoryPage) -UI:pullEvents() -UI.term:reset() +UI:start() diff --git a/common/Devices.lua b/common/Devices.lua index 6ad2f08..fc2cecd 100644 --- a/common/Devices.lua +++ b/common/Devices.lua @@ -3,7 +3,6 @@ local Event = require('opus.event') local UI = require('opus.ui') local Util = require('opus.util') -local colors = _G.colors local peripheral = _G.peripheral --[[ -- PeripheralsPage -- ]] -- @@ -16,6 +15,20 @@ local peripheralsPage = UI.Page { }, sortColumn = 'type', autospace = true, + enable = function(self) + local sides = peripheral.getNames() + + Util.clear(self.values) + for _,side in pairs(sides) do + table.insert(self.values, { + type = peripheral.getType(side), + side = side + }) + end + self:update() + self:adjustWidth() + UI.Grid.enable(self) + end, }, statusBar = UI.StatusBar { values = 'Select peripheral', @@ -23,52 +36,35 @@ local peripheralsPage = UI.Page { accelerators = { [ 'control-q' ] = 'quit', }, + updatePeripherals = function(self) + if UI:getCurrentPage() == self then + self.grid:draw() + self:sync() + end + end, + eventHandler = function(self, event) + if event.type == 'quit' then + UI:quit() + + elseif event.type == 'grid_select' then + UI:setPage('methods', event.selected) + + end + return UI.Page.eventHandler(self, event) + end, } -function peripheralsPage.grid:enable() - local sides = peripheral.getNames() - - Util.clear(self.values) - for _,side in pairs(sides) do - table.insert(self.values, { - type = peripheral.getType(side), - side = side - }) - end - self:update() - self:adjustWidth() - UI.Grid.enable(self) -end - -function peripheralsPage:updatePeripherals() - if UI:getCurrentPage() == self then - self.grid:draw() - self:sync() - end -end - -function peripheralsPage:eventHandler(event) - if event.type == 'quit' then - Event.exitPullEvents() - - elseif event.type == 'grid_select' then - UI:setPage('methods', event.selected) - - end - return UI.Page.eventHandler(self, event) -end - --[[ -- MethodsPage -- ]] -- local methodsPage = UI.Page { - backgroundColor = colors.black, doc = UI.TextArea { - backgroundColor = colors.black, - x = 2, y = 2, ex = -1, ey = -7, + backgroundColor = 'black', + ey = -7, + marginLeft = 1, marginTop = 1, }, grid = UI.ScrollingGrid { y = -6, ey = -2, columns = { - { heading = 'Name', key = 'name', width = UI.term.width } + { heading = 'Name', key = 'name' } }, sortColumn = 'name', }, @@ -198,4 +194,4 @@ UI:setPages({ methods = methodsPage, }) -UI:pullEvents() +UI:start() diff --git a/common/DiskCopy.lua b/common/DiskCopy.lua index 849dbe3..b3efc4e 100644 --- a/common/DiskCopy.lua +++ b/common/DiskCopy.lua @@ -122,7 +122,6 @@ function page:drawInfo(drive, textArea) return isValid(drive) and fs.getFreeSpace(drive.getMountPath()) or 0 end - textArea:setCursorPos(1, 1) textArea:print(string.format('Drive: %s%s%s\nLabel: %s%s%s\nUsed: %s%s%s\nFree: %s%s%s', Ansi.yellow, drive.name, Ansi.reset, isValid(drive) and Ansi.yellow or Ansi.orange, getLabel():sub(1, 10), Ansi.reset, @@ -138,6 +137,7 @@ function page:scan() self.copyButton.inactive = not valid self:draw() + self.progress:clear() self.progress:centeredWrite(1, 'Analyzing Disks..') self.progress:sync() @@ -167,6 +167,7 @@ function page:copy() throttle() end + self.progress:clear() self.progress:centeredWrite(1, 'Computing..') self.progress:sync() @@ -211,6 +212,8 @@ function page:copy() self.progress:clear() rawCopy(sdrive.getMountPath(), tdrive.getMountPath()) cleanup() + + self.progress:clear() self.progress:centeredWrite(1, 'Copy Complete', colors.lime, colors.black) self.progress:sync() @@ -270,4 +273,4 @@ Event.onTimeout(.2, function() end) UI:setPage(page) -UI:pullEvents() +UI:start() diff --git a/common/Events.lua b/common/Events.lua index 6a0dd2c..783e38c 100644 --- a/common/Events.lua +++ b/common/Events.lua @@ -27,6 +27,26 @@ local page = UI.Page { }, autospace = true, disableHeader = true, + getDisplayValues = function(_, row) + row = Util.shallowCopy(row) + + local function tovalue(s) + if type(s) == 'table' then + return 'table' + end + return s + end + + for k,v in pairs(row) do + row[k] = tovalue(v) + end + + return row + end, + draw = function(self) + self:adjustWidth() + UI.Grid.draw(self) + end, }, accelerators = { f = 'filter', @@ -36,81 +56,48 @@ local page = UI.Page { [ 'control-q' ] = 'quit', }, filtered = { }, -} + eventHandler = function(self, event) + if event.type == 'filter' then + local entry = self.grid:getSelected() + self.filtered[entry.event] = true -function page:eventHandler(event) + elseif event.type == 'toggle' then + self.paused = not self.paused + if self.paused then + self.menuBar.pauseButton.text = 'Resume' + else + self.menuBar.pauseButton.text = 'Pause ' + end + self.menuBar:draw() - if event.type == 'filter' then - local entry = self.grid:getSelected() - self.filtered[entry.event] = true + elseif event.type == 'grid_select' then + multishell.openTab({ + path = 'sys/apps/Lua.lua', + args = { event.selected }, + focused = true, + }) - elseif event.type == 'toggle' then - self.paused = not self.paused - if self.paused then - self.menuBar.pauseButton.text = 'Resume' - else - self.menuBar.pauseButton.text = 'Pause ' - end - self.menuBar:draw() - - elseif event.type == 'grid_select' then - multishell.openTab({ - path = 'sys/apps/Lua.lua', - args = { event.selected }, - focused = true, - }) - - elseif event.type == 'reset' then - self.filtered = { } - self.grid:setValues({ }) - self.grid:draw() - if self.paused then - self:emit({ type = 'toggle' }) - end - - elseif event.type == 'clear' then - self.grid:setValues({ }) - self.grid:draw() - - elseif event.type == 'quit' then - UI:exitPullEvents() - - --[[ - elseif event.type == 'focus_change' then - if event.focused == self.grid then - if not self.paused then + elseif event.type == 'reset' then + self.filtered = { } + self.grid:setValues({ }) + self.grid:draw() + if self.paused then self:emit({ type = 'toggle' }) end + + elseif event.type == 'clear' then + self.grid:setValues({ }) + self.grid:draw() + + elseif event.type == 'quit' then + UI:quit() + + else + return UI.Page.eventHandler(self, event) end - --]] - - else - return UI.Page.eventHandler(self, event) - end - return true -end - -function page.grid:getDisplayValues(row) - row = Util.shallowCopy(row) - - local function tovalue(s) - if type(s) == 'table' then - return 'table' - end - return s - end - - for k,v in pairs(row) do - row[k] = tovalue(v) - end - - return row -end - -function page.grid:draw() - self:adjustWidth() - UI.Grid.draw(self) -end + return true + end, +} local updated = false local timerId = os.startTimer(1) @@ -150,6 +137,6 @@ end kernel.hook('*', hookFunction) UI:setPage(page) -UI:pullEvents() +UI:start() kernel.unhook('*', hookFunction) diff --git a/common/Follow.lua b/common/Follow.lua index 3ad0007..6c2e84d 100644 --- a/common/Follow.lua +++ b/common/Follow.lua @@ -42,7 +42,6 @@ local page = UI.Page { }, range = UI.SlideOut { y = -7, height = 7, - backgroundColor = colors.cyan, titleBar = UI.TitleBar { event = 'cancel', title = 'Enter range', @@ -239,6 +238,6 @@ Event.addRoutine(function() end) UI:setPage(page) -UI:pullEvents() +UI:start() swarm:stop() diff --git a/common/SoundPlayer.lua b/common/SoundPlayer.lua index bcce449..bb36029 100644 --- a/common/SoundPlayer.lua +++ b/common/SoundPlayer.lua @@ -60,4 +60,4 @@ function page:eventHandler(event) end UI:setPage(page) -UI:pullEvents() +UI:start() diff --git a/common/Turtles.lua b/common/Turtles.lua index f27a004..cc48825 100644 --- a/common/Turtles.lua +++ b/common/Turtles.lua @@ -2,7 +2,6 @@ local Config = require('opus.config') local Event = require('opus.event') local itemDB = require('core.itemDB') local Socket = require('opus.socket') -local Terminal = require('opus.terminal') local UI = require('opus.ui') local Util = require('opus.util') @@ -11,10 +10,7 @@ local fs = _G.fs local multishell = _ENV.multishell local network = _G.network local os = _G.os -local shell = _ENV.shell -local term = _G.term ---UI.Button.defaults.focusIndicator = ' ' UI:configure('Turtles', ...) local config = { } @@ -31,13 +27,26 @@ local options = { local SCRIPTS_PATH = 'packages/common/etc/scripts' -local nullTerm = Terminal.getNullTerm(term.current()) -local socket +local socket, turtle, page -local page = UI.Page { +page = UI.Page { coords = UI.Window { backgroundColor = colors.black, height = 3, + marginTop = 1, marginLeft = 1, + draw = function(self) + local t = turtle + self:clear() + if t then + self:setCursorPos(2, 2) + local ind = 'GPS' + if not t.point.gps then + ind = 'REL' + end + self:print(string.format('%s : %d,%d,%d', + ind, t.point.x, t.point.y, t.point.z)) + end + end, }, tabs = UI.Tabs { x = 1, y = 4, ey = -2, @@ -50,6 +59,23 @@ local page = UI.Page { disableHeader = true, sortColumn = 'label', autospace = true, + draw = function(self) + Util.clear(self.values) + local files = fs.list(SCRIPTS_PATH) + for _,path in pairs(files) do + table.insert(self.values, { label = path, path = fs.combine(SCRIPTS_PATH, path) }) + end + self:update() + UI.ScrollingGrid.draw(self) + end, + eventHandler = function(self, event) + if event.type == 'grid_select' then + page:runScript(event.selected.label) + else + return UI.ScrollingGrid.eventHandler(self, event) + end + return true + end, }, turtles = UI.ScrollingGrid { tabTitle = 'Select', @@ -63,6 +89,41 @@ local page = UI.Page { disableHeader = true, sortColumn = 'label', autospace = true, + getDisplayValues = function(_, row) + row = Util.shallowCopy(row) + if row.fuel then + row.fuel = Util.toBytes(row.fuel) + end + if row.distance then + row.distance = Util.round(row.distance, 1) + end + return row + end, + draw = function(self) + Util.clear(self.values) + for _,v in pairs(network) do + if v.fuel then + table.insert(self.values, v) + end + end + self:update() + UI.ScrollingGrid.draw(self) + end, + eventHandler = function(self, event) + if event.type == 'grid_select' then + turtle = event.selected + config.id = event.selected.id + Config.update('Turtles', config) + multishell.setTitle(multishell.getCurrent(), turtle.label) + if socket then + socket:close() + socket = nil + end + else + return UI.ScrollingGrid.eventHandler(self, event) + end + return true + end, }, inventory = UI.ScrollingGrid { backgroundColor = colors.cyan, @@ -70,10 +131,58 @@ local page = UI.Page { columns = { { heading = '', key = 'index', width = 2 }, { heading = '', key = 'count', width = 2 }, - { heading = 'Inventory', key = 'key', width = UI.term.width - 7 }, + { heading = 'Inventory', key = 'key' }, }, disableHeader = true, sortColumn = 'index', + getRowTextColor = function(self, row, selected) + if turtle and row.selected then + return colors.yellow + end + return UI.ScrollingGrid.getRowTextColor(self, row, selected) + end, + draw = function(self) + local t = turtle + Util.clear(self.values) + if t then + for k,v in pairs(t.inv or { }) do -- new method (less data) + local index, count = k:match('(%d+),(%d+)') + v = { + index = tonumber(index), + key = v, + count = tonumber(count), + } + table.insert(self.values, v) + end + + for _,v in pairs(t.inventory or { }) do + if v.count > 0 then + table.insert(self.values, v) + end + end + + for _,v in pairs(self.values) do + if v.index == t.slotIndex then + v.selected = true + end + if v.key then + v.key = itemDB:getName(v.key) + end + end + end + self:adjustWidth() + self:update() + UI.ScrollingGrid.draw(self) + end, + eventHandler = function(self, event) + if event.type == 'grid_select' then + local fn = string.format('turtle.select(%d)', event.selected.index) + page:runFunction(fn) + else + return UI.ScrollingGrid.eventHandler(self, event) + end + return true + end, }, --[[ policy = UI.ScrollingGrid { @@ -134,6 +243,15 @@ local page = UI.Page { { key = 'distance', width = 6 }, { key = 'fuel', width = 6 }, }, + draw = function(self) + local t = turtle + if t then + self.values.status = t.status + self.values.distance = t.distance and Util.round(t.distance, 2) + self.values.fuel = Util.toBytes(t.fuel) + end + UI.StatusBar.draw(self) + end, }, notification = UI.Notification(), accelerators = { @@ -141,15 +259,10 @@ local page = UI.Page { }, } -function page:enable(turtle) - self.turtle = turtle - UI.Page.enable(self) -end - function page:runFunction(script, nowrap) for _ = 1, 2 do if not socket then - socket = Socket.connect(self.turtle.id, 161) + socket = Socket.connect(turtle.id, 161) end if socket then @@ -170,149 +283,52 @@ function page:runFunction(script, nowrap) end function page:runScript(scriptName) - if self.turtle then + if turtle then self.notification:info('Connecting') self:sync() - local cmd = string.format('Script %d %s', self.turtle.id, scriptName) - local ot = term.redirect(nullTerm) - pcall(function() shell.run(cmd) end) - term.redirect(ot) - self.notification:success('Sent') - end -end - -function page.coords:draw() - local t = self.parent.turtle - self:clear() - if t then - self:setCursorPos(2, 2) - local ind = 'GPS' - if not t.point.gps then - ind = 'REL' + local script = Util.readFile(fs.combine(SCRIPTS_PATH, scriptName)) + if not script then + print('Unable to read script file') end - self:print(string.format('%s : %d,%d,%d', - ind, t.point.x, t.point.y, t.point.z)) - end -end ---[[ Inventory Tab ]]-- -function page.tabs.inventory:getRowTextColor(row, selected) - if page.turtle and row.selected then - return colors.yellow - end - return UI.ScrollingGrid.getRowTextColor(self, row, selected) -end - -function page.tabs.inventory:draw() - local t = page.turtle - Util.clear(self.values) - if t then - for k,v in pairs(t.inv or { }) do -- new method (less data) - local index, count = k:match('(%d+),(%d+)') - v = { - index = tonumber(index), - key = v, - count = tonumber(count), + local function processVariables() + local variables = { + COMPUTER_ID = os.getComputerID, + GPS = function() + local pt = require('opus.gps').getPoint() + if not pt then + error('Unable to determine location') + end + return _G.textutils.serialize(pt) + end, } - table.insert(self.values, v) - end - - for _,v in pairs(t.inventory or { }) do - if v.count > 0 then - table.insert(self.values, v) + for k,v in pairs(variables) do + local token = string.format('{%s}', k) + if script:find(token, 1, true) then + local s, m = pcall(v) + if not s then + self.notification:error(m) + return + end + script = script:gsub(token, m) + end end + return true end - for _,v in pairs(self.values) do - if v.index == t.slotIndex then - v.selected = true + if processVariables(script) then + local socket = Socket.connect(turtle.id, 161) + if not socket then + self.notification:error('Unable to connect') + return end - if v.key then - v.key = itemDB:getName(v.key) - end - end - end - self:adjustWidth() - self:update() - UI.ScrollingGrid.draw(self) -end - -function page.tabs.inventory:eventHandler(event) - if event.type == 'grid_select' then - local fn = string.format('turtle.select(%d)', event.selected.index) - page:runFunction(fn) - else - return UI.ScrollingGrid.eventHandler(self, event) - end - return true -end - -function page.tabs.scripts:draw() - Util.clear(self.values) - local files = fs.list(SCRIPTS_PATH) - for _,path in pairs(files) do - table.insert(self.values, { label = path, path = fs.combine(SCRIPTS_PATH, path) }) - end - self:update() - UI.ScrollingGrid.draw(self) -end - -function page.tabs.scripts:eventHandler(event) - if event.type == 'grid_select' then - page:runScript(event.selected.label) - else - return UI.ScrollingGrid.eventHandler(self, event) - end - return true -end - -function page.tabs.turtles:getDisplayValues(row) - row = Util.shallowCopy(row) - if row.fuel then - row.fuel = Util.toBytes(row.fuel) - end - if row.distance then - row.distance = Util.round(row.distance, 1) - end - return row -end - -function page.tabs.turtles:draw() - Util.clear(self.values) - for _,v in pairs(network) do - if v.fuel then - table.insert(self.values, v) - end - end - self:update() - UI.ScrollingGrid.draw(self) -end - -function page.tabs.turtles:eventHandler(event) - if event.type == 'grid_select' then - page.turtle = event.selected - config.id = event.selected.id - Config.update('Turtles', config) - multishell.setTitle(multishell.getCurrent(), page.turtle.label) - if socket then + socket:write({ type = 'script', args = script }) socket:close() - socket = nil - end - else - return UI.ScrollingGrid.eventHandler(self, event) - end - return true -end -function page.statusBar:draw() - local t = self.parent.turtle - if t then - self.values.status = t.status - self.values.distance = t.distance and Util.round(t.distance, 2) - self.values.fuel = Util.toBytes(t.fuel) + self.notification:success('Sent') + end end - UI.StatusBar.draw(self) end function page:showBlocks() @@ -335,7 +351,7 @@ end function page:eventHandler(event) if event.type == 'quit' then - UI:exitPullEvents() + UI:quit() elseif event.type == 'tab_select' then config.tab = event.button.text @@ -354,19 +370,14 @@ function page:eventHandler(event) return true end -function page:enable() - UI.Page.enable(self) --- self.tabs:activateTab(page.tabs.turtles) -end - if not Util.getOptions(options, { ... }, true) then return end if options.turtle.value >= 0 then for _ = 1, 10 do - page.turtle = _G.network[options.turtle.value] - if page.turtle then + turtle = _G.network[options.turtle.value] + if turtle then break end os.sleep(1) @@ -374,9 +385,9 @@ if options.turtle.value >= 0 then end Event.onInterval(1, function() - if page.turtle then - local t = _G.network[page.turtle.id] - page.turtle = t + if turtle then + --local t = _G.network[turtle.id] + --turtle = t page:draw() page:sync() end @@ -387,5 +398,4 @@ if config.tab then end UI:setPage(page) - -UI:pullEvents() +UI:start() diff --git a/common/edit.lua b/common/edit.lua index f74bdb6..804d40c 100644 --- a/common/edit.lua +++ b/common/edit.lua @@ -1,6 +1,10 @@ -local input = require('opus.input') +local Array = require('opus.array') +local Config = require('opus.config') +local fuzzy = require('opus.fuzzy') +local UI = require('opus.ui') +local Util = require('opus.util') -local colors = _G.colors +local device = _G.device local fs = _G.fs local multishell = _ENV.multishell local os = _G.os @@ -8,27 +12,15 @@ local shell = _ENV.shell local term = _G.term local textutils = _G.textutils -shell.setCompletionFunction(shell.getRunningProgram(), function(_, index, text) - if index == 1 then - return fs.complete(text, shell.dir(), true, false) - end -end) +local _format = string.format +local _rep = string.rep +local _sub = string.sub +local _concat = table.concat +local _insert = table.insert +local _remove = table.remove +local _unpack = table.unpack -local tArgs = { ... } -if #tArgs == 0 then - error( "Usage: edit " ) -end - --- Error checking -local sPath = shell.resolve(tArgs[1]) -local bReadOnly = fs.isReadOnly(sPath) -if fs.exists(sPath) and fs.isDir(sPath) then - error( "Cannot edit a directory." ) -end - -if multishell then - multishell.setTitle(multishell.getCurrent(), fs.getName(sPath)) -end +local config = Config.load('editor') local x, y = 1, 1 local w, h = term.getSize() @@ -36,39 +28,35 @@ local scrollX = 0 local scrollY = 0 local lastPos = { x = 1, y = 1 } local tLines = { } -local bRunning = true -local sStatus = "" -local isError local fileInfo -local lastAction - +local actions +local lastSave local dirty = { y = 1, ey = h } local mark = { } local searchPattern -local undo = { chain = { }, pointer = 0 } -local complete = { } +local undo = { chain = { }, redo = { } } +h = h - 1 + +local bgColor = 'gray' local color = { - textColor = '0', - keywordColor = '4', - commentColor = 'd', - stringColor = 'e', - bgColor = colors.black, - highlightColor = colors.orange, - cursorColor = colors.lime, - errorBackground = colors.red, + text = '0', + keyword = '2', + comment = 'd', + string = '1', + mark = '8', + bg = '7', } if not term.isColor() then + bgColor = 'black' color = { - textColor = '0', - keywordColor = '8', - commentColor = '8', - stringColor = '8', - bgColor = colors.black, - highlightColor = colors.lightGray, - cursorColor = colors.white, - errorBackground = colors.gray, + text = '0', + keyword = '8', + comment = '8', + string = '8', + mark = '7', + bg = 'f', } end @@ -78,10 +66,9 @@ local keyMapping = { down = 'down', left = 'left', right = 'right', - pageUp = 'pageUp', - [ 'control-b' ] = 'pageUp', - pageDown = 'pageDown', --- [ 'control-f' ] = 'pageDown', + pageUp = 'page_up', + [ 'control-b' ] = 'page_up', + pageDown = 'page_down', home = 'home', [ 'end' ] = 'toend', [ 'control-home' ] = 'top', @@ -93,7 +80,7 @@ local keyMapping = { [ 'scroll_down' ] = 'scroll_down', [ 'control-down' ] = 'scroll_down', [ 'mouse_click' ] = 'go_to', - [ 'control-l' ] = 'goto_line', + [ 'control-g' ] = 'goto_line', -- marking [ 'shift-up' ] = 'mark_up', @@ -108,6 +95,8 @@ local keyMapping = { [ 'shift-end' ] = 'mark_end', [ 'shift-home' ] = 'mark_home', [ 'mouse_down' ] = 'mark_anchor', + [ 'mouse_doubleclick' ] = 'mark_current_word', + [ 'mouse_tripleclick' ] = 'mark_line', -- editing delete = 'delete', @@ -117,17 +106,21 @@ local keyMapping = { paste = 'paste', tab = 'tab', [ 'control-z' ] = 'undo', + [ 'control-Z' ] = 'redo', [ 'control-space' ] = 'autocomplete', + [ 'control-shift-space' ] = 'peripheral', -- copy/paste [ 'control-x' ] = 'cut', [ 'control-c' ] = 'copy', --- [ 'control-shift-paste' ] = 'paste_internal', + [ 'control-y' ] = 'paste_internal', -- file [ 'control-s' ] = 'save', + [ 'control-S' ] = 'save_as', [ 'control-q' ] = 'exit', [ 'control-enter' ] = 'run', + [ 'control-p' ] = 'quick_open', -- search [ 'control-f' ] = 'find_prompt', @@ -135,264 +128,613 @@ local keyMapping = { [ 'control-n' ] = 'find_next', -- misc - [ 'control-g' ] = 'status', + [ 'control-i' ] = 'status', [ 'control-r' ] = 'refresh', - [ 'control' ] = 'menu', } -local messages = { - menu = '^s: save, ^q: quit, ^enter: run', - wrapped = 'search hit BOTTOM, continuing at TOP', +local page = UI.Page { + menuBar = UI.MenuBar { + transitionHint = 'slideLeft', + buttons = { + { text = 'File', dropdown = { + { text = 'New ', event = 'menu_action', action = 'file_new' }, + { text = 'Open... ', event = 'menu_action', action = 'file_open' }, + { text = 'Quick Open... ^p', event = 'menu_action', action = 'quick_open' }, + { text = 'Recent... ', event = 'menu_action', action = 'recent' }, + { spacer = true }, + { text = 'Save ^s', event = 'menu_action', action = 'save' }, + { text = 'Save As... ^S', event = 'menu_action', action = 'save_as' }, + { spacer = true }, + { text = 'Quit ^q', event = 'menu_action', action = 'exit' }, + } }, + { text = 'Edit', dropdown = { + { text = 'Cut ^x', event = 'menu_action', action = 'cut' }, + { text = 'Copy ^c', event = 'menu_action', action = 'copy' }, + { text = 'Paste ^y,^V', event = 'menu_action', action = 'paste_internal' }, + { spacer = true }, + { text = 'Find... ^f', event = 'menu_action', action = 'find_prompt' }, + { text = 'Find Next ^n', event = 'menu_action', action = 'find_next' }, + { spacer = true }, + { text = 'Go to line... ^g', event = 'menu_action', action = 'goto_line' }, + { text = 'Mark all ^a', event = 'menu_action', action = 'mark_all' }, + } }, + { text = 'Code', dropdown = { + { text = 'Complete ^space', event = 'menu_action', action = 'autocomplete' }, + { text = 'Run ^enter', event = 'menu_action', action = 'run' }, + { spacer = true }, + { text = 'Peripheral ^SPACE', event = 'menu_action', action = 'peripheral' }, + } }, + }, + status = UI.Text { + textColor = 'gray', + x = -9, width = 9, + align = 'right', + }, + }, + gotoLine = UI.MiniSlideOut { + x = -15, y = -2, + label = 'Line', + lineNo = UI.TextEntry { + x = 7, width = 7, + limit = 5, + transform = 'number', + accelerators = { + [ 'enter' ] = 'accept', + }, + }, + show = function(self) + self.lineNo:reset() + UI.MiniSlideOut.show(self) + end, + eventHandler = function(self, event) + if event.type == 'accept' then + if self.lineNo.value then + actions.process('go_to', 1, self.lineNo.value) + end + self:hide() + return true + end + return UI.MiniSlideOut.eventHandler(self, event) + end, + }, + search = UI.MiniSlideOut { + x = '50%', y = -2, + label = 'Find', + search = UI.TextEntry { + x = 7, ex = -3, + limit = 512, + accelerators = { + [ 'enter' ] = 'accept', + }, + }, + show = function(self) + self.search:markAll() + UI.MiniSlideOut.show(self) + end, + eventHandler = function(self, event) + if event.type == 'accept' then + local text = self.search.value + if text and #text > 0 then + searchPattern = text:lower() + if searchPattern then + actions.unmark() + actions.process('find', searchPattern, x) + end + end + self:hide() + return true + end + return UI.MiniSlideOut.eventHandler(self, event) + end, + }, + save_as = UI.MiniSlideOut { + x = '30%', y = -2, + label = 'Save', + filename = UI.TextEntry { + x = 7, ex = -3, + limit = 512, + accelerators = { + [ 'enter' ] = 'accept', + }, + }, + show = function(self) + self.filename:setValue(fileInfo.path) + self.filename:setPosition(#self.filename.value) + UI.MiniSlideOut.show(self) + end, + eventHandler = function(self, event) + if event.type == 'accept' then + local text = self.filename.value + if text and #text > 0 then + actions.save('/' .. text) + end + self:hide() + return true + end + return UI.MiniSlideOut.eventHandler(self, event) + end, + }, + unsaved = UI.Question { + x = -25, y = -2, + label = 'Save', + cancel = UI.Button { + x = 16, + text = 'Cancel', + backgroundColor = 'primary', + event = 'question_cancel', + }, + show = function(self, action) + self.action = action + UI.MiniSlideOut.show(self) + end, + eventHandler = function(self, event) + if event.type == 'question_yes' then + if actions.save() then + self:hide() + actions.process(self.action) + end + elseif event.type == 'question_no' then + actions.process(self.action, true) + self:hide() + elseif event.type == 'question_cancel' then + self:hide() + end + return UI.MiniSlideOut.eventHandler(self, event) + end, + }, + file_open = UI.FileSelect { + modal = true, + enable = function() end, + show = function(self) + UI.FileSelect.enable(self, fs.getDir(fileInfo.path)) + self:focusFirst() + self:draw() + self:addTransition('expandUp', { easing = 'outBounce', ticks = 12 }) + end, + eventHandler = function(self, event) + if event.type == 'select_cancel' then + self:disable() + elseif event.type == 'select_file' then + self:disable() + actions.process('open', event.file) + end + return UI.FileSelect.eventHandler(self, event) + end, + }, + recent = UI.SlideOut { + grid = UI.Grid { + x = 2, y = 2, ey = -4, ex = -2, + columns = { + { key = 'name', heading = 'Name' }, + { key = 'dir', heading = 'Directory' }, + }, + accelerators = { + backspace = 'slide_hide', + }, + }, + cancel = UI.Button { + x = -9, y = -2, + text = 'Cancel', + event = 'slide_hide', + }, + show = function(self) + local t = { } + for _,v in pairs(config.recent or { }) do + table.insert(t, { name = fs.getName(v), dir = fs.getDir(v), path = v }) + end + self.grid:setValues(t) + self.grid:setIndex(1) + UI.SlideOut.show(self) + self:addTransition('expandUp', { easing = 'outBounce', ticks = 12 }) + end, + eventHandler = function(self, event) + if event.type == 'grid_select' then + actions.process('open', event.selected.path) + self:hide() + return true + end + return UI.SlideOut.eventHandler(self, event) + end, + }, + quick_open = UI.SlideOut { + filter_entry = UI.TextEntry { + x = 2, y = 2, ex = -2, + limit = 256, + shadowText = 'File name', + accelerators = { + [ 'enter' ] = 'accept', + [ 'up' ] = 'grid_up', + [ 'down' ] = 'grid_down', + }, + }, + grid = UI.ScrollingGrid { + x = 2, y = 3, ex = -2, ey = -4, + disableHeader = true, + columns = { + { key = 'name' }, + { key = 'dir', textColor = 'lightGray' }, + }, + accelerators = { + grid_select = 'accept', + }, + }, + cancel = UI.Button { + x = -9, y = -2, + text = 'Cancel', + event = 'slide_hide', + }, + apply_filter = function(self, filter) + local t = { } + if filter then + filter = filter:lower() + self.grid.sortColumn = 'score' + self.grid.inverseSort = true + + for _,v in pairs(self.listing) do + v.score = fuzzy(v.lname, filter) + if v.score then + _insert(t, v) + end + end + else + self.grid.sortColumn = 'lname' + self.grid.inverseSort = false + t = self.listing + end + + self.grid:setValues(t) + self.grid:setIndex(1) + end, + show = function(self) + local listing = { } + local function recurse(dir) + local files = fs.list(dir) + for _,f in ipairs(files) do + local fullName = fs.combine(dir, f) + if fs.native.isDir(fullName) then -- skip virtual dirs + if f ~= '.git' then recurse(fullName) end + else + _insert(listing, { + name = f, + dir = dir, + lname = f:lower(), + fullName = fullName, + }) + end + end + end + recurse('') + self.listing = listing + self:apply_filter() + self.filter_entry:reset() + UI.SlideOut.show(self) + self:addTransition('expandUp', { easing = 'outBounce', ticks = 12 }) + end, + eventHandler = function(self, event) + if event.type == 'grid_up' then + self.grid:emit({ type = 'scroll_up' }) + + elseif event.type == 'grid_down' then + self.grid:emit({ type = 'scroll_down' }) + + elseif event.type == 'accept' then + local sel = self.grid:getSelected() + if sel then + actions.process('open', sel.fullName) + self:hide() + end + + elseif event.type == 'text_change' then + self:apply_filter(event.text) + self.grid:draw() + + else + return UI.SlideOut.eventHandler(self, event) + end + return true + end, + }, + completions = UI.SlideOut { + x = -12, y = 2, + transitionHint = 'slideLeft', + grid = UI.Grid { + x = 2, y = 2, ey = -2, + columns = { + { key = 'text', heading = 'Completion' }, + }, + accelerators = { + [ ' ' ] = 'down', + backspace = 'slide_hide', + }, + }, + cancel = UI.Button { + y = -1, x = -9, + text = 'Cancel', + backgroundColor = 'black', + backgroundFocusColor = 'black', + textColor = 'lightGray', + event = 'slide_hide', + }, + show = function(self, values) + local m = 12 + for _, v in pairs(values) do + m = #v.text > m and #v.text or m + end + m = m + 3 + m = m > self.parent.width and self.parent.width or m + self.ox = -m + self:resize() + self.grid:setValues(values) + self.grid:setIndex(1) + UI.SlideOut.show(self) + end, + eventHandler = function(self, event) + if event.type == 'grid_select' then + actions.process('insertText', x, y, event.selected.complete) + self:hide() + return true + end + return UI.SlideOut.eventHandler(self, event) + end, + }, + peripheral = UI.SlideOut { + x = '20%', y = 2, + transitionHint = 'slideLeft', + grid1 = UI.Grid { + x = 2, y = 2, ey = 5, + sortColumn = 'name', + columns = { + { key = 'name', heading = 'Peripheral' }, + }, + accelerators = { + [ ' ' ] = 'down', + backspace = 'slide_hide', + grid_focus_row = 'select_peripheral', + grid_select = 'complete', + }, + scan = function(self) + self.values = { } + for k, v in pairs(device) do + table.insert(self.values, { name = k, complete = 'peripheral.wrap("' .. v.side .. '")' }) + end + end, + postInit = function(self) + self:scan() + end, + }, + grid2 = UI.Grid { + x = 2, y = 6, ey = -2, + sortColumn = 'method', + columns = { + { key = 'method', heading = 'Method' }, + }, + accelerators = { + [ ' ' ] = 'down', + backspace = 'slide_hide', + grid_select = 'complete', + }, + showMethods = function(self) + local dev = device[self.parent.grid1:getSelected().name] + local t = { } + if dev then + pcall(function() + local docs = dev.getDocs and dev.getDocs() + for k, v in pairs(dev) do + if type(v) == 'function' then + local m = docs and docs[k] and docs[k]:match('^function%((.+)%).+') + table.insert(t, { method = k, complete = k .. '(' .. (m or '') .. ')' }) + end + end + end) + end + self:setValues(t) + end, + enable = function(self) + self:showMethods() + UI.Grid.enable(self) + end, + }, + cancel = UI.Button { + y = -1, x = -9, + text = 'Cancel', + backgroundColor = 'black', + backgroundFocusColor = 'black', + textColor = 'lightGray', + event = 'slide_hide', + }, + eventHandler = function(self, event) + if event.type == 'complete' then + actions.process('insertText', x, y, event.selected.complete) + actions.process('left') + self:hide() + return true + elseif event.type == 'select_peripheral' then + self.grid2:showMethods() + self.grid2:setIndex(1) + self.grid2:update() + self.grid2:draw() + end + return UI.SlideOut.eventHandler(self, event) + end, + }, + editor = UI.Window { + y = 2, + backgroundColor = bgColor, + transitionHint = 'slideRight', + cursorBlink = true, + focus = function(self) + if self.focused then + self:setCursorPos(x - scrollX, y - scrollY) + end + end, + resize = function(self) + UI.Window.resize(self) + + w, h = self.width, self.height + actions.set_cursor(x, y) + actions.dirty_all() + actions.redraw() + end, + draw = function() + actions.redraw() + end, + eventHandler = function(_, event) + if event.ie then + local action, param, param2 + local ie = event.ie + + if ie.code == 'char' then + action = keyMapping.char + param = ie.ch + + elseif ie.code == "mouse_click" or + ie.code == 'mouse_drag' or + ie.code == 'shift-mouse_click' or + ie.code == 'mouse_down' or + ie.code == 'mouse_doubleclick' then + + action = keyMapping[ie.code] + param = ie.x + scrollX + param2 = ie.y + scrollY + + elseif event.type == 'paste' then + action = keyMapping.paste + param = event.text + + else + action = keyMapping[ie.code] + end + + if action then + actions.process(action, param, param2) + return true + end + end + end, + }, + notification = UI.Notification { }, + enable = function(self) + UI.Page.enable(self) + self:setFocus(self.editor) + end, + checkFocus = function(self) + if not self.focused or not self.focused.enabled then + -- if no current focus, set it to the editor + self:setFocus(self.editor) + end + end, + eventHandler = function(self, event) + if event.type == 'menu_action' then + actions.process(event.element.action) + return true + end + return UI.Page.eventHandler(self, event) + end, } -if w < 32 then - messages = { - menu = '^s = save, ^q = quit', - wrapped = 'search wrapped', - } -end local function getFileInfo(path) - local abspath = shell.resolve(path) + path = fs.combine('/', path) local fi = { - abspath = abspath, path = path, - isNew = not fs.exists(abspath), - dirExists = fs.exists(fs.getDir(abspath)), - modified = false, + isNew = not fs.exists(path), + dirExists = fs.exists(fs.getDir(path)), + isReadOnly = fs.isReadOnly(path), } - if fi.isDir then - fi.isReadOnly = true - else - fi.isReadOnly = fs.isReadOnly(fi.abspath) + + if path ~= config.filename then + config.filename = path + config.recent = config.recent or { } + + Array.removeByValue(config.recent, path) + table.insert(config.recent, 1, path) + while #config.recent > 10 do + table.remove(config.recent) + end + + Config.update('editor', config) + end + + if multishell then + multishell.setTitle(multishell.getCurrent(), fs.getName(fi.path)) end return fi end -local function setStatus(pattern, ...) - sStatus = string.format(pattern, ...) -end - -local function setError(pattern, ...) - setStatus(pattern, ...) - isError = true -end - -local function load(path) - tLines = {} - if fs.exists(path) then - local file = io.open(path, "r") - local sLine = file:read() - while sLine do - table.insert(tLines, sLine) - sLine = file:read() - end - file:close() - end - - if #tLines == 0 then - table.insert(tLines, '') - end - - fileInfo = getFileInfo(tArgs[1]) - - local name = fileInfo.path - if w < 32 then - name = fs.getName(fileInfo.path) - end - if fileInfo.isNew then - if not fileInfo.dirExists then - setStatus('"%s" [New DIRECTORY]', name) - else - setStatus('"%s" [New File]', name) - end - elseif fileInfo.isReadOnly then - setStatus('"%s" [readonly] %dL, %dC', - name, #tLines, fs.getSize(fileInfo.abspath)) - else - setStatus('"%s" %dL, %dC', - name, #tLines, fs.getSize(fileInfo.abspath)) - end -end - -local function save( _sPath ) - -- Create intervening folder - local sDir = _sPath:sub(1, _sPath:len() - fs.getName(_sPath):len() ) - if not fs.exists( sDir ) then - fs.makeDir( sDir ) - end - - -- Save - local file = nil - local function innerSave() - file = fs.open( _sPath, "w" ) - if file then - for _,sLine in ipairs( tLines ) do - file.write(sLine .. "\n") - end - else - error( "Failed to open ".._sPath ) - end - end - - local ok, err = pcall( innerSave ) - if file then - file.close() - end - return ok, err -end - -local function split(str, pattern) - pattern = pattern or "(.-)\n" - local t = {} - local function helper(line) table.insert(t, line) return "" end - helper((str:gsub(pattern, helper))) - return t -end - -local tKeywords = { - ["and"] = true, - ["break"] = true, - ["do"] = true, - ["else"] = true, - ["elseif"] = true, - ["end"] = true, - ["false"] = true, - ["for"] = true, - ["function"] = true, - ["if"] = true, - ["in"] = true, - ["local"] = true, - ["nil"] = true, - ["not"] = true, - ["or"] = true, - ["repeat"] = true, - ["return"] = true, - ["then"] = true, - ["true"] = true, - ["until"]= true, - ["while"] = true, +local keywords = Util.transpose { + 'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'if', + 'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while' } -local function writeHighlighted(sLine, ny) - local buffer = { - fg = '', - text = '', - } +local function writeHighlighted(sLine, ny, dy) + local buffer = { fg = { }, text = { } } local function tryWrite(line, regex, fgcolor) local match = line:match(regex) if match then - local fg - if type(fgcolor) == "string" then - fg = fgcolor - else - fg = fgcolor(match) - end - buffer.text = buffer.text .. match - buffer.fg = buffer.fg .. string.rep(fg, #match) - return line:sub(#match + 1) + local fg = type(fgcolor) == "string" and fgcolor or fgcolor(match) + _insert(buffer.text, match) + _insert(buffer.fg, _rep(fg, #match)) + return _sub(line, #match + 1) end return nil end while #sLine > 0 do sLine = - tryWrite(sLine, "^%-%-%[%[.-%]%]", color.commentColor ) or - tryWrite(sLine, "^%-%-.*", color.commentColor ) or - tryWrite(sLine, "^\".-[^\\]\"", color.stringColor ) or - tryWrite(sLine, "^\'.-[^\\]\'", color.stringColor ) or - tryWrite(sLine, "^%[%[.-%]%]", color.stringColor ) or + -- tryWrite(sLine, "^[%\26]", '7' ) or + tryWrite(sLine, "^%-%-%[%[.-%]%]", color.comment ) or + tryWrite(sLine, "^%-%-.*", color.comment ) or + tryWrite(sLine, "^\".-[^\\]\"", color.string ) or + tryWrite(sLine, "^\'.-[^\\]\'", color.string ) or + tryWrite(sLine, "^%[%[.-%]%]", color.string ) or tryWrite(sLine, "^[%w_]+", function(match) - if tKeywords[match] then - return color.keywordColor - end - return color.textColor + return keywords[match] and color.keyword or color.text end) or - tryWrite(sLine, "^[^%w_]", color.textColor) + tryWrite(sLine, "^[^%w_]", color.text) end - buffer.fg = buffer.fg .. '7' - buffer.text = buffer.text .. '.' + buffer.fg = _concat(buffer.fg) .. '7' + buffer.text = _concat(buffer.text) .. '\183' if mark.active and ny >= mark.y and ny <= mark.ey then - local sx = 1 - if ny == mark.y then - sx = mark.x - end - local ex = #buffer.text - if ny == mark.ey then - ex = mark.ex - end - buffer.bg = string.rep('f', sx - 1) .. - string.rep('7', ex - sx) .. - string.rep('f', #buffer.text - ex + 1) - + local sx = ny == mark.y and mark.x or 1 + local ex = ny == mark.ey and mark.ex or #buffer.text + buffer.bg = _rep(color.bg, sx - 1) .. + _rep(color.mark, ex - sx) .. + _rep(color.bg, #buffer.text - ex + 1) else - buffer.bg = string.rep('f', #buffer.text) + buffer.bg = _rep(color.bg, #buffer.text) end - term.blit(buffer.text, buffer.fg, buffer.bg) + page.editor:blit(1 - scrollX, dy, buffer.text, buffer.bg, buffer.fg) end local function redraw() if dirty.y > 0 then - term.setBackgroundColor(color.bgColor) for dy = 1, h do - local sLine = tLines[dy + scrollY] if sLine ~= nil then if dy + scrollY >= dirty.y and dy + scrollY <= dirty.ey then - term.setCursorPos(1 - scrollX, dy) - term.clearLine() - writeHighlighted(sLine, dy + scrollY) + page.editor:clearLine(dy) + writeHighlighted(sLine, dy + scrollY, dy) end else - term.setCursorPos(1 - scrollX, dy) - term.clearLine() + page.editor:clearLine(dy) end end end - -- Draw status - if #sStatus > 0 then - if isError then - term.setTextColor(colors.white) - term.setBackgroundColor(color.errorBackground) - else - term.setTextColor(color.highlightColor) - term.setBackgroundColor(colors.gray) - end - term.setCursorPos(1, h) - term.clearLine() - term.write(string.format(' %s ', sStatus)) + local modifiedIndicator = undo.chain[#undo.chain] == lastSave and ' ' or '*' + page.menuBar.status.value = _format('%d:%d%s', y, x, modifiedIndicator) + page.menuBar.status:draw() + + if page.editor.focused then + page.editor:setCursorPos(x - scrollX, y - scrollY) end - if not (w < 32 and #sStatus > 0) then - local modifiedIndicator = ' ' - if undo.chain[1] then - modifiedIndicator = '*' - end - - local str = string.format(' %d:%d %s', - y, x, modifiedIndicator) - term.setTextColor(color.highlightColor) - term.setBackgroundColor(colors.gray) - term.setCursorPos(w - #str + 1, h) - term.write(str) - end - - term.setTextColor(color.cursorColor) - term.setCursorPos(x - scrollX, y - scrollY) - dirty.y, dirty.ey = 0, 0 - if #sStatus > 0 then - sStatus = '' - dirty.y = scrollY + h - dirty.ey = dirty.y - end - isError = false end local function nextWord(line, cx) @@ -407,148 +749,98 @@ local function nextWord(line, cx) end end -local function hacky_read() - local _oldSetCursorPos = term.setCursorPos - local _oldGetCursorPos = term.getCursorPos +actions = { + info = function(pattern, ...) + page.notification:info(_format(pattern, ...)) + end, - term.setCursorPos = function(cx) - return _oldSetCursorPos(cx, h) - end - term.getCursorPos = function() - local cx = _oldGetCursorPos() - return cx, 1 - end - - local s, m = pcall(function() return _G.read() end) - term.setCursorPos = _oldSetCursorPos - term.getCursorPos = _oldGetCursorPos - if s then - return m - end - if m == 'Terminated' then - bRunning = false - end - return '' -end - -local actions -local __actions = { - - input = function(prompt) - term.setTextColor(color.highlightColor) - term.setBackgroundColor(colors.gray) - term.setCursorPos(1, h) - term.clearLine() - term.write(prompt) - local str = hacky_read() - term.setCursorBlink(true) - input:reset() - term.setCursorPos(x - scrollX, y - scrollY) - actions.dirty_line(scrollY + h) - return str + error = function(pattern, ...) + page.notification:error(_format(pattern, ...)) end, undo = function() - local last = table.remove(undo.chain) + local last = _remove(undo.chain) if last then undo.active = true - actions[last.action](unpack(last.args)) + table.insert(undo.redo, { }) + for i = #last, 1, -1 do + local u = last[i] + actions[u.action](_unpack(u.args)) + end undo.active = false else - setStatus('Already at oldest change') + actions.info('already at oldest change') end end, - addUndo = function(entry) - local last = undo.chain[#undo.chain] - if last and last.action == entry.action then - if last.action == 'deleteText' then - if last.args[3] == entry.args[1] and - last.args[4] == entry.args[2] then - last.args = { - last.args[1], last.args[2], entry.args[3], entry.args[4], - last.args[5] .. entry.args[5] - } - else - table.insert(undo.chain, entry) - end - else - -- insertText (need to finish) - table.insert(undo.chain, entry) - end + undo_add = function(entry) + if undo.active then + local last = undo.redo[#undo.redo] + table.insert(last, entry) else - table.insert(undo.chain, entry) + if not undo.redo_active then + undo.redo = { } + end + local last = undo.chain[#undo.chain] + if last and undo.continue then + table.insert(last, entry) + else + _insert(undo.chain, { entry }) + end + end + end, + + redo = function() + local last = _remove(undo.redo) + if last then + -- too many flags ! + undo.redo_active = true + undo.continue = false + for i = #last, 1, -1 do + local u = last[i] + actions[u.action](_unpack(u.args)) + undo.continue = true + end + undo.redo_active = false + else + actions.info('already at newest change') end end, autocomplete = function() - if lastAction ~= 'autocomplete' or not complete.results then - local sLine = tLines[y]:sub(1, x - 1) - local nStartPos = sLine:find("[a-zA-Z0-9_%.]+$") - if nStartPos then - sLine = sLine:sub(nStartPos) + local sLine = tLines[y]:sub(1, x - 1):match("[a-zA-Z0-9_%.]+$") + local results = sLine and textutils.complete(sLine, _ENV) or { } + + if #results == 0 then + actions.error('no completions available') + + elseif #results == 1 then + actions.insertText(x, y, results[1]) + + elseif #results > 1 then + local prefix = sLine:match('^.+%.(.*)$') or sLine + for i = 1, #results do + results[i] = { + text = prefix .. results[i], + complete = results[i], + } end - if #sLine > 0 then - complete.results = textutils.complete(sLine) - else - complete.results = { } - end - complete.index = 0 - complete.x = x + page.completions:show(results) end + end, - if #complete.results == 0 then - setError('No completions available') - - elseif #complete.results == 1 then - actions.insertText(x, y, complete.results[1]) - complete.results = nil - - elseif #complete.results > 1 then - local prefix = complete.results[1] - for n = 1, #complete.results do - local result = complete.results[n] - while #prefix > 0 do - if result:find(prefix, 1, true) == 1 then - break - end - prefix = prefix:sub(1, #prefix - 1) - end - end - if #prefix > 0 then - actions.insertText(x, y, prefix) - complete.results = nil - else - if complete.index > 0 then - actions.deleteText(complete.x, y, complete.x + #complete.results[complete.index], y) - end - complete.index = complete.index + 1 - if complete.index > #complete.results then - complete.index = 1 - end - actions.insertText(complete.x, y, complete.results[complete.index]) - end - end + peripheral = function() + page.peripheral:show() end, refresh = function() actions.dirty_all() mark.continue = mark.active - setStatus('refreshed') - end, - - menu = function() - setStatus(messages.menu) - mark.continue = mark.active + actions.info('refreshed') end, goto_line = function() - local lineNo = tonumber(actions.input('Line: ')) - if lineNo then - actions.go_to(1, lineNo) - else - setStatus('Invalid line number') - end + page.gotoLine:show() end, find = function(pattern, sx) @@ -558,19 +850,18 @@ local __actions = { if ny > nLines then ny = ny - nLines end - local nx = tLines[ny]:lower():find(pattern, sx) + local nx = tLines[ny]:lower():find(pattern, sx, true) if nx then if ny < y or ny == y and nx <= x then - setStatus(messages.wrapped) + actions.info('search hit BOTTOM, continuing at TOP') end actions.go_to(nx, ny) actions.mark_to(nx + #pattern, ny) - actions.go_to(nx, ny) return end sx = 1 end - setError('Pattern not found') + actions.error('pattern not found') end, find_next = function() @@ -581,58 +872,174 @@ local __actions = { end, find_prompt = function() - local text = actions.input('/') - if #text > 0 then - searchPattern = text:lower() - if searchPattern then - actions.unmark() - actions.find(searchPattern, x) - end - end + page.search:show() end, - save = function() - if bReadOnly then - setError("Access denied") + quick_open = function(force) + if not force and undo.chain[#undo.chain] ~= lastSave then + page.unsaved:show('quick_open') else - local ok = save(sPath) - if ok then - setStatus('"%s" %dL, %dC written', - fileInfo.path, #tLines, fs.getSize(fileInfo.abspath)) + page.quick_open:show() + end + end, + + file_open = function(force) + if not force and undo.chain[#undo.chain] ~= lastSave then + page.unsaved:show('file_open') + else + page.file_open:show() + end + end, + + recent = function(force) + if not force and undo.chain[#undo.chain] ~= lastSave then + page.unsaved:show('recent') + else + page.recent:show() + end + end, + + file_new = function(force) + if not force and undo.chain[#undo.chain] ~= lastSave then + page.unsaved:show('file_new') + else + actions.open('/untitled.txt') + end + end, + + open = function(filename) + if not actions.load(filename) then + actions.error('unable to load file') + end + end, + + load = function(path) + if not path or (fs.exists(path) and fs.isDir(path)) then + return false + end + fileInfo = getFileInfo(path) + + x, y = 1, 1 + scrollX, scrollY = 0, 0 + lastPos = { x = 1, y = 1 } + lastSave = nil + dirty = { y = 1, ey = h } + mark = { } + undo = { chain = { }, redo = { } } + + tLines = Util.readLines(fileInfo.path) or { } + if #tLines == 0 then + _insert(tLines, '') + end + + --[[ + local function detabify(l) + return l:gsub('\26\26', '\9'):gsub('\26', '\9') + end ]] + + -- since we can't handle tabs, convert them to spaces :( + local t1, t2 = ' ', ' ' + local function tabify(l) + repeat + local i = l:find('\9') + if i then + local tabs = (i - 1) % 2 == 0 and t2 or t1 + l = l:sub(1, i - 1) .. tabs .. l:sub(i + 1) + end + until not i + return l + end + + for k, v in pairs(tLines) do + tLines[k] = tabify(v) + end + + local name = fileInfo.path + if fileInfo.isNew then + if not fileInfo.dirExists then + actions.info('"%s" [New DIRECTORY]', name) else - setError("Error saving to %s", sPath) + actions.info('"%s" [New File]', name) + end + elseif fileInfo.isReadOnly then + actions.info('"%s" [readonly] %dL, %dC', + name, #tLines, fs.getSize(fileInfo.path)) + else + actions.info('"%s" %dL, %dC', + name, #tLines, fs.getSize(fileInfo.path)) + end + + return true + end, + + save = function(filename) + filename = filename or fileInfo.path + if fs.isReadOnly(filename) then + actions.error("access denied") + else + local s, m = pcall(function() + if not Util.writeLines(filename, tLines) then + error("Failed to open " .. filename) + end + end) + + if s then + lastSave = undo.chain[#undo.chain] + fileInfo = getFileInfo(filename) + actions.info('"%s" %dL, %dC written', + fileInfo.path, #tLines, fs.getSize(fileInfo.path)) + return true + else + actions.error(m) end end end, - exit = function() - bRunning = false + save_as = function() + page.save_as:show() + end, + + exit = function(force) + if not force and undo.chain[#undo.chain] ~= lastSave then + page.unsaved:show('exit') + else + UI:quit() + end end, run = function() - input:reset() - local sTempPath = "/.temp" - local ok = save(sTempPath) - if ok then - local nTask = shell.openTab(sTempPath) + if not multishell then + actions.error('open available with multishell') + return + end + if undo.chain[#undo.chain] == lastSave then + local nTask = shell.openTab(fileInfo.path) if nTask then shell.switchTab(nTask) else - setError("Error starting Task") + actions.error("error starting Task") end - os.sleep(0) - fs.delete(sTempPath) else - setError("Error saving to %s", sTempPath) + local fn, msg = load(_concat(tLines, '\n'), fileInfo.path) + if fn then + multishell.openTab({ + fn = fn, + focused = true, + title = fs.getName(fileInfo.path), + }) + else + local ln = msg:match(':(%d+):') + if ln and tonumber(ln) then + actions.go_to(1, tonumber(ln)) + end + actions.error(msg) + end end end, status = function() - local modified = '' - if undo.chain[1] then - modified = '[Modified] ' - end - setStatus('"%s" %s%d lines --%d%%--', + local modified = undo.chain[#undo.chain] == lastSave and '' or '[Modified] ' + actions.info('"%s" %s%d lines --%d%%--', fileInfo.path, modified, #tLines, math.floor((y - 1) / (#tLines - 1) * 100)) end, @@ -738,12 +1145,40 @@ local __actions = { actions.mark_finish() end, + mark_line = function() + actions.home() + actions.mark_begin() + actions.toend() + actions.right() + actions.mark_finish() + end, + mark_word = function() actions.mark_begin() actions.word() actions.mark_finish() end, + mark_current_word = function(cx, cy) + local index = 1 + actions.go_to(cx, cy) + while true do + local s, e = tLines[y]:find('%w+', index) + if not s or s - 1 > x then + break + end + if x >= s and x <= e then + x = s + actions.mark_begin() + x = e + 1 + actions.mark_finish() + x, y = cx, cy + break + end + index = e + 1 + end + end, + mark_backword = function() actions.mark_begin() actions.backword() @@ -773,7 +1208,7 @@ local __actions = { actions.dirty_all() end, - setCursor = function() + set_cursor = function() lastPos.x = x lastPos.y = y @@ -781,18 +1216,18 @@ local __actions = { local screenY = y - scrollY if screenX < 1 then - scrollX = x - 1 + scrollX = math.max(0, x - 4) actions.dirty_all() elseif screenX > w then - scrollX = x - w + scrollX = x - w + 3 actions.dirty_all() end if screenY < 1 then scrollY = y - 1 actions.dirty_all() - elseif screenY > h - 1 then - scrollY = y - (h - 1) + elseif screenY > h then + scrollY = y - h actions.dirty_all() end end, @@ -827,12 +1262,12 @@ local __actions = { actions.insertText(x, y, ' ') end, - pageUp = function() - actions.go_to(x, y - (h - 1)) + page_up = function() + actions.go_to(x, y - h) end, - pageDown = function() - actions.go_to(x, y + (h - 1)) + page_down = function() + actions.go_to(x, y + h) end, home = function() @@ -907,39 +1342,35 @@ local __actions = { actions.dirty_line(y) x = x + #text else - local lines = split(text) + local lines = Util.split(text) local remainder = sLine:sub(x) tLines[y] = sLine:sub(1, x - 1) .. lines[1] actions.dirty_range(y, #tLines + #lines) x = x + #lines[1] for k = 2, #lines do y = y + 1 - table.insert(tLines, y, lines[k]) + _insert(tLines, y, lines[k]) x = #lines[k] + 1 end tLines[y] = tLines[y]:sub(1, x) .. remainder end - if not undo.active then - actions.addUndo( - { action = 'deleteText', args = { sx, sy, x, y, text } }) - end + actions.undo_add( + { action = 'deleteText', args = { sx, sy, x, y } }) end, deleteText = function(sx, sy, ex, ey) x = sx y = sy - if not undo.active then - local text = actions.copyText(sx, sy, ex, ey) - actions.addUndo( - { action = 'insertText', args = { sx, sy, text } }) - end + local text = actions.copyText(sx, sy, ex, ey) + actions.undo_add( + { action = 'insertText', args = { sx, sy, text } }) local front = tLines[sy]:sub(1, sx - 1) local back = tLines[ey]:sub(ex, #tLines[ey]) for _ = 2, ey - sy + 1 do - table.remove(tLines, y + 1) + _remove(tLines, y + 1) end tLines[y] = front .. back if sy ~= ey then @@ -966,10 +1397,10 @@ local __actions = { end local str = line:sub(cx, ex) count = count + #str - table.insert(lines, str) + _insert(lines, str) end end - return table.concat(lines, '\n'), count + return _concat(lines, '\n'), count end, delete = function() @@ -986,9 +1417,7 @@ local __actions = { end, backspace = function() - if mark.active then - actions.delete() - elseif actions.left() then + if mark.active or actions.left() then actions.delete() end end, @@ -1003,7 +1432,7 @@ local __actions = { if mark.active then actions.delete() end - actions.insertText(x, y, '\n' .. string.rep(' ', spaces)) + actions.insertText(x, y, '\n' .. _rep(' ', spaces)) end, char = function(ch) @@ -1016,7 +1445,7 @@ local __actions = { copy_marked = function() local text = actions.copyText(mark.x, mark.y, mark.ex, mark.ey) os.queueEvent('clipboard_copy', text) - setStatus('shift-^v to paste') + actions.info('shift-^v to paste') end, cut = function() @@ -1039,12 +1468,16 @@ local __actions = { end if text then actions.insertText(x, y, text) - setStatus('%d chars added', #text) + actions.info('%d chars added', #text) else - setStatus('Clipboard empty') + actions.info('clipboard empty') end end, + paste_internal = function() + os.queueEvent('clipboard_paste') + end, + go_to = function(cx, cy) y = math.min(math.max(cy, 1), #tLines) x = math.min(math.max(cx, 1), #tLines[y] + 1) @@ -1059,54 +1492,19 @@ local __actions = { end, scroll_down = function() - local nMaxScroll = #tLines - (h-1) + local nMaxScroll = #tLines - h if scrollY < nMaxScroll then scrollY = scrollY + 1 actions.dirty_all() end mark.continue = mark.active end, -} -actions = __actions + redraw = function() + redraw() + end, -load(sPath) -term.setCursorBlink(true) -redraw() - -while bRunning do - local sEvent, param, param2, param3 = os.pullEventRaw() - local action - - if sEvent == 'terminate' then - action = 'exit' - elseif sEvent == 'multishell_focus' then -- opus only event - input:reset() - elseif sEvent == "mouse_click" or - sEvent == 'mouse_drag' or - sEvent == 'mouse_up' or - sEvent == 'mouse_down' then - local ie = input:translate(sEvent, param, param2, param3) - if param3 < h or sEvent == 'mouse_drag' then - if ie.code then - action = keyMapping[ie.code] - param = param2 + scrollX - param2 = param3 + scrollY - end - end - else - local ie = input:translate(sEvent, param, param2) - if ie then - if ie.ch and #ie.ch == 1 then - action = keyMapping.char - param = ie.ch - else - action = keyMapping[ie.code] - end - end - end - - if action then + process = function(action, ...) if not actions[action] then error('Invaid action: ' .. action) end @@ -1114,31 +1512,35 @@ while bRunning do local wasMarking = mark.continue mark.continue = false - actions[action](param, param2) - if action ~= 'menu' then - lastAction = action - end + -- for undo purposes, treat tab and enter as char actions + local a = (action == 'tab' or action == 'enter') and 'char' or action + undo.continue = a == undo.lastAction + + actions[action](...) + + undo.lastAction = a if x ~= lastPos.x or y ~= lastPos.y then - actions.setCursor() + actions.set_cursor() end if not mark.continue and wasMarking then actions.unmark() end - redraw() + actions.redraw() + end, +} - elseif sEvent == "term_resize" then - w,h = term.getSize() - actions.setCursor(x, y) - actions.dirty_all() - redraw() - end +local args = { ... } +local filename = args[1] and shell.resolve(args[1]) +if not (actions.load(filename) or actions.load(config.filename) or actions.load('untitled.txt')) then + error('Error opening file') end --- Cleanup -term.setBackgroundColor(colors.black) -term.setTextColor(colors.white) -term.clear() -term.setCursorBlink(false) -term.setCursorPos(1, 1) +UI:setPage(page) +local s, m = pcall(function() UI:start() end) +if not s then + actions.save('/crash.txt') + print('Editor has crashed. File saved as /crash.txt') + error(m) +end diff --git a/common/etc/apps.db b/common/etc/apps.db index d12829c..d8608bb 100644 --- a/common/etc/apps.db +++ b/common/etc/apps.db @@ -57,7 +57,7 @@ category = "Apps", requires = "advancedComputer", iconExt = "\030 \031 \128\030d\159\030 \031d\140\030d\031 \155\030 \0315\140\0305\031 \155\030 \128\010\030 \031d\136\145\0315\136\145\031d\153\031 \128\0315\153\010\030 \031 \128\031d\130\140\134\0315\140\134\031 \128", - run = "packages/common/hexedit.lua", + run = "fileui --exec=hexedit.lua", }, [ "fb1c39e9f4f3c2628ad173ab401a6e4e4baf783d" ] = { title = "Sounds", @@ -65,4 +65,17 @@ run = "SoundPlayer", iconExt = "\030 \031 \128\0307\159\129\030 \0317\149\0310\144\0300\031 \155\030 \0310\137\144\010\0307\0317\128\128\128\030 \149\0300\031 \149\030 \128\0310\149\0300\031 \149\010\030 \031 \128\0317\130\0307\031 \144\030 \0317\149\0310\129\134\152\129", }, + [ "464c4ffd019e1e9691dcf0537c797353ef2b1c1d4833d3d463e5b74ae4547344" ] = { + title = "Editor", + category = "Apps", + run = "edit", + iconExt = "7\ +¨¨¨¨f€0¨\ +¨¨f€0¨¨f€", + }, + [ "3f00927a719345edd4a8316599d3b328857987547f8884306861161ffa09647e" ] = { + title = "Write", + category = "Apps", + run = "write", + }, } diff --git a/common/etc/scripts/goHome b/common/etc/scripts/goHome index 2749820..94fd138 100644 --- a/common/etc/scripts/goHome +++ b/common/etc/scripts/goHome @@ -1,5 +1,5 @@ _G.requireInjector(_ENV) -local config = require('config').load('gps') +local config = require('opus.config').load('gps') if config.home then if turtle.enableGPS() then return turtle.pathfind(config.home) diff --git a/common/etc/scripts/moveTo b/common/etc/scripts/moveTo deleted file mode 100644 index c68e1db..0000000 --- a/common/etc/scripts/moveTo +++ /dev/null @@ -1,29 +0,0 @@ -turtle.run(function() - - _G.requireInjector(_ENV) - - local GPS = require('gps') - local Socket = require('socket') - - local id = {COMPUTER_ID} - - if not turtle.enableGPS() then - error('turtle: No GPS found') - end - - local socket = Socket.connect(id, 161) - if not socket then - error('turtle: Unable to connect to ' .. id) - end - - socket:write({ type = 'gps' }) - - local pt = socket:read(3) - if not pt then - error('turtle: No GPS response') - end - - if not turtle.pathfind(pt) then - error('Unable to go to location') - end -end) diff --git a/common/etc/scripts/moveTo.lua b/common/etc/scripts/moveTo.lua new file mode 100644 index 0000000..72d597a --- /dev/null +++ b/common/etc/scripts/moveTo.lua @@ -0,0 +1,17 @@ +local turtle = _G.turtle + +turtle.run(function() + _G.requireInjector(_ENV) + + local GPS = require('opus.gps') + + if not turtle.enableGPS() then + error('turtle: No GPS found') + end + + local pt = {GPS} + + if not turtle.pathfind(pt) then + error('Unable to go to location') + end +end) diff --git a/common/etc/scripts/setHome b/common/etc/scripts/setHome index 9f88beb..be69170 100644 --- a/common/etc/scripts/setHome +++ b/common/etc/scripts/setHome @@ -1,5 +1,5 @@ _G.requireInjector(_ENV) -local Config = require('config') +local Config = require('opus.config') local pt = turtle.enableGPS() if pt then local config = Config.load('gps', { }) diff --git a/common/etc/scripts/summon b/common/etc/scripts/summon index 45e0c9e..66dcded 100644 --- a/common/etc/scripts/summon +++ b/common/etc/scripts/summon @@ -2,9 +2,9 @@ local function summon(id) _G.requireInjector(_ENV) - local GPS = require('gps') - local Point = require('point') - local Socket = require('socket') + local GPS = require('opus.gps') + local Point = require('opus.point') + local Socket = require('opus.socket') turtle.setStatus('GPSing') turtle.setPoint({ x = 0, y = 0, z = 0, heading = 0 }) diff --git a/common/recorder.lua b/common/recorder.lua index e6c7a94..4ea8824 100644 --- a/common/recorder.lua +++ b/common/recorder.lua @@ -19,10 +19,14 @@ local Util = require('opus.util') local multishell = _ENV.multishell local os = _G.os -local recTerm, oldTerm, arg, showInput, skipLast, lastDelay, curInput = {}, Util.shallowCopy(multishell.term), {...}, false, false, 2, "" +local colours = _G.colors + +local args, options = Util.parse(...) + +local oldTerm, showInput, skipLast, lastDelay, curInput = Util.shallowCopy(multishell.term), false, false, 2, "" local curBlink, oldBlink, tTerm, buffer, colourNum, xPos, yPos, oldXPos, oldYPos, tCol, bCol, xSize, ySize = false, false, {}, {}, {}, 1, 1, 1, 1, colours.white, colours.black, oldTerm.getSize() local greys, buttons = {["0"] = true, ["7"] = true, ["8"] = true, ["f"] = true}, {"l", "r", "m"} -local charW, charH, chars, resp +local charW, charH, chars local calls = { } local curCalls = { delay = 0 } @@ -32,38 +36,44 @@ local callCount = 0 local function showSyntax() print('Gif Recorder by Bomb Bloke\n') print('Syntax: recGif [-i] [-s] [-ld:] filename') - print(' -i : show input') - print(' -s : skip last') - print(' -ld : last delay') + print(' --showInput : show input') + print(' --skipLast : skip last') + print(' --lastDelay : last delay') + print(' --noResize : dont resize') end -for i = #arg, 1, -1 do - local curArg = arg[i]:lower() - - if curArg == "-i" then - showInput, ySize = true, ySize + 1 - table.remove(arg, i) - elseif curArg == "-s" then - skipLast = true - table.remove(arg, i) - elseif curArg:sub(1, 4) == "-ld:" then - curArg = tonumber(curArg:sub(5)) - if curArg then lastDelay = curArg end - table.remove(arg, i) - elseif curArg == '-?' then - showSyntax() - return - elseif i ~= #arg then - showSyntax() - printError('\nInvalid argument') - return - end +if options.showInput then + showInput, ySize = true, ySize + 1 end -print('Gif Recorder by Bomb Bloke\n') -print('Press control-p to stop recording') +if options.skipLast then + skipLast = true +end -local filename = arg[#arg] +if options.lastDelay then + lastDelay = options.lastDelay +end + +if options.help then + showSyntax() + return +end + +if options.daemon then + device.keyboard.addHotkey('control-P', function() + multishell.openTab({ + path = 'sys/apps/shell.lua', + args = { arg[0], '--noResize', '--rawOutput', 'recorder.gif' }, + }) + end) + return +end + +print('Gif Recorder by Bomb Bloke') +print(version) +print('\nPress control-p to stop recording') + +local filename = args[1] if not filename then print('Enter file name:') filename = read() @@ -131,37 +141,34 @@ end -- Build a terminal that records stuff: -recTerm = multishell.term +local recTerm = multishell.term for key, func in pairs(oldTerm) do - recTerm[key] = function(...) - local result = { func(...) } + if type(func) == 'function' then + recTerm[key] = function(...) + local result = { func(...) } - if callCount == 0 then - os.queueEvent('capture_frame') + if callCount == 0 then + os.queueEvent('capture_frame') + end + callCount = callCount + 1 + curCalls[callCount] = { key, ... } + return unpack(result) end - callCount = callCount + 1 - curCalls[callCount] = { key, ... } - return unpack(result) end end local tabId = multishell.getCurrent() +multishell.hideTab(tabId) + +if not options.noResize then + os.queueEvent('term_resize') +end _G.device.keyboard.addHotkey('control-p', function() os.queueEvent('recorder_stop') end) -local tabs = multishell.getTabs() -for _,tab in pairs(tabs) do - if tab.isOverview then - multishell.hideTab(tabId) - multishell.setFocus(tab.tabId) - os.queueEvent('term_resize') - break - end -end - local curTime = os.clock() - 1 while true do @@ -200,8 +207,12 @@ if skipLast and #calls > 1 then calls[#calls] = nil end calls[#calls].delay = lastDelay +if options.rawOutput then + Util.writeTable('tmp/raw.txt', calls) + return +end + print(string.format("Encoding %d frames...", #calls)) ---Util.writeTable('tmp/raw.txt', calls) -- Perform a quick re-parse of the recorded data (adding frames for when the cursor blinks): diff --git a/examples/.package b/examples/.package deleted file mode 100644 index da8c112..0000000 --- a/examples/.package +++ /dev/null @@ -1,6 +0,0 @@ -{ - title = 'Example programs for coding in Opus', - repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/examples', - description = [[Starter/example programs for creating Opus programs.]], - license = 'MIT', -} diff --git a/examples/grid.lua b/examples/grid.lua deleted file mode 100644 index b979a87..0000000 --- a/examples/grid.lua +++ /dev/null @@ -1,44 +0,0 @@ -local UI = require('opus.ui') - -local page = UI.Page { - menuBar = UI.MenuBar { - buttons = { - { text = 'Shuffle', event = 'shuffle' }, - { text = 'Clear', event = 'clear', }, - } - }, - grid = UI.ScrollingGrid { - y = 2, ey = -2, - values = { - { col = 'column1', value = 'value1' }, - { col = 'column2', value = 'value2' }, - { col = 'column3', value = 'value3' }, - }, - columns = { - { heading = 'HDR1', key = 'col' }, - { heading = 'HDR2', key = 'value' }, - } - }, - statusBar = UI.StatusBar { }, -} - -function page:eventHandler(event) - if event.type == 'grid_select' then - self.statusBar:setStatus('Selected: ' .. event.selected.value) - - elseif event.type == 'shuffle' then - for _,v in pairs(self.grid.values) do - v.col = 'column' .. math.random(1,3) - end - self.grid:update() - self.grid:draw() - - elseif event.type == 'clear' then - self.grid:setValues({ }) - self.grid:draw() - end - return UI.Page.eventHandler(self, event) -end - -UI:setPage(page) -UI:pullEvents() diff --git a/gps/gpsServer.lua b/gps/gpsServer.lua index 58461d6..d66b575 100644 --- a/gps/gpsServer.lua +++ b/gps/gpsServer.lua @@ -307,4 +307,4 @@ else end UI:setPage(page) -UI:pullEvents() +UI:start() diff --git a/ignore/Music.lua b/ignore/Music.lua index 2af2b86..7ff44c2 100644 --- a/ignore/Music.lua +++ b/ignore/Music.lua @@ -250,5 +250,3 @@ turtle.setStatus('Jamming') UI:pullEvents() turtle.setStatus('idle') page:play(false) - -UI.term:reset() diff --git a/ignore/passthrough.lua b/ignore/passthrough.lua new file mode 100644 index 0000000..dae7403 --- /dev/null +++ b/ignore/passthrough.lua @@ -0,0 +1,174 @@ +local _rep = string.rep +local _sub = string.sub +local colors = _G.colors + +local palette = { } + +for n = 1, 16 do + palette[2 ^ (n - 1)] = _sub("0123456789abcdef", n, n) +end + +local swindow = { } + +function swindow.createPassthrough(parent, wx, wy, width, height) + local window = { } + local cx, cy = 1, 1 + local blink = false + local fg = colors.white + local bg = colors.black + + local function crop(text, x) + local w = #text + + if x < 1 then + text = _sub(text, 2 - x) + w = w + x - 1 + x = 1 + end + + if x + w - 1 > width then + text = _sub(text, 1, width - x + 1) + end + + return text + end + + local function blit(text, fg, bg) + parent.setCursorPos(cx + wx - 1, cy + wy - 1) + parent.blit(text, fg, bg) + cx = cx + #text + end + + function window.write(text) + if cy > 0 and cy <= height then + text = crop(tostring(text), cx) + if #text > 0 then + --parent.setCursorPos(cx + wx - 1, cy + wy - 1) + blit(text, _rep(palette[fg], #text), _rep(palette[bg], #text)) + end + end + end + + function window.blit(text, fg, bg) + if cy > 0 and cy <= height then + text = crop(tostring(text), cx) + if #text > 0 then + blit(text, crop(tostring(fg), cx), crop(tostring(bg), cx)) + end + end + end + + function window.clear() + local filler = _rep(' ', width) + for i = 1, height do + parent.setCursorPos(wx, i + wy - 1) + parent.write(filler) + end + end + + function window.clearLine() + if cy > 0 and cy <= height then + local filler = _rep(' ', width) + parent.setCursorPos(cx + wx - 1, cy + wy - 1) + parent.write(filler) + end + end + + function window.getCursorPos() + return cx, cy + end + + function window.setCursorPos(x, y) + cx = math.floor(x) + cy = math.floor(y) + parent.setCursorPos(cx + wx - 1, cy + wy - 1) + end + + function window.setCursorBlink(b) + blink = b + parent.setCursorBlink(b) + end + + function window.getCursorBlink() + return blink + end + + window.isColor = parent.isColor + window.isColour = parent.isColour + window.setPaletteColour = parent.setPaletteColour + window.setPaletteColor = parent.setPaletteColor + window.getPaletteColour = parent.getPaletteColour + window.getPaletteColor = parent.getPaletteColour + window.setBackgroundColor = parent.setBackgroundColor + window.setBackgroundColour = parent.setBackgroundColor + window.getBackgroundColor = parent.getBackgroundColor + window.getBackgroundColour = parent.getBackgroundColor + window.setVisible = parent.setVisible + window.redraw = function() end --parent.redraw + + function window.getTextColor() + return fg + end + window.getTextColour = window.getTextColor + + function window.setTextColor(textColor) + fg = textColor + parent.setTextColor(fg) + end + window.setTextColour = window.setTextColor + + function window.restoreCursor() + parent.setCursorPos(cx + wx - 1, cy + wy - 1) + parent.setTextColor(fg) + parent.setCursorBlink(blink) + end + + function window.getSize() + return width, height + end + + function window.scroll( n ) + if n ~= 0 then + local lines = { } + for i = 1, height do + lines[i] = { parent.getLine(wy + i - 1) } + end + + for newY = 1, height do + local y = newY + n + parent.setCursorPos(wx, wy + newY - 1) + if y >= 1 and y <= height then + parent.blit(table.unpack(lines[y])) + else + parent.blit( + _rep(' ', width), + _rep(palette[fg], width), + _rep(palette[bg], width)) + end + end + parent.setCursorPos(cx + wx - 1, cy + wy - 1) + end + end + + function window.getLine(y) + local t, tc, bc = parent.getLine(y + cy - 1) + return t:sub(1, width), tc:sub(1, width), bc:sub(1, width) + end + + function window.getPosition() + return wx, wy + end + + function window.reposition(nNewX, nNewY, nNewWidth, nNewHeight, newParent) + wx = nNewX + wy = nNewY + width = nNewWidth + height = nNewHeight + + window.restoreCursor() + end + + return window +end + +return swindow diff --git a/ignore/twm.lua b/ignore/twm.lua index 24f908a..07bfc3e 100644 --- a/ignore/twm.lua +++ b/ignore/twm.lua @@ -4,42 +4,21 @@ local Util = require('opus.util') local colors = _G.colors local os = _G.os -local peripheral = _G.peripheral local printError = _G.printError -local shell = _ENV.shell local term = _G.term local window = _G.window -local function syntax() - printError('Syntax:') - error('mwm [--config=filename] [monitor]') -end - -local args = Util.parse(...) local UID = 0 local multishell = { } local processes = { } local parentTerm = term.current() -local sessionFile = args.config or 'usr/config/mwm' -local monName = args[1] +local sessionFile = 'usr/config/twm' local running -local parentMon +local parentMon = term.current() local defaultEnv = Util.shallowCopy(_ENV) defaultEnv.multishell = multishell -if monName == 'terminal' then - parentMon = term.current() -elseif monName then - parentMon = peripheral.wrap(monName) or syntax() -else - parentMon = peripheral.find('monitor') or syntax() -end - -if parentMon.setTextScale then - parentMon.setTextScale(.5) -end - local monDim, termDim = { }, { } monDim.width, monDim.height = parentMon.getSize() termDim.width, termDim.height = parentTerm.getSize() @@ -73,13 +52,10 @@ local function write(win, x, y, text) end local function redraw() - --monitor.clear() monitor.canvas:dirty() - --monitor.setBackgroundColor(colors.gray) monitor.canvas:clear(colors.gray) - for k, process in ipairs(processes) do + for _, process in ipairs(processes) do process.container.canvas:dirty() - process:focus(k == #processes) end end @@ -112,7 +88,8 @@ function Process:new(args) height = args.height + 1, path = args.path, args = args.args or { }, - title = args.title or 'shell', + title = args.title or 'shell', + timestamp = os.clock(), isMoving = false, isResizing = false, }, { __index = Process }) @@ -155,20 +132,12 @@ function Process:new(args) end function Process:focus(focused) - if focused then - self.container.setBackgroundColor(colors.yellow) - else - self.container.setBackgroundColor(colors.lightGray) - end + self.container.setBackgroundColor(focused and colors.yellow or colors.lightGray) self.container.setTextColor(colors.black) write(self.container, 1, 1, string.rep(' ', self.width)) write(self.container, 2, 1, self.title) write(self.container, self.width - 1, 1, '*') write(self.container, self.width - 3, 1, '\029') - - if focused then - self.window.restoreCursor() - end end function Process:drawSizers() @@ -188,17 +157,18 @@ function Process:adjustDimensions() self.y = math.min(self.y, monDim.height - self.height + 1) end -function Process:reposition() +function Process:reposition(resizing) self:adjustDimensions() self.container.reposition(self.x, self.y, self.width, self.height) - self.container.setBackgroundColor(colors.black) - self.container.clear() self.window.reposition(1, 2, self.width, self.height - 1) if self.window ~= self.terminal then if self.terminal.reposition then -- ?? self.terminal.reposition(1, 1, self.width, self.height - 1) end - end + end + if resizing then + self:focus(self == processes[#processes]) + end redraw() end @@ -225,7 +195,7 @@ function Process:resize(x, y) self.height = y - self.isResizing.y + self.isResizing.h self.width = x - self.isResizing.x + self.isResizing.w - self:reposition() + self:reposition(true) self:resume('term_resize') self:drawSizers() multishell.saveSession(sessionFile) @@ -278,7 +248,8 @@ function multishell.setFocus(uid) process.container.canvas:raise() process:focus(true) - process.container.canvas:dirty() + process.container.canvas:dirty() + process.window.restoreCursor() end return true end @@ -423,14 +394,15 @@ function multishell.start() elseif focused.isMoving then focused.x = event[3] - focused.isMoving.x + focused.isMoving.ox focused.y = event[4] - focused.isMoving.y + focused.isMoving.oy - focused:reposition() + focused:reposition(false) end end elseif event[1] == 'char' or event[1] == 'key' or event[1] == 'key_up' or - event[1] == 'paste' then + event[1] == 'paste' or + event[1] == 'mouse_scroll' then local focused = processes[#processes] if focused then diff --git a/lzwfs/system/lzwfs.lua b/lzwfs/system/lzwfs.lua index 2c76f30..3f0a7a2 100644 --- a/lzwfs/system/lzwfs.lua +++ b/lzwfs/system/lzwfs.lua @@ -16,16 +16,19 @@ local config = Config.load('lzwfs', { local tab = UI.Tab { tabTitle = 'Compression', description = 'Disk compression', + [1] = UI.Window { + x = 2, y = 2, ex = -2, ey = 6, + }, label1 = UI.Text { - x = 2, y = 2, + x = 3, y = 3, value = 'Enable compression', }, checkbox = UI.Checkbox { - x = 20, y = 2, + x = 21, y = 3, value = config.enabled }, entry = UI.TextEntry { - x = 2, y = 4, ex = -2, + x = 3, y = 5 , ex = -3, limit = 256, shadowText = 'enter new path', accelerators = { @@ -34,7 +37,7 @@ local tab = UI.Tab { help = 'add a new path', }, grid = UI.Grid { - x = 2, ex = -2, y = 6, ey = -5, + x = 2, ex = -2, y = 8, ey = -5, disableHeader = true, columns = { { key = 'value' } }, autospace = true, @@ -45,7 +48,7 @@ local tab = UI.Tab { }, }, button = UI.Button { - x = -9, ex = -2, y = -3, + x = -8, ex = -2, y = -3, text = 'Apply', event = 'apply', }, diff --git a/milo/MiloLocal.lua b/milo/MiloLocal.lua index 68b409f..7ba699b 100644 --- a/milo/MiloLocal.lua +++ b/milo/MiloLocal.lua @@ -216,7 +216,7 @@ _G._syslog = function(...) end local s, m = pcall(function() - UI:pullEvents() + UI:start() end) turtle.setStatus('idle') diff --git a/milo/MiloRemote.lua b/milo/MiloRemote.lua index 32cbd06..1f2caa8 100644 --- a/milo/MiloRemote.lua +++ b/milo/MiloRemote.lua @@ -1,6 +1,6 @@ local Config = require('opus.config') local Event = require('opus.event') -local fuzzy = require('milo.fuzzyMatch') +local fuzzy = require('opus.fuzzy') local Sound = require('opus.sound') local Socket = require('opus.socket') local sync = require('opus.sync').sync @@ -63,8 +63,8 @@ local page = UI.Page { x = 1, ex = -13, limit = 50, shadowText = 'filter', - backgroundColor = colors.cyan, - backgroundFocusColor = colors.cyan, + backgroundColor = 'primary', + backgroundFocusColor = 'primary', accelerators = { [ 'enter' ] = 'eject', [ 'up' ] = 'grid_up', @@ -169,7 +169,7 @@ end function page:eventHandler(event) if event.type == 'quit' then - UI:exitPullEvents() + UI:quit() elseif event.type == 'setup' then self.setup.form:setValues(context.state) @@ -517,14 +517,14 @@ local function loadDirectory(dir) }) end end - page.menuBar.config:add({ dropmenu = UI.DropMenu { buttons = dropdown } }) + page.menuBar.config.dropdown = dropdown end local programDir = fs.getDir(shell.getRunningProgram()) loadDirectory(fs.combine(programDir, 'plugins/remote')) UI:setPage(page) -UI:pullEvents() +UI:start() if context.socket then context.socket:close() diff --git a/milo/apis/fuzzyMatch.lua b/milo/apis/fuzzyMatch.lua deleted file mode 100644 index 7a6082d..0000000 --- a/milo/apis/fuzzyMatch.lua +++ /dev/null @@ -1,19 +0,0 @@ --- Based on Squid's fuzzy search --- https://github.com/SquidDev-CC/artist/blob/vnext/artist/lib/match.lua --- --- not very fuzzy anymore - -local SCORE_WEIGHT = 1000 -local LEADING_LETTER_PENALTY = -3 -local LEADING_LETTER_PENALTY_MAX = -9 - -local _find = string.find -local _max = math.max - -return function(str, pattern) - local start = _find(str, pattern, 1, true) - if start then - -- All letters before the current one are considered leading, so add them to our penalty - return SCORE_WEIGHT + _max(LEADING_LETTER_PENALTY * (start - 1), LEADING_LETTER_PENALTY_MAX) - end -end diff --git a/milo/apis/massAdapter.lua b/milo/apis/massAdapter.lua deleted file mode 100644 index f272e4b..0000000 --- a/milo/apis/massAdapter.lua +++ /dev/null @@ -1,53 +0,0 @@ -local class = require('opus.class') -local itemDB = require('core.itemDB') -local Mini = require('milo.miniAdapter') - -local os = _G.os - -local Adapter = class(Mini) - -function Adapter:init(args) - Mini.init(self, args) - - self._rawList = self.list - - function self.list() - -- wait for up to 1 sec until any items that have been inserted - -- into interface are added to the system - for _ = 0, 20 do - if #self._rawList() == 0 then - break - end - os.sleep(0) - end - - local list = { } - for _, v in pairs(self.listAvailableItems()) do - list[itemDB:makeKey(v)] = v - end - return list - end - - function self.getItemMeta(key) - local item = self.findItem(itemDB:splitKey(key)) - if item and item.getMetadata then - return item.getMetadata() - end - end - - function self.pushItems(target, key, amount, slot) - local item = self.findItem(itemDB:splitKey(key)) - if item and item.export then - return item.export(target, amount, slot) - end - return 0 - end - - function self.pullItems(target, key, amount, slot) - _G._syslog({target, key, amount, slot }) - return 0 - end - -end - -return Adapter diff --git a/milo/core/listing.lua b/milo/core/listing.lua index b19b4d2..34f1b6c 100644 --- a/milo/core/listing.lua +++ b/milo/core/listing.lua @@ -1,6 +1,6 @@ local Craft = require('milo.craft2') local Event = require('opus.event') -local fuzzy = require('milo.fuzzyMatch') +local fuzzy = require('opus.fuzzy') local Milo = require('milo') local Sound = require('opus.sound') local UI = require('opus.ui') @@ -54,8 +54,8 @@ local page = UI.Page { limit = 50, shadowText = 'filter', shadowTextColor = colors.gray, - backgroundColor = colors.cyan, - backgroundFocusColor = colors.cyan, + backgroundColor = 'primary', + backgroundFocusColor = 'primary', accelerators = { [ 'enter' ] = 'eject', [ 'up' ] = 'grid_up', @@ -66,7 +66,7 @@ local page = UI.Page { storageStatus = UI.Text { x = -17, ex = -10, textColor = colors.lime, - backgroundColor = colors.cyan, + backgroundColor = 'primary', value = '', }, amount = UI.TextEntry { @@ -208,7 +208,7 @@ end function page:eventHandler(event) if event.type == 'quit' then - UI:exitPullEvents() + UI:quit() elseif event.type == 'eject' or event.type == 'grid_select' then self:eject(1) diff --git a/milo/core/machines.lua b/milo/core/machines.lua index 844e491..b3779db 100644 --- a/milo/core/machines.lua +++ b/milo/core/machines.lua @@ -7,7 +7,6 @@ local Util = require('opus.util') local colors = _G.colors local device = _G.device -local turtle = _G.turtle local context = Milo:getContext() @@ -22,8 +21,8 @@ local networkPage = UI.Page { y = -2, x = 1, ex = -9, limit = 50, shadowText = 'filter', - backgroundColor = colors.cyan, - backgroundFocusColor = colors.cyan, + backgroundColor = 'primary', + backgroundFocusColor = 'primary', }, grid = UI.ScrollingGrid { y = 2, ey = -3, @@ -195,7 +194,6 @@ nodeWizard = UI.Page { pages = { general = UI.WizardPage { index = 1, - backgroundColor = colors.cyan, form = UI.Form { x = 2, ex = -2, y = 1, ey = 3, manualControls = true, @@ -236,11 +234,11 @@ The settings will take effect immediately!]], }, }, statusBar = UI.StatusBar { - backgroundColor = colors.cyan, + backgroundColor = 'primary', }, notification = UI.Notification { }, filter = UI.SlideOut { - backgroundColor = colors.cyan, + noFill = true, menuBar = UI.MenuBar { buttons = { { text = 'Save', event = 'save' }, @@ -248,7 +246,8 @@ The settings will take effect immediately!]], }, }, grid = UI.ScrollingGrid { - x = 2, ex = -6, y = 2, ey = -6, + x = 2, ex = -6, y = 3, ey = -7, + disableHeader = true, columns = { { heading = 'Name', key = 'displayName' }, }, @@ -262,7 +261,7 @@ The settings will take effect immediately!]], text = '-', event = 'remove_entry', help = 'Remove', }, form = UI.Form { - x = 2, y = -4, height = 3, + x = 2, y = -5, height = 3, margin = 1, manualControls = true, [1] = UI.Checkbox { @@ -290,7 +289,7 @@ The settings will take effect immediately!]], }, }, statusBar = UI.StatusBar { - backgroundColor = colors.cyan, + backgroundColor = 'primary', }, }, } diff --git a/milo/plugins/activityView.lua b/milo/plugins/activityView.lua index 723d313..1d8cffd 100644 --- a/milo/plugins/activityView.lua +++ b/milo/plugins/activityView.lua @@ -17,7 +17,6 @@ Right-clicking on the activity monitor will reset the totals.]] local wizardPage = UI.WizardPage { title = 'Activity Monitor', index = 2, - backgroundColor = colors.cyan, [1] = UI.TextArea { x = 2, ex = -2, y = 2, ey = 6, marginRight = 0, diff --git a/milo/plugins/backupView.lua b/milo/plugins/backupView.lua index f52a00e..f53516a 100644 --- a/milo/plugins/backupView.lua +++ b/milo/plugins/backupView.lua @@ -3,7 +3,6 @@ local Event = require('opus.event') local Milo = require('milo') local UI = require('opus.ui') -local colors = _G.colors local device = _G.device local fs = _G.fs local os = _G.os @@ -23,7 +22,6 @@ Backup configuration files each minecraft day. local wizardPage = UI.WizardPage { title = 'Backup Drive', index = 2, - backgroundColor = colors.cyan, [1] = UI.TextArea { x = 2, ex = -2, y = 2, ey = -2, value = string.format(template, Ansi.yellow, Ansi.reset), diff --git a/milo/plugins/brewingStandView.lua b/milo/plugins/brewingStandView.lua index 88b550a..c97004e 100644 --- a/milo/plugins/brewingStandView.lua +++ b/milo/plugins/brewingStandView.lua @@ -18,7 +18,6 @@ Note that you do not need to import items from the brewing stand or export blaze local wizardPage = UI.WizardPage { title = 'Brewing Stand', index = 2, - backgroundColor = colors.cyan, [1] = UI.TextArea { x = 2, ex = -2, y = 2, ey = -2, value = string.format(template, Ansi.yellow, Ansi.reset), diff --git a/milo/plugins/emitterView.lua b/milo/plugins/emitterView.lua index f3c51d3..6198a6a 100644 --- a/milo/plugins/emitterView.lua +++ b/milo/plugins/emitterView.lua @@ -4,12 +4,10 @@ local itemDB = require('core.itemDB') local colors = _G.colors local device = _G.device -local context = Milo:getContext() local wizardPage = UI.WizardPage { title = 'Level Emitter', index = 2, - backgroundColor = colors.cyan, [1] = UI.TextArea { x = 2, y = 1, height = 2, diff --git a/milo/plugins/inputChestView.lua b/milo/plugins/inputChestView.lua index fe0fd9a..5709f3b 100644 --- a/milo/plugins/inputChestView.lua +++ b/milo/plugins/inputChestView.lua @@ -1,7 +1,6 @@ local Ansi = require('opus.ansi') local UI = require('opus.ui') -local colors = _G.colors local device = _G.device --[[ Configuration Screen ]] @@ -14,7 +13,6 @@ Any items placed in this chest will be imported into storage. local inputChestWizardPage = UI.WizardPage { title = 'Input Chest', index = 2, - backgroundColor = colors.cyan, [1] = UI.TextArea { x = 2, ex = -2, y = 2, ey = -2, value = string.format(template, Ansi.yellow, Ansi.reset), diff --git a/milo/plugins/item.lua b/milo/plugins/item.lua index c6dcd85..75c2fc4 100644 --- a/milo/plugins/item.lua +++ b/milo/plugins/item.lua @@ -41,7 +41,6 @@ function page:eventHandler(event) elseif event.type == 'focus_change' then self.statusBar:setStatus(event.focused.help) - self.statusBar:draw() elseif event.type == 'success_message' then self.notification:success(event.message) diff --git a/milo/plugins/item/infoTab.lua b/milo/plugins/item/infoTab.lua index 92bb37d..fb96fc1 100644 --- a/milo/plugins/item/infoTab.lua +++ b/milo/plugins/item/infoTab.lua @@ -1,14 +1,11 @@ local Ansi = require('opus.ansi') local UI = require('opus.ui') -local colors = _G.colors - local infoTab = UI.Tab { tabTitle = 'Info', index = 4, - backgroundColor = colors.cyan, textArea = UI.TextArea { - x = 2, ex = -2, y = 2, + x = 2, ex = -2, y = 2, ey = -2, }, } diff --git a/milo/plugins/item/machinesTab.lua b/milo/plugins/item/machinesTab.lua index 4ff7e04..8f52595 100644 --- a/milo/plugins/item/machinesTab.lua +++ b/milo/plugins/item/machinesTab.lua @@ -9,7 +9,6 @@ local context = Milo:getContext() local machinesTab = UI.Tab { tabTitle = 'Machine', index = 3, - backgroundColor = colors.cyan, grid = UI.ScrollingGrid { x = 2, ex = -2, y = 2, ey = -2, disableHeader = true, diff --git a/milo/plugins/item/recipeTab.lua b/milo/plugins/item/recipeTab.lua index 0a08fca..93e321f 100644 --- a/milo/plugins/item/recipeTab.lua +++ b/milo/plugins/item/recipeTab.lua @@ -3,12 +3,9 @@ local itemDB = require('core.itemDB') local Milo = require('milo') local UI = require('opus.ui') -local colors = _G.colors - local recipeTab = UI.Tab { tabTitle = 'Recipe', index = 2, - backgroundColor = colors.cyan, grid = UI.ScrollingGrid { x = 2, ex = -2, y = 2, ey = -4, disableHeader = true, diff --git a/milo/plugins/item/resetTab.lua b/milo/plugins/item/resetTab.lua index a0a12ae..0d9d34d 100644 --- a/milo/plugins/item/resetTab.lua +++ b/milo/plugins/item/resetTab.lua @@ -9,7 +9,7 @@ local context = Milo:getContext() local resetTab = UI.Tab { tabTitle = 'Reset', index = 5, - backgroundColor = colors.cyan, + noFill = true, textArea = UI.TextArea { y = 2, ey = 6, textColor = colors.yellow, diff --git a/milo/plugins/jobMonitor.lua b/milo/plugins/jobMonitor.lua index 846e279..a548fcf 100644 --- a/milo/plugins/jobMonitor.lua +++ b/milo/plugins/jobMonitor.lua @@ -15,7 +15,6 @@ local os = _G.os local wizardPage = UI.WizardPage { title = 'Crafting Monitor', index = 2, - backgroundColor = colors.cyan, [1] = UI.TextArea { x = 2, ex = -2, y = 2, ey = 3, marginRight = 0, diff --git a/milo/plugins/massStorageView.lua b/milo/plugins/massStorageView.lua deleted file mode 100644 index e198b3e..0000000 --- a/milo/plugins/massStorageView.lua +++ /dev/null @@ -1,43 +0,0 @@ -local Ansi = require('opus.ansi') -local UI = require('opus.ui') - -local colors = _G.colors -local device = _G.device - ---[[ Configuration Screen ]] -local template = -[[%sWarning%s - -Must an interface for Refined Storage / Applied Energistics. - -Add all speed upgrades possible. -]] - -local wizardPage = UI.WizardPage { - title = 'Mass Storage', - index = 2, - backgroundColor = colors.cyan, - [1] = UI.TextArea { - x = 2, ex = -2, y = 2, ey = -2, - value = string.format(template, Ansi.red, Ansi.reset), - }, -} - -function wizardPage:isValidFor(node) - if node.mtype == 'storage' then - local m = device[node.name] - return m and m.listAvailableItems - end -end - -function wizardPage:setNode(node) - self.node = node -end - -function wizardPage:validate() - self.node.adapterType = 'massAdapter' - return true -end - --- disable until a way is found to transfer between 2 non-transferrable nodes --- UI:getPage('nodeWizard').wizard:add({ inputChest = wizardPage }) diff --git a/milo/plugins/remote/setup.lua b/milo/plugins/remote/setup.lua index 5d12d21..856278d 100644 --- a/milo/plugins/remote/setup.lua +++ b/milo/plugins/remote/setup.lua @@ -10,12 +10,11 @@ local STARTUP_FILE = 'usr/autorun/miloRemote.lua' local context = ({ ... })[1] local setup = UI.SlideOut { - backgroundColor = colors.cyan, titleBar = UI.TitleBar { title = 'Remote Setup', }, form = UI.Form { - x = 2, ex = -2, y = 2, ey = -1, + y = 2, ey = -1, [1] = UI.TextEntry { formLabel = 'Server', formKey = 'server', help = 'ID for the server', @@ -40,18 +39,18 @@ local setup = UI.SlideOut { formLabel = 'Run on startup', formKey = 'runOnStartup', help = 'Run this program on startup' }, - info = UI.TextArea { - x = 1, ex = -1, y = 6, ey = -4, - textColor = colors.yellow, - marginLeft = 0, - marginRight = 0, - value = [[The Milo turtle must connect to a manipulator with a ]] .. - [[bound introspection module. The neural interface must ]] .. - [[also have an introspection module.]], - }, + }, + info = UI.TextArea { + x = 2, ex = -2, y = 8, ey = -4, + textColor = colors.yellow, + marginLeft = 0, + marginRight = 0, + value = [[The Milo turtle must connect to a manipulator with a ]] .. + [[bound introspection module. The neural interface must ]] .. + [[also have an introspection module.]], }, statusBar = UI.StatusBar { - backgroundColor = colors.cyan, + backgroundColor = 'primary', }, } diff --git a/milo/plugins/speakerView.lua b/milo/plugins/speakerView.lua index c5668dc..1b5cd3c 100644 --- a/milo/plugins/speakerView.lua +++ b/milo/plugins/speakerView.lua @@ -14,7 +14,6 @@ end local wizardPage = UI.WizardPage { title = 'Speaker', index = 2, - backgroundColor = colors.cyan, [1] = UI.Text { x = 2, y = 2, textColor = colors.yellow, diff --git a/milo/plugins/statsView.lua b/milo/plugins/statsView.lua index 9f9c0a2..b6ab90c 100644 --- a/milo/plugins/statsView.lua +++ b/milo/plugins/statsView.lua @@ -18,7 +18,6 @@ Right-clicking on the activity monitor will reset the totals.]] local wizardPage = UI.WizardPage { title = 'Status Monitor', index = 2, - backgroundColor = colors.cyan, [1] = UI.TextArea { x = 2, ex = -2, y = 2, ey = 6, marginRight = 0, @@ -86,6 +85,7 @@ local function createPage(node) [1] = UI.Tab { tabTitle = 'Overview', backgroundColor = colors.black, + noFill = true, onlineLabel = UI.Text { x = 2, y = 2, value = 'Storage Status', @@ -136,12 +136,14 @@ local function createPage(node) }, [2] = UI.Tab { tabTitle = 'Stats', + noFill = true, textArea = UI.TextArea { y = 3, }, }, [3] = UI.Tab { tabTitle = 'Storage', + noFill = true, grid = UI.ScrollingGrid { y = 2, columns = { @@ -156,6 +158,7 @@ local function createPage(node) }, [4] = UI.Tab { tabTitle = 'Offline', + noFill = true, grid = UI.ScrollingGrid { y = 2, columns = { @@ -166,12 +169,14 @@ local function createPage(node) }, [5] = UI.Tab { tabTitle = 'Activity', + noFill = true, term = UI.Embedded { --visible = true, }, }, [6] = UI.Tab { tabTitle = 'Tasks', + noFill = true, grid = UI.ScrollingGrid { y = 2, values = context.tasks, diff --git a/milo/plugins/storageView.lua b/milo/plugins/storageView.lua index b1d51c2..18e2c0e 100644 --- a/milo/plugins/storageView.lua +++ b/milo/plugins/storageView.lua @@ -7,7 +7,6 @@ local device = _G.device local storageView = UI.WizardPage { title = 'Storage Options - General', index = 2, - backgroundColor = colors.cyan, form = UI.Form { x = 2, ex = -2, y = 1, ey = -2, manualControls = true, @@ -62,7 +61,6 @@ UI:getPage('nodeWizard').wizard:add({ storageGeneral = storageView }) local lockView = UI.WizardPage { title = 'Storage Options - Locking', index = 3, - backgroundColor = colors.cyan, form = UI.Form { x = 2, ex = -2, y = 1, ey = 3, manualControls = true, diff --git a/milo/plugins/transferView.lua b/milo/plugins/transferView.lua index 3fa70ff..df6079e 100644 --- a/milo/plugins/transferView.lua +++ b/milo/plugins/transferView.lua @@ -12,7 +12,6 @@ local context = Milo:getContext() local wizardPage = UI.WizardPage { title = 'Transfer Inventory', index = 2, - backgroundColor = colors.cyan, grid = UI.ScrollingGrid { y = 2, ey = -2, values = context.storage.nodes, diff --git a/milo/plugins/trashcanView.lua b/milo/plugins/trashcanView.lua index 2de4a4e..fc1e70f 100644 --- a/milo/plugins/trashcanView.lua +++ b/milo/plugins/trashcanView.lua @@ -9,7 +9,6 @@ local device = _G.device local wizardPage = UI.WizardPage { title = 'Trashcan', index = 2, - backgroundColor = colors.cyan, info = UI.TextArea { x = 1, ex = -1, y = 2, ey = 4, textColor = colors.yellow, diff --git a/miners/scanningMiner.lua b/miners/scanningMiner.lua index 1d5e361..588259b 100644 --- a/miners/scanningMiner.lua +++ b/miners/scanningMiner.lua @@ -637,8 +637,7 @@ Event.addRoutine(function() end) UI:setPage(page) -UI:pullEvents() -UI.term:reset() +UI:start() turtle.reset() diff --git a/monitor/mwm.lua b/monitor/mwm.lua index a1e5eb1..a397606 100644 --- a/monitor/mwm.lua +++ b/monitor/mwm.lua @@ -121,7 +121,10 @@ function Process:new(args) self.terminal = self.window self.container.canvas.parent = monitor.canvas - table.insert(monitor.canvas.layers, 1, self.container.canvas) + if not monitor.canvas.children then + monitor.canvas.children = { } + end + table.insert(monitor.canvas.children, 1, self.container.canvas) self.container.canvas:setVisible(true) --self.container.getSize = self.window.getSize diff --git a/neural/Equipment.lua b/neural/Equipment.lua new file mode 100644 index 0000000..32b849e --- /dev/null +++ b/neural/Equipment.lua @@ -0,0 +1,142 @@ +local Event = require('opus.event') +local itemDB = require('core.itemDB') +local neural = require('neural.interface') +local UI = require('opus.ui') +local Util = require('opus.util') + +local device = _G.device + +neural.assertModules({ + 'plethora:sensor', + 'plethora:kinetic', + 'plethora:introspection', +}) +UI:configure('Equipment', ...) + +local equipment = device.neuralInterface.getEquipment() + +local slots = { + 'primary', + 'offhand', + 'boots', + 'leggings', + 'chest', + 'helmet', +} + +local page = UI.Page { + menuBar = UI.MenuBar { + buttons = { + { text = 'Drop', event = 'drop' }, + { text = 'Suck', event = 'suck' }, + }, + }, + grid = UI.Grid { + y = 2, + columns = { + { heading = 'Slot', key = 'index', width = 7 }, + { heading = 'Name', key = 'displayName' }, + { heading = 'Count', key = 'count', width = 5, align = 'right' }, + }, + sortColumn = 'index', + accelerators = { + grid_select = 'show_detail', + }, + getDisplayValues = function(_, row) + row = Util.shallowCopy(row) + if row.name then + local item = itemDB:get( + table.concat({ row.name, row.damage, row.nbtHash }, ':'), + function() + return equipment.getItemMeta(row.index) + end) + row.displayName = item.displayName + else + row.displayName = 'empty' + end + row.index = slots[row.index] + return row + end, + }, + accelerators = { + [ 'control-q' ] = 'quit', + }, + detail = UI.SlideOut { + menuBar = UI.MenuBar { + buttons = { + { text = 'Back', event = 'slide_hide' }, + }, + }, + grid = UI.ScrollingGrid { + y = 2, + columns = { + { heading = 'Name', key = 'name' }, + { heading = 'Value', key = 'value' }, + }, + sortColumn = 'name', + accelerators = { + grid_select = 'inspect', + }, + }, + show = function(self, slot) + local detail = equipment.getItemMeta(slot.index) + local t = { } + for k,v in pairs(detail) do + table.insert(t, { + name = k, + value = v, + }) + end + self.grid:setValues(t) + self.grid:setIndex(1) + UI.SlideOut.show(self) + end, + }, + enable = function(self) + self:refresh() + UI.Page.enable(self) + end, + refresh = function(self) + local t = { } + local list = equipment.list() + for i = 1, equipment.size() do + local v = list[i] or { } + v.index = i + table.insert(t, v) + end + self.grid:setValues(t) + self.grid:draw() + end, + eventHandler = function(self, event) + if event.type == 'quit' then + UI:quit() + + elseif event.type == 'show_detail' then + if event.selected.name then + self.detail:show(event.selected) + end + + elseif event.type == 'drop' then + local selected = self.grid:getSelected() + equipment.drop(selected.index) + self:refresh() + + elseif event.type == 'suck' then + local selected = self.grid:getSelected() + equipment.suck(selected.index) + self:refresh() + end + + UI.Page.eventHandler(self, event) + end, +} + +Event.onInterval(1, function() + page:refresh() + page:sync() +end) + +UI:setPage(page) +UI:start() + +itemDB:flush() diff --git a/neural/Scanner.lua b/neural/Scanner.lua index 11edab2..4dd6be5 100644 --- a/neural/Scanner.lua +++ b/neural/Scanner.lua @@ -182,7 +182,7 @@ end function page:eventHandler(event) if event.type == 'quit' then - Event.exitPullEvents() + UI:quit() elseif event.type == 'scan' then self:scan() @@ -211,7 +211,7 @@ Event.onTimeout(0, function() page:sync() end) -UI:pullEvents() +UI:start() if canvas then canvas:clear() diff --git a/neural/Sensor.lua b/neural/Sensor.lua index d926c05..42d0b9a 100644 --- a/neural/Sensor.lua +++ b/neural/Sensor.lua @@ -253,7 +253,7 @@ end function page:eventHandler(event) if event.type == 'quit' then - Event.exitPullEvents() + UI:quit() elseif event.type == 'tab_activate' then config.activeTab = event.activated.tabTitle @@ -268,7 +268,7 @@ if config.activeTab then end UI:setPage(page) -UI:pullEvents() +UI:start() if canvas then canvas:clear() diff --git a/neural/etc/apps.db b/neural/etc/apps.db index 61040b2..31c01d7 100644 --- a/neural/etc/apps.db +++ b/neural/etc/apps.db @@ -29,4 +29,10 @@ run = "ores.lua", requires = "neuralInterface", }, + [ "7f2465ac7cefab2766e6ee0714647089df9364b0ff09858c84b21b8a436a845d" ] = { + title = "Equipment", + category = "Neural", + run = "Equipment", + requires = "neuralInterface", + } } diff --git a/neural/mobRancher.lua b/neural/mobRancher.lua index c09d7d3..cef0975 100644 --- a/neural/mobRancher.lua +++ b/neural/mobRancher.lua @@ -1,6 +1,15 @@ --[[ - Breed either cows or sheep. - Must be run on a mob with the same height. + Changed to use a 2-high mob (smaller mobs may work ?) + Updated due to entity.look working correctly now. + + The mob looks head-on to the lever. Make sure the + lever is accessible by the mob. + + Laser is now optional - if no laser, the mobs will be + punched (or provide a stick). Best mob may be a + skeleton (unlimited ammo). + + Feeding hand has been changed to off-hand. ]] local Array = require('opus.array') @@ -21,80 +30,100 @@ local WALK_SPEED = 1.5 neural.assertModules({ 'plethora:sensor', 'plethora:scanner', - 'plethora:laser', + --'plethora:laser', 'plethora:kinetic', 'plethora:introspection', }) +local caninimals = config.animals or { [ config.animal ] = true } +local animals = { } +for k in pairs(caninimals) do + animals[k] = { } +end + local fed = { } local function resupply() - local slot = neural.getEquipment().list()[1] + local slot = neural.getEquipment().list()[2] if slot and slot.count > 32 then return end print('resupplying') for _ = 1, 2 do - local dispenser = Map.find(neural.scan(), 'name', 'minecraft:dispenser') + local dispenser = Map.find(neural.scan(), 'name', 'minecraft:lever') if not dispenser then print('dispenser not found') break end - if math.abs(dispenser.x) <= 1 and math.abs(dispenser.z) <= 1 then - neural.lookAt(dispenser) + if math.abs(dispenser.x) <= 1.2 and math.abs(dispenser.z) <= 1.2 then + neural.lookAt({ x = dispenser.x, y = dispenser.y, z = dispenser.z }) for _ = 1, 8 do - neural.use(0, 'off') + if not neural.use(0, 'off') then + break + end os.sleep(.2) - neural.getEquipment().suck(1, 64) + neural.getEquipment().suck(2, 64) end break else - neural.walkTo({ x = dispenser.x, y = 0, z = dispenser.z }, WALK_SPEED) + neural.walkTo({ x = dispenser.x, y = 0, z = dispenser.z }, WALK_SPEED, .5) end end end local function breed(entity) - print('breeding') - entity.lastFed = os.clock() - fed[entity.id] = entity + print('breeding ' .. entity.name) neural.walkTo(entity, WALK_SPEED, 1) entity = neural.getMetaByID(entity.id) if entity then neural.lookAt(entity) - neural.use(1) + if neural.use(1, 'off') then + entity.lastFed = os.clock() + fed[entity.id] = entity + end os.sleep(.1) end end local function kill(entity) - print('killing') - neural.walkTo(entity, WALK_SPEED, 2.5) + print('killing ' .. entity.name) + neural.walkTo(entity, WALK_SPEED, (neural.fire or neural.shoot) and 2.5 or .5) entity = neural.getMetaByID(entity.id) if entity then neural.lookAt(entity) - neural.fireAt({ x = entity.x, y = 0, z = entity.z }) + if neural.fire or neural.shoot then + neural.shootAt(entity) + else + neural.swing() + end Sound.play('entity.firework.launch') os.sleep(.2) end end -local function getEntities() - local sheep = Array.filter(neural.sense(), function(entity) - if entity.name == 'Sheep' and entity.y > -.5 then - return true - end - end) - if #sheep > config.maxAdults then - return sheep +local function shuffle(tbl) + for i = #tbl, 2, -1 do + local j = math.random(i) + tbl[i], tbl[j] = tbl[j], tbl[i] end + return tbl +end - return Map.filter(neural.sense(), function(entity) - if entity.name == config.animal and entity.y > -.5 then - return true +local function getEntities() + return shuffle(Array.filter(neural.sense(), function(entity) + if animals[entity.name] then + if not animals[entity.name].height then + entity = neural.getMetaByID(entity.id) + if entity and not entity.isChild and entity.motionX == 0 and entity.motionZ == 0 then + animals[entity.name].height = entity.y + return true + end + elseif entity.y == animals[entity.name].height then + return true + end end - end) + end)) end local function getHungry(entities) @@ -105,13 +134,25 @@ local function getHungry(entities) end end -local function randomEntity(entities) - local r = math.random(1, Map.size(entities)) - local i = 1 +local function getCount(entities, name) + local c = 0 for _, v in pairs(entities) do - i = i + 1 - if i > r then - return v + if v.name == name then + c = c + 1 + end + end + print(name .. ' ' .. c) + return c +end + +local function getKillable(entities) + print('map: ' .. Map.size(fed)) + if Map.size(fed) > 1000 then + fed = { } + end + for name in pairs(animals) do + if getCount(entities, name) > config.maxAdults then + return Array.find(entities, 'name', name) end end end @@ -120,9 +161,10 @@ while true do resupply() local entities = getEntities() + local killable = getKillable(entities) - if Map.size(entities) > config.maxAdults then - kill(randomEntity(entities)) + if killable then + kill(killable) else local entity = getHungry(entities) if entity then diff --git a/recipeBook/recipeBook.lua b/recipeBook/recipeBook.lua index 3aa67c3..65d4fc7 100644 --- a/recipeBook/recipeBook.lua +++ b/recipeBook/recipeBook.lua @@ -61,12 +61,12 @@ local page = UI.Page { autospace = true, }, add = UI.SlideOut { - backgroundColor = colors.cyan, + height = 9, y = -9, titleBar = UI.TitleBar { title = 'Add a new book', }, form = UI.Form { - x = 2, ex = -2, y = 2, ey = -1, + y = 2, [1] = UI.TextEntry { formLabel = 'Name', formKey = 'name', shadowText = 'Friendly name', @@ -100,11 +100,10 @@ function page.info:draw() self:clear() if book then - self:setCursorPos(1, 1) self:print( - string.format('Name: %s%s%s\n', Ansi.yellow, book.name, Ansi.reset)) - self:print( - string.format('Version: %s%s%s\n', Ansi.yellow, book.version, Ansi.reset)) + string.format('Name: %s%s%s\nVersion: %s%s%s\n', + Ansi.yellow, book.name, Ansi.reset, + Ansi.yellow, book.version, Ansi.reset)) self.button.text = book.enabled and 'Disable' or 'Enable' self.button:draw() @@ -173,11 +172,11 @@ function page:eventHandler(event) self.info:draw() elseif event.type == 'quit' then - UI:exitPullEvents() + UI:quit() end UI.Page.eventHandler(self, event) end UI:setPage(page) -UI:pullEvents() +UI:start() diff --git a/screenSaver/autorun/saver.lua b/screenSaver/autorun/saver.lua index 274b82f..db33bc6 100644 --- a/screenSaver/autorun/saver.lua +++ b/screenSaver/autorun/saver.lua @@ -11,7 +11,7 @@ if not multishell then end local config = Config.load('saver', { - enabled = true, + enabled = false, timeout = 60, random = true, specific = nil, diff --git a/screenSaver/system/saver.lua b/screenSaver/system/saver.lua index b8fa7a3..7391f2d 100644 --- a/screenSaver/system/saver.lua +++ b/screenSaver/system/saver.lua @@ -6,23 +6,26 @@ local config = Config.load('saver', { timeout = 60, }) -local tab = UI.Tab { +return UI.Tab { tabTitle = 'Screen Saver', description = 'Screen saver', + [1] = UI.Window { + x = 2, y = 2, ex = -2, ey = 5, + }, label1 = UI.Text { - x = 2, y = 3, + x = 3, y = 3, value = 'Enabled', }, checkbox = UI.Checkbox { - x = 20, y = 3, + x = 21, y = 3, value = config.enabled }, label2 = UI.Text { - x = 2, y = 4, + x = 3, y = 4, value = 'Timeout', }, timeout = UI.TextEntry { - x = 20, y = 4, width = 6, + x = 21, y = 4, width = 6, limit = 4, transform = 'number', value = config.timeout, @@ -31,28 +34,25 @@ local tab = UI.Tab { }, }, button = UI.Button { - x = 20, y = 6, - text = 'Update', + x = -8, ex = -2, y = -2, + text = 'Apply', event = 'update', }, -} + eventHandler = function(self, event) + if event.type =='checkbox_change' then + config.enabled = not not event.checked -function tab:eventHandler(event) - if event.type =='checkbox_change' then - config.enabled = not not event.checked + elseif event.type == 'update' then + if self.timeout.value then + config.timeout = self.timeout.value + Config.update('saver', config) - elseif event.type == 'update' then - if self.timeout.value then - config.timeout = self.timeout.value - Config.update('saver', config) - - self:emit({ type = 'success_message', message = 'Settings updated' }) - os.queueEvent('config_update', 'saver', config) - else - self:emit({ type = 'error_message', message = 'Invalid timeout' }) + self:emit({ type = 'success_message', message = 'Settings updated' }) + os.queueEvent('config_update', 'saver', config) + else + self:emit({ type = 'error_message', message = 'Invalid timeout' }) + end end - end - return UI.Tab.eventHandler(self, event) -end - -return tab + return UI.Tab.eventHandler(self, event) + end, +} diff --git a/secure/autorun/lock.lua b/secure/autorun/lock.lua index f951dfc..a8965d7 100644 --- a/secure/autorun/lock.lua +++ b/secure/autorun/lock.lua @@ -49,7 +49,7 @@ local function buildLockScreen() #self.pass.value > 0 and Security.verifyPassword(SHA.compute(self.pass.value)) then - UI:exitPullEvents() -- valid + UI:quit() -- valid else self.notification:error('Invalid password', math.max(counter, 2)) self:sync() @@ -66,7 +66,7 @@ local function buildLockScreen() Event.onTerminate(function() return false end) UI:setPage(page) - UI:pullEvents() + UI:start() -- restart lock timer timer = os.startTimer(config.timeout) diff --git a/secure/system/secure.lua b/secure/system/secure.lua index 113ad9b..9fd0137 100644 --- a/secure/system/secure.lua +++ b/secure/system/secure.lua @@ -9,20 +9,23 @@ local config = Config.load('secure', { local tab = UI.Tab { tabTitle = 'Secure', description = 'Secure options', + [1] = UI.Window { + x = 2, y = 2, ex = -2, ey = 5, + }, label1 = UI.Text { - x = 2, y = 3, + x = 3, y = 3, value = 'Screen Locking', }, checkbox = UI.Checkbox { - x = 20, y = 3, + x = 21, y = 3, value = config.enabled }, label2 = UI.Text { - x = 2, y = 4, + x = 3, y = 4, value = 'Lock timeout', }, timeout = UI.TextEntry { - x = 20, y = 4, width = 6, + x = 21, y = 4, width = 6, limit = 4, transform = 'number', value = config.timeout, @@ -31,8 +34,8 @@ local tab = UI.Tab { }, }, button = UI.Button { - x = 20, y = 6, - text = 'Update', + x = -8, ex = -2, y = -2, + text = 'Apply', event = 'update', }, } diff --git a/swshop/Shoplogs.lua b/swshop/Shoplogs.lua index 5766b08..5a478e3 100644 --- a/swshop/Shoplogs.lua +++ b/swshop/Shoplogs.lua @@ -189,4 +189,4 @@ end) page.grid:loadTransactions() UI:setPage(page) -UI:pullEvents() +UI:start() diff --git a/swshop/shopConfig.lua b/swshop/shopConfig.lua index 026ad16..857d0c1 100644 --- a/swshop/shopConfig.lua +++ b/swshop/shopConfig.lua @@ -9,7 +9,6 @@ local os = _G.os local wizardPage = UI.WizardPage { title = 'Store Front', index = 2, - backgroundColor = colors.cyan, form = UI.Form { x = 2, ex = -2, y = 1, ey = -2, manualControls = true, @@ -90,7 +89,6 @@ end local passwordPage = UI.WizardPage { title = 'Krist Settings', index = 3, - backgroundColor = colors.cyan, form = UI.Form { x = 2, ex = -2, y = 1, ey = -2, manualControls = true, @@ -112,7 +110,7 @@ local passwordPage = UI.WizardPage { preview = UI.TextEntry { formIndex = 4, formLabel = 'Using address', formKey = 'address', - backgroundColor = colors.cyan, + backgroundColor = 'primary', textColor = colors.yellow, inactive = true, }, diff --git a/swshop/shopTab.lua b/swshop/shopTab.lua index 169d6ee..f731ebf 100644 --- a/swshop/shopTab.lua +++ b/swshop/shopTab.lua @@ -10,7 +10,7 @@ local shopTab = UI.Tab { tabTitle = 'Store', index = 2, form = UI.Form { - x = 2, ex = -2, y = 1, ey = -2, + x = 2, ex = -2, y = 2, ey = -2, manualControls = true, [1] = UI.TextEntry { formLabel = 'Name', formKey = 'name', diff --git a/turtle/autorun/6.tl3.lua b/turtle/init/6.tl3.lua similarity index 100% rename from turtle/autorun/6.tl3.lua rename to turtle/init/6.tl3.lua -- 2.49.1 From 8d014c0098ce1003965f6faded64c522db3491a3 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Wed, 22 Apr 2020 23:36:03 -0600 Subject: [PATCH 24/90] remove a few screen savers - some bugfixes --- ccemux/system/ccemux.lua | 7 +++++-- common/DiskCopy.lua | 4 ++-- common/Events.lua | 4 ---- common/edit.lua | 6 +++++- screenSaver/etc/fstab | 4 ++-- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/ccemux/system/ccemux.lua b/ccemux/system/ccemux.lua index f05df3d..d2f0e43 100644 --- a/ccemux/system/ccemux.lua +++ b/ccemux/system/ccemux.lua @@ -46,6 +46,7 @@ local tab = UI.Tab { columns = { { heading = 'Side', key = 'side', width = 8 }, { heading = 'Type', key = 'type' }, + { heading = 'ID', key = 'args', width = 4 }, }, }, } @@ -56,7 +57,7 @@ function tab:updatePeripherals(config) table.insert(self.grid.values, { side = k, type = v.type, - args = v.args, + args = v.args and v.args.id, }) end self.grid:update() @@ -83,7 +84,6 @@ function tab:eventHandler(event) self:emit({ type = 'error_message', message = 'Invalid drive ID' }) else ccemux.detach(event.values.side) - ccemux.attach(event.values.side, event.values.type) local config = Config.load('ccemux') config[event.values.side] = { @@ -93,6 +93,9 @@ function tab:eventHandler(event) config[event.values.side].args = { id = event.values.drive_id } + ccemux.attach(event.values.side, event.values.type, { id = event.values.drive_id }) + else + ccemux.attach(event.values.side, event.values.type) end Config.update('ccemux', config) self:updatePeripherals(config) diff --git a/common/DiskCopy.lua b/common/DiskCopy.lua index b3efc4e..cf9d616 100644 --- a/common/DiskCopy.lua +++ b/common/DiskCopy.lua @@ -94,7 +94,7 @@ function page:enable() self.eject.value = config.eject self.automatic.value = config.automatic - self.dir.x = math.floor((self.width / 2) - 3) + 1 + self.dir:move(math.floor((self.width / 2) - 3) + 1, self.dir.y) UI.Page.enable(self) end @@ -218,7 +218,7 @@ function page:copy() self.progress:sync() self.progress.value = 0 - self.progress:clear() +-- self.progress:clear() self:scan() diff --git a/common/Events.lua b/common/Events.lua index 783e38c..ae58ca2 100644 --- a/common/Events.lua +++ b/common/Events.lua @@ -43,10 +43,6 @@ local page = UI.Page { return row end, - draw = function(self) - self:adjustWidth() - UI.Grid.draw(self) - end, }, accelerators = { f = 'filter', diff --git a/common/edit.lua b/common/edit.lua index 804d40c..c2097e3 100644 --- a/common/edit.lua +++ b/common/edit.lua @@ -575,6 +575,10 @@ local page = UI.Page { actions.dirty_all() actions.redraw() end, + setCursorPos = function(self, cx, cy) + self.cursorBlink = cy >= 1 and cy <= self.height + UI.Window.setCursorPos(self, cx, cy) + end, draw = function() actions.redraw() end, @@ -715,7 +719,7 @@ local function redraw() if dirty.y > 0 then for dy = 1, h do local sLine = tLines[dy + scrollY] - if sLine ~= nil then + if sLine and #sLine > 0 then if dy + scrollY >= dirty.y and dy + scrollY <= dirty.ey then page.editor:clearLine(dy) writeHighlighted(sLine, dy + scrollY, dy) diff --git a/screenSaver/etc/fstab b/screenSaver/etc/fstab index e89b176..31905fb 100644 --- a/screenSaver/etc/fstab +++ b/screenSaver/etc/fstab @@ -4,11 +4,11 @@ packages/screenSaver/savers/visualizer.lua urlfs https://raw.githubusercontent.c packages/screenSaver/savers/random.lua urlfs https://pastebin.com/raw/XXW0r5zt packages/screenSaver/savers/melting.lua urlfs http://pastebin.com/raw/raUv6Pap packages/screenSaver/savers/bubbles.lua urlfs https://pastebin.com/raw/JCR8YTww -packages/screenSaver/savers/fire.lua urlfs https://pastebin.com/raw/4CY4AYj3 +#packages/screenSaver/savers/fire.lua urlfs https://pastebin.com/raw/4CY4AYj3 packages/screenSaver/savers/rain.lua urlfs https://pastebin.com/raw/P86Hm99N packages/screenSaver/savers/snow.lua urlfs https://pastebin.com/raw/j1dwdLKw packages/screenSaver/savers/fireworks.lua urlfs https://pastebin.com/raw/Yn5sWt3f packages/screenSaver/savers/starfield.lua urlfs https://pastebin.com/raw/AQm9R6nT -packages/screenSaver/savers/nyan.lua urlfs https://pastebin.com/raw/YJnT6Adu +#packages/screenSaver/savers/nyan.lua urlfs https://pastebin.com/raw/YJnT6Adu packages/screenSaver/savers/bounce.lua urlfs https://pastebin.com/raw/WLrfdyNy packages/screenSaver/savers/antfarm.lua urlfs https://pastebin.com/raw/h9x3h7aw \ No newline at end of file -- 2.49.1 From ef0886ec85cd9003e75c7b0b9fd1f2f0e64d43a0 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Sun, 26 Apr 2020 19:39:28 -0600 Subject: [PATCH 25/90] wizard and tab rework --- ccemux/system/ccemux.lua | 2 +- common/Turtles.lua | 336 +++++++++++++------------ common/etc/apps.db | 2 +- common/multiMiner.lua | 6 +- lzwfs/system/lzwfs.lua | 2 +- milo/core/learnWizard.lua | 32 ++- milo/core/machines.lua | 396 ++++++++++++++---------------- milo/plugins/item/infoTab.lua | 2 +- milo/plugins/item/machinesTab.lua | 2 +- milo/plugins/item/manageTab.lua | 2 +- milo/plugins/item/recipeTab.lua | 2 +- milo/plugins/item/resetTab.lua | 2 +- milo/plugins/remote/autostore.lua | 4 +- milo/plugins/statsView.lua | 16 +- neural/Sensor.lua | 8 +- screenSaver/system/saver.lua | 2 +- secure/system/secure.lua | 2 +- shellex/apis/transfer.lua | 2 +- swshop/shopTab.lua | 2 +- turtle/system/turtle.lua | 2 +- 20 files changed, 398 insertions(+), 426 deletions(-) diff --git a/ccemux/system/ccemux.lua b/ccemux/system/ccemux.lua index d2f0e43..d066bab 100644 --- a/ccemux/system/ccemux.lua +++ b/ccemux/system/ccemux.lua @@ -6,7 +6,7 @@ local ccemux = _G.ccemux local sides = { 'bottom', 'top', 'back', 'front', 'right', 'left' } local tab = UI.Tab { - tabTitle = 'CCEmuX', + title = 'CCEmuX', description = 'CCEmuX peripherals', form = UI.Form { x = 2, ex = -2, y = 2, ey = 5, diff --git a/common/Turtles.lua b/common/Turtles.lua index cc48825..07d89e6 100644 --- a/common/Turtles.lua +++ b/common/Turtles.lua @@ -5,7 +5,6 @@ local Socket = require('opus.socket') local UI = require('opus.ui') local Util = require('opus.util') -local colors = _G.colors local fs = _G.fs local multishell = _ENV.multishell local network = _G.network @@ -31,7 +30,7 @@ local socket, turtle, page page = UI.Page { coords = UI.Window { - backgroundColor = colors.black, + backgroundColor = 'black', height = 3, marginTop = 1, marginLeft = 1, draw = function(self) @@ -50,156 +49,149 @@ page = UI.Page { }, tabs = UI.Tabs { x = 1, y = 4, ey = -2, - scripts = UI.ScrollingGrid { - tabTitle = 'Run', - backgroundColor = colors.cyan, - columns = { - { heading = '', key = 'label' }, + UI.Tab { + title = 'Run', + scripts = UI.ScrollingGrid { + backgroundColor = 'primary', + columns = { + { heading = '', key = 'label' }, + }, + disableHeader = true, + sortColumn = 'label', + autospace = true, + draw = function(self) + Util.clear(self.values) + local files = fs.list(SCRIPTS_PATH) + for _,path in pairs(files) do + table.insert(self.values, { label = path, path = fs.combine(SCRIPTS_PATH, path) }) + end + self:update() + UI.ScrollingGrid.draw(self) + end, + eventHandler = function(self, event) + if event.type == 'grid_select' then + page:runScript(event.selected.label) + else + return UI.ScrollingGrid.eventHandler(self, event) + end + return true + end, }, - disableHeader = true, - sortColumn = 'label', - autospace = true, - draw = function(self) - Util.clear(self.values) - local files = fs.list(SCRIPTS_PATH) - for _,path in pairs(files) do - table.insert(self.values, { label = path, path = fs.combine(SCRIPTS_PATH, path) }) - end - self:update() - UI.ScrollingGrid.draw(self) - end, - eventHandler = function(self, event) - if event.type == 'grid_select' then - page:runScript(event.selected.label) - else - return UI.ScrollingGrid.eventHandler(self, event) - end - return true - end, }, - turtles = UI.ScrollingGrid { - tabTitle = 'Select', - backgroundColor = colors.cyan, - columns = { - { heading = 'label', key = 'label' }, - { heading = 'Dist', key = 'distance' }, - { heading = 'Status', key = 'status' }, - { heading = 'Fuel', key = 'fuel' }, - }, - disableHeader = true, - sortColumn = 'label', - autospace = true, - getDisplayValues = function(_, row) - row = Util.shallowCopy(row) - if row.fuel then - row.fuel = Util.toBytes(row.fuel) - end - if row.distance then - row.distance = Util.round(row.distance, 1) - end - return row - end, - draw = function(self) - Util.clear(self.values) - for _,v in pairs(network) do - if v.fuel then - table.insert(self.values, v) + UI.Tab { + title = 'Select', + turtles = UI.ScrollingGrid { + backgroundColor = 'primary', + columns = { + { heading = 'label', key = 'label' }, + { heading = 'Dist', key = 'distance' }, + { heading = 'Status', key = 'status' }, + { heading = 'Fuel', key = 'fuel' }, + }, + disableHeader = true, + sortColumn = 'label', + autospace = true, + getDisplayValues = function(_, row) + row = Util.shallowCopy(row) + if row.fuel then + row.fuel = Util.toBytes(row.fuel) end - end - self:update() - UI.ScrollingGrid.draw(self) - end, - eventHandler = function(self, event) - if event.type == 'grid_select' then - turtle = event.selected - config.id = event.selected.id - Config.update('Turtles', config) - multishell.setTitle(multishell.getCurrent(), turtle.label) - if socket then - socket:close() - socket = nil + if row.distance then + row.distance = Util.round(row.distance, 1) end - else - return UI.ScrollingGrid.eventHandler(self, event) - end - return true - end, - }, - inventory = UI.ScrollingGrid { - backgroundColor = colors.cyan, - tabTitle = 'Inv', - columns = { - { heading = '', key = 'index', width = 2 }, - { heading = '', key = 'count', width = 2 }, - { heading = 'Inventory', key = 'key' }, - }, - disableHeader = true, - sortColumn = 'index', - getRowTextColor = function(self, row, selected) - if turtle and row.selected then - return colors.yellow - end - return UI.ScrollingGrid.getRowTextColor(self, row, selected) - end, - draw = function(self) - local t = turtle - Util.clear(self.values) - if t then - for k,v in pairs(t.inv or { }) do -- new method (less data) - local index, count = k:match('(%d+),(%d+)') - v = { - index = tonumber(index), - key = v, - count = tonumber(count), - } - table.insert(self.values, v) - end - - for _,v in pairs(t.inventory or { }) do - if v.count > 0 then + return row + end, + draw = function(self) + Util.clear(self.values) + for _,v in pairs(network) do + if v.fuel then table.insert(self.values, v) end end - - for _,v in pairs(self.values) do - if v.index == t.slotIndex then - v.selected = true + self:update() + UI.ScrollingGrid.draw(self) + end, + eventHandler = function(self, event) + if event.type == 'grid_select' then + turtle = event.selected + config.id = event.selected.id + Config.update('Turtles', config) + multishell.setTitle(multishell.getCurrent(), turtle.label) + if socket then + socket:close() + socket = nil end - if v.key then - v.key = itemDB:getName(v.key) + else + return UI.ScrollingGrid.eventHandler(self, event) + end + return true + end, + }, + }, + UI.Tab { + title = 'Inv', + inventory = UI.ScrollingGrid { + backgroundColor = 'primary', + columns = { + { heading = '', key = 'index', width = 2 }, + { heading = '', key = 'count', width = 2 }, + { heading = 'Inventory', key = 'key' }, + }, + disableHeader = true, + sortColumn = 'index', + getRowTextColor = function(self, row, selected) + if turtle and row.selected then + return 'yellow' + end + return UI.ScrollingGrid.getRowTextColor(self, row, selected) + end, + draw = function(self) + local t = turtle + Util.clear(self.values) + if t then + for k,v in pairs(t.inv or { }) do -- new method (less data) + local index, count = k:match('(%d+),(%d+)') + v = { + index = tonumber(index), + key = v, + count = tonumber(count), + } + table.insert(self.values, v) + end + + for _,v in pairs(t.inventory or { }) do + if v.count > 0 then + table.insert(self.values, v) + end + end + + for _,v in pairs(self.values) do + if v.index == t.slotIndex then + v.selected = true + end + if v.key then + v.key = itemDB:getName(v.key) + end end end - end - self:adjustWidth() - self:update() - UI.ScrollingGrid.draw(self) - end, - eventHandler = function(self, event) - if event.type == 'grid_select' then - local fn = string.format('turtle.select(%d)', event.selected.index) - page:runFunction(fn) - else - return UI.ScrollingGrid.eventHandler(self, event) - end - return true - end, - }, ---[[ - policy = UI.ScrollingGrid { - tabTitle = 'Mod', - backgroundColor = UI.TabBar.defaults.selectedBackgroundColor, - columns = { - { heading = 'label', key = 'label' }, + self:adjustWidth() + self:update() + UI.ScrollingGrid.draw(self) + end, + eventHandler = function(self, event) + if event.type == 'grid_select' then + local fn = string.format('turtle.select(%d)', event.selected.index) + page:runFunction(fn) + else + return UI.ScrollingGrid.eventHandler(self, event) + end + return true + end, }, - values = policies, - disableHeader = true, - sortColumn = 'label', - autospace = true, }, - ]] - action = UI.Window { - tabTitle = 'Action', - backgroundColor = colors.cyan, + UI.Tab { + title = 'Action', + backgroundColor = 'primary', moveUp = UI.Button { x = 5, y = 2, text = 'up', @@ -233,8 +225,41 @@ page = UI.Page { info = UI.TextArea { x = 15, y = 2, inactive = true, - } + }, + showBlocks = function(self) + local script = [[ + local function inspect(direction) + local s,b = turtle['inspect' .. (direction or '')]() + if not s then + return 'minecraft:air:0' + end + return string.format('%s:%d', b.name, b.metadata) + end + + local bu, bf, bd = inspect('Up'), inspect(), inspect('Down') + return string.format('%s\n%s\n%s', bu, bf, bd) + ]] + + local s, m = self:runFunction(script, true) + self.info:setText(s or m) + end, + eventHandler = function(self, event) + if event.type == 'button_press' then + if event.button.fn then + self:runFunction(event.button.fn, event.button.nowrap) + self:showBlocks() + end + return true + end + return UI.Tab.eventHandler(self, event) + end, }, + enable = function(self) + if config.tab then + self:selectTab(Util.find(self, 'title', config.tab)) + end + UI.Tabs.enable(self) + end }, statusBar = UI.StatusBar { values = { }, @@ -331,24 +356,6 @@ function page:runScript(scriptName) end end -function page:showBlocks() - local script = [[ - local function inspect(direction) - local s,b = turtle['inspect' .. (direction or '')]() - if not s then - return 'minecraft:air:0' - end - return string.format('%s:%d', b.name, b.metadata) - end - - local bu, bf, bd = inspect('Up'), inspect(), inspect('Down') - return string.format('%s\n%s\n%s', bu, bf, bd) - ]] - - local s, m = self:runFunction(script, true) - self.tabs.action.info:setText(s or m) -end - function page:eventHandler(event) if event.type == 'quit' then UI:quit() @@ -357,13 +364,6 @@ function page:eventHandler(event) config.tab = event.button.text Config.update('Turtles', config) - elseif event.type == 'button_press' then - if event.button.fn then - self:runFunction(event.button.fn, event.button.nowrap) - self:showBlocks() - elseif event.button.script then - self:runScript(event.button.script) - end else return UI.Page.eventHandler(self, event) end @@ -393,9 +393,5 @@ Event.onInterval(1, function() end end) -if config.tab then - page.tabs.tabBar:selectTab(config.tab) -end - UI:setPage(page) UI:start() diff --git a/common/etc/apps.db b/common/etc/apps.db index d8608bb..7580ce5 100644 --- a/common/etc/apps.db +++ b/common/etc/apps.db @@ -57,7 +57,7 @@ category = "Apps", requires = "advancedComputer", iconExt = "\030 \031 \128\030d\159\030 \031d\140\030d\031 \155\030 \0315\140\0305\031 \155\030 \128\010\030 \031d\136\145\0315\136\145\031d\153\031 \128\0315\153\010\030 \031 \128\031d\130\140\134\0315\140\134\031 \128", - run = "fileui --exec=hexedit.lua", + run = "fileui --exec=hexedit.lua --title=hexedit", }, [ "fb1c39e9f4f3c2628ad173ab401a6e4e4baf783d" ] = { title = "Sounds", diff --git a/common/multiMiner.lua b/common/multiMiner.lua index d721461..a271261 100644 --- a/common/multiMiner.lua +++ b/common/multiMiner.lua @@ -308,7 +308,7 @@ local containerText = { } local containTab = UI.Tab { - tabTitle = 'Contain', + title = 'Contain', button = UI.Button { x = 2, y = 2, text = 'Set corner', @@ -321,7 +321,7 @@ local containTab = UI.Tab { } local blocksTab = UI.Tab { - tabTitle = 'Blocks', + title = 'Blocks', grid = UI.ScrollingGrid { y = 1, columns = { @@ -333,7 +333,7 @@ local blocksTab = UI.Tab { } local turtlesTab = UI.Tab { - tabTitle = 'Turtles', + title = 'Turtles', grid = UI.ScrollingGrid { y = 1, values = pool, diff --git a/lzwfs/system/lzwfs.lua b/lzwfs/system/lzwfs.lua index 3f0a7a2..69bccee 100644 --- a/lzwfs/system/lzwfs.lua +++ b/lzwfs/system/lzwfs.lua @@ -14,7 +14,7 @@ local config = Config.load('lzwfs', { }) local tab = UI.Tab { - tabTitle = 'Compression', + title = 'Compression', description = 'Disk compression', [1] = UI.Window { x = 2, y = 2, ex = -2, ey = 6, diff --git a/milo/core/learnWizard.lua b/milo/core/learnWizard.lua index 0f8917f..5fbcc05 100644 --- a/milo/core/learnWizard.lua +++ b/milo/core/learnWizard.lua @@ -1,33 +1,29 @@ local Milo = require('milo') local UI = require('opus.ui') -local turtle = _G.turtle - local learnPage = UI.Page { titleBar = UI.TitleBar { title = 'Learn Recipe' }, wizard = UI.Wizard { y = 2, ey = -2, - pages = { - general = UI.WizardPage { - index = 1, - grid = UI.ScrollingGrid { - x = 2, ex = -2, y = 2, ey = -2, - disableHeader = true, - columns = { - { heading = 'Name', key = 'name'}, - }, - sortColumn = 'name', - }, - accelerators = { - grid_select = 'nextView', + general = UI.WizardPage { + index = 1, + grid = UI.ScrollingGrid { + x = 2, ex = -2, y = 2, ey = -2, + disableHeader = true, + columns = { + { heading = 'Name', key = 'name'}, }, + sortColumn = 'name', + }, + accelerators = { + grid_select = 'nextView', }, }, }, notification = UI.Notification { }, } -local general = learnPage.wizard.pages.general +local general = learnPage.wizard.general function general:validate() Milo:setState('learnType', self.grid:getSelected().value) @@ -37,7 +33,7 @@ end function learnPage:enable() local t = { } - for _, page in pairs(self.wizard.pages) do + for _, page in pairs(self.wizard:getPages()) do if page.validFor then t[page.validFor] = { name = page.validFor, @@ -63,7 +59,7 @@ function learnPage.wizard:getPage(index) local pages = { } table.insert(pages, general) local selected = general.grid:getSelected() - for _, page in pairs(self.pages) do + for _, page in pairs(self:getPages()) do if page.validFor and (not selected or selected.value == page.validFor) then table.insert(pages, page) end diff --git a/milo/core/machines.lua b/milo/core/machines.lua index b3779db..a336d37 100644 --- a/milo/core/machines.lua +++ b/milo/core/machines.lua @@ -35,6 +35,36 @@ local networkPage = UI.Page { }, sortColumn = 'displayName', help = 'Select Node', + getDisplayValues = function(_, row) + row = Util.shallowCopy(row) + local t = { row.name:match(':(.+)_(%d+)$') } + if #t ~= 2 then + t = { row.name:match('(.+)_(%d+)$') } + end + if t and #t == 2 then + row.name, row.suffix = table.unpack(t) + row.name = row.name .. '_' .. row.suffix + end + row.displayName = row.displayName or row.name + return row + end, + getRowTextColor = function(self, row, selected) + if not device[row.name] then + return colors.red + end + if row.mtype == 'ignore' then + return colors.lightGray + end + return UI.Grid.getRowTextColor(self, row, selected) + end, + sortCompare = function(self, a, b) + if self.sortColumn == 'displayName' then + local an = a.displayName or a.name + local bn = b.displayName or b.name + return an:lower() < bn:lower() + end + return UI.Grid.sortCompare(self, a, b) + end, }, remove = UI.Button { y = -2, x = -4, @@ -55,39 +85,6 @@ local networkPage = UI.Page { } } -function networkPage.grid:getDisplayValues(row) - row = Util.shallowCopy(row) - local t = { row.name:match(':(.+)_(%d+)$') } - if #t ~= 2 then - t = { row.name:match('(.+)_(%d+)$') } - end - if t and #t == 2 then - row.name, row.suffix = table.unpack(t) - row.name = row.name .. '_' .. row.suffix - end - row.displayName = row.displayName or row.name - return row -end - -function networkPage.grid:getRowTextColor(row, selected) - if not device[row.name] then - return colors.red - end - if row.mtype == 'ignore' then - return colors.lightGray - end - return UI.Grid:getRowTextColor(row, selected) -end - -function networkPage.grid:sortCompare(a, b) - if self.sortColumn == 'displayName' then - local an = a.displayName or a.name - local bn = b.displayName or b.name - return an:lower() < bn:lower() - end - return UI.Grid.sortCompare(self, a, b) -end - function networkPage:getList() for _, v in pairs(device) do if not context.storage.nodes[v.name] then @@ -96,7 +93,7 @@ function networkPage:getList() mtype = 'ignore', category = 'ignore', } - for _, page in pairs(nodeWizard.wizard.pages) do + for _, page in pairs(nodeWizard.wizard:getPages()) do if page.isValidType and page:isValidType(node) then context.storage.nodes[v.name] = node break @@ -191,46 +188,91 @@ nodeWizard = UI.Page { titleBar = UI.TitleBar { title = 'Configure' }, wizard = UI.Wizard { y = 2, ey = -2, - pages = { - general = UI.WizardPage { - index = 1, - form = UI.Form { - x = 2, ex = -2, y = 1, ey = 3, - manualControls = true, - [1] = UI.TextEntry { - formLabel = 'Name', formKey = 'displayName', - help = 'Set a friendly name', - limit = 64, - }, - [2] = UI.Chooser { - width = 25, - formLabel = 'Type', formKey = 'mtype', - --nochoice = 'Storage', - help = 'Select type', - }, + general = UI.WizardPage { + index = 1, + form = UI.Form { + x = 2, ex = -2, y = 1, ey = 3, + manualControls = true, + [1] = UI.TextEntry { + formLabel = 'Name', formKey = 'displayName', + help = 'Set a friendly name', + limit = 64, }, - grid = UI.ScrollingGrid { - y = 5, ey = -2, x = 2, ex = -2, - columns = { - { heading = 'Slot', key = 'slot', width = 4 }, - { heading = 'Name', key = 'displayName', }, - { heading = 'Qty', key = 'count' , width = 3 }, - }, - sortColumn = 'slot', - help = 'Contents of inventory', + [2] = UI.Chooser { + width = 25, + formLabel = 'Type', formKey = 'mtype', + --nochoice = 'Storage', + help = 'Select type', }, }, - confirmation = UI.WizardPage { - title = 'Confirm changes', - index = 2, - notice = UI.TextArea { - x = 2, ex = -2, y = 2, ey = -2, - value = + grid = UI.ScrollingGrid { + y = 5, ey = -2, x = 2, ex = -2, + columns = { + { heading = 'Slot', key = 'slot', width = 4 }, + { heading = 'Name', key = 'displayName', }, + { heading = 'Qty', key = 'count' , width = 3 }, + }, + sortColumn = 'slot', + help = 'Contents of inventory', + }, + getDisplayValues = function(_, row) + row = Util.shallowCopy(row) + row.displayName = itemDB:getName(row) + return row + end, + enable = function(self) + UI.WizardPage.enable(self) + self:focusFirst() + end, + isValidFor = function() + return false + end, + showInventory = function(self, node) + local inventory + + if device[node.name] and device[node.name].list then + pcall(function() + inventory = device[node.name].list() + for k,v in pairs(inventory) do + v.slot = k + end + end) + end + + self.grid:setValues(inventory or { }) + end, + validate = function(self) + if self.form:save() then + nodeWizard.node.category = Util.find(nodeWizard.choices, 'value', nodeWizard.node.mtype).category + + nodeWizard.nodePages = { } + table.insert(nodeWizard.nodePages, nodeWizard.wizard.general) + for _, page in pairs(nodeWizard.wizard:getPages()) do + if not page.isValidFor or page:isValidFor(nodeWizard.node) then + table.insert(nodeWizard.nodePages, page) + if page.setNode then + page:setNode(nodeWizard.node) + end + end + end + table.insert(nodeWizard.nodePages, nodeWizard.wizard.confirmation) + return true + end + end, + }, + confirmation = UI.WizardPage { + title = 'Confirm changes', + index = 2, + notice = UI.TextArea { + x = 2, ex = -2, y = 2, ey = -2, + value = [[Press accept to save the changes. The settings will take effect immediately!]], - }, }, + isValidFor = function() + return false + end, }, }, statusBar = UI.StatusBar { @@ -255,6 +297,11 @@ The settings will take effect immediately!]], accelerators = { delete = 'remove_entry', }, + getDisplayValues = function(_, row) + row = Util.shallowCopy(row) + row.displayName = itemDB:getName(row) + return row + end, }, remove = UI.Button { x = -4, y = 4, @@ -291,143 +338,76 @@ The settings will take effect immediately!]], statusBar = UI.StatusBar { backgroundColor = 'primary', }, + show = function(self, entry, callback, whitelistOnly) + self.entry = entry + self.callback = callback + + if not self.entry.filter then + self.entry.filter = { } + end + + self.form:setValues(entry) + self:resetGrid() + + self.form[3].inactive = whitelistOnly + + UI.SlideOut.show(self) + self:setFocus(self.form.scan) + + Milo:pauseCrafting({ key = 'gridInUse', msg = 'Crafting paused' }) + end, + hide = function(self) + UI.SlideOut.hide(self) + Milo:resumeCrafting({ key = 'gridInUse' }) + end, + resetGrid = function(self) + local t = { } + for k in pairs(self.entry.filter) do + table.insert(t, itemDB:splitKey(k)) + end + self.grid:setValues(t) + end, + eventHandler = function(self, event) + if event.type == 'focus_change' then + self.statusBar:setStatus(event.focused.help) + + elseif event.type == 'scan_turtle' then + local inventory = Milo:getTurtleInventory() + for _,item in pairs(inventory) do + self.entry.filter[itemDB:makeKey(item)] = true + end + self:resetGrid() + self.grid:update() + self.grid:draw() + Milo:emptyInventory() + + elseif event.type == 'remove_entry' then + local row = self.grid:getSelected() + if row then + Util.removeByValue(self.grid.values, row) + self.grid:update() + self.grid:draw() + end + + elseif event.type == 'save' then + self.form:save() + self.entry.filter = { } + for _,v in pairs(self.grid.values) do + self.entry.filter[itemDB:makeKey(v)] = true + end + self:hide() + self.callback() + + elseif event.type == 'cancel' then + self:hide() + else + return UI.SlideOut.eventHandler(self, event) + end + return true + end, }, } ---[[ Filter slide out ]] -- -function nodeWizard.filter:show(entry, callback, whitelistOnly) - self.entry = entry - self.callback = callback - - if not self.entry.filter then - self.entry.filter = { } - end - - self.form:setValues(entry) - self:resetGrid() - - self.form[3].inactive = whitelistOnly - - UI.SlideOut.show(self) - self:setFocus(self.form.scan) - - Milo:pauseCrafting({ key = 'gridInUse', msg = 'Crafting paused' }) -end - -function nodeWizard.filter:hide() - UI.SlideOut.hide(self) - Milo:resumeCrafting({ key = 'gridInUse' }) -end - -function nodeWizard.filter:resetGrid() - local t = { } - for k in pairs(self.entry.filter) do - table.insert(t, itemDB:splitKey(k)) - end - self.grid:setValues(t) -end - -function nodeWizard.filter.grid:getDisplayValues(row) - row = Util.shallowCopy(row) - row.displayName = itemDB:getName(row) - return row -end - -function nodeWizard.filter:eventHandler(event) - if event.type == 'focus_change' then - self.statusBar:setStatus(event.focused.help) - - elseif event.type == 'scan_turtle' then - local inventory = Milo:getTurtleInventory() - for _,item in pairs(inventory) do - self.entry.filter[itemDB:makeKey(item)] = true - end - self:resetGrid() - self.grid:update() - self.grid:draw() - Milo:emptyInventory() - - elseif event.type == 'remove_entry' then - local row = self.grid:getSelected() - if row then - Util.removeByValue(self.grid.values, row) - self.grid:update() - self.grid:draw() - end - - elseif event.type == 'save' then - self.form:save() - self.entry.filter = { } - for _,v in pairs(self.grid.values) do - self.entry.filter[itemDB:makeKey(v)] = true - end - self:hide() - self.callback() - - elseif event.type == 'cancel' then - self:hide() - - else - return UI.SlideOut.eventHandler(self, event) - end - return true -end - ---[[ General Page ]] -- -function nodeWizard.wizard.pages.general:enable() - UI.WizardPage.enable(self) - self:focusFirst() -end - -function nodeWizard.wizard.pages.general:isValidFor() - return false -end - -function nodeWizard.wizard.pages.general:showInventory(node) - local inventory - - if device[node.name] and device[node.name].list then - pcall(function() - inventory = device[node.name].list() - for k,v in pairs(inventory) do - v.slot = k - end - end) - end - - self.grid:setValues(inventory or { }) -end - -function nodeWizard.wizard.pages.general.grid:getDisplayValues(row) - row = Util.shallowCopy(row) - row.displayName = itemDB:getName(row) - return row -end - -function nodeWizard.wizard.pages.general:validate() - if self.form:save() then - nodeWizard.node.category = Util.find(nodeWizard.choices, 'value', nodeWizard.node.mtype).category - - nodeWizard.nodePages = { } - table.insert(nodeWizard.nodePages, nodeWizard.wizard.pages.general) - for _, page in pairs(nodeWizard.wizard.pages) do - if not page.isValidFor or page:isValidFor(nodeWizard.node) then - table.insert(nodeWizard.nodePages, page) - if page.setNode then - page:setNode(nodeWizard.node) - end - end - end - table.insert(nodeWizard.nodePages, nodeWizard.wizard.pages.confirmation) - return true - end -end - ---[[ Confirmation ]]-- -function nodeWizard.wizard.pages.confirmation:isValidFor() - return false -end - --[[ Wizard ]] -- function nodeWizard:enable(node) local adapter = node.adapter @@ -440,7 +420,7 @@ function nodeWizard:enable(node) { name = 'Ignore', value = 'ignore', category = 'ignore' }, { name = 'Hidden', value = 'hidden', category = 'ignore', help = 'Do not show in list' }, } - for _, page in pairs(self.wizard.pages) do + for _, page in pairs(self.wizard:getPages()) do if page.isValidType then local choice = page:isValidType(self.node) if choice and not Util.find(self.choices, 'value', choice.value) then @@ -448,15 +428,15 @@ function nodeWizard:enable(node) end end end - self.wizard.pages.general.form[1].shadowText = self.node.name - self.wizard.pages.general.form[2].choices = self.choices - self.wizard.pages.general.form:setValues(self.node) + self.wizard.general.form[1].shadowText = self.node.name + self.wizard.general.form[2].choices = self.choices + self.wizard.general.form:setValues(self.node) - self.wizard.pages.general:showInventory(self.node) + self.wizard.general:showInventory(self.node) self.nodePages = { } - table.insert(self.nodePages, self.wizard.pages.general) - table.insert(self.nodePages, self.wizard.pages.confirmation) + table.insert(self.nodePages, self.wizard.general) + table.insert(self.nodePages, self.wizard.confirmation) UI.Page.enable(self) end diff --git a/milo/plugins/item/infoTab.lua b/milo/plugins/item/infoTab.lua index fb96fc1..6634948 100644 --- a/milo/plugins/item/infoTab.lua +++ b/milo/plugins/item/infoTab.lua @@ -2,7 +2,7 @@ local Ansi = require('opus.ansi') local UI = require('opus.ui') local infoTab = UI.Tab { - tabTitle = 'Info', + title = 'Info', index = 4, textArea = UI.TextArea { x = 2, ex = -2, y = 2, ey = -2, diff --git a/milo/plugins/item/machinesTab.lua b/milo/plugins/item/machinesTab.lua index 8f52595..4f4e7a7 100644 --- a/milo/plugins/item/machinesTab.lua +++ b/milo/plugins/item/machinesTab.lua @@ -7,7 +7,7 @@ local colors = _G.colors local context = Milo:getContext() local machinesTab = UI.Tab { - tabTitle = 'Machine', + title = 'Machine', index = 3, grid = UI.ScrollingGrid { x = 2, ex = -2, y = 2, ey = -2, diff --git a/milo/plugins/item/manageTab.lua b/milo/plugins/item/manageTab.lua index 2df0a13..e1f49fe 100644 --- a/milo/plugins/item/manageTab.lua +++ b/milo/plugins/item/manageTab.lua @@ -7,7 +7,7 @@ local Util = require('opus.util') local context = Milo:getContext() local manageTab = UI.Tab { - tabTitle = 'Manage', + title = 'Manage', index = 1, form = UI.Form { x = 1, ex = -1, ey = -1, diff --git a/milo/plugins/item/recipeTab.lua b/milo/plugins/item/recipeTab.lua index 93e321f..591715f 100644 --- a/milo/plugins/item/recipeTab.lua +++ b/milo/plugins/item/recipeTab.lua @@ -4,7 +4,7 @@ local Milo = require('milo') local UI = require('opus.ui') local recipeTab = UI.Tab { - tabTitle = 'Recipe', + title = 'Recipe', index = 2, grid = UI.ScrollingGrid { x = 2, ex = -2, y = 2, ey = -4, diff --git a/milo/plugins/item/resetTab.lua b/milo/plugins/item/resetTab.lua index 0d9d34d..217124b 100644 --- a/milo/plugins/item/resetTab.lua +++ b/milo/plugins/item/resetTab.lua @@ -7,7 +7,7 @@ local colors = _G.colors local context = Milo:getContext() local resetTab = UI.Tab { - tabTitle = 'Reset', + title = 'Reset', index = 5, noFill = true, textArea = UI.TextArea { diff --git a/milo/plugins/remote/autostore.lua b/milo/plugins/remote/autostore.lua index 0c7272b..e7315f2 100644 --- a/milo/plugins/remote/autostore.lua +++ b/milo/plugins/remote/autostore.lua @@ -23,7 +23,7 @@ local page = UI.Page { tabs = UI.Tabs { y = 2, ey = -2, inventory = UI.Tab { - tabTitle = 'Inventory', + title = 'Inventory', grid = UI.ScrollingGrid { y = 2, ey = -2, columns = { @@ -33,7 +33,7 @@ local page = UI.Page { }, }, autostore = UI.Tab { - tabTitle = 'Deposit', + title = 'Deposit', grid = UI.ScrollingGrid { y = 2, ey = -2, columns = { diff --git a/milo/plugins/statsView.lua b/milo/plugins/statsView.lua index b6ab90c..799c1a2 100644 --- a/milo/plugins/statsView.lua +++ b/milo/plugins/statsView.lua @@ -83,7 +83,7 @@ local function createPage(node) parent = monitor, tabs = UI.Tabs { [1] = UI.Tab { - tabTitle = 'Overview', + title = 'Overview', backgroundColor = colors.black, noFill = true, onlineLabel = UI.Text { @@ -135,14 +135,14 @@ local function createPage(node) }, }, [2] = UI.Tab { - tabTitle = 'Stats', + title = 'Stats', noFill = true, textArea = UI.TextArea { y = 3, }, }, [3] = UI.Tab { - tabTitle = 'Storage', + title = 'Storage', noFill = true, grid = UI.ScrollingGrid { y = 2, @@ -157,7 +157,7 @@ local function createPage(node) }, }, [4] = UI.Tab { - tabTitle = 'Offline', + title = 'Offline', noFill = true, grid = UI.ScrollingGrid { y = 2, @@ -168,14 +168,14 @@ local function createPage(node) }, }, [5] = UI.Tab { - tabTitle = 'Activity', + title = 'Activity', noFill = true, term = UI.Embedded { --visible = true, }, }, [6] = UI.Tab { - tabTitle = 'Tasks', + title = 'Tasks', noFill = true, grid = UI.ScrollingGrid { y = 2, @@ -449,7 +449,7 @@ Unlocked Slots : %d of %d (%d%%) function page:eventHandler(event) if event.type == 'tab_activate' then local state = Milo:getState('statusState') or { } - state[node.name] = event.activated.tabTitle + state[node.name] = event.activated.title Milo:setState('statusState', state) end return UI.Page.eventHandler(self, event) @@ -472,7 +472,7 @@ Unlocked Slots : %d of %d (%d%%) -- restore active tab local tabState = Milo:getState('statusState') or { } if tabState[node.name] then - page.tabs:selectTab(Util.find(page.tabs, 'tabTitle', tabState[node.name])) + page.tabs:selectTab(Util.find(page.tabs, 'title', tabState[node.name])) end return page diff --git a/neural/Sensor.lua b/neural/Sensor.lua index 42d0b9a..dbd3c98 100644 --- a/neural/Sensor.lua +++ b/neural/Sensor.lua @@ -23,7 +23,7 @@ local config = Config.load('Sensor') local page = UI.Page { tabs = UI.Tabs { listing = UI.Tab { - tabTitle = 'Listing', + title = 'Listing', grid = UI.ScrollingGrid { columns = { { heading = 'Name', key = 'displayName' }, @@ -35,7 +35,7 @@ local page = UI.Page { }, }, summary = UI.Tab { - tabTitle = 'Summary', + title = 'Summary', grid = UI.ScrollingGrid { columns = { { heading = 'Name', key = 'displayName' }, @@ -256,7 +256,7 @@ function page:eventHandler(event) UI:quit() elseif event.type == 'tab_activate' then - config.activeTab = event.activated.tabTitle + config.activeTab = event.activated.title Config.update('Sensor', config) end @@ -264,7 +264,7 @@ function page:eventHandler(event) end if config.activeTab then - page.tabs:selectTab(Util.find(page.tabs.children, 'tabTitle', config.activeTab)) + page.tabs:selectTab(Util.find(page.tabs.children, 'title', config.activeTab)) end UI:setPage(page) diff --git a/screenSaver/system/saver.lua b/screenSaver/system/saver.lua index 7391f2d..6bf0408 100644 --- a/screenSaver/system/saver.lua +++ b/screenSaver/system/saver.lua @@ -7,7 +7,7 @@ local config = Config.load('saver', { }) return UI.Tab { - tabTitle = 'Screen Saver', + title = 'Screen Saver', description = 'Screen saver', [1] = UI.Window { x = 2, y = 2, ex = -2, ey = 5, diff --git a/secure/system/secure.lua b/secure/system/secure.lua index 9fd0137..05f1e18 100644 --- a/secure/system/secure.lua +++ b/secure/system/secure.lua @@ -7,7 +7,7 @@ local config = Config.load('secure', { }) local tab = UI.Tab { - tabTitle = 'Secure', + title = 'Secure', description = 'Secure options', [1] = UI.Window { x = 2, y = 2, ex = -2, ey = 5, diff --git a/shellex/apis/transfer.lua b/shellex/apis/transfer.lua index aeb781a..d29b730 100644 --- a/shellex/apis/transfer.lua +++ b/shellex/apis/transfer.lua @@ -172,7 +172,7 @@ function lib.recurse(fromPath, toPath, options, origin, top) end end if mv then - if fs.list(toReal)() then -- to is NOT empty + if fs.exists(toPath) then -- to is NOT empty return nil, "cannot move '" .. fromPath .. "' to '" .. toPath .. "': Directory not empty" end status(verbose, fromPath, toPath) diff --git a/swshop/shopTab.lua b/swshop/shopTab.lua index f731ebf..f098574 100644 --- a/swshop/shopTab.lua +++ b/swshop/shopTab.lua @@ -7,7 +7,7 @@ local os = _G.os local config = Config.load('shop') local shopTab = UI.Tab { - tabTitle = 'Store', + title = 'Store', index = 2, form = UI.Form { x = 2, ex = -2, y = 2, ey = -2, diff --git a/turtle/system/turtle.lua b/turtle/system/turtle.lua index a144205..172d32d 100644 --- a/turtle/system/turtle.lua +++ b/turtle/system/turtle.lua @@ -9,7 +9,7 @@ if turtle then local config = Config.load('gps') local gpsTab = UI.Tab { - tabTitle = 'Home', + title = 'Home', description = 'Turtle home location', labelText = UI.Text { x = 3, ex = -3, y = 2, -- 2.49.1 From 8a9878b8e52f9ed5177dc239bbdfe86d135cd804 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Mon, 27 Apr 2020 22:31:57 -0600 Subject: [PATCH 26/90] minor bug fixes --- ccemux/autorun/startup.lua | 13 +++++++++++++ milo/core/machines.lua | 10 +++++----- miners/etc/apps.db | 4 ++-- monitor/mwm.lua | 3 +++ swshop/etc/apps.db | 2 +- turtle/system/turtle.lua | 4 ++-- 6 files changed, 26 insertions(+), 10 deletions(-) diff --git a/ccemux/autorun/startup.lua b/ccemux/autorun/startup.lua index ca21d16..cb3d466 100644 --- a/ccemux/autorun/startup.lua +++ b/ccemux/autorun/startup.lua @@ -1,6 +1,7 @@ local ccemux = _G.ccemux local fs = _G.fs local peripheral = _G.peripheral +local textutils = _G.textutils if ccemux then -- add a System setup tab @@ -13,4 +14,16 @@ if ccemux then ccemux.attach(k, v.type, v.args) end end + + _G.kernel.hook('clipboard_copy', function(_, args) + local data = args[1] + if type(data) == 'table' then + local s, m = pcall(textutils.serialize, data) + data = s and m or tostring(data) + end + + if data then + ccemux.setClipboard(data) + end + end) end diff --git a/milo/core/machines.lua b/milo/core/machines.lua index a336d37..049c5a1 100644 --- a/milo/core/machines.lua +++ b/milo/core/machines.lua @@ -214,12 +214,12 @@ nodeWizard = UI.Page { }, sortColumn = 'slot', help = 'Contents of inventory', + getDisplayValues = function(_, row) + row = Util.shallowCopy(row) + row.displayName = itemDB:getName(row) + return row + end, }, - getDisplayValues = function(_, row) - row = Util.shallowCopy(row) - row.displayName = itemDB:getName(row) - return row - end, enable = function(self) UI.WizardPage.enable(self) self:focusFirst() diff --git a/miners/etc/apps.db b/miners/etc/apps.db index 4d0341f..df03753 100644 --- a/miners/etc/apps.db +++ b/miners/etc/apps.db @@ -1,14 +1,14 @@ { [ "4486006f811b88cacd5f211fd579717e29b600cd" ] = { title = "Simple", - category = "Mining", + category = "Turtle", icon = "\030 \0315\\\030 \031 \010\030 \0304\031f _ \030 \031c/\0315\\\010\030 \0304 ", run = "simpleMiner.lua", requires = 'turtle', }, [ "da39a3ee5e6b4b0d3255bfef95601890afd80709" ] = { title = "Scanning", - category = "Mining", + category = "Turtle", icon = "\030 \0315\\\030 \031 \010\030 \0304\031f _ \030 \031c/\0315\\\010\030 \0304 ", run = "scanningMiner.lua", requires = 'turtle', diff --git a/monitor/mwm.lua b/monitor/mwm.lua index a397606..eef8753 100644 --- a/monitor/mwm.lua +++ b/monitor/mwm.lua @@ -120,6 +120,9 @@ function Process:new(args) self.window = window.create(self.container, 2, 3, args.width, args.height, true) self.terminal = self.window + self.container.setBackgroundColor(colors.black) + self.container.clear() + self.container.canvas.parent = monitor.canvas if not monitor.canvas.children then monitor.canvas.children = { } diff --git a/swshop/etc/apps.db b/swshop/etc/apps.db index b0dc5c9..68115b0 100644 --- a/swshop/etc/apps.db +++ b/swshop/etc/apps.db @@ -1,7 +1,7 @@ { [ "377aaf9a802a23873738c0c3916282c8884c11fb" ] = { title = "ShopLogs", - category = "Shop", + category = "Apps", run = "Shoplogs /usr/swshop.log", iconExt = "\0300\031f\136\140\132\0308\031 \130\030 \0318\144\010\0300\0315/\0305\031d\\\030f\0310\142\143\030 \149\010\0305\031d\\\030d\0315/\0300\031f\132\140\030 \0310\149", }, diff --git a/turtle/system/turtle.lua b/turtle/system/turtle.lua index 172d32d..b4bda06 100644 --- a/turtle/system/turtle.lua +++ b/turtle/system/turtle.lua @@ -1,7 +1,6 @@ local Config = require('opus.config') local UI = require('opus.ui') -local colors = _G.colors local fs = _G.fs local turtle = _G.turtle @@ -11,9 +10,10 @@ if turtle then local gpsTab = UI.Tab { title = 'Home', description = 'Turtle home location', + noFill = true, labelText = UI.Text { x = 3, ex = -3, y = 2, - textColor = colors.yellow, + textColor = 'yellow', value = 'On restart, return to this location' }, grid = UI.Grid { -- 2.49.1 From 9bf017fe2718098ea9190aa82e2e6fd1b1794703 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Sat, 2 May 2020 22:22:30 -0600 Subject: [PATCH 27/90] rename help files with .txt - overlay.lua creates a terminal compatible window on the overlay glasses - you can now run any program on the glasses overlay --- builder/help/{builder => builder.txt} | 0 ccemux/system/ccemux.lua | 193 +++++++++--------- common/.package | 3 + common/Devices.lua | 95 ++++----- common/debugMonitor.lua | 3 +- common/edit.lua | 2 +- farms/help/{farmer => farmer.txt} | 0 .../help/{superTreefarm => superTreefarm.txt} | 0 .../help/{scanningMiner => scanningMiner.txt} | 0 miners/help/{simpleMiner => simpleMiner.txt} | 0 monitor/help/{mirror => mirror.txt} | 0 neural/overlay.lua | 108 ++++++++++ swshop/help/{swshop => swshop.txt} | 0 13 files changed, 258 insertions(+), 146 deletions(-) rename builder/help/{builder => builder.txt} (100%) rename farms/help/{farmer => farmer.txt} (100%) rename farms/help/{superTreefarm => superTreefarm.txt} (100%) rename miners/help/{scanningMiner => scanningMiner.txt} (100%) rename miners/help/{simpleMiner => simpleMiner.txt} (100%) rename monitor/help/{mirror => mirror.txt} (100%) create mode 100644 neural/overlay.lua rename swshop/help/{swshop => swshop.txt} (100%) diff --git a/builder/help/builder b/builder/help/builder.txt similarity index 100% rename from builder/help/builder rename to builder/help/builder.txt diff --git a/ccemux/system/ccemux.lua b/ccemux/system/ccemux.lua index d066bab..2c60daf 100644 --- a/ccemux/system/ccemux.lua +++ b/ccemux/system/ccemux.lua @@ -6,121 +6,120 @@ local ccemux = _G.ccemux local sides = { 'bottom', 'top', 'back', 'front', 'right', 'left' } local tab = UI.Tab { - title = 'CCEmuX', - description = 'CCEmuX peripherals', - form = UI.Form { - x = 2, ex = -2, y = 2, ey = 5, - values = { - side = 'bottom', - type = 'wireless_modem', - }, - manualControls = true, - side = UI.Chooser { - formLabel = 'Side', formKey = 'side', - width = 10, - }, - ptype = UI.Chooser { - formLabel = 'Type', formKey = 'type', - width = 10, - choices = { - { name = 'Modem', value = 'wireless_modem' }, - { name = 'Drive', value = 'disk_drive' }, - }, - }, - drive_id = UI.TextEntry { - x = 19, y = 3, - formKey = 'drive_id', - shadowText = 'id', - width = 5, - limit = 3, - transform = 'number', - }, - add = UI.Button { - x = -6, y = 3, width = 5, - text = 'Add', event = 'form_ok', - help = 'Add items to turtle to add to filter', - }, - }, - grid = UI.Grid { - x = 2, ex = -2, y = 7, ey = -2, - columns = { - { heading = 'Side', key = 'side', width = 8 }, - { heading = 'Type', key = 'type' }, - { heading = 'ID', key = 'args', width = 4 }, - }, - }, + title = 'CCEmuX', + description = 'CCEmuX peripherals', + form = UI.Form { + x = 2, ex = -2, y = 2, ey = 5, + values = { + side = 'bottom', + type = 'wireless_modem', + }, + manualControls = true, + side = UI.Chooser { + formLabel = 'Side', formKey = 'side', + width = 10, + }, + ptype = UI.Chooser { + formLabel = 'Type', formKey = 'type', + width = 10, + choices = { + { name = 'Modem', value = 'wireless_modem' }, + { name = 'Drive', value = 'disk_drive' }, + }, + }, + drive_id = UI.TextEntry { + x = 19, y = 3, + formKey = 'drive_id', + shadowText = 'id', + width = 5, + limit = 3, + transform = 'number', + }, + add = UI.Button { + x = -6, y = 3, width = 5, + text = 'Add', event = 'form_ok', + }, + }, + grid = UI.Grid { + x = 2, ex = -2, y = 7, ey = -2, + columns = { + { heading = 'Side', key = 'side', width = 8 }, + { heading = 'Type', key = 'type' }, + { heading = 'ID', key = 'args', width = 4 }, + }, + }, } function tab:updatePeripherals(config) - self.grid.values = { } - for k,v in pairs(config) do - table.insert(self.grid.values, { - side = k, - type = v.type, - args = v.args and v.args.id, - }) - end - self.grid:update() + self.grid.values = { } + for k,v in pairs(config) do + table.insert(self.grid.values, { + side = k, + type = v.type, + args = v.args and v.args.id, + }) + end + self.grid:update() end function tab:enable() - local config = Config.load('ccemux') + local config = Config.load('ccemux') - local choices = { } - for _,k in pairs(sides) do - table.insert(choices, { name = k, value = k }) - end - self.form.side.choices = choices + local choices = { } + for _,k in pairs(sides) do + table.insert(choices, { name = k, value = k }) + end + self.form.side.choices = choices - self:updatePeripherals(config) - UI.Tab.enable(self) + self:updatePeripherals(config) + UI.Tab.enable(self) - self.form.drive_id.enabled = false + self.form.drive_id.enabled = false end function tab:eventHandler(event) - if event.type == 'form_complete' then - if event.values.type == 'disk_drive' and not event.values.drive_id then - self:emit({ type = 'error_message', message = 'Invalid drive ID' }) - else - ccemux.detach(event.values.side) + if event.type == 'form_complete' then + if event.values.type == 'disk_drive' and not event.values.drive_id then + self:emit({ type = 'error_message', message = 'Invalid drive ID' }) + else + ccemux.detach(event.values.side) - local config = Config.load('ccemux') - config[event.values.side] = { - type = event.values.type - } - if event.values.type == 'disk_drive' then - config[event.values.side].args = { - id = event.values.drive_id - } - ccemux.attach(event.values.side, event.values.type, { id = event.values.drive_id }) - else - ccemux.attach(event.values.side, event.values.type) - end - Config.update('ccemux', config) - self:updatePeripherals(config) - self.grid:draw() + local config = Config.load('ccemux') + config[event.values.side] = { + type = event.values.type + } + if event.values.type == 'disk_drive' then + config[event.values.side].args = { + id = event.values.drive_id + } + ccemux.attach(event.values.side, event.values.type, { id = event.values.drive_id }) + else + ccemux.attach(event.values.side, event.values.type) + end + Config.update('ccemux', config) + self:updatePeripherals(config) + self.grid:draw() - self:emit({ type = 'success_message', message = 'Attached' }) - end + self:emit({ type = 'success_message', message = 'Attached' }) + end - elseif event.type == 'choice_change' then - if event.element == self.form.ptype then - self.form.drive_id.enabled = event.value == 'disk_drive' - self.form:draw() - end + elseif event.type == 'choice_change' then + if event.element == self.form.ptype then + self.form.drive_id.enabled = event.value == 'disk_drive' + self.form:draw() + end - elseif event.type == 'grid_select' then - local config = Config.load('ccemux') - config[event.selected.side] = nil - Config.update('ccemux', config) - self:updatePeripherals(config) - self.grid:draw() + elseif event.type == 'grid_select' then + local config = Config.load('ccemux') + config[event.selected.side] = nil + Config.update('ccemux', config) + self:updatePeripherals(config) + self.grid:draw() - self:emit({ type = 'success_message', message = 'Detached' }) + self:emit({ type = 'success_message', message = 'Detached' }) - return true - end + return true + end end return tab diff --git a/common/.package b/common/.package index 939bc39..4ffb026 100644 --- a/common/.package +++ b/common/.package @@ -13,4 +13,7 @@ * and more... ]], license = 'MIT', + required = { + 'core', + }, } diff --git a/common/Devices.lua b/common/Devices.lua index fc2cecd..7710bd5 100644 --- a/common/Devices.lua +++ b/common/Devices.lua @@ -3,26 +3,26 @@ local Event = require('opus.event') local UI = require('opus.ui') local Util = require('opus.util') -local peripheral = _G.peripheral +local device = _G.device --[[ -- PeripheralsPage -- ]] -- local peripheralsPage = UI.Page { grid = UI.ScrollingGrid { ey = -2, columns = { + --{ heading = 'Name', key = 'name' }, { heading = 'Type', key = 'type' }, { heading = 'Side', key = 'side' }, }, sortColumn = 'type', autospace = true, enable = function(self) - local sides = peripheral.getNames() - Util.clear(self.values) - for _,side in pairs(sides) do + for _,v in pairs(device) do table.insert(self.values, { - type = peripheral.getType(side), - side = side + type = v.type, + side = v.side, + name = v.name, }) end self:update() @@ -75,49 +75,50 @@ local methodsPage = UI.Page { [ 'control-q' ] = 'back', backspace = 'back', }, + enable = function(self, p) + self.peripheral = p or self.peripheral + + p = device[self.peripheral.name] + if p.getDocs then + -- plethora + self.grid.values = { } + for k,v in pairs(p.getDocs()) do + table.insert(self.grid.values, { + name = k, + doc = v, + }) + end + elseif not p.getAdvancedMethodsData then + -- computercraft + self.grid.values = { } + for k,v in pairs(p) do + if type(v) == 'function' then + table.insert(self.grid.values, { + name = k, + noext = true, + }) + end + end + else + -- open peripherals + self.grid.values = p.getAdvancedMethodsData() + for name,f in pairs(self.grid.values) do + f.name = name + end + end + + self.grid:update() + self.grid:setIndex(1) + + self.doc:setText(self:getDocumentation()) + + self.statusBar:setStatus(self.peripheral.type) + UI.Page.enable(self) + + self:setFocus(self.grid) + end, } -function methodsPage:enable(p) - self.peripheral = p or self.peripheral - - p = peripheral.wrap(self.peripheral.side) - if p.getDocs then - -- plethora - self.grid.values = { } - for k,v in pairs(p.getDocs()) do - table.insert(self.grid.values, { - name = k, - doc = v, - }) - end - elseif not p.getAdvancedMethodsData then - -- computercraft - self.grid.values = { } - for name in pairs(p) do - table.insert(self.grid.values, { - name = name, - noext = true, - }) - end - else - -- open peripherals - self.grid.values = p.getAdvancedMethodsData() - for name,f in pairs(self.grid.values) do - f.name = name - end - end - - self.grid:update() - self.grid:setIndex(1) - - self.doc:setText(self:getDocumentation()) - - self.statusBar:setStatus(self.peripheral.type) - UI.Page.enable(self) - - self:setFocus(self.grid) -end - function methodsPage:eventHandler(event) if event.type == 'back' then UI:setPage(peripheralsPage) diff --git a/common/debugMonitor.lua b/common/debugMonitor.lua index 2d963b5..74e8c84 100644 --- a/common/debugMonitor.lua +++ b/common/debugMonitor.lua @@ -1,11 +1,12 @@ local Util = require('opus.util') +local device = _G.device local os = _G.os local peripheral = _G.peripheral local term = _G.term local args = { ... } -local mon = args[1] and peripheral.wrap(args[1]) or +local mon = args[1] and device[args[1]] or peripheral.wrap(args[1]) or peripheral.find('monitor') or error('Syntax: debug ') diff --git a/common/edit.lua b/common/edit.lua index c2097e3..f0c7be3 100644 --- a/common/edit.lua +++ b/common/edit.lua @@ -306,7 +306,7 @@ local page = UI.Page { x = 2, y = 2, ey = -4, ex = -2, columns = { { key = 'name', heading = 'Name' }, - { key = 'dir', heading = 'Directory' }, + { key = 'dir', heading = 'Directory', textColor = 'lightGray' }, }, accelerators = { backspace = 'slide_hide', diff --git a/farms/help/farmer b/farms/help/farmer.txt similarity index 100% rename from farms/help/farmer rename to farms/help/farmer.txt diff --git a/farms/help/superTreefarm b/farms/help/superTreefarm.txt similarity index 100% rename from farms/help/superTreefarm rename to farms/help/superTreefarm.txt diff --git a/miners/help/scanningMiner b/miners/help/scanningMiner.txt similarity index 100% rename from miners/help/scanningMiner rename to miners/help/scanningMiner.txt diff --git a/miners/help/simpleMiner b/miners/help/simpleMiner.txt similarity index 100% rename from miners/help/simpleMiner rename to miners/help/simpleMiner.txt diff --git a/monitor/help/mirror b/monitor/help/mirror.txt similarity index 100% rename from monitor/help/mirror rename to monitor/help/mirror.txt diff --git a/neural/overlay.lua b/neural/overlay.lua new file mode 100644 index 0000000..598d209 --- /dev/null +++ b/neural/overlay.lua @@ -0,0 +1,108 @@ +local Terminal = require('opus.terminal') + +local colors = _G.colors +local device = _G.device + +--[[ + Create a device for glasses + Usable as a redirect or UI target + + Example usage: + Files --display=glasses + debugMonitor glasses + + In a program: + local prev = term.redirect(device.glasses) + shell.run('shell') + term.redirect(prev) + + Glasses do not use the CC font - so extended chars + do not display correctly. +]] + +-- configurable +local w, h = 46, 19 +local scale = .5 + +local glasses = device['plethora:glasses'] +local canvas = glasses.canvas() +local _, cy = 1, 1 +local _, gh = canvas:getSize() +local lines = { } +local map = { + ['0'] = 0xF0F0F0FF, + ['1'] = 0xF2B233FF, + ['2'] = 0xE57FD8FF, + ['3'] = 0x99B2F2FF, + ['4'] = 0xDEDE6CFF, + ['5'] = 0x7FCC19FF, + ['6'] = 0xF2B2CCFF, + ['7'] = 0x4C4C4CFF, + ['8'] = 0x999999FF, + ['9'] = 0x4C99B2FF, + ['a'] = 0xB266E5FF, + ['b'] = 0x3366CCFF, + ['c'] = 0x7F664CFF, + ['d'] = 0x57A64EFF, + ['e'] = 0xCC4C4CFF, + ['f'] = 0x191919FF, -- transparent +} + +local xs, ys = 6 * scale, 9 * scale + +-- Position bottom left +local group = canvas.addGroup({ x = 1, y = gh - (h * ys) - 10 }) + +for y = 1, h do + lines[y] = { + text = { }, + bg = { } + } + for x = 1, w do + lines[y].bg[x] = group.addRectangle(x * xs, y * ys, xs, ys, 0xF0F0F04F) + lines[y].text[x] = group.addText({ x * xs, y * ys }, '', 0x7FCC19FF) + lines[y].text[x].setScale(scale) + end +end + +device.glasses = Terminal.window({ + getSize = function() + return w, h + end, + isColor = function() + return true + end, + clear = function() + --canvas.clear() + end, + blit = function(text, fg, bg) + for x = 1, #text do + local ln = lines[cy] + ln.bg[x].setColor(map[bg:sub(x, x)]) + ln.text[x].setColor(map[fg:sub(x, x)]) + ln.text[x].setText(text:sub(x, x)) + end + end, + setCursorPos = function(_, y) + -- full lines are always blit + cy = y + end, + setBackgroundColor = function() + end, + setTextColor = function() + end, + setCursorBlink = function() + end, + getBackgroundColor = function() + return colors.black + end, + getTextColor = function() + return colors.white + end, +}, 1, 1, w, h, true) + +function device.glasses.setTextScale() end + +device.glasses.side = 'glasses' +device.glasses.type = 'glasses' +device.glasses.name = 'glasses' diff --git a/swshop/help/swshop b/swshop/help/swshop.txt similarity index 100% rename from swshop/help/swshop rename to swshop/help/swshop.txt -- 2.49.1 From 38b5c4a5ed34bbd17830e7233af7761f0e4230c7 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Sat, 2 May 2020 23:45:58 -0600 Subject: [PATCH 28/90] support mouse clicks when using plethora keyboard on overlay --- neural/overlay.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/neural/overlay.lua b/neural/overlay.lua index 598d209..83984c5 100644 --- a/neural/overlay.lua +++ b/neural/overlay.lua @@ -2,6 +2,7 @@ local Terminal = require('opus.terminal') local colors = _G.colors local device = _G.device +local kernel = _G.kernel --[[ Create a device for glasses @@ -51,7 +52,8 @@ local map = { local xs, ys = 6 * scale, 9 * scale -- Position bottom left -local group = canvas.addGroup({ x = 1, y = gh - (h * ys) - 10 }) +local pos = { x = 1, y = gh - (h * ys) - 10 } +local group = canvas.addGroup(pos) for y = 1, h do lines[y] = { @@ -106,3 +108,9 @@ function device.glasses.setTextScale() end device.glasses.side = 'glasses' device.glasses.type = 'glasses' device.glasses.name = 'glasses' + +kernel.hook('glasses_click', function(_, eventData) + os.queueEvent('monitor_touch', 'glasses', + math.floor((eventData[2] - pos.x) / xs), + math.floor((eventData[3] - pos.y) / ys)) +end) -- 2.49.1 From 39fb43d6a3e002b89af2d6b18169b5f47ec899f3 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Mon, 4 May 2020 02:53:17 -0400 Subject: [PATCH 29/90] Update storageGen to a more user-friendly UI - Able to merge into current Milo configs - Moved it to the Milo package --- milo/apps/storageGen.lua | 143 +++++++++++++++++++++++++++++++++++ miloApps/apps/storageGen.lua | 29 ------- 2 files changed, 143 insertions(+), 29 deletions(-) create mode 100644 milo/apps/storageGen.lua delete mode 100644 miloApps/apps/storageGen.lua diff --git a/milo/apps/storageGen.lua b/milo/apps/storageGen.lua new file mode 100644 index 0000000..4dd4e03 --- /dev/null +++ b/milo/apps/storageGen.lua @@ -0,0 +1,143 @@ +--[[ + For initially setting up large amounts of storage chests. +]] + +local UI = require('opus.ui') +local Util = require('opus.util') +local Peripheral = require('opus.peripheral') + +local defaultStoragePath = "/usr/config/storage" + +local page = UI.Page { + notification = UI.Notification {}, + + infoText = UI.TextArea { + x = 2, y = 2, + height = 2, + textColor = colors.yellow, + value = "Select storage types to merge into your Milo storage config.", + }, + + typeGrid = UI.CheckboxGrid { + x = 2, y = 4, + ex = -2, ey = -4, + sortColumn = "amount", + inverseSort = true, + columns = { + {heading = 'Type', key = 'type'}, + {heading = 'Amount', key = 'amount', align = 'right', width = 6}, + }, + }, + + rescanButton = UI.Button { + x = 2, y = -2, + text = 'Rescan', + event = 'rescan', + }, + + doneButton = UI.Button { + x = -7, y = -2, + text = 'Save', + event = 'save', + }, + + confirm = UI.Question { + x = -38, y = -2, + label = 'Overwrite Milo settings?', + }, + + fileSelect = UI.FileSelect { + modal = true, + enable = function() end, + transitionHint = 'expandUp', + show = function(self) + UI.FileSelect.enable(self) + self:focusFirst() + self:draw() + end, + disable = function(self) + UI.FileSelect.disable(self) + self.parent:focusFirst() + self.parent:capture(self.parent) + end, + eventHandler = function(self, event) + if event.type == 'select_cancel' then + self:disable() + elseif event.type == 'select_file' then + self:disable() + end + return UI.FileSelect.eventHandler(self, event) + end, + }, +} + +function page:scan() + _syslog("Scanned") + + self.storages = Util.filter(Peripheral.getList(), function(dev) + return not not dev.pushItems + end) + + local types = {} + Util.each(self.storages, function(dev, name) + if not types[dev.type] then types[dev.type] = {amount = 0, type = dev.type} end + types[dev.type].amount = types[dev.type].amount + 1 + end) + + self.typeGrid:setValues(types) + self:draw() + self:sync() +end + +function page:saveConfig(path) + _syslog("Saving to "..path) + + local config = Util.readTable(path) or {} + Util.each(self.storages, function(dev, name) + if self.typeGrid.values[dev.type] and self.typeGrid.values[dev.type].checked then + config[name] = { + name = name, + category = 'storage', + mtype = 'storage', + } + end + end) + Util.writeTable(path, config) + self.notification:success("Config saved to "..path) +end + +function page:enable() + self:scan() + UI.Page.enable(self) +end + +function page.typeGrid:getRowTextColor(row, selected) + return row.checked and colors.yellow or UI.Grid.getRowTextColor(self, row, selected) +end + +function page:eventHandler(event) + _syslog(event) + if event.type == "rescan" then + self:scan() + + elseif event.type == "save" then + self.confirm:show() + + elseif event.type == "question_yes" then + self:saveConfig(defaultStoragePath) + self.confirm:hide() + + elseif event.type == "question_no" then + self.confirm:hide() + self.fileSelect:show() + + elseif event.type == "select_file" then + self:saveConfig(event.file) + + else return UI.Page.eventHandler(self, event) + end + return true +end + +UI:setPage(page) +UI:start() diff --git a/miloApps/apps/storageGen.lua b/miloApps/apps/storageGen.lua deleted file mode 100644 index e2676a5..0000000 --- a/miloApps/apps/storageGen.lua +++ /dev/null @@ -1,29 +0,0 @@ ---[[ - For initially setting up large amounts of storage chests. -]] - -local Util = require('opus.util') - -local peripheral = _G.peripheral - -local args = { ... } -local st = args[1] or error('Specify a storage type (ie. minecraft:chest)') - -local config = { } -peripheral.find(st, function(n) - config[n] = { - name = n, - category = 'storage', - mtype = 'storage', - } -end) - -print('Found ' .. Util.size(config)) - -if Util.size(config) == 0 then - error('Invalid peripheral type') -end - -Util.writeTable('usr/config/storageGen', config) -print('storageGen file created in usr/config') -print('update /usr/config/storage with contents (or rename to storage)') -- 2.49.1 From caa525e31d045c5456e93eb386540d8aa29b9aa6 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Mon, 4 May 2020 04:09:13 -0400 Subject: [PATCH 30/90] Swshop: Add option to show out of stock items - Make ShopView use colors from ui.theme --- swshop/shopConfig.lua | 13 ++++++++----- swshop/shopView.lua | 23 +++++++++++++---------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/swshop/shopConfig.lua b/swshop/shopConfig.lua index 857d0c1..c5958ea 100644 --- a/swshop/shopConfig.lua +++ b/swshop/shopConfig.lua @@ -29,10 +29,13 @@ local wizardPage = UI.WizardPage { [3] = UI.Checkbox { formLabel = 'Single shop', formKey = 'refundInvalid', help = 'Only this shop uses this domain', - limit = 64, }, - [4] = UI.Chooser { - formLabel = 'RS Signal', formKey = 'rsSide', formIndex = 5, + [4] = UI.Checkbox { + formLabel = 'Show out of stock', formKey = 'showOutOfStock', + help = 'Show out of stock items in red', + }, + [5] = UI.Chooser { + formLabel = 'RS Signal', formKey = 'rsSide', formIndex = 6, width = 10, nochoice = 'Top', choices = { @@ -45,9 +48,9 @@ local wizardPage = UI.WizardPage { }, required = true, }, - [5] = UI.Chooser { + [6] = UI.Chooser { width = 9, - formIndex = 6, + formIndex = 7, formLabel = 'Font Size', formKey = 'textScale', nochoice = 'Small', choices = { diff --git a/swshop/shopView.lua b/swshop/shopView.lua index 223ff6c..4a51b17 100644 --- a/swshop/shopView.lua +++ b/swshop/shopView.lua @@ -49,15 +49,15 @@ local function createPage(node) local page = UI.Page { parent = monitor, header = UI.Window { - backgroundColor = colors.cyan, + backgroundColor = 'primary', ey = 3, }, grid = UI.Grid { y = 4, ey = -7, headerHeight = 3, - headerBackgroundColor = colors.gray, + headerBackgroundColor = 'tertiary', backgroundSelectedColor = colors.black, - unfocusedBackgroundSelectedColor = colors.gray, + unfocusedBackgroundSelectedColor = 'tertiary', columns = { { heading = 'Stock', key = 'count', width = 6, align = 'right' }, { heading = 'Name', key = 'displayName' }, @@ -68,17 +68,17 @@ local function createPage(node) }, footer = UI.Window { y = -6, - backgroundColor = colors.gray, + backgroundColor = 'tertiary', prevButton = UI.Button { x = 2, y = 3, height = 3, width = 5, event = 'previous', - backgroundColor = colors.lightGray, + backgroundColor = 'secondary', text = ' \017 ', }, nextButton = UI.Button { x = -6, y = 3, height = 3, width = 5, event = 'next', - backgroundColor = colors.lightGray, + backgroundColor = 'secondary', text = ' \016 ', }, info = UI.Window { @@ -94,7 +94,7 @@ local function createPage(node) if node.header then self:centeredWrite(2, node.header, nil, colors.white) end - self:write(self.width - 15, 3, 'powered by Milo', nil, colors.gray) + self:write(self.width - 15, 3, 'powered by Milo', nil, 'tertiary') end function page.footer.info:draw() @@ -112,6 +112,9 @@ local function createPage(node) end function page.grid:getRowTextColor(row, selected) + if row.count < 1 then + return colors.red + end if selected then return colors.yellow end @@ -151,11 +154,11 @@ local function createPage(node) local list = Milo:listItems() self.grid.values = { } for k,v in pairs(config) do - local item = list[k] - if item and item.count > 0 then + local item = list[k] or itemDB:get(k) + if item and (node.showOutOfStock or item.count > 0) then table.insert(self.grid.values, { displayName = item.displayName, - count = item.count, + count = item.count or 0, name = v.name, price = v.price, info = v.info, -- 2.49.1 From 8f13a0932e0e2e480d087b8ae63919cb833fcaa9 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Mon, 4 May 2020 16:45:31 -0600 Subject: [PATCH 31/90] neural window manager --- ccemux/system/ccemux.lua | 192 +++++++++++++++++++-------------------- neural/apis/glasses.lua | 121 ++++++++++++++++++++++++ neural/nwm.lua | 160 ++++++++++++++++++++++++++++++++ neural/overlay.lua | 116 ----------------------- 4 files changed, 377 insertions(+), 212 deletions(-) create mode 100644 neural/apis/glasses.lua create mode 100644 neural/nwm.lua delete mode 100644 neural/overlay.lua diff --git a/ccemux/system/ccemux.lua b/ccemux/system/ccemux.lua index 2c60daf..e4360ea 100644 --- a/ccemux/system/ccemux.lua +++ b/ccemux/system/ccemux.lua @@ -6,120 +6,120 @@ local ccemux = _G.ccemux local sides = { 'bottom', 'top', 'back', 'front', 'right', 'left' } local tab = UI.Tab { - title = 'CCEmuX', - description = 'CCEmuX peripherals', - form = UI.Form { - x = 2, ex = -2, y = 2, ey = 5, - values = { - side = 'bottom', - type = 'wireless_modem', - }, - manualControls = true, - side = UI.Chooser { - formLabel = 'Side', formKey = 'side', - width = 10, - }, - ptype = UI.Chooser { - formLabel = 'Type', formKey = 'type', - width = 10, - choices = { - { name = 'Modem', value = 'wireless_modem' }, - { name = 'Drive', value = 'disk_drive' }, - }, - }, - drive_id = UI.TextEntry { - x = 19, y = 3, - formKey = 'drive_id', - shadowText = 'id', - width = 5, - limit = 3, - transform = 'number', - }, - add = UI.Button { - x = -6, y = 3, width = 5, - text = 'Add', event = 'form_ok', - }, - }, - grid = UI.Grid { - x = 2, ex = -2, y = 7, ey = -2, - columns = { - { heading = 'Side', key = 'side', width = 8 }, - { heading = 'Type', key = 'type' }, - { heading = 'ID', key = 'args', width = 4 }, - }, - }, + title = 'CCEmuX', + description = 'CCEmuX peripherals', + form = UI.Form { + x = 2, ex = -2, y = 2, ey = 5, + values = { + side = 'bottom', + type = 'wireless_modem', + }, + manualControls = true, + side = UI.Chooser { + formLabel = 'Side', formKey = 'side', + width = 10, + }, + ptype = UI.Chooser { + formLabel = 'Type', formKey = 'type', + width = 10, + choices = { + { name = 'Modem', value = 'wireless_modem' }, + { name = 'Drive', value = 'disk_drive' }, + }, + }, + drive_id = UI.TextEntry { + x = 19, y = 3, + formKey = 'drive_id', + shadowText = 'id', + width = 5, + limit = 3, + transform = 'number', + }, + add = UI.Button { + x = -6, y = 3, width = 5, + text = 'Add', event = 'form_ok', + }, + }, + grid = UI.Grid { + x = 2, ex = -2, y = 7, ey = -2, + columns = { + { heading = 'Side', key = 'side', width = 8 }, + { heading = 'Type', key = 'type' }, + { heading = 'ID', key = 'args', width = 4 }, + }, + }, } function tab:updatePeripherals(config) - self.grid.values = { } - for k,v in pairs(config) do - table.insert(self.grid.values, { - side = k, - type = v.type, - args = v.args and v.args.id, - }) - end - self.grid:update() + self.grid.values = { } + for k,v in pairs(config) do + table.insert(self.grid.values, { + side = k, + type = v.type, + args = v.args and v.args.id, + }) + end + self.grid:update() end function tab:enable() - local config = Config.load('ccemux') + local config = Config.load('ccemux') - local choices = { } - for _,k in pairs(sides) do - table.insert(choices, { name = k, value = k }) - end - self.form.side.choices = choices + local choices = { } + for _,k in pairs(sides) do + table.insert(choices, { name = k, value = k }) + end + self.form.side.choices = choices - self:updatePeripherals(config) - UI.Tab.enable(self) + self:updatePeripherals(config) + UI.Tab.enable(self) - self.form.drive_id.enabled = false + self.form.drive_id.enabled = false end function tab:eventHandler(event) - if event.type == 'form_complete' then - if event.values.type == 'disk_drive' and not event.values.drive_id then - self:emit({ type = 'error_message', message = 'Invalid drive ID' }) - else - ccemux.detach(event.values.side) + if event.type == 'form_complete' then + if event.values.type == 'disk_drive' and not event.values.drive_id then + self:emit({ type = 'error_message', message = 'Invalid drive ID' }) + else + ccemux.detach(event.values.side) - local config = Config.load('ccemux') - config[event.values.side] = { - type = event.values.type - } - if event.values.type == 'disk_drive' then - config[event.values.side].args = { - id = event.values.drive_id - } - ccemux.attach(event.values.side, event.values.type, { id = event.values.drive_id }) - else - ccemux.attach(event.values.side, event.values.type) - end - Config.update('ccemux', config) - self:updatePeripherals(config) - self.grid:draw() + local config = Config.load('ccemux') + config[event.values.side] = { + type = event.values.type + } + if event.values.type == 'disk_drive' then + config[event.values.side].args = { + id = event.values.drive_id + } + ccemux.attach(event.values.side, event.values.type, { id = event.values.drive_id }) + else + ccemux.attach(event.values.side, event.values.type) + end + Config.update('ccemux', config) + self:updatePeripherals(config) + self.grid:draw() - self:emit({ type = 'success_message', message = 'Attached' }) - end + self:emit({ type = 'success_message', message = 'Attached' }) + end - elseif event.type == 'choice_change' then - if event.element == self.form.ptype then - self.form.drive_id.enabled = event.value == 'disk_drive' - self.form:draw() - end + elseif event.type == 'choice_change' then + if event.element == self.form.ptype then + self.form.drive_id.enabled = event.value == 'disk_drive' + self.form:draw() + end - elseif event.type == 'grid_select' then - local config = Config.load('ccemux') - config[event.selected.side] = nil - Config.update('ccemux', config) - self:updatePeripherals(config) - self.grid:draw() + elseif event.type == 'grid_select' then + local config = Config.load('ccemux') + config[event.selected.side] = nil + Config.update('ccemux', config) + self:updatePeripherals(config) + self.grid:draw() - self:emit({ type = 'success_message', message = 'Detached' }) + self:emit({ type = 'success_message', message = 'Detached' }) - return true - end + return true + end end return tab diff --git a/neural/apis/glasses.lua b/neural/apis/glasses.lua new file mode 100644 index 0000000..f6b62a1 --- /dev/null +++ b/neural/apis/glasses.lua @@ -0,0 +1,121 @@ +--[[ + Create a terminal compatible window for glasses canvas. +]] + +local Terminal = require('opus.terminal') + +local colors = _G.colors +local device = _G.device + +local scale = .5 +local xs, ys = 6 * scale, 9 * scale + +local Glasses = { } + +function Glasses.create(name, sx, sy, w, h) + w, h = w or 46, h or 19 + sx, sy = sx or 1, sy or 20 + + local glasses = device['plethora:glasses'] + local canvas = glasses.canvas() + local _, cy = 1, 1 + local lines = { } + local map = { + ['0'] = 0xF0F0F0FF, + ['1'] = 0xF2B233FF, + ['2'] = 0xE57FD8FF, + ['3'] = 0x99B2F2FF, + ['4'] = 0xDEDE6CFF, + ['5'] = 0x7FCC19FF, + ['6'] = 0xF2B2CCFF, + ['7'] = 0x4C4C4CFF, + ['8'] = 0x999999FF, + ['9'] = 0x4C99B2FF, + ['a'] = 0xB266E5FF, + ['b'] = 0x3366CCFF, + ['c'] = 0x7F664CFF, + ['d'] = 0x57A64EFF, + ['e'] = 0xCC4C4CFF, + ['f'] = 0x191919FF, + } + + -- Position bottom left + local pos = { x = sx * xs, y = sy * ys } + + local function init(group) + for y = 1, h do + lines[y] = { + text = { }, + bg = { } + } + for x = 1, w do + lines[y].bg[x] = group.addRectangle(x * xs, y * ys, xs, ys, 0xF0F0F04F) + lines[y].text[x] = group.addText({ x * xs, y * ys }, '', 0x7FCC19FF) + lines[y].text[x].setScale(scale) + end + end + end + + local group = canvas.addGroup(pos) + init(group) + + local gterm = Terminal.window({ + getSize = function() + return w, h + end, + isColor = function() + return true + end, + clear = function() + for y = 1, h do + for x = 1, w do + local ln = lines[y] + ln.bg[x].setColor(0xF0F0F04F) + ln.text[x].setText('') + end + end + end, + blit = function(text, fg, bg) + for x = 1, #text do + local ln = lines[cy] + ln.bg[x].setColor(map[bg:sub(x, x)]) + ln.text[x].setColor(map[fg:sub(x, x)]) + ln.text[x].setText(text:sub(x, x)) + end + end, + setCursorPos = function(_, y) + cy = y -- full lines are always blit + end, + getTextColor = function() + return colors.white + end, + setTextColor = function() end, + getBackgroundColor = function() + return colors.black + end, + setBackgroundColor = function() end, + setCursorBlink = function() end, + }, 1, 1, w, h, true) + + function gterm.setTextScale() end + function gterm.getPosition() return sx, sy end + function gterm.setVisible() end + function gterm.raise() + local g = canvas.addGroup(pos) + init(g) + gterm.redraw() + group.remove() + group = g + end + function gterm.destroy() + group.remove() + end + + gterm.name = name + gterm.side = name + gterm.type = 'glasses' + + return gterm +end + +return Glasses diff --git a/neural/nwm.lua b/neural/nwm.lua new file mode 100644 index 0000000..b956e62 --- /dev/null +++ b/neural/nwm.lua @@ -0,0 +1,160 @@ +--[[ + A simplistic window manager for glasses. + TODO: support moving windows via mouse drag. +]] + +local Config = require('opus.config') +local Glasses = require('neural.glasses') +local UI = require('opus.ui') +local Util = require('opus.util') + +local kernel = _G.kernel +local multishell = _ENV.multishell +local shell = _ENV.shell + +local sandbox = Util.shallowCopy(_ENV) + +-- TODO: figure out how to better define scaling +local scale = .5 +local xs, ys = 6 * scale, 9 * scale + +local events = { + glasses_click = 'mouse_click', + glasses_up = 'mouse_up', + glasses_drag = 'mouse_drag', + glasses_scroll = 'mouse_scroll', +} + +local hookEvents = { 'glasses_click', 'glasses_up', 'glasses_drag', 'glasses_scroll' } + +local function hook(e, eventData) + local currentTab = kernel.getFocused() + local x = math.floor(eventData[2] / xs) + local y = math.floor(eventData[3] / ys) + local clickedTab + + for _,tab in ipairs(kernel.routines) do + if tab.window.type == 'glasses' then + local wx, wy = tab.window.getPosition() + local ww, wh = tab.window.getSize() + + if x >= wx and x <= wx + ww and y >= wy and y <= wy + wh then + clickedTab = tab + x = x - wx + y = y - wy + break + end + end + end + + if clickedTab then + if clickedTab ~= currentTab then + clickedTab.window.raise() + multishell.setFocus(clickedTab.uid) + end + + kernel.event(events[e], { + eventData[1], x, y, clickedTab.window.side, + }) + + end + return true +end + +local config = Config.load('nwm', { session = { } }) + +local function run(args) + local window = Glasses.create('glasses', args.x, args.y, args.w, args.h) + + local env = Util.shallowCopy(sandbox) + _G.requireInjector(env) + + multishell.openTab({ + path = args.path, + args = args.args, + env = env, + focused = false, + hidden = true, + onDestroy = function() + Util.removeByValue(config.session, args) + Config.update('nwm', config) + window.destroy() + end, + window = window, + }) +end + +kernel.hook(hookEvents, hook) + +UI:setPage(UI.Page { + form = UI.Form { + values = { + x = 1, y = 25, w = 51, h = 19, + }, + path = UI.TextEntry { + y = 5, + formKey = 'path', formLabel = 'Run', required = true, + }, + args = UI.TextEntry { + y = 7, + formKey = 'args', formLabel = 'Args', + }, + UI.Text { + x = 7, y = 5, + textColor = 'yellow', + value = ' x y' + }, + wx = UI.TextEntry { + x = 7, y = 6, width = 7, limit = 3, + transform = 'number', + formKey = 'x', required = true, + }, + wy = UI.TextEntry { + x = 15, y = 6, width = 7, limit = 4, + transform = 'number', + formKey = 'y', required = true, + }, + UI.Text { + x = 7, y = 8, + textColor = 'yellow', + value = ' width height' + }, + ww = UI.TextEntry { + x = 7, y = 9, width = 7, limit = 4, + transform = 'number', + formKey = 'w', required = true, + }, + wh = UI.TextEntry { + x = 15, y = 9, width = 7, limit = 4, + transform = 'number', + formKey = 'h', required = true, + }, + }, + notification = UI.Notification { }, + eventHandler = function(self, event) + if event.type == 'form_complete' then + local args = Util.shallowCopy(event.values) + args.path = shell.resolveProgram(args.path) + if not args.path then + self.notification:error('Invalid program') + else + if args.args then + args.args = Util.split(args.args, '(.-) ') + end + table.insert(config.session, args) + Config.update('nwm', config) + run(args) + self.notification:success('Started program') + end + end + return UI.Page.eventHandler(self, event) + end, +}) + +for _,v in pairs(config.session) do + run(v) +end + +UI:start() + +kernel.unhook(hookEvents, hook) diff --git a/neural/overlay.lua b/neural/overlay.lua deleted file mode 100644 index 83984c5..0000000 --- a/neural/overlay.lua +++ /dev/null @@ -1,116 +0,0 @@ -local Terminal = require('opus.terminal') - -local colors = _G.colors -local device = _G.device -local kernel = _G.kernel - ---[[ - Create a device for glasses - Usable as a redirect or UI target - - Example usage: - Files --display=glasses - debugMonitor glasses - - In a program: - local prev = term.redirect(device.glasses) - shell.run('shell') - term.redirect(prev) - - Glasses do not use the CC font - so extended chars - do not display correctly. -]] - --- configurable -local w, h = 46, 19 -local scale = .5 - -local glasses = device['plethora:glasses'] -local canvas = glasses.canvas() -local _, cy = 1, 1 -local _, gh = canvas:getSize() -local lines = { } -local map = { - ['0'] = 0xF0F0F0FF, - ['1'] = 0xF2B233FF, - ['2'] = 0xE57FD8FF, - ['3'] = 0x99B2F2FF, - ['4'] = 0xDEDE6CFF, - ['5'] = 0x7FCC19FF, - ['6'] = 0xF2B2CCFF, - ['7'] = 0x4C4C4CFF, - ['8'] = 0x999999FF, - ['9'] = 0x4C99B2FF, - ['a'] = 0xB266E5FF, - ['b'] = 0x3366CCFF, - ['c'] = 0x7F664CFF, - ['d'] = 0x57A64EFF, - ['e'] = 0xCC4C4CFF, - ['f'] = 0x191919FF, -- transparent -} - -local xs, ys = 6 * scale, 9 * scale - --- Position bottom left -local pos = { x = 1, y = gh - (h * ys) - 10 } -local group = canvas.addGroup(pos) - -for y = 1, h do - lines[y] = { - text = { }, - bg = { } - } - for x = 1, w do - lines[y].bg[x] = group.addRectangle(x * xs, y * ys, xs, ys, 0xF0F0F04F) - lines[y].text[x] = group.addText({ x * xs, y * ys }, '', 0x7FCC19FF) - lines[y].text[x].setScale(scale) - end -end - -device.glasses = Terminal.window({ - getSize = function() - return w, h - end, - isColor = function() - return true - end, - clear = function() - --canvas.clear() - end, - blit = function(text, fg, bg) - for x = 1, #text do - local ln = lines[cy] - ln.bg[x].setColor(map[bg:sub(x, x)]) - ln.text[x].setColor(map[fg:sub(x, x)]) - ln.text[x].setText(text:sub(x, x)) - end - end, - setCursorPos = function(_, y) - -- full lines are always blit - cy = y - end, - setBackgroundColor = function() - end, - setTextColor = function() - end, - setCursorBlink = function() - end, - getBackgroundColor = function() - return colors.black - end, - getTextColor = function() - return colors.white - end, -}, 1, 1, w, h, true) - -function device.glasses.setTextScale() end - -device.glasses.side = 'glasses' -device.glasses.type = 'glasses' -device.glasses.name = 'glasses' - -kernel.hook('glasses_click', function(_, eventData) - os.queueEvent('monitor_touch', 'glasses', - math.floor((eventData[2] - pos.x) / xs), - math.floor((eventData[3] - pos.y) / ys)) -end) -- 2.49.1 From 87a3f9fa965ae4d037719092ded5405428616ae8 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Mon, 4 May 2020 21:21:43 -0400 Subject: [PATCH 32/90] Cleanup --- milo/apps/storageGen.lua | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/milo/apps/storageGen.lua b/milo/apps/storageGen.lua index 4dd4e03..c4ea024 100644 --- a/milo/apps/storageGen.lua +++ b/milo/apps/storageGen.lua @@ -1,7 +1,3 @@ ---[[ - For initially setting up large amounts of storage chests. -]] - local UI = require('opus.ui') local Util = require('opus.util') local Peripheral = require('opus.peripheral') @@ -72,10 +68,8 @@ local page = UI.Page { } function page:scan() - _syslog("Scanned") - self.storages = Util.filter(Peripheral.getList(), function(dev) - return not not dev.pushItems + return dev.pushItems end) local types = {} @@ -90,11 +84,9 @@ function page:scan() end function page:saveConfig(path) - _syslog("Saving to "..path) - local config = Util.readTable(path) or {} Util.each(self.storages, function(dev, name) - if self.typeGrid.values[dev.type] and self.typeGrid.values[dev.type].checked then + if self.typeGrid.values[dev.type] and self.typeGrid.values[dev.type].checked and not config[name] then config[name] = { name = name, category = 'storage', @@ -103,12 +95,13 @@ function page:saveConfig(path) end end) Util.writeTable(path, config) + self.notification:success("Config saved to "..path) end function page:enable() - self:scan() UI.Page.enable(self) + self:scan() end function page.typeGrid:getRowTextColor(row, selected) @@ -116,7 +109,6 @@ function page.typeGrid:getRowTextColor(row, selected) end function page:eventHandler(event) - _syslog(event) if event.type == "rescan" then self:scan() -- 2.49.1 From 8db9a89f680cc7304ed478d95fa6d40a4d34ffe9 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Tue, 5 May 2020 17:26:44 -0600 Subject: [PATCH 33/90] nwm opacity / improvements - env cleanup --- common/Appstore.lua | 9 ++-- neural/apis/glasses.lua | 92 +++++++++++++++++++---------------------- neural/nwm.lua | 69 ++++++++++++++----------------- secure/autorun/lock.lua | 4 -- 4 files changed, 75 insertions(+), 99 deletions(-) diff --git a/common/Appstore.lua b/common/Appstore.lua index 7576f5f..7e0ad85 100644 --- a/common/Appstore.lua +++ b/common/Appstore.lua @@ -27,9 +27,6 @@ local function unregisterApp(key) end end -local sandboxEnv = Util.shallowCopy(_ENV) -setmetatable(sandboxEnv, { __index = _G }) - multishell.setTitle(multishell.getCurrent(), 'App Store') UI:configure('Appstore', ...) @@ -60,7 +57,7 @@ local function downloadApp(app) end local function runApp(app, checkExists, ...) - + local env = shell.makeEnv() local path, fn local args = { ... } @@ -81,14 +78,14 @@ local function runApp(app, checkExists, ...) error('Failed to download') end - _G.setfenv(fn, sandboxEnv) + _G.setfenv(fn, env) fn(table.unpack(args)) end end multishell.openTab({ title = app.name, - env = sandboxEnv, + env = env, path = path, fn = fn, focused = true, diff --git a/neural/apis/glasses.lua b/neural/apis/glasses.lua index f6b62a1..1d8a1d4 100644 --- a/neural/apis/glasses.lua +++ b/neural/apis/glasses.lua @@ -3,55 +3,64 @@ ]] local Terminal = require('opus.terminal') +local Util = require('opus.util') local colors = _G.colors local device = _G.device -local scale = .5 -local xs, ys = 6 * scale, 9 * scale - local Glasses = { } -function Glasses.create(name, sx, sy, w, h) - w, h = w or 46, h or 19 - sx, sy = sx or 1, sy or 20 +function Glasses.create(args) + local opts = { + x = 1, y = 20, + width = 51, height = 19, + scale = .5, + name = 'glasses', + opacity = 0xff, + } + Util.merge(opts, args) + local xs, ys = 6 * opts.scale, 9 * opts.scale local glasses = device['plethora:glasses'] local canvas = glasses.canvas() local _, cy = 1, 1 local lines = { } local map = { - ['0'] = 0xF0F0F0FF, - ['1'] = 0xF2B233FF, - ['2'] = 0xE57FD8FF, - ['3'] = 0x99B2F2FF, - ['4'] = 0xDEDE6CFF, - ['5'] = 0x7FCC19FF, - ['6'] = 0xF2B2CCFF, - ['7'] = 0x4C4C4CFF, - ['8'] = 0x999999FF, - ['9'] = 0x4C99B2FF, - ['a'] = 0xB266E5FF, - ['b'] = 0x3366CCFF, - ['c'] = 0x7F664CFF, - ['d'] = 0x57A64EFF, - ['e'] = 0xCC4C4CFF, - ['f'] = 0x191919FF, + ['0'] = 0xF0F0F000, + ['1'] = 0xF2B23300, + ['2'] = 0xE57FD800, + ['3'] = 0x99B2F200, + ['4'] = 0xDEDE6C00, + ['5'] = 0x7FCC1900, + ['6'] = 0xF2B2CC00, + ['7'] = 0x4C4C4C00, + ['8'] = 0x99999900, + ['9'] = 0x4C99B200, + ['a'] = 0xB266E500, + ['b'] = 0x3366CC00, + ['c'] = 0x7F664C00, + ['d'] = 0x57A64E00, + ['e'] = 0xCC4C4C00, + ['f'] = 0x19191900, } + for k,v in pairs(map) do + map[k] = v + opts.opacity + end + -- Position bottom left - local pos = { x = sx * xs, y = sy * ys } + local pos = { x = opts.x * xs, y = opts.y * ys } local function init(group) - for y = 1, h do + for y = 1, opts.height do lines[y] = { text = { }, bg = { } } - for x = 1, w do + for x = 1, opts.width do lines[y].bg[x] = group.addRectangle(x * xs, y * ys, xs, ys, 0xF0F0F04F) lines[y].text[x] = group.addText({ x * xs, y * ys }, '', 0x7FCC19FF) - lines[y].text[x].setScale(scale) + lines[y].text[x].setScale(opts.scale) end end end @@ -60,21 +69,9 @@ function Glasses.create(name, sx, sy, w, h) init(group) local gterm = Terminal.window({ - getSize = function() - return w, h - end, isColor = function() return true end, - clear = function() - for y = 1, h do - for x = 1, w do - local ln = lines[y] - ln.bg[x].setColor(0xF0F0F04F) - ln.text[x].setText('') - end - end - end, blit = function(text, fg, bg) for x = 1, #text do local ln = lines[cy] @@ -86,19 +83,11 @@ function Glasses.create(name, sx, sy, w, h) setCursorPos = function(_, y) cy = y -- full lines are always blit end, - getTextColor = function() - return colors.white - end, - setTextColor = function() end, - getBackgroundColor = function() - return colors.black - end, - setBackgroundColor = function() end, setCursorBlink = function() end, - }, 1, 1, w, h, true) + }, 1, 1, opts.width, opts.height, true) function gterm.setTextScale() end - function gterm.getPosition() return sx, sy end + function gterm.getPosition() return opts.x, opts.y end function gterm.setVisible() end function gterm.raise() local g = canvas.addGroup(pos) @@ -110,9 +99,12 @@ function Glasses.create(name, sx, sy, w, h) function gterm.destroy() group.remove() end + function gterm.getTextScale() + return opts.scale + end - gterm.name = name - gterm.side = name + gterm.name = opts.name + gterm.side = opts.name gterm.type = 'glasses' return gterm diff --git a/neural/nwm.lua b/neural/nwm.lua index b956e62..7e54db0 100644 --- a/neural/nwm.lua +++ b/neural/nwm.lua @@ -12,7 +12,7 @@ local kernel = _G.kernel local multishell = _ENV.multishell local shell = _ENV.shell -local sandbox = Util.shallowCopy(_ENV) +local config = Config.load('nwm', { session = { } }) -- TODO: figure out how to better define scaling local scale = .5 @@ -25,8 +25,6 @@ local events = { glasses_scroll = 'mouse_scroll', } -local hookEvents = { 'glasses_click', 'glasses_up', 'glasses_drag', 'glasses_scroll' } - local function hook(e, eventData) local currentTab = kernel.getFocused() local x = math.floor(eventData[2] / xs) @@ -61,19 +59,15 @@ local function hook(e, eventData) return true end -local config = Config.load('nwm', { session = { } }) +local hookEvents = Util.keys(events) +kernel.hook(hookEvents, hook) local function run(args) - local window = Glasses.create('glasses', args.x, args.y, args.w, args.h) - - local env = Util.shallowCopy(sandbox) - _G.requireInjector(env) + local window = Glasses.create(args) multishell.openTab({ path = args.path, args = args.args, - env = env, - focused = false, hidden = true, onDestroy = function() Util.removeByValue(config.session, args) @@ -84,66 +78,63 @@ local function run(args) }) end -kernel.hook(hookEvents, hook) - UI:setPage(UI.Page { form = UI.Form { values = { - x = 1, y = 25, w = 51, h = 19, + x = 1, y = 25, width = 51, height = 19, + opacity = 255, }, - path = UI.TextEntry { - y = 5, - formKey = 'path', formLabel = 'Run', required = true, + UI.TextEntry { + formKey = 'run', formLabel = 'Run', required = true, }, - args = UI.TextEntry { - y = 7, - formKey = 'args', formLabel = 'Args', + UI.Slider { + min = 0, max = 255, + formLabel = 'Opacity', formKey = 'opacity', formIndex = 3, }, UI.Text { - x = 7, y = 5, + x = 10, y = 5, textColor = 'yellow', value = ' x y' }, - wx = UI.TextEntry { - x = 7, y = 6, width = 7, limit = 3, + UI.TextEntry { + x = 10, y = 6, width = 7, limit = 3, transform = 'number', formKey = 'x', required = true, }, - wy = UI.TextEntry { - x = 15, y = 6, width = 7, limit = 4, + UI.TextEntry { + x = 18, y = 6, width = 7, limit = 4, transform = 'number', formKey = 'y', required = true, }, UI.Text { - x = 7, y = 8, + x = 10, y = 8, textColor = 'yellow', value = ' width height' }, - ww = UI.TextEntry { - x = 7, y = 9, width = 7, limit = 4, + UI.TextEntry { + x = 10, y = 9, width = 7, limit = 4, transform = 'number', - formKey = 'w', required = true, + formKey = 'width', required = true, }, - wh = UI.TextEntry { - x = 15, y = 9, width = 7, limit = 4, + UI.TextEntry { + x = 18, y = 9, width = 7, limit = 4, transform = 'number', - formKey = 'h', required = true, + formKey = 'height', required = true, }, }, notification = UI.Notification { }, eventHandler = function(self, event) if event.type == 'form_complete' then - local args = Util.shallowCopy(event.values) - args.path = shell.resolveProgram(args.path) - if not args.path then + local opts = Util.shallowCopy(event.values) + local words = Util.split(opts.run, '(.-) ') + opts.path = shell.resolveProgram(table.remove(words, 1)) + if not opts.path then self.notification:error('Invalid program') else - if args.args then - args.args = Util.split(args.args, '(.-) ') - end - table.insert(config.session, args) + opts.args = #words > 0 and words + table.insert(config.session, opts) Config.update('nwm', config) - run(args) + run(opts) self.notification:success('Started program') end end diff --git a/secure/autorun/lock.lua b/secure/autorun/lock.lua index a8965d7..eadff4c 100644 --- a/secure/autorun/lock.lua +++ b/secure/autorun/lock.lua @@ -17,9 +17,6 @@ local config = Config.load('secure', { local timer = config.enabled and os.startTimer(config.timeout) -local sandboxEnv = Util.shallowCopy(_ENV) -setmetatable(sandboxEnv, { __index = _G }) - local function buildLockScreen() _G.requireInjector(_ENV) @@ -81,7 +78,6 @@ local function showLockScreen() pinned = true, focused = true, title = 'Lock', - env = sandboxEnv, }) end -- 2.49.1 From 59de8e7f63d544351e13cd62c7f8d89967976754 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Wed, 6 May 2020 02:54:15 -0400 Subject: [PATCH 34/90] Update k.lua and others --- swshop/apis/jua.lua | 2 +- swshop/apis/k.lua | 16 +++++++++++++--- swshop/apis/r.lua | 19 +++++++++++-------- swshop/apis/w.lua | 28 ++++++++++++++-------------- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/swshop/apis/jua.lua b/swshop/apis/jua.lua index 273b9ec..4ee5b73 100644 --- a/swshop/apis/jua.lua +++ b/swshop/apis/jua.lua @@ -119,4 +119,4 @@ return { go = go, stop = stop, await = await -} \ No newline at end of file +} diff --git a/swshop/apis/k.lua b/swshop/apis/k.lua index 47ad499..4adf49e 100644 --- a/swshop/apis/k.lua +++ b/swshop/apis/k.lua @@ -101,6 +101,15 @@ function addresses(cb, limit, offset) end, "/addresses?limit="..(limit or 50).."&offset="..(offset or 0)) end +function name(cb, name) + asserttype(cb, "callback", "function") + asserttype(name, "name", "string") + + api_request(function(success, data) + cb(success and data and data.ok, data.name or data) + end, "/names/"..name) +end + function rich(cb, limit, offset) asserttype(cb, "callback", "function") asserttype(limit, "limit", "number", true) @@ -165,7 +174,8 @@ local wsEventNameLookup = { names = "name", ownNames = "name", ownWebhooks = "webhook", - motd = "motd" + motd = "motd", + keepalive = "keepalive", } local wsEvents = {} @@ -362,6 +372,7 @@ return { addressTransactions = addressTransactions, addressNames = addressNames, addresses = addresses, + name = name, rich = rich, transactions = transactions, latestTransactions = latestTransactions, @@ -369,5 +380,4 @@ return { makeTransaction = makeTransaction, connect = connect, parseMeta = parseMeta, - sha256 = sha256, -} \ No newline at end of file +} diff --git a/swshop/apis/r.lua b/swshop/apis/r.lua index 3982634..f967776 100644 --- a/swshop/apis/r.lua +++ b/swshop/apis/r.lua @@ -18,13 +18,16 @@ end local function findID(url) local found = gfind(url, idPatt) - if found then - return tonumber(found[#found]:sub(found[#found]:find("%d+"))) - end + if not found then return nil end + return tonumber(found[#found]:sub(found[#found]:find("%d+"))) end local function newID() - return #callbackRegistry + 1 + for i = 1, math.huge do + if not callbackRegistry[i] then + return i + end + end end local function trimID(url) @@ -36,8 +39,8 @@ end function request(callback, url, headers, postData) local id = newID() local newUrl = url .. "#R" .. id - http.request(newUrl, postData, headers) callbackRegistry[id] = callback + http.request(newUrl, postData, headers) end function init(jua) @@ -46,7 +49,7 @@ function init(jua) local id = findID(url) if id and callbackRegistry[id] then callbackRegistry[id](true, trimID(url), handle) - table.remove(callbackRegistry, id) + callbackRegistry[id] = nil end end) @@ -54,7 +57,7 @@ function init(jua) local id = findID(url) if id and callbackRegistry[id] then callbackRegistry[id](false, trimID(url), handle) - table.remove(callbackRegistry, id) + callbackRegistry[id] = nil end end) end @@ -62,4 +65,4 @@ end return { request = request, init = init -} \ No newline at end of file +} diff --git a/swshop/apis/w.lua b/swshop/apis/w.lua index 85fadf7..7fe1e3e 100644 --- a/swshop/apis/w.lua +++ b/swshop/apis/w.lua @@ -31,8 +31,8 @@ end local function findID(url) local found = gfind(url, idPatt) - local id = tonumber(found[#found]:sub(found[#found]:find("%d+"))) - return id + if not found then return nil end + return tonumber(found[#found]:sub(found[#found]:find("%d+"))) end local function newID() @@ -71,57 +71,57 @@ function init(jua) jua = jua if async then jua.on("websocket_success", function(event, url, handle) - local success, id = pcall(findID,url) - if success and id and callbackRegistry[id] and callbackRegistry[id].success then + local id = findID(url) + if id and callbackRegistry[id].success then callbackRegistry[id].success(id, handle) end end) jua.on("websocket_failure", function(event, url) - local success, id = pcall(findID,url) - if success and id and callbackRegistry[id] and callbackRegistry[id].failure then + local id = findID(url) + if id and callbackRegistry[id].failure then callbackRegistry[id].failure(id) end table.remove(callbackRegistry, id) end) jua.on("websocket_message", function(event, url, data) - local success, id = pcall(findID,url) - if success and id and callbackRegistry[id] and callbackRegistry[id].message then + local id = findID(url) + if id and callbackRegistry[id].message then callbackRegistry[id].message(id, data) end end) jua.on("websocket_closed", function(event, url) - local success, id = pcall(findID,url) - if success and id and callbackRegistry[id] and callbackRegistry[id].closed then + local id = findID(url) + if id and callbackRegistry[id].closed then callbackRegistry[id].closed(id) end table.remove(callbackRegistry, id) end) else jua.on("socket_connect", function(event, id) - if id and callbackRegistry[id] and callbackRegistry[id].success then + if id and callbackRegistry[id].success then callbackRegistry[id].success(id, wsRegistry[id]) end end) jua.on("socket_error", function(event, id, msg) - if id and callbackRegistry[id] and callbackRegistry[id].failure then + if id and callbackRegistry[id].failure then callbackRegistry[id].failure(id, msg) end table.remove(callbackRegistry, id) end) jua.on("socket_message", function(event, id) - if id and callbackRegistry[id] and callbackRegistry[id].message then + if id and callbackRegistry[id].message then local data = wsRegistry[id].read() callbackRegistry[id].message(id, data) end end) jua.on("socket_closed", function(event, id) - if id and callbackRegistry[id] and callbackRegistry[id].closed then + if id and callbackRegistry[id].closed then callbackRegistry[id].closed(id) end table.remove(callbackRegistry, id) -- 2.49.1 From bc3a48f30f0526e2b6b937bf52e2b749de17dec9 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Wed, 6 May 2020 03:03:49 -0400 Subject: [PATCH 35/90] Swshop update - Toggle lamp based on keepalive packets from the node - Add functionality to split profits of a bought item (Will document later) --- swshop/swshop.lua | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/swshop/swshop.lua b/swshop/swshop.lua index a90c20a..eaf1dc9 100644 --- a/swshop/swshop.lua +++ b/swshop/swshop.lua @@ -45,7 +45,7 @@ local function getItemDetails(item) t = textutils.unserialize(t) for key, v in pairs(t) do if v.name == item then - return key, tonumber(v.price) + return key, v end end end @@ -89,7 +89,9 @@ local function handleTransaction(transaction) logTransaction(t, { refund = amount, reason = reason }) end - t.itemId, t.price = getItemDetails(metadata.name) + local id, item = getItemDetails(metadata.name) + t.itemId, t.price = id, tonumber(item.price) + if not t.itemId or not t.price then print('invalid item') logTransaction(t, { reason = 'invalid item' }) @@ -104,6 +106,15 @@ local function handleTransaction(transaction) return refundTransaction(value, "error=Please pay the price listed on-screen.") end + if item.shares then + local amount = t.value - (t.value % t.price) + for _, share in pairs(item.shares) do + local cut = math.floor(amount * share.rate) -- Don't include fractional Krist + print("Sent shares of "..cut.." to "..share.address) + await(k.makeTransaction, privatekey, share.address, cut, share.meta) + end + end + local count = math.floor(value / t.price) local uid = math.random() print(string.format('requesting %d of %s', count, t.itemId)) @@ -151,6 +162,12 @@ local function connect() local transaction = data.transaction handleTransaction(transaction) end) + + await(ws.subscribe, "keepalive", function(data) + local state = rs.getOutput(rsSide) + rs.setOutput(rsSide, not state) + end) + assert(success, "Failed to subscribe to event") end -- 2.49.1 From 428477bdec1629b6345867ec826d15ad9901faec Mon Sep 17 00:00:00 2001 From: Anavrins Date: Wed, 6 May 2020 19:02:44 -0400 Subject: [PATCH 36/90] Whoops --- swshop/shopConfig.lua | 1 - swshop/shopView.lua | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/swshop/shopConfig.lua b/swshop/shopConfig.lua index c5958ea..8c1ffba 100644 --- a/swshop/shopConfig.lua +++ b/swshop/shopConfig.lua @@ -37,7 +37,6 @@ local wizardPage = UI.WizardPage { [5] = UI.Chooser { formLabel = 'RS Signal', formKey = 'rsSide', formIndex = 6, width = 10, - nochoice = 'Top', choices = { {name = 'Bottom', value = 'bottom'}, {name = 'Top', value = 'top'}, diff --git a/swshop/shopView.lua b/swshop/shopView.lua index 4a51b17..1afafd3 100644 --- a/swshop/shopView.lua +++ b/swshop/shopView.lua @@ -155,7 +155,7 @@ local function createPage(node) self.grid.values = { } for k,v in pairs(config) do local item = list[k] or itemDB:get(k) - if item and (node.showOutOfStock or item.count > 0) then + if item and (node.showOutOfStock or item.count or 0 > 0) then table.insert(self.grid.values, { displayName = item.displayName, count = item.count or 0, -- 2.49.1 From 9eff14f3ba9f945d54f86d54b523781ec3b4db92 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Wed, 6 May 2020 18:02:01 -0600 Subject: [PATCH 37/90] titlebar/close/moving of nwm windows --- common/debugMonitor.lua | 12 ++++++--- common/edit.lua | 3 --- lzwfs/system/lzwfs.lua | 1 - neural/apis/glasses.lua | 8 +++++- neural/fisher.lua | 2 +- neural/nwm.lua | 54 ++++++++++++++++++++++++++++++++++++++--- 6 files changed, 68 insertions(+), 12 deletions(-) diff --git a/common/debugMonitor.lua b/common/debugMonitor.lua index 74e8c84..d198f77 100644 --- a/common/debugMonitor.lua +++ b/common/debugMonitor.lua @@ -6,12 +6,16 @@ local peripheral = _G.peripheral local term = _G.term local args = { ... } -local mon = args[1] and device[args[1]] or peripheral.wrap(args[1]) or +local mon = not args[1] and term.current() or + device[args[1]] or + peripheral.wrap(args[1]) or peripheral.find('monitor') or error('Syntax: debug ') mon.clear() -mon.setTextScale(.5) +if mon.setTextScale then + mon.setTextScale(.5) +end mon.setCursorPos(1, 1) local oldDebug = _G._syslog @@ -26,7 +30,9 @@ repeat local e, side = os.pullEventRaw('monitor_touch') if e == 'monitor_touch' and side == mon.side then mon.clear() - mon.setTextScale(.5) + if mon.setTextScale then + mon.setTextScale(.5) + end mon.setCursorPos(1, 1) end until e == 'terminate' diff --git a/common/edit.lua b/common/edit.lua index f0c7be3..27784e1 100644 --- a/common/edit.lua +++ b/common/edit.lua @@ -202,7 +202,6 @@ local page = UI.Page { label = 'Find', search = UI.TextEntry { x = 7, ex = -3, - limit = 512, accelerators = { [ 'enter' ] = 'accept', }, @@ -232,7 +231,6 @@ local page = UI.Page { label = 'Save', filename = UI.TextEntry { x = 7, ex = -3, - limit = 512, accelerators = { [ 'enter' ] = 'accept', }, @@ -339,7 +337,6 @@ local page = UI.Page { quick_open = UI.SlideOut { filter_entry = UI.TextEntry { x = 2, y = 2, ex = -2, - limit = 256, shadowText = 'File name', accelerators = { [ 'enter' ] = 'accept', diff --git a/lzwfs/system/lzwfs.lua b/lzwfs/system/lzwfs.lua index 69bccee..5f898bd 100644 --- a/lzwfs/system/lzwfs.lua +++ b/lzwfs/system/lzwfs.lua @@ -29,7 +29,6 @@ local tab = UI.Tab { }, entry = UI.TextEntry { x = 3, y = 5 , ex = -3, - limit = 256, shadowText = 'enter new path', accelerators = { enter = 'add_path', diff --git a/neural/apis/glasses.lua b/neural/apis/glasses.lua index 1d8a1d4..1224883 100644 --- a/neural/apis/glasses.lua +++ b/neural/apis/glasses.lua @@ -5,7 +5,6 @@ local Terminal = require('opus.terminal') local Util = require('opus.util') -local colors = _G.colors local device = _G.device local Glasses = { } @@ -102,6 +101,13 @@ function Glasses.create(args) function gterm.getTextScale() return opts.scale end + function gterm.move(x, y) + if opts.x ~= x or opts.y ~= y then + opts.x, opts.y = x, y + pos = { x = opts.x * xs, y = opts.y * ys } + group.setPosition(pos.x, pos.y) + end + end gterm.name = opts.name gterm.side = opts.name diff --git a/neural/fisher.lua b/neural/fisher.lua index 88597af..d9c4c8a 100644 --- a/neural/fisher.lua +++ b/neural/fisher.lua @@ -47,7 +47,7 @@ local fsm = machine.create({ -- state changes onenterwait = function() - print('waitng for fishing rod to be selected') + print('waiting for fishing rod to be selected') if icon then icon.remove() icon = canvas.addItem({ w - 20, h - 20 }, 'minecraft:fishing_rod' ) diff --git a/neural/nwm.lua b/neural/nwm.lua index 7e54db0..91d2fe5 100644 --- a/neural/nwm.lua +++ b/neural/nwm.lua @@ -1,6 +1,9 @@ --[[ A simplistic window manager for glasses. - TODO: support moving windows via mouse drag. + + TODO: + opacity for text/background separately + support for specifying scale factor ]] local Config = require('opus.config') @@ -8,15 +11,17 @@ local Glasses = require('neural.glasses') local UI = require('opus.ui') local Util = require('opus.util') +local fs = _G.fs local kernel = _G.kernel local multishell = _ENV.multishell local shell = _ENV.shell local config = Config.load('nwm', { session = { } }) --- TODO: figure out how to better define scaling +-- TODO: figure out how to better support scaling local scale = .5 local xs, ys = 6 * scale, 9 * scale +local dragging local events = { glasses_click = 'mouse_click', @@ -31,16 +36,39 @@ local function hook(e, eventData) local y = math.floor(eventData[3] / ys) local clickedTab + if dragging then + if e == 'glasses_up' then + dragging = nil + elseif e == 'glasses_drag' then + local dx = x - dragging.ax + local dy = y - dragging.ay + dragging.tab.window.move(dragging.wx + dx, dragging.wy + dy) + dragging.tab.titleBar.move(dragging.wx + dx, dragging.wy + dy - 1) + + dragging.tab.wmargs.x = dragging.wx + dx + dragging.tab.wmargs.y = dragging.wy + dy + Config.update('nwm', config) + end + return + end + for _,tab in ipairs(kernel.routines) do if tab.window.type == 'glasses' then local wx, wy = tab.window.getPosition() local ww, wh = tab.window.getSize() - if x >= wx and x <= wx + ww and y >= wy and y <= wy + wh then + if x >= wx and x <= wx + ww and y > wy and y < wy + wh then clickedTab = tab x = x - wx y = y - wy break + elseif e == 'glasses_click' and x >= wx and x <= wx + ww and y == wy then + if x == wx + ww - 1 then + multishell.terminate(tab.uid) + else + dragging = { tab = tab, ax = x, ay = y, wx = wx, wy = wy } + end + return end end end @@ -65,6 +93,18 @@ kernel.hook(hookEvents, hook) local function run(args) local window = Glasses.create(args) + local titleBar = Glasses.create({ + x = args.x, + y = args.y - 1, + height = 1, + width = args.width, + opacity = args.opacity, + }) + titleBar.canvas:clear('yellow') + titleBar.canvas:write(1, 1, ' ' .. fs.getName(args.path), nil, 'black') + titleBar.canvas:write(args.width - 2, 1, ' x ', nil, 'black') + titleBar.redraw() + multishell.openTab({ path = args.path, args = args.args, @@ -73,8 +113,11 @@ local function run(args) Util.removeByValue(config.session, args) Config.update('nwm', config) window.destroy() + titleBar.destroy() end, window = window, + titleBar = titleBar, + wmargs = args, }) end @@ -90,6 +133,8 @@ UI:setPage(UI.Page { UI.Slider { min = 0, max = 255, formLabel = 'Opacity', formKey = 'opacity', formIndex = 3, + labelWidth = 3, + transform = math.floor, }, UI.Text { x = 10, y = 5, @@ -137,6 +182,9 @@ UI:setPage(UI.Page { run(opts) self.notification:success('Started program') end + + elseif event.type == 'form_cancel' then + UI:quit() end return UI.Page.eventHandler(self, event) end, -- 2.49.1 From 94b743bfc0a904bb9410b343c1108aed9864cb33 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Wed, 6 May 2020 18:49:28 -0600 Subject: [PATCH 38/90] remove border around windows in mwm --- monitor/mwm.lua | 131 ++++++++++++++++------------------------ neural/apis/glasses.lua | 5 +- 2 files changed, 56 insertions(+), 80 deletions(-) diff --git a/monitor/mwm.lua b/monitor/mwm.lua index eef8753..9eda98f 100644 --- a/monitor/mwm.lua +++ b/monitor/mwm.lua @@ -103,8 +103,8 @@ function Process:new(args) uid = nextUID(), x = args.x or 1, y = args.y or 1, - width = args.width + 2, - height = args.height + 3, + width = args.width, + height = args.height + 1, path = args.path, args = args.args or { }, title = args.title or 'shell', @@ -117,7 +117,7 @@ function Process:new(args) end self.container = Terminal.window(monitor, self.x, self.y, self.width, self.height, true) - self.window = window.create(self.container, 2, 3, args.width, args.height, true) + self.window = window.create(self.container, 1, 2, args.width, args.height, true) self.terminal = self.window self.container.setBackgroundColor(colors.black) @@ -153,62 +153,35 @@ function Process:new(args) return self end -function Process:focus(focused) - if focused then - self.container.setBackgroundColor(colors.yellow) - else - self.container.setBackgroundColor(colors.gray) - end - self.container.setTextColor(colors.black) - write(self.container, 2, 2, string.rep(' ', self.width - 2)) - write(self.container, 3, 2, self.title) - write(self.container, self.width - 2, 2, '*') +function Process:drawTitle(focused) + if self.showSizers and focused then + local sizers = '\25 \26 \24 \27' - if focused then - self.window.restoreCursor() - elseif self.showSizers then - self:drawSizers(false) + self.container.setBackgroundColor(colors.yellow) + self.container.setTextColor(colors.black) + + write(self.container, 1, 1, string.rep(' ', self.width)) + write(self.container, 2, 1, sizers) + + local str = string.format('%d x %d', self.width, self.height - 1) + write(self.container, 10, 1, str) + else + if focused then + self.container.setBackgroundColor(colors.yellow) + else + self.container.setBackgroundColor(colors.lightGray) + end + self.container.setTextColor(colors.black) + write(self.container, 1, 1, string.rep(' ', self.width)) + write(self.container, 2, 1, self.title) end + write(self.container, self.width - 1, 1, '*') end -function Process:drawSizers(showSizers) - local sizeChars = { - '\135', '\139', '\141', '\142' - } - - if Util.getVersion() < 1.8 then - sizeChars = { - '#', '#', '#', '#' - } - end - - self.showSizers = showSizers - - self.container.setBackgroundColor(colors.black) - self.container.setTextColor(colors.white) - - if self.showSizers then - write(self.container, 1, 1, sizeChars[1]) - write(self.container, self.width, 1, sizeChars[2]) - write(self.container, 1, self.height, sizeChars[3]) - write(self.container, self.width, self.height, sizeChars[4]) - - self.container.setTextColor(colors.yellow) - write(self.container, 1, 3, '+') - write(self.container, 1, 5, '-') - write(self.container, 3, 1, '+') - write(self.container, 5, 1, '-') - - local str = string.format('%d x %d', self.width - 2, self.height - 3) - write(self.container, (self.width - #str) / 2, 1, str) - - else - write(self.container, 1, 1, string.rep(' ', self.width)) - write(self.container, self.width, 1, ' ') - write(self.container, 1, self.height, ' ') - write(self.container, self.width, self.height, ' ') - write(self.container, 1, 3, ' ') - write(self.container, 1, 5, ' ') +function Process:focus(focused) + self:drawTitle(focused) + if focused then + self.window.restoreCursor() end end @@ -227,48 +200,48 @@ function Process:reposition() self.container.reposition(self.x, self.y, self.width, self.height) self.container.setBackgroundColor(colors.black) self.container.clear() - self.window.reposition(2, 3, self.width - 2, self.height - 3) + self.window.reposition(1, 2, self.width, self.height - 1) if self.window ~= self.terminal then - self.terminal.reposition(1, 1, self.width - 2, self.height - 3) + self.terminal.reposition(1, 1, self.width, self.height - 1) end redraw() end function Process:click(x, y) - if y == 2 then -- title bar - if x == self.width - 2 then + if y == 1 then -- title bar + if x == self.width - 1 then self:resume('terminate') + elseif not self.showSizers then + self.showSizers = not self.showSizers + self:drawTitle(true) else - self:drawSizers(not self.showSizers) + self:resizeClick(x, y) end - - elseif x == 1 or y == 1 then -- sizers - self:resizeClick(x, y) - elseif x > 1 and x < self.width then if self.showSizers then - self:drawSizers(false) + self.showSizers = false + self:drawTitle(true) end - self:resume('mouse_click', 1, x - 1, y - 2) - self:resume('mouse_up', 1, x - 1, y - 2) + self:resume('mouse_click', 1, x, y - 1) + self:resume('mouse_up', 1, x, y - 1) end end -function Process:resizeClick(x, y) - if x == 1 and y == 3 then +function Process:resizeClick(x) + if x == 2 then self.height = self.height + 1 - elseif x == 1 and y == 5 then + elseif x == 6 then self.height = self.height - 1 - elseif x == 3 and y == 1 then + elseif x == 4 then self.width = self.width + 1 - elseif x == 5 and y == 1 then + elseif x == 8 then self.width = self.width - 1 else return end self:reposition() self:resume('term_resize') - self:drawSizers(true) + self:drawTitle(true) multishell.saveSession(sessionFile) end @@ -393,8 +366,8 @@ function multishell.saveSession(filename) table.insert(t, { x = process.x, y = process.y, - width = process.width - 2, - height = process.height - 3, + width = process.width, + height = process.height - 1, path = process.path, args = process.args, }) @@ -446,7 +419,7 @@ function multishell.start() process.x = math.floor(x - (process.width) / 2) process.y = y process:reposition() - process:drawSizers(true) + process:drawTitle(true) multishell.saveSession(sessionFile) end end @@ -458,7 +431,7 @@ function multishell.start() if not focused.isShell then multishell.setFocus(1) -- shell is always 1 else - focused:resume(unpack(event)) + focused:resume(table.unpack(event)) end elseif event[1] == 'char' or @@ -468,12 +441,12 @@ function multishell.start() local focused = processes[#processes] if focused then - focused:resume(unpack(event)) + focused:resume(table.unpack(event)) end else for _,process in pairs(Util.shallowCopy(processes)) do - process:resume(unpack(event)) + process:resume(table.unpack(event)) end end diff --git a/neural/apis/glasses.lua b/neural/apis/glasses.lua index 1224883..be5fb86 100644 --- a/neural/apis/glasses.lua +++ b/neural/apis/glasses.lua @@ -1,5 +1,8 @@ --[[ Create a terminal compatible window for glasses canvas. + + Note that the extended chars on glasses are not consistent with + the normal cc font. ]] local Terminal = require('opus.terminal') @@ -47,9 +50,9 @@ function Glasses.create(args) map[k] = v + opts.opacity end - -- Position bottom left local pos = { x = opts.x * xs, y = opts.y * ys } + -- create an entry for every char as the glasses font is not fixed width :( local function init(group) for y = 1, opts.height do lines[y] = { -- 2.49.1 From 1fc2d08c18125d94e4dd89384e614c8e151f3c64 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Thu, 7 May 2020 17:15:30 -0600 Subject: [PATCH 39/90] some documentation and some bad icons --- neural/apis/glasses.lua | 50 ++++++++++++++++++++++----------------- neural/autorun/splash.lua | 36 +++++++++------------------- neural/etc/apps.db | 14 ++++++++++- neural/mobFollow.lua | 5 ++++ neural/neuralLook.lua | 5 ++++ neural/neuralRecorder.lua | 4 ++++ neural/nwm.lua | 6 ++--- 7 files changed, 69 insertions(+), 51 deletions(-) diff --git a/neural/apis/glasses.lua b/neural/apis/glasses.lua index be5fb86..1fb398c 100644 --- a/neural/apis/glasses.lua +++ b/neural/apis/glasses.lua @@ -8,26 +8,12 @@ local Terminal = require('opus.terminal') local Util = require('opus.util') -local device = _G.device +local device = _G.device local Glasses = { } -function Glasses.create(args) - local opts = { - x = 1, y = 20, - width = 51, height = 19, - scale = .5, - name = 'glasses', - opacity = 0xff, - } - Util.merge(opts, args) - - local xs, ys = 6 * opts.scale, 9 * opts.scale - local glasses = device['plethora:glasses'] - local canvas = glasses.canvas() - local _, cy = 1, 1 - local lines = { } - local map = { +function Glasses.getPalette(opacity) + local pal = { ['0'] = 0xF0F0F000, ['1'] = 0xF2B23300, ['2'] = 0xE57FD800, @@ -45,10 +31,30 @@ function Glasses.create(args) ['e'] = 0xCC4C4C00, ['f'] = 0x19191900, } - - for k,v in pairs(map) do - map[k] = v + opts.opacity + if opacity then + for k,v in pairs(pal) do + pal[k] = v + opacity + end end + return pal +end + +function Glasses.create(args) + local opts = { + x = 1, y = 20, + width = 51, height = 19, + scale = .5, + name = 'glasses', + opacity = 0xff, + } + Util.merge(opts, args) + + local xs, ys = 6 * opts.scale, 9 * opts.scale + local glasses = device['plethora:glasses'] + local canvas = glasses.canvas() + local _, cy = 1, 1 + local lines = { } + local pal = Glasses.getPalette(opts.opacity) local pos = { x = opts.x * xs, y = opts.y * ys } @@ -77,8 +83,8 @@ function Glasses.create(args) blit = function(text, fg, bg) for x = 1, #text do local ln = lines[cy] - ln.bg[x].setColor(map[bg:sub(x, x)]) - ln.text[x].setColor(map[fg:sub(x, x)]) + ln.bg[x].setColor(pal[bg:sub(x, x)]) + ln.text[x].setColor(pal[fg:sub(x, x)]) ln.text[x].setText(text:sub(x, x)) end end, diff --git a/neural/autorun/splash.lua b/neural/autorun/splash.lua index b74f722..ac92708 100644 --- a/neural/autorun/splash.lua +++ b/neural/autorun/splash.lua @@ -1,7 +1,10 @@ +local device = _G.device +local kernel = _G.kernel + local opus = { 'fffff00', 'ffff07000', - 'ff00770b00 4444', + 'ff00770b00f4444', 'ff077777444444444', 'f07777744444444444', 'f0000777444444444', @@ -12,29 +15,14 @@ local opus = { '077000000000', } -local hex = { - ['0'] = 0xF0F0F04F, - ['1'] = 0xF2B2334F, - ['2'] = 0xE57FD84F, - ['3'] = 0x99B2F24F, - ['4'] = 0xDEDE6C4F, - ['5'] = 0x7FCC194F, - ['6'] = 0xF2B2CC4F, - ['7'] = 0x4C4C4C4F, - ['8'] = 0x9999994F, - ['9'] = 0x4C99B24F, - ['a'] = 0xB266E54F, - ['b'] = 0x3366CC4F, - ['c'] = 0x7F664C4F, - ['d'] = 0x57A64E4F, - ['e'] = 0xCC4C4C4F, --- ['f'] = 0x191919FF, -- transparent -} - local function update() - local canvas = device['plethora:glasses'] and device['plethora:glasses'].canvas() + local canvas = device['plethora:glasses'] and device['plethora:glasses'].canvas() if canvas then - local Tween = require('opus.ui.tween') + local Tween = require('opus.ui.tween') + local Glasses = require('neural.glasses') + + local pal = Glasses.getPalette(0x4f) + pal['f'] = nil -- transparent canvas.clear() local w, h = canvas.getSize() @@ -42,7 +30,7 @@ local function update() local group = canvas.addGroup(pos) local function drawLine(k, line) for i = 1, #line do - local pix = hex[line:sub(i, i)] + local pix = pal[line:sub(i, i)] if pix then group.addRectangle(i*1.5, k*2.25, 1.5, 2.25, pix) end @@ -63,8 +51,6 @@ local function update() end kernel.run({ - title = 'opus', - env = _ENV, hidden = true, fn = update, }) diff --git a/neural/etc/apps.db b/neural/etc/apps.db index 31c01d7..f61f97a 100644 --- a/neural/etc/apps.db +++ b/neural/etc/apps.db @@ -3,12 +3,14 @@ title = "ElytraFly", category = "Neural", run = "elytraFly.lua", + iconExt = "\30\55\31\50\131\30\56\31\55\135\30\55\31\56\148\30\50\31\55\140\30\56\151\139\30\55\31\50\131\10\30\55\31\56\138\30\56\128\30\55\149\30\50\31\50\128\30\56\31\55\149\31\56\128\30\55\133\10\30\50\31\55\138\30\55\31\56\139\30\50\31\55\159\31\50\128\30\55\144\31\56\135\30\50\31\55\133", requires = "neuralInterface", }, [ "0d57bd5497ca26a92593094bf3e21c5097192fa3" ] = { title = "Fisher", category = "Neural", run = "fisher.lua", + iconExt = "\30\50\31\50\128\128\128\30\99\135\30\55\31\99\135\30\50\31\50\128\10\30\50\31\50\128\30\99\159\30\50\31\99\158\129\30\55\31\50\149\30\50\128\10\30\99\31\50\135\30\50\31\99\135\31\50\128\31\48\140\31\55\129\31\50\128", requires = "neuralInterface", }, [ "9101fc1744ab274aaa0b54ee22b14ca53ee6e236" ] = { @@ -27,12 +29,22 @@ title = "Ores", category = "Neural", run = "ores.lua", + iconExt = "\30\50\31\50\128\30\48\135\30\51\31\48\131\145\30\50\31\51\144\31\50\128\10\30\48\31\50\149\31\51\151\30\51\31\48\131\31\57\138\30\57\31\51\147\30\50\31\50\128\10\30\50\31\50\128\31\51\139\30\57\140\134\30\50\31\57\129\31\50\128", requires = "neuralInterface", }, [ "7f2465ac7cefab2766e6ee0714647089df9364b0ff09858c84b21b8a436a845d" ] = { title = "Equipment", category = "Neural", run = "Equipment", + iconExt = "\30\55\31\50\151\30\48\31\55\135\30\51\131\131\30\57\131\30\55\31\57\144\30\50\31\50\128\10\30\55\31\50\149\31\51\151\30\50\31\55\135\131\30\55\31\57\130\149\30\50\31\50\128\10\30\50\31\55\130\131\31\50\128\128\31\55\130\131\31\50\128", requires = "neuralInterface", - } + }, + [ "b492db2750ce6e21640a530c75a39ee7a6ac411e5962b6d96d27572784cd8ab4" ] = { + title = "Nwm", + category = "Neural", + iconExt = "\30\50\31\50\128\31\99\140\144\31\50\128\31\99\136\30\99\31\50\155\30\50\128\128\ +\30\50\31\50\128\128\31\99\137\30\102\31\102\128\30\50\132\30\102\31\50\145\30\50\31\102\157\31\50\128\ +\30\50\31\50\128\128\128\128\128\128\128\128", + run = "nwm.lua", + }, } diff --git a/neural/mobFollow.lua b/neural/mobFollow.lua index 1ef1601..b14818a 100644 --- a/neural/mobFollow.lua +++ b/neural/mobFollow.lua @@ -3,6 +3,11 @@ local Util = require('opus.util') local Point = require('opus.point') local Proxy = require('core.proxy') +--[[ + Have a mob follow you. The mob's neural must have the + neural package installed. +]] + local os = _G.os local args = { ... } diff --git a/neural/neuralLook.lua b/neural/neuralLook.lua index 9a35c78..c2dbbf5 100644 --- a/neural/neuralLook.lua +++ b/neural/neuralLook.lua @@ -2,6 +2,11 @@ local Array = require('opus.array') local neural = require('neural.interface') local Point = require('opus.point') +--[[ + Animate an armor stand or mob. Will just look + at anything that moves. +]] + local os = _G.os neural.assertModules({ diff --git a/neural/neuralRecorder.lua b/neural/neuralRecorder.lua index b1484f6..9966706 100644 --- a/neural/neuralRecorder.lua +++ b/neural/neuralRecorder.lua @@ -2,6 +2,10 @@ local GPS = require('opus.gps') local Point = require('opus.point') local Util = require('opus.util') +--[[ + Record your movements for playback on another mob. +]] + local os = _G.os local parallel = _G.parallel local peripheral = _G.peripheral diff --git a/neural/nwm.lua b/neural/nwm.lua index 91d2fe5..b1d230e 100644 --- a/neural/nwm.lua +++ b/neural/nwm.lua @@ -87,9 +87,6 @@ local function hook(e, eventData) return true end -local hookEvents = Util.keys(events) -kernel.hook(hookEvents, hook) - local function run(args) local window = Glasses.create(args) @@ -190,6 +187,9 @@ UI:setPage(UI.Page { end, }) +local hookEvents = Util.keys(events) +kernel.hook(hookEvents, hook) + for _,v in pairs(config.session) do run(v) end -- 2.49.1 From ad4cc5884fecea554d9d53ecc37fbcd72fac0f72 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Fri, 8 May 2020 22:31:06 -0600 Subject: [PATCH 40/90] editor/nwm bug fixes --- common/edit.lua | 27 ++++++++++++++------------- neural/nwm.lua | 12 +++++------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/common/edit.lua b/common/edit.lua index 27784e1..ee61066 100644 --- a/common/edit.lua +++ b/common/edit.lua @@ -9,7 +9,7 @@ local fs = _G.fs local multishell = _ENV.multishell local os = _G.os local shell = _ENV.shell -local term = _G.term +local term = _G.term.current() local textutils = _G.textutils local _format = string.format @@ -318,7 +318,7 @@ local page = UI.Page { show = function(self) local t = { } for _,v in pairs(config.recent or { }) do - table.insert(t, { name = fs.getName(v), dir = fs.getDir(v), path = v }) + _insert(t, { name = fs.getName(v), dir = fs.getDir(v), path = v }) end self.grid:setValues(t) self.grid:setIndex(1) @@ -453,10 +453,9 @@ local page = UI.Page { event = 'slide_hide', }, show = function(self, values) - local m = 12 - for _, v in pairs(values) do - m = #v.text > m and #v.text or m - end + local m = Util.reduce(values, function(m, v) + return #v.text > m and #v.text or m + end, 12) m = m + 3 m = m > self.parent.width and self.parent.width or m self.ox = -m @@ -492,7 +491,9 @@ local page = UI.Page { scan = function(self) self.values = { } for k, v in pairs(device) do - table.insert(self.values, { name = k, complete = 'peripheral.wrap("' .. v.side .. '")' }) + if type(v.side) == 'string' then + _insert(self.values, { name = k, complete = 'peripheral.wrap("' .. v.side .. '")' }) + end end end, postInit = function(self) @@ -519,7 +520,7 @@ local page = UI.Page { for k, v in pairs(dev) do if type(v) == 'function' then local m = docs and docs[k] and docs[k]:match('^function%((.+)%).+') - table.insert(t, { method = k, complete = k .. '(' .. (m or '') .. ')' }) + _insert(t, { method = k, complete = k .. '(' .. (m or '') .. ')' }) end end end) @@ -648,9 +649,9 @@ local function getFileInfo(path) config.recent = config.recent or { } Array.removeByValue(config.recent, path) - table.insert(config.recent, 1, path) + _insert(config.recent, 1, path) while #config.recent > 10 do - table.remove(config.recent) + _remove(config.recent) end Config.update('editor', config) @@ -763,7 +764,7 @@ actions = { local last = _remove(undo.chain) if last then undo.active = true - table.insert(undo.redo, { }) + _insert(undo.redo, { }) for i = #last, 1, -1 do local u = last[i] actions[u.action](_unpack(u.args)) @@ -777,14 +778,14 @@ actions = { undo_add = function(entry) if undo.active then local last = undo.redo[#undo.redo] - table.insert(last, entry) + _insert(last, entry) else if not undo.redo_active then undo.redo = { } end local last = undo.chain[#undo.chain] if last and undo.continue then - table.insert(last, entry) + _insert(last, entry) else _insert(undo.chain, { entry }) end diff --git a/neural/nwm.lua b/neural/nwm.lua index b1d230e..00154f8 100644 --- a/neural/nwm.lua +++ b/neural/nwm.lua @@ -76,7 +76,7 @@ local function hook(e, eventData) if clickedTab then if clickedTab ~= currentTab then clickedTab.window.raise() - multishell.setFocus(clickedTab.uid) + kernel.raise(clickedTab.uid) end kernel.event(events[e], { @@ -88,8 +88,6 @@ local function hook(e, eventData) end local function run(args) - local window = Glasses.create(args) - local titleBar = Glasses.create({ x = args.x, y = args.y - 1, @@ -102,17 +100,17 @@ local function run(args) titleBar.canvas:write(args.width - 2, 1, ' x ', nil, 'black') titleBar.redraw() - multishell.openTab({ + kernel.run({ path = args.path, args = args.args, hidden = true, - onDestroy = function() + onExit = function(self) Util.removeByValue(config.session, args) Config.update('nwm', config) - window.destroy() + self.window.destroy() titleBar.destroy() end, - window = window, + window = Glasses.create(args), titleBar = titleBar, wmargs = args, }) -- 2.49.1 From d902acacf4c7f18933a1b581ae5fa01ea25a778f Mon Sep 17 00:00:00 2001 From: Luca S Date: Sun, 10 May 2020 07:48:39 +0200 Subject: [PATCH 41/90] Add a Defragment button to Milo (#36) --- milo/apis/storage.lua | 128 ++++++++++++++++++++++++++++++++++++++++++ milo/core/listing.lua | 18 ++++++ 2 files changed, 146 insertions(+) diff --git a/milo/apis/storage.lua b/milo/apis/storage.lua index dfd2061..a4d19c4 100644 --- a/milo/apis/storage.lua +++ b/milo/apis/storage.lua @@ -291,6 +291,134 @@ function Storage:listItems(throttle) return self.cache end +-- provide a raw list of all the items in all storage chests +-- it might be beneficial to move this to the adapter class at some point and cache the raw item list in this class at some point +function Storage:listItemsRaw(throttle) + local res = {} + + throttle = throttle or Util.throttle() + + for _, v in pairs(self.nodes) do + if v.category == "storage" then + local chest = device[v.name] + local items = chest.list() + + for slot, item in pairs(items) do + items[slot] = itemDB:get(item, function() return chest.getItemMeta(slot) end) + end + + res[v.name] = items + throttle() + end + end + + return res +end + +-- provide a list of items and which chests provide them +function Storage:listProviders(throttle) + local res = {} + + local rawItems = self:listItemsRaw(throttle) + + for chest, items in pairs(rawItems) do + for slot, item in pairs(items) do + local key = table.concat({item.name, item.damage, item.nbtHash}, ":") + if not res[key] then + res[key] = {} + end + table.insert(res[key], {item = item, device = device[chest], lockedToThis = (self.nodes[chest].lock or {})[key] or false, slot = slot}) + end + end + return res +end + +-- defrags the storage system +function Storage:defrag(throttle) + local items = self:listProviders(throttle) + local slotsSaved = 0 + + -- This will make sure the table is sorted in the following order: + -- Unlocked stacks with less than maxCount items | Locked stacks with less than maxCount items | stacks with more than maxCount items + -- This way the locked stacks will be filled first + local function sortFunction(a, b) + local preferenceA, preferenceB + preferenceA = (a.item.count == a.item.maxCount and 3) + or (a.lockedToThis and 2) + or 1 + preferenceB = (b.item.count == b.item.maxCount and 3) + or (b.lockedToThis and 2) + or 1 + + if preferenceA < preferenceB then + return true + elseif preferenceB > preferenceA then + return false + else + return a.item.count < b.item.count + end + end + + for _, providers in pairs(items) do + table.sort(providers, sortFunction) + + -- We're done when we either compressed the stacks so far, that there's only one left (#providers == 1) + -- Or when we've compressed so far, that the there's only one stack which has a lower count than the maxCount + -- Because of the sorting, we know that this will be the stack in providers[1], so we check if providers[2] is at the maxCount + while #providers > 1 and providers[2].item.count ~= providers[2].item.maxCount do + local from = providers[1] + local to + + -- We're pushing to the highest stack which is still below the maxCount, this way as many slots as possible will be filled + -- This loop is guarenteed to assign a value to "to", as the only cases where it wouldn't (#providers == 1 or no provider with less than maxCount) + -- are ruled out by the condition of the outer while loop + for i = 2, #providers do + -- Give preference to locked chests + if not (to and to.lockedToThis or false) and providers[i].lockedToThis then + to = providers[i] + elseif ((to and to.lockedToThis or false) == providers[i].lockedToThis) and providers[i].item.count < providers[i].item.maxCount then + to = providers[i] + elseif providers[i].item.count == providers[i].item.maxCount then + -- As this slot is already at maxCount, all the remaining ones will also be due to sorting + -- If any of the remaining providers is locked that doesn't matter. We wouldn't have been able to push there anyways + break + end + end + + local toMove = math.min(to.item.maxCount - to.item.count, from.item.count) + local s, m = pcall(function() + from.device.pushItems(to.device.name, from.slot, toMove, to.slot) + end) + + if not s and m then + _G._syslog(m) + end + + if s then + to.item.count = to.item.count + toMove + from.item.count = from.item.count - toMove + else + -- Do not try to send to the target again after it failed + for i = 2, #providers do + if to == providers[i] then + table.remove(providers, i) + break + end + end + end + + if from.item.count <= 0 then + table.remove(providers, 1) + slotsSaved = slotsSaved + 1 + end + + table.sort(providers, sortFunction) + end + end + + return slotsSaved +end + function Storage:updateCache(adapter, item, count) if not adapter.cache then adapter.dirty = true diff --git a/milo/core/listing.lua b/milo/core/listing.lua index 34f1b6c..eefff37 100644 --- a/milo/core/listing.lua +++ b/milo/core/listing.lua @@ -33,6 +33,11 @@ local page = UI.Page { event = 'rescan', help = 'Rescan all inventories' }, + { + text = 'Defragment storage', + event = 'defrag', + help = 'Defragments the storage' + } }, }, }, @@ -250,6 +255,10 @@ function page:eventHandler(event) self.grid:draw() self:setFocus(self.statusBar.filter) + elseif event.type == 'defrag' then + self:defrag() + self:refresh(true) + elseif event.type == 'toggle_display' then displayMode = (displayMode + 1) % 2 Util.merge(event.button, displayModes[displayMode]) @@ -357,6 +366,15 @@ function page:refresh(force) self.throttle:disable() end +function page:defrag() + local throttle = function() self.throttle:update() end + + self.throttle:enable() + local saved = context.storage:defrag(throttle) + self.throttle:disable() + self:notifyInfo(("Saved %d slots"):format(saved)) +end + function page:applyFilter() local function filterItems(t, filter) self.grid.sortColumn = Milo:getState('sortColumn') or 'count' -- 2.49.1 From 759e4e2b958f96600aa9455851e60276c1012c95 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Sun, 10 May 2020 14:04:42 -0600 Subject: [PATCH 42/90] cleanup --- common/Devices.lua | 2 +- common/recorder.lua | 10 +++++----- monitor/mirrorClient.lua | 3 +-- neural/nwm.lua | 17 +++++++++-------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/common/Devices.lua b/common/Devices.lua index 7710bd5..c9c5556 100644 --- a/common/Devices.lua +++ b/common/Devices.lua @@ -132,7 +132,7 @@ end function methodsPage:getDocumentation() local method = self.grid:getSelected() - if method.noext then -- computercraft docs + if not method or method.noext then -- computercraft docs return 'No documentation' end diff --git a/common/recorder.lua b/common/recorder.lua index 4ea8824..86dfc3c 100644 --- a/common/recorder.lua +++ b/common/recorder.lua @@ -23,7 +23,7 @@ local colours = _G.colors local args, options = Util.parse(...) -local oldTerm, showInput, skipLast, lastDelay, curInput = Util.shallowCopy(multishell.term), false, false, 2, "" +local oldTerm, showInput, skipLast, lastDelay, curInput = Util.shallowCopy(_G.device.terminal), false, false, 2, "" local curBlink, oldBlink, tTerm, buffer, colourNum, xPos, yPos, oldXPos, oldYPos, tCol, bCol, xSize, ySize = false, false, {}, {}, {}, 1, 1, 1, 1, colours.white, colours.black, oldTerm.getSize() local greys, buttons = {["0"] = true, ["7"] = true, ["8"] = true, ["f"] = true}, {"l", "r", "m"} local charW, charH, chars @@ -60,7 +60,7 @@ if options.help then end if options.daemon then - device.keyboard.addHotkey('control-P', function() + _G.device.keyboard.addHotkey('control-P', function() multishell.openTab({ path = 'sys/apps/shell.lua', args = { arg[0], '--noResize', '--rawOutput', 'recorder.gif' }, @@ -141,7 +141,7 @@ end -- Build a terminal that records stuff: -local recTerm = multishell.term +local recTerm = _G.device.terminal for key, func in pairs(oldTerm) do if type(func) == 'function' then @@ -153,7 +153,7 @@ for key, func in pairs(oldTerm) do end callCount = callCount + 1 curCalls[callCount] = { key, ... } - return unpack(result) + return table.unpack(result) end end end @@ -196,7 +196,7 @@ end _G.device.keyboard.removeHotkey('control-p') for k,fn in pairs(oldTerm) do - multishell.term[k] = fn + _G.device.terminal[k] = fn end multishell.unhideTab(tabId) diff --git a/monitor/mirrorClient.lua b/monitor/mirrorClient.lua index 2e1aca7..b7c8091 100644 --- a/monitor/mirrorClient.lua +++ b/monitor/mirrorClient.lua @@ -2,7 +2,6 @@ local Event = require('opus.event') local Socket = require('opus.socket') local Util = require('opus.util') -local multishell = _ENV.multishell local os = _G.os local remoteId @@ -23,7 +22,7 @@ local function wrapTerm(socket) 'setTextColor', 'setTextColour', 'setBackgroundColor', 'setBackgroundColour', 'scroll', 'setCursorBlink', } - socket.term = multishell.term + socket.term = _G.device.terminal socket.oldTerm = Util.shallowCopy(socket.term) for _,k in pairs(methods) do diff --git a/neural/nwm.lua b/neural/nwm.lua index 00154f8..0fa4d29 100644 --- a/neural/nwm.lua +++ b/neural/nwm.lua @@ -31,10 +31,10 @@ local events = { } local function hook(e, eventData) - local currentTab = kernel.getFocused() + local current = kernel.getFocused() local x = math.floor(eventData[2] / xs) local y = math.floor(eventData[3] / ys) - local clickedTab + local clicked if dragging then if e == 'glasses_up' then @@ -58,7 +58,7 @@ local function hook(e, eventData) local ww, wh = tab.window.getSize() if x >= wx and x <= wx + ww and y > wy and y < wy + wh then - clickedTab = tab + clicked = tab x = x - wx y = y - wy break @@ -73,14 +73,14 @@ local function hook(e, eventData) end end - if clickedTab then - if clickedTab ~= currentTab then - clickedTab.window.raise() - kernel.raise(clickedTab.uid) + if clicked then + if clicked ~= current then + clicked.window.raise() + kernel.raise(clicked.uid) end kernel.event(events[e], { - eventData[1], x, y, clickedTab.window.side, + eventData[1], x, y, clicked.window.side, }) end @@ -104,6 +104,7 @@ local function run(args) path = args.path, args = args.args, hidden = true, + title = fs.getName(args.path), onExit = function(self) Util.removeByValue(config.session, args) Config.update('nwm', config) -- 2.49.1 From ad32dcc2df408567782f4cbcf855123711ceeee1 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Mon, 11 May 2020 17:26:43 -0600 Subject: [PATCH 43/90] updates for environment handling changes --- common/Appstore.lua | 5 +- common/Events.lua | 2 +- common/Turtles.lua | 6 +- common/debugMonitor.lua | 1 + common/edit.lua | 4 +- common/etc/scripts/goHome | 1 - common/etc/scripts/moveTo.lua | 2 - common/etc/scripts/setHome | 1 - common/etc/scripts/summon | 3 - common/recorder.lua | 2 +- monitor/mwm.lua | 18 ++--- neural/Scanner.lua | 2 +- neural/autorun/splash.lua | 2 +- neural/nwm.lua | 134 ++++++++++++++++++++++--------- screenSaver/autorun/saver.lua | 2 +- secure/autorun/lock.lua | 144 +++++++++++++++++----------------- secure/unlock.lua | 20 ++--- 17 files changed, 200 insertions(+), 149 deletions(-) diff --git a/common/Appstore.lua b/common/Appstore.lua index 7e0ad85..8e87ef1 100644 --- a/common/Appstore.lua +++ b/common/Appstore.lua @@ -57,7 +57,7 @@ local function downloadApp(app) end local function runApp(app, checkExists, ...) - local env = shell.makeEnv() + local env = shell.makeEnv(_ENV) local path, fn local args = { ... } @@ -83,9 +83,8 @@ local function runApp(app, checkExists, ...) end end - multishell.openTab({ + multishell.openTab(_ENV, { title = app.name, - env = env, path = path, fn = fn, focused = true, diff --git a/common/Events.lua b/common/Events.lua index ae58ca2..f2539af 100644 --- a/common/Events.lua +++ b/common/Events.lua @@ -67,7 +67,7 @@ local page = UI.Page { self.menuBar:draw() elseif event.type == 'grid_select' then - multishell.openTab({ + multishell.openTab(_ENV, { path = 'sys/apps/Lua.lua', args = { event.selected }, focused = true, diff --git a/common/Turtles.lua b/common/Turtles.lua index 07d89e6..66c6a54 100644 --- a/common/Turtles.lua +++ b/common/Turtles.lua @@ -223,7 +223,7 @@ page = UI.Page { fn = 'turtle.turnRight', }, info = UI.TextArea { - x = 15, y = 2, + x = 15, y = 1, inactive = true, }, showBlocks = function(self) @@ -240,13 +240,13 @@ page = UI.Page { return string.format('%s\n%s\n%s', bu, bf, bd) ]] - local s, m = self:runFunction(script, true) + local s, m = page:runFunction(script, true) self.info:setText(s or m) end, eventHandler = function(self, event) if event.type == 'button_press' then if event.button.fn then - self:runFunction(event.button.fn, event.button.nowrap) + page:runFunction(event.button.fn, event.button.nowrap) self:showBlocks() end return true diff --git a/common/debugMonitor.lua b/common/debugMonitor.lua index d198f77..949375a 100644 --- a/common/debugMonitor.lua +++ b/common/debugMonitor.lua @@ -24,6 +24,7 @@ _G._syslog = function(...) local oldTerm = term.redirect(mon) Util.print(...) term.redirect(oldTerm) + oldDebug(...) end repeat diff --git a/common/edit.lua b/common/edit.lua index ee61066..d702e1f 100644 --- a/common/edit.lua +++ b/common/edit.lua @@ -1024,7 +1024,7 @@ actions = { else local fn, msg = load(_concat(tLines, '\n'), fileInfo.path) if fn then - multishell.openTab({ + multishell.openTab(_ENV, { fn = fn, focused = true, title = fs.getName(fileInfo.path), @@ -1544,5 +1544,5 @@ local s, m = pcall(function() UI:start() end) if not s then actions.save('/crash.txt') print('Editor has crashed. File saved as /crash.txt') - error(m) + error(m, -1) end diff --git a/common/etc/scripts/goHome b/common/etc/scripts/goHome index 94fd138..6e12c25 100644 --- a/common/etc/scripts/goHome +++ b/common/etc/scripts/goHome @@ -1,4 +1,3 @@ -_G.requireInjector(_ENV) local config = require('opus.config').load('gps') if config.home then if turtle.enableGPS() then diff --git a/common/etc/scripts/moveTo.lua b/common/etc/scripts/moveTo.lua index 72d597a..f62520e 100644 --- a/common/etc/scripts/moveTo.lua +++ b/common/etc/scripts/moveTo.lua @@ -1,8 +1,6 @@ local turtle = _G.turtle turtle.run(function() - _G.requireInjector(_ENV) - local GPS = require('opus.gps') if not turtle.enableGPS() then diff --git a/common/etc/scripts/setHome b/common/etc/scripts/setHome index be69170..2b347a7 100644 --- a/common/etc/scripts/setHome +++ b/common/etc/scripts/setHome @@ -1,4 +1,3 @@ -_G.requireInjector(_ENV) local Config = require('opus.config') local pt = turtle.enableGPS() if pt then diff --git a/common/etc/scripts/summon b/common/etc/scripts/summon index 66dcded..17c514c 100644 --- a/common/etc/scripts/summon +++ b/common/etc/scripts/summon @@ -1,7 +1,4 @@ local function summon(id) - - _G.requireInjector(_ENV) - local GPS = require('opus.gps') local Point = require('opus.point') local Socket = require('opus.socket') diff --git a/common/recorder.lua b/common/recorder.lua index 86dfc3c..bd5e7d5 100644 --- a/common/recorder.lua +++ b/common/recorder.lua @@ -61,7 +61,7 @@ end if options.daemon then _G.device.keyboard.addHotkey('control-P', function() - multishell.openTab({ + multishell.openTab(_ENV, { path = 'sys/apps/shell.lua', args = { arg[0], '--noResize', '--rawOutput', 'recorder.gif' }, }) diff --git a/monitor/mwm.lua b/monitor/mwm.lua index 9eda98f..73ec2ee 100644 --- a/monitor/mwm.lua +++ b/monitor/mwm.lua @@ -1,3 +1,4 @@ +local Alt = require('opus.alternate') local Terminal = require('opus.terminal') local trace = require('opus.trace') local Util = require('opus.util') @@ -25,8 +26,7 @@ local monName = args[1] local running local parentMon -local defaultEnv = Util.shallowCopy(_ENV) -defaultEnv.multishell = multishell +_ENV.multishell = multishell if monName then parentMon = peripheral.wrap(monName) or syntax() else @@ -93,8 +93,8 @@ end --[[ A runnable process ]]-- local Process = { } -function Process:new(args) - args.env = args.env or Util.shallowCopy(defaultEnv) +function Process:new(env, args) + args.env = shell.makeEnv(env) args.width = args.width or termDim.width args.height = args.height or termDim.height @@ -329,7 +329,7 @@ function multishell.getTabs() end function multishell.launch(env, file, ...) - return multishell.openTab({ + return multishell.openTab(env, { path = file, env = env, title = 'shell', @@ -337,8 +337,8 @@ function multishell.launch(env, file, ...) }) end -function multishell.openTab(tabInfo) - local process = Process:new(tabInfo) +function multishell.openTab(env, tabInfo) + local process = Process:new(env, tabInfo) table.insert(processes, 1, process) @@ -380,7 +380,7 @@ function multishell.loadSession(filename) local config = Util.readTable(filename) if config then for k = #config, 1, -1 do - multishell.openTab(config[k]) + multishell.openTab(_ENV, config[k]) end end end @@ -497,7 +497,7 @@ local function addShell() process.co = coroutine.create(function() print('To run a program on the monitor, type "fg "') print('To quit, type "exit"') - os.run(Util.shallowCopy(defaultEnv), shell.resolveProgram('shell')) + os.run(shell.makeEnv(_ENV), Alt.get('shell')) multishell.stop() end) diff --git a/neural/Scanner.lua b/neural/Scanner.lua index 4dd6be5..5debfd7 100644 --- a/neural/Scanner.lua +++ b/neural/Scanner.lua @@ -188,7 +188,7 @@ function page:eventHandler(event) self:scan() elseif event.type == 'grid_select' and event.element == self.detail.grid then - multishell.openTab({ + multishell.openTab(_ENV, { path = 'sys/apps/Lua.lua', args = { event.selected }, focused = true, diff --git a/neural/autorun/splash.lua b/neural/autorun/splash.lua index ac92708..e5b32d9 100644 --- a/neural/autorun/splash.lua +++ b/neural/autorun/splash.lua @@ -50,7 +50,7 @@ local function update() end end -kernel.run({ +kernel.run(_ENV, { hidden = true, fn = update, }) diff --git a/neural/nwm.lua b/neural/nwm.lua index 0fa4d29..6100cd0 100644 --- a/neural/nwm.lua +++ b/neural/nwm.lua @@ -11,10 +11,11 @@ local Glasses = require('neural.glasses') local UI = require('opus.ui') local Util = require('opus.util') -local fs = _G.fs -local kernel = _G.kernel -local multishell = _ENV.multishell -local shell = _ENV.shell +local device = _G.device +local fs = _G.fs +local kernel = _G.kernel +local shell = _ENV.shell +local window = _G.window local config = Config.load('nwm', { session = { } }) @@ -22,6 +23,14 @@ local config = Config.load('nwm', { session = { } }) local scale = .5 local xs, ys = 6 * scale, 9 * scale local dragging +local canvas = device['plethora:glasses'].canvas() +local cw, ch = canvas.getSize() +local opacity = 127 + +local multishell = Util.shallowCopy(_ENV.multishell) +_ENV.multishell = multishell + +cw, ch = cw / xs, ch / ys local events = { glasses_click = 'mouse_click', @@ -42,7 +51,7 @@ local function hook(e, eventData) elseif e == 'glasses_drag' then local dx = x - dragging.ax local dy = y - dragging.ay - dragging.tab.window.move(dragging.wx + dx, dragging.wy + dy) + dragging.tab.gwindow.move(dragging.wx + dx, dragging.wy + dy) dragging.tab.titleBar.move(dragging.wx + dx, dragging.wy + dy - 1) dragging.tab.wmargs.x = dragging.wx + dx @@ -53,9 +62,9 @@ local function hook(e, eventData) end for _,tab in ipairs(kernel.routines) do - if tab.window.type == 'glasses' then - local wx, wy = tab.window.getPosition() - local ww, wh = tab.window.getSize() + if tab.gwindow then + local wx, wy = tab.gwindow.getPosition() + local ww, wh = tab.gwindow.getSize() if x >= wx and x <= wx + ww and y > wy and y < wy + wh then clicked = tab @@ -75,46 +84,90 @@ local function hook(e, eventData) if clicked then if clicked ~= current then - clicked.window.raise() + clicked.gwindow.raise() kernel.raise(clicked.uid) end kernel.event(events[e], { - eventData[1], x, y, clicked.window.side, + eventData[1], x, y, clicked.gwindow.side, }) end return true end -local function run(args) +function multishell.openTab(env, tab) + if not tab.wmargs then + tab.wmargs = { + x = math.random(1, cw - 51 + 1), + y = math.random(1, ch - 19 + 1), + width = 51, + height = 19, + opacity = opacity, + path = tab.path, + args = tab.args, + } + table.insert(config.session, tab.wmargs) + Config.update('nwm', config) + else + tab.path = tab.wmargs.path + tab.args = tab.wmargs.args + end + + if tab.path ~= 'sys/apps/shell.lua' then + if tab.args and #tab.args > 0 then + tab.args = { tab.path .. ' ' .. table.concat(tab.args or { }, ' ') } + else + tab.args = { tab.path } + end + tab.path = 'sys/apps/shell.lua' + end + + local wmargs = tab.wmargs + local titleBar = Glasses.create({ - x = args.x, - y = args.y - 1, + x = wmargs.x, + y = wmargs.y - 1, height = 1, - width = args.width, - opacity = args.opacity, + width = wmargs.width, + opacity = wmargs.opacity, }) titleBar.canvas:clear('yellow') - titleBar.canvas:write(1, 1, ' ' .. fs.getName(args.path), nil, 'black') - titleBar.canvas:write(args.width - 2, 1, ' x ', nil, 'black') + titleBar.canvas:write(1, 1, ' ' .. fs.getName(tab.path), nil, 'black') + titleBar.canvas:write(wmargs.width - 2, 1, ' x ', nil, 'black') titleBar.redraw() - kernel.run({ - path = args.path, - args = args.args, - hidden = true, - title = fs.getName(args.path), - onExit = function(self) - Util.removeByValue(config.session, args) - Config.update('nwm', config) - self.window.destroy() - titleBar.destroy() - end, - window = Glasses.create(args), - titleBar = titleBar, - wmargs = args, - }) + if not tab.title and tab.path then + tab.title = fs.getName(tab.path):match('([^%.]+)') + end + tab.hidden = true + tab.title = tab.title or 'untitled' + + local w, h = device.terminal.getSize() + tab.window = window.create(device.terminal, 1, 2, w, h - 1, false) + tab.gwindow = Glasses.create(wmargs) + tab.terminal = tab.gwindow + tab.titleBar = titleBar + tab.onExit = tab.onExit or function(self) + Util.removeByValue(config.session, tab.wmargs) + Config.update('nwm', config) + self.gwindow.destroy() + self.titleBar.destroy() + end + + local routine, message = kernel.run(env, tab) + return routine and routine.uid, message +end + +function multishell.setTitle(tabId, title) + local tab = kernel.find(tabId) + if tab then + tab.title = title + tab.titleBar.canvas:clear('yellow') + tab.titleBar.canvas:write(1, 1, ' ' .. title, nil, 'black') + tab.titleBar.canvas:write(tab.wmargs.width - 2, 1, ' x ', nil, 'black') + tab.titleBar.redraw() + end end UI:setPage(UI.Page { @@ -131,6 +184,12 @@ UI:setPage(UI.Page { formLabel = 'Opacity', formKey = 'opacity', formIndex = 3, labelWidth = 3, transform = math.floor, + eventHandler = function(self, event) + if event.type == 'slider_update' then + opacity = event.value + end + return UI.Slider.eventHandler(self, event) + end, }, UI.Text { x = 10, y = 5, @@ -168,14 +227,15 @@ UI:setPage(UI.Page { if event.type == 'form_complete' then local opts = Util.shallowCopy(event.values) local words = Util.split(opts.run, '(.-) ') - opts.path = shell.resolveProgram(table.remove(words, 1)) - if not opts.path then + words[1] = shell.resolveProgram(words[1]) + if not words[1] then self.notification:error('Invalid program') else - opts.args = #words > 0 and words + opts.path = 'sys/apps/shell.lua' + opts.args = table.concat(words, ' ') table.insert(config.session, opts) Config.update('nwm', config) - run(opts) + multishell.openTab(_ENV, { wmargs = opts }) self.notification:success('Started program') end @@ -190,7 +250,7 @@ local hookEvents = Util.keys(events) kernel.hook(hookEvents, hook) for _,v in pairs(config.session) do - run(v) + multishell.openTab(_ENV, { wmargs = v }) end UI:start() diff --git a/screenSaver/autorun/saver.lua b/screenSaver/autorun/saver.lua index db33bc6..318968a 100644 --- a/screenSaver/autorun/saver.lua +++ b/screenSaver/autorun/saver.lua @@ -31,7 +31,7 @@ local function showScreenSaver() local w, h = kernel.terminal.getSize() local win = window.create(kernel.terminal, 1, 1, w, h, true) - saverUid = multishell.openTab({ + saverUid = multishell.openTab(_ENV, { path = saver, focused = true, title = 'Saver', diff --git a/secure/autorun/lock.lua b/secure/autorun/lock.lua index eadff4c..c71ac38 100644 --- a/secure/autorun/lock.lua +++ b/secure/autorun/lock.lua @@ -7,109 +7,107 @@ local keyboard = device.keyboard local multishell = _ENV.multishell if not multishell then - return + return end local config = Config.load('secure', { enabled = false, - timeout = 60, + timeout = 60, }) local timer = config.enabled and os.startTimer(config.timeout) local function buildLockScreen() - _G.requireInjector(_ENV) + local Event = require('opus.event') + local Security = require('opus.security') + local SHA = require('opus.crypto.sha2') + local UI = require('opus.ui') - local Event = require('opus.event') - local Security = require('opus.security') - local SHA = require('opus.crypto.sha2') - local UI = require('opus.ui') + local counter = .1 - local counter = .1 + local page = UI.Page { + pass = UI.TextEntry { + x = 10, ex = -10, y = "50%", + limit = 32, + mask = true, + shadowText = 'password', + accelerators = { + enter = 'password', + }, + }, + notification = UI.Notification(), + } + function page:eventHandler(event) + if event.type == 'password' then - local page = UI.Page { - pass = UI.TextEntry { - x = 10, ex = -10, y = "50%", - limit = 32, - mask = true, - shadowText = 'password', - accelerators = { - enter = 'password', - }, - }, - notification = UI.Notification(), - } - function page:eventHandler(event) - if event.type == 'password' then + if self.pass.value and + #self.pass.value > 0 and + Security.verifyPassword(SHA.compute(self.pass.value)) then - if self.pass.value and - #self.pass.value > 0 and - Security.verifyPassword(SHA.compute(self.pass.value)) then + UI:quit() -- valid + else + self.notification:error('Invalid password', math.max(counter, 2)) + self:sync() + os.sleep(counter) + counter = counter * 2 - UI:quit() -- valid - else - self.notification:error('Invalid password', math.max(counter, 2)) - self:sync() - os.sleep(counter) - counter = counter * 2 + self.pass:reset() + end + else + UI.Page.eventHandler(self, event) + end + end - self.pass:reset() - end - else - UI.Page.eventHandler(self, event) - end - end + Event.onTerminate(function() return false end) - Event.onTerminate(function() return false end) + UI:setPage(page) + UI:start() - UI:setPage(page) - UI:start() - - -- restart lock timer - timer = os.startTimer(config.timeout) + -- restart lock timer + timer = os.startTimer(config.timeout) end local function showLockScreen() - timer = nil - multishell.openTab({ - path = 'sys/apps/Lock.lua', - fn = buildLockScreen, - noTerminate = true, - pinned = true, - focused = true, - title = 'Lock', - }) + timer = nil + multishell.openTab(_ENV, { + path = 'sys/apps/Lock.lua', + fn = buildLockScreen, + noTerminate = true, + pinned = true, + focused = true, + title = 'Lock', + }) end keyboard.addHotkey('control-l', function() - if timer then - os.cancelTimer(timer) - showLockScreen() - end + if timer then + os.cancelTimer(timer) + showLockScreen() + end end) kernel.hook({ 'mouse_up', 'mouse_drag', 'key_up', 'mouse_scroll' }, function() - if timer then - os.cancelTimer(timer) - timer = os.startTimer(config.timeout) - end + if timer then + os.cancelTimer(timer) + timer = os.startTimer(config.timeout) + end end) kernel.hook('timer', function(_, eventData) - if timer and eventData[1] == timer then - showLockScreen() - end + if timer and eventData[1] == timer then + showLockScreen() + end end) kernel.hook('config_update', function(_, eventData) - if eventData[1] == 'secure' then - Util.merge(config, eventData[2]) - if timer then - os.cancelTimer(timer) - timer = nil - end - if config.enabled then - timer = os.startTimer(config.timeout) - end - end + if eventData[1] == 'secure' then + Util.merge(config, eventData[2]) + if timer then + os.cancelTimer(timer) + timer = nil + end + if config.enabled then + timer = os.startTimer(config.timeout) + end + end end) diff --git a/secure/unlock.lua b/secure/unlock.lua index 6fc5e58..0cdeb02 100644 --- a/secure/unlock.lua +++ b/secure/unlock.lua @@ -16,15 +16,15 @@ term.setCursorPos(1, 1) term.clear() repeat - local s, m = pcall(function() - local password = Terminal.readPassword('Enter password: ') + local s, m = pcall(function() + local password = Terminal.readPassword('Enter password: ') - if password and Security.verifyPassword(SHA.compute(password)) then - return true - end - error('Invalid password') - end) - if not s and m then - _G.printError(m) - end + if password and Security.verifyPassword(SHA.compute(password)) then + return true + end + error('Invalid password') + end) + if not s and m then + _G.printError(m) + end until s -- 2.49.1 From bc0cf883b40193365c819f6dcf69f649df1f2c86 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Tue, 12 May 2020 21:26:02 -0600 Subject: [PATCH 44/90] Improved error messages + nwm opacity/resizing --- monitor/mwm.lua | 3 +- neural/apis/glasses.lua | 77 +++++++++------ neural/apis/interface.lua | 10 +- neural/elytraFly.lua | 11 ++- neural/nwm.lua | 199 +++++++++++++++++--------------------- 5 files changed, 152 insertions(+), 148 deletions(-) diff --git a/monitor/mwm.lua b/monitor/mwm.lua index 73ec2ee..fa19aa1 100644 --- a/monitor/mwm.lua +++ b/monitor/mwm.lua @@ -12,8 +12,7 @@ local term = _G.term local window = _G.window local function syntax() - printError('Syntax:') - error('mwm [--config=filename] [monitor]') + error('Syntax:\nmwm [--config=filename] [monitor]') end local args = Util.parse(...) diff --git a/neural/apis/glasses.lua b/neural/apis/glasses.lua index 1fb398c..e529aa8 100644 --- a/neural/apis/glasses.lua +++ b/neural/apis/glasses.lua @@ -12,31 +12,32 @@ local device = _G.device local Glasses = { } +local palette = { + ['0'] = 0xF0F0F000, + ['1'] = 0xF2B23300, + ['2'] = 0xE57FD800, + ['3'] = 0x99B2F200, + ['4'] = 0xDEDE6C00, + ['5'] = 0x7FCC1900, + ['6'] = 0xF2B2CC00, + ['7'] = 0x4C4C4C00, + ['8'] = 0x99999900, + ['9'] = 0x4C99B200, + ['a'] = 0xB266E500, + ['b'] = 0x3366CC00, + ['c'] = 0x7F664C00, + ['d'] = 0x57A64E00, + ['e'] = 0xCC4C4C00, + ['f'] = 0x19191900, +} + function Glasses.getPalette(opacity) - local pal = { - ['0'] = 0xF0F0F000, - ['1'] = 0xF2B23300, - ['2'] = 0xE57FD800, - ['3'] = 0x99B2F200, - ['4'] = 0xDEDE6C00, - ['5'] = 0x7FCC1900, - ['6'] = 0xF2B2CC00, - ['7'] = 0x4C4C4C00, - ['8'] = 0x99999900, - ['9'] = 0x4C99B200, - ['a'] = 0xB266E500, - ['b'] = 0x3366CC00, - ['c'] = 0x7F664C00, - ['d'] = 0x57A64E00, - ['e'] = 0xCC4C4C00, - ['f'] = 0x19191900, - } if opacity then - for k,v in pairs(pal) do - pal[k] = v + opacity + for k,v in pairs(palette) do + palette[k] = v + opacity end end - return pal + return palette end function Glasses.create(args) @@ -54,7 +55,7 @@ function Glasses.create(args) local canvas = glasses.canvas() local _, cy = 1, 1 local lines = { } - local pal = Glasses.getPalette(opts.opacity) + local pal = palette -- Glasses.getPalette(opts.opacity) local pos = { x = opts.x * xs, y = opts.y * ys } @@ -83,9 +84,11 @@ function Glasses.create(args) blit = function(text, fg, bg) for x = 1, #text do local ln = lines[cy] - ln.bg[x].setColor(pal[bg:sub(x, x)]) - ln.text[x].setColor(pal[fg:sub(x, x)]) - ln.text[x].setText(text:sub(x, x)) + if ln and x <= opts.width then + ln.bg[x].setColor(pal[bg:sub(x, x)] + opts.opacity) + ln.text[x].setColor(pal[fg:sub(x, x)] + opts.opacity) + ln.text[x].setText(text:sub(x, x)) + end end end, setCursorPos = function(_, y) @@ -110,6 +113,13 @@ function Glasses.create(args) function gterm.getTextScale() return opts.scale end + function gterm.getOpacity() + return opts.opacity + end + function gterm.setOpacity(opacity) + opts.opacity = opacity + gterm.redraw() + end function gterm.move(x, y) if opts.x ~= x or opts.y ~= y then opts.x, opts.y = x, y @@ -117,10 +127,21 @@ function Glasses.create(args) group.setPosition(pos.x, pos.y) end end + function gterm.reposition2(x, y, w, h) + opts.x, opts.y = x, y + opts.width, opts.height = w, h + pos = { x = opts.x * xs, y = opts.y * ys } + local g = canvas.addGroup(pos) + init(g) + group.remove() + group = g + gterm.reposition(1, 1, w, h) + gterm.redraw() + end - gterm.name = opts.name - gterm.side = opts.name - gterm.type = 'glasses' + --gterm.name = opts.name + --gterm.side = opts.name + --gterm.type = 'glasses' return gterm end diff --git a/neural/apis/interface.lua b/neural/apis/interface.lua index 083b678..344b45b 100644 --- a/neural/apis/interface.lua +++ b/neural/apis/interface.lua @@ -17,12 +17,14 @@ function Neural.assertModules(modules) for _, m in pairs(modules) do if not Neural.hasModule(m) then - print('Required:') + local t = { } + table.insert(t, 'Required:') for _, v in pairs(modules) do - print(' * ' .. (modules[v] or v)) + table.insert(t, ' * ' .. (modules[v] or v)) end - print('') - error('Missing: ' .. (all[m] or m)) + table.insert(t, '') + table.insert(t, 'Missing: ' .. (all[m] or m)) + error(table.concat(t, '\n')) end end end diff --git a/neural/elytraFly.lua b/neural/elytraFly.lua index eb171ff..776e769 100644 --- a/neural/elytraFly.lua +++ b/neural/elytraFly.lua @@ -11,11 +11,12 @@ local parallel = _G.parallel local STARTUP_FILE = 'usr/autorun/fly.lua' if not modules.launch or not modules.getMetaOwner then - print([[Required: -* Kinetic augment -* Entity sensor -* Introspection module]]) - error('missing required item') + error([[Required: + * Kinetic augment + * Entity sensor + * Introspection module + +Missing required item]]) end if not fs.exists(STARTUP_FILE) then diff --git a/neural/nwm.lua b/neural/nwm.lua index 6100cd0..0df24b8 100644 --- a/neural/nwm.lua +++ b/neural/nwm.lua @@ -8,13 +8,14 @@ local Config = require('opus.config') local Glasses = require('neural.glasses') -local UI = require('opus.ui') local Util = require('opus.util') +local colors = _G.colors local device = _G.device local fs = _G.fs local kernel = _G.kernel local shell = _ENV.shell +local term = _G.term local window = _G.window local config = Config.load('nwm', { session = { } }) @@ -22,10 +23,9 @@ local config = Config.load('nwm', { session = { } }) -- TODO: figure out how to better support scaling local scale = .5 local xs, ys = 6 * scale, 9 * scale -local dragging +local dragging, resizing local canvas = device['plethora:glasses'].canvas() local cw, ch = canvas.getSize() -local opacity = 127 local multishell = Util.shallowCopy(_ENV.multishell) _ENV.multishell = multishell @@ -40,7 +40,6 @@ local events = { } local function hook(e, eventData) - local current = kernel.getFocused() local x = math.floor(eventData[2] / xs) local y = math.floor(eventData[3] / ys) local clicked @@ -61,37 +60,78 @@ local function hook(e, eventData) return end + if resizing then + if e == 'glasses_up' then + resizing.group.remove() + if resizing.dx then + resizing.tab.wmargs.x = resizing.dx + resizing.tab.wmargs.y = resizing.dy + resizing.tab.wmargs.width = resizing.dw + resizing.tab.wmargs.height = resizing.dh + resizing.tab.gwindow.reposition2(resizing.dx, resizing.dy, resizing.dw, resizing.dh) + resizing.tab:resume('term_resize') + + resizing.tab.titleBar.reposition2(resizing.dx, resizing.dy - 1, resizing.dw, 1) + resizing.tab.titleBar:draw(resizing.tab.title) + Config.update('nwm', config) + end + resizing = nil + + elseif e == 'glasses_drag' then + local dx = x - resizing.ax + local dy = y - resizing.ay + + resizing.dx = resizing.tab.wmargs.x + resizing.dy = math.min(resizing.tab.wmargs.y + dy, resizing.tab.wmargs.y + resizing.tab.wmargs.height - 4) + resizing.dw = math.max(resizing.tab.wmargs.width + dx, 8) + resizing.dh = math.max(resizing.tab.wmargs.height - dy, 4) + + resizing.group.setPosition((resizing.dx + 1) * xs, resizing.dy * ys) + resizing.group.setSize(resizing.dw * xs, (resizing.dh + 1) * ys) + end + return + end + for _,tab in ipairs(kernel.routines) do if tab.gwindow then local wx, wy = tab.gwindow.getPosition() local ww, wh = tab.gwindow.getSize() - if x >= wx and x <= wx + ww and y > wy and y < wy + wh then + if x >= wx and x <= wx + ww and y > wy and y <= wy + wh then clicked = tab x = x - wx y = y - wy break - elseif e == 'glasses_click' and x >= wx and x <= wx + ww and y == wy then - if x == wx + ww - 1 then - multishell.terminate(tab.uid) - else - dragging = { tab = tab, ax = x, ay = y, wx = wx, wy = wy } + elseif x >= wx and x <= wx + ww and y == wy then + if e == 'glasses_click' then + if x == wx + ww - 1 then + multishell.terminate(tab.uid) + elseif x == wx + ww - 3 then + local pos = { x = (tab.wmargs.x + 1) * xs, y = tab.wmargs.y * ys } + resizing = { tab = tab, ax = x, ay = y } + resizing.group = canvas.addRectangle(pos.x, pos.y, tab.wmargs.width * xs, (tab.wmargs.height + 1) * ys, 0xF0F0F04F) + else + dragging = { tab = tab, ax = x, ay = y, wx = wx, wy = wy } + end + return + elseif e == 'glasses_scroll' then + tab.wmargs.opacity = Util.clamp(tab.wmargs.opacity - (eventData[1] * 5), 0, 255) + Config.update('nwm', config) + tab.gwindow.setOpacity(tab.wmargs.opacity) end - return end end end if clicked then + local current = kernel.getFocused() if clicked ~= current then clicked.gwindow.raise() + clicked.titleBar.raise() kernel.raise(clicked.uid) end - kernel.event(events[e], { - eventData[1], x, y, clicked.gwindow.side, - }) - + clicked:resume(events[e], eventData[1], x, y) end return true end @@ -103,7 +143,7 @@ function multishell.openTab(env, tab) y = math.random(1, ch - 19 + 1), width = 51, height = 19, - opacity = opacity, + opacity = 192, path = tab.path, args = tab.args, } @@ -125,24 +165,28 @@ function multishell.openTab(env, tab) local wmargs = tab.wmargs - local titleBar = Glasses.create({ - x = wmargs.x, - y = wmargs.y - 1, - height = 1, - width = wmargs.width, - opacity = wmargs.opacity, - }) - titleBar.canvas:clear('yellow') - titleBar.canvas:write(1, 1, ' ' .. fs.getName(tab.path), nil, 'black') - titleBar.canvas:write(wmargs.width - 2, 1, ' x ', nil, 'black') - titleBar.redraw() - if not tab.title and tab.path then tab.title = fs.getName(tab.path):match('([^%.]+)') end tab.hidden = true tab.title = tab.title or 'untitled' + local titleBar = Glasses.create({ + x = wmargs.x, + y = wmargs.y - 1, + height = 1, + width = wmargs.width, + opacity = 160, + }) + titleBar.routine = tab + function titleBar:draw(title) + titleBar.canvas:clear('yellow') + titleBar.canvas:write(1, 1, ' ' .. title, nil, 'black') + titleBar.canvas:write(self.routine.wmargs.width - 4, 1, ' + x ', nil, 'black') + titleBar.redraw() + end + titleBar:draw(tab.title) + local w, h = device.terminal.getSize() tab.window = window.create(device.terminal, 1, 2, w, h - 1, false) tab.gwindow = Glasses.create(wmargs) @@ -163,89 +207,10 @@ function multishell.setTitle(tabId, title) local tab = kernel.find(tabId) if tab then tab.title = title - tab.titleBar.canvas:clear('yellow') - tab.titleBar.canvas:write(1, 1, ' ' .. title, nil, 'black') - tab.titleBar.canvas:write(tab.wmargs.width - 2, 1, ' x ', nil, 'black') - tab.titleBar.redraw() + tab.titleBar:draw(title) end end -UI:setPage(UI.Page { - form = UI.Form { - values = { - x = 1, y = 25, width = 51, height = 19, - opacity = 255, - }, - UI.TextEntry { - formKey = 'run', formLabel = 'Run', required = true, - }, - UI.Slider { - min = 0, max = 255, - formLabel = 'Opacity', formKey = 'opacity', formIndex = 3, - labelWidth = 3, - transform = math.floor, - eventHandler = function(self, event) - if event.type == 'slider_update' then - opacity = event.value - end - return UI.Slider.eventHandler(self, event) - end, - }, - UI.Text { - x = 10, y = 5, - textColor = 'yellow', - value = ' x y' - }, - UI.TextEntry { - x = 10, y = 6, width = 7, limit = 3, - transform = 'number', - formKey = 'x', required = true, - }, - UI.TextEntry { - x = 18, y = 6, width = 7, limit = 4, - transform = 'number', - formKey = 'y', required = true, - }, - UI.Text { - x = 10, y = 8, - textColor = 'yellow', - value = ' width height' - }, - UI.TextEntry { - x = 10, y = 9, width = 7, limit = 4, - transform = 'number', - formKey = 'width', required = true, - }, - UI.TextEntry { - x = 18, y = 9, width = 7, limit = 4, - transform = 'number', - formKey = 'height', required = true, - }, - }, - notification = UI.Notification { }, - eventHandler = function(self, event) - if event.type == 'form_complete' then - local opts = Util.shallowCopy(event.values) - local words = Util.split(opts.run, '(.-) ') - words[1] = shell.resolveProgram(words[1]) - if not words[1] then - self.notification:error('Invalid program') - else - opts.path = 'sys/apps/shell.lua' - opts.args = table.concat(words, ' ') - table.insert(config.session, opts) - Config.update('nwm', config) - multishell.openTab(_ENV, { wmargs = opts }) - self.notification:success('Started program') - end - - elseif event.type == 'form_cancel' then - UI:quit() - end - return UI.Page.eventHandler(self, event) - end, -}) - local hookEvents = Util.keys(events) kernel.hook(hookEvents, hook) @@ -253,6 +218,22 @@ for _,v in pairs(config.session) do multishell.openTab(_ENV, { wmargs = v }) end -UI:start() +term.setBackgroundColor(colors.black) +term.setTextColor(colors.white) +term.clear() +print('Scroll on a titlebar adjusts opacity\n') +print('Run a program') +pcall(function() + while true do + _G.write('> ') + local p = _G.read(nil, nil, shell.complete) + if p and #Util.trim(p) > 0 then + multishell.openTab(_ENV, { + path = 'sys/apps/shell.lua', + args = { p }, + }) + end + end +end) kernel.unhook(hookEvents, hook) -- 2.49.1 From 0619eee41c31accea72f3c6ee661c1feb4d873f5 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Sat, 16 May 2020 10:40:49 -0600 Subject: [PATCH 45/90] milo storage perf fix - nwm update --- milo/apis/storage.lua | 26 +++++++++++-- neural/apis/glasses.lua | 5 --- neural/nwm.lua | 86 +++++++++++++++++++++++++---------------- 3 files changed, 74 insertions(+), 43 deletions(-) diff --git a/milo/apis/storage.lua b/milo/apis/storage.lua index a4d19c4..0c71d57 100644 --- a/milo/apis/storage.lua +++ b/milo/apis/storage.lua @@ -520,6 +520,8 @@ local function rawExport(source, target, item, qty, slot) if amount > 0 then source.lastUpdate = os.clock() target.lastUpdate = os.clock() + else + break end end qty = qty - amount @@ -551,6 +553,7 @@ function Storage:export(target, slot, count, item) if amount ~= pcount then -- this *should* only happen if cache is out of sync + -- out the target is full self:updateCache(adapter, item, pcount - amount) end @@ -561,28 +564,43 @@ function Storage:export(target, slot, count, item) end count = count - amount total = total + amount + + return amount end -- request from adapters with this item for _, adapter in self:onlineAdapters() do local cache = adapter.cache and adapter.cache[key] if cache then - provide(adapter, math.min(count, cache.count)) + local request = math.min(count, cache.count) + + local amount = provide(adapter, request) + + -- couldn't provide the amount that was requested + -- either the target must be full - or the cache is invalid + if amount ~= request then + break + end if count <= 0 then return total end end end - _G._syslog('STORAGE warning: %s(%d): %s%s %s failed to export', - item.displayName or item.name, count, self:_sn(target.name), - slot and string.format('[%d]', slot) or '[*]', key) + if slot then -- ignore warning when exporting to all slots + _G._syslog('STORAGE warning: %s(%d): %s%s %s failed to export', + item.displayName or item.name, count, self:_sn(target.name), + slot and string.format('[%d]', slot) or '[*]', key) + end -- TODO: If there are misses when a slot is specified than something is wrong... -- The caller should confirm the quantity beforehand -- If no slot and full amount is not exported, then no need to check rest of adapters -- ... so should not reach here +-- but... there is the case where exporting to all slots of the target +-- this is valid + return total end diff --git a/neural/apis/glasses.lua b/neural/apis/glasses.lua index e529aa8..fd64550 100644 --- a/neural/apis/glasses.lua +++ b/neural/apis/glasses.lua @@ -99,7 +99,6 @@ function Glasses.create(args) function gterm.setTextScale() end function gterm.getPosition() return opts.x, opts.y end - function gterm.setVisible() end function gterm.raise() local g = canvas.addGroup(pos) init(g) @@ -139,10 +138,6 @@ function Glasses.create(args) gterm.redraw() end - --gterm.name = opts.name - --gterm.side = opts.name - --gterm.type = 'glasses' - return gterm end diff --git a/neural/nwm.lua b/neural/nwm.lua index 0df24b8..996f910 100644 --- a/neural/nwm.lua +++ b/neural/nwm.lua @@ -7,6 +7,7 @@ ]] local Config = require('opus.config') +local Event = require('opus.event') local Glasses = require('neural.glasses') local Util = require('opus.util') @@ -39,14 +40,26 @@ local events = { glasses_scroll = 'mouse_scroll', } -local function hook(e, eventData) - local x = math.floor(eventData[2] / xs) - local y = math.floor(eventData[3] / ys) - local clicked +local timer +local function writeConfig() + if timer then + Event.off(timer) + end + timer = Event.onTimeout(5, function() + _syslog('writing config') + Config.update('nwm', config) + timer = nil + end) +end + +Event.on(Util.keys(events), function(e, button, x, y) + x = math.floor(x / xs) + y = math.floor((y - 1) / ys) if dragging then if e == 'glasses_up' then dragging = nil + elseif e == 'glasses_drag' then local dx = x - dragging.ax local dy = y - dragging.ay @@ -55,7 +68,7 @@ local function hook(e, eventData) dragging.tab.wmargs.x = dragging.wx + dx dragging.tab.wmargs.y = dragging.wy + dy - Config.update('nwm', config) + writeConfig() end return end @@ -73,7 +86,7 @@ local function hook(e, eventData) resizing.tab.titleBar.reposition2(resizing.dx, resizing.dy - 1, resizing.dw, 1) resizing.tab.titleBar:draw(resizing.tab.title) - Config.update('nwm', config) + writeConfig() end resizing = nil @@ -92,6 +105,8 @@ local function hook(e, eventData) return end + local clicked + for _,tab in ipairs(kernel.routines) do if tab.gwindow then local wx, wy = tab.gwindow.getPosition() @@ -102,6 +117,7 @@ local function hook(e, eventData) x = x - wx y = y - wy break + elseif x >= wx and x <= wx + ww and y == wy then if e == 'glasses_click' then if x == wx + ww - 1 then @@ -113,12 +129,14 @@ local function hook(e, eventData) else dragging = { tab = tab, ax = x, ay = y, wx = wx, wy = wy } end - return + elseif e == 'glasses_scroll' then - tab.wmargs.opacity = Util.clamp(tab.wmargs.opacity - (eventData[1] * 5), 0, 255) - Config.update('nwm', config) + tab.wmargs.opacity = Util.clamp(tab.wmargs.opacity - (button * 5), 0, 255) + writeConfig() tab.gwindow.setOpacity(tab.wmargs.opacity) end + + return end end end @@ -131,10 +149,10 @@ local function hook(e, eventData) kernel.raise(clicked.uid) end - clicked:resume(events[e], eventData[1], x, y) + clicked:resume(events[e], button, x, y) end return true -end +end) function multishell.openTab(env, tab) if not tab.wmargs then @@ -148,7 +166,7 @@ function multishell.openTab(env, tab) args = tab.args, } table.insert(config.session, tab.wmargs) - Config.update('nwm', config) + writeConfig() else tab.path = tab.wmargs.path tab.args = tab.wmargs.args @@ -194,7 +212,7 @@ function multishell.openTab(env, tab) tab.titleBar = titleBar tab.onExit = tab.onExit or function(self) Util.removeByValue(config.session, tab.wmargs) - Config.update('nwm', config) + writeConfig() self.gwindow.destroy() self.titleBar.destroy() end @@ -211,29 +229,29 @@ function multishell.setTitle(tabId, title) end end -local hookEvents = Util.keys(events) -kernel.hook(hookEvents, hook) +Event.addRoutine(function() + term.setBackgroundColor(colors.black) + term.setTextColor(colors.white) + term.clear() + print('Scroll on a titlebar adjusts opacity\n') + print('Run a program') + pcall(function() + while true do + _G.write('> ') + local p = _G.read(nil, nil, shell.complete) + if p and #Util.trim(p) > 0 then + multishell.openTab(_ENV, { + path = 'sys/apps/shell.lua', + args = { p }, + }) + end + end + end) + Event.exitPullEvents() +end) for _,v in pairs(config.session) do multishell.openTab(_ENV, { wmargs = v }) end -term.setBackgroundColor(colors.black) -term.setTextColor(colors.white) -term.clear() -print('Scroll on a titlebar adjusts opacity\n') -print('Run a program') -pcall(function() - while true do - _G.write('> ') - local p = _G.read(nil, nil, shell.complete) - if p and #Util.trim(p) > 0 then - multishell.openTab(_ENV, { - path = 'sys/apps/shell.lua', - args = { p }, - }) - end - end -end) - -kernel.unhook(hookEvents, hook) +Event.pullEvents() -- 2.49.1 From 249414e02702039d6b63c398e6eba1f0b5b9c995 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Sun, 17 May 2020 19:38:29 -0600 Subject: [PATCH 46/90] change editors default file to untitled.lua - add package for LuaFileSystem port --- common/edit.lua | 20 ++- lfs/.package | 11 ++ lfs/etc/fstab | 1 + lfs/lfs.lua | 237 +++++++++++++++++++++++++++++++++++ milo/apis/storage.lua | 2 +- packages.list | 1 + penlight/.package | 8 ++ penlight/etc/fstab | 1 + penlight/init/6.penlight.lua | 19 +++ 9 files changed, 297 insertions(+), 3 deletions(-) create mode 100644 lfs/.package create mode 100644 lfs/etc/fstab create mode 100644 lfs/lfs.lua create mode 100644 penlight/.package create mode 100644 penlight/etc/fstab create mode 100644 penlight/init/6.penlight.lua diff --git a/common/edit.lua b/common/edit.lua index d702e1f..a7d0b02 100644 --- a/common/edit.lua +++ b/common/edit.lua @@ -6,6 +6,7 @@ local Util = require('opus.util') local device = _G.device local fs = _G.fs +local keys = _G.keys local multishell = _ENV.multishell local os = _G.os local shell = _ENV.shell @@ -905,7 +906,7 @@ actions = { if not force and undo.chain[#undo.chain] ~= lastSave then page.unsaved:show('file_new') else - actions.open('/untitled.txt') + actions.open('/untitled.lua') end end, @@ -1028,6 +1029,21 @@ actions = { fn = fn, focused = true, title = fs.getName(fileInfo.path), + chainExit = function(_, result) + -- display results of process before + -- closing window + if result then -- clean exit + -- any errors will be picked up by multishells + -- error handling + print('Press enter to exit') + while true do + local e, code = os.pullEventRaw('key') + if e == 'terminate' or e == 'key' and code == keys.enter then + break + end + end + end + end, }) else local ln = msg:match(':(%d+):') @@ -1535,7 +1551,7 @@ actions = { local args = { ... } local filename = args[1] and shell.resolve(args[1]) -if not (actions.load(filename) or actions.load(config.filename) or actions.load('untitled.txt')) then +if not (actions.load(filename) or actions.load(config.filename) or actions.load('untitled.lua')) then error('Error opening file') end diff --git a/lfs/.package b/lfs/.package new file mode 100644 index 0000000..2354b1d --- /dev/null +++ b/lfs/.package @@ -0,0 +1,11 @@ +{ + title = 'LuaFileSystem', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/lfs', + description = [[LuaFileSystem port for computercraft +See: https://github.com/keplerproject/luafilesystem + +LuaFileSystem is a Lua library developed to complement the set of functions related to file systems offered by the standard Lua distribution. + +LuaFileSystem offers a portable way to access the underlying directory structure and file attributes. LuaFileSystem is free software and uses the same license as Lua 5.x (MIT).]], + license = 'MIT', +} diff --git a/lfs/etc/fstab b/lfs/etc/fstab new file mode 100644 index 0000000..125e42a --- /dev/null +++ b/lfs/etc/fstab @@ -0,0 +1 @@ +rom/modules/main/lfs.lua linkfs /packages/lfs/lfs.lua \ No newline at end of file diff --git a/lfs/lfs.lua b/lfs/lfs.lua new file mode 100644 index 0000000..56d5e7b --- /dev/null +++ b/lfs/lfs.lua @@ -0,0 +1,237 @@ +-- a port of LuaFileSystem +-- https://keplerproject.github.io/luafilesystem/manual.html + +local fs = _G.fs +local shell = _ENV.shell + +local lfs = { + _VERSION = '1.8.0.computercraft' +} + +-- lfs.attributes (filepath [, request_name | result_table]) +-- Returns a table with the file attributes corresponding to filepath (or nil followed +-- by an error message and a system-dependent error code in case of error). If the second +-- optional argument is given and is a string, then only the value of the named attribute +-- is returned (this use is equivalent to lfs.attributes(filepath)[request_name], but the +-- table is not created and only one attribute is retrieved from the O.S.). if a table is +-- passed as the second argument, it (result_table) is filled with attributes and returned +-- instead of a new table. The attributes are described as follows; attribute mode is a +-- string, all the others are numbers, and the time related attributes use the same time +-- reference of os.time: +function lfs.attributes(path, request_name) + path = shell.resolve(path) + + local s, fsattr = pcall(fs.attributes, path) + if not s then + return nil, fsattr, 1 + end + + local attributes = type(request_name) == 'table' and request_name or { } + + -- on Unix systems, this represents the device that the inode resides on. + -- On Windows systems, represents the drive number of the disk containing the file + attributes.dev = fs.getDrive(path) + + -- on Unix systems, this represents the inode number. + -- On Windows systems this has no meaning + attributes.ino = nil + + --string representing the associated protection mode + -- (the values could be file, directory, link, socket, named pipe, + -- char device, block device or other) + attributes.mode = fsattr.isDir and 'directory' or 'file' + + -- number of hard links to the file + attributes.nlink = 0 + + -- user-id of owner (Unix only, always 0 on Windows) + attributes.uid = 0 + + -- group-id of owner (Unix only, always 0 on Windows) + attributes.gid = 0 + + -- on Unix systems, represents the device type, for special file inodes. + -- On Windows systems represents the same as dev + attributes.rdev = attributes.dev + + -- time of last access + attributes.access = fsattr.modification + + -- time of last data modification + attributes.modification = fsattr.modification + + -- time of last file status change + attributes.change = fsattr.modification + + -- file size, in bytes + attributes.size = fsattr.size + + -- file permissions string + local perm = (fs.isDir or fs.isReadOnly(path)) and 'r-x' or 'rwx' + attributes.permissions = perm .. perm .. perm + + -- block allocated for file; (Unix only) + attributes.blocks = nil + + -- optimal file system I/O blocksize; (Unix only) + attributes.blksize = nil + + return type(request_name) ~= 'string' and attributes or attributes[request_name] +end + +-- lfs.chdir (path) +-- Changes the current working directory to the given path. +-- Returns true in case of success or nil plus an error string. +function lfs.chdir(path) + path = shell.resolve(path) + if fs.isDir(path) then + shell.setDir(path) + return true + end + return nil, path .. ': No such directory' +end + +-- lfs.currentdir () +-- Returns a string with the current working directory or nil plus an error string. +function lfs.currentdir() + return '/' .. shell.dir() +end + +-- iter, dir_obj = lfs.dir (path) +-- Lua iterator over the entries of a given directory. +-- Each time the iterator is called with dir_obj it returns a directory +-- entry's name as a string, or nil if there are no more entries. +-- You can also iterate by calling dir_obj:next(), and explicitly close the +-- directory before the iteration finished with dir_obj:close(). +-- Raises an error if path is not a directory. +function lfs.dir(path) + path = shell.resolve(path) + local set = fs.list(path) + local iter = function() + local key, value = next(set) + set[key or false] = nil + return value + end + return iter, { + valid = true, + closed = false, + next = function(self) + if not self.valid then + error('file iterator invalid') + end + local n = iter() + if not n then + self.valid = false + end + return n + end, + close = function(self) + if self.closed then + error('file iterator invalid') + end + self.closed = true + self.valid = false + end, + } +end + +-- lfs.link (old, new[, symlink]) +-- Creates a link. The first argument is the object to link to and the second is the +-- name of the link. If the optional third argument is true, the link will by a symbolic +-- link (by default, a hard link is created). +function lfs.link(old, new, symlink) + if not symlink then + return false + end + -- hard links are not supported in vfs :( + old = shell.resolve(old) + new = shell.resolve(new) + return not not fs.mount(new, 'linkfs', old) +end + +-- lfs.mkdir (dirname) +-- Creates a new directory. The argument is the name of the new directory. +-- Returns true in case of success or nil, an error message and a system-dependent +-- error code in case of error. +function lfs.mkdir(dirname) + dirname = shell.resolve(dirname) + if fs.exists(fs.getDir(dirname)) then + fs.makeDir(dirname) + if fs.isDir(dirname) then + return true + end + end + return nil, dirname .. ': Unable to create directory', 1 +end + +-- lfs.rmdir (dirname) +-- Removes an existing directory. The argument is the name of the directory. +-- Returns true in case of success or nil, an error message and a system-dependent +-- error code in case of error. +function lfs.rmdir(dirname) + dirname = shell.resolve(dirname) + if not fs.exists(dirname) or not fs.isDir(dirname) then + return false, dirname .. ': Not a directory', 1 + end + pcall(fs.delete, dirname) + return not fs.exists(dirname) or false, dirname .. ': Unable to remove directory', 1 +end + +-- lfs.setmode (file, mode) +-- Sets the writing mode for a file. The mode string can be either "binary" or "text". +-- Returns true followed the previous mode string for the file, or nil followed by an +-- error string in case of errors. On non-Windows platforms, where the two modes are +-- identical, setting the mode has no effect, and the mode is always returned as binary. +function lfs.setmode(file) + if tostring(file) == 'file (closed)' then + error('closed file') + end + return true, 'binary' +end + +-- lfs.symlinkattributes (filepath [, request_name]) +-- Identical to lfs.attributes except that it obtains information about the link itself +-- (not the file it refers to). It also adds a target field, containing the file name that +-- the symlink points to. On Windows this function does not yet support links, and is +-- identical to lfs.attributes. +function lfs.symlinkattributes(filepath, request_name) + filepath = shell.resolve(filepath) + + local target = fs.resolve(filepath) + local attribs = lfs.attributes('/' .. target) + if filepath ~= target then + attribs.target = '/' .. target + attribs.mode = 'link' + end + + return request_name and attribs[request_name] or attribs +end + +-- lfs.touch (filepath [, atime [, mtime]]) +-- Set access and modification times of a file. This function is a bind to utime function. +-- The first argument is the filename, the second argument (atime) is the access time, and +-- the third argument (mtime) is the modification time. Both times are provided in seconds +-- (which should be generated with Lua standard function os.time). If the modification time +-- is omitted, the access time provided is used; if both times are omitted, the current time +-- is used. +-- Returns true in case of success or nil, an error message and a system-dependent error +-- code in case of error. +function lfs.touch(filename, atime, mtime) + mtime = mtime or atime + filename = shell.resolve(filename) + + if atime or mtime then + error('setting access/modification time is not supported') + end + + -- cc does not suport setting atime/mtime + -- error('lfs.touch not supported') + if not fs.exists(filename) then + local f = fs.open(filename, 'w') + if f then + f.close() + end + end +end + +return lfs diff --git a/milo/apis/storage.lua b/milo/apis/storage.lua index 0c71d57..70d7a59 100644 --- a/milo/apis/storage.lua +++ b/milo/apis/storage.lua @@ -553,7 +553,7 @@ function Storage:export(target, slot, count, item) if amount ~= pcount then -- this *should* only happen if cache is out of sync - -- out the target is full + -- or... the target is full self:updateCache(adapter, item, pcount - amount) end diff --git a/packages.list b/packages.list index eaee2b8..b674e1a 100644 --- a/packages.list +++ b/packages.list @@ -9,6 +9,7 @@ [ 'games' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/games/.package', -- [ 'glasses' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/glasses/.package', [ 'gps' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/gps/.package', + [ 'lfs' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/lfs/.package', [ 'lzwfs' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/lzwfs/.package', [ 'mbs' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/mbs/.package', [ 'milo' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/milo/.package', diff --git a/penlight/.package b/penlight/.package new file mode 100644 index 0000000..4543cf2 --- /dev/null +++ b/penlight/.package @@ -0,0 +1,8 @@ +{ + title = 'Penlight apis', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/penlight', + description = [[See: https://github.com/Tieske/Penlight + +Penlight brings together a set of generally useful pure Lua modules, focusing on input data handling (such as reading configuration files), functional programming (such as map, reduce, placeholder expressions, etc), and OS path management. Much of the functionality is inspired by the Python standard libraries.]], + license = 'MIT', +} diff --git a/penlight/etc/fstab b/penlight/etc/fstab new file mode 100644 index 0000000..ca81d64 --- /dev/null +++ b/penlight/etc/fstab @@ -0,0 +1 @@ +rom/modules/main/pl gitfs Tieske/Penlight/master/lua/pl \ No newline at end of file diff --git a/penlight/init/6.penlight.lua b/penlight/init/6.penlight.lua new file mode 100644 index 0000000..79f29cc --- /dev/null +++ b/penlight/init/6.penlight.lua @@ -0,0 +1,19 @@ +--[[ +local getfenv = _G.getfenv + +-- penlight requires a global package to determine path separator +-- some funky things in penlight regarding global access +_G.package = { + config = '/\n:\n?\n!\n-', +} + +_G.require = function(module) + for i = 2, 3 do + local env = getfenv(i) + if env ~= _G then + return env.require(module) + end + end + error('invalid environment for require') +end +]] \ No newline at end of file -- 2.49.1 From 46c1e3f7e5996ae3cc9df20e030925b940d13bd3 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Mon, 18 May 2020 11:38:32 -0600 Subject: [PATCH 47/90] storage export perf fix --- milo/plugins/exportTask.lua | 44 ++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/milo/plugins/exportTask.lua b/milo/plugins/exportTask.lua index c80159e..c499c7f 100644 --- a/milo/plugins/exportTask.lua +++ b/milo/plugins/exportTask.lua @@ -18,6 +18,9 @@ function ExportTask:cycle(context) for node in context.storage:filterActive('machine', filter) do tasks:add(function() + + local slots + for _, entry in pairs(node.exports) do if not entry.filter then @@ -66,13 +69,42 @@ function ExportTask:cycle(context) end local function exportItems() + local function isPossible(key) + local filterItem = itemDB:get(key) + + if not node.adapter.__size then + node.adapter.__size = node.adapter.size() + end + + -- note that this does not guarantee a match - as + -- we don't have full meta of item in slot + for i = 1, node.adapter.__size do + local slot = slots[i] + if (not slot or slot.name == filterItem.name and + (entry.ignoreDamage or slot.damage == filterItem.damage) and + slot.count < filterItem.maxCount) then + + return true + end + end + end + for key in pairs(entry.filter) do - local items = Milo:getMatches(itemDB:splitKey(key), entry) - for _,item in pairs(items) do - if context.storage:export(node, nil, item.count, item) == 0 then - -- TODO: really shouldn't break here as there may be room in other slots - -- leaving for now for performance reasons - break + if not slots then + slots = node.adapter.list() + end + if isPossible(key) then + local items = Milo:getMatches(itemDB:splitKey(key), entry) + for _,item in pairs(items) do + if context.storage:export(node, nil, item.count, item) == 0 then + -- TODO: really shouldn't break here as there may be room in other slots + -- leaving for now for performance reasons + + break + else + -- refresh the slots + slots = nil + end end end end -- 2.49.1 From 72a85f1a4a6c38d36d33a2f59e11cb0098171c62 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Mon, 18 May 2020 21:20:35 -0600 Subject: [PATCH 48/90] revert perf change --- milo/apis/storage.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/milo/apis/storage.lua b/milo/apis/storage.lua index 70d7a59..c8d6642 100644 --- a/milo/apis/storage.lua +++ b/milo/apis/storage.lua @@ -521,7 +521,7 @@ local function rawExport(source, target, item, qty, slot) source.lastUpdate = os.clock() target.lastUpdate = os.clock() else - break + -- break -- this should work ?? is cache out of sync ? end end qty = qty - amount -- 2.49.1 From 503a340035410505dc04c93f401c2292b90daca2 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Mon, 18 May 2020 21:57:04 -0600 Subject: [PATCH 49/90] more rework on exportTask --- milo/plugins/exportTask.lua | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/milo/plugins/exportTask.lua b/milo/plugins/exportTask.lua index c499c7f..5d64d74 100644 --- a/milo/plugins/exportTask.lua +++ b/milo/plugins/exportTask.lua @@ -69,20 +69,17 @@ function ExportTask:cycle(context) end local function exportItems() - local function isPossible(key) - local filterItem = itemDB:get(key) - + local function canExport(item) if not node.adapter.__size then node.adapter.__size = node.adapter.size() end - -- note that this does not guarantee a match - as - -- we don't have full meta of item in slot for i = 1, node.adapter.__size do local slot = slots[i] - if (not slot or slot.name == filterItem.name and - (entry.ignoreDamage or slot.damage == filterItem.damage) and - slot.count < filterItem.maxCount) then + if (not slot or slot.name == item.name and + (entry.ignoreDamage or slot.damage == item.damage) and + (entry.ignoreNbtHash or slot.nbtHash == item.nbtHash) and + slot.count < item.maxCount) then return true end @@ -93,18 +90,14 @@ function ExportTask:cycle(context) if not slots then slots = node.adapter.list() end - if isPossible(key) then - local items = Milo:getMatches(itemDB:splitKey(key), entry) - for _,item in pairs(items) do + local items = Milo:getMatches(itemDB:splitKey(key), entry) + for _,item in pairs(items) do + if canExport(item) then if context.storage:export(node, nil, item.count, item) == 0 then - -- TODO: really shouldn't break here as there may be room in other slots - -- leaving for now for performance reasons - break - else - -- refresh the slots - slots = nil end + -- refresh the slots + slots = nil end end end -- 2.49.1 From 3d5f665b598b8c16ade99f66b8053dd5f7c60ed3 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Mon, 18 May 2020 22:01:21 -0600 Subject: [PATCH 50/90] oops --- milo/plugins/exportTask.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/milo/plugins/exportTask.lua b/milo/plugins/exportTask.lua index 5d64d74..3fc6a9f 100644 --- a/milo/plugins/exportTask.lua +++ b/milo/plugins/exportTask.lua @@ -74,6 +74,10 @@ function ExportTask:cycle(context) node.adapter.__size = node.adapter.size() end + if not slots then + slots = node.adapter.list() + end + for i = 1, node.adapter.__size do local slot = slots[i] if (not slot or slot.name == item.name and @@ -87,9 +91,6 @@ function ExportTask:cycle(context) end for key in pairs(entry.filter) do - if not slots then - slots = node.adapter.list() - end local items = Milo:getMatches(itemDB:splitKey(key), entry) for _,item in pairs(items) do if canExport(item) then -- 2.49.1 From 2c27787f2747e641e3f920d27b320dade43e253d Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Tue, 19 May 2020 17:06:30 -0600 Subject: [PATCH 51/90] better fuzzy searching --- collections/.package | 9 + collections/apis/init.lua | 1257 ++++++++++++++++++++++++++++++++ collections/etc/fstab | 1 + collections/tests/tests.lua | 1030 ++++++++++++++++++++++++++ common/edit.lua | 69 +- common/etc/fstab | 3 +- lfs/{lfs.lua => apis/init.lua} | 0 lfs/etc/fstab | 2 +- milo/MiloRemote.lua | 2 +- milo/core/listing.lua | 2 +- penlight/etc/fstab | 2 +- 11 files changed, 2331 insertions(+), 46 deletions(-) create mode 100644 collections/.package create mode 100644 collections/apis/init.lua create mode 100644 collections/etc/fstab create mode 100644 collections/tests/tests.lua rename lfs/{lfs.lua => apis/init.lua} (100%) diff --git a/collections/.package b/collections/.package new file mode 100644 index 0000000..cb0e412 --- /dev/null +++ b/collections/.package @@ -0,0 +1,9 @@ +{ + title = 'Collections', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/collections', + description = [[Collections (rework) +See: https://github.com/imliam/Lua-Collections + +Collections are like tables on steroids. They are designed to act as a fluent wrapper when working with structured data, offering the developer convenience for common tasks.]], + license = 'MIT', +} diff --git a/collections/apis/init.lua b/collections/apis/init.lua new file mode 100644 index 0000000..8a98395 --- /dev/null +++ b/collections/apis/init.lua @@ -0,0 +1,1257 @@ +-- collections.lua - A robust collection class based on Laravel collections +-- +-- @module collections.lua +-- @alias Collection +-- @url https://github.com/ImLiam/Lua-Collections + +local Collection = { + version = '0.2.0' +} + +--- Returns all elements from a collection as a table +function Collection:all() + local tbl = {} + for key, value in pairs(self.table) do + tbl[key] = value + end + return tbl +end + +--- Adds an item to the end of a collection +function Collection:append(value) + table.insert(self.table, value) + return self +end + +--- Returns the average value of a list or given key +function Collection:average(key) + local count = self:count() + if count > 0 then + return self:sum(key) / count + end +end + +--- Breaks the collection into multiple smaller collections of a given size +function Collection:chunk(count) + local chunks = self:new() + local currentChunk = {} + + if count <= 0 then + return Collection:new({ {} }) + end + + for _, value in pairs(self.table) do + table.insert(currentChunk, value) + if #currentChunk == count then + chunks:push(currentChunk) + currentChunk = {} + end + end + if #currentChunk > 0 then + chunks:push(currentChunk) + end + return chunks +end + +--- Returns a copy of the collection +function Collection:clone() + local cloned = {} + for key, value in pairs(self.table) do + cloned[key] = value + end + return self:new(cloned) +end + +--- Collapses a collection of tables into a single, flat collection +function Collection:collapse() + local collapsed = self:new() + for _, value in pairs(self.table) do + for _, innerValue in pairs(value) do + collapsed:push(innerValue) + end + end + return collapsed +end + +--- Combines the keys of the collection with the values of another table +function Collection:combine(values) + local combined = self:new() + for key, value in pairs(values) do + if self.table[key] then + combined:set(self.table[key], value) + end + end + return combined +end + +--- Determines whether the collection contains a given item +function Collection:contains(containValue, recursive) + + local function checkContains(key, value) + if type(containValue) == 'function' then + local result = containValue(key, value) + if result then + return true + end + else + if value == containValue then + return true + end + end + return false + end + + for key, value in pairs(self.table) do + if type(value) == 'table' and recursive then + for innerKey, innerValue in pairs(value) do + if checkContains(innerKey, innerValue) then + return true + end + end + else + if checkContains(key, value) then + return true + end + end + end + + return false +end + +--- Turns an associative table into an indexed one, removing string keys +function Collection:convertToIndexed() + local notAssociative = self:new() + for _, value in pairs(self.table) do + notAssociative:push(value) + end + return notAssociative +end + +--- Returns the total number of items in the collection +function Collection:count() + local i = 0 + for _ in pairs(self.table) do + i = i + 1 + end + return i +end + +--- Deals the collection into a number of groups in order, one at a time +function Collection:deal(hands) + local splitted = self:times(hands, function() return {} end) + local currentSection = 1 + + for _, value in pairs(self.table) do + table.insert(splitted.table[currentSection], value) + + currentSection = currentSection + 1 + if currentSection > #splitted.table then + currentSection = 1 + end + end + + return splitted +end + +--- Compares a collection against another table based on its values +--- Returns the values in the original collection that are not present in the given table +function Collection:diff(difference) + local differenceList = {} + for _, value in pairs(difference) do + differenceList[value] = true + end + + local finalDifferences = self:new() + for _, value in pairs(self.table) do + if not differenceList[value] then + finalDifferences:push(value) + end + end + return finalDifferences +end + +--- Compares the collection against another table based on its keys +--- Returns the key / value pairs in the original collection that are not present in the table +function Collection:diffKeys(difference) + local differenceList = {} + for key in pairs(difference) do + differenceList[key] = true + end + + local finalDifferences = self:new() + for key, value in pairs(self.table) do + if not differenceList[key] then + finalDifferences:set(key, value) + end + end + return finalDifferences +end + +--- Iterates over the items in the collection and passes each to a callback +function Collection:each(callback) + for key, value in pairs(self.table) do + if callback(key, value) == false then + break + end + end + return self +end + +--- Iterates over the numerically indexed items in the collection and passes each to a callback +function Collection:eachi(callback) + for key, value in ipairs(self.table) do + if callback(key, value) == false then + break + end + end + return self +end + +--- Compares a table with the internal table of the collection +function Collection:equals(tbl, ignoreMetatables, tbl2) + tbl2 = tbl2 or self.table + if tbl == tbl2 then return true end + local tblType = type(tbl) + local tbl2Type = type(tbl2) + if tblType ~= tbl2Type then return false end + if tblType ~= 'table' then return false end + + if not ignoreMetatables then + local mt1 = getmetatable(tbl) + if mt1 and mt1.__eq then + return tbl == tbl2 + end + end + + local keySet = {} + + for key1, value1 in pairs(tbl) do + local value2 = tbl2[key1] + if value2 == nil or self:equals(value1, ignoreMetatables, value2) == false then + return false + end + keySet[key1] = true + end + + for key2, _ in pairs(tbl2) do + if not keySet[key2] then return false end + end + return true +end + +--- Verify that all elements of the collection pass a truth test +function Collection:every(callback) + for key, value in pairs(self.table) do + if not callback(key, value) then + return false + end + end + return true +end + +--- Returns all items in the collection except for those with specified keys +function Collection:except(keys) + local exceptList = {} + for _, value in pairs(keys) do + exceptList[value] = true + end + + local tbl = self:new() + for key, value in pairs(self.table) do + if not exceptList[key] then + tbl:set(key, value) + end + end + return tbl +end + +--- Internal function used to determine if a value is falsey +function Collection.falsyValue(_, value) + for _, v in ipairs({0, false, ''}) do + if v == value then + return true + end + end + + if type(value) == 'table' then + if next(value) == nil then + return true + end + end + + if not value then + return true + end + + return false +end + +--- Filters the collection using the given callback, keeping only items that pass a truth test +function Collection:filter(callback) + local filtered = self:new() + for key, value in pairs(self.table) do + local response = false + if callback then + response = callback(key, value) + elseif not self:falsyValue(value) then + response = true + end + if response then + filtered:set(key, value) + end + end + return filtered +end + +--- Returns the first element in the collection, or that passes a truth test +function Collection:first(callback) + for key, value in pairs(self.table) do + if callback then + if callback(key, value) then + return value + end + else + return value + end + end +end + +--- Flattens a multi-dimensional collection into a single dimension +function Collection:flatten(depth, tbl, currentDepth) + local flattened = self:new() + local iterable = tbl or self.table + currentDepth = currentDepth or 0 + for _, value in pairs(iterable) do + if type(value) == 'table' + and ((depth and currentDepth < depth - 1) or not depth) then + local flatInside = self:flatten(depth, value, currentDepth + 1):all() + for _, v in pairs(flatInside) do + flattened:push(v) + end + else + flattened:push(value) + end + end + if tbl then + return flattened + else + return flattened + end +end + +--- Swaps the collection's keys with their corresponding values +function Collection:flip() + local flipped = self:new() + for key, value in pairs(self.table) do + flipped:set(value, key) + end + return flipped +end + +--- Removes an item from the collection by its key +function Collection:forget(key) + if self.table[key] then + self.table[key] = nil + end + return self +end + +--- Returns a collection containing the items that would be present for a given page number +function Collection:forPage(pageNumber, perPage) + local page = self:new() + local i = 1 + for _, value in pairs(self.table) do + if i > (pageNumber - 1) * perPage and i <= pageNumber * perPage then + page:push(value) + end + i = i + 1 + end + return page +end + +--- Returns the item of a given key +function Collection:get(key, default) + if self.table[key] then + return self.table[key] + elseif default then + if type(default) == 'function' then + return default(key) + else + return default + end + end +end + +--- Groups the collection's items by a given key +function Collection:groupBy(groupKey) + local grouped = self:new() + for _, value in pairs(self.table) do + + local currentGroupKey = groupKey + + if value[currentGroupKey] then + if not grouped:has(value[currentGroupKey]) then + grouped:set(value[currentGroupKey], {}) + end + table.insert(grouped.table[value[currentGroupKey]], value) + end + + end + return grouped +end + +--- Determines if a given key exists in the collection +function Collection:has(key) + if self.table[key] then + return true + end + return false +end + +--- Joins the items in a collection into a string +function Collection:implode(implodedKey, delimeter) + if type(self:first()) == 'table' then + local toImplode = {} + for _, value in pairs(self.table) do + if value[implodedKey] then + table.insert(toImplode, value[implodedKey]) + end + end + return table.concat(toImplode, delimeter or ', ') + else + return table.concat(self.table, implodedKey or ', ') + end +end + +--- Inserts a value at a given numeric index +function Collection:insert(value, position) + table.insert(self.table, position, value) + return self +end + +--- Removes any values from the original collection that are not present in the passed table +function Collection:intersect(intersection) + local intersected = self:new() + intersection = Collection:new(intersection):flip():all() + + for key, value in pairs(self.table) do + if intersection[value] then + intersected:set(key, value) + end + end + + return intersected +end + +--- Determines whether the collection is associative +function Collection:isAssociative() + if self:count() > #self.table then + return true + end + return false +end + +--- Determines if the collection is empty +function Collection:isEmpty() + if next(self.table) == nil then + return true + end + return false +end + +--- Determines if the collection is not empty +function Collection:isNotEmpty() + if next(self.table) ~= nil then + return true + end + return false +end + +--- Keys the collection by the given key +function Collection:keyBy(keyName) + local keyed = self:new() + for key, value in pairs(self.table) do + if type(keyName) == 'function' then + local response = keyName(key, value) + keyed:set(response, value) + else + keyed:set(value[keyName], value) + end + end + return keyed +end + +--- Returns a list of the collection's keys +function Collection:keys() + local keys = self:new() + for key in pairs(self.table) do + keys:push(key) + end + return keys +end + +--- Returns the last element in the collection, or that passes a truth test +function Collection:last(callback) + local currentValue + for key, value in pairs(self.table) do + if callback then + if callback(key, value) then + currentValue = value + end + else + currentValue = value + end + end + return currentValue +end + +--- Iterates through the collection and passes each value to the callback, which can then modify the values +function Collection:map(callback) + local remapped = self:new() + for key, value in pairs(self.table) do + local newKey, newValue = callback(key, value) + remapped:set(newKey, newValue) + end + return remapped +end + +--- Iterates through the the collection and remaps the key and value based on the return of a callback +function Collection:mapWithKeys(callback) + local mapped = self:new() + for key, value in pairs(self.table) do + local k, v = callback(key, value) + mapped:set(k, v) + end + return mapped +end + +--- Returns the maximum value of a set of given values +function Collection:max(maxKey) + local max + for _, value in pairs(self.table) do + if maxKey then + if not max or value[maxKey] > max then + max = value[maxKey] + end + else + if not max or value > max then + max = value + end + end + end + return max +end + +--- Returns the median value of a set of given values +function Collection:median(medianKey) + local all = {} + for _, value in pairs(self.table) do + if medianKey then + table.insert(all, value[medianKey]) + else + table.insert(all, value) + end + end + table.sort(all, function(a, b) return a < b end) + + if math.fmod(#all, 2) == 0 then + return (all[#all / 2] + all[(#all / 2) + 1] ) / 2 + else + return all[math.ceil(#all/2)] + end +end + +--- Merges the given table with the original collection +function Collection:merge(toMerge) + local merged = self:clone() + for key, value in pairs(toMerge) do + if type(key) == 'number' then + merged:push(value) + else + merged:set(key, value) + end + end + return merged +end + +--- Returns the minimum value of a set of given values +function Collection:min(minKey) + local min + for _, value in pairs(self.table) do + if minKey then + if not min or value[minKey] < min then + min = value[minKey] + end + else + if not min or value < min then + min = value + end + end + end + return min +end + +--- Returns the mode value of a given key +function Collection:mode(modeKey) + local counts = {} + + for _, value in pairs(self.table) do + if modeKey then + value = value[modeKey] + end + if counts[value] == nil then + counts[value] = 1 + else + counts[value] = counts[value] + 1 + end + end + + local biggestCount = 0 + + for _, value in pairs(counts) do + if value > biggestCount then + biggestCount = value + end + end + + local temp = self:new() + + for key, value in pairs(counts) do + if value == biggestCount then + temp:push(key) + end + end + + return temp +end + +--- Creates a new collection instance +function Collection.new(_, tbl) + return setmetatable({ table = tbl or {} }, { __index = Collection, __tostring = Collection.toString }) +end + +--- Creates a new collection consisting of every nth element +function Collection:nth(step, offset) + local nth = self:new() + local position = 1 + offset = (offset and offset + 1) or 1 + + for _, value in pairs(self.table) do + if position % step == offset then + nth:push(value) + end + position = position + 1 + end + + return nth +end + +--- Returns the items in the collection with the specified keys +function Collection:only(keys) + local onlyList = {} + for _, value in pairs(keys) do + onlyList[value] = true + end + + local tbl = self:new() + for key, value in pairs(self.table) do + if onlyList[key] then + tbl:set(key, value) + end + end + return tbl +end + +--- Returns a pair of elements that pass and fail a given truth test +function Collection:partition(callback) + local valid = Collection:new() + local invalid = Collection:new() + + for key, value in pairs(self.table) do + local result = callback(key, value) + if result then + valid:push(value) + else + invalid:push(value) + end + end + + return valid, invalid +end + +-- Passes the collection to the given callback and returns the result +function Collection:pipe(callback) + return callback(self) +end + +--- Retrives all of the values for a given key +function Collection:pluck(valueName, keyName) + local plucked = self:new() + for _, value in pairs(self.table) do + if value[valueName] then + if keyName then + plucked:set(value[keyName], value[valueName]) + else + plucked:push(value[valueName]) + end + end + end + return plucked +end + +--- Removes and returns the last item from the collection +function Collection:pop() + return table.remove(self.table, #self.table) +end + +--- Adds an item to the beginning of the collection +function Collection:prepend(value) + table.insert(self.table, 1, value) + return self +end + +--- Removes and returns an item from the collection by key +function Collection:pull(key) + if type(key) == 'number' then + return table.remove(self.table, key) + else + local pulled = self.table[key] + self.table[key] = nil + return pulled + end +end + +--- Sets the given key and value in the collection +function Collection:put(key, value) + self.table[key] = value + return self +end + + +--- Returns a random item or number of items from the collection +function Collection:random(count, rep) + local all = self:new(self.table):convertToIndexed():all() + local random = self:new() + count = count or 1 + + if count < 0 then + error('Positive number expected, negative number given.') + end + + for _ = 1, count do + if #all > 0 then + local randomElement + if rep then + randomElement = all[math.random(#all)] + else + randomElement = table.remove(all, math.random(#all)) + end + random:push(randomElement) + end + end + + if count == 1 and random[1] then + return random[1] + end + return random +end + +--- Reduces the collection to a single value, passing the result of each iteration into the next +function Collection:reduce(callback, default) + local carry = default + for _, value in pairs(self.table) do + carry = callback(carry, value) + end + return carry +end + +--- Filters the collection using the given fallback +function Collection:reject(callback) + local notRejected = self:new() + for key, value in pairs(self.table) do + local rejected = false + if callback then + rejected = callback(key, value) + elseif not self:falsyValue(value) then + rejected = true + end + if not rejected then + notRejected:set(key, value) + end + end + return notRejected +end + +--- Fixes numerical keys to put them in order +function Collection:resort() + local sorted = self:new() + for key, value in pairs(self.table) do + if type(key) == 'number' then + sorted:push(value) + else + sorted[key] = value + end + end + return sorted +end + +--- Reverses the order of the numerical keys in the collection +function Collection:reverse() + local reversed = self:new() + for key, value in pairs(self.table) do + if type(key) == 'number' then + reversed:prepend(value) + else + reversed:set(key, value) + end + end + return reversed +end + +--- Searches the collection for a value and returns the key +function Collection:search(callback) + for key, value in pairs(self.table) do + if type(callback) == 'function' then + local result = callback(key, value) + if result then + return key + end + else + if callback == value then + return key + end + end + end +end + +--- Removes and returns the first item from the collection +function Collection:shift() + for key, value in pairs(self.table) do + if type(key) == 'number' then + return table.remove(self.table, key) + else + self.table[key] = nil + return value + end + end +end + +--- Randomly shuffles the order of items in the collection +function Collection:shuffle() + local shuffled = self:new() + local numericKeys = {} + local numericValues = {} + for key, value in pairs(self.table) do + if type(key) == 'number' then + table.insert(numericKeys, key) + table.insert(numericValues, value) + end + end + + for key, value in pairs(self.table) do + if type(key) == 'number' then + shuffled:set(table.remove(numericKeys, math.random(#numericKeys)), table.remove(numericValues, math.random(#numericValues))) + -- todo: make this push into a random spot in the array + else + shuffled:set(key, value) + end + end + return shuffled +end + +--- Returns a slice of the collection at the given index +function Collection:slice(index, size) + local slice = self:new() + local i = 0 + for key, value in ipairs(self.table) do + if key >= index + 1 then + slice:append(value) + if size then + i = i + 1 + if i == size then + return slice + end + end + end + end + return slice +end + +--- Sorts the items in the collection +function Collection:sort(callback) + local sorted = self:clone() + if callback and type(callback) == 'function' then + table.sort(sorted.table, callback) + elseif callback then + table.sort(sorted.table, function(a, b) return a[callback] < b[callback] end) + else + table.sort(sorted.table, function(a, b) return a < b end) + end + return sorted +end + +--- Same as the Collection:sort() method, but returns the collection in the opposite order +function Collection:sortDesc(callback) + local sorted = self:clone() + if callback and type(callback) == 'function' then + table.sort(sorted.table, callback) + sorted = sorted:reverse() + elseif callback then + table.sort(sorted.table, function(a, b) return a[callback] > b[callback] end) + else + table.sort(sorted.table, function(a, b) return a > b end) + end + return sorted +end + +--- Removes and returns a slice of items starting at the specified index +function Collection:splice(index, size, replacements) + local spliced = self:new() + local toRemove = {} + local i = 0 + for key, value in ipairs(self.table) do + if key >= index + 1 then + + spliced:append(value) + table.insert(toRemove, key) + + if size then + i = i + 1 + if i == size then + break + end + end + + end + end + + local removedIndex = 0 + for _, key in pairs(toRemove) do + if type(key) == 'number' then + table.remove(self.table, key + removedIndex) + removedIndex = removedIndex - 1 + else + self.table[key] = nil + end + end + + if replacements then + for _ = 1, #replacements do + self:insert(table.remove(replacements, #replacements), index + 1) + end + end + + return spliced +end + +--- Breaks the collection into the given number of groups +function Collection:split(count) + local groupSize = math.ceil(self:count() / count) + return self:chunk(groupSize) +end + +--- Returns the sum of items in the collection +function Collection:sum(key) + local sum = 0 + for i, value in pairs(self.table) do + if key then + if value[key] then + sum = sum + value[key] + else + error('Value "' .. key .. '" does not exist in collection object with key "' .. i .. '"') + end + else + sum = sum + value + end + end + return sum +end + +--- Returns a collection with the specified number of items +function Collection:take(count) + local taken = self:new() + if count >= 0 then + for i = 1, count do + if self.table[i] then + taken:append(self.table[i]) + else + break + end + end + else + local iterations = 0 + for i = #self.table, 1, -1 do + if self.table[i] then + taken:prepend(self.table[i]) + else + break + end + iterations = iterations + 1 + if iterations == -count then + break + end + end + end + return taken +end + +--- Internal function used to determine if a table is associative +function Collection.tableIsAssociative(_, tbl) + local totalCount = 0 + for _ in pairs(tbl) do + totalCount = totalCount + 1 + end + if totalCount > #tbl then + return true + end + return false +end + +--- Internal method used by the Collection:toJSON method to recursively convert tables +function Collection:tableToJSON(tbl) + local jsonRepresentation = function(value) + if type(value) == 'table' then + return self:tableToJSON(value) + elseif type(value) == 'string' then + return '"' .. value:gsub('"', '\\"') .. '"' + elseif type(value) == 'number' then + return value + elseif type(value) == 'boolean' then + return (value and 'true' or 'false') + end + end + + local jsonElements = {} + if self:tableIsAssociative(tbl) then + for key, value in pairs(tbl) do + local json = jsonRepresentation(value) + if json then + json = '"' .. key:gsub('"', '\\"') .. '":' .. json + table.insert(jsonElements, json) + end + end + return '{' .. table.concat(jsonElements, ',') .. '}' + else + for _, value in pairs(tbl) do + local json = jsonRepresentation(value) + table.insert(jsonElements, json) + end + return '[' .. table.concat(jsonElements, ',') .. ']' + end +end + +--- Internal method used by the Collection:toString method to recursively convert tables +function Collection:tableToString(tbl) + local luaRepresentation = function(value) + if type(value) == 'table' then + return self:tableToString(value) + elseif type(value) == 'string' then + return '"' .. value .. '"' + elseif type(value) == 'number' then + return value + elseif type(value) == 'boolean' then + return (value and 'true' or 'false') + end + end + + local luaElements = {} + + for key, value in pairs(tbl) do + local luaString = luaRepresentation(value) + if luaString then + if type(key) == 'number' then + luaString = '[' .. key .. ']=' .. luaString + elseif type(key) == 'string' then + if key:match('%W') then + luaString = '["' .. key:gsub('"', '\\"') .. '"]=' .. luaString + else + luaString = key .. '=' .. luaString + end + end + table.insert(luaElements, luaString) + end + end + return '{' .. table.concat(luaElements, ',') .. '}' +end + +--- Executes the given callback without affecting the collection itself +function Collection:tap(callback) + callback(self) + return self +end + +--- Creates a new collection by invoking the callback a given amount of times +function Collection:times(count, callback) + local tbl = {} + for i = 1, count do + table.insert(tbl, callback(i, tbl)) + end + return self:new(tbl) +end + +--- Returns a reference to the underlying table of the collection +function Collection:toTable() + return self.table +end + +--- Returns a JSON string representation of the collection's values +function Collection:toJSON() + return self:tableToJSON(self.table) +end + +--- Returns a string representation of a Lua table +function Collection:toString() + return self:tableToString(self.table) +end + +--- Iterates over the collection and calls the given callback with each item in the collection, replacing the values in the collection with the response +function Collection:transform(callback) + local transformed = self:new() + for key, value in pairs(self.table) do + transformed:set(key, callback(key, value)) + end + return transformed +end + +--- Adds the given table to the collection +function Collection:union(tbl) + local unionised = self:clone() + for key, value in pairs(tbl) do + if not unionised:has(key) then + unionised:set(key, value) + end + end + return unionised +end + +--- Returns all of the unique items in the collection +function Collection:unique(callback) + local unique = self:new() + local keyList = {} + + for key, value in pairs(self.table) do + local valueToCheck = value + if type(callback) == 'function' then + valueToCheck = callback(key, value) + elseif callback then + valueToCheck = value[callback] + end + if keyList[valueToCheck] ~= nil then + keyList[valueToCheck] = false + else + keyList[valueToCheck] = key + end + end + + for _, key in pairs(keyList) do + if key ~= false then + unique:push(self:get(key)) + end + end + + return unique +end + +--- Executes the given callback when a condition is met +function Collection:when(condition, callback) + if condition then + callback(self) + end + return self +end + +--- Filters the collection by a given key / value pair +function Collection:where(filterKey, filterValue) + local filtered = self:new() + for _, value in pairs(self.table) do + if value[filterKey] == filterValue then + filtered:push(value) + end + end + return filtered +end + +--- Filters the collection by a given key / value contained within the given table +function Collection:whereIn(filterKey, filterValues) + local filtered = self:new() + for _, value in pairs(self.table) do + for _, filterValue in ipairs(filterValues) do + if value[filterKey] == filterValue then + filtered:push(value) + end + end + end + return filtered +end + +--- Filters the collection by a given key / value not contained within the given table +function Collection:whereNotIn(filterKey, filterValues) + local filtered = self:new() + for _, value in pairs(self.table) do + local allowed = true + for _, filterValue in ipairs(filterValues) do + if value[filterKey] == filterValue then + allowed = false + end + end + if allowed then + filtered:push(value) + end + end + return filtered +end + +--- Merges the value of the given table to the value of the original collection at the same index +function Collection:zip(values) + local zipped = self:clone() + for key, value in pairs(values) do + if zipped:has(key) then + zipped:set(key, {zipped:get(key), value}) + end + end + return zipped +end + + + +-----[[ ALIASES FOR OTHER FUNCTIONS ]]----- + + + +--- Alias for the Collection:average() method +Collection.avg = Collection.average + +--- Alias for the Collection:average() method +Collection.mean = Collection.average + +--- Alias for the Collection:each() method +Collection.forEach = Collection.each + +--- Alias for the Collection:eachi() method +Collection.forEachi = Collection.eachi + +--- Alias for the Collection:forget() method +Collection.remove = Collection.forget + +--- Alias for the Collection:convertToIndexed() method +Collection.deassociate = Collection.convertToIndexed + +--- Alias for the Collection:append() method +Collection.push = Collection.append + +--- Alias for the Collection:put() method +Collection.set = Collection.put + +--- Alias for the Collection:resort() method +Collection.values = Collection.resort + +--- Alias for the Collection:sort() method +Collection.sortAsc = Collection.sort + +--- Alias for the Collection:average() method +Collection.replace = Collection.splice + +return setmetatable( + { new = Collection.new }, + { __call = Collection.new } +) diff --git a/collections/etc/fstab b/collections/etc/fstab new file mode 100644 index 0000000..115abbc --- /dev/null +++ b/collections/etc/fstab @@ -0,0 +1 @@ +packages/collections/tests/tests.lua urlfs https://raw.githubusercontent.com/imliam/Lua-Collections/master/tests.lua \ No newline at end of file diff --git a/collections/tests/tests.lua b/collections/tests/tests.lua new file mode 100644 index 0000000..0f177a4 --- /dev/null +++ b/collections/tests/tests.lua @@ -0,0 +1,1030 @@ +local Collection = require "collections" + +local collect = Collection +--- Dump and die (for debugging purposes) +local function dd(value) + if type(value) == 'table' then + print(Collection:tableToString(value)) + elseif type(value) == 'string' then + print('"' .. value .. '"') + else + print(value) + end + os.exit() +end + +--- Assert that two tables are equal +function assert_tables_equal(tbl1, tbl2) + if Collection(tbl1):equals(tbl2) then + return true + end + return error('Compared tables are not identical.') +end + +--[[ all ]]-- +do + assert_tables_equal( + Collection({'a', 'b', 'c'}):all(), + {'a', 'b', 'c'} + ) +end + +--[[ append ]]-- +do + assert_tables_equal( + Collection({1, 2, 3, 4}):append(5):all(), + {1, 2, 3, 4, 5} + ) +end + +--[[ average ]]-- +do + assert(Collection({1, 1, 2, 4}):average() == 2) + + assert(Collection({ {foo = 10}, {foo = 10}, {foo = 20}, {foo = 40} }):average('foo') == 20) +end + +--[[ chunk ]]-- +do + assert_tables_equal( + Collection({1, 2, 3, 4, 5, 6, 7}):chunk(4):all(), + { {1, 2, 3, 4}, {5, 6, 7} } + ) + + assert_tables_equal( + Collection({1, 2, 3, 4, 5, 6, 7}):chunk(0):all(), + { {} } + ) +end + +--[[ clone ]]-- +do + local collection = Collection({1, 2, 3, 4, 5}) + local clone = collection:clone():append(6) + + assert_tables_equal( + clone:all(), + {1, 2, 3, 4, 5, 6} + ) + + assert_tables_equal( + collection:all(), + {1, 2, 3, 4, 5} + ) + +end + +--[[ collapse ]]-- +do + assert_tables_equal( + Collection({ {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }):collapse():all(), + {1, 2, 3, 4, 5, 6, 7, 8, 9} + ) +end + +--[[ combine ]]-- +do + assert_tables_equal( + Collection({'name', 'age'}):combine({'George', 29}):all(), + {name = 'George', age = 29} + ) +end + +--[[ contains ]]-- +do + assert(Collection({'Cat', 'Dog'}):contains('Cat') == true) + + assert(Collection({'Cat', 'Dog'}):contains('Walrus') == false) + + assert(Collection({evil = 'Cat', good = 'Dog'}):contains('Cat') == true) + + assert(Collection({1, 2, 3, 4, 5}):contains(function(key, value) + return value > 5 + end) == false) + + assert(Collection({ {'Cat', 'Dog'}, {'Rabbit', 'Mouse'} }):contains('Cat', true) == true) + + assert(Collection({ {'Cat', 'Dog'}, {'Rabbit', 'Mouse'} }):contains('Cat') == false) +end + +--[[ convertToIndexed ]]-- +do + assert_tables_equal( + Collection({name = 'Liam', language = 'Lua'}):convertToIndexed():all(), + {'Liam', 'Lua'} + ) +end + +--[[ count ]]-- +do + assert(Collection({'a', 'b', 'c', 'd', 'e'}):count() == 5) +end + +--[[ deal ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}):deal(3):all(), + { {1, 4, 7, 10}, {2, 5, 8}, {3, 6, 9} } + ) +end + +--[[ diff ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5, 6}):diff({2, 4, 6, 8}):all(), + {1, 3, 5} + ) +end + +--[[ diffKeys ]]-- +do + assert_tables_equal( + collect({one = 10, two = 20, three = 30, four = 40, five = 50}) + :diffKeys({two = 2, four = 4, six = 6, eight = 8}) + :all(), + {one = 10, three = 30, five = 50} + ) + +end + +--[[ each ]]-- +do + local tbl = {} + collect({'a', 'b', 'c'}):each(function(key, value) + tbl[key] = value + end) + + assert_tables_equal(tbl, {'a', 'b', 'c'}) +end + +--[[ every ]]-- +do + assert(collect({1, 2, 3, 4}):every(function(key, value) + return value > 2 + end) == false) +end + +--[[ except ]]-- +do + assert_tables_equal( + collect({productID = 1, price=100, discount = false}) + :except({'price', 'discount'}) + :all(), + {productID = 1} + ) +end + +--[[ equals ]]-- +do + local collection = collect({ + 1, 2, 3, + [97] = 97, [98] = 98, [99] = 99, key = true, + sub = {1, 2, 3, sub = 'Hello world.'} + }) + + assert(collection.table[1] == 1) + assert(collection.table[2] == 2) + assert(collection.table[3] == 3) + assert(collection.table[97] == 97) + assert(collection.table[98] == 98) + assert(collection.table[99] == 99) + assert(collection.table['key'] == true) + assert(collection.table['sub'][1] == 1) + assert(collection.table['sub'][2] == 2) + assert(collection.table['sub'][3] == 3) + assert(collection.table['sub']['sub'] == 'Hello world.') + + assert(collection:equals({1, 2, 3, 4, 5}) == false) + + assert(collection:equals({ + 1, 2, 3, + [97] = 97, [98] = 98, [99] = 99, key = true, + sub = {1, 2, 3, sub = 'Hello world.'} + }) == true) + + assert(collection:equals({ + 1, 2, 3, 4, + [97] = 97, [98] = 98, [99] = 99, key = true, + sub = {1, 2, 3, sub = 'Hello world.'} + }) == false) + + assert(collect({1, 2, 3, 4, 5}):equals({1, 2, 3, 4, 5}) == true) + + assert(collect({1, 2, 3, 4, 5}):equals({1, 2, 3}) == false) +end + +--[[ filter ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4}):filter(function(key, value) + return value > 2 + end):all(), + {[3] = 3, [4] = 4} + ) + + assert_tables_equal( + collect({1, 2, 3, nil, false, '', 0, {}}):filter():all(), + {1, 2, 3} + ) +end + +--[[ first ]]-- +do + assert(collect({1, 2, 3, 4}):first() == 1) + + assert(collect({1, 2, 3, 4}):first(function(key, value) + return value > 2 + end) == 3) +end + +--[[ flatten ]]-- +do + assert_tables_equal( + collect({name = 'Taylor', languages = {'php', 'javascript', 'lua'} }):flatten():all(), + {'Taylor', 'php', 'javascript', 'lua'} + ) + + assert_tables_equal( + collect({Apple = {name = 'iPhone 6S', brand = 'Apple'}, Samsung = {name = 'Galaxy S7', brand = 'Samsung'} }) + :flatten(1):resort():all(), + { {name = 'iPhone 6S', brand = 'Apple'}, {name = 'Galaxy S7', brand = 'Samsung'} } + ) + + assert_tables_equal( + collect({Apple = {name = 'iPhone 6S', brand = 'Apple'}, Samsung = {name = 'Galaxy S7', brand = 'Samsung'} }) + :flatten(2):resort():all(), + {'iPhone 6S', 'Apple', 'Galaxy S7', 'Samsung'} + ) +end + +--[[ flip ]]-- +do + assert_tables_equal( + collect({name = 'Liam', language = 'Lua'}):flip():all(), + {Liam = 'name', Lua = 'language'} + ) +end + +--[[ forget ]]-- +do + assert_tables_equal( + collect({name = 'Liam', language = 'Lua'}):forget('language'):all(), + {name = 'Liam'} + ) + +end + +--[[ forPage ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5, 6, 7, 8, 9}):forPage(2, 3):all(), + {4, 5, 6} + ) +end + +--[[ get ]]-- +do + assert(collect({name = 'Liam', language = 'Lua'}):get('name') == 'Liam') + + assert(collect({name = 'Liam', language = 'Lua'}):get('foo', 'Default value') == 'Default value') + + assert(collect({name = 'Liam', language = 'Lua'}):get('foo', function(key) + return '"' .. key .. '" was not found in the collection' + end) == '"foo" was not found in the collection') +end + +--[[ groupBy ]]-- +do + assert_tables_equal( + collect({ + {name = 'Liam', language = 'Lua'}, + {name = 'Jeffrey', language = 'PHP'}, + {name = 'Taylor', language = 'PHP'} + }):groupBy('language'):all(), + + { + PHP = { + {name = 'Jeffrey', language = 'PHP'}, + {name = 'Taylor', language = 'PHP'} + }, + Lua = { + {name = 'Liam', language = 'Lua'} + } + } + ) +end + +--[[ has ]]-- +do + assert(collect({name = 'Liam', language = 'Lua'}):has('language') == true) +end + +--[[ implode ]]-- +do + assert(collect({'Lua', 'PHP'}):implode() == 'Lua, PHP') + + assert(collect({'Lua', 'PHP'}):implode(' | ') == 'Lua | PHP') + + assert(collect({ + {name = 'Liam', language = 'Lua'}, + {name = 'Jeffrey', language = 'PHP'} + }):implode('language') == 'Lua, PHP') + + assert(collect({ + {name = 'Liam', language = 'Lua'}, + {name = 'Jeffrey', language = 'PHP'} + }):implode('language', ' | ') == 'Lua | PHP') +end + +--[[ intersect ]]-- +do + assert_tables_equal( + collect({'Desk', 'Sofa', 'Chair'}) + :intersect({'Desk', 'Chair', 'Bookcase'}) + :all(), + {[1] = 'Desk', [3] = 'Chair'} + ) +end + +--[[ isAssociative ]]-- +do + assert(collect({1, 2, 3, 4, 5}):isAssociative() == false) + + assert(collect({name = 'Liam', language = 'Lua'}):isAssociative() == true) +end + +--[[ isEmpty ]]-- +do + assert(collect({'Desk', 'Sofa', 'Chair'}):isEmpty() == false) + + assert(collect():isEmpty() == true) +end + +--[[ isNotEmpty ]]-- +do + assert(collect({'Desk', 'Sofa', 'Chair'}):isNotEmpty() == true) + + assert(collect():isNotEmpty() == false) +end + +--[[ keyBy ]]-- +do + assert_tables_equal( + collect({ + {name = 'Liam', language = 'Lua'}, + {name = 'Jeffrey', language = 'PHP'} + }):keyBy('language'):all(), + { + Lua = {name = 'Liam', language = 'Lua'}, + PHP = {name = 'Jeffrey', language = 'PHP'} + } + ) + + assert_tables_equal( + collect({ + {name = 'Liam', language = 'Lua'}, + {name = 'Jeffrey', language = 'PHP'} + }):keyBy(function(key, value) + return value['language']:lower() + end):all(), + { + lua = {name = 'Liam', language = 'Lua'}, + php = {name = 'Jeffrey', language = 'PHP'} + } + ) +end + +--[[ keys ]]-- +do + assert_tables_equal( + collect({name = 'Liam', language = 'Lua'}):keys():all(), + {'name', 'language'} + ) +end + +--[[ last ]]-- +do + assert(collect({1, 2, 3, 4}):last() == 4) + + assert(collect({1, 2, 3, 4}):last(function(key, value) + return value > 2 + end) == 4) +end + +--[[ map ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5}):map(function(key, value) + return key, value * 2 + end):all(), + {2, 4, 6, 8, 10} + ) +end + +--[[ mapWithKeys ]]-- +do + assert_tables_equal( + collect({ + {name = 'Liam', language = 'Lua'}, + {name = 'Jeffrey', language = 'PHP'} + }):mapWithKeys(function(key, value) + return value['language'], value['name'] + end):all(), + { + Lua = 'Liam', + PHP = 'Jeffrey' + } + ) +end + +--[[ max ]]-- +do + assert(collect({1, 2, 3, 4, 5}):max() == 5) + + assert(collect({ {foo = 10}, {foo = 20} }):max('foo') == 20) +end + +--[[ median ]]-- +do + assert(collect({1, 1, 2, 4}):median() == 1.5) + + assert(collect({ {foo = 10}, {foo = 10}, {foo = 20}, {foo = 40} }):median('foo') == 15) +end + +--[[ merge ]]-- +do + assert_tables_equal( + collect({'Desk', 'Chair'}):merge({'Bookcase', 'Door'}):all(), + {'Desk', 'Chair', 'Bookcase', 'Door'} + ) + + assert_tables_equal( + collect({name = 'Liam', language = 'Lua'}) + :merge({name = 'Taylor', experiencedYears = 14 }) + :all(), + {name = 'Taylor', language = 'Lua', experiencedYears = 14} + ) +end + +--[[ min ]]-- +do + assert(collect({1, 2, 3, 4, 5}):min() == 1) + + assert(collect({ {foo = 10}, {foo = 20} }):min('foo') == 10) +end + +--[[ mode ]]-- +do + assert_tables_equal( + collect({1, 1, 2, 4}):mode():all(), + {1} + ) + + assert_tables_equal( + collect({ {foo = 10}, {foo = 10}, {foo = 20}, {foo = 20}, {foo = 40} }) + :mode('foo') + :all(), + {10, 20} + ) +end + +--[[ new ]]-- +do + assert_tables_equal( + Collection:new({'Hello', 'world'}):all(), + {'Hello', 'world'} + ) +end + +--[[ nth ]]-- +do + assert_tables_equal( + collect({'a', 'b', 'c', 'd', 'e', 'f'}):nth(4):all(), + {'a', 'e'} + ) + + assert_tables_equal( + collect({'a', 'b', 'c', 'd', 'e', 'f'}):nth(4, 1):all(), + {'b', 'f'} + ) +end + +--[[ only ]]-- +do + assert_tables_equal( + collect({name = 'Taylor', language = 'Lua', experiencedYears = 14}) + :only({'name', 'experiencedYears'}) + :all(), + {name = 'Taylor', experiencedYears = 14} + ) +end + +--[[ partition ]]-- +do + local passed, failed = collect({1, 2, 3, 4, 5, 6}):partition(function(key, value) + return value < 3 + end) + + assert_tables_equal( + passed:all(), + {1, 2} + ) + + assert_tables_equal( + failed:all(), + {3, 4, 5, 6} + ) +end + +--[[ pipe ]]-- +do + assert(collect({1, 2, 3}):pipe(function(collection) + return collection:sum() + end) == 6) +end + +--[[ pluck ]]-- +do + assert_tables_equal( + collect({ + {name = 'Liam', language = 'Lua'}, + {name = 'Jeffrey', language = 'PHP'} + }):pluck('name'):all(), + {'Liam', 'Jeffrey'} + ) + + assert_tables_equal( + collect({ + {name = 'Liam', language = 'Lua'}, + {name = 'Jeffrey', language = 'PHP'} + }):pluck('name', 'language'):all(), + {Lua = 'Liam', PHP = 'Jeffrey'} + ) +end + +--[[ pop ]]-- +do + local collection = collect({1, 2, 3, 4, 5}) + + assert(collection:pop() == 5) + + assert_tables_equal( + collection:all(), + {1, 2, 3, 4} + ) +end + +--[[ prepend ]]-- +do + local collection = collect({1, 2, 3, 4, 5}) + collection:prepend(0) + + assert_tables_equal( + collection:all(), + {0, 1, 2, 3, 4, 5} + ) +end + +--[[ pull ]]-- +do + local collection = collect({name = 'Liam', language = 'Lua'}) + + assert(collection:pull('language') == 'Lua') + + assert_tables_equal( + collection:all(), + {name = 'Liam'} + ) +end + +--[[ put ]]-- +do + assert_tables_equal( + collect({name = 'Liam', language = 'Lua'}) + :put('count', 12) + :all(), + {name = 'Liam', language = 'Lua', count = 12} + ) +end + +--[[ random ]]-- +do + assert(type(collect({1, 2, 3, 4, 5}):random():first()) == 'number') + + assert(type(collect({1, 2, 3, 4, 5}):random(3):all()) == 'table') + + assert(collect({1, 2, 3, 4, 5}):random(12):count() == 5) + + assert(collect({1, 2, 3, 4, 5}):random(12, true):count() == 12) +end + +--[[ reduce ]]-- +do + assert(collect({1, 2, 3}):reduce(function(carry, value) + return carry + value + end, 4) == 10) +end + +--[[ reject ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4}):reject(function(key, value) + return value > 2 + end):all(), + {1, 2} + ) +end + +--[[ resort ]]-- +do + assert_tables_equal( + collect({[1] = 'a', [5] = 'b'}):resort():all(), + {[1] = 'a', [2] = 'b'} + ) +end + +--[[ reverse ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5}):reverse():all(), + {5, 4, 3, 2, 1} + ) +end + +--[[ search ]]-- +do + assert(collect({2, 4, 6, 8}):search(4) == 2) + + assert(collect({2, 4, 6, 8}):search(function(key, value) + return value > 5 + end) == 3) +end + +--[[ shift ]]-- +do + local collection = collect({1, 2, 3, 4, 5}) + + assert(collection:shift() == 1) + + assert_tables_equal( + collection:all(), + {2, 3, 4, 5} + ) + +end + +--[[ shuffle ]]-- +do + assert(type(collect({1, 2, 3, 4, 5}):shuffle():all()) == 'table') + + assert(collect({1, 2, 3, 4, 5}):shuffle():count() == 5) +end + +--[[ slice ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}):slice(4):all(), + {5, 6, 7, 8, 9, 10} + ) + + assert_tables_equal( + collect({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}):slice(4, 2):all(), + {5, 6} + ) +end + +--[[ sort ]]-- +do + assert_tables_equal( + collect({5, 3, 1, 2, 4}):sort():all(), + {1, 2, 3, 4, 5} + ) + + assert_tables_equal( + collect({ + {application = 'Google +', users = 12}, + {application = 'Facebook', users = 593}, + {application = 'MySpace', users = 62} + }):sort('users'):all(), + { + {application = 'Google +', users = 12}, + {application = 'MySpace', users = 62}, + {application = 'Facebook', users = 593} + } + ) + + assert_tables_equal( + collect({ + {name = 'Desk', colors = {'Black', 'Mahogany'}}, + {name = 'Chair', colors = {'Black'}}, + {name = 'Bookcase', colors = {'Red', 'Beige', 'Brown'}} + }):sort(function(a, b) + return #a['colors'] < #b['colors'] + end):all(), + { + {name = 'Chair', colors = {'Black'}}, + {name = 'Desk', colors = {'Black', 'Mahogany'}}, + {name = 'Bookcase', colors = {'Red', 'Beige', 'Brown'}} + } + ) +end + +--[[ sortDesc ]]-- +do + assert_tables_equal( + collect({5, 3, 1, 2, 4}):sortDesc():all(), + {5, 4, 3, 2, 1} + ) + + assert_tables_equal( + collect({ + {application = 'Google +', users = 12}, + {application = 'Facebook', users = 593}, + {application = 'MySpace', users = 62} + }):sortDesc('users'):all(), + { + {application = 'Facebook', users = 593}, + {application = 'MySpace', users = 62}, + {application = 'Google +', users = 12} + } + ) + + assert_tables_equal( + collect({ + {name = 'Desk', colors = {'Black', 'Mahogany'}}, + {name = 'Chair', colors = {'Black'}}, + {name = 'Bookcase', colors = {'Red', 'Beige', 'Brown'}} + }):sortDesc(function(a, b) + return #a['colors'] < #b['colors'] + end):all(), + { + {name = 'Bookcase', colors = {'Red', 'Beige', 'Brown'}}, + {name = 'Desk', colors = {'Black', 'Mahogany'}}, + {name = 'Chair', colors = {'Black'}} + } + ) +end + +--[[ splice ]]-- +do + local collection1 = collect({1, 2, 3, 4, 5}) + + assert_tables_equal( + collection1:splice(2):all(), + {3, 4, 5} + ) + + assert_tables_equal( + collection1:all(), + {1, 2} + ) + + + + local collection2 = collect({1, 2, 3, 4, 5}) + + assert_tables_equal( + collection2:splice(2, 2):all(), + {3, 4} + ) + + assert_tables_equal( + collection2:all(), + {1, 2, 5} + ) + + + + local collection3 = collect({1, 2, 3, 4, 5}) + + assert_tables_equal( + collection3:splice(2, 2, {'c', 'd'}):all(), + {3, 4} + ) + + assert_tables_equal( + collection3:all(), + {1, 2, 'c', 'd', 5} + ) +end + +--[[ split ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5}):split(3):all(), + { {1, 2}, {3, 4}, {5} } + ) + + assert_tables_equal( + collect({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}):split(3):all(), + { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10} } + ) +end + +--[[ sum ]]-- +do + assert(collect({1, 2, 3, 4, 5}):sum() == 15) + + assert(collect({ {pages = 176}, {pages = 1096} }):sum('pages') == 1272) +end + +--[[ take ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5}):take(2):all(), + {1, 2} + ) + + assert_tables_equal( + collect({1, 2, 3, 4, 5}):take(-2):all(), + {4, 5} + ) +end + +--[[ tap ]]-- +do + local count + collect({1, 2, 3, 4, 5}):tap(function(collection) + count = collection:count() + end) + assert(count == 5) +end + +--[[ times ]]-- +do + assert_tables_equal( + Collection():times(10, function(count) + return count * 9 + end):all(), + {9, 18, 27, 36, 45, 54, 63, 72, 81, 90} + ) +end + +--[[ toJSON ]]-- +do + local jsonString = collect({ + {name = 'Desk', colors = {'Black', 'Mahogany'}}, + {name = 'Chair', colors = {'Black'}}, + {name = 'Bookcase', colors = {'Red', 'Beige', 'Brown'}} + }):toJSON() + + assert(jsonString == '[{"name":"Desk","colors":["Black","Mahogany"]},{"name":"Chair","colors":["Black"]},{"name":"Bookcase","colors":["Red","Beige","Brown"]}]') +end + +--[[ toString ]]-- +do + local tableString = collect({ + {name = 'Desk', colors = {'Black', 'Mahogany'}}, + {name = 'Chair', colors = {'Black'}}, + {name = 'Bookcase', colors = {'Red', 'Beige', 'Brown'}} + }):toString() + + assert(tableString == '{[1]={name="Desk",colors={[1]="Black",[2]="Mahogany"}},[2]={name="Chair",colors={[1]="Black"}},[3]={name="Bookcase",colors={[1]="Red",[2]="Beige",[3]="Brown"}}}') + +end + +--[[ toTable ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5}):toTable(), + {1, 2, 3, 4, 5} + ) +end + +--[[ transform ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5}):transform(function(key, value) + return value * 2 + end):all(), + {2, 4, 6, 8, 10} + ) +end + +--[[ union ]]-- +do + assert_tables_equal( + collect({a = 'Hello', b = 'Goodbye'}) + :union({a = 'Howdy', c = 'Pleasure to meet you'}) + :all(), + {a = 'Hello', b = 'Goodbye', c = 'Pleasure to meet you'} + ) +end + +--[[ unique ]]-- +do + assert_tables_equal( + collect({1, 1, 2, 2, 3, 4, 2}):unique():all(), + {3, 4} + ) + + assert_tables_equal( + collect({ + {name = 'iPhone 6', brand = 'Apple', type = 'phone'}, + {name = 'iPhone 5', brand = 'Apple', type = 'phone'}, + {name = 'Apple Watch', brand = 'Apple', type = 'watch'}, + {name = 'Galaxy S6', brand = 'Samsung', type = 'phone'}, + {name = 'Galaxy Gear', brand = 'Samsung', type = 'watch'}, + {name = 'Pixel', brand = 'Google', type = 'phone'} + }):unique('brand'):all(), + { + {name = 'Pixel', brand = 'Google', type = 'phone'} + } + ) + + assert_tables_equal( + collect({ + {name = 'iPhone 6', brand = 'Apple', type = 'phone'}, + {name = 'iPhone 5', brand = 'Apple', type = 'phone'}, + {name = 'Apple Watch', brand = 'Apple', type = 'watch'}, + {name = 'Galaxy S6', brand = 'Samsung', type = 'phone'}, + {name = 'Galaxy Gear', brand = 'Samsung', type = 'watch'}, + {name = 'Pixel', brand = 'Google', type = 'phone'} + }):unique(function(key, value) + return value['brand'] .. value['type'] + end):all(), + { + {name = 'Galaxy Gear', brand = 'Samsung', type = 'watch'}, + {name = 'Apple Watch', brand = 'Apple', type = 'watch'}, + {name = 'Pixel', brand = 'Google', type = 'phone'}, + {name = 'Galaxy S6', brand = 'Samsung', type = 'phone'} + } + ) +end + +--[[ when ]]-- +do + assert_tables_equal( + collect({1, 2, 3}):when(true, function(collection) + return collection:push(4) + end):all(), + {1, 2, 3, 4} + ) +end + +--[[ where ]]-- +do + assert_tables_equal( + collect({ + {name = 'iPhone 6', brand = 'Apple', type = 'phone'}, + {name = 'iPhone 5', brand = 'Apple', type = 'phone'}, + {name = 'Apple Watch', brand = 'Apple', type = 'watch'}, + {name = 'Galaxy S6', brand = 'Samsung', type = 'phone'}, + {name = 'Galaxy Gear', brand = 'Samsung', type = 'watch'} + }):where('type', 'watch'):all(), + { + {name = 'Apple Watch', brand = 'Apple', type = 'watch'}, + {name = 'Galaxy Gear', brand = 'Samsung', type = 'watch'} + } + ) +end + +--[[ whereIn ]]-- +do + assert_tables_equal( + collect({ + {name = 'iPhone 6', brand = 'Apple', type = 'phone'}, + {name = 'iPhone 5', brand = 'Apple', type = 'phone'}, + {name = 'Apple Watch', brand = 'Apple', type = 'watch'}, + {name = 'Galaxy S6', brand = 'Samsung', type = 'phone'}, + {name = 'Galaxy Gear', brand = 'Samsung', type = 'watch'} + }):whereIn('name', {'iPhone 6', 'iPhone 5', 'Galaxy S6'}):all(), + { + {name = 'iPhone 6', brand = 'Apple', type = 'phone'}, + {name = 'iPhone 5', brand = 'Apple', type = 'phone'}, + {name = 'Galaxy S6', brand = 'Samsung', type = 'phone'} + } + ) +end + +--[[ whereNotIn ]]-- +do + assert_tables_equal( + collect({ + {name = 'iPhone 6', brand = 'Apple', type = 'phone'}, + {name = 'iPhone 5', brand = 'Apple', type = 'phone'}, + {name = 'Apple Watch', brand = 'Apple', type = 'watch'}, + {name = 'Galaxy S6', brand = 'Samsung', type = 'phone'}, + {name = 'Galaxy Gear', brand = 'Samsung', type = 'watch'} + }):whereNotIn('name', {'iPhone 6', 'iPhone 5', 'Galaxy S6'}):all(), + { + {name = 'Apple Watch', brand = 'Apple', type = 'watch'}, + {name = 'Galaxy Gear', brand = 'Samsung', type = 'watch'} + } + ) +end + +--[[ zip ]]-- +do + assert_tables_equal( + collect({'Chair', 'Desk'}):zip({100, 200}):all(), + { {'Chair', 100}, {'Desk', 200} } + ) + + assert_tables_equal( + collect({'Chair', 'Desk'}):zip({100, 200, 300}):all(), + { {'Chair', 100}, {'Desk', 200} } + ) +end + +print('All tests passed.') diff --git a/common/edit.lua b/common/edit.lua index a7d0b02..bfc157a 100644 --- a/common/edit.lua +++ b/common/edit.lua @@ -362,29 +362,21 @@ local page = UI.Page { event = 'slide_hide', }, apply_filter = function(self, filter) - local t = { } if filter then filter = filter:lower() self.grid.sortColumn = 'score' - self.grid.inverseSort = true - for _,v in pairs(self.listing) do - v.score = fuzzy(v.lname, filter) - if v.score then - _insert(t, v) - end + for _,v in pairs(self.grid.values) do + v.score = -fuzzy(v.lname, filter) end else self.grid.sortColumn = 'lname' - self.grid.inverseSort = false - t = self.listing end - self.grid:setValues(t) + self.grid:update() self.grid:setIndex(1) end, show = function(self) - local listing = { } local function recurse(dir) local files = fs.list(dir) for _,f in ipairs(files) do @@ -392,7 +384,7 @@ local page = UI.Page { if fs.native.isDir(fullName) then -- skip virtual dirs if f ~= '.git' then recurse(fullName) end else - _insert(listing, { + _insert(self.grid.values, { name = f, dir = dir, lname = f:lower(), @@ -402,7 +394,6 @@ local page = UI.Page { end end recurse('') - self.listing = listing self:apply_filter() self.filter_entry:reset() UI.SlideOut.show(self) @@ -1015,44 +1006,40 @@ actions = { actions.error('open available with multishell') return end + local routine = { + focused = true, + title = fs.getName(fileInfo.path), + chainExit = function(_, result) + -- display results of process before closing window + if result then -- clean exit + -- any errors will be picked up by multishells + -- error handling + print('Press enter to exit') + while true do + local e, code = os.pullEventRaw('key') + if e == 'terminate' or e == 'key' and code == keys.enter then + break + end + end + end + end, + } if undo.chain[#undo.chain] == lastSave then - local nTask = shell.openTab(fileInfo.path) - if nTask then - shell.switchTab(nTask) - else - actions.error("error starting Task") - end + routine.path = 'sys/apps/shell.lua' + routine.args = { fileInfo.path } else local fn, msg = load(_concat(tLines, '\n'), fileInfo.path) - if fn then - multishell.openTab(_ENV, { - fn = fn, - focused = true, - title = fs.getName(fileInfo.path), - chainExit = function(_, result) - -- display results of process before - -- closing window - if result then -- clean exit - -- any errors will be picked up by multishells - -- error handling - print('Press enter to exit') - while true do - local e, code = os.pullEventRaw('key') - if e == 'terminate' or e == 'key' and code == keys.enter then - break - end - end - end - end, - }) - else + if not fn then local ln = msg:match(':(%d+):') if ln and tonumber(ln) then actions.go_to(1, tonumber(ln)) end actions.error(msg) + return end + routine.fn = fn end + multishell.openTab(_ENV, routine) end, status = function() diff --git a/common/etc/fstab b/common/etc/fstab index e636640..00b921e 100644 --- a/common/etc/fstab +++ b/common/etc/fstab @@ -3,4 +3,5 @@ packages/common/hexedit.lua urlfs https://pastebin.com/raw/Ds9ajsp4 packages/common/colors.lua urlfs https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/ignore/colors.lua packages/common/cowsay.lua urlfs https://pastebin.com/raw/n00VQJsw packages/common/calc.lua urlfs https://pastebin.com/raw/nAinUn1h -packages/common/write.lua urlfs https://pastebin.com/raw/RSyhCjqv \ No newline at end of file +packages/common/write.lua urlfs https://pastebin.com/raw/RSyhCjqv +packages/common/apis/debugger.lua urlfs https://raw.githubusercontent.com/slembcke/debugger.lua/master/debugger.lua \ No newline at end of file diff --git a/lfs/lfs.lua b/lfs/apis/init.lua similarity index 100% rename from lfs/lfs.lua rename to lfs/apis/init.lua diff --git a/lfs/etc/fstab b/lfs/etc/fstab index 125e42a..ec3b32c 100644 --- a/lfs/etc/fstab +++ b/lfs/etc/fstab @@ -1 +1 @@ -rom/modules/main/lfs.lua linkfs /packages/lfs/lfs.lua \ No newline at end of file +packages/lfs/tests/test.lua urlfs https://raw.githubusercontent.com/keplerproject/luafilesystem/master/tests/test.lua \ No newline at end of file diff --git a/milo/MiloRemote.lua b/milo/MiloRemote.lua index 1f2caa8..fda2876 100644 --- a/milo/MiloRemote.lua +++ b/milo/MiloRemote.lua @@ -318,7 +318,7 @@ function page:applyFilter() v.score = fuzzy(v.lname, filter) if v.score then if v.count > 0 then - v.score = v.score + 1 + v.score = v.score + .2 end table.insert(r, v) end diff --git a/milo/core/listing.lua b/milo/core/listing.lua index eefff37..945c194 100644 --- a/milo/core/listing.lua +++ b/milo/core/listing.lua @@ -390,7 +390,7 @@ function page:applyFilter() v.score = fuzzy(v.lname, filter) if v.score then if v.count > 0 then - v.score = v.score + 1 + v.score = v.score + .2 end table.insert(r, v) end diff --git a/penlight/etc/fstab b/penlight/etc/fstab index ca81d64..2a1632b 100644 --- a/penlight/etc/fstab +++ b/penlight/etc/fstab @@ -1 +1 @@ -rom/modules/main/pl gitfs Tieske/Penlight/master/lua/pl \ No newline at end of file +#rom/modules/main/pl gitfs Tieske/Penlight/master/lua/pl \ No newline at end of file -- 2.49.1 From cb58a553f5f4935c83b5633ac9f1e2fd125c94e3 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Sat, 23 May 2020 21:44:55 -0600 Subject: [PATCH 52/90] Lua debugger part 1 --- common/edit.lua | 93 ++------- debugger/.package | 6 + debugger/apis/init.lua | 201 ++++++++++++++++++++ debugger/debug.lua | 407 ++++++++++++++++++++++++++++++++++++++++ debugger/example.lua | 28 +++ lzwfs/lzwfs.lua | 354 +++++++++++++++++----------------- lzwfs/startup.lua | 4 +- mbs/autorun/startup.lua | 4 +- minify/minifyDir.lua | 24 +-- neural/Equipment.lua | 148 +++++++-------- packages.list | 3 +- 11 files changed, 923 insertions(+), 349 deletions(-) create mode 100644 debugger/.package create mode 100644 debugger/apis/init.lua create mode 100644 debugger/debug.lua create mode 100644 debugger/example.lua diff --git a/common/edit.lua b/common/edit.lua index bfc157a..54f5ba6 100644 --- a/common/edit.lua +++ b/common/edit.lua @@ -335,92 +335,23 @@ local page = UI.Page { return UI.SlideOut.eventHandler(self, event) end, }, - quick_open = UI.SlideOut { - filter_entry = UI.TextEntry { - x = 2, y = 2, ex = -2, - shadowText = 'File name', - accelerators = { - [ 'enter' ] = 'accept', - [ 'up' ] = 'grid_up', - [ 'down' ] = 'grid_down', - }, - }, - grid = UI.ScrollingGrid { - x = 2, y = 3, ex = -2, ey = -4, - disableHeader = true, - columns = { - { key = 'name' }, - { key = 'dir', textColor = 'lightGray' }, - }, - accelerators = { - grid_select = 'accept', - }, - }, - cancel = UI.Button { - x = -9, y = -2, - text = 'Cancel', - event = 'slide_hide', - }, - apply_filter = function(self, filter) - if filter then - filter = filter:lower() - self.grid.sortColumn = 'score' - - for _,v in pairs(self.grid.values) do - v.score = -fuzzy(v.lname, filter) - end - else - self.grid.sortColumn = 'lname' - end - - self.grid:update() - self.grid:setIndex(1) - end, + quick_open = UI.QuickSelect { + modal = true, + enable = function() end, show = function(self) - local function recurse(dir) - local files = fs.list(dir) - for _,f in ipairs(files) do - local fullName = fs.combine(dir, f) - if fs.native.isDir(fullName) then -- skip virtual dirs - if f ~= '.git' then recurse(fullName) end - else - _insert(self.grid.values, { - name = f, - dir = dir, - lname = f:lower(), - fullName = fullName, - }) - end - end - end - recurse('') - self:apply_filter() - self.filter_entry:reset() - UI.SlideOut.show(self) + UI.QuickSelect.enable(self) + self:focusFirst() + self:draw() self:addTransition('expandUp', { easing = 'outBounce', ticks = 12 }) end, eventHandler = function(self, event) - if event.type == 'grid_up' then - self.grid:emit({ type = 'scroll_up' }) - - elseif event.type == 'grid_down' then - self.grid:emit({ type = 'scroll_down' }) - - elseif event.type == 'accept' then - local sel = self.grid:getSelected() - if sel then - actions.process('open', sel.fullName) - self:hide() - end - - elseif event.type == 'text_change' then - self:apply_filter(event.text) - self.grid:draw() - - else - return UI.SlideOut.eventHandler(self, event) + if event.type == 'select_cancel' then + self:disable() + elseif event.type == 'select_file' then + self:disable() + actions.process('open', event.file) end - return true + return UI.QuickSelect.eventHandler(self, event) end, }, completions = UI.SlideOut { diff --git a/debugger/.package b/debugger/.package new file mode 100644 index 0000000..b16807f --- /dev/null +++ b/debugger/.package @@ -0,0 +1,6 @@ +{ + title = 'Lua Debugger', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/debugger', + description = [[Lua interactive debugger]], + license = 'MIT', +} \ No newline at end of file diff --git a/debugger/apis/init.lua b/debugger/apis/init.lua new file mode 100644 index 0000000..61ed024 --- /dev/null +++ b/debugger/apis/init.lua @@ -0,0 +1,201 @@ +--[[ +some portions from https://github.com/slembcke/debugger.lua +]] + +local fs = _G.fs + +local dbg = { } + +local function hookBreakpoint(info) + if dbg.breakpoints then + for _,v in pairs(dbg.breakpoints) do + if v.line == info.currentline and v.file == info.short_src then + return true + end + end + end +end + +local function hookFunction(fn) + return function(info) + return info.func == fn + end +end + +local function hookStep() + local co = coroutine.running() + return function() + return co == coroutine.running() + end +end + +local function hookStepStacksize(n) + local co = coroutine.running() + local i = 2 + while true do + local info = debug.getinfo(i) + if not info then + break + end + i = i + 1 + end + return function() + if co == coroutine.running() then + if not debug.getinfo(i - n) then + return true + end + end + end +end + +local function hookStepOut() + return hookStepStacksize(1) +end + +local function hookStepOver() + return hookStepStacksize(0) +end + +local hookEval = function() end + +-- Create a table of all the locally accessible variables. +-- Globals are not included when running the locals command +local function local_bindings(offset, stack_inspect_offset) + offset = offset + 1 + stack_inspect_offset -- add this function to the offset + local func = debug.getinfo(offset).func + local bindings = {} + + -- Retrieve the upvalues + do local i = 1; while true do + local name, value = debug.getupvalue(func, i) + if not name then break end + bindings[name] = value + i = i + 1 + end end + + -- Retrieve the locals (overwriting any upvalues) + do local i = 1; while true do + local name, value = debug.getlocal(offset, i) + if not name then break end + bindings[name] = value + i = i + 1 + end end + + -- Retrieve the varargs (works in Lua 5.2 and LuaJIT) + local varargs = {} + do local i = 1; while true do + local name, value = debug.getlocal(offset, -i) + if not name then break end + varargs[i] = value + i = i + 1 + end end + if #varargs > 0 then bindings["..."] = varargs end + + return bindings +end + +local function get_trace(offset, stack_inspect_offset) + local function format_loc(file, line) return file..":"..line end + local function format_stack_frame_info(info) + local filename = info.source:match("@(.*)") + local source = filename and fs.getName(filename) or info.short_src + local namewhat = (info.namewhat == "" and "chunk at" or info.namewhat) + local name = (info.name and "'"..info.name.."'" or format_loc(source, info.linedefined)) + return format_loc(source, info.currentline).." in "..namewhat.." "..name + end + + offset = offset + 1 -- add this function to the offset + local t = { } + local i = 0 + while true do + local info = debug.getinfo(offset + i) + if not info then break end + t[i] = { + index = i, + current = (i == stack_inspect_offset), + desc = format_stack_frame_info(info), + info = info, + } + i = i + 1 + end + + return t +end + +local inHook = false + +local function hook() + local info = debug.getinfo(2) + if info.currentline < 0 then + return + end + if not inHook and hookEval(info) then + inHook = true + + local offset = 2 -- the offset from this function to the code being debugged + local inspectOffset = 0 + + repeat + local done = true + local snapshot = { + info = debug.getinfo(offset + inspectOffset), + locals = local_bindings(offset, inspectOffset), + stack = get_trace(offset, inspectOffset), + } + + inspectOffset = 0 -- reset + + local cmd, param = dbg.read(snapshot) + if cmd == 's' then + hookEval = hookStep() + elseif cmd == 'n' then + hookEval = hookStepOver() + elseif cmd == 'f' then + hookEval = hookStepOut() + elseif cmd == 'c' then + hookEval = hookBreakpoint + elseif cmd == 'd' then -- detach + debug.sethook() + elseif cmd == 'q' then + os.exit(0) + elseif cmd == 'b' then + dbg.breakpoints = param + done = false + elseif cmd == 'i' then + -- inspect stack at this offset + inspectOffset = param + done = false + end + until done + + inHook = false + end +end + +debug.sethook(hook, 'l') + +-- Expose the debugger's functions +dbg.hook = hook +dbg.exit = function(err) os.exit(err) end +dbg.stopIn = function(fn) + hookEval = hookFunction(fn) +end +dbg.debugger = nil + +dbg.read = function(info) + _G._pinfo = info + + os.sleep(0) -- this is important ... + dbg.debugger:resume('debugger', 'info', info) + + while true do + local _, cmd, args = os.pullEvent('debugger') + if cmd == 'b' then + dbg.breakpoints = args + else + return cmd, args + end + end +end + +return dbg diff --git a/debugger/debug.lua b/debugger/debug.lua new file mode 100644 index 0000000..1e2cdcd --- /dev/null +++ b/debugger/debug.lua @@ -0,0 +1,407 @@ +local Config = require('opus.config') +local Event = require('opus.event') +local UI = require('opus.ui') +local Util = require('opus.util') + +local fs = _G.fs +local getfenv = _G.getfenv +local kernel = _G.kernel +local multishell = _ENV.multishell +local shell = _ENV.shell + +local args = { ... } +local filename = shell.resolveProgram(table.remove(args, 1)) + +if not filename then + error('file not found') +end + +local config = Config.load('debugger') +if not config.filename then + config.filename = { } +end + +local breakpoints = config.filename +local currentFile +local debugFile, debugLine + +local debugger = kernel.getCurrent() +local client + +local function startClient() + local env = kernel.makeEnv(_ENV) + + local clientId = multishell.openTab(nil, { + env = env, + title = fs.getName(filename):match('([^%.]+)'), + args = args, + fn = function(...) + local dbg = require('debugger') + local fn = loadfile(filename, env) + + local cocreate = coroutine.create + env.coroutine = require('opus.util').shallowCopy(coroutine) + env.coroutine.create = function(f, ...) + local co = cocreate(f, ...) + debug.sethook(co, dbg.hook, "l") + return co + end + + dbg.debugger = debugger + dbg.breakpoints = breakpoints + dbg.stopIn(fn) + fn(...) + end, + }) + client = kernel.find(clientId) +end + +local function loadSource(file) + currentFile = file:match('@?(.*)') + local src = { } + local lines = Util.readLines(file:match('@?(.*)')) + + if lines then + for i = 1, #lines do + table.insert(src, { line = i, source = lines[i] }) + end + end + + return src +end + +local function message(...) + client:resume('debugger', ...) +end + +local page = UI.Page { + menuBar = UI.MenuBar { + buttons = { + { text = 'Continue', event = 'cmd', cmd = 'c' }, + { text = 'Step', event = 'cmd', cmd = 's' }, + { text = 'Step Over', event = 'cmd', cmd = 'n' }, + { text = 'Step Out', event = 'cmd', cmd = 'f' }, + { text = 'Restart', event = 'restart', width = 9, ex = -1 }, + }, + }, + + container = UI.Window { + y = 2, ey = '50%', + locals = UI.ScrollingGrid { + ey = -2, + disableHeader = true, + columns = { + { heading = 'Key', key = 'name' }, + { heading = 'Value', key = 'value', textColor = 'yellow' }, + }, + --sortColumn = 'name', + autospace = true, + accelerators = { + grid_select = 'show_variable', + }, + }, + statusBar = UI.StatusBar { + ex = -7, y = -1, + backgroundColor = 'primary', + textColor = 'white', + }, + UI.Button { + y = -1, x = -6, + event = 'open', + text = 'Open', + } + }, + + tabs = UI.Tabs { + y = '50%', + source = UI.Tab { + title = 'Source', + index = 1, + grid = UI.ScrollingGrid { + disableHeader = true, + columns = { + { key = 'marker', width = 1 }, + { key = 'line', textColor = 'cyan', width = 4 }, + { heading = 'heading', key = 'source' }, + }, + getDisplayValues = function(_, row) + for _,v in pairs(breakpoints) do + if v.file == currentFile and v.line == row.line then + return { + marker = '!', + line = row.line, + source = row.source, + } + end + end + return row + end, + getRowTextColor = function(self, row, selected) + return row.line == debugLine and currentFile == debugFile and 'yellow' + or UI.Grid.getRowTextColor(self, row, selected) + end, + eventHandler = function(self, event) + if event.type == 'grid_select' then + self:emit({ + type = 'toggle_breakpoint', + file = currentFile, + line = event.selected.line, + }) + end + return UI.Grid.eventHandler(self, event) + end, + }, + }, + + stack = UI.Tab { + title = 'Stack', + index = 3, + grid = UI.ScrollingGrid { + columns = { + { key = 'index', width = 2 }, + { heading = 'heading', key = 'desc' }, + }, + getRowTextColor = function(self, row, selected) + return row.current and 'yellow' + or UI.Grid.getRowTextColor(self, row, selected) + end, + sortColumn = 'index', + }, + eventHandler = function(self, event) + if event.type == 'grid_select' then + message('r', event.selected.index) + else + return UI.Grid.eventHandler(self, event) + end + end, + }, + + env = UI.Tab { + title = 'Env', + index = 4, + grid = UI.ScrollingGrid { + columns = { + { heading = 'Key', key = 'name' }, + { heading = 'Value', key = 'value', textColor = 'yellow' }, + }, + autospace = true, + accelerators = { + grid_select = 'show_variable', + }, + sortCompare = function() end, + }, + }, + + breaks = UI.Tab { + title = 'Breakpoints', + index = 2, + menuBar = UI.MenuBar { + buttons = { + { text = 'Toggle', event = 'toggle' }, + { text = 'Remove', event = 'remove' }, + { text = 'Clear', event = 'clear' }, + }, + }, + grid = UI.ScrollingGrid { + y = 2, + columns = { + { heading = 'Line', key = 'line', width = 5 }, + { heading = 'Name', key = 'short' }, + { heading = 'Path', key = 'path', textColor = 'lightGray' }, + }, + values = breakpoints, + autospace = true, + getRowTextColor = function(self, row, selected) + return row.disabled and 'lightGray' + or UI.Grid.getRowTextColor(self, row, selected) + end, + }, + eventHandler = function(self, event) + if event.type == 'clear' then + Util.clear(self.grid.values) + self:emit({ type = 'update_breakpoints' }) + + elseif event.type == 'toggle' then + local bp = self.grid:getSelected() + if bp then + bp.disabled = not bp.disabled + self:emit({ type = 'update_breakpoints' }) + end + + elseif event.type == 'grid_select' then + self:emit({ + type = 'open_file', + file = event.selected.file, + line = event.selected.line, + }) + + elseif event.type == 'remove' then + local bp = self.grid:getSelected() + if bp then + Util.removeByValue(self.grid.values, bp) + self:emit({ type = 'update_breakpoints' }) + end + + end + return UI.Tab.eventHandler(self, event) + end, + }, + }, + + quick_open = UI.QuickSelect { + modal = true, + enable = function() end, + show = function(self) + UI.QuickSelect.enable(self) + self:focusFirst() + self:draw() + self:addTransition('expandUp', { easing = 'outBounce', ticks = 12 }) + end, + eventHandler = function(self, event) + if event.type == 'select_cancel' then + self:disable() + elseif event.type == 'select_file' then + self.parent:openFile(event.file) + self:disable() + end + return UI.QuickSelect.eventHandler(self, event) + end, + }, + + openFile = function(self, file, line) + if file ~= currentFile then + local src = loadSource(file) + self.tabs.source.grid:setValues(src) + end + if line then + self.tabs.source.grid:setIndex(#self.tabs.source.grid.values) + self.tabs.source.grid:setIndex(math.max(1, line - 4)) + end + self.tabs.source.grid:setIndex(line or 1) + self.tabs:selectTab(self.tabs.source) + + if currentFile == debugFile then + self.container.statusBar:setStatus( + string.format('%s : %d', fs.getName(file), debugLine)) + else + self.container.statusBar:setStatus(fs.getName(file)) + end + self:draw() + end, + eventHandler = function(self, event) + if event.type == 'cmd' then + self.container.statusBar:setStatus('Running...') + message(event.element.cmd) + + elseif event.type == 'restart' then + if kernel.find(client.uid) then + client:resume('terminate') + end + startClient() + + elseif event.type == 'open' then + self.quick_open:show() + + elseif event.type == 'open_file' then + self:openFile(event.file, event.line) + + elseif event.type == 'update_breakpoints' then + self.tabs.breaks.grid:update() + self.tabs.breaks.grid:draw() + self.tabs.source.grid:draw() + message('b', breakpoints) + Config.update('debugger', config) + + elseif event.type == 'toggle_breakpoint' then + for k,v in pairs(breakpoints) do + if v.file == event.file and v.line == event.line then + table.remove(breakpoints, k) + self:emit({ type = 'update_breakpoints' }) + return + end + end + + table.insert(breakpoints, { + file = event.file, + line = event.line, + short = fs.getName(event.file), + path = fs.getDir(event.file), + }) + + self:emit({ type = 'update_breakpoints' }) + + elseif event.type == 'show_variable' then + if type(event.selected.raw) == 'table' then + if event.selected.children then + event.selected.children = nil + else + event.selected.children = { } + local t = event.selected.raw + for k,v in pairs(t) do + local depth = event.selected.depth or 0 + table.insert(event.selected.children, + { name = (' '):rep(depth + 2) .. k, value = tostring(v), raw = v, depth = depth + 2 }) + end + table.sort(event.selected.children, function(a, b) return a.name < b.name end) + end + local t = { } + local function insert(values) + for _,v in pairs(values) do + table.insert(t, v) + if v.children then + insert(v.children) + end + end + end + insert(event.element.orig) + event.element:setValues(t) + event.element:draw() + end + end + return UI.Page.eventHandler(self, event) + end, + enable = function(self) + UI.Page.enable(self) + startClient() + end, +} + +Event.on('debugger', function(_, cmd, data) + if cmd == 'info' then + kernel.raise(debugger.uid) + + -- local tab + local t = { } + for k,v in pairs(data.locals or { }) do + table.insert(t, { name = k, value = tostring(v), raw = v }) + end + table.sort(t, function(a, b) return a.name < b.name end) + page.container.locals:setValues(t) + page.container.locals.orig = Util.shallowCopy(t) + + -- env tab + t = { } + for k,v in pairs(getfenv(data.info.func)) do + table.insert(t, { name = k, value = tostring(v), raw = v }) + end + page.tabs.env.grid:setValues(t) + page.tabs.env.grid.orig = Util.shallowCopy(t) + + debugLine = data.info.currentline + debugFile = data.info.source:match('@?(.*)') + + -- source tab + page:openFile(debugFile, debugLine) + + -- stack + page.tabs.stack.grid:setValues(data.stack) + + page:draw() + page:sync() + end +end) + +UI:setPage(page) +UI:start() + +message('d') diff --git a/debugger/example.lua b/debugger/example.lua new file mode 100644 index 0000000..7743808 --- /dev/null +++ b/debugger/example.lua @@ -0,0 +1,28 @@ +local function m2(a) + return a +end + +local function method(times) + local a = 2 + -- use step out to return out of method + for _ = 1, times do + a = a * a + end + return m2(a) +end + +print('before') +-- breakpoint +--dbg() +print('after') + +local i = 2 +print(i) +local res = method(i) + +dofile("rom/modules/main/cc/expect.lua") + +print(res) +print('result: ' .. res) + +error('f') diff --git a/lzwfs/lzwfs.lua b/lzwfs/lzwfs.lua index 382bd7a..bad922a 100644 --- a/lzwfs/lzwfs.lua +++ b/lzwfs/lzwfs.lua @@ -19,9 +19,9 @@ local SIGC = 'LZWC' local basedictcompress = {} local basedictdecompress = {} for i = 0, 255 do - local ic, iic = char(i), char(i, 0) - basedictcompress[ic] = iic - basedictdecompress[iic] = ic + local ic, iic = char(i), char(i, 0) + basedictcompress[ic] = iic + basedictdecompress[iic] = ic end local native = { open = fs.open } @@ -29,123 +29,123 @@ local enabled = false local filters = { } local function dictAddA(str, dict, a, b) - if a >= 256 then - a, b = 0, b+1 - if b >= 256 then - dict = {} - b = 1 - end - end - dict[str] = char(a,b) - a = a+1 - return dict, a, b + if a >= 256 then + a, b = 0, b+1 + if b >= 256 then + dict = {} + b = 1 + end + end + dict[str] = char(a,b) + a = a+1 + return dict, a, b end local function compress(input) - if type(input) ~= "string" then - error ("string expected, got "..type(input)) - end - local len = #input - if len <= 1 then - return input - end + if type(input) ~= "string" then + error ("string expected, got "..type(input)) + end + local len = #input + if len <= 1 then + return input + end - local dict = {} - local a, b = 0, 1 + local dict = {} + local a, b = 0, 1 - local result = { SIGC } - local resultlen = 1 - local n = 2 - local word = "" - for i = 1, len do - local c = sub(input, i, i) - local wc = word..c - if not (basedictcompress[wc] or dict[wc]) then - local write = basedictcompress[word] or dict[word] - if not write then - error "algorithm error, could not fetch word" - end - result[n] = write - resultlen = resultlen + #write - n = n+1 - if len <= resultlen then - return input - end - dict, a, b = dictAddA(wc, dict, a, b) - word = c - else - word = wc - end - end - result[n] = basedictcompress[word] or dict[word] - resultlen = resultlen+#result[n] - if len <= resultlen then - return input - end - return tconcat(result) + local result = { SIGC } + local resultlen = 1 + local n = 2 + local word = "" + for i = 1, len do + local c = sub(input, i, i) + local wc = word..c + if not (basedictcompress[wc] or dict[wc]) then + local write = basedictcompress[word] or dict[word] + if not write then + error "algorithm error, could not fetch word" + end + result[n] = write + resultlen = resultlen + #write + n = n+1 + if len <= resultlen then + return input + end + dict, a, b = dictAddA(wc, dict, a, b) + word = c + else + word = wc + end + end + result[n] = basedictcompress[word] or dict[word] + resultlen = resultlen+#result[n] + if len <= resultlen then + return input + end + return tconcat(result) end local function dictAddB(str, dict, a, b) - if a >= 256 then - a, b = 0, b+1 - if b >= 256 then - dict = {} - b = 1 - end - end - dict[char(a,b)] = str - a = a+1 - return dict, a, b + if a >= 256 then + a, b = 0, b+1 + if b >= 256 then + dict = {} + b = 1 + end + end + dict[char(a,b)] = str + a = a+1 + return dict, a, b end local function decompress(input) - if type(input) ~= "string" then - error( "string expected, got "..type(input)) - end + if type(input) ~= "string" then + error( "string expected, got "..type(input)) + end - if #input <= 1 then - return input - end + if #input <= 1 then + return input + end - local control = sub(input, 1, 4) - if control ~= SIGC then - return input - end - input = sub(input, 5) - local len = #input + local control = sub(input, 1, 4) + if control ~= SIGC then + return input + end + input = sub(input, 5) + local len = #input - if len < 2 then - error("invalid input - not a compressed string") - end + if len < 2 then + error("invalid input - not a compressed string") + end - local dict = {} - local a, b = 0, 1 + local dict = {} + local a, b = 0, 1 - local result = {} - local n = 1 - local last = sub(input, 1, 2) - result[n] = basedictdecompress[last] or dict[last] - n = n+1 - for i = 3, len, 2 do - local code = sub(input, i, i+1) - local lastStr = basedictdecompress[last] or dict[last] - if not lastStr then - error( "could not find last from dict. Invalid input?") - end - local toAdd = basedictdecompress[code] or dict[code] - if toAdd then - result[n] = toAdd - n = n+1 - dict, a, b = dictAddB(lastStr..sub(toAdd, 1, 1), dict, a, b) - else - local tmp = lastStr..sub(lastStr, 1, 1) - result[n] = tmp - n = n+1 - dict, a, b = dictAddB(tmp, dict, a, b) - end - last = code - end - return tconcat(result) + local result = {} + local n = 1 + local last = sub(input, 1, 2) + result[n] = basedictdecompress[last] or dict[last] + n = n+1 + for i = 3, len, 2 do + local code = sub(input, i, i+1) + local lastStr = basedictdecompress[last] or dict[last] + if not lastStr then + error( "could not find last from dict. Invalid input?") + end + local toAdd = basedictdecompress[code] or dict[code] + if toAdd then + result[n] = toAdd + n = n+1 + dict, a, b = dictAddB(lastStr..sub(toAdd, 1, 1), dict, a, b) + else + local tmp = lastStr..sub(lastStr, 1, 1) + result[n] = tmp + n = n+1 + dict, a, b = dictAddB(tmp, dict, a, b) + end + last = code + end + return tconcat(result) end local function split(str, pattern) @@ -157,39 +157,39 @@ local function split(str, pattern) end local function matchesFilter(fname) - if not fname:find('lzwfs') then -- don't compress anything with lzwfs in name (sigh) - for _, filter in pairs(filters) do - if fname:match(filter) then - return true - end - end - end + if not fname:find('lzwfs') then -- don't compress anything with lzwfs in name (sigh) + for _, filter in pairs(filters) do + if fname:match(filter) then + return true + end + end + end end function fs.open(fname, flags) - if not enabled then - return native.open(fname, flags) - end + if not enabled then + return native.open(fname, flags) + end - if flags == 'r' then - local f, err = native.open(fname, 'rb') - if not f then - return f, err - end + if flags == 'r' then + local f, err = native.open(fname, 'rb') + if not f then + return f, err + end local ctr = 0 local lines return { - read = function() - if not lines then - lines = decompress(f.readAll()) - end - ctr = ctr + 1 - return lines:sub(ctr, ctr) - end, - readLine = function() + read = function() if not lines then - lines = split(decompress(f.readAll())) + lines = decompress(f.readAll()) + end + ctr = ctr + 1 + return lines:sub(ctr, ctr) + end, + readLine = function() + if not lines then + lines = split(decompress(f.readAll())) end ctr = ctr + 1 return lines[ctr] @@ -197,61 +197,61 @@ function fs.open(fname, flags) readAll = function() return decompress(f.readAll()) end, - close = function() - f.close() - end, - } - elseif flags == 'w' or flags == 'a' then - if not matchesFilter(fs.combine(fname, '')) then - return native.open(fname, flags) - end - - local c = { } - - if flags == 'a' then - local f = fs.open(fname, 'r') - if f then - tinsert(c, f.readAll()) - f.close() - end - end - - local f, err = native.open(fname, 'wb') - if not f then - return f, err - end - - return { - write = function(str) - tinsert(c, str) - end, - writeLine = function(str) - tinsert(c, str) - tinsert(c, '\n') - end, - flush = function() - -- this isn't gonna work... - -- f.write(compress(tconcat(c))) - f.flush(); - end, - close = function() - f.write(compress(tconcat(c))) - f.close() + close = function() + f.close() end, } - end + elseif flags == 'w' or flags == 'a' then + if not matchesFilter(fs.combine(fname, '')) then + return native.open(fname, flags) + end - return native.open(fname, flags) + local c = { } + + if flags == 'a' then + local f = fs.open(fname, 'r') + if f then + tinsert(c, f.readAll()) + f.close() + end + end + + local f, err = native.open(fname, 'wb') + if not f then + return f, err + end + + return { + write = function(str) + tinsert(c, str) + end, + writeLine = function(str) + tinsert(c, str) + tinsert(c, '\n') + end, + flush = function() + -- this isn't gonna work... + -- f.write(compress(tconcat(c))) + f.flush(); + end, + close = function() + f.write(compress(tconcat(c))) + f.close() + end, + } + end + + return native.open(fname, flags) end function fs.option(category, action, option) - if category == 'compression' then - if action == 'enabled' then - enabled = option - elseif action == 'filters' then - filters = option - end - end + if category == 'compression' then + if action == 'enabled' then + enabled = option + elseif action == 'filters' then + filters = option + end + end end print('lzwfs started') diff --git a/lzwfs/startup.lua b/lzwfs/startup.lua index 03a21c6..e8fa675 100644 --- a/lzwfs/startup.lua +++ b/lzwfs/startup.lua @@ -6,11 +6,11 @@ local CONFIG = 'usr/config/lzwfs' local config = { } if fs.exists(CONFIG) then - local f = fs.open(CONFIG, 'r') + local f = fs.open(CONFIG, 'r') if f then config = textutils.unserialize(f.readAll()) f.close() - end + end end os.run(_ENV, '/packages/lzwfs/lzwfs.lua') diff --git a/mbs/autorun/startup.lua b/mbs/autorun/startup.lua index 8ff9e9a..098c671 100644 --- a/mbs/autorun/startup.lua +++ b/mbs/autorun/startup.lua @@ -3,7 +3,7 @@ local shell = _ENV.shell if not fs.exists('.mbs') then print('Installing MBS') - shell.run('mbs download') + --shell.run('mbs download') end print('Initializing MBS') -shell.run('mbs startup') +--shell.run('mbs startup') diff --git a/minify/minifyDir.lua b/minify/minifyDir.lua index 8aa6932..4b307e9 100644 --- a/minify/minifyDir.lua +++ b/minify/minifyDir.lua @@ -2,24 +2,24 @@ local fs = _G.fs local shell = _ENV.shell local function recurse(path) - if fs.isDir(path) then - for _, v in pairs(fs.listEx(path)) do - if not v.isReadOnly then - recurse(fs.combine(path, v.name)) - end - end - elseif path:match('%.lua$') and not fs.isReadOnly(path) then - local sz = fs.getSize(path) - shell.run('minify.lua minify ' .. path) - print(string.format('%s : %.2f%%', path, (sz - fs.getSize(path)) / sz * 100)) - end + if fs.isDir(path) then + for _, v in pairs(fs.listEx(path)) do + if not v.isReadOnly then + recurse(fs.combine(path, v.name)) + end + end + elseif path:match('%.lua$') and not fs.isReadOnly(path) then + local sz = fs.getSize(path) + shell.run('minify.lua minify ' .. path) + print(string.format('%s : %.2f%%', path, (sz - fs.getSize(path)) / sz * 100)) + end end local path = ({ ... })[1] or error('Syntax: minifyDir PATH') path = fs.combine(path, '') if not fs.isDir(path) then - error('Invalid path') + error('Invalid path') end recurse(path) diff --git a/neural/Equipment.lua b/neural/Equipment.lua index 32b849e..df782bd 100644 --- a/neural/Equipment.lua +++ b/neural/Equipment.lua @@ -16,12 +16,12 @@ UI:configure('Equipment', ...) local equipment = device.neuralInterface.getEquipment() local slots = { - 'primary', - 'offhand', - 'boots', - 'leggings', - 'chest', - 'helmet', + 'primary', + 'offhand', + 'boots', + 'leggings', + 'chest', + 'helmet', } local page = UI.Page { @@ -34,29 +34,29 @@ local page = UI.Page { grid = UI.Grid { y = 2, columns = { - { heading = 'Slot', key = 'index', width = 7 }, + { heading = 'Slot', key = 'index', width = 7 }, { heading = 'Name', key = 'displayName' }, { heading = 'Count', key = 'count', width = 5, align = 'right' }, }, - sortColumn = 'index', - accelerators = { - grid_select = 'show_detail', - }, - getDisplayValues = function(_, row) - row = Util.shallowCopy(row) - if row.name then - local item = itemDB:get( - table.concat({ row.name, row.damage, row.nbtHash }, ':'), - function() - return equipment.getItemMeta(row.index) - end) - row.displayName = item.displayName - else - row.displayName = 'empty' - end - row.index = slots[row.index] - return row - end, + sortColumn = 'index', + accelerators = { + grid_select = 'show_detail', + }, + getDisplayValues = function(_, row) + row = Util.shallowCopy(row) + if row.name then + local item = itemDB:get( + table.concat({ row.name, row.damage, row.nbtHash }, ':'), + function() + return equipment.getItemMeta(row.index) + end) + row.displayName = item.displayName + else + row.displayName = 'empty' + end + row.index = slots[row.index] + return row + end, }, accelerators = { [ 'control-q' ] = 'quit', @@ -77,58 +77,58 @@ local page = UI.Page { accelerators = { grid_select = 'inspect', }, - }, - show = function(self, slot) - local detail = equipment.getItemMeta(slot.index) - local t = { } - for k,v in pairs(detail) do - table.insert(t, { - name = k, - value = v, - }) - end - self.grid:setValues(t) - self.grid:setIndex(1) - UI.SlideOut.show(self) - end, - }, - enable = function(self) - self:refresh() - UI.Page.enable(self) - end, - refresh = function(self) - local t = { } - local list = equipment.list() - for i = 1, equipment.size() do - local v = list[i] or { } - v.index = i - table.insert(t, v) - end - self.grid:setValues(t) - self.grid:draw() - end, - eventHandler = function(self, event) - if event.type == 'quit' then - UI:quit() + }, + show = function(self, slot) + local detail = equipment.getItemMeta(slot.index) + local t = { } + for k,v in pairs(detail) do + table.insert(t, { + name = k, + value = v, + }) + end + self.grid:setValues(t) + self.grid:setIndex(1) + UI.SlideOut.show(self) + end, + }, + enable = function(self) + self:refresh() + UI.Page.enable(self) + end, + refresh = function(self) + local t = { } + local list = equipment.list() + for i = 1, equipment.size() do + local v = list[i] or { } + v.index = i + table.insert(t, v) + end + self.grid:setValues(t) + self.grid:draw() + end, + eventHandler = function(self, event) + if event.type == 'quit' then + UI:quit() - elseif event.type == 'show_detail' then - if event.selected.name then - self.detail:show(event.selected) - end + elseif event.type == 'show_detail' then + if event.selected.name then + self.detail:show(event.selected) + end - elseif event.type == 'drop' then - local selected = self.grid:getSelected() - equipment.drop(selected.index) - self:refresh() + elseif event.type == 'drop' then + local selected = self.grid:getSelected() + equipment.drop(selected.index) + self:refresh() - elseif event.type == 'suck' then - local selected = self.grid:getSelected() - equipment.suck(selected.index) - self:refresh() - end + elseif event.type == 'suck' then + local selected = self.grid:getSelected() + equipment.suck(selected.index) + self:refresh() + end - UI.Page.eventHandler(self, event) - end, + UI.Page.eventHandler(self, event) + end, } Event.onInterval(1, function() diff --git a/packages.list b/packages.list index b674e1a..41bbf6a 100644 --- a/packages.list +++ b/packages.list @@ -4,6 +4,7 @@ [ 'ccemux' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/ccemux/.package', [ 'common' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/common/.package', [ 'core' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/core/.package', + [ 'debugger' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/debugger/.package', [ 'farms' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/farms/.package', -- [ 'forestry' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/forestry/.package', [ 'games' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/games/.package', @@ -11,7 +12,7 @@ [ 'gps' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/gps/.package', [ 'lfs' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/lfs/.package', [ 'lzwfs' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/lzwfs/.package', - [ 'mbs' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/mbs/.package', +-- [ 'mbs' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/mbs/.package', [ 'milo' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/milo/.package', [ 'miloApps' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/miloApps/.package', [ 'miners' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/miners/.package', -- 2.49.1 From f07302588cb53fe6553d32cba5d727610fe22d84 Mon Sep 17 00:00:00 2001 From: cynagen <43963132+cynagen@users.noreply.github.com> Date: Sat, 23 May 2020 21:09:22 -0700 Subject: [PATCH 53/90] Add furniController.lua (#37) * Update exportTask.lua Cheap fix to stop cascading storage updates from eating up all the working time available after failing exports, this needs to be more fleshed out better, if the chest is full, scan the slots for any that match and are not full, stop blindly exporting * Create furniController.lua (Super)MultiFurnace Controller (did I mention, it's SUPER!?) This app is designed to wrap up to 4 of kepler155c's MultiFurnace (multiFurni) arrays and divide up workload, it functions as a straight drop-in for furni.lua and no other adjustments are required to make it work. Additionally, you get the ability to target any one multiFurni array for smelting when creating the machine recipe in Milo. Further uses for this direct targeting can be limited-scope auto-smelting, using anywhere from 1/2 to 1/4 of your total capacity to auto-smelt, leaving the rest of the cluster available for you when you want it, great for multi-user bases! Setup: Plug this turtle into a modem on your Milo network, and another modem leading to 2-4 turtles running furni.lua (or even furniController.lua for chained setups), configure useSlot4 on line 20, map in Milo and go! * Update furniController.lua Cleaned up bad reference naming to avoid confusion. Additionally moved relevant variables to local scope, cleaned up a few other minor things. * Update exportTask.lua Fleshed out the exportItems function which was blindly firing items into full chests. Made it a little more sensitive, slight increase in average processing time, but no more erroneous hits to export causing perf issues. * Update exportTask.lua Clean up cached .list() after done, accidentally left it in, bloating storage config sizes * Update exportTask.lua Fixing some logic, make less plethora calls * Update exportTask.lua * Update exportTask.lua Last fix, was making a call to Util when it wasn't defined, and left the export function completely dead. Defined correctly, now exporting as expected, whoops. * Update exportTask.lua So uhh, yeah, the looped getItemMeta calls cost me a LOT of time. Figured out most of the relevant data I wanted was already being provided elsewhere, no extra calls to make, it's as fast as I can make it now. Now it's dependent on the storage optimizations as they stand, this does not speed anything up, just makes it more accurate. * Sync with kepler155c's branch * Another resync --- miloApps/apps/furniController.lua | 174 ++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 miloApps/apps/furniController.lua diff --git a/miloApps/apps/furniController.lua b/miloApps/apps/furniController.lua new file mode 100644 index 0000000..d6ad9ec --- /dev/null +++ b/miloApps/apps/furniController.lua @@ -0,0 +1,174 @@ +-- (Super)MultiFurnace Controller (did I mention, it's SUPER!?) +-- This app is designed to wrap up to 4 of kepler155c's furni.lua (multiFurni) arrays and divide up workload, +-- it functions as a straight drop-in for furni.lua and no other adjustments are required to make it work. +-- Additionally, you get the ability to target any one multiFurni array for smelting when creating the machine recipe in Milo +-- Further uses for this direct targeting can be limited-scope auto-smelting, using anywhere from 1/2 to 1/4 of your total +-- capacity to auto-smelt, leaving the rest of the cluster available for you when you want it, great for multi-user bases! + +-- Plug this turtle into a modem on your Milo network, and another modem leading to 2-4 turtles running furni.lua (or even furniController.lua for chained setups) + +-- Slots are as follows: +-- 1: Master Furnace Input +-- 2: Master Furnace Fuel Input +-- 3: Master Furnace Output +-- 4: Optional (Options: "fuel"/"input"/"output"/nil (Default: nil [ignore])) +-- 5-8: Furnace Inputs for 2 to 4 furni.lua arrays (slots mapped in order ascending, T1_Input=4, T2_Input=5, etc) +-- 9-12: Furnace Fuels for 2 to 4 furni.lua arrays +-- 13-16: Furnace Outputs for 2 to 4 furni.lua arrays + +-- Only user option, slot 4, what to use it for? +local useSlot4=nil + +-- Begin main program code +-- Find modem with -only- turtles attached (these should be our furni/furniController arrays) +local devModem="" +for nDev,modem in ipairs({peripheral.find("modem")}) do + if #modem.getNamesRemote() > 1 then -- We're only looking for 2 or more devices on a network segment, ignore anything less + nTurtles=0 + for nDev,dev in ipairs(modem.getNamesRemote()) do + if modem.getTypeRemote(dev) == "turtle" then nTurtles=nTurtles+1 end + end + if nTurtles==#modem.getNamesRemote() then + devModem=modem + break + end + end +end +if devModem ~= "" then + print("Found turtle-only modem, detected",#devModem.getNamesRemote(),"turtles",(#devModem.getNamesRemote() > 4 and "(Max 4 will be mapped)" or "(All will be mapped)")) +else + print("Did not find a modem with only turtles connected. Please check your networking cables/modems and devices, then try again.") + return -- We don't have a modem, halt +end +local devTurtles={} +for nDev,dev in ipairs(devModem.getNamesRemote()) do + if #devTurtles >= 4 then break end + devTurtles[#devTurtles+1]=peripheral.wrap(dev) + print("Mapping",devTurtles[#devTurtles].getLabel(),"("..dev..")","as device",#devTurtles) + devTurtles[#devTurtles].inputSlot=#devTurtles+4 -- Set the input for this device on second row + devTurtles[#devTurtles].fuelSlot=#devTurtles+8 -- Set the fuel for this device on third row + devTurtles[#devTurtles].outputSlot=#devTurtles+12 -- Set the output for this device on fourth row + if devTurtles[#devTurtles].isOn() then + print("Resynchronizing",devTurtles[#devTurtles].getLabel()) + devTurtles[#devTurtles].reboot() + else + print("Powering",devTurtles[#devTurtles].getLabel(),"on") + devTurtles[#devTurtles].turnOn() + end +end + +local turtle.list = function () -- Supplement a local .list()-like function + local tList={} + for i=1,16 do + local slotData=turtle.getItemDetail(i) -- DWGFJTLR + if slotData ~= nil then + slotData.maxStack=turtle.getItemCount(i) + turtle.getItemSpace(i) + tList[i]=slotData + end + end + return tList +end + +function manageLocalResources() + --print("Input found") + local itemCount=turtle.getItemCount(1)/#devTurtles + if itemCount < 1 then itemCount = 1 end -- Patch for single items fed into master slot + if turtle.getItemCount(1) > 0 then -- Input + for nDev,dev in ipairs(devTurtles) do + local devList=dev.list() + if turtle.select(1) and ((turtle.compareTo(dev.inputSlot) and turtle.getItemSpace(dev.inputSlot) > 0) or turtle.getItemCount(dev.inputSlot) == 0) then -- Move Input + turtle.transferTo(dev.inputSlot,itemCount) + end + if useSlot4 == "input" and turtle.select(4) and ((turtle.compareTo(dev.inputSlot) and turtle.getItemSpace(dev.inputSlot) > 0) or turtle.getItemCount(dev.inputSlot) == 0) then -- Move Input + turtle.transferTo(dev.inputSlot,itemCount) + end --Slot 4 input + end + end + if turtle.select(2) and turtle.getSelectedSlot() == 2 then -- Fuel + for nDev,dev in ipairs(devTurtles) do + local devList=dev.list() + if turtle.compareTo(dev.fuelSlot) or turtle.getItemCount(dev.fuelSlot) == 0 then turtle.transferTo(dev.fuelSlot,64) end -- Move fuel into slot + end + end + if useSlot4 == "fuel" and turtle.select(4) and turtle.getSelectedSlot() == 4 then -- Fuel + for nDev,dev in ipairs(devTurtles) do + local devList=dev.list() + if turtle.compareTo(dev.fuelSlot) or turtle.getItemCount(dev.fuelSlot) == 0 then turtle.transferTo(dev.fuelSlot,64) end -- Move fuel into slot + end + end -- Slot 4 fuel + + for nDev,dev in ipairs(devTurtles) do + local devList=dev.list() + if turtle.getItemCount(3) == 0 and turtle.getItemCount(dev.outputSlot) > 0 then + turtle.select(dev.outputSlot) + turtle.transferTo(3,64) + elseif turtle.getItemCount(dev.outputSlot) > 0 and turtle.select(3) and turtle.compareTo(dev.outputSlot) then -- Move output + --turtle.setStatus("moving") + turtle.select(dev.outputSlot) + turtle.transferTo(3,64) + end + if useSlot4 == "output" then -- Slot 4 output + if turtle.getItemCount(4) == 0 and turtle.getItemCount(dev.outputSlot) > 0 then + turtle.select(dev.outputSlot) + turtle.transferTo(4,64) + elseif turtle.getItemCount(dev.outputSlot) > 0 and turtle.select(4) and turtle.compareTo(dev.outputSlot) then + turtle.select(dev.outputSlot) + turtle.transferTo(4,64) + end + end + end +end + +function manageRemoteResources() + local localList=turtle.list() + for nDev,dev in ipairs(devTurtles) do + local devList=dev.list() + --print(textutils.serialize(devList)) + if devList[2] == nil then + if localList[dev.fuelSlot] ~= nil then + turtle.setStatus("fueling") + dev.pullItems(devModem.getNameLocal(),dev.fuelSlot,64,2) + end + else + if localList[dev.fuelSlot] ~= nil and localList[dev.fuelSlot].name == devList[2].name then + turtle.setStatus("fueling") + dev.pullItems(devModem.getNameLocal(),dev.fuelSlot,64,2) + end + end + if devList[1] == nil then + if localList[dev.inputSlot] ~= nil then + turtle.setStatus("pushing") + dev.pullItems(devModem.getNameLocal(),dev.inputSlot,64,1) + end + else + if localList[dev.inputSlot] ~= nil and localList[dev.inputSlot].name == devList[1].name then + turtle.setStatus("pushing") + dev.pullItems(devModem.getNameLocal(),dev.inputSlot,64,1) + end + end -- Move input + if devList[3] ~= nil then + if localList[dev.outputSlot] == nil then + turtle.setStatus("pulling") + dev.pushItems(devModem.getNameLocal(),3,64,dev.outputSlot) + end + else + if localList[dev.outputSlot] ~= nil then + if devList[3] ~= nil and localList[dev.outputSlot].name == devList[3].name then + turtle.setStatus("pulling") + dev.pushItems(devModem.getNameLocal(),3,64,dev.outputSlot) + end + end + end + end +end + +local lastList={} +while true do -- Main Loop + local turtleList=turtle.list() + if turtleList[1]~=nil or turtleList[13]~=nil or turtleList[14]~=nil or turtleList[15]~=nil or turtleList[16]~=nil or ((useSlot4=="input" or useSlot4=="fuel") and turtleList[4]~=nil) then + manageLocalResources() + end + manageRemoteResources() + turtle.setStatus("sleeping") + os.sleep(1) +end -- 2.49.1 From 26b693d6096914ad7cf3a7560ff0c8d3a3b9c32c Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Sun, 24 May 2020 15:18:18 -0600 Subject: [PATCH 54/90] debugger part 2 --- debugger/apis/init.lua | 43 +++++++++++---- debugger/autorun/startup.lua | 4 ++ debugger/debug.lua | 103 ++++++++++++++++++++++++----------- debugger/example.lua | 3 +- debugger/help/debug.txt | 6 ++ 5 files changed, 116 insertions(+), 43 deletions(-) create mode 100644 debugger/autorun/startup.lua create mode 100644 debugger/help/debug.txt diff --git a/debugger/apis/init.lua b/debugger/apis/init.lua index 61ed024..233d71b 100644 --- a/debugger/apis/init.lua +++ b/debugger/apis/init.lua @@ -9,7 +9,7 @@ local dbg = { } local function hookBreakpoint(info) if dbg.breakpoints then for _,v in pairs(dbg.breakpoints) do - if v.line == info.currentline and v.file == info.short_src then + if v.line == info.currentline and v.file == info.short_src and not v.disabled then return true end end @@ -24,8 +24,8 @@ end local function hookStep() local co = coroutine.running() - return function() - return co == coroutine.running() + return function(info) + return co == coroutine.running() or hookBreakpoint(info) end end @@ -39,12 +39,13 @@ local function hookStepStacksize(n) end i = i + 1 end - return function() + return function(info) if co == coroutine.running() then if not debug.getinfo(i - n) then return true end end + return hookBreakpoint(info) end end @@ -63,13 +64,13 @@ local hookEval = function() end local function local_bindings(offset, stack_inspect_offset) offset = offset + 1 + stack_inspect_offset -- add this function to the offset local func = debug.getinfo(offset).func - local bindings = {} + local bindings = { } -- Retrieve the upvalues do local i = 1; while true do local name, value = debug.getupvalue(func, i) if not name then break end - bindings[name] = value + bindings[name] = { type = 'U', raw = value } i = i + 1 end end @@ -77,21 +78,32 @@ local function local_bindings(offset, stack_inspect_offset) do local i = 1; while true do local name, value = debug.getlocal(offset, i) if not name then break end - bindings[name] = value + bindings[name] = { type = 'L', raw = value } i = i + 1 end end -- Retrieve the varargs (works in Lua 5.2 and LuaJIT) - local varargs = {} + local varargs = { } do local i = 1; while true do local name, value = debug.getlocal(offset, -i) if not name then break end varargs[i] = value i = i + 1 end end - if #varargs > 0 then bindings["..."] = varargs end + if #varargs > 0 then + bindings["..."] = { type = 'V', value = varargs } + end - return bindings + local t = { } + for k,v in pairs(bindings) do + if v.raw ~= nil then + v.name = k + v.value = tostring(v.raw) + table.insert(t, v) + end + end + + return t end local function get_trace(offset, stack_inspect_offset) @@ -172,6 +184,17 @@ local function hook() end end +local cocreate = coroutine.create +_ENV.coroutine = { } +for k,v in pairs(_G.coroutine) do + _ENV.coroutine[k] = v +end +_ENV.coroutine.create = function(f, ...) + local co = cocreate(f, ...) + debug.sethook(co, dbg.hook, "l") + return co +end + debug.sethook(hook, 'l') -- Expose the debugger's functions diff --git a/debugger/autorun/startup.lua b/debugger/autorun/startup.lua new file mode 100644 index 0000000..2f61f96 --- /dev/null +++ b/debugger/autorun/startup.lua @@ -0,0 +1,4 @@ +local completion = require('cc.shell.completion') + +_ENV.shell.setCompletionFunction("packages/debugger/debug.lua", + completion.build(completion.program)) diff --git a/debugger/debug.lua b/debugger/debug.lua index 1e2cdcd..71b71c4 100644 --- a/debugger/debug.lua +++ b/debugger/debug.lua @@ -16,12 +16,14 @@ if not filename then error('file not found') end +UI:disableEffects() + local config = Config.load('debugger') -if not config.filename then - config.filename = { } +if not config[filename] then + config[filename] = { } end -local breakpoints = config.filename +local breakpoints = config[filename] local currentFile local debugFile, debugLine @@ -37,14 +39,10 @@ local function startClient() args = args, fn = function(...) local dbg = require('debugger') - local fn = loadfile(filename, env) + local fn, msg = loadfile(filename, env) - local cocreate = coroutine.create - env.coroutine = require('opus.util').shallowCopy(coroutine) - env.coroutine.create = function(f, ...) - local co = cocreate(f, ...) - debug.sethook(co, dbg.hook, "l") - return co + if not fn then + error(msg, -1) end dbg.debugger = debugger @@ -56,10 +54,32 @@ local function startClient() client = kernel.find(clientId) end +local romFiles = { + load = function(self) + local function recurse(dir) + local files = fs.list(dir) + for _,f in ipairs(files) do + local fullName = fs.combine(dir, f) + if fs.isDir(fullName) then + recurse(fullName) + else + self.files[f] = fullName + end + end + end + recurse('rom/apis') + end, + get = function(self, file) + return self.files[file] + end, + files = { }, +} +romFiles:load() + local function loadSource(file) - currentFile = file:match('@?(.*)') + currentFile = romFiles:get(file) or file:match('@?(.*)') local src = { } - local lines = Util.readLines(file:match('@?(.*)')) + local lines = Util.readLines(currentFile) if lines then for i = 1, #lines do @@ -77,11 +97,11 @@ end local page = UI.Page { menuBar = UI.MenuBar { buttons = { - { text = 'Continue', event = 'cmd', cmd = 'c' }, - { text = 'Step', event = 'cmd', cmd = 's' }, - { text = 'Step Over', event = 'cmd', cmd = 'n' }, - { text = 'Step Out', event = 'cmd', cmd = 'f' }, - { text = 'Restart', event = 'restart', width = 9, ex = -1 }, + { text = 'Continue', event = 'cmd', cmd = 'c' }, + { text = 'Step', event = 'cmd', cmd = 's' }, + { text = 'Over', event = 'cmd', cmd = 'n' }, + { text = 'Out', event = 'cmd', cmd = 'f' }, + { text = 'Restart', event = 'restart', width = 9, ex = -1 }, }, }, @@ -94,11 +114,15 @@ local page = UI.Page { { heading = 'Key', key = 'name' }, { heading = 'Value', key = 'value', textColor = 'yellow' }, }, - --sortColumn = 'name', autospace = true, accelerators = { grid_select = 'show_variable', }, + getRowTextColor = function(self, row, selected) + return row.type == 'U' and 'cyan' + or row.type == 'V' and 'lime' + or UI.Grid.getRowTextColor(self, row, selected) + end, }, statusBar = UI.StatusBar { ex = -7, y = -1, @@ -128,7 +152,7 @@ local page = UI.Page { for _,v in pairs(breakpoints) do if v.file == currentFile and v.line == row.line then return { - marker = '!', + marker = v.disabled and 'x' or '!', line = row.line, source = row.source, } @@ -136,6 +160,9 @@ local page = UI.Page { end return row end, + accelerators = { + t = 'toggle_enabled' + }, getRowTextColor = function(self, row, selected) return row.line == debugLine and currentFile == debugFile and 'yellow' or UI.Grid.getRowTextColor(self, row, selected) @@ -147,6 +174,17 @@ local page = UI.Page { file = currentFile, line = event.selected.line, }) + elseif event.type == 'toggle_enabled' then + local line = self:getSelected() and self:getSelected().line + if line then + for _,v in pairs(breakpoints) do + if v.file == currentFile and v.line == line then + v.disabled = not v.disabled + self:emit({ type = 'update_breakpoints' }) + break + end + end + end end return UI.Grid.eventHandler(self, event) end, @@ -157,6 +195,7 @@ local page = UI.Page { title = 'Stack', index = 3, grid = UI.ScrollingGrid { + disableHeader = true, columns = { { key = 'index', width = 2 }, { heading = 'heading', key = 'desc' }, @@ -166,14 +205,14 @@ local page = UI.Page { or UI.Grid.getRowTextColor(self, row, selected) end, sortColumn = 'index', + eventHandler = function(self, event) + if event.type == 'grid_select' then + message('i', event.selected.index) + else + return UI.Grid.eventHandler(self, event) + end + end, }, - eventHandler = function(self, event) - if event.type == 'grid_select' then - message('r', event.selected.index) - else - return UI.Grid.eventHandler(self, event) - end - end, }, env = UI.Tab { @@ -371,10 +410,10 @@ Event.on('debugger', function(_, cmd, data) kernel.raise(debugger.uid) -- local tab - local t = { } - for k,v in pairs(data.locals or { }) do - table.insert(t, { name = k, value = tostring(v), raw = v }) - end + local t = data.locals +-- for k,v in pairs(data.locals or { }) do +-- table.insert(t, { name = k, value = tostring(v), raw = v }) +-- end table.sort(t, function(a, b) return a.name < b.name end) page.container.locals:setValues(t) page.container.locals.orig = Util.shallowCopy(t) @@ -404,4 +443,6 @@ end) UI:setPage(page) UI:start() -message('d') +if kernel.find(client.uid) then + client:resume('terminate') +end diff --git a/debugger/example.lua b/debugger/example.lua index 7743808..8df3d2b 100644 --- a/debugger/example.lua +++ b/debugger/example.lua @@ -12,8 +12,7 @@ local function method(times) end print('before') --- breakpoint ---dbg() +term.current().clear() print('after') local i = 2 diff --git a/debugger/help/debug.txt b/debugger/help/debug.txt new file mode 100644 index 0000000..0ae9756 --- /dev/null +++ b/debugger/help/debug.txt @@ -0,0 +1,6 @@ +An interactive debugger for lua + +debug must be enabled! + +Run from a shell prompt +> debug FILE [ARGS] -- 2.49.1 From 70001196cb9202a445a582915eb756debc269659 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Mon, 25 May 2020 21:49:17 -0600 Subject: [PATCH 55/90] debugger - error handling, ui rework --- debugger/apis/init.lua | 150 ++++++++------- debugger/debug.lua | 420 +++++++++++++++++++++-------------------- debugger/example.lua | 31 ++- 3 files changed, 319 insertions(+), 282 deletions(-) diff --git a/debugger/apis/init.lua b/debugger/apis/init.lua index 233d71b..fe9de22 100644 --- a/debugger/apis/init.lua +++ b/debugger/apis/init.lua @@ -1,35 +1,36 @@ ---[[ -some portions from https://github.com/slembcke/debugger.lua -]] +-- this code is loaded into the code being debugged +-- some portions from https://github.com/slembcke/debugger.lua local fs = _G.fs local dbg = { } -local function hookBreakpoint(info) +local function breakpointHook(info) if dbg.breakpoints then for _,v in pairs(dbg.breakpoints) do - if v.line == info.currentline and v.file == info.short_src and not v.disabled then - return true + if v.line == info.currentline and v.file == info.short_src then +print(v.line, not v.disabled) + return not v.disabled end end end end -local function hookFunction(fn) +local function functionHook(fn) return function(info) return info.func == fn end end -local function hookStep() +local function stepHook() local co = coroutine.running() return function(info) - return co == coroutine.running() or hookBreakpoint(info) + return co == coroutine.running() + or breakpointHook(info) end end -local function hookStepStacksize(n) +local function stackSizeHook(n) local co = coroutine.running() local i = 2 while true do @@ -40,27 +41,23 @@ local function hookStepStacksize(n) i = i + 1 end return function(info) - if co == coroutine.running() then - if not debug.getinfo(i - n) then - return true - end - end - return hookBreakpoint(info) + return co == coroutine.running() + and not debug.getinfo(i - n) + or breakpointHook(info) end end -local function hookStepOut() - return hookStepStacksize(1) +local function stepOutHook() + return stackSizeHook(1) end -local function hookStepOver() - return hookStepStacksize(0) +local function stepOverHook() + return stackSizeHook(0) end local hookEval = function() end -- Create a table of all the locally accessible variables. --- Globals are not included when running the locals command local function local_bindings(offset, stack_inspect_offset) offset = offset + 1 + stack_inspect_offset -- add this function to the offset local func = debug.getinfo(offset).func @@ -96,7 +93,7 @@ local function local_bindings(offset, stack_inspect_offset) local t = { } for k,v in pairs(bindings) do - if v.raw ~= nil then + if k ~= '(*temporary)' then v.name = k v.value = tostring(v.raw) table.insert(t, v) @@ -138,45 +135,44 @@ local inHook = false local function hook() local info = debug.getinfo(2) - if info.currentline < 0 then - return - end + if not inHook and hookEval(info) then inHook = true - local offset = 2 -- the offset from this function to the code being debugged local inspectOffset = 0 repeat local done = true - local snapshot = { - info = debug.getinfo(offset + inspectOffset), - locals = local_bindings(offset, inspectOffset), - stack = get_trace(offset, inspectOffset), - } + local snapshot = { + info = debug.getinfo(2 + inspectOffset), + locals = local_bindings(2, inspectOffset), + stack = get_trace(2, inspectOffset), + } inspectOffset = 0 -- reset - local cmd, param = dbg.read(snapshot) + os.queueEvent('debuggerX', dbg.debugger.uid, snapshot) + + local e, cmd, param + repeat + e, cmd, param = os.pullEvent('debugger') + until e == 'debugger' + if cmd == 's' then - hookEval = hookStep() + hookEval = stepHook() elseif cmd == 'n' then - hookEval = hookStepOver() + hookEval = stepOverHook() elseif cmd == 'f' then - hookEval = hookStepOut() + hookEval = stepOutHook() elseif cmd == 'c' then - hookEval = hookBreakpoint - elseif cmd == 'd' then -- detach - debug.sethook() - elseif cmd == 'q' then - os.exit(0) - elseif cmd == 'b' then - dbg.breakpoints = param - done = false + hookEval = breakpointHook elseif cmd == 'i' then - -- inspect stack at this offset + -- get snapshot of stack at this offset inspectOffset = param done = false + else + os.sleep(1) + done = false end until done @@ -184,41 +180,51 @@ local function hook() end end -local cocreate = coroutine.create -_ENV.coroutine = { } -for k,v in pairs(_G.coroutine) do - _ENV.coroutine[k] = v -end -_ENV.coroutine.create = function(f, ...) - local co = cocreate(f, ...) - debug.sethook(co, dbg.hook, "l") - return co +function dbg.call(f, ...) + local args = { ... } + return xpcall( + function() + f(table.unpack(args)) + end, + function(err) + hookEval = stepHook() + + -- An error has occurred + return err + end) end +_ENV.coroutine = setmetatable({ + + create = function(f) + local co = _G.coroutine.create(function(...) + local r = { dbg.call(f, ...) } + + if not r[1] then + error(r[2], -1) + end + + return table.unpack(r, 2) + end) + + debug.sethook(co, hook, 'l') + return co + end + --[[ + create = function(f) + local co = _G.coroutine.create(f) + debug.sethook(co, hook, 'l') + return co + end + ]] +}, { __index = coroutine }) + debug.sethook(hook, 'l') -- Expose the debugger's functions -dbg.hook = hook -dbg.exit = function(err) os.exit(err) end dbg.stopIn = function(fn) - hookEval = hookFunction(fn) + hookEval = functionHook(fn) end dbg.debugger = nil -dbg.read = function(info) - _G._pinfo = info - - os.sleep(0) -- this is important ... - dbg.debugger:resume('debugger', 'info', info) - - while true do - local _, cmd, args = os.pullEvent('debugger') - if cmd == 'b' then - dbg.breakpoints = args - else - return cmd, args - end - end -end - return dbg diff --git a/debugger/debug.lua b/debugger/debug.lua index 71b71c4..5f4f2a2 100644 --- a/debugger/debug.lua +++ b/debugger/debug.lua @@ -32,12 +32,13 @@ local client local function startClient() local env = kernel.makeEnv(_ENV) + currentFile = nil local clientId = multishell.openTab(nil, { env = env, title = fs.getName(filename):match('([^%.]+)'), args = args, - fn = function(...) + fn = function() local dbg = require('debugger') local fn, msg = loadfile(filename, env) @@ -45,10 +46,14 @@ local function startClient() error(msg, -1) end - dbg.debugger = debugger + -- breakpoint table is shared across processes dbg.breakpoints = breakpoints + dbg.debugger = debugger dbg.stopIn(fn) - fn(...) + local s, m = dbg.call(fn, table.unpack(args)) + if not s then + error(m, -1) + end end, }) client = kernel.find(clientId) @@ -79,7 +84,7 @@ romFiles:load() local function loadSource(file) currentFile = romFiles:get(file) or file:match('@?(.*)') local src = { } - local lines = Util.readLines(currentFile) + local lines = Util.readLines(currentFile) or type(file) == 'string' and Util.split(file) if lines then for i = 1, #lines do @@ -95,199 +100,210 @@ local function message(...) end local page = UI.Page { - menuBar = UI.MenuBar { - buttons = { - { text = 'Continue', event = 'cmd', cmd = 'c' }, - { text = 'Step', event = 'cmd', cmd = 's' }, - { text = 'Over', event = 'cmd', cmd = 'n' }, - { text = 'Out', event = 'cmd', cmd = 'f' }, - { text = 'Restart', event = 'restart', width = 9, ex = -1 }, - }, - }, + backgroundColor = 'black', container = UI.Window { - y = 2, ey = '50%', - locals = UI.ScrollingGrid { + y = 1, ey = '50%', + tabs = UI.Tabs { ey = -2, - disableHeader = true, - columns = { - { heading = 'Key', key = 'name' }, - { heading = 'Value', key = 'value', textColor = 'yellow' }, - }, - autospace = true, - accelerators = { - grid_select = 'show_variable', - }, - getRowTextColor = function(self, row, selected) - return row.type == 'U' and 'cyan' - or row.type == 'V' and 'lime' - or UI.Grid.getRowTextColor(self, row, selected) - end, - }, - statusBar = UI.StatusBar { - ex = -7, y = -1, - backgroundColor = 'primary', - textColor = 'white', - }, - UI.Button { - y = -1, x = -6, - event = 'open', - text = 'Open', - } - }, + unselectedBackgroundColor = 'black', - tabs = UI.Tabs { - y = '50%', - source = UI.Tab { - title = 'Source', - index = 1, - grid = UI.ScrollingGrid { - disableHeader = true, - columns = { - { key = 'marker', width = 1 }, - { key = 'line', textColor = 'cyan', width = 4 }, - { heading = 'heading', key = 'source' }, + locals = UI.Tab { + title = 'Locals', + index = 1, + grid = UI.ScrollingGrid { + disableHeader = true, + unfocusedBackgroundSelectedColor = 'black', + columns = { + { heading = 'Key', key = 'name' }, + { heading = 'Value', key = 'value', textColor = 'yellow' }, + }, + autospace = true, + accelerators = { + grid_select = 'show_variable', + }, + getRowTextColor = function(self, row, selected) + return row.type == 'U' and 'cyan' + or row.type == 'V' and 'lime' + or UI.Grid.getRowTextColor(self, row, selected) + end, }, - getDisplayValues = function(_, row) - for _,v in pairs(breakpoints) do - if v.file == currentFile and v.line == row.line then - return { - marker = v.disabled and 'x' or '!', - line = row.line, - source = row.source, - } + }, + + stack = UI.Tab { + title = 'Stack', + index = 3, + grid = UI.ScrollingGrid { + disableHeader = true, + sortColumn = 'index', + unfocusedBackgroundSelectedColor = 'black', + columns = { + { key = 'index', width = 2 }, + { heading = 'heading', key = 'desc' }, + }, + getRowTextColor = function(self, row, selected) + return row.current and 'yellow' + or UI.Grid.getRowTextColor(self, row, selected) + end, + eventHandler = function(self, event) + if event.type == 'grid_select' then + message('i', event.selected.index) + else + return UI.Grid.eventHandler(self, event) end - end - return row - end, - accelerators = { - t = 'toggle_enabled' + end, + }, + }, + + env = UI.Tab { + title = 'Env', + index = 4, + grid = UI.ScrollingGrid { + disableHeader = true, + autospace = true, + unfocusedBackgroundSelectedColor = 'black', + columns = { + { heading = 'Key', key = 'name' }, + { heading = 'Value', key = 'value', textColor = 'yellow' }, + }, + accelerators = { + grid_select = 'show_variable', + }, + sortCompare = function() end, + }, + }, + + breaks = UI.Tab { + title = 'Breakpoints', + index = 2, + menuBar = UI.MenuBar { + buttons = { + { text = 'Toggle', event = 'toggle' }, + { text = 'Remove', event = 'remove' }, + { text = 'Clear', event = 'clear' }, + }, + }, + grid = UI.ScrollingGrid { + y = 2, + values = breakpoints, + autospace = true, + columns = { + { heading = 'Line', key = 'line', width = 5 }, + { heading = 'Name', key = 'short' }, + { heading = 'Path', key = 'path', textColor = 'lightGray' }, + }, + getRowTextColor = function(self, row, selected) + return row.disabled and 'lightGray' + or UI.Grid.getRowTextColor(self, row, selected) + end, }, - getRowTextColor = function(self, row, selected) - return row.line == debugLine and currentFile == debugFile and 'yellow' - or UI.Grid.getRowTextColor(self, row, selected) - end, eventHandler = function(self, event) - if event.type == 'grid_select' then + if event.type == 'clear' then + Util.clear(self.grid.values) + self:emit({ type = 'update_breakpoints' }) + + elseif event.type == 'toggle' then + local bp = self.grid:getSelected() + if bp then + bp.disabled = not bp.disabled + self:emit({ type = 'update_breakpoints' }) + end + + elseif event.type == 'grid_select' then self:emit({ - type = 'toggle_breakpoint', - file = currentFile, + type = 'open_file', + file = event.selected.file, line = event.selected.line, }) - elseif event.type == 'toggle_enabled' then - local line = self:getSelected() and self:getSelected().line - if line then - for _,v in pairs(breakpoints) do - if v.file == currentFile and v.line == line then - v.disabled = not v.disabled - self:emit({ type = 'update_breakpoints' }) - break - end - end + + elseif event.type == 'remove' then + local bp = self.grid:getSelected() + if bp then + Util.removeByValue(self.grid.values, bp) + self:emit({ type = 'update_breakpoints' }) + end + + end + return UI.Tab.eventHandler(self, event) + end, + }, + }, + + menuBar = UI.MenuBar { + y = -1, + buttons = { + { text = 'Continue', event = 'cmd', cmd = 'c' }, + { text = 'Step', event = 'cmd', cmd = 's' }, + { text = 'Over', event = 'cmd', cmd = 'n' }, + { text = 'Out', event = 'cmd', cmd = 'f' }, + { text = 'Restart', event = 'restart', width = 9, ex = -1 }, + }, + }, + }, + + source = UI.ScrollingGrid { + y = '50%', ey = -2, + disableHeader = true, + columns = { + { key = 'marker', width = 1 }, + { key = 'line', textColor = 'cyan', width = 4 }, + { heading = 'heading', key = 'source' }, + }, + accelerators = { + t = 'toggle_enabled' + }, + getDisplayValues = function(_, row) + for _,v in pairs(breakpoints) do + if v.file == currentFile and v.line == row.line then + return { + marker = v.disabled and 'x' or '!', + line = row.line, + source = row.source, + } + end + end + return row + end, + getRowTextColor = function(self, row, selected) + return row.line == debugLine and currentFile == debugFile and 'yellow' + or UI.Grid.getRowTextColor(self, row, selected) + end, + eventHandler = function(self, event) + if event.type == 'grid_select' then + self:emit({ + type = 'toggle_breakpoint', + file = currentFile, + line = event.selected.line, + }) + elseif event.type == 'toggle_enabled' then + local line = self:getSelected() and self:getSelected().line + if line then + for _,v in pairs(breakpoints) do + if v.file == currentFile and v.line == line then + v.disabled = not v.disabled + self:emit({ type = 'update_breakpoints' }) + break end end - return UI.Grid.eventHandler(self, event) - end, - }, - }, - - stack = UI.Tab { - title = 'Stack', - index = 3, - grid = UI.ScrollingGrid { - disableHeader = true, - columns = { - { key = 'index', width = 2 }, - { heading = 'heading', key = 'desc' }, - }, - getRowTextColor = function(self, row, selected) - return row.current and 'yellow' - or UI.Grid.getRowTextColor(self, row, selected) - end, - sortColumn = 'index', - eventHandler = function(self, event) - if event.type == 'grid_select' then - message('i', event.selected.index) - else - return UI.Grid.eventHandler(self, event) - end - end, - }, - }, - - env = UI.Tab { - title = 'Env', - index = 4, - grid = UI.ScrollingGrid { - columns = { - { heading = 'Key', key = 'name' }, - { heading = 'Value', key = 'value', textColor = 'yellow' }, - }, - autospace = true, - accelerators = { - grid_select = 'show_variable', - }, - sortCompare = function() end, - }, - }, - - breaks = UI.Tab { - title = 'Breakpoints', - index = 2, - menuBar = UI.MenuBar { - buttons = { - { text = 'Toggle', event = 'toggle' }, - { text = 'Remove', event = 'remove' }, - { text = 'Clear', event = 'clear' }, - }, - }, - grid = UI.ScrollingGrid { - y = 2, - columns = { - { heading = 'Line', key = 'line', width = 5 }, - { heading = 'Name', key = 'short' }, - { heading = 'Path', key = 'path', textColor = 'lightGray' }, - }, - values = breakpoints, - autospace = true, - getRowTextColor = function(self, row, selected) - return row.disabled and 'lightGray' - or UI.Grid.getRowTextColor(self, row, selected) - end, - }, - eventHandler = function(self, event) - if event.type == 'clear' then - Util.clear(self.grid.values) - self:emit({ type = 'update_breakpoints' }) - - elseif event.type == 'toggle' then - local bp = self.grid:getSelected() - if bp then - bp.disabled = not bp.disabled - self:emit({ type = 'update_breakpoints' }) - end - - elseif event.type == 'grid_select' then - self:emit({ - type = 'open_file', - file = event.selected.file, - line = event.selected.line, - }) - - elseif event.type == 'remove' then - local bp = self.grid:getSelected() - if bp then - Util.removeByValue(self.grid.values, bp) - self:emit({ type = 'update_breakpoints' }) - end - end - return UI.Tab.eventHandler(self, event) - end, - }, + end + return UI.Grid.eventHandler(self, event) + end, + }, + statusBar = UI.StatusBar { + ex = -7, y = -1, + backgroundColor = 'black', + textColor = 'orange', + }, + UI.FlatButton { + y = -1, x = -5, + textColor = 'orange', + event = 'open', + text = 'Open', }, quick_open = UI.QuickSelect { + y = '50%', modal = true, enable = function() end, show = function(self) @@ -310,26 +326,25 @@ local page = UI.Page { openFile = function(self, file, line) if file ~= currentFile then local src = loadSource(file) - self.tabs.source.grid:setValues(src) + self.source:setValues(src) end if line then - self.tabs.source.grid:setIndex(#self.tabs.source.grid.values) - self.tabs.source.grid:setIndex(math.max(1, line - 4)) + self.source:setIndex(#self.source.values) + self.source:setIndex(math.max(1, line - 4)) end - self.tabs.source.grid:setIndex(line or 1) - self.tabs:selectTab(self.tabs.source) + self.source:setIndex(line or 1) if currentFile == debugFile then - self.container.statusBar:setStatus( + self.statusBar:setStatus( string.format('%s : %d', fs.getName(file), debugLine)) else - self.container.statusBar:setStatus(fs.getName(file)) + self.statusBar:setStatus(fs.getName(file)) end self:draw() end, eventHandler = function(self, event) if event.type == 'cmd' then - self.container.statusBar:setStatus('Running...') + self.statusBar:setStatus('Running...') message(event.element.cmd) elseif event.type == 'restart' then @@ -345,10 +360,9 @@ local page = UI.Page { self:openFile(event.file, event.line) elseif event.type == 'update_breakpoints' then - self.tabs.breaks.grid:update() - self.tabs.breaks.grid:draw() - self.tabs.source.grid:draw() - message('b', breakpoints) + self.container.tabs.breaks.grid:update() + self.container.tabs.breaks.grid:draw() + self.source:draw() Config.update('debugger', config) elseif event.type == 'toggle_breakpoint' then @@ -405,26 +419,22 @@ local page = UI.Page { end, } -Event.on('debugger', function(_, cmd, data) - if cmd == 'info' then +Event.on('debuggerX', function(_, uid, data) + if uid == debugger.uid then kernel.raise(debugger.uid) -- local tab - local t = data.locals --- for k,v in pairs(data.locals or { }) do --- table.insert(t, { name = k, value = tostring(v), raw = v }) --- end - table.sort(t, function(a, b) return a.name < b.name end) - page.container.locals:setValues(t) - page.container.locals.orig = Util.shallowCopy(t) + table.sort(data.locals, function(a, b) return a.name < b.name end) + page.container.tabs.locals.grid:setValues(data.locals) + page.container.tabs.locals.grid.orig = Util.shallowCopy(data.locals) -- env tab - t = { } + local t = { } for k,v in pairs(getfenv(data.info.func)) do table.insert(t, { name = k, value = tostring(v), raw = v }) end - page.tabs.env.grid:setValues(t) - page.tabs.env.grid.orig = Util.shallowCopy(t) + page.container.tabs.env.grid:setValues(t) + page.container.tabs.env.grid.orig = Util.shallowCopy(t) debugLine = data.info.currentline debugFile = data.info.source:match('@?(.*)') @@ -433,7 +443,7 @@ Event.on('debugger', function(_, cmd, data) page:openFile(debugFile, debugLine) -- stack - page.tabs.stack.grid:setValues(data.stack) + page.container.tabs.stack.grid:setValues(data.stack) page:draw() page:sync() diff --git a/debugger/example.lua b/debugger/example.lua index 8df3d2b..d8093a2 100644 --- a/debugger/example.lua +++ b/debugger/example.lua @@ -4,16 +4,37 @@ end local function method(times) local a = 2 - -- use step out to return out of method for _ = 1, times do a = a * a end return m2(a) end -print('before') -term.current().clear() -print('after') +local chunk = load([[ + local j = 5 + for i = 1, 5 do + j = j * i + end + --table.insert(j, 5) + return j]], nil, nil, _ENV) + +local j = chunk() +print(j) + +require('opus.util').print(coroutine) +local co = coroutine.create(function(args) + print('in coroutine') + return 'hi' +end) + +local _, t = coroutine.resume(co, 'test') +while coroutine.status(co) ~= 'dead' do + coroutine.resume(co, os.pullEvent()) + --print('alive') +end +print(coroutine.status(co)) + +print(t) local i = 2 print(i) @@ -24,4 +45,4 @@ dofile("rom/modules/main/cc/expect.lua") print(res) print('result: ' .. res) -error('f') +table.insert(res, 5) -- 2.49.1 From 8fef5d35803a18348319c12f2a70710ac9dda007 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Wed, 27 May 2020 14:49:51 -0600 Subject: [PATCH 56/90] debugger cleanup + ls now supports date --- debugger/apis/init.lua | 12 +++++++----- debugger/debug.lua | 37 ++++++++++++++++++++++++++++++++++++- lfs/.package | 2 +- shellex/apis/filesystem.lua | 6 ++++++ shellex/ls.lua | 32 ++++++++++++++++++++++++++++++-- 5 files changed, 80 insertions(+), 9 deletions(-) diff --git a/debugger/apis/init.lua b/debugger/apis/init.lua index fe9de22..ff1d199 100644 --- a/debugger/apis/init.lua +++ b/debugger/apis/init.lua @@ -9,7 +9,6 @@ local function breakpointHook(info) if dbg.breakpoints then for _,v in pairs(dbg.breakpoints) do if v.line == info.currentline and v.file == info.short_src then -print(v.line, not v.disabled) return not v.disabled end end @@ -96,6 +95,9 @@ local function local_bindings(offset, stack_inspect_offset) if k ~= '(*temporary)' then v.name = k v.value = tostring(v.raw) + --if type(v.raw) == 'table' and not next(v.raw) then + -- v.value = 'table: (empty)' + --end table.insert(t, v) end end @@ -180,11 +182,11 @@ local function hook() end end -function dbg.call(f, ...) +function dbg.call(fn, ...) local args = { ... } return xpcall( function() - f(table.unpack(args)) + fn(table.unpack(args)) end, function(err) hookEval = stepHook() @@ -196,9 +198,9 @@ end _ENV.coroutine = setmetatable({ - create = function(f) + create = function(fn) local co = _G.coroutine.create(function(...) - local r = { dbg.call(f, ...) } + local r = { dbg.call(fn, ...) } if not r[1] then error(r[2], -1) diff --git a/debugger/debug.lua b/debugger/debug.lua index 5f4f2a2..2afa093 100644 --- a/debugger/debug.lua +++ b/debugger/debug.lua @@ -291,7 +291,7 @@ local page = UI.Page { end, }, statusBar = UI.StatusBar { - ex = -7, y = -1, + ex = -12, y = -1, backgroundColor = 'black', textColor = 'orange', }, @@ -301,6 +301,12 @@ local page = UI.Page { event = 'open', text = 'Open', }, + UI.FlatButton { + y = -1, x = -10, + textColor = 'orange', + event = 'edit_file', + text = 'Edit', + }, quick_open = UI.QuickSelect { y = '50%', @@ -323,6 +329,18 @@ local page = UI.Page { end, }, + textDisplay = UI.SlideOut { + ey = '50%', + textArea = UI.TextArea { + ey = -2, + }, + UI.Button { + x = '50%', y = -1, + text = 'Ok', + event = 'slide_hide', + } + }, + openFile = function(self, file, line) if file ~= currentFile then local src = loadSource(file) @@ -342,6 +360,17 @@ local page = UI.Page { end self:draw() end, + + editFile = function(_, file) + if fs.exists(file) then + multishell.openTab(_ENV, { + path = 'sys/apps/shell.lua', + args = { 'edit ' .. file }, + focused = true, + }) + end + end, + eventHandler = function(self, event) if event.type == 'cmd' then self.statusBar:setStatus('Running...') @@ -356,6 +385,9 @@ local page = UI.Page { elseif event.type == 'open' then self.quick_open:show() + elseif event.type == 'edit_file' then + self:editFile(currentFile) + elseif event.type == 'open_file' then self:openFile(event.file, event.line) @@ -409,6 +441,9 @@ local page = UI.Page { insert(event.element.orig) event.element:setValues(t) event.element:draw() + else + self.textDisplay.textArea:setValue(event.selected.value) + self.textDisplay:show() end end return UI.Page.eventHandler(self, event) diff --git a/lfs/.package b/lfs/.package index 2354b1d..1f411da 100644 --- a/lfs/.package +++ b/lfs/.package @@ -8,4 +8,4 @@ LuaFileSystem is a Lua library developed to complement the set of functions rela LuaFileSystem offers a portable way to access the underlying directory structure and file attributes. LuaFileSystem is free software and uses the same license as Lua 5.x (MIT).]], license = 'MIT', -} +} \ No newline at end of file diff --git a/shellex/apis/filesystem.lua b/shellex/apis/filesystem.lua index 7389faf..248ee40 100644 --- a/shellex/apis/filesystem.lua +++ b/shellex/apis/filesystem.lua @@ -65,4 +65,10 @@ return { realPath = function(...) return ... end, remove = function(a) fs.delete(a) return true end, size = fs.getSize, + attributes = function(f) + local s, m = pcall(fs.attributes, f) + if s then + return m + end + end } diff --git a/shellex/ls.lua b/shellex/ls.lua index 3e3c990..8168ac4 100644 --- a/shellex/ls.lua +++ b/shellex/ls.lua @@ -6,6 +6,8 @@ local unicode = require("shellex.unicode") local tx = require("shellex.transforms") local text = require("shellex.text") +local term = _G.term.current() + local dirsArg, ops = shell.parse(...) if ops.help then @@ -50,6 +52,8 @@ local function stat(names, index) info.sort_name = info.name:gsub("^%.","") info.isLink, info.link = fs.isLink(info.full_path) info.size = info.isLink and 0 or fs.size(info.full_path) + local attrs = fs.attributes(info.full_path) + info.time = attrs and (attrs.modification / 1000) or 0 info.ext = info.name:match("(%.[^.]+)$") or "" names[index] = info return info @@ -166,6 +170,24 @@ local function formatSize(size) return nod(math.floor(size*10)/10)..sizes[unit] end +local function pad(txt) + txt = tostring(txt) + return #txt >= 2 and txt or "0"..txt + end + +local function formatDate(epochms) + --local day_names={"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"} + local month_names={"January","February","March","April","May","June","July","August","September","October","November","December"} + if epochms == 0 then return "" end + local d = os.date("*t", epochms) + local day, hour, min, sec = nod(d.day), pad(nod(d.hour)), pad(nod(d.min)), pad(nod(d.sec)) + if ops["full-time"] then + return string.format("%s-%s-%s %s:%s:%s ", d.year, pad(nod(d.month)), pad(day), hour, min, sec) + else + return string.format("%s %+2s %+2s:%+2s ", month_names[d.month]:sub(1,3), day, hour, pad(min)) + end + end + local function filter(names) if ops.a then return names @@ -200,6 +222,9 @@ local function sort(names) table.sort(names, function(a, b) local ast = stat(names, ni(a)) local bst = stat(names, ni(b)) + if ast[key] == bst[key] then + return ast.name > bst.name + end return ast[key] > bst[key] end) end @@ -239,9 +264,11 @@ local function display(names) if ops.l then lines.n = #names local max_size_width = 1 + local max_date_width = 0 for i=1,lines.n do local info = stat(names, i) max_size_width = math.max(max_size_width, formatSize(info.size):len()) + max_date_width = math.max(max_date_width, formatDate(info.time):len()) end mt.__index = function(_, index) local info = stat(names, index) @@ -252,8 +279,9 @@ local function display(names) end) local write_mode = info.fs and info.fs.isReadOnly() and '-' or 'w' local size = formatSize(info.size) - local format = "%s-r%s %+"..tostring(max_size_width)..'s ' - local meta = string.format(format, file_type, write_mode, size) + local modDate = formatDate(info.time) + local format = "%s-r%s %+"..tostring(max_size_width)..'s %'..tostring(max_date_width).."s" + local meta = string.format(format, file_type, write_mode, size, modDate) local item = info.name..link_target return {{name = meta}, {color = colorize(info), name = item}} -- 2.49.1 From 6394a48766f02b3a3d38eeef0219760c8f4e4233 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Thu, 28 May 2020 22:04:20 -0600 Subject: [PATCH 57/90] support multiple simultaneous breakpoints in coroutines --- debugger/apis/init.lua | 74 +++++++++++++++++++++--------------------- debugger/example.lua | 22 +++++++++++++ 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/debugger/apis/init.lua b/debugger/apis/init.lua index ff1d199..78cfbd6 100644 --- a/debugger/apis/init.lua +++ b/debugger/apis/init.lua @@ -3,7 +3,13 @@ local fs = _G.fs -local dbg = { } +local dbg = { + hooks = { }, + waits = { }, + debugger = nil, + breakpoints = nil, +} +_G._dbg = dbg -- for testing purposes local function breakpointHook(info) if dbg.breakpoints then @@ -22,15 +28,12 @@ local function functionHook(fn) end local function stepHook() - local co = coroutine.running() - return function(info) - return co == coroutine.running() - or breakpointHook(info) + return function() + return true end end local function stackSizeHook(n) - local co = coroutine.running() local i = 2 while true do local info = debug.getinfo(i) @@ -40,8 +43,7 @@ local function stackSizeHook(n) i = i + 1 end return function(info) - return co == coroutine.running() - and not debug.getinfo(i - n) + return not debug.getinfo(i - n) or breakpointHook(info) end end @@ -54,8 +56,6 @@ local function stepOverHook() return stackSizeHook(0) end -local hookEval = function() end - -- Create a table of all the locally accessible variables. local function local_bindings(offset, stack_inspect_offset) offset = offset + 1 + stack_inspect_offset -- add this function to the offset @@ -133,14 +133,12 @@ local function get_trace(offset, stack_inspect_offset) return t end -local inHook = false - local function hook() local info = debug.getinfo(2) - if not inHook and hookEval(info) then - inHook = true + local h = dbg.hooks[coroutine.running()] + if h and h.eval(info) then local inspectOffset = 0 repeat @@ -153,6 +151,10 @@ local function hook() } inspectOffset = 0 -- reset + table.insert(dbg.waits, h) + while dbg.waits[1] ~= h do + os.sleep(.1) + end os.queueEvent('debuggerX', dbg.debugger.uid, snapshot) local e, cmd, param @@ -160,25 +162,24 @@ local function hook() e, cmd, param = os.pullEvent('debugger') until e == 'debugger' + table.remove(dbg.waits, 1) + if cmd == 's' then - hookEval = stepHook() + h.eval = stepHook() elseif cmd == 'n' then - hookEval = stepOverHook() + h.eval = stepOverHook() elseif cmd == 'f' then - hookEval = stepOutHook() + h.eval = stepOutHook() elseif cmd == 'c' then - hookEval = breakpointHook + h.eval = breakpointHook elseif cmd == 'i' then -- get snapshot of stack at this offset inspectOffset = param done = false else - os.sleep(1) done = false end until done - - inHook = false end end @@ -189,19 +190,23 @@ function dbg.call(fn, ...) fn(table.unpack(args)) end, function(err) - hookEval = stepHook() + dbg.hooks[coroutine.running()].eval = stepHook() -- An error has occurred return err end) end -_ENV.coroutine = setmetatable({ +dbg.stopIn = function(fn) + dbg.hooks[coroutine.running()].eval = functionHook(fn) +end +_ENV.coroutine = setmetatable({ create = function(fn) local co = _G.coroutine.create(function(...) local r = { dbg.call(fn, ...) } + dbg.hooks[coroutine.running()] = nil if not r[1] then error(r[2], -1) end @@ -209,24 +214,19 @@ _ENV.coroutine = setmetatable({ return table.unpack(r, 2) end) + dbg.hooks[co] = { + co = co, + eval = breakpointHook, + } debug.sethook(co, hook, 'l') return co end - --[[ - create = function(f) - local co = _G.coroutine.create(f) - debug.sethook(co, hook, 'l') - return co - end - ]] }, { __index = coroutine }) +dbg.hooks[coroutine.running()] = { + co = coroutine.running(), + eval = function() return false end, +} + debug.sethook(hook, 'l') - --- Expose the debugger's functions -dbg.stopIn = function(fn) - hookEval = functionHook(fn) -end -dbg.debugger = nil - return dbg diff --git a/debugger/example.lua b/debugger/example.lua index d8093a2..9c220d1 100644 --- a/debugger/example.lua +++ b/debugger/example.lua @@ -10,6 +10,28 @@ local function method(times) return m2(a) end +local Event = require('opus.event') + +Event.on('event1', function() + print('event1') +end) + +Event.on('event2', function() + print('event2') +end) + +Event.onTimeout(10, function() + Event.exitPullEvents() +end) + +local function xx() + os.queueEvent('event1') + os.queueEvent('event2') + + Event.pullEvents() +end +xx() + local chunk = load([[ local j = 5 for i = 1, 5 do -- 2.49.1 From d203510527c2136f0ae97047e47fff521ff84b58 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Sat, 30 May 2020 20:06:30 -0600 Subject: [PATCH 58/90] debugger refactor --- common/edit.lua | 14 ++++++++++---- common/etc/apps.db | 5 +++++ common/etc/fstab | 3 +-- debugger/apis/init.lua | 11 ++--------- debugger/debug.lua | 28 ++++++++++++++++++++++------ debugger/etc/apps.db | 7 +++++++ debugger/example.lua | 12 +++++++++++- 7 files changed, 58 insertions(+), 22 deletions(-) create mode 100644 debugger/etc/apps.db diff --git a/common/edit.lua b/common/edit.lua index 54f5ba6..2dbd1ff 100644 --- a/common/edit.lua +++ b/common/edit.lua @@ -1,6 +1,5 @@ local Array = require('opus.array') local Config = require('opus.config') -local fuzzy = require('opus.fuzzy') local UI = require('opus.ui') local Util = require('opus.util') @@ -492,7 +491,7 @@ local page = UI.Page { UI.Window.resize(self) w, h = self.width, self.height - actions.set_cursor(x, y) + actions.set_cursor() actions.dirty_all() actions.redraw() end, @@ -940,7 +939,7 @@ actions = { local routine = { focused = true, title = fs.getName(fileInfo.path), - chainExit = function(_, result) + onExit = function(_, result) -- display results of process before closing window if result then -- clean exit -- any errors will be picked up by multishells @@ -1467,12 +1466,19 @@ actions = { end, } -local args = { ... } +local args, options = Util.parse( ... ) local filename = args[1] and shell.resolve(args[1]) if not (actions.load(filename) or actions.load(config.filename) or actions.load('untitled.lua')) then error('Error opening file') end +if tonumber(options.line) then + scrollY = math.max(0, tonumber(options.line) - math.floor(h / 2)) + actions.go_to(1, tonumber(options.line)) + actions.set_cursor() + actions.dirty_all() +end + UI:setPage(page) local s, m = pcall(function() UI:start() end) if not s then diff --git a/common/etc/apps.db b/common/etc/apps.db index 7580ce5..2e2a32b 100644 --- a/common/etc/apps.db +++ b/common/etc/apps.db @@ -78,4 +78,9 @@ category = "Apps", run = "write", }, + [ "c543ece81605c7d202121c62080a0db4020fc2c75bfac35d101d7f3e93c93949" ] = { + category = "Apps", + run = "ascii", + title = "Ascii", + }, } diff --git a/common/etc/fstab b/common/etc/fstab index 00b921e..e636640 100644 --- a/common/etc/fstab +++ b/common/etc/fstab @@ -3,5 +3,4 @@ packages/common/hexedit.lua urlfs https://pastebin.com/raw/Ds9ajsp4 packages/common/colors.lua urlfs https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/ignore/colors.lua packages/common/cowsay.lua urlfs https://pastebin.com/raw/n00VQJsw packages/common/calc.lua urlfs https://pastebin.com/raw/nAinUn1h -packages/common/write.lua urlfs https://pastebin.com/raw/RSyhCjqv -packages/common/apis/debugger.lua urlfs https://raw.githubusercontent.com/slembcke/debugger.lua/master/debugger.lua \ No newline at end of file +packages/common/write.lua urlfs https://pastebin.com/raw/RSyhCjqv \ No newline at end of file diff --git a/debugger/apis/init.lua b/debugger/apis/init.lua index 78cfbd6..44e4b16 100644 --- a/debugger/apis/init.lua +++ b/debugger/apis/init.lua @@ -6,10 +6,8 @@ local fs = _G.fs local dbg = { hooks = { }, waits = { }, - debugger = nil, breakpoints = nil, } -_G._dbg = dbg -- for testing purposes local function breakpointHook(info) if dbg.breakpoints then @@ -155,12 +153,7 @@ local function hook() while dbg.waits[1] ~= h do os.sleep(.1) end - os.queueEvent('debuggerX', dbg.debugger.uid, snapshot) - - local e, cmd, param - repeat - e, cmd, param = os.pullEvent('debugger') - until e == 'debugger' + local cmd, param = dbg.read(snapshot) table.remove(dbg.waits, 1) @@ -187,7 +180,7 @@ function dbg.call(fn, ...) local args = { ... } return xpcall( function() - fn(table.unpack(args)) + return fn(table.unpack(args)) end, function(err) dbg.hooks[coroutine.running()].eval = stepHook() diff --git a/debugger/debug.lua b/debugger/debug.lua index 2afa093..b26b675 100644 --- a/debugger/debug.lua +++ b/debugger/debug.lua @@ -46,6 +46,18 @@ local function startClient() error(msg, -1) end + dbg.read = function(snapshot) + os.sleep(0) -- not sure why, but we need a sleep before :resume + -- directly resuming debugger routine to prevent + -- serialization of the snapshot + dbg.debugger:resume('debuggerX', dbg.debugger.uid, snapshot) + local e, cmd, param + repeat + e, cmd, param = os.pullEvent('debugger') + until e == 'debugger' + return cmd, param + end + -- breakpoint table is shared across processes dbg.breakpoints = breakpoints dbg.debugger = debugger @@ -106,8 +118,6 @@ local page = UI.Page { y = 1, ey = '50%', tabs = UI.Tabs { ey = -2, - unselectedBackgroundColor = 'black', - locals = UI.Tab { title = 'Locals', index = 1, @@ -361,11 +371,12 @@ local page = UI.Page { self:draw() end, - editFile = function(_, file) + editFile = function(self, file) if fs.exists(file) then + local line = self.source:getSelected().line multishell.openTab(_ENV, { path = 'sys/apps/shell.lua', - args = { 'edit ' .. file }, + args = { ('edit --line=%d %s'):format(line , file) }, focused = true, }) end @@ -424,8 +435,12 @@ local page = UI.Page { local t = event.selected.raw for k,v in pairs(t) do local depth = event.selected.depth or 0 - table.insert(event.selected.children, - { name = (' '):rep(depth + 2) .. k, value = tostring(v), raw = v, depth = depth + 2 }) + table.insert(event.selected.children, { + name = (' '):rep(depth + 2) .. tostring(k), + value = tostring(v), + raw = v, + depth = depth + 2 + }) end table.sort(event.selected.children, function(a, b) return a.name < b.name end) end @@ -468,6 +483,7 @@ Event.on('debuggerX', function(_, uid, data) for k,v in pairs(getfenv(data.info.func)) do table.insert(t, { name = k, value = tostring(v), raw = v }) end + table.sort(t, function(a, b) return a.name < b.name end) page.container.tabs.env.grid:setValues(t) page.container.tabs.env.grid.orig = Util.shallowCopy(t) diff --git a/debugger/etc/apps.db b/debugger/etc/apps.db new file mode 100644 index 0000000..7f8a171 --- /dev/null +++ b/debugger/etc/apps.db @@ -0,0 +1,7 @@ +{ + [ "1a03bd2fd107c453f3183e30b9716f82200671e8270fbbefbe602f5a48705527" ] = { + run = "fileui --exec=debug.lua --title=debug", + title = "Debug", + category = "Apps", + }, +} diff --git a/debugger/example.lua b/debugger/example.lua index 9c220d1..20ce2f8 100644 --- a/debugger/example.lua +++ b/debugger/example.lua @@ -1,3 +1,14 @@ +--[[ + -- a very simple debugger implementation + local dbg = require('debugger') + dbg.read = function(snapshot) + print(('%s: %d'):format(snapshot.info.source, snapshot.info.currentline)) + write('> ') + return read() + end + dbg.stopIn(debug.getinfo(1).func) +]] + local function m2(a) return a end @@ -43,7 +54,6 @@ local chunk = load([[ local j = chunk() print(j) -require('opus.util').print(coroutine) local co = coroutine.create(function(args) print('in coroutine') return 'hi' -- 2.49.1 From fb5e69f703e33276c6760d4f14f1e7f57be112b8 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Sun, 31 May 2020 23:51:04 -0600 Subject: [PATCH 59/90] compression package wip + debugger bugfixes --- common/edit.lua | 6 +- compress/.package | 6 + compress/apis/deflatelua.lua | 530 +++++++++++++++++++++++++++++++++++ compress/apis/lzw.lua | 142 ++++++++++ compress/apis/tar.lua | 211 ++++++++++++++ compress/untar.lua | 46 +++ debugger/apis/init.lua | 25 +- debugger/debug.lua | 2 +- tests/test-ramfs.lua | 30 ++ tests/test-tar.lua | 20 ++ 10 files changed, 1011 insertions(+), 7 deletions(-) create mode 100644 compress/.package create mode 100644 compress/apis/deflatelua.lua create mode 100644 compress/apis/lzw.lua create mode 100644 compress/apis/tar.lua create mode 100644 compress/untar.lua create mode 100644 tests/test-ramfs.lua create mode 100644 tests/test-tar.lua diff --git a/common/edit.lua b/common/edit.lua index 2dbd1ff..2a6e110 100644 --- a/common/edit.lua +++ b/common/edit.lua @@ -901,11 +901,7 @@ actions = { if fs.isReadOnly(filename) then actions.error("access denied") else - local s, m = pcall(function() - if not Util.writeLines(filename, tLines) then - error("Failed to open " .. filename) - end - end) + local s, m = pcall(Util.writeFile, filename, table.concat(tLines, '\n')) if s then lastSave = undo.chain[#undo.chain] diff --git a/compress/.package b/compress/.package new file mode 100644 index 0000000..b6e67db --- /dev/null +++ b/compress/.package @@ -0,0 +1,6 @@ +{ + title = 'Compress', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/compress', + description = [[untar / gunzip]], + license = 'MIT', +} diff --git a/compress/apis/deflatelua.lua b/compress/apis/deflatelua.lua new file mode 100644 index 0000000..cf7c95e --- /dev/null +++ b/compress/apis/deflatelua.lua @@ -0,0 +1,530 @@ +--[[ +see: https://github.com/davidm/lua-compress-deflatelua/ +for licensing / details +--]] + +local M = {_TYPE='module', _NAME='compress.deflatelua', _VERSION='0.3.20111128'} + +local assert = assert +local error = error +local ipairs = ipairs +local pairs = pairs +local tostring = tostring +local type = type +local setmetatable = setmetatable +local io = io +local math = math +local table_sort = table.sort +local math_max = math.max +local string_char = string.char +local band = bit32.band +local lshift = bit32.lshift +local rshift = bit32.rshift + +local function runtime_error(s, level) + level = level or 1 + error(s, level+1) +end + + +local function make_outstate(outbs) + local outstate = {} + outstate.outbs = outbs + outstate.window = {} + outstate.window_pos = 1 + return outstate +end + + +local function output(outstate, byte) + local window_pos = outstate.window_pos + outstate.outbs(byte) + outstate.window[window_pos] = byte + outstate.window_pos = window_pos % 32768 + 1 -- 32K +end + + +local function noeof(val) + return assert(val, 'unexpected end of file') +end + + +local function hasbit(bits, bit) + return bits % (bit + bit) >= bit +end + + +local function memoize(f) + local mt = {} + local t = setmetatable({}, mt) + function mt:__index(k) + local v = f(k) + t[k] = v + return v + end + return t +end + + +-- small optimization (lookup table for powers of 2) +local pow2 = memoize(function(n) return 2^n end) + +--local tbits = memoize( +-- function(bits) +-- return memoize( function(bit) return getbit(bits, bit) end ) +-- end ) + + +-- weak metatable marking objects as bitstream type +local is_bitstream = setmetatable({}, {__mode='k'}) + +local function bytestream_from_file(fh) + local o = {} + function o.read() + local sb = fh:read(1) + if sb then return sb:byte() end + end + return o +end + + +local function bytestream_from_string(s) + local i = 1 + local o = {} + function o.read() + local by + if i <= #s then + by = s:byte(i) + i = i + 1 + end + return by + end + return o +end + + +local function bytestream_from_function(f) + local o = {} + function o.read() + return f() + end + return o +end + + +local function bitstream_from_bytestream(bys) + local buf_byte = 0 + local buf_nbit = 0 + local o = {} + + function o.nbits_left_in_byte() + return buf_nbit + end + + function o:read(nbits) + nbits = nbits or 1 + while buf_nbit < nbits do + local byte = bys:read() + if not byte then return end -- note: more calls also return nil + buf_byte = buf_byte + lshift(byte, buf_nbit) + buf_nbit = buf_nbit + 8 + end + local bits + if nbits == 0 then + bits = 0 + elseif nbits == 32 then + bits = buf_byte + buf_byte = 0 + else + bits = band(buf_byte, rshift(0xffffffff, 32 - nbits)) + buf_byte = rshift(buf_byte, nbits) + end + buf_nbit = buf_nbit - nbits + return bits + end + + + is_bitstream[o] = true + + return o +end + + +local function get_bitstream(o) + local bs + if is_bitstream[o] then + return o + elseif io.type(o) == 'file' then + bs = bitstream_from_bytestream(bytestream_from_file(o)) + elseif type(o) == 'string' then + bs = bitstream_from_bytestream(bytestream_from_string(o)) + elseif type(o) == 'function' then + bs = bitstream_from_bytestream(bytestream_from_function(o)) + else + runtime_error 'unrecognized type' + end + return bs +end + + +local function get_obytestream(o) + local bs + if io.type(o) == 'file' then + bs = function(sbyte) o:write(string_char(sbyte)) end + elseif type(o) == 'function' then + bs = o + else + runtime_error('unrecognized type: ' .. tostring(o)) + end + return bs +end + + +local function HuffmanTable(init, is_full) + local t = {} + if is_full then + for val,nbits in pairs(init) do + if nbits ~= 0 then + t[#t+1] = {val=val, nbits=nbits} + end + end + else + for i=1,#init-2,2 do + local firstval, nbits, nextval = init[i], init[i+1], init[i+2] + if nbits ~= 0 then + for val=firstval,nextval-1 do + t[#t+1] = {val=val, nbits=nbits} + end + end + end + end + table_sort(t, function(a,b) + return a.nbits == b.nbits and a.val < b.val or a.nbits < b.nbits + end) + + -- assign codes + local code = 1 -- leading 1 marker + local nbits = 0 + for _,s in ipairs(t) do + if s.nbits ~= nbits then + code = code * pow2[s.nbits - nbits] + nbits = s.nbits + end + s.code = code + code = code + 1 + end + + local minbits = math.huge + local look = {} + for _,s in ipairs(t) do + minbits = math.min(minbits, s.nbits) + look[s.code] = s.val + end + + local msb = function(bits, nbits) + local res = 0 + for _=1,nbits do + res = lshift(res, 1) + band(bits, 1) + bits = rshift(bits, 1) + end + return res + end + + local tfirstcode = memoize( + function(bits) return pow2[minbits] + msb(bits, minbits) end) + + function t:read(bs) + local code = 1 -- leading 1 marker + local nbits = 0 + while 1 do + if nbits == 0 then -- small optimization (optional) + code = tfirstcode[noeof(bs:read(minbits))] + nbits = nbits + minbits + else + local b = noeof(bs:read()) + nbits = nbits + 1 + code = code * 2 + b -- MSB first + end + local val = look[code] + if val then + return val + end + end + end + + return t +end + + +local function parse_gzip_header(bs) + -- local FLG_FTEXT = 2^0 + local FLG_FHCRC = 2^1 + local FLG_FEXTRA = 2^2 + local FLG_FNAME = 2^3 + local FLG_FCOMMENT = 2^4 + + local id1 = bs:read(8) + local id2 = bs:read(8) + if id1 ~= 31 or id2 ~= 139 then + runtime_error 'not in gzip format' + end + bs:read(8) -- compression method + local flg = bs:read(8) -- FLaGs + local mtime = bs:read(32) -- Modification TIME + local xfl = bs:read(8) -- eXtra FLags + local os = bs:read(8) -- Operating System + + if not os then runtime_error 'invalid header' end + + if hasbit(flg, FLG_FEXTRA) then + local xlen = bs:read(16) + local extra = 0 + for i=1,xlen do + extra = bs:read(8) + end + if not extra then runtime_error 'invalid header' end + end + + local function parse_zstring(bs) + repeat + local by = bs:read(8) + if not by then runtime_error 'invalid header' end + until by == 0 + end + + if hasbit(flg, FLG_FNAME) then + parse_zstring(bs) + end + + if hasbit(flg, FLG_FCOMMENT) then + parse_zstring(bs) + end + + if hasbit(flg, FLG_FHCRC) then + local crc16 = bs:read(16) + if not crc16 then runtime_error 'invalid header' end + -- IMPROVE: check CRC. where is an example .gz file that + -- has this set? + end +end + +local function parse_zlib_header(bs) + local cm = bs:read(4) -- Compression Method + local cinfo = bs:read(4) -- Compression info + local fcheck = bs:read(5) -- FLaGs: FCHECK (check bits for CMF and FLG) + local fdict = bs:read(1) -- FLaGs: FDICT (present dictionary) + local flevel = bs:read(2) -- FLaGs: FLEVEL (compression level) + local cmf = cinfo * 16 + cm -- CMF (Compresion Method and flags) + local flg = fcheck + fdict * 32 + flevel * 64 -- FLaGs + + if cm ~= 8 then -- not "deflate" + runtime_error("unrecognized zlib compression method: " + cm) + end + if cinfo > 7 then + runtime_error("invalid zlib window size: cinfo=" + cinfo) + end + local window_size = 2^(cinfo + 8) + + if (cmf*256 + flg) % 31 ~= 0 then + runtime_error("invalid zlib header (bad fcheck sum)") + end + + if fdict == 1 then + runtime_error("FIX:TODO - FDICT not currently implemented") + local dictid_ = bs:read(32) + end + + return window_size +end + +local function parse_huffmantables(bs) + local hlit = bs:read(5) -- # of literal/length codes - 257 + local hdist = bs:read(5) -- # of distance codes - 1 + local hclen = noeof(bs:read(4)) -- # of code length codes - 4 + + local ncodelen_codes = hclen + 4 + local codelen_init = {} + local codelen_vals = { + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15} + for i=1,ncodelen_codes do + local nbits = bs:read(3) + local val = codelen_vals[i] + codelen_init[val] = nbits + end + local codelentable = HuffmanTable(codelen_init, true) + + local function decode(ncodes) + local init = {} + local nbits + local val = 0 + while val < ncodes do + local codelen = codelentable:read(bs) + --FIX:check nil? + local nrepeat + if codelen <= 15 then + nrepeat = 1 + nbits = codelen + elseif codelen == 16 then + nrepeat = 3 + noeof(bs:read(2)) + -- nbits unchanged + elseif codelen == 17 then + nrepeat = 3 + noeof(bs:read(3)) + nbits = 0 + elseif codelen == 18 then + nrepeat = 11 + noeof(bs:read(7)) + nbits = 0 + else + error 'ASSERT' + end + for i=1,nrepeat do + init[val] = nbits + val = val + 1 + end + end + local huffmantable = HuffmanTable(init, true) + return huffmantable + end + + local nlit_codes = hlit + 257 + local ndist_codes = hdist + 1 + + local littable = decode(nlit_codes) + local disttable = decode(ndist_codes) + + return littable, disttable +end + + +local tdecode_len_base +local tdecode_len_nextrabits +local tdecode_dist_base +local tdecode_dist_nextrabits +local function parse_compressed_item(bs, outstate, littable, disttable) + local val = littable:read(bs) + if val < 256 then -- literal + output(outstate, val) + elseif val == 256 then -- end of block + return true + else + if not tdecode_len_base then + local t = {[257]=3} + local skip = 1 + for i=258,285,4 do + for j=i,i+3 do t[j] = t[j-1] + skip end + if i ~= 258 then skip = skip * 2 end + end + t[285] = 258 + tdecode_len_base = t + end + if not tdecode_len_nextrabits then + local t = {} + + for i=257,285 do + local j = math_max(i - 261, 0) + t[i] = rshift(j, 2) + end + + t[285] = 0 + tdecode_len_nextrabits = t + end + local len_base = tdecode_len_base[val] + local nextrabits = tdecode_len_nextrabits[val] + local extrabits = bs:read(nextrabits) + local len = len_base + extrabits + + if not tdecode_dist_base then + local t = {[0]=1} + local skip = 1 + for i=1,29,2 do + for j=i,i+1 do t[j] = t[j-1] + skip end + if i ~= 1 then skip = skip * 2 end + end + tdecode_dist_base = t + end + if not tdecode_dist_nextrabits then + local t = {} + + for i=0,29 do + local j = math_max(i - 2, 0) + t[i] = rshift(j, 1) + end + + tdecode_dist_nextrabits = t + end + local dist_val = disttable:read(bs) + local dist_base = tdecode_dist_base[dist_val] + local dist_nextrabits = tdecode_dist_nextrabits[dist_val] + local dist_extrabits = bs:read(dist_nextrabits) + local dist = dist_base + dist_extrabits + + for i=1,len do + local pos = (outstate.window_pos - 1 - dist) % 32768 + 1 -- 32K + output(outstate, assert(outstate.window[pos], 'invalid distance')) + end + end + return false +end + + +local function parse_block(bs, outstate, throttle) + local bfinal = bs:read(1) + local btype = bs:read(2) + + local BTYPE_NO_COMPRESSION = 0 + local BTYPE_FIXED_HUFFMAN = 1 + local BTYPE_DYNAMIC_HUFFMAN = 2 + local BTYPE_RESERVED_ = 3 + + if btype == BTYPE_NO_COMPRESSION then + bs:read(bs:nbits_left_in_byte()) + local len = bs:read(16) + local nlen_ = noeof(bs:read(16)) + + for _=1,len do + local by = noeof(bs:read(8)) + output(outstate, by) + end + elseif btype == BTYPE_FIXED_HUFFMAN or btype == BTYPE_DYNAMIC_HUFFMAN then + local littable, disttable + if btype == BTYPE_DYNAMIC_HUFFMAN then + littable, disttable = parse_huffmantables(bs) + else + littable = HuffmanTable {0,8, 144,9, 256,7, 280,8, 288,nil} + disttable = HuffmanTable {0,5, 32,nil} + end + + repeat + local is_done = parse_compressed_item( + bs, outstate, littable, disttable) + throttle() + until is_done + else + runtime_error 'unrecognized compression type' + end + + return bfinal ~= 0 +end + +function M.inflate(t) + local bs = get_bitstream(t.input) + local outbs = get_obytestream(t.output) + local outstate = make_outstate(outbs) + + repeat + local is_final = parse_block(bs, outstate, t.throttle) + until is_final +end +local inflate = M.inflate + +function M.gunzip(t) + local bs = get_bitstream(t.input) + local outbs = get_obytestream(t.output) + + parse_gzip_header(bs) + + inflate{input=bs, output=outbs, throttle=t.throttle or function() end} + + bs:read(bs:nbits_left_in_byte()) +end + +return M \ No newline at end of file diff --git a/compress/apis/lzw.lua b/compress/apis/lzw.lua new file mode 100644 index 0000000..3d4c4b5 --- /dev/null +++ b/compress/apis/lzw.lua @@ -0,0 +1,142 @@ +-- see: https://github.com/Rochet2/lualzw +-- MIT License - Copyright (c) 2016 Rochet2 + +local char = string.char +local type = type +local sub = string.sub +local tconcat = table.concat + +local SIGC = 'LZWC' + +local basedictcompress = {} +local basedictdecompress = {} +for i = 0, 255 do + local ic, iic = char(i), char(i, 0) + basedictcompress[ic] = iic + basedictdecompress[iic] = ic +end + +local function dictAddA(str, dict, a, b) + if a >= 256 then + a, b = 0, b+1 + if b >= 256 then + dict = {} + b = 1 + end + end + dict[str] = char(a,b) + a = a+1 + return dict, a, b +end + +local function compress(input) + if type(input) ~= "string" then + error ("string expected, got "..type(input)) + end + local len = #input + if len <= 1 then + return input + end + + local dict = {} + local a, b = 0, 1 + + local result = { SIGC } + local resultlen = 1 + local n = 2 + local word = "" + for i = 1, len do + local c = sub(input, i, i) + local wc = word..c + if not (basedictcompress[wc] or dict[wc]) then + local write = basedictcompress[word] or dict[word] + if not write then + error "algorithm error, could not fetch word" + end + result[n] = write + resultlen = resultlen + #write + n = n+1 + if len <= resultlen then + return input + end + dict, a, b = dictAddA(wc, dict, a, b) + word = c + else + word = wc + end + end + result[n] = basedictcompress[word] or dict[word] + resultlen = resultlen+#result[n] + if len <= resultlen then + return input + end + return tconcat(result) +end + +local function dictAddB(str, dict, a, b) + if a >= 256 then + a, b = 0, b+1 + if b >= 256 then + dict = {} + b = 1 + end + end + dict[char(a,b)] = str + a = a+1 + return dict, a, b +end + +local function decompress(input) + if type(input) ~= "string" then + error( "string expected, got "..type(input)) + end + + if #input <= 1 then + return input + end + + local control = sub(input, 1, 4) + if control ~= SIGC then + return input + end + input = sub(input, 5) + local len = #input + + if len < 2 then + error("invalid input - not a compressed string") + end + + local dict = {} + local a, b = 0, 1 + + local result = {} + local n = 1 + local last = sub(input, 1, 2) + result[n] = basedictdecompress[last] or dict[last] + n = n+1 + for i = 3, len, 2 do + local code = sub(input, i, i+1) + local lastStr = basedictdecompress[last] or dict[last] + if not lastStr then + error( "could not find last from dict. Invalid input?") + end + local toAdd = basedictdecompress[code] or dict[code] + if toAdd then + result[n] = toAdd + n = n+1 + dict, a, b = dictAddB(lastStr..sub(toAdd, 1, 1), dict, a, b) + else + local tmp = lastStr..sub(lastStr, 1, 1) + result[n] = tmp + n = n+1 + dict, a, b = dictAddB(tmp, dict, a, b) + end + last = code + end + return tconcat(result) +end + +return { + compress = compress, + decompress = decompress, +} diff --git a/compress/apis/tar.lua b/compress/apis/tar.lua new file mode 100644 index 0000000..c64a9be --- /dev/null +++ b/compress/apis/tar.lua @@ -0,0 +1,211 @@ + +-- see: https://github.com/luarocks/luarocks/blob/master/src/luarocks/tools/tar.lua +-- A pure-Lua implementation of untar (unpacking .tar archives) + +local tar = { } + +local fs = _G.fs + +local blocksize = 512 + +local function get_typeflag(flag) + if flag == "0" or flag == "\0" then return "file" + elseif flag == "1" then return "link" + elseif flag == "2" then return "symlink" -- "reserved" in POSIX, "symlink" in GNU + elseif flag == "3" then return "character" + elseif flag == "4" then return "block" + elseif flag == "5" then return "directory" + elseif flag == "6" then return "fifo" + elseif flag == "7" then return "contiguous" -- "reserved" in POSIX, "contiguous" in GNU + elseif flag == "x" then return "next file" + elseif flag == "g" then return "global extended header" + elseif flag == "L" then return "long name" + elseif flag == "K" then return "long link name" + end + return "unknown" +end + +local function octal_to_number(octal) + local exp = 0 + local number = 0 + octal = octal:gsub("%s", "") + for i = #octal,1,-1 do + local digit = tonumber(octal:sub(i,i)) + if not digit then + break + end + number = number + (digit * 8^exp) + exp = exp + 1 + end + return number +end + +local function checksum_header(block) + local sum = 256 + for i = 1,148 do + local b = block:byte(i) or 0 + sum = sum + b + end + for i = 157,500 do + local b = block:byte(i) or 0 + sum = sum + b + end + return sum +end + +local function nullterm(s) +_G._zz = s + return s:match("^[^%z]*") +end + +local function read_header_block(block) + local header = {} + header.name = nullterm(block:sub(1,100)) + header.mode = nullterm(block:sub(101,108)):gsub(" ", "") + header.uid = octal_to_number(nullterm(block:sub(109,116))) + header.gid = octal_to_number(nullterm(block:sub(117,124))) + header.size = octal_to_number(nullterm(block:sub(125,136))) + header.mtime = octal_to_number(nullterm(block:sub(137,148))) + header.chksum = octal_to_number(nullterm(block:sub(149,156))) + header.typeflag = get_typeflag(block:sub(157,157)) + header.linkname = nullterm(block:sub(158,257)) + header.magic = block:sub(258,263) + header.version = block:sub(264,265) + header.uname = nullterm(block:sub(266,297)) + header.gname = nullterm(block:sub(298,329)) + header.devmajor = octal_to_number(nullterm(block:sub(330,337))) + header.devminor = octal_to_number(nullterm(block:sub(338,345))) + header.prefix = block:sub(346,500) + if not checksum_header(block) == header.chksum then + return false, "Failed header checksum" + end + return header +end + +function tar.untar(filename, destdir, verbose) + assert(type(filename) == "string") + assert(type(destdir) == "string") + + local tar_handle = io.open(filename, "rb") + if not tar_handle then return nil, "Error opening file "..filename end + + local long_name, long_link_name + local ok, err + + local make_dir = function(a) + if not fs.exists(a) then + fs.makeDir(a) + end + return true + end + + while true do + local block + repeat + block = tar_handle:read(blocksize) + until (not block) or checksum_header(block) > 256 + if not block then break end + if #block < blocksize then + ok, err = nil, "Invalid block size -- corrupted file?" + break + end + local header + header, err = read_header_block(block) + if not header then + ok = false + break + end + + local file_data = tar_handle:read(math.ceil(header.size / blocksize) * blocksize):sub(1,header.size) + + if header.typeflag == "long name" then + long_name = nullterm(file_data) + elseif header.typeflag == "long link name" then + long_link_name = nullterm(file_data) + else + if long_name then + header.name = long_name + long_name = nil + end + if long_link_name then + header.name = long_link_name + long_link_name = nil + end + end + local pathname = fs.combine(destdir, header.name) + + if header.typeflag == "directory" then + ok, err = make_dir(pathname) + if not ok then + break + end + elseif header.typeflag == "file" then + local dirname = fs.getDir(pathname) + if dirname ~= "" then + ok, err = make_dir(dirname) + if not ok then + break + end + end + local file_handle + if verbose then + print(pathname) + end + file_handle, err = io.open(pathname, "wb") + if not file_handle then + ok = nil + break + end + file_handle:write(file_data) + file_handle:close() + end + end + tar_handle:close() + return ok, err +end + +local function create_header_block(filename, abspath) + local block = ('\0'):rep(512) + + local function number_to_octal(n) + return ('%o'):format(n) + end + + local function ins(pos, istr) + block = block:sub(1, pos - 1) .. istr .. block:sub(pos + #istr) + end + + ins(1, filename) -- header + ins(125, number_to_octal(fs.getSize(abspath))) + ins(157, '0') -- typeflag + + ins(149, number_to_octal(checksum_header(block))) + + return block +end + +-- the bare minimum for this program to untar +function tar.tar(filename, root, files) + assert(type(filename) == "string") + assert(type(root) == "string") + assert(type(files) == "table") + + local tar_handle = io.open(filename, "wb") + if not tar_handle then return nil, "Error opening file "..filename end + + for _, file in pairs(files) do + local abs = fs.combine(root, file) + local block = create_header_block(file, abs) + tar_handle:write(block) + local f = require('opus.util').readFile(abs, 'rb') + tar_handle:write(f) + local padding = #f % 512 + if padding > 0 then + tar_handle:write(('\0'):rep(512 - padding)) + end + end + tar_handle:close() + return true +end + +return tar \ No newline at end of file diff --git a/compress/untar.lua b/compress/untar.lua new file mode 100644 index 0000000..080c9d4 --- /dev/null +++ b/compress/untar.lua @@ -0,0 +1,46 @@ +-- see: https://github.com/luarocks/luarocks/blob/master/src/luarocks/tools/tar.lua + +-- TODO: support normal tar syntax -- tar xvf + +local DEFLATE = require('compress.deflatelua') +local tar = require('compress.tar') + +local fs = _G.fs +local io = _G.io +local shell = _ENV.shell + +local args = { ... } + +if not args[2] then + error('Syntax: tar FILE DESTDIR') +end + +local inFile = shell.resolve(args[1]) +local outDir = shell.resolve(args[2]) + +if inFile:match('(.+)%.[gG][zZ]$') then + local TMP_FILE = '.out.tar' + + local fh = io.open(inFile, 'rb') or error('Error opening ' .. inFile) + + fs.mount(TMP_FILE, 'ramfs', 'file') + local ofh = io.open(TMP_FILE, 'wb') + + DEFLATE.gunzip {input=fh, output=ofh, disable_crc=true} + + fh:close() + ofh:close() + + local s, m = tar.untar(TMP_FILE, outDir, true) + + fs.delete(TMP_FILE) + + if not s then + error(m) + end +else + local s, m = tar.untar(inFile, outDir) + if not s then + error(m) + end +end diff --git a/debugger/apis/init.lua b/debugger/apis/init.lua index 44e4b16..a7b97d3 100644 --- a/debugger/apis/init.lua +++ b/debugger/apis/init.lua @@ -9,10 +9,33 @@ local dbg = { breakpoints = nil, } +local romFiles = { + load = function(self) + local function recurse(dir) + local files = fs.list(dir) + for _,f in ipairs(files) do + local fullName = fs.combine(dir, f) + if fs.isDir(fullName) then + recurse(fullName) + else + self.files[f] = fullName + end + end + end + recurse('rom/apis') + end, + get = function(self, file) + return self.files[file] + end, + files = { }, +} +romFiles:load() + local function breakpointHook(info) if dbg.breakpoints then + local src = romFiles:get(info.short_src) or info.short_src for _,v in pairs(dbg.breakpoints) do - if v.line == info.currentline and v.file == info.short_src then + if v.line == info.currentline and v.file == src then return not v.disabled end end diff --git a/debugger/debug.lua b/debugger/debug.lua index b26b675..e2cfbe3 100644 --- a/debugger/debug.lua +++ b/debugger/debug.lua @@ -376,7 +376,7 @@ local page = UI.Page { local line = self.source:getSelected().line multishell.openTab(_ENV, { path = 'sys/apps/shell.lua', - args = { ('edit --line=%d %s'):format(line , file) }, + args = { ('edit --line=%d %s'):format(line , '/' .. file) }, focused = true, }) end diff --git a/tests/test-ramfs.lua b/tests/test-ramfs.lua new file mode 100644 index 0000000..caa57bc --- /dev/null +++ b/tests/test-ramfs.lua @@ -0,0 +1,30 @@ +local ramfs = require('opus.fs.ramfs') + +local fs = _G.fs + +local str = 'hello' + +local node = fs.mount('test.bin', 'ramfs', 'file') +local file = ramfs.open(node, 'test.bin', 'wb') +for i = 1, 5 do + file.write(str:sub(i, i)) +end +file.close() + +node = fs.getNode('test.bin') +file = ramfs.open(node, 'test.bin', 'r') +local s = file.read(5) +file.close() + +assert(s == str) + +file = ramfs.open(node, 'test.bin', 'w') +file.write(str) +file.close() + +node = fs.getNode('test.bin') +file = ramfs.open(node, 'test.bin', 'rb') +s = file.read(5) +file.close() + +assert(s == str) diff --git a/tests/test-tar.lua b/tests/test-tar.lua new file mode 100644 index 0000000..272b3ed --- /dev/null +++ b/tests/test-tar.lua @@ -0,0 +1,20 @@ +local tar = require('compress.tar') +local Util = require('opus.util') + +local fs = _G.fs + +local files = { + 'test-ramfs.lua', + 'test-tar.lua', +} + +fs.mount('packages/tests/test.tar', 'ramfs', 'file') +tar.tar('packages/tests/test.tar', 'packages/tests', files) + +fs.mount('packages/tests/untar', 'ramfs', 'directory') +tar.untar('packages/tests/test.tar', 'packages/tests/untar') + +local s1 = Util.readFile('packages/tests/test-ramfs.lua', 'r') +local s2 = Util.readFile('packages/tests/untar/test-ramfs.lua', 'r') + +assert(s1 == s2) -- 2.49.1 From b289594c7f17a1bf9297899b5a118c9d42e510fd Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Mon, 1 Jun 2020 16:54:28 -0600 Subject: [PATCH 60/90] compression package - debugger fixes --- compress/apis/libdeflate.lua | 3525 ++++++++++++++++++++++++++++++++++ compress/apis/tar.lua | 21 +- compress/compress.lua | 53 + compress/uncompress.lua | 66 + compress/untar.lua | 46 - debugger/debug.lua | 22 +- ignore/chars.lua | 15 - ignore/glasses.lua | 196 -- ignore/rabbitRancher.lua | 173 -- lfs/apis/init.lua | 2 +- 10 files changed, 3676 insertions(+), 443 deletions(-) create mode 100644 compress/apis/libdeflate.lua create mode 100644 compress/compress.lua create mode 100644 compress/uncompress.lua delete mode 100644 compress/untar.lua delete mode 100644 ignore/chars.lua delete mode 100644 ignore/glasses.lua delete mode 100644 ignore/rabbitRancher.lua diff --git a/compress/apis/libdeflate.lua b/compress/apis/libdeflate.lua new file mode 100644 index 0000000..b994b5d --- /dev/null +++ b/compress/apis/libdeflate.lua @@ -0,0 +1,3525 @@ +--[[-- +LibDeflate 1.0.1-release
+Pure Lua compressor and decompressor with high compression ratio using +DEFLATE/zlib format. + +@file LibDeflate.lua +@author Haoqian He (Github: SafeteeWoW; World of Warcraft: Safetyy-Illidan(US)) +@copyright LibDeflate <2018-2019> Haoqian He +@license GNU LESSER GENERAL PUBLIC LICENSE Version 3 or later + +This library is implemented according to the following specifications.
+Report a bug if LibDeflate is not fully compliant with those specs.
+Both compressors and decompressors have been implemented in the library.
+1. RFC1950: DEFLATE Compressed Data Format Specification version 1.3
+https://tools.ietf.org/html/rfc1951
+2. RFC1951: ZLIB Compressed Data Format Specification version 3.3
+https://tools.ietf.org/html/rfc1950
+ +This library requires Lua 5.1/5.2/5.3/5.4 interpreter or LuaJIT v2.0+.
+This library does not have any dependencies.
+Note at the time of this release, Lua 5.4 final is not released yet.
+For Lua 5.4, This library is tested with its first beta version.
+
+This file "LibDeflate.lua" is the only source file of +the library.
+Submit suggestions or report bugs to +https://github.com/safeteeWow/LibDeflate/issues +]] + +--[[ +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see https://www.gnu.org/licenses/. + +Credits and Disclaimer: +The following projects are used to the help to test the correctness +of this program. The code of this program (LibDeflate.lua) does not +use their code directly, but uses their ideas and algorithms. Their original +licenses shall be comply when used. + +1. zlib, by Jean-loup Gailly (compression) and Mark Adler (decompression). + http://www.zlib.net/ + Licensed under zlib License. http://www.zlib.net/zlib_license.html + For the compression algorithm. +2. puff, by Mark Adler. https://github.com/madler/zlib/tree/master/contrib/puff + Licensed under zlib License. http://www.zlib.net/zlib_license.html + For the decompression algorithm. +3. LibCompress, by jjsheets and Galmok of European Stormrage (Horde) + https://www.wowace.com/projects/libcompress + Licensed under GPLv2. + https://www.gnu.org/licenses/old-licenses/gpl-2.0.html + For the code to create customized codec. +4. WeakAuras2, + https://github.com/WeakAuras/WeakAuras2 + Licensed under GPLv2. + For the 6bit encoding and decoding. +]] + +--[[ + Curseforge auto-packaging replacements: + + Project Date: @project-date-iso@ + Project Hash: @project-hash@ + Project Version: @project-version@ +--]] + +local LibDeflate + +do + -- Semantic version. all lowercase. + -- Suffix can be alpha1, alpha2, beta1, beta2, rc1, rc2, etc. + -- NOTE: Two version numbers needs to modify. + -- 1. On the top of LibDeflate.lua + -- 2. _VERSION + -- 3. _MINOR + + -- version to store the official version of LibDeflate + local _VERSION = "1.0.1-release" + + -- When MAJOR is changed, I should name it as LibDeflate2 + local _MAJOR = "LibDeflate" + + -- Update this whenever a new version, for LibStub version registration. + local _MINOR = 2 + + local _COPYRIGHT = + "LibDeflate ".._VERSION + .." Copyright (C) 2018-2019 Haoqian He." + .." License LGPLv3+: GNU Lesser General Public License version 3 or later" + + -- Register in the World of Warcraft library "LibStub" if detected. + if LibStub then + local lib, minor = LibStub:GetLibrary(_MAJOR, true) + if lib and minor and minor >= _MINOR then -- No need to update. + return lib + else -- Update or first time register + LibDeflate = LibStub:NewLibrary(_MAJOR, _MINOR) + -- NOTE: It is important that new version has implemented + -- all exported APIs and tables in the old version, + -- so the old library is fully garbage collected, + -- and we 100% ensure the backward compatibility. + end + else -- "LibStub" is not detected. + LibDeflate = {} + end + + LibDeflate._VERSION = _VERSION + LibDeflate._MAJOR = _MAJOR + LibDeflate._MINOR = _MINOR + LibDeflate._COPYRIGHT = _COPYRIGHT +end + +-- localize Lua api for faster access. +local assert = assert +local error = error +local pairs = pairs +local string_byte = string.byte +local string_char = string.char +local string_find = string.find +local string_gsub = string.gsub +local string_sub = string.sub +local table_concat = table.concat +local table_sort = table.sort +local tostring = tostring +local type = type + +-- Converts i to 2^i, (0<=i<=32) +-- This is used to implement bit left shift and bit right shift. +-- "x >> y" in C: "(x-x%_pow2[y])/_pow2[y]" in Lua +-- "x << y" in C: "x*_pow2[y]" in Lua +local _pow2 = {} + +-- Converts any byte to a character, (0<=byte<=255) +local _byte_to_char = {} + +-- _reverseBitsTbl[len][val] stores the bit reverse of +-- the number with bit length "len" and value "val" +-- For example, decimal number 6 with bits length 5 is binary 00110 +-- It's reverse is binary 01100, +-- which is decimal 12 and 12 == _reverseBitsTbl[5][6] +-- 1<=len<=9, 0<=val<=2^len-1 +-- The reason for 1<=len<=9 is that the max of min bitlen of huffman code +-- of a huffman alphabet is 9? +local _reverse_bits_tbl = {} + +-- Convert a LZ77 length (3<=len<=258) to +-- a deflate literal/LZ77_length code (257<=code<=285) +local _length_to_deflate_code = {} + +-- convert a LZ77 length (3<=len<=258) to +-- a deflate literal/LZ77_length code extra bits. +local _length_to_deflate_extra_bits = {} + +-- Convert a LZ77 length (3<=len<=258) to +-- a deflate literal/LZ77_length code extra bit length. +local _length_to_deflate_extra_bitlen = {} + +-- Convert a small LZ77 distance (1<=dist<=256) to a deflate code. +local _dist256_to_deflate_code = {} + +-- Convert a small LZ77 distance (1<=dist<=256) to +-- a deflate distance code extra bits. +local _dist256_to_deflate_extra_bits = {} + +-- Convert a small LZ77 distance (1<=dist<=256) to +-- a deflate distance code extra bit length. +local _dist256_to_deflate_extra_bitlen = {} + +-- Convert a literal/LZ77_length deflate code to LZ77 base length +-- The key of the table is (code - 256), 257<=code<=285 +local _literal_deflate_code_to_base_len = { + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, +} + +-- Convert a literal/LZ77_length deflate code to base LZ77 length extra bits +-- The key of the table is (code - 256), 257<=code<=285 +local _literal_deflate_code_to_extra_bitlen = { + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, +} + +-- Convert a distance deflate code to base LZ77 distance. (0<=code<=29) +local _dist_deflate_code_to_base_dist = { + [0] = 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577, +} + +-- Convert a distance deflate code to LZ77 bits length. (0<=code<=29) +local _dist_deflate_code_to_extra_bitlen = { + [0] = 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, +} + +-- The code order of the first huffman header in the dynamic deflate block. +-- See the page 12 of RFC1951 +local _rle_codes_huffman_bitlen_order = {16, 17, 18, + 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15, +} + +-- The following tables are used by fixed deflate block. +-- The value of these tables are assigned at the bottom of the source. + +-- The huffman code of the literal/LZ77_length deflate codes, +-- in fixed deflate block. +local _fix_block_literal_huffman_code + +-- Convert huffman code of the literal/LZ77_length to deflate codes, +-- in fixed deflate block. +local _fix_block_literal_huffman_to_deflate_code + +-- The bit length of the huffman code of literal/LZ77_length deflate codes, +-- in fixed deflate block. +local _fix_block_literal_huffman_bitlen + +-- The count of each bit length of the literal/LZ77_length deflate codes, +-- in fixed deflate block. +local _fix_block_literal_huffman_bitlen_count + +-- The huffman code of the distance deflate codes, +-- in fixed deflate block. +local _fix_block_dist_huffman_code + +-- Convert huffman code of the distance to deflate codes, +-- in fixed deflate block. +local _fix_block_dist_huffman_to_deflate_code + +-- The bit length of the huffman code of the distance deflate codes, +-- in fixed deflate block. +local _fix_block_dist_huffman_bitlen + +-- The count of each bit length of the huffman code of +-- the distance deflate codes, +-- in fixed deflate block. +local _fix_block_dist_huffman_bitlen_count + +for i = 0, 255 do + _byte_to_char[i] = string_char(i) +end + +do + local pow = 1 + for i = 0, 32 do + _pow2[i] = pow + pow = pow * 2 + end +end + +for i = 1, 9 do + _reverse_bits_tbl[i] = {} + for j=0, _pow2[i+1]-1 do + local reverse = 0 + local value = j + for _ = 1, i do + -- The following line is equivalent to "res | (code %2)" in C. + reverse = reverse - reverse%2 + + (((reverse%2==1) or (value % 2) == 1) and 1 or 0) + value = (value-value%2)/2 + reverse = reverse * 2 + end + _reverse_bits_tbl[i][j] = (reverse-reverse%2)/2 + end +end + +-- The source code is written according to the pattern in the numbers +-- in RFC1951 Page10. +do + local a = 18 + local b = 16 + local c = 265 + local bitlen = 1 + for len = 3, 258 do + if len <= 10 then + _length_to_deflate_code[len] = len + 254 + _length_to_deflate_extra_bitlen[len] = 0 + elseif len == 258 then + _length_to_deflate_code[len] = 285 + _length_to_deflate_extra_bitlen[len] = 0 + else + if len > a then + a = a + b + b = b * 2 + c = c + 4 + bitlen = bitlen + 1 + end + local t = len-a-1+b/2 + _length_to_deflate_code[len] = (t-(t%(b/8)))/(b/8) + c + _length_to_deflate_extra_bitlen[len] = bitlen + _length_to_deflate_extra_bits[len] = t % (b/8) + end + end +end + +-- The source code is written according to the pattern in the numbers +-- in RFC1951 Page11. +do + _dist256_to_deflate_code[1] = 0 + _dist256_to_deflate_code[2] = 1 + _dist256_to_deflate_extra_bitlen[1] = 0 + _dist256_to_deflate_extra_bitlen[2] = 0 + + local a = 3 + local b = 4 + local code = 2 + local bitlen = 0 + for dist = 3, 256 do + if dist > b then + a = a * 2 + b = b * 2 + code = code + 2 + bitlen = bitlen + 1 + end + _dist256_to_deflate_code[dist] = (dist <= a) and code or (code+1) + _dist256_to_deflate_extra_bitlen[dist] = (bitlen < 0) and 0 or bitlen + if b >= 8 then + _dist256_to_deflate_extra_bits[dist] = (dist-b/2-1) % (b/4) + end + end +end + +--- Calculate the Adler-32 checksum of the string.
+-- See RFC1950 Page 9 https://tools.ietf.org/html/rfc1950 for the +-- definition of Adler-32 checksum. +-- @param str [string] the input string to calcuate its Adler-32 checksum. +-- @return [integer] The Adler-32 checksum, which is greater or equal to 0, +-- and less than 2^32 (4294967296). +function LibDeflate:Adler32(str) + -- This function is loop unrolled by better performance. + -- + -- Here is the minimum code: + -- + -- local a = 1 + -- local b = 0 + -- for i=1, #str do + -- local s = string.byte(str, i, i) + -- a = (a+s)%65521 + -- b = (b+a)%65521 + -- end + -- return b*65536+a + if type(str) ~= "string" then + error(("Usage: LibDeflate:Adler32(str):" + .." 'str' - string expected got '%s'."):format(type(str)), 2) + end + local strlen = #str + + local i = 1 + local a = 1 + local b = 0 + while i <= strlen - 15 do + local x1, x2, x3, x4, x5, x6, x7, x8, + x9, x10, x11, x12, x13, x14, x15, x16 = string_byte(str, i, i+15) + b = (b+16*a+16*x1+15*x2+14*x3+13*x4+12*x5+11*x6+10*x7+9*x8+8*x9 + +7*x10+6*x11+5*x12+4*x13+3*x14+2*x15+x16)%65521 + a = (a+x1+x2+x3+x4+x5+x6+x7+x8+x9+x10+x11+x12+x13+x14+x15+x16)%65521 + i = i + 16 + end + while (i <= strlen) do + local x = string_byte(str, i, i) + a = (a + x) % 65521 + b = (b + a) % 65521 + i = i + 1 + end + return (b*65536+a) % 4294967296 +end + +-- Compare adler32 checksum. +-- adler32 should be compared with a mod to avoid sign problem +-- 4072834167 (unsigned) is the same adler32 as -222133129 +local function IsEqualAdler32(actual, expected) + return (actual % 4294967296) == (expected % 4294967296) +end + +--- Create a preset dictionary. +-- +-- This function is not fast, and the memory consumption of the produced +-- dictionary is about 50 times of the input string. Therefore, it is suggestted +-- to run this function only once in your program. +-- +-- It is very important to know that if you do use a preset dictionary, +-- compressors and decompressors MUST USE THE SAME dictionary. That is, +-- dictionary must be created using the same string. If you update your program +-- with a new dictionary, people with the old version won't be able to transmit +-- data with people with the new version. Therefore, changing the dictionary +-- must be very careful. +-- +-- The parameters "strlen" and "adler32" add a layer of verification to ensure +-- the parameter "str" is not modified unintentionally during the program +-- development. +-- +-- @usage local dict_str = "1234567890" +-- +-- -- print(dict_str:len(), LibDeflate:Adler32(dict_str)) +-- -- Hardcode the print result below to verify it to avoid acciently +-- -- modification of 'str' during the program development. +-- -- string length: 10, Adler-32: 187433486, +-- -- Don't calculate string length and its Adler-32 at run-time. +-- +-- local dict = LibDeflate:CreateDictionary(dict_str, 10, 187433486) +-- +-- @param str [string] The string used as the preset dictionary.
+-- You should put stuffs that frequently appears in the dictionary +-- string and preferablely put more frequently appeared stuffs toward the end +-- of the string.
+-- Empty string and string longer than 32768 bytes are not allowed. +-- @param strlen [integer] The length of 'str'. Please pass in this parameter +-- as a hardcoded constant, in order to verify the content of 'str'. The value +-- of this parameter should be known before your program runs. +-- @param adler32 [integer] The Adler-32 checksum of 'str'. Please pass in this +-- parameter as a hardcoded constant, in order to verify the content of 'str'. +-- The value of this parameter should be known before your program runs. +-- @return [table] The dictionary used for preset dictionary compression and +-- decompression. +-- @raise error if 'strlen' does not match the length of 'str', +-- or if 'adler32' does not match the Adler-32 checksum of 'str'. +function LibDeflate:CreateDictionary(str, strlen, adler32) + if type(str) ~= "string" then + error(("Usage: LibDeflate:CreateDictionary(str, strlen, adler32):" + .." 'str' - string expected got '%s'."):format(type(str)), 2) + end + if type(strlen) ~= "number" then + error(("Usage: LibDeflate:CreateDictionary(str, strlen, adler32):" + .." 'strlen' - number expected got '%s'."):format( + type(strlen)), 2) + end + if type(adler32) ~= "number" then + error(("Usage: LibDeflate:CreateDictionary(str, strlen, adler32):" + .." 'adler32' - number expected got '%s'."):format( + type(adler32)), 2) + end + if strlen ~= #str then + error(("Usage: LibDeflate:CreateDictionary(str, strlen, adler32):" + .." 'strlen' does not match the actual length of 'str'." + .." 'strlen': %u, '#str': %u ." + .." Please check if 'str' is modified unintentionally.") + :format(strlen, #str)) + end + if strlen == 0 then + error(("Usage: LibDeflate:CreateDictionary(str, strlen, adler32):" + .." 'str' - Empty string is not allowed."), 2) + end + if strlen > 32768 then + error(("Usage: LibDeflate:CreateDictionary(str, strlen, adler32):" + .." 'str' - string longer than 32768 bytes is not allowed." + .." Got %d bytes."):format(strlen), 2) + end + local actual_adler32 = self:Adler32(str) + if not IsEqualAdler32(adler32, actual_adler32) then + error(("Usage: LibDeflate:CreateDictionary(str, strlen, adler32):" + .." 'adler32' does not match the actual adler32 of 'str'." + .." 'adler32': %u, 'Adler32(str)': %u ." + .." Please check if 'str' is modified unintentionally.") + :format(adler32, actual_adler32)) + end + + local dictionary = {} + dictionary.adler32 = adler32 + dictionary.hash_tables = {} + dictionary.string_table = {} + dictionary.strlen = strlen + local string_table = dictionary.string_table + local hash_tables = dictionary.hash_tables + string_table[1] = string_byte(str, 1, 1) + string_table[2] = string_byte(str, 2, 2) + if strlen >= 3 then + local i = 1 + local hash = string_table[1]*256+string_table[2] + while i <= strlen - 2 - 3 do + local x1, x2, x3, x4 = string_byte(str, i+2, i+5) + string_table[i+2] = x1 + string_table[i+3] = x2 + string_table[i+4] = x3 + string_table[i+5] = x4 + hash = (hash*256+x1)%16777216 + local t = hash_tables[hash] + if not t then t = {}; hash_tables[hash] = t end + t[#t+1] = i-strlen + i = i + 1 + hash = (hash*256+x2)%16777216 + t = hash_tables[hash] + if not t then t = {}; hash_tables[hash] = t end + t[#t+1] = i-strlen + i = i + 1 + hash = (hash*256+x3)%16777216 + t = hash_tables[hash] + if not t then t = {}; hash_tables[hash] = t end + t[#t+1] = i-strlen + i = i + 1 + hash = (hash*256+x4)%16777216 + t = hash_tables[hash] + if not t then t = {}; hash_tables[hash] = t end + t[#t+1] = i-strlen + i = i + 1 + end + while i <= strlen - 2 do + local x = string_byte(str, i+2) + string_table[i+2] = x + hash = (hash*256+x)%16777216 + local t = hash_tables[hash] + if not t then t = {}; hash_tables[hash] = t end + t[#t+1] = i-strlen + i = i + 1 + end + end + return dictionary +end + +-- Check if the dictionary is valid. +-- @param dictionary The preset dictionary for compression and decompression. +-- @return true if valid, false if not valid. +-- @return if not valid, the error message. +local function IsValidDictionary(dictionary) + if type(dictionary) ~= "table" then + return false, ("'dictionary' - table expected got '%s'.") + :format(type(dictionary)) + end + if type(dictionary.adler32) ~= "number" + or type(dictionary.string_table) ~= "table" + or type(dictionary.strlen) ~= "number" + or dictionary.strlen <= 0 + or dictionary.strlen > 32768 + or dictionary.strlen ~= #dictionary.string_table + or type(dictionary.hash_tables) ~= "table" + then + return false, ("'dictionary' - corrupted dictionary.") + :format(type(dictionary)) + end + return true, "" +end + +--[[ + key of the configuration table is the compression level, + and its value stores the compression setting. + These numbers come from zlib source code. + + Higher compression level usually means better compression. + (Because LibDeflate uses a simplified version of zlib algorithm, + there is no guarantee that higher compression level does not create + bigger file than lower level, but I can say it's 99% likely) + + Be careful with the high compression level. This is a pure lua + implementation compressor/decompressor, which is significant slower than + a C/C++ equivalant compressor/decompressor. Very high compression level + costs significant more CPU time, and usually compression size won't be + significant smaller when you increase compression level by 1, when the + level is already very high. Benchmark yourself if you can afford it. + + See also https://github.com/madler/zlib/blob/master/doc/algorithm.txt, + https://github.com/madler/zlib/blob/master/deflate.c for more information. + + The meaning of each field: + @field 1 use_lazy_evaluation: + true/false. Whether the program uses lazy evaluation. + See what is "lazy evaluation" in the link above. + lazy_evaluation improves ratio, but relatively slow. + @field 2 good_prev_length: + Only effective if lazy is set, Only use 1/4 of max_chain, + if prev length of lazy match is above this. + @field 3 max_insert_length/max_lazy_match: + If not using lazy evaluation, + insert new strings in the hash table only if the match length is not + greater than this length. + If using lazy evaluation, only continue lazy evaluation, + if previous match length is strictly smaller than this value. + @field 4 nice_length: + Number. Don't continue to go down the hash chain, + if match length is above this. + @field 5 max_chain: + Number. The maximum number of hash chains we look. +--]] +local _compression_level_configs = { + [0] = {false, nil, 0, 0, 0}, -- level 0, no compression + [1] = {false, nil, 4, 8, 4}, -- level 1, similar to zlib level 1 + [2] = {false, nil, 5, 18, 8}, -- level 2, similar to zlib level 2 + [3] = {false, nil, 6, 32, 32}, -- level 3, similar to zlib level 3 + [4] = {true, 4, 4, 16, 16}, -- level 4, similar to zlib level 4 + [5] = {true, 8, 16, 32, 32}, -- level 5, similar to zlib level 5 + [6] = {true, 8, 16, 128, 128}, -- level 6, similar to zlib level 6 + [7] = {true, 8, 32, 128, 256}, -- (SLOW) level 7, similar to zlib level 7 + [8] = {true, 32, 128, 258, 1024} , --(SLOW) level 8,similar to zlib level 8 + [9] = {true, 32, 258, 258, 4096}, + -- (VERY SLOW) level 9, similar to zlib level 9 +} + +-- Check if the compression/decompression arguments is valid +-- @param str The input string. +-- @param check_dictionary if true, check if dictionary is valid. +-- @param dictionary The preset dictionary for compression and decompression. +-- @param check_configs if true, check if config is valid. +-- @param configs The compression configuration table +-- @return true if valid, false if not valid. +-- @return if not valid, the error message. +local function IsValidArguments(str, + check_dictionary, dictionary, + check_configs, configs) + + if type(str) ~= "string" then + return false, + ("'str' - string expected got '%s'."):format(type(str)) + end + if check_dictionary then + local dict_valid, dict_err = IsValidDictionary(dictionary) + if not dict_valid then + return false, dict_err + end + end + if check_configs then + local type_configs = type(configs) + if type_configs ~= "nil" and type_configs ~= "table" then + return false, + ("'configs' - nil or table expected got '%s'.") + :format(type(configs)) + end + if type_configs == "table" then + for k, v in pairs(configs) do + if k ~= "level" and k ~= "strategy" then + return false, + ("'configs' - unsupported table key in the configs: '%s'.") + :format(k) + elseif k == "level" and not _compression_level_configs[v] then + return false, + ("'configs' - unsupported 'level': %s."):format(tostring(v)) + elseif k == "strategy" and v ~= "fixed" and v ~= "huffman_only" + and v ~= "dynamic" then + -- random_block_type is for testing purpose + return false, ("'configs' - unsupported 'strategy': '%s'.") + :format(tostring(v)) + end + end + end + end + return true, "" +end + + + +--[[ -------------------------------------------------------------------------- + Compress code +--]] -------------------------------------------------------------------------- + +-- partial flush to save memory +local _FLUSH_MODE_MEMORY_CLEANUP = 0 +-- full flush with partial bytes +local _FLUSH_MODE_OUTPUT = 1 +-- write bytes to get to byte boundary +local _FLUSH_MODE_BYTE_BOUNDARY = 2 +-- no flush, just get num of bits written so far +local _FLUSH_MODE_NO_FLUSH = 3 + +--[[ + Create an empty writer to easily write stuffs as the unit of bits. + Return values: + 1. WriteBits(code, bitlen): + 2. WriteString(str): + 3. Flush(mode): +--]] +local function CreateWriter() + local buffer_size = 0 + local cache = 0 + local cache_bitlen = 0 + local total_bitlen = 0 + local buffer = {} + -- When buffer is big enough, flush into result_buffer to save memory. + local result_buffer = {} + + -- Write bits with value "value" and bit length of "bitlen" into writer. + -- @param value: The value being written + -- @param bitlen: The bit length of "value" + -- @return nil + local function WriteBits(value, bitlen) + cache = cache + value * _pow2[cache_bitlen] + cache_bitlen = cache_bitlen + bitlen + total_bitlen = total_bitlen + bitlen + -- Only bulk to buffer every 4 bytes. This is quicker. + if cache_bitlen >= 32 then + buffer_size = buffer_size + 1 + buffer[buffer_size] = + _byte_to_char[cache % 256] + .._byte_to_char[((cache-cache%256)/256 % 256)] + .._byte_to_char[((cache-cache%65536)/65536 % 256)] + .._byte_to_char[((cache-cache%16777216)/16777216 % 256)] + local rshift_mask = _pow2[32 - cache_bitlen + bitlen] + cache = (value - value%rshift_mask)/rshift_mask + cache_bitlen = cache_bitlen - 32 + end + end + + -- Write the entire string into the writer. + -- @param str The string being written + -- @return nil + local function WriteString(str) + for _ = 1, cache_bitlen, 8 do + buffer_size = buffer_size + 1 + buffer[buffer_size] = string_char(cache % 256) + cache = (cache-cache%256)/256 + end + cache_bitlen = 0 + buffer_size = buffer_size + 1 + buffer[buffer_size] = str + total_bitlen = total_bitlen + #str*8 + end + + -- Flush current stuffs in the writer and return it. + -- This operation will free most of the memory. + -- @param mode See the descrtion of the constant and the source code. + -- @return The total number of bits stored in the writer right now. + -- for byte boundary mode, it includes the padding bits. + -- for output mode, it does not include padding bits. + -- @return Return the outputs if mode is output. + local function FlushWriter(mode) + if mode == _FLUSH_MODE_NO_FLUSH then + return total_bitlen + end + + if mode == _FLUSH_MODE_OUTPUT + or mode == _FLUSH_MODE_BYTE_BOUNDARY then + -- Full flush, also output cache. + -- Need to pad some bits if cache_bitlen is not multiple of 8. + local padding_bitlen = (8 - cache_bitlen % 8) % 8 + + if cache_bitlen > 0 then + -- padding with all 1 bits, mainly because "\000" is not + -- good to be tranmitted. I do this so "\000" is a little bit + -- less frequent. + cache = cache - _pow2[cache_bitlen] + + _pow2[cache_bitlen+padding_bitlen] + for _ = 1, cache_bitlen, 8 do + buffer_size = buffer_size + 1 + buffer[buffer_size] = _byte_to_char[cache % 256] + cache = (cache-cache%256)/256 + end + + cache = 0 + cache_bitlen = 0 + end + if mode == _FLUSH_MODE_BYTE_BOUNDARY then + total_bitlen = total_bitlen + padding_bitlen + return total_bitlen + end + end + + local flushed = table_concat(buffer) + buffer = {} + buffer_size = 0 + result_buffer[#result_buffer+1] = flushed + + if mode == _FLUSH_MODE_MEMORY_CLEANUP then + return total_bitlen + else + return total_bitlen, table_concat(result_buffer) + end + end + + return WriteBits, WriteString, FlushWriter +end + +-- Push an element into a max heap +-- @param heap A max heap whose max element is at index 1. +-- @param e The element to be pushed. Assume element "e" is a table +-- and comparison is done via its first entry e[1] +-- @param heap_size current number of elements in the heap. +-- NOTE: There may be some garbage stored in +-- heap[heap_size+1], heap[heap_size+2], etc.. +-- @return nil +local function MinHeapPush(heap, e, heap_size) + heap_size = heap_size + 1 + heap[heap_size] = e + local value = e[1] + local pos = heap_size + local parent_pos = (pos-pos%2)/2 + + while (parent_pos >= 1 and heap[parent_pos][1] > value) do + local t = heap[parent_pos] + heap[parent_pos] = e + heap[pos] = t + pos = parent_pos + parent_pos = (parent_pos-parent_pos%2)/2 + end +end + +-- Pop an element from a max heap +-- @param heap A max heap whose max element is at index 1. +-- @param heap_size current number of elements in the heap. +-- @return the poped element +-- Note: This function does not change table size of "heap" to save CPU time. +local function MinHeapPop(heap, heap_size) + local top = heap[1] + local e = heap[heap_size] + local value = e[1] + heap[1] = e + heap[heap_size] = top + heap_size = heap_size - 1 + + local pos = 1 + local left_child_pos = pos * 2 + local right_child_pos = left_child_pos + 1 + + while (left_child_pos <= heap_size) do + local left_child = heap[left_child_pos] + if (right_child_pos <= heap_size + and heap[right_child_pos][1] < left_child[1]) then + local right_child = heap[right_child_pos] + if right_child[1] < value then + heap[right_child_pos] = e + heap[pos] = right_child + pos = right_child_pos + left_child_pos = pos * 2 + right_child_pos = left_child_pos + 1 + else + break + end + else + if left_child[1] < value then + heap[left_child_pos] = e + heap[pos] = left_child + pos = left_child_pos + left_child_pos = pos * 2 + right_child_pos = left_child_pos + 1 + else + break + end + end + end + + return top +end + +-- Deflate defines a special huffman tree, which is unique once the bit length +-- of huffman code of all symbols are known. +-- @param bitlen_count Number of symbols with a specific bitlen +-- @param symbol_bitlen The bit length of a symbol +-- @param max_symbol The max symbol among all symbols, +-- which is (number of symbols - 1) +-- @param max_bitlen The max huffman bit length among all symbols. +-- @return The huffman code of all symbols. +local function GetHuffmanCodeFromBitlen(bitlen_counts, symbol_bitlens + , max_symbol, max_bitlen) + local huffman_code = 0 + local next_codes = {} + local symbol_huffman_codes = {} + for bitlen = 1, max_bitlen do + huffman_code = (huffman_code+(bitlen_counts[bitlen-1] or 0))*2 + next_codes[bitlen] = huffman_code + end + for symbol = 0, max_symbol do + local bitlen = symbol_bitlens[symbol] + if bitlen then + huffman_code = next_codes[bitlen] + next_codes[bitlen] = huffman_code + 1 + + -- Reverse the bits of huffman code, + -- because most signifant bits of huffman code + -- is stored first into the compressed data. + -- @see RFC1951 Page5 Section 3.1.1 + if bitlen <= 9 then -- Have cached reverse for small bitlen. + symbol_huffman_codes[symbol] = + _reverse_bits_tbl[bitlen][huffman_code] + else + local reverse = 0 + for _ = 1, bitlen do + reverse = reverse - reverse%2 + + (((reverse%2==1) + or (huffman_code % 2) == 1) and 1 or 0) + huffman_code = (huffman_code-huffman_code%2)/2 + reverse = reverse*2 + end + symbol_huffman_codes[symbol] = (reverse-reverse%2)/2 + end + end + end + return symbol_huffman_codes +end + +-- A helper function to sort heap elements +-- a[1], b[1] is the huffman frequency +-- a[2], b[2] is the symbol value. +local function SortByFirstThenSecond(a, b) + return a[1] < b[1] or + (a[1] == b[1] and a[2] < b[2]) +end + +-- Calculate the huffman bit length and huffman code. +-- @param symbol_count: A table whose table key is the symbol, and table value +-- is the symbol frenquency (nil means 0 frequency). +-- @param max_bitlen: See description of return value. +-- @param max_symbol: The maximum symbol +-- @return a table whose key is the symbol, and the value is the huffman bit +-- bit length. We guarantee that all bit length <= max_bitlen. +-- For 0<=symbol<=max_symbol, table value could be nil if the frequency +-- of the symbol is 0 or nil. +-- @return a table whose key is the symbol, and the value is the huffman code. +-- @return a number indicating the maximum symbol whose bitlen is not 0. +local function GetHuffmanBitlenAndCode(symbol_counts, max_bitlen, max_symbol) + local heap_size + local max_non_zero_bitlen_symbol = -1 + local leafs = {} + local heap = {} + local symbol_bitlens = {} + local symbol_codes = {} + local bitlen_counts = {} + + --[[ + tree[1]: weight, temporarily used as parent and bitLengths + tree[2]: symbol + tree[3]: left child + tree[4]: right child + --]] + local number_unique_symbols = 0 + for symbol, count in pairs(symbol_counts) do + number_unique_symbols = number_unique_symbols + 1 + leafs[number_unique_symbols] = {count, symbol} + end + + if (number_unique_symbols == 0) then + -- no code. + return {}, {}, -1 + elseif (number_unique_symbols == 1) then + -- Only one code. In this case, its huffman code + -- needs to be assigned as 0, and bit length is 1. + -- This is the only case that the return result + -- represents an imcomplete huffman tree. + local symbol = leafs[1][2] + symbol_bitlens[symbol] = 1 + symbol_codes[symbol] = 0 + return symbol_bitlens, symbol_codes, symbol + else + table_sort(leafs, SortByFirstThenSecond) + heap_size = number_unique_symbols + for i = 1, heap_size do + heap[i] = leafs[i] + end + + while (heap_size > 1) do + -- Note: pop does not change table size of heap + local leftChild = MinHeapPop(heap, heap_size) + heap_size = heap_size - 1 + local rightChild = MinHeapPop(heap, heap_size) + heap_size = heap_size - 1 + local newNode = + {leftChild[1]+rightChild[1], -1, leftChild, rightChild} + MinHeapPush(heap, newNode, heap_size) + heap_size = heap_size + 1 + end + + -- Number of leafs whose bit length is greater than max_len. + local number_bitlen_overflow = 0 + + -- Calculate bit length of all nodes + local fifo = {heap[1], 0, 0, 0} -- preallocate some spaces. + local fifo_size = 1 + local index = 1 + heap[1][1] = 0 + while (index <= fifo_size) do -- Breath first search + local e = fifo[index] + local bitlen = e[1] + local symbol = e[2] + local left_child = e[3] + local right_child = e[4] + if left_child then + fifo_size = fifo_size + 1 + fifo[fifo_size] = left_child + left_child[1] = bitlen + 1 + end + if right_child then + fifo_size = fifo_size + 1 + fifo[fifo_size] = right_child + right_child[1] = bitlen + 1 + end + index = index + 1 + + if (bitlen > max_bitlen) then + number_bitlen_overflow = number_bitlen_overflow + 1 + bitlen = max_bitlen + end + if symbol >= 0 then + symbol_bitlens[symbol] = bitlen + max_non_zero_bitlen_symbol = + (symbol > max_non_zero_bitlen_symbol) + and symbol or max_non_zero_bitlen_symbol + bitlen_counts[bitlen] = (bitlen_counts[bitlen] or 0) + 1 + end + end + + -- Resolve bit length overflow + -- @see ZLib/trees.c:gen_bitlen(s, desc), for reference + if (number_bitlen_overflow > 0) then + repeat + local bitlen = max_bitlen - 1 + while ((bitlen_counts[bitlen] or 0) == 0) do + bitlen = bitlen - 1 + end + -- move one leaf down the tree + bitlen_counts[bitlen] = bitlen_counts[bitlen] - 1 + -- move one overflow item as its brother + bitlen_counts[bitlen+1] = (bitlen_counts[bitlen+1] or 0) + 2 + bitlen_counts[max_bitlen] = bitlen_counts[max_bitlen] - 1 + number_bitlen_overflow = number_bitlen_overflow - 2 + until (number_bitlen_overflow <= 0) + + index = 1 + for bitlen = max_bitlen, 1, -1 do + local n = bitlen_counts[bitlen] or 0 + while (n > 0) do + local symbol = leafs[index][2] + symbol_bitlens[symbol] = bitlen + n = n - 1 + index = index + 1 + end + end + end + + symbol_codes = GetHuffmanCodeFromBitlen(bitlen_counts, symbol_bitlens, + max_symbol, max_bitlen) + return symbol_bitlens, symbol_codes, max_non_zero_bitlen_symbol + end +end + +-- Calculate the first huffman header in the dynamic huffman block +-- @see RFC1951 Page 12 +-- @param lcode_bitlen: The huffman bit length of literal/LZ77_length. +-- @param max_non_zero_bitlen_lcode: The maximum literal/LZ77_length symbol +-- whose huffman bit length is not zero. +-- @param dcode_bitlen: The huffman bit length of LZ77 distance. +-- @param max_non_zero_bitlen_dcode: The maximum LZ77 distance symbol +-- whose huffman bit length is not zero. +-- @return The run length encoded codes. +-- @return The extra bits. One entry for each rle code that needs extra bits. +-- (code == 16 or 17 or 18). +-- @return The count of appearance of each rle codes. +local function RunLengthEncodeHuffmanBitlen( + lcode_bitlens, + max_non_zero_bitlen_lcode, + dcode_bitlens, + max_non_zero_bitlen_dcode) + local rle_code_tblsize = 0 + local rle_codes = {} + local rle_code_counts = {} + local rle_extra_bits_tblsize = 0 + local rle_extra_bits = {} + local prev = nil + local count = 0 + + -- If there is no distance code, assume one distance code of bit length 0. + -- RFC1951: One distance code of zero bits means that + -- there are no distance codes used at all (the data is all literals). + max_non_zero_bitlen_dcode = (max_non_zero_bitlen_dcode < 0) + and 0 or max_non_zero_bitlen_dcode + local max_code = max_non_zero_bitlen_lcode+max_non_zero_bitlen_dcode+1 + + for code = 0, max_code+1 do + local len = (code <= max_non_zero_bitlen_lcode) + and (lcode_bitlens[code] or 0) + or ((code <= max_code) + and (dcode_bitlens[code-max_non_zero_bitlen_lcode-1] or 0) or nil) + if len == prev then + count = count + 1 + if len ~= 0 and count == 6 then + rle_code_tblsize = rle_code_tblsize + 1 + rle_codes[rle_code_tblsize] = 16 + rle_extra_bits_tblsize = rle_extra_bits_tblsize + 1 + rle_extra_bits[rle_extra_bits_tblsize] = 3 + rle_code_counts[16] = (rle_code_counts[16] or 0) + 1 + count = 0 + elseif len == 0 and count == 138 then + rle_code_tblsize = rle_code_tblsize + 1 + rle_codes[rle_code_tblsize] = 18 + rle_extra_bits_tblsize = rle_extra_bits_tblsize + 1 + rle_extra_bits[rle_extra_bits_tblsize] = 127 + rle_code_counts[18] = (rle_code_counts[18] or 0) + 1 + count = 0 + end + else + if count == 1 then + rle_code_tblsize = rle_code_tblsize + 1 + rle_codes[rle_code_tblsize] = prev + rle_code_counts[prev] = (rle_code_counts[prev] or 0) + 1 + elseif count == 2 then + rle_code_tblsize = rle_code_tblsize + 1 + rle_codes[rle_code_tblsize] = prev + rle_code_tblsize = rle_code_tblsize + 1 + rle_codes[rle_code_tblsize] = prev + rle_code_counts[prev] = (rle_code_counts[prev] or 0) + 2 + elseif count >= 3 then + rle_code_tblsize = rle_code_tblsize + 1 + local rleCode = (prev ~= 0) and 16 or (count <= 10 and 17 or 18) + rle_codes[rle_code_tblsize] = rleCode + rle_code_counts[rleCode] = (rle_code_counts[rleCode] or 0) + 1 + rle_extra_bits_tblsize = rle_extra_bits_tblsize + 1 + rle_extra_bits[rle_extra_bits_tblsize] = + (count <= 10) and (count - 3) or (count - 11) + end + + prev = len + if len and len ~= 0 then + rle_code_tblsize = rle_code_tblsize + 1 + rle_codes[rle_code_tblsize] = len + rle_code_counts[len] = (rle_code_counts[len] or 0) + 1 + count = 0 + else + count = 1 + end + end + end + + return rle_codes, rle_extra_bits, rle_code_counts +end + +-- Load the string into a table, in order to speed up LZ77. +-- Loop unrolled 16 times to speed this function up. +-- @param str The string to be loaded. +-- @param t The load destination +-- @param start str[index] will be the first character to be loaded. +-- @param end str[index] will be the last character to be loaded +-- @param offset str[index] will be loaded into t[index-offset] +-- @return t +local function LoadStringToTable(str, t, start, stop, offset) + local i = start - offset + while i <= stop - 15 - offset do + t[i], t[i+1], t[i+2], t[i+3], t[i+4], t[i+5], t[i+6], t[i+7], t[i+8], + t[i+9], t[i+10], t[i+11], t[i+12], t[i+13], t[i+14], t[i+15] = + string_byte(str, i + offset, i + 15 + offset) + i = i + 16 + end + while (i <= stop - offset) do + t[i] = string_byte(str, i + offset, i + offset) + i = i + 1 + end + return t +end + +-- Do LZ77 process. This function uses the majority of the CPU time. +-- @see zlib/deflate.c:deflate_fast(), zlib/deflate.c:deflate_slow() +-- @see https://github.com/madler/zlib/blob/master/doc/algorithm.txt +-- This function uses the algorithms used above. You should read the +-- algorithm.txt above to understand what is the hash function and the +-- lazy evaluation. +-- +-- The special optimization used here is hash functions used here. +-- The hash function is just the multiplication of the three consective +-- characters. So if the hash matches, it guarantees 3 characters are matched. +-- This optimization can be implemented because Lua table is a hash table. +-- +-- @param level integer that describes compression level. +-- @param string_table table that stores the value of string to be compressed. +-- The index of this table starts from 1. +-- The caller needs to make sure all values needed by this function +-- are loaded. +-- Assume "str" is the origin input string into the compressor +-- str[block_start]..str[block_end+3] needs to be loaded into +-- string_table[block_start-offset]..string_table[block_end-offset] +-- If dictionary is presented, the last 258 bytes of the dictionary +-- needs to be loaded into sing_table[-257..0] +-- (See more in the description of offset.) +-- @param hash_tables. The table key is the hash value (0<=hash<=16777216=256^3) +-- The table value is an array0 that stores the indexes of the +-- input data string to be compressed, such that +-- hash == str[index]*str[index+1]*str[index+2] +-- Indexes are ordered in this array. +-- @param block_start The indexes of the input data string to be compressed. +-- that starts the LZ77 block. +-- @param block_end The indexes of the input data string to be compressed. +-- that stores the LZ77 block. +-- @param offset str[index] is stored in string_table[index-offset], +-- This offset is mainly an optimization to limit the index +-- of string_table, so lua can access this table quicker. +-- @param dictionary See LibDeflate:CreateDictionary +-- @return literal/LZ77_length deflate codes. +-- @return the extra bits of literal/LZ77_length deflate codes. +-- @return the count of each literal/LZ77 deflate code. +-- @return LZ77 distance deflate codes. +-- @return the extra bits of LZ77 distance deflate codes. +-- @return the count of each LZ77 distance deflate code. +local function GetBlockLZ77Result(level, string_table, hash_tables, block_start, + block_end, offset, dictionary) + local config = _compression_level_configs[level] + local config_use_lazy + , config_good_prev_length + , config_max_lazy_match + , config_nice_length + , config_max_hash_chain = + config[1], config[2], config[3], config[4], config[5] + + local config_max_insert_length = (not config_use_lazy) + and config_max_lazy_match or 2147483646 + local config_good_hash_chain = + (config_max_hash_chain-config_max_hash_chain%4/4) + + local hash + + local dict_hash_tables + local dict_string_table + local dict_string_len = 0 + + if dictionary then + dict_hash_tables = dictionary.hash_tables + dict_string_table = dictionary.string_table + dict_string_len = dictionary.strlen + assert(block_start == 1) + if block_end >= block_start and dict_string_len >= 2 then + hash = dict_string_table[dict_string_len-1]*65536 + + dict_string_table[dict_string_len]*256 + string_table[1] + local t = hash_tables[hash] + if not t then t = {}; hash_tables[hash] = t end + t[#t+1] = -1 + end + if block_end >= block_start+1 and dict_string_len >= 1 then + hash = dict_string_table[dict_string_len]*65536 + + string_table[1]*256 + string_table[2] + local t = hash_tables[hash] + if not t then t = {}; hash_tables[hash] = t end + t[#t+1] = 0 + end + end + + hash = (string_table[block_start-offset] or 0)*256 + + (string_table[block_start+1-offset] or 0) + + local lcodes = {} + local lcode_tblsize = 0 + local lcodes_counts = {} + local dcodes = {} + local dcodes_tblsize = 0 + local dcodes_counts = {} + + local lextra_bits = {} + local lextra_bits_tblsize = 0 + local dextra_bits = {} + local dextra_bits_tblsize = 0 + + local match_available = false + local prev_len + local prev_dist + local cur_len = 0 + local cur_dist = 0 + + local index = block_start + local index_end = block_end + (config_use_lazy and 1 or 0) + + -- the zlib source code writes separate code for lazy evaluation and + -- not lazy evaluation, which is easier to understand. + -- I put them together, so it is a bit harder to understand. + -- because I think this is easier for me to maintain it. + while (index <= index_end) do + local string_table_index = index - offset + prev_len = cur_len + prev_dist = cur_dist + cur_len = 0 + + hash = (hash*256+(string_table[string_table_index+2] or 0))%16777216 + + local chain_index + local cur_chain + local hash_chain = hash_tables[hash] + local chain_old_size + if not hash_chain then + chain_old_size = 0 + hash_chain = {} + hash_tables[hash] = hash_chain + if dict_hash_tables then + cur_chain = dict_hash_tables[hash] + chain_index = cur_chain and #cur_chain or 0 + else + chain_index = 0 + end + else + chain_old_size = #hash_chain + cur_chain = hash_chain + chain_index = chain_old_size + end + + if index <= block_end then + hash_chain[chain_old_size+1] = index + end + + if (chain_index > 0 and index + 2 <= block_end + and (not config_use_lazy or prev_len < config_max_lazy_match)) then + + local depth = + (config_use_lazy and prev_len >= config_good_prev_length) + and config_good_hash_chain or config_max_hash_chain + + while chain_index >= 1 and depth > 0 do + local prev = cur_chain[chain_index] + + if index - prev > 32768 then + break + end + if prev < index then + local j = 3 + + if prev >= -257 then + local prev_table_index = prev-offset + -- NOTE for author: + -- j < 258 and index + j <= block_end + -- This is the right condition + while (j < 258 and index + j <= block_end) do + if (string_table[prev_table_index+j] + == string_table[string_table_index+j]) then + j = j + 1 + else + break + end + end + else + local prev_table_index = dict_string_len+prev + -- NOTE for author: + -- j < 258 and index + j <= block_end + -- This is the right condition + while (j < 258 and index + j <= block_end) do + if (dict_string_table[prev_table_index+j] + == string_table[string_table_index+j]) then + j = j + 1 + else + break + end + end + end + if j > cur_len then + cur_len = j + cur_dist = index - prev + end + if cur_len >= config_nice_length then + break + end + end + + chain_index = chain_index - 1 + depth = depth - 1 + if chain_index == 0 and prev > 0 and dict_hash_tables then + cur_chain = dict_hash_tables[hash] + chain_index = cur_chain and #cur_chain or 0 + end + end + end + + if not config_use_lazy then + prev_len, prev_dist = cur_len, cur_dist + end + if ((not config_use_lazy or match_available) + and (prev_len > 3 or (prev_len == 3 and prev_dist < 4096)) + and cur_len <= prev_len )then + local code = _length_to_deflate_code[prev_len] + local length_extra_bits_bitlen = + _length_to_deflate_extra_bitlen[prev_len] + local dist_code, dist_extra_bits_bitlen, dist_extra_bits + if prev_dist <= 256 then -- have cached code for small distance. + dist_code = _dist256_to_deflate_code[prev_dist] + dist_extra_bits = _dist256_to_deflate_extra_bits[prev_dist] + dist_extra_bits_bitlen = + _dist256_to_deflate_extra_bitlen[prev_dist] + else + dist_code = 16 + dist_extra_bits_bitlen = 7 + local a = 384 + local b = 512 + + while true do + if prev_dist <= a then + dist_extra_bits = (prev_dist-(b/2)-1) % (b/4) + break + elseif prev_dist <= b then + dist_extra_bits = (prev_dist-(b/2)-1) % (b/4) + dist_code = dist_code + 1 + break + else + dist_code = dist_code + 2 + dist_extra_bits_bitlen = dist_extra_bits_bitlen + 1 + a = a*2 + b = b*2 + end + end + end + lcode_tblsize = lcode_tblsize + 1 + lcodes[lcode_tblsize] = code + lcodes_counts[code] = (lcodes_counts[code] or 0) + 1 + + dcodes_tblsize = dcodes_tblsize + 1 + dcodes[dcodes_tblsize] = dist_code + dcodes_counts[dist_code] = (dcodes_counts[dist_code] or 0) + 1 + + if length_extra_bits_bitlen > 0 then + local lenExtraBits = _length_to_deflate_extra_bits[prev_len] + lextra_bits_tblsize = lextra_bits_tblsize + 1 + lextra_bits[lextra_bits_tblsize] = lenExtraBits + end + if dist_extra_bits_bitlen > 0 then + dextra_bits_tblsize = dextra_bits_tblsize + 1 + dextra_bits[dextra_bits_tblsize] = dist_extra_bits + end + + for i=index+1, index+prev_len-(config_use_lazy and 2 or 1) do + hash = (hash*256+(string_table[i-offset+2] or 0))%16777216 + if prev_len <= config_max_insert_length then + hash_chain = hash_tables[hash] + if not hash_chain then + hash_chain = {} + hash_tables[hash] = hash_chain + end + hash_chain[#hash_chain+1] = i + end + end + index = index + prev_len - (config_use_lazy and 1 or 0) + match_available = false + elseif (not config_use_lazy) or match_available then + local code = string_table[config_use_lazy + and (string_table_index-1) or string_table_index] + lcode_tblsize = lcode_tblsize + 1 + lcodes[lcode_tblsize] = code + lcodes_counts[code] = (lcodes_counts[code] or 0) + 1 + index = index + 1 + else + match_available = true + index = index + 1 + end + end + + -- Write "end of block" symbol + lcode_tblsize = lcode_tblsize + 1 + lcodes[lcode_tblsize] = 256 + lcodes_counts[256] = (lcodes_counts[256] or 0) + 1 + + return lcodes, lextra_bits, lcodes_counts, dcodes, dextra_bits + , dcodes_counts +end + +-- Get the header data of dynamic block. +-- @param lcodes_count The count of each literal/LZ77_length codes. +-- @param dcodes_count The count of each Lz77 distance codes. +-- @return a lots of stuffs. +-- @see RFC1951 Page 12 +local function GetBlockDynamicHuffmanHeader(lcodes_counts, dcodes_counts) + local lcodes_huffman_bitlens, lcodes_huffman_codes + , max_non_zero_bitlen_lcode = + GetHuffmanBitlenAndCode(lcodes_counts, 15, 285) + local dcodes_huffman_bitlens, dcodes_huffman_codes + , max_non_zero_bitlen_dcode = + GetHuffmanBitlenAndCode(dcodes_counts, 15, 29) + + local rle_deflate_codes, rle_extra_bits, rle_codes_counts = + RunLengthEncodeHuffmanBitlen(lcodes_huffman_bitlens + ,max_non_zero_bitlen_lcode, dcodes_huffman_bitlens + , max_non_zero_bitlen_dcode) + + local rle_codes_huffman_bitlens, rle_codes_huffman_codes = + GetHuffmanBitlenAndCode(rle_codes_counts, 7, 18) + + local HCLEN = 0 + for i = 1, 19 do + local symbol = _rle_codes_huffman_bitlen_order[i] + local length = rle_codes_huffman_bitlens[symbol] or 0 + if length ~= 0 then + HCLEN = i + end + end + + HCLEN = HCLEN - 4 + local HLIT = max_non_zero_bitlen_lcode + 1 - 257 + local HDIST = max_non_zero_bitlen_dcode + 1 - 1 + if HDIST < 0 then HDIST = 0 end + + return HLIT, HDIST, HCLEN, rle_codes_huffman_bitlens + , rle_codes_huffman_codes, rle_deflate_codes, rle_extra_bits + , lcodes_huffman_bitlens, lcodes_huffman_codes + , dcodes_huffman_bitlens, dcodes_huffman_codes +end + +-- Get the size of dynamic block without writing any bits into the writer. +-- @param ... Read the source code of GetBlockDynamicHuffmanHeader() +-- @return the bit length of the dynamic block +local function GetDynamicHuffmanBlockSize(lcodes, dcodes, HCLEN + , rle_codes_huffman_bitlens, rle_deflate_codes + , lcodes_huffman_bitlens, dcodes_huffman_bitlens) + + local block_bitlen = 17 -- 1+2+5+5+4 + block_bitlen = block_bitlen + (HCLEN+4)*3 + + for i = 1, #rle_deflate_codes do + local code = rle_deflate_codes[i] + block_bitlen = block_bitlen + rle_codes_huffman_bitlens[code] + if code >= 16 then + block_bitlen = block_bitlen + + ((code == 16) and 2 or (code == 17 and 3 or 7)) + end + end + + local length_code_count = 0 + for i = 1, #lcodes do + local code = lcodes[i] + local huffman_bitlen = lcodes_huffman_bitlens[code] + block_bitlen = block_bitlen + huffman_bitlen + if code > 256 then -- Length code + length_code_count = length_code_count + 1 + if code > 264 and code < 285 then -- Length code with extra bits + local extra_bits_bitlen = + _literal_deflate_code_to_extra_bitlen[code-256] + block_bitlen = block_bitlen + extra_bits_bitlen + end + local dist_code = dcodes[length_code_count] + local dist_huffman_bitlen = dcodes_huffman_bitlens[dist_code] + block_bitlen = block_bitlen + dist_huffman_bitlen + + if dist_code > 3 then -- dist code with extra bits + local dist_extra_bits_bitlen = (dist_code-dist_code%2)/2 - 1 + block_bitlen = block_bitlen + dist_extra_bits_bitlen + end + end + end + return block_bitlen +end + +-- Write dynamic block. +-- @param ... Read the source code of GetBlockDynamicHuffmanHeader() +local function CompressDynamicHuffmanBlock(WriteBits, is_last_block + , lcodes, lextra_bits, dcodes, dextra_bits, HLIT, HDIST, HCLEN + , rle_codes_huffman_bitlens, rle_codes_huffman_codes + , rle_deflate_codes, rle_extra_bits + , lcodes_huffman_bitlens, lcodes_huffman_codes + , dcodes_huffman_bitlens, dcodes_huffman_codes) + + WriteBits(is_last_block and 1 or 0, 1) -- Last block identifier + WriteBits(2, 2) -- Dynamic Huffman block identifier + + WriteBits(HLIT, 5) + WriteBits(HDIST, 5) + WriteBits(HCLEN, 4) + + for i = 1, HCLEN+4 do + local symbol = _rle_codes_huffman_bitlen_order[i] + local length = rle_codes_huffman_bitlens[symbol] or 0 + WriteBits(length, 3) + end + + local rleExtraBitsIndex = 1 + for i=1, #rle_deflate_codes do + local code = rle_deflate_codes[i] + WriteBits(rle_codes_huffman_codes[code] + , rle_codes_huffman_bitlens[code]) + if code >= 16 then + local extraBits = rle_extra_bits[rleExtraBitsIndex] + WriteBits(extraBits, (code == 16) and 2 or (code == 17 and 3 or 7)) + rleExtraBitsIndex = rleExtraBitsIndex + 1 + end + end + + local length_code_count = 0 + local length_code_with_extra_count = 0 + local dist_code_with_extra_count = 0 + + for i=1, #lcodes do + local deflate_codee = lcodes[i] + local huffman_code = lcodes_huffman_codes[deflate_codee] + local huffman_bitlen = lcodes_huffman_bitlens[deflate_codee] + WriteBits(huffman_code, huffman_bitlen) + if deflate_codee > 256 then -- Length code + length_code_count = length_code_count + 1 + if deflate_codee > 264 and deflate_codee < 285 then + -- Length code with extra bits + length_code_with_extra_count = length_code_with_extra_count + 1 + local extra_bits = lextra_bits[length_code_with_extra_count] + local extra_bits_bitlen = + _literal_deflate_code_to_extra_bitlen[deflate_codee-256] + WriteBits(extra_bits, extra_bits_bitlen) + end + -- Write distance code + local dist_deflate_code = dcodes[length_code_count] + local dist_huffman_code = dcodes_huffman_codes[dist_deflate_code] + local dist_huffman_bitlen = + dcodes_huffman_bitlens[dist_deflate_code] + WriteBits(dist_huffman_code, dist_huffman_bitlen) + + if dist_deflate_code > 3 then -- dist code with extra bits + dist_code_with_extra_count = dist_code_with_extra_count + 1 + local dist_extra_bits = dextra_bits[dist_code_with_extra_count] + local dist_extra_bits_bitlen = + (dist_deflate_code-dist_deflate_code%2)/2 - 1 + WriteBits(dist_extra_bits, dist_extra_bits_bitlen) + end + end + end +end + +-- Get the size of fixed block without writing any bits into the writer. +-- @param lcodes literal/LZ77_length deflate codes +-- @param decodes LZ77 distance deflate codes +-- @return the bit length of the fixed block +local function GetFixedHuffmanBlockSize(lcodes, dcodes) + local block_bitlen = 3 + local length_code_count = 0 + for i=1, #lcodes do + local code = lcodes[i] + local huffman_bitlen = _fix_block_literal_huffman_bitlen[code] + block_bitlen = block_bitlen + huffman_bitlen + if code > 256 then -- Length code + length_code_count = length_code_count + 1 + if code > 264 and code < 285 then -- Length code with extra bits + local extra_bits_bitlen = + _literal_deflate_code_to_extra_bitlen[code-256] + block_bitlen = block_bitlen + extra_bits_bitlen + end + local dist_code = dcodes[length_code_count] + block_bitlen = block_bitlen + 5 + + if dist_code > 3 then -- dist code with extra bits + local dist_extra_bits_bitlen = + (dist_code-dist_code%2)/2 - 1 + block_bitlen = block_bitlen + dist_extra_bits_bitlen + end + end + end + return block_bitlen +end + +-- Write fixed block. +-- @param lcodes literal/LZ77_length deflate codes +-- @param decodes LZ77 distance deflate codes +local function CompressFixedHuffmanBlock(WriteBits, is_last_block, + lcodes, lextra_bits, dcodes, dextra_bits) + WriteBits(is_last_block and 1 or 0, 1) -- Last block identifier + WriteBits(1, 2) -- Fixed Huffman block identifier + local length_code_count = 0 + local length_code_with_extra_count = 0 + local dist_code_with_extra_count = 0 + for i=1, #lcodes do + local deflate_code = lcodes[i] + local huffman_code = _fix_block_literal_huffman_code[deflate_code] + local huffman_bitlen = _fix_block_literal_huffman_bitlen[deflate_code] + WriteBits(huffman_code, huffman_bitlen) + if deflate_code > 256 then -- Length code + length_code_count = length_code_count + 1 + if deflate_code > 264 and deflate_code < 285 then + -- Length code with extra bits + length_code_with_extra_count = length_code_with_extra_count + 1 + local extra_bits = lextra_bits[length_code_with_extra_count] + local extra_bits_bitlen = + _literal_deflate_code_to_extra_bitlen[deflate_code-256] + WriteBits(extra_bits, extra_bits_bitlen) + end + -- Write distance code + local dist_code = dcodes[length_code_count] + local dist_huffman_code = _fix_block_dist_huffman_code[dist_code] + WriteBits(dist_huffman_code, 5) + + if dist_code > 3 then -- dist code with extra bits + dist_code_with_extra_count = dist_code_with_extra_count + 1 + local dist_extra_bits = dextra_bits[dist_code_with_extra_count] + local dist_extra_bits_bitlen = (dist_code-dist_code%2)/2 - 1 + WriteBits(dist_extra_bits, dist_extra_bits_bitlen) + end + end + end +end + +-- Get the size of store block without writing any bits into the writer. +-- @param block_start The start index of the origin input string +-- @param block_end The end index of the origin input string +-- @param Total bit lens had been written into the compressed result before, +-- because store block needs to shift to byte boundary. +-- @return the bit length of the fixed block +local function GetStoreBlockSize(block_start, block_end, total_bitlen) + assert(block_end-block_start+1 <= 65535) + local block_bitlen = 3 + total_bitlen = total_bitlen + 3 + local padding_bitlen = (8-total_bitlen%8)%8 + block_bitlen = block_bitlen + padding_bitlen + block_bitlen = block_bitlen + 32 + block_bitlen = block_bitlen + (block_end - block_start + 1) * 8 + return block_bitlen +end + +-- Write the store block. +-- @param ... lots of stuffs +-- @return nil +local function CompressStoreBlock(WriteBits, WriteString, is_last_block, str + , block_start, block_end, total_bitlen) + assert(block_end-block_start+1 <= 65535) + WriteBits(is_last_block and 1 or 0, 1) -- Last block identifer. + WriteBits(0, 2) -- Store block identifier. + total_bitlen = total_bitlen + 3 + local padding_bitlen = (8-total_bitlen%8)%8 + if padding_bitlen > 0 then + WriteBits(_pow2[padding_bitlen]-1, padding_bitlen) + end + local size = block_end - block_start + 1 + WriteBits(size, 16) + + -- Write size's one's complement + local comp = (255 - size % 256) + (255 - (size-size%256)/256)*256 + WriteBits(comp, 16) + + WriteString(str:sub(block_start, block_end)) +end + +-- Do the deflate +-- Currently using a simple way to determine the block size +-- (This is why the compression ratio is little bit worse than zlib when +-- the input size is very large +-- The first block is 64KB, the following block is 32KB. +-- After each block, there is a memory cleanup operation. +-- This is not a fast operation, but it is needed to save memory usage, so +-- the memory usage does not grow unboundly. If the data size is less than +-- 64KB, then memory cleanup won't happen. +-- This function determines whether to use store/fixed/dynamic blocks by +-- calculating the block size of each block type and chooses the smallest one. +local function Deflate(configs, WriteBits, WriteString, FlushWriter, str + , dictionary) + local string_table = {} + local hash_tables = {} + local is_last_block = nil + local block_start + local block_end + local bitlen_written + local total_bitlen = FlushWriter(_FLUSH_MODE_NO_FLUSH) + local strlen = #str + local offset + + local level + local strategy + if configs then + if configs.level then + level = configs.level + end + if configs.strategy then + strategy = configs.strategy + end + end + + if not level then + if strlen < 2048 then + level = 7 + elseif strlen > 65536 then + level = 3 + else + level = 5 + end + end + + while not is_last_block do + if not block_start then + block_start = 1 + block_end = 64*1024 - 1 + offset = 0 + else + block_start = block_end + 1 + block_end = block_end + 32*1024 + offset = block_start - 32*1024 - 1 + end + + if block_end >= strlen then + block_end = strlen + is_last_block = true + else + is_last_block = false + end + + local lcodes, lextra_bits, lcodes_counts, dcodes, dextra_bits + , dcodes_counts + + local HLIT, HDIST, HCLEN, rle_codes_huffman_bitlens + , rle_codes_huffman_codes, rle_deflate_codes + , rle_extra_bits, lcodes_huffman_bitlens, lcodes_huffman_codes + , dcodes_huffman_bitlens, dcodes_huffman_codes + + local dynamic_block_bitlen + local fixed_block_bitlen + local store_block_bitlen + + if level ~= 0 then + + -- GetBlockLZ77 needs block_start to block_end+3 to be loaded. + LoadStringToTable(str, string_table, block_start, block_end + 3 + , offset) + if block_start == 1 and dictionary then + local dict_string_table = dictionary.string_table + local dict_strlen = dictionary.strlen + for i=0, (-dict_strlen+1)<-257 + and -257 or (-dict_strlen+1), -1 do + string_table[i] = dict_string_table[dict_strlen+i] + end + end + + if strategy == "huffman_only" then + lcodes = {} + LoadStringToTable(str, lcodes, block_start, block_end + , block_start-1) + lextra_bits = {} + lcodes_counts = {} + lcodes[block_end - block_start+2] = 256 -- end of block + for i=1, block_end - block_start+2 do + local code = lcodes[i] + lcodes_counts[code] = (lcodes_counts[code] or 0) + 1 + end + dcodes = {} + dextra_bits = {} + dcodes_counts = {} + else + lcodes, lextra_bits, lcodes_counts, dcodes, dextra_bits + , dcodes_counts = GetBlockLZ77Result(level, string_table + , hash_tables, block_start, block_end, offset, dictionary + ) + end + + HLIT, HDIST, HCLEN, rle_codes_huffman_bitlens + , rle_codes_huffman_codes, rle_deflate_codes + , rle_extra_bits, lcodes_huffman_bitlens, lcodes_huffman_codes + , dcodes_huffman_bitlens, dcodes_huffman_codes = + GetBlockDynamicHuffmanHeader(lcodes_counts, dcodes_counts) + dynamic_block_bitlen = GetDynamicHuffmanBlockSize( + lcodes, dcodes, HCLEN, rle_codes_huffman_bitlens + , rle_deflate_codes, lcodes_huffman_bitlens + , dcodes_huffman_bitlens) + fixed_block_bitlen = GetFixedHuffmanBlockSize(lcodes, dcodes) + end + + store_block_bitlen = GetStoreBlockSize(block_start, block_end + , total_bitlen) + + local min_bitlen = store_block_bitlen + min_bitlen = (fixed_block_bitlen and fixed_block_bitlen < min_bitlen) + and fixed_block_bitlen or min_bitlen + min_bitlen = (dynamic_block_bitlen + and dynamic_block_bitlen < min_bitlen) + and dynamic_block_bitlen or min_bitlen + + if level == 0 or (strategy ~= "fixed" and strategy ~= "dynamic" and + store_block_bitlen == min_bitlen) then + CompressStoreBlock(WriteBits, WriteString, is_last_block + , str, block_start, block_end, total_bitlen) + total_bitlen = total_bitlen + store_block_bitlen + elseif strategy ~= "dynamic" and ( + strategy == "fixed" or fixed_block_bitlen == min_bitlen) then + CompressFixedHuffmanBlock(WriteBits, is_last_block, + lcodes, lextra_bits, dcodes, dextra_bits) + total_bitlen = total_bitlen + fixed_block_bitlen + elseif strategy == "dynamic" or dynamic_block_bitlen == min_bitlen then + CompressDynamicHuffmanBlock(WriteBits, is_last_block, lcodes + , lextra_bits, dcodes, dextra_bits, HLIT, HDIST, HCLEN + , rle_codes_huffman_bitlens, rle_codes_huffman_codes + , rle_deflate_codes, rle_extra_bits + , lcodes_huffman_bitlens, lcodes_huffman_codes + , dcodes_huffman_bitlens, dcodes_huffman_codes) + total_bitlen = total_bitlen + dynamic_block_bitlen + end + + if is_last_block then + bitlen_written = FlushWriter(_FLUSH_MODE_NO_FLUSH) + else + bitlen_written = FlushWriter(_FLUSH_MODE_MEMORY_CLEANUP) + end + + assert(bitlen_written == total_bitlen) + + -- Memory clean up, so memory consumption does not always grow linearly + -- , even if input string is > 64K. + -- Not a very efficient operation, but this operation won't happen + -- when the input data size is less than 64K. + if not is_last_block then + local j + if dictionary and block_start == 1 then + j = 0 + while (string_table[j]) do + string_table[j] = nil + j = j - 1 + end + end + dictionary = nil + j = 1 + for i = block_end-32767, block_end do + string_table[j] = string_table[i-offset] + j = j + 1 + end + + for k, t in pairs(hash_tables) do + local tSize = #t + if tSize > 0 and block_end+1 - t[1] > 32768 then + if tSize == 1 then + hash_tables[k] = nil + else + local new = {} + local newSize = 0 + for i = 2, tSize do + j = t[i] + if block_end+1 - j <= 32768 then + newSize = newSize + 1 + new[newSize] = j + end + end + hash_tables[k] = new + end + end + end + end + end +end + +--- The description to compression configuration table.
+-- Any field can be nil to use its default.
+-- Table with keys other than those below is an invalid table. +-- @class table +-- @name compression_configs +-- @field level The compression level ranged from 0 to 9. 0 is no compression. +-- 9 is the slowest but best compression. Use nil for default level. +-- @field strategy The compression strategy. "fixed" to only use fixed deflate +-- compression block. "dynamic" to only use dynamic block. "huffman_only" to +-- do no LZ77 compression. Only do huffman compression. + + +-- @see LibDeflate:CompressDeflate(str, configs) +-- @see LibDeflate:CompressDeflateWithDict(str, dictionary, configs) +local function CompressDeflateInternal(str, dictionary, configs) + local WriteBits, WriteString, FlushWriter = CreateWriter() + Deflate(configs, WriteBits, WriteString, FlushWriter, str, dictionary) + local total_bitlen, result = FlushWriter(_FLUSH_MODE_OUTPUT) + local padding_bitlen = (8-total_bitlen%8)%8 + return result, padding_bitlen +end + +-- @see LibDeflate:CompressZlib +-- @see LibDeflate:CompressZlibWithDict +local function CompressZlibInternal(str, dictionary, configs) + local WriteBits, WriteString, FlushWriter = CreateWriter() + + local CM = 8 -- Compression method + local CINFO = 7 --Window Size = 32K + local CMF = CINFO*16+CM + WriteBits(CMF, 8) + + local FDIST = dictionary and 1 or 0 + local FLEVEL = 2 -- Default compression + local FLG = FLEVEL*64+FDIST*32 + local FCHECK = (31-(CMF*256+FLG)%31) + -- The FCHECK value must be such that CMF and FLG, + -- when viewed as a 16-bit unsigned integer stored + -- in MSB order (CMF*256 + FLG), is a multiple of 31. + FLG = FLG + FCHECK + WriteBits(FLG, 8) + + if FDIST == 1 then + local adler32 = dictionary.adler32 + local byte0 = adler32 % 256 + adler32 = (adler32 - byte0) / 256 + local byte1 = adler32 % 256 + adler32 = (adler32 - byte1) / 256 + local byte2 = adler32 % 256 + adler32 = (adler32 - byte2) / 256 + local byte3 = adler32 % 256 + WriteBits(byte3, 8) + WriteBits(byte2, 8) + WriteBits(byte1, 8) + WriteBits(byte0, 8) + end + + Deflate(configs, WriteBits, WriteString, FlushWriter, str, dictionary) + FlushWriter(_FLUSH_MODE_BYTE_BOUNDARY) + + local adler32 = LibDeflate:Adler32(str) + + -- Most significant byte first + local byte3 = adler32%256 + adler32 = (adler32 - byte3) / 256 + local byte2 = adler32%256 + adler32 = (adler32 - byte2) / 256 + local byte1 = adler32%256 + adler32 = (adler32 - byte1) / 256 + local byte0 = adler32%256 + + WriteBits(byte0, 8) + WriteBits(byte1, 8) + WriteBits(byte2, 8) + WriteBits(byte3, 8) + local total_bitlen, result = FlushWriter(_FLUSH_MODE_OUTPUT) + local padding_bitlen = (8-total_bitlen%8)%8 + return result, padding_bitlen +end + +--- Compress using the raw deflate format. +-- @param str [string] The data to be compressed. +-- @param configs [table/nil] The configuration table to control the compression +-- . If nil, use the default configuration. +-- @return [string] The compressed data. +-- @return [integer] The number of bits padded at the end of output. +-- 0 <= bits < 8
+-- This means the most significant "bits" of the last byte of the returned +-- compressed data are padding bits and they don't affect decompression. +-- You don't need to use this value unless you want to do some postprocessing +-- to the compressed data. +-- @see compression_configs +-- @see LibDeflate:DecompressDeflate +function LibDeflate:CompressDeflate(str, configs) + local arg_valid, arg_err = IsValidArguments(str, false, nil, true, configs) + if not arg_valid then + error(("Usage: LibDeflate:CompressDeflate(str, configs): " + ..arg_err), 2) + end + return CompressDeflateInternal(str, nil, configs) +end + +--- Compress using the raw deflate format with a preset dictionary. +-- @param str [string] The data to be compressed. +-- @param dictionary [table] The preset dictionary produced by +-- LibDeflate:CreateDictionary +-- @param configs [table/nil] The configuration table to control the compression +-- . If nil, use the default configuration. +-- @return [string] The compressed data. +-- @return [integer] The number of bits padded at the end of output. +-- 0 <= bits < 8
+-- This means the most significant "bits" of the last byte of the returned +-- compressed data are padding bits and they don't affect decompression. +-- You don't need to use this value unless you want to do some postprocessing +-- to the compressed data. +-- @see compression_configs +-- @see LibDeflate:CreateDictionary +-- @see LibDeflate:DecompressDeflateWithDict +function LibDeflate:CompressDeflateWithDict(str, dictionary, configs) + local arg_valid, arg_err = IsValidArguments(str, true, dictionary + , true, configs) + if not arg_valid then + error(("Usage: LibDeflate:CompressDeflateWithDict" + .."(str, dictionary, configs): " + ..arg_err), 2) + end + return CompressDeflateInternal(str, dictionary, configs) +end + +--- Compress using the zlib format. +-- @param str [string] the data to be compressed. +-- @param configs [table/nil] The configuration table to control the compression +-- . If nil, use the default configuration. +-- @return [string] The compressed data. +-- @return [integer] The number of bits padded at the end of output. +-- Should always be 0. +-- Zlib formatted compressed data never has padding bits at the end. +-- @see compression_configs +-- @see LibDeflate:DecompressZlib +function LibDeflate:CompressZlib(str, configs) + local arg_valid, arg_err = IsValidArguments(str, false, nil, true, configs) + if not arg_valid then + error(("Usage: LibDeflate:CompressZlib(str, configs): " + ..arg_err), 2) + end + return CompressZlibInternal(str, nil, configs) +end + +--- Compress using the zlib format with a preset dictionary. +-- @param str [string] the data to be compressed. +-- @param dictionary [table] A preset dictionary produced +-- by LibDeflate:CreateDictionary() +-- @param configs [table/nil] The configuration table to control the compression +-- . If nil, use the default configuration. +-- @return [string] The compressed data. +-- @return [integer] The number of bits padded at the end of output. +-- Should always be 0. +-- Zlib formatted compressed data never has padding bits at the end. +-- @see compression_configs +-- @see LibDeflate:CreateDictionary +-- @see LibDeflate:DecompressZlibWithDict +function LibDeflate:CompressZlibWithDict(str, dictionary, configs) + local arg_valid, arg_err = IsValidArguments(str, true, dictionary + , true, configs) + if not arg_valid then + error(("Usage: LibDeflate:CompressZlibWithDict" + .."(str, dictionary, configs): " + ..arg_err), 2) + end + return CompressZlibInternal(str, dictionary, configs) +end + +--[[ -------------------------------------------------------------------------- + Decompress code +--]] -------------------------------------------------------------------------- + +--[[ + Create a reader to easily reader stuffs as the unit of bits. + Return values: + 1. ReadBits(bitlen) + 2. ReadBytes(bytelen, buffer, buffer_size) + 3. Decode(huffman_bitlen_count, huffman_symbol, min_bitlen) + 4. ReaderBitlenLeft() + 5. SkipToByteBoundary() +--]] +local function CreateReader(input_string) + local input = input_string + local input_strlen = #input_string + local input_next_byte_pos = 1 + local cache_bitlen = 0 + local cache = 0 + + -- Read some bits. + -- To improve speed, this function does not + -- check if the input has been exhausted. + -- Use ReaderBitlenLeft() < 0 to check it. + -- @param bitlen the number of bits to read + -- @return the data is read. + local function ReadBits(bitlen) + local rshift_mask = _pow2[bitlen] + local code + if bitlen <= cache_bitlen then + code = cache % rshift_mask + cache = (cache - code) / rshift_mask + cache_bitlen = cache_bitlen - bitlen + else -- Whether input has been exhausted is not checked. + local lshift_mask = _pow2[cache_bitlen] + local byte1, byte2, byte3, byte4 = string_byte(input + , input_next_byte_pos, input_next_byte_pos+3) + -- This requires lua number to be at least double () + cache = cache + ((byte1 or 0)+(byte2 or 0)*256 + + (byte3 or 0)*65536+(byte4 or 0)*16777216)*lshift_mask + input_next_byte_pos = input_next_byte_pos + 4 + cache_bitlen = cache_bitlen + 32 - bitlen + code = cache % rshift_mask + cache = (cache - code) / rshift_mask + end + return code + end + + -- Read some bytes from the reader. + -- Assume reader is on the byte boundary. + -- @param bytelen The number of bytes to be read. + -- @param buffer The byte read will be stored into this buffer. + -- @param buffer_size The buffer will be modified starting from + -- buffer[buffer_size+1], ending at buffer[buffer_size+bytelen-1] + -- @return the new buffer_size + local function ReadBytes(bytelen, buffer, buffer_size) + assert(cache_bitlen % 8 == 0) + + local byte_from_cache = (cache_bitlen/8 < bytelen) + and (cache_bitlen/8) or bytelen + for _=1, byte_from_cache do + local byte = cache % 256 + buffer_size = buffer_size + 1 + buffer[buffer_size] = string_char(byte) + cache = (cache - byte) / 256 + end + cache_bitlen = cache_bitlen - byte_from_cache*8 + bytelen = bytelen - byte_from_cache + if (input_strlen - input_next_byte_pos - bytelen + 1) * 8 + + cache_bitlen < 0 then + return -1 -- out of input + end + for i=input_next_byte_pos, input_next_byte_pos+bytelen-1 do + buffer_size = buffer_size + 1 + buffer[buffer_size] = string_sub(input, i, i) + end + + input_next_byte_pos = input_next_byte_pos + bytelen + return buffer_size + end + + -- Decode huffman code + -- To improve speed, this function does not check + -- if the input has been exhausted. + -- Use ReaderBitlenLeft() < 0 to check it. + -- Credits for Mark Adler. This code is from puff:Decode() + -- @see puff:Decode(...) + -- @param huffman_bitlen_count + -- @param huffman_symbol + -- @param min_bitlen The minimum huffman bit length of all symbols + -- @return The decoded deflate code. + -- Negative value is returned if decoding fails. + local function Decode(huffman_bitlen_counts, huffman_symbols, min_bitlen) + local code = 0 + local first = 0 + local index = 0 + local count + if min_bitlen > 0 then + if cache_bitlen < 15 and input then + local lshift_mask = _pow2[cache_bitlen] + local byte1, byte2, byte3, byte4 = + string_byte(input, input_next_byte_pos + , input_next_byte_pos+3) + -- This requires lua number to be at least double () + cache = cache + ((byte1 or 0)+(byte2 or 0)*256 + +(byte3 or 0)*65536+(byte4 or 0)*16777216)*lshift_mask + input_next_byte_pos = input_next_byte_pos + 4 + cache_bitlen = cache_bitlen + 32 + end + + local rshift_mask = _pow2[min_bitlen] + cache_bitlen = cache_bitlen - min_bitlen + code = cache % rshift_mask + cache = (cache - code) / rshift_mask + -- Reverse the bits + code = _reverse_bits_tbl[min_bitlen][code] + + count = huffman_bitlen_counts[min_bitlen] + if code < count then + return huffman_symbols[code] + end + index = count + first = count * 2 + code = code * 2 + end + + for bitlen = min_bitlen+1, 15 do + local bit + bit = cache % 2 + cache = (cache - bit) / 2 + cache_bitlen = cache_bitlen - 1 + + code = (bit==1) and (code + 1 - code % 2) or code + count = huffman_bitlen_counts[bitlen] or 0 + local diff = code - first + if diff < count then + return huffman_symbols[index + diff] + end + index = index + count + first = first + count + first = first * 2 + code = code * 2 + end + -- invalid literal/length or distance code + -- in fixed or dynamic block (run out of code) + return -10 + end + + local function ReaderBitlenLeft() + return (input_strlen - input_next_byte_pos + 1) * 8 + cache_bitlen + end + + local function SkipToByteBoundary() + local skipped_bitlen = cache_bitlen%8 + local rshift_mask = _pow2[skipped_bitlen] + cache_bitlen = cache_bitlen - skipped_bitlen + cache = (cache - cache % rshift_mask) / rshift_mask + end + + return ReadBits, ReadBytes, Decode, ReaderBitlenLeft, SkipToByteBoundary +end + +-- Create a deflate state, so I can pass in less arguments to functions. +-- @param str the whole string to be decompressed. +-- @param dictionary The preset dictionary. nil if not provided. +-- This dictionary should be produced by LibDeflate:CreateDictionary(str) +-- @return The decomrpess state. +local function CreateDecompressState(str, dictionary) + local ReadBits, ReadBytes, Decode, ReaderBitlenLeft + , SkipToByteBoundary = CreateReader(str) + local state = + { + ReadBits = ReadBits, + ReadBytes = ReadBytes, + Decode = Decode, + ReaderBitlenLeft = ReaderBitlenLeft, + SkipToByteBoundary = SkipToByteBoundary, + buffer_size = 0, + buffer = {}, + result_buffer = {}, + dictionary = dictionary, + } + return state +end + +-- Get the stuffs needed to decode huffman codes +-- @see puff.c:construct(...) +-- @param huffman_bitlen The huffman bit length of the huffman codes. +-- @param max_symbol The maximum symbol +-- @param max_bitlen The min huffman bit length of all codes +-- @return zero or positive for success, negative for failure. +-- @return The count of each huffman bit length. +-- @return A table to convert huffman codes to deflate codes. +-- @return The minimum huffman bit length. +local function GetHuffmanForDecode(huffman_bitlens, max_symbol, max_bitlen) + local huffman_bitlen_counts = {} + local min_bitlen = max_bitlen + for symbol = 0, max_symbol do + local bitlen = huffman_bitlens[symbol] or 0 + min_bitlen = (bitlen > 0 and bitlen < min_bitlen) + and bitlen or min_bitlen + huffman_bitlen_counts[bitlen] = (huffman_bitlen_counts[bitlen] or 0)+1 + end + + if huffman_bitlen_counts[0] == max_symbol+1 then -- No Codes + return 0, huffman_bitlen_counts, {}, 0 -- Complete, but decode will fail + end + + local left = 1 + for len = 1, max_bitlen do + left = left * 2 + left = left - (huffman_bitlen_counts[len] or 0) + if left < 0 then + return left -- Over-subscribed, return negative + end + end + + -- Generate offsets info symbol table for each length for sorting + local offsets = {} + offsets[1] = 0 + for len = 1, max_bitlen-1 do + offsets[len + 1] = offsets[len] + (huffman_bitlen_counts[len] or 0) + end + + local huffman_symbols = {} + for symbol = 0, max_symbol do + local bitlen = huffman_bitlens[symbol] or 0 + if bitlen ~= 0 then + local offset = offsets[bitlen] + huffman_symbols[offset] = symbol + offsets[bitlen] = offsets[bitlen] + 1 + end + end + + -- Return zero for complete set, positive for incomplete set. + return left, huffman_bitlen_counts, huffman_symbols, min_bitlen +end + +-- Decode a fixed or dynamic huffman blocks, excluding last block identifier +-- and block type identifer. +-- @see puff.c:codes() +-- @param state decompression state that will be modified by this function. +-- @see CreateDecompressState +-- @param ... Read the source code +-- @return 0 on success, other value on failure. +local function DecodeUntilEndOfBlock(state, lcodes_huffman_bitlens + , lcodes_huffman_symbols, lcodes_huffman_min_bitlen + , dcodes_huffman_bitlens, dcodes_huffman_symbols + , dcodes_huffman_min_bitlen) + local buffer, buffer_size, ReadBits, Decode, ReaderBitlenLeft + , result_buffer = + state.buffer, state.buffer_size, state.ReadBits, state.Decode + , state.ReaderBitlenLeft, state.result_buffer + local dictionary = state.dictionary + local dict_string_table + local dict_strlen + + local buffer_end = 1 + if dictionary and not buffer[0] then + -- If there is a dictionary, copy the last 258 bytes into + -- the string_table to make the copy in the main loop quicker. + -- This is done only once per decompression. + dict_string_table = dictionary.string_table + dict_strlen = dictionary.strlen + buffer_end = -dict_strlen + 1 + for i=0, (-dict_strlen+1)<-257 and -257 or (-dict_strlen+1), -1 do + buffer[i] = _byte_to_char[dict_string_table[dict_strlen+i]] + end + end + + repeat + local symbol = Decode(lcodes_huffman_bitlens + , lcodes_huffman_symbols, lcodes_huffman_min_bitlen) + if symbol < 0 or symbol > 285 then + -- invalid literal/length or distance code in fixed or dynamic block + return -10 + elseif symbol < 256 then -- Literal + buffer_size = buffer_size + 1 + buffer[buffer_size] = _byte_to_char[symbol] + elseif symbol > 256 then -- Length code + symbol = symbol - 256 + local bitlen = _literal_deflate_code_to_base_len[symbol] + bitlen = (symbol >= 8) + and (bitlen + + ReadBits(_literal_deflate_code_to_extra_bitlen[symbol])) + or bitlen + symbol = Decode(dcodes_huffman_bitlens, dcodes_huffman_symbols + , dcodes_huffman_min_bitlen) + if symbol < 0 or symbol > 29 then + -- invalid literal/length or distance code in fixed or dynamic block + return -10 + end + local dist = _dist_deflate_code_to_base_dist[symbol] + dist = (dist > 4) and (dist + + ReadBits(_dist_deflate_code_to_extra_bitlen[symbol])) or dist + + local char_buffer_index = buffer_size-dist+1 + if char_buffer_index < buffer_end then + -- distance is too far back in fixed or dynamic block + return -11 + end + if char_buffer_index >= -257 then + for _=1, bitlen do + buffer_size = buffer_size + 1 + buffer[buffer_size] = buffer[char_buffer_index] + char_buffer_index = char_buffer_index + 1 + end + else + char_buffer_index = dict_strlen + char_buffer_index + for _=1, bitlen do + buffer_size = buffer_size + 1 + buffer[buffer_size] = + _byte_to_char[dict_string_table[char_buffer_index]] + char_buffer_index = char_buffer_index + 1 + end + end + end + + if ReaderBitlenLeft() < 0 then + return 2 -- available inflate data did not terminate + end + + if buffer_size >= 65536 then + result_buffer[#result_buffer+1] = + table_concat(buffer, "", 1, 32768) + for i=32769, buffer_size do + buffer[i-32768] = buffer[i] + end + buffer_size = buffer_size - 32768 + buffer[buffer_size+1] = nil + -- NOTE: buffer[32769..end] and buffer[-257..0] are not cleared. + -- This is why "buffer_size" variable is needed. + end + until symbol == 256 + + state.buffer_size = buffer_size + + return 0 +end + +-- Decompress a store block +-- @param state decompression state that will be modified by this function. +-- @return 0 if succeeds, other value if fails. +local function DecompressStoreBlock(state) + local buffer, buffer_size, ReadBits, ReadBytes, ReaderBitlenLeft + , SkipToByteBoundary, result_buffer = + state.buffer, state.buffer_size, state.ReadBits, state.ReadBytes + , state.ReaderBitlenLeft, state.SkipToByteBoundary, state.result_buffer + + SkipToByteBoundary() + local bytelen = ReadBits(16) + if ReaderBitlenLeft() < 0 then + return 2 -- available inflate data did not terminate + end + local bytelenComp = ReadBits(16) + if ReaderBitlenLeft() < 0 then + return 2 -- available inflate data did not terminate + end + + if bytelen % 256 + bytelenComp % 256 ~= 255 then + return -2 -- Not one's complement + end + if (bytelen-bytelen % 256)/256 + + (bytelenComp-bytelenComp % 256)/256 ~= 255 then + return -2 -- Not one's complement + end + + -- Note that ReadBytes will skip to the next byte boundary first. + buffer_size = ReadBytes(bytelen, buffer, buffer_size) + if buffer_size < 0 then + return 2 -- available inflate data did not terminate + end + + -- memory clean up when there are enough bytes in the buffer. + if buffer_size >= 65536 then + result_buffer[#result_buffer+1] = table_concat(buffer, "", 1, 32768) + for i=32769, buffer_size do + buffer[i-32768] = buffer[i] + end + buffer_size = buffer_size - 32768 + buffer[buffer_size+1] = nil + end + state.buffer_size = buffer_size + return 0 +end + +-- Decompress a fixed block +-- @param state decompression state that will be modified by this function. +-- @return 0 if succeeds other value if fails. +local function DecompressFixBlock(state) + return DecodeUntilEndOfBlock(state + , _fix_block_literal_huffman_bitlen_count + , _fix_block_literal_huffman_to_deflate_code, 7 + , _fix_block_dist_huffman_bitlen_count + , _fix_block_dist_huffman_to_deflate_code, 5) +end + +-- Decompress a dynamic block +-- @param state decompression state that will be modified by this function. +-- @return 0 if success, other value if fails. +local function DecompressDynamicBlock(state) + local ReadBits, Decode = state.ReadBits, state.Decode + local nlen = ReadBits(5) + 257 + local ndist = ReadBits(5) + 1 + local ncode = ReadBits(4) + 4 + if nlen > 286 or ndist > 30 then + -- dynamic block code description: too many length or distance codes + return -3 + end + + local rle_codes_huffman_bitlens = {} + + for i = 1, ncode do + rle_codes_huffman_bitlens[_rle_codes_huffman_bitlen_order[i]] = + ReadBits(3) + end + + local rle_codes_err, rle_codes_huffman_bitlen_counts, + rle_codes_huffman_symbols, rle_codes_huffman_min_bitlen = + GetHuffmanForDecode(rle_codes_huffman_bitlens, 18, 7) + if rle_codes_err ~= 0 then -- Require complete code set here + -- dynamic block code description: code lengths codes incomplete + return -4 + end + + local lcodes_huffman_bitlens = {} + local dcodes_huffman_bitlens = {} + -- Read length/literal and distance code length tables + local index = 0 + while index < nlen + ndist do + local symbol -- Decoded value + local bitlen -- Last length to repeat + + symbol = Decode(rle_codes_huffman_bitlen_counts + , rle_codes_huffman_symbols, rle_codes_huffman_min_bitlen) + + if symbol < 0 then + return symbol -- Invalid symbol + elseif symbol < 16 then + if index < nlen then + lcodes_huffman_bitlens[index] = symbol + else + dcodes_huffman_bitlens[index-nlen] = symbol + end + index = index + 1 + else + bitlen = 0 + if symbol == 16 then + if index == 0 then + -- dynamic block code description: repeat lengths + -- with no first length + return -5 + end + if index-1 < nlen then + bitlen = lcodes_huffman_bitlens[index-1] + else + bitlen = dcodes_huffman_bitlens[index-nlen-1] + end + symbol = 3 + ReadBits(2) + elseif symbol == 17 then -- Repeat zero 3..10 times + symbol = 3 + ReadBits(3) + else -- == 18, repeat zero 11.138 times + symbol = 11 + ReadBits(7) + end + if index + symbol > nlen + ndist then + -- dynamic block code description: + -- repeat more than specified lengths + return -6 + end + while symbol > 0 do -- Repeat last or zero symbol times + symbol = symbol - 1 + if index < nlen then + lcodes_huffman_bitlens[index] = bitlen + else + dcodes_huffman_bitlens[index-nlen] = bitlen + end + index = index + 1 + end + end + end + + if (lcodes_huffman_bitlens[256] or 0) == 0 then + -- dynamic block code description: missing end-of-block code + return -9 + end + + local lcodes_err, lcodes_huffman_bitlen_counts + , lcodes_huffman_symbols, lcodes_huffman_min_bitlen = + GetHuffmanForDecode(lcodes_huffman_bitlens, nlen-1, 15) + --dynamic block code description: invalid literal/length code lengths, + -- Incomplete code ok only for single length 1 code + if (lcodes_err ~=0 and (lcodes_err < 0 + or nlen ~= (lcodes_huffman_bitlen_counts[0] or 0) + +(lcodes_huffman_bitlen_counts[1] or 0))) then + return -7 + end + + local dcodes_err, dcodes_huffman_bitlen_counts + , dcodes_huffman_symbols, dcodes_huffman_min_bitlen = + GetHuffmanForDecode(dcodes_huffman_bitlens, ndist-1, 15) + -- dynamic block code description: invalid distance code lengths, + -- Incomplete code ok only for single length 1 code + if (dcodes_err ~=0 and (dcodes_err < 0 + or ndist ~= (dcodes_huffman_bitlen_counts[0] or 0) + + (dcodes_huffman_bitlen_counts[1] or 0))) then + return -8 + end + + -- Build buffman table for literal/length codes + return DecodeUntilEndOfBlock(state, lcodes_huffman_bitlen_counts + , lcodes_huffman_symbols, lcodes_huffman_min_bitlen + , dcodes_huffman_bitlen_counts, dcodes_huffman_symbols + , dcodes_huffman_min_bitlen) +end + +-- Decompress a deflate stream +-- @param state: a decompression state +-- @return the decompressed string if succeeds. nil if fails. +local function Inflate(state) + local ReadBits = state.ReadBits + + local is_last_block + while not is_last_block do + is_last_block = (ReadBits(1) == 1) + local block_type = ReadBits(2) + local status + if block_type == 0 then + status = DecompressStoreBlock(state) + elseif block_type == 1 then + status = DecompressFixBlock(state) + elseif block_type == 2 then + status = DecompressDynamicBlock(state) + else + return nil, -1 -- invalid block type (type == 3) + end + if status ~= 0 then + return nil, status + end + end + + state.result_buffer[#state.result_buffer+1] = + table_concat(state.buffer, "", 1, state.buffer_size) + local result = table_concat(state.result_buffer) + return result +end + +-- @see LibDeflate:DecompressDeflate(str) +-- @see LibDeflate:DecompressDeflateWithDict(str, dictionary) +local function DecompressDeflateInternal(str, dictionary) + local state = CreateDecompressState(str, dictionary) + local result, status = Inflate(state) + if not result then + return nil, status + end + + local bitlen_left = state.ReaderBitlenLeft() + local bytelen_left = (bitlen_left - bitlen_left % 8) / 8 + return result, bytelen_left +end + +-- @see LibDeflate:DecompressZlib(str) +-- @see LibDeflate:DecompressZlibWithDict(str) +local function DecompressZlibInternal(str, dictionary) + local state = CreateDecompressState(str, dictionary) + local ReadBits = state.ReadBits + + local CMF = ReadBits(8) + if state.ReaderBitlenLeft() < 0 then + return nil, 2 -- available inflate data did not terminate + end + local CM = CMF % 16 + local CINFO = (CMF - CM) / 16 + if CM ~= 8 then + return nil, -12 -- invalid compression method + end + if CINFO > 7 then + return nil, -13 -- invalid window size + end + + local FLG = ReadBits(8) + if state.ReaderBitlenLeft() < 0 then + return nil, 2 -- available inflate data did not terminate + end + if (CMF*256+FLG)%31 ~= 0 then + return nil, -14 -- invalid header checksum + end + + local FDIST = ((FLG-FLG%32)/32 % 2) + local FLEVEL = ((FLG-FLG%64)/64 % 4) -- luacheck: ignore FLEVEL + + if FDIST == 1 then + if not dictionary then + return nil, -16 -- need dictonary, but dictionary is not provided. + end + local byte3 = ReadBits(8) + local byte2 = ReadBits(8) + local byte1 = ReadBits(8) + local byte0 = ReadBits(8) + local actual_adler32 = byte3*16777216+byte2*65536+byte1*256+byte0 + if state.ReaderBitlenLeft() < 0 then + return nil, 2 -- available inflate data did not terminate + end + if not IsEqualAdler32(actual_adler32, dictionary.adler32) then + return nil, -17 -- dictionary adler32 does not match + end + end + local result, status = Inflate(state) + if not result then + return nil, status + end + state.SkipToByteBoundary() + + local adler_byte0 = ReadBits(8) + local adler_byte1 = ReadBits(8) + local adler_byte2 = ReadBits(8) + local adler_byte3 = ReadBits(8) + if state.ReaderBitlenLeft() < 0 then + return nil, 2 -- available inflate data did not terminate + end + + local adler32_expected = adler_byte0*16777216 + + adler_byte1*65536 + adler_byte2*256 + adler_byte3 + local adler32_actual = LibDeflate:Adler32(result) + if not IsEqualAdler32(adler32_expected, adler32_actual) then + return nil, -15 -- Adler32 checksum does not match + end + + local bitlen_left = state.ReaderBitlenLeft() + local bytelen_left = (bitlen_left - bitlen_left % 8) / 8 + return result, bytelen_left +end + +--- Decompress a raw deflate compressed data. +-- @param str [string] The data to be decompressed. +-- @return [string/nil] If the decompression succeeds, return the decompressed +-- data. If the decompression fails, return nil. You should check if this return +-- value is non-nil to know if the decompression succeeds. +-- @return [integer] If the decompression succeeds, return the number of +-- unprocessed bytes in the input compressed data. This return value is a +-- positive integer if the input data is a valid compressed data appended by an +-- arbitary non-empty string. This return value is 0 if the input data does not +-- contain any extra bytes.
+-- If the decompression fails (The first return value of this function is nil), +-- this return value is undefined. +-- @see LibDeflate:CompressDeflate +function LibDeflate:DecompressDeflate(str) + local arg_valid, arg_err = IsValidArguments(str) + if not arg_valid then + error(("Usage: LibDeflate:DecompressDeflate(str): " + ..arg_err), 2) + end + return DecompressDeflateInternal(str) +end + +--- Decompress a raw deflate compressed data with a preset dictionary. +-- @param str [string] The data to be decompressed. +-- @param dictionary [table] The preset dictionary used by +-- LibDeflate:CompressDeflateWithDict when the compressed data is produced. +-- Decompression and compression must use the same dictionary. +-- Otherwise wrong decompressed data could be produced without generating any +-- error. +-- @return [string/nil] If the decompression succeeds, return the decompressed +-- data. If the decompression fails, return nil. You should check if this return +-- value is non-nil to know if the decompression succeeds. +-- @return [integer] If the decompression succeeds, return the number of +-- unprocessed bytes in the input compressed data. This return value is a +-- positive integer if the input data is a valid compressed data appended by an +-- arbitary non-empty string. This return value is 0 if the input data does not +-- contain any extra bytes.
+-- If the decompression fails (The first return value of this function is nil), +-- this return value is undefined. +-- @see LibDeflate:CompressDeflateWithDict +function LibDeflate:DecompressDeflateWithDict(str, dictionary) + local arg_valid, arg_err = IsValidArguments(str, true, dictionary) + if not arg_valid then + error(("Usage: LibDeflate:DecompressDeflateWithDict(str, dictionary): " + ..arg_err), 2) + end + return DecompressDeflateInternal(str, dictionary) +end + +--- Decompress a zlib compressed data. +-- @param str [string] The data to be decompressed +-- @return [string/nil] If the decompression succeeds, return the decompressed +-- data. If the decompression fails, return nil. You should check if this return +-- value is non-nil to know if the decompression succeeds. +-- @return [integer] If the decompression succeeds, return the number of +-- unprocessed bytes in the input compressed data. This return value is a +-- positive integer if the input data is a valid compressed data appended by an +-- arbitary non-empty string. This return value is 0 if the input data does not +-- contain any extra bytes.
+-- If the decompression fails (The first return value of this function is nil), +-- this return value is undefined. +-- @see LibDeflate:CompressZlib +function LibDeflate:DecompressZlib(str) + local arg_valid, arg_err = IsValidArguments(str) + if not arg_valid then + error(("Usage: LibDeflate:DecompressZlib(str): " + ..arg_err), 2) + end + return DecompressZlibInternal(str) +end + +--- Decompress a zlib compressed data with a preset dictionary. +-- @param str [string] The data to be decompressed +-- @param dictionary [table] The preset dictionary used by +-- LibDeflate:CompressDeflateWithDict when the compressed data is produced. +-- Decompression and compression must use the same dictionary. +-- Otherwise wrong decompressed data could be produced without generating any +-- error. +-- @return [string/nil] If the decompression succeeds, return the decompressed +-- data. If the decompression fails, return nil. You should check if this return +-- value is non-nil to know if the decompression succeeds. +-- @return [integer] If the decompression succeeds, return the number of +-- unprocessed bytes in the input compressed data. This return value is a +-- positive integer if the input data is a valid compressed data appended by an +-- arbitary non-empty string. This return value is 0 if the input data does not +-- contain any extra bytes.
+-- If the decompression fails (The first return value of this function is nil), +-- this return value is undefined. +-- @see LibDeflate:CompressZlibWithDict +function LibDeflate:DecompressZlibWithDict(str, dictionary) + local arg_valid, arg_err = IsValidArguments(str, true, dictionary) + if not arg_valid then + error(("Usage: LibDeflate:DecompressZlibWithDict(str, dictionary): " + ..arg_err), 2) + end + return DecompressZlibInternal(str, dictionary) +end + +-- Calculate the huffman code of fixed block +do + _fix_block_literal_huffman_bitlen = {} + for sym=0, 143 do + _fix_block_literal_huffman_bitlen[sym] = 8 + end + for sym=144, 255 do + _fix_block_literal_huffman_bitlen[sym] = 9 + end + for sym=256, 279 do + _fix_block_literal_huffman_bitlen[sym] = 7 + end + for sym=280, 287 do + _fix_block_literal_huffman_bitlen[sym] = 8 + end + + _fix_block_dist_huffman_bitlen = {} + for dist=0, 31 do + _fix_block_dist_huffman_bitlen[dist] = 5 + end + local status + status, _fix_block_literal_huffman_bitlen_count + , _fix_block_literal_huffman_to_deflate_code = + GetHuffmanForDecode(_fix_block_literal_huffman_bitlen, 287, 9) + assert(status == 0) + status, _fix_block_dist_huffman_bitlen_count, + _fix_block_dist_huffman_to_deflate_code = + GetHuffmanForDecode(_fix_block_dist_huffman_bitlen, 31, 5) + assert(status == 0) + + _fix_block_literal_huffman_code = + GetHuffmanCodeFromBitlen(_fix_block_literal_huffman_bitlen_count + , _fix_block_literal_huffman_bitlen, 287, 9) + _fix_block_dist_huffman_code = + GetHuffmanCodeFromBitlen(_fix_block_dist_huffman_bitlen_count + , _fix_block_dist_huffman_bitlen, 31, 5) +end + +-- Encoding algorithms +-- Prefix encoding algorithm +-- implemented by Galmok of European Stormrage (Horde), galmok@gmail.com +-- From LibCompress , +-- which is licensed under GPLv2 +-- The code has been modified by the author of LibDeflate. +------------------------------------------------------------------------------ + +-- to be able to match any requested byte value, the search +-- string must be preprocessed characters to escape with %: +-- ( ) . % + - * ? [ ] ^ $ +-- "illegal" byte values: +-- 0 is replaces %z +local _gsub_escape_table = { + ["\000"] = "%z", ["("] = "%(", [")"] = "%)", ["."] = "%.", + ["%"] = "%%", ["+"] = "%+", ["-"] = "%-", ["*"] = "%*", + ["?"] = "%?", ["["] = "%[", ["]"] = "%]", ["^"] = "%^", + ["$"] = "%$", +} + +local function escape_for_gsub(str) + return str:gsub("([%z%(%)%.%%%+%-%*%?%[%]%^%$])", _gsub_escape_table) +end + +--- Create a custom codec with encoder and decoder.
+-- This codec is used to convert an input string to make it not contain +-- some specific bytes. +-- This created codec and the parameters of this function do NOT take +-- localization into account. One byte (0-255) in the string is exactly one +-- character (0-255). +-- Credits to LibCompress. +-- @param reserved_chars [string] The created encoder will ensure encoded +-- data does not contain any single character in reserved_chars. This parameter +-- should be non-empty. +-- @param escape_chars [string] The escape character(s) used in the created +-- codec. The codec converts any character included in reserved\_chars / +-- escape\_chars / map\_chars to (one escape char + one character not in +-- reserved\_chars / escape\_chars / map\_chars). +-- You usually only need to provide a length-1 string for this parameter. +-- Length-2 string is only needed when +-- reserved\_chars + escape\_chars + map\_chars is longer than 127. +-- This parameter should be non-empty. +-- @param map_chars [string] The created encoder will map every +-- reserved\_chars:sub(i, i) (1 <= i <= #map\_chars) to map\_chars:sub(i, i). +-- This parameter CAN be empty string. +-- @return [table/nil] If the codec cannot be created, return nil.
+-- If the codec can be created according to the given +-- parameters, return the codec, which is a encode/decode table. +-- The table contains two functions:
+-- t:Encode(str) returns the encoded string.
+-- t:Decode(str) returns the decoded string if succeeds. nil if fails. +-- @return [nil/string] If the codec is successfully created, return nil. +-- If not, return a string that describes the reason why the codec cannot be +-- created. +-- @usage +-- -- Create an encoder/decoder that maps all "\000" to "\003", +-- -- and escape "\001" (and "\002" and "\003") properly +-- local codec = LibDeflate:CreateCodec("\000\001", "\002", "\003") +-- +-- local encoded = codec:Encode(SOME_STRING) +-- -- "encoded" does not contain "\000" or "\001" +-- local decoded = codec:Decode(encoded) +-- -- assert(decoded == SOME_STRING) +function LibDeflate:CreateCodec(reserved_chars, escape_chars + , map_chars) + -- select a default escape character + if type(reserved_chars) ~= "string" + or type(escape_chars) ~= "string" + or type(map_chars) ~= "string" then + error( + "Usage: LibDeflate:CreateCodec(reserved_chars," + .." escape_chars, map_chars):" + .." All arguments must be string.", 2) + end + + if escape_chars == "" then + return nil, "No escape characters supplied." + end + if #reserved_chars < #map_chars then + return nil, "The number of reserved characters must be" + .." at least as many as the number of mapped chars." + end + if reserved_chars == "" then + return nil, "No characters to encode." + end + + local encode_bytes = reserved_chars..escape_chars..map_chars + -- build list of bytes not available as a suffix to a prefix byte + local taken = {} + for i = 1, #encode_bytes do + local byte = string_byte(encode_bytes, i, i) + if taken[byte] then -- Modified by LibDeflate: + return nil, "There must be no duplicate characters in the" + .." concatenation of reserved_chars, escape_chars and" + .." map_chars." + end + taken[byte] = true + end + + -- Modified by LibDeflate: + -- Store the patterns and replacement in tables for later use. + -- This function is modified that loadstring() lua api is no longer used. + local decode_patterns = {} + local decode_repls = {} + + -- the encoding can be a single gsub + -- , but the decoding can require multiple gsubs + local encode_search = {} + local encode_translate = {} + + -- map single byte to single byte + if #map_chars > 0 then + local decode_search = {} + local decode_translate = {} + for i = 1, #map_chars do + local from = string_sub(reserved_chars, i, i) + local to = string_sub(map_chars, i, i) + encode_translate[from] = to + encode_search[#encode_search+1] = from + decode_translate[to] = from + decode_search[#decode_search+1] = to + end + decode_patterns[#decode_patterns+1] = + "([".. escape_for_gsub(table_concat(decode_search)).."])" + decode_repls[#decode_repls+1] = decode_translate + end + + local escape_char_index = 1 + local escape_char = string_sub(escape_chars + , escape_char_index, escape_char_index) + -- map single byte to double-byte + local r = 0 -- suffix char value to the escapeChar + + local decode_search = {} + local decode_translate = {} + for i = 1, #encode_bytes do + local c = string_sub(encode_bytes, i, i) + if not encode_translate[c] then + -- this loop will update escapeChar and r + while r >= 256 or taken[r] do + -- Bug in LibCompress r81 + -- while r < 256 and taken[r] do + r = r + 1 + if r > 255 then -- switch to next escapeChar + decode_patterns[#decode_patterns+1] = + escape_for_gsub(escape_char) + .."([" + .. escape_for_gsub(table_concat(decode_search)).."])" + decode_repls[#decode_repls+1] = decode_translate + + escape_char_index = escape_char_index + 1 + escape_char = string_sub(escape_chars, escape_char_index + , escape_char_index) + r = 0 + decode_search = {} + decode_translate = {} + + -- Fixes Another bug in LibCompress r82. + -- LibCompress checks this error condition + -- right after "if r > 255 then" + -- This is why error case should also be tested. + if not escape_char or escape_char == "" then + -- actually I don't need to check + -- "not ecape_char", but what if Lua changes + -- the behavior of string.sub() in the future? + -- we are out of escape chars and we need more! + return nil, "Out of escape characters." + end + end + end + + local char_r = _byte_to_char[r] + encode_translate[c] = escape_char..char_r + encode_search[#encode_search+1] = c + decode_translate[char_r] = c + decode_search[#decode_search+1] = char_r + r = r + 1 + end + if i == #encode_bytes then + decode_patterns[#decode_patterns+1] = + escape_for_gsub(escape_char).."([" + .. escape_for_gsub(table_concat(decode_search)).."])" + decode_repls[#decode_repls+1] = decode_translate + end + end + + local codec = {} + + local encode_pattern = "([" + .. escape_for_gsub(table_concat(encode_search)).."])" + local encode_repl = encode_translate + + function codec:Encode(str) + if type(str) ~= "string" then + error(("Usage: codec:Encode(str):" + .." 'str' - string expected got '%s'."):format(type(str)), 2) + end + return string_gsub(str, encode_pattern, encode_repl) + end + + local decode_tblsize = #decode_patterns + local decode_fail_pattern = "([" + .. escape_for_gsub(reserved_chars).."])" + + function codec:Decode(str) + if type(str) ~= "string" then + error(("Usage: codec:Decode(str):" + .." 'str' - string expected got '%s'."):format(type(str)), 2) + end + if string_find(str, decode_fail_pattern) then + return nil + end + for i = 1, decode_tblsize do + str = string_gsub(str, decode_patterns[i], decode_repls[i]) + end + return str + end + + return codec +end + +local _addon_channel_codec + +local function GenerateWoWAddonChannelCodec() + return LibDeflate:CreateCodec("\000", "\001", "") +end + +--- Encode the string to make it ready to be transmitted in World of +-- Warcraft addon channel.
+-- The encoded string is guaranteed to contain no NULL ("\000") character. +-- @param str [string] The string to be encoded. +-- @return The encoded string. +-- @see LibDeflate:DecodeForWoWAddonChannel +function LibDeflate:EncodeForWoWAddonChannel(str) + if type(str) ~= "string" then + error(("Usage: LibDeflate:EncodeForWoWAddonChannel(str):" + .." 'str' - string expected got '%s'."):format(type(str)), 2) + end + if not _addon_channel_codec then + _addon_channel_codec = GenerateWoWAddonChannelCodec() + end + return _addon_channel_codec:Encode(str) +end + +--- Decode the string produced by LibDeflate:EncodeForWoWAddonChannel +-- @param str [string] The string to be decoded. +-- @return [string/nil] The decoded string if succeeds. nil if fails. +-- @see LibDeflate:EncodeForWoWAddonChannel +function LibDeflate:DecodeForWoWAddonChannel(str) + if type(str) ~= "string" then + error(("Usage: LibDeflate:DecodeForWoWAddonChannel(str):" + .." 'str' - string expected got '%s'."):format(type(str)), 2) + end + if not _addon_channel_codec then + _addon_channel_codec = GenerateWoWAddonChannelCodec() + end + return _addon_channel_codec:Decode(str) +end + +-- For World of Warcraft Chat Channel Encoding +-- implemented by Galmok of European Stormrage (Horde), galmok@gmail.com +-- From LibCompress , +-- which is licensed under GPLv2 +-- The code has been modified by the author of LibDeflate. +-- Following byte values are not allowed: +-- \000, s, S, \010, \013, \124, % +-- Because SendChatMessage will error +-- if an UTF8 multibyte character is incomplete, +-- all character values above 127 have to be encoded to avoid this. +-- This costs quite a bit of bandwidth (about 13-14%) +-- Also, because drunken status is unknown for the received +-- , strings used with SendChatMessage should be terminated with +-- an identifying byte value, after which the server MAY add "...hic!" +-- or as much as it can fit(!). +-- Pass the identifying byte as a reserved character to this function +-- to ensure the encoding doesn't contain that value. +-- or use this: local message, match = arg1:gsub("^(.*)\029.-$", "%1") +-- arg1 is message from channel, \029 is the string terminator +-- , but may be used in the encoded datastream as well. :-) +-- This encoding will expand data anywhere from: +-- 0% (average with pure ascii text) +-- 53.5% (average with random data valued zero to 255) +-- 100% (only encoding data that encodes to two bytes) +local function GenerateWoWChatChannelCodec() + local r = {} + for i = 128, 255 do + r[#r+1] = _byte_to_char[i] + end + + local reserved_chars = "sS\000\010\013\124%"..table_concat(r) + return LibDeflate:CreateCodec(reserved_chars + , "\029\031", "\015\020") +end + +local _chat_channel_codec + +--- Encode the string to make it ready to be transmitted in World of +-- Warcraft chat channel.
+-- See also https://wow.gamepedia.com/ValidChatMessageCharacters +-- @param str [string] The string to be encoded. +-- @return [string] The encoded string. +-- @see LibDeflate:DecodeForWoWChatChannel +function LibDeflate:EncodeForWoWChatChannel(str) + if type(str) ~= "string" then + error(("Usage: LibDeflate:EncodeForWoWChatChannel(str):" + .." 'str' - string expected got '%s'."):format(type(str)), 2) + end + if not _chat_channel_codec then + _chat_channel_codec = GenerateWoWChatChannelCodec() + end + return _chat_channel_codec:Encode(str) +end + +--- Decode the string produced by LibDeflate:EncodeForWoWChatChannel. +-- @param str [string] The string to be decoded. +-- @return [string/nil] The decoded string if succeeds. nil if fails. +-- @see LibDeflate:EncodeForWoWChatChannel +function LibDeflate:DecodeForWoWChatChannel(str) + if type(str) ~= "string" then + error(("Usage: LibDeflate:DecodeForWoWChatChannel(str):" + .." 'str' - string expected got '%s'."):format(type(str)), 2) + end + if not _chat_channel_codec then + _chat_channel_codec = GenerateWoWChatChannelCodec() + end + return _chat_channel_codec:Decode(str) +end + +-- Credits to WeakAuras , +-- and Galmok (galmok@gmail.com) for the 6 bit encoding algorithm. +-- The result of encoding will be 25% larger than the +-- origin string, but every single byte of the encoding result will be +-- printable characters as the following. +local _byte_to_6bit_char = { + [0]="a", "b", "c", "d", "e", "f", "g", "h", + "i", "j", "k", "l", "m", "n", "o", "p", + "q", "r", "s", "t", "u", "v", "w", "x", + "y", "z", "A", "B", "C", "D", "E", "F", + "G", "H", "I", "J", "K", "L", "M", "N", + "O", "P", "Q", "R", "S", "T", "U", "V", + "W", "X", "Y", "Z", "0", "1", "2", "3", + "4", "5", "6", "7", "8", "9", "(", ")", +} + +local _6bit_to_byte = { + [97]=0,[98]=1,[99]=2,[100]=3,[101]=4,[102]=5,[103]=6,[104]=7, + [105]=8,[106]=9,[107]=10,[108]=11,[109]=12,[110]=13,[111]=14,[112]=15, + [113]=16,[114]=17,[115]=18,[116]=19,[117]=20,[118]=21,[119]=22,[120]=23, + [121]=24,[122]=25,[65]=26,[66]=27,[67]=28,[68]=29,[69]=30,[70]=31, + [71]=32,[72]=33,[73]=34,[74]=35,[75]=36,[76]=37,[77]=38,[78]=39, + [79]=40,[80]=41,[81]=42,[82]=43,[83]=44,[84]=45,[85]=46,[86]=47, + [87]=48,[88]=49,[89]=50,[90]=51,[48]=52,[49]=53,[50]=54,[51]=55, + [52]=56,[53]=57,[54]=58,[55]=59,[56]=60,[57]=61,[40]=62,[41]=63, +} + +--- Encode the string to make it printable.
+-- +-- Credis to WeakAuras2, this function is equivalant to the implementation +-- it is using right now.
+-- The encoded string will be 25% larger than the origin string. However, every +-- single byte of the encoded string will be one of 64 printable ASCII +-- characters, which are can be easier copied, pasted and displayed. +-- (26 lowercase letters, 26 uppercase letters, 10 numbers digits, +-- left parenthese, or right parenthese) +-- @param str [string] The string to be encoded. +-- @return [string] The encoded string. +function LibDeflate:EncodeForPrint(str) + if type(str) ~= "string" then + error(("Usage: LibDeflate:EncodeForPrint(str):" + .." 'str' - string expected got '%s'."):format(type(str)), 2) + end + local strlen = #str + local strlenMinus2 = strlen - 2 + local i = 1 + local buffer = {} + local buffer_size = 0 + while i <= strlenMinus2 do + local x1, x2, x3 = string_byte(str, i, i+2) + i = i + 3 + local cache = x1+x2*256+x3*65536 + local b1 = cache % 64 + cache = (cache - b1) / 64 + local b2 = cache % 64 + cache = (cache - b2) / 64 + local b3 = cache % 64 + local b4 = (cache - b3) / 64 + buffer_size = buffer_size + 1 + buffer[buffer_size] = + _byte_to_6bit_char[b1].._byte_to_6bit_char[b2] + .._byte_to_6bit_char[b3].._byte_to_6bit_char[b4] + end + + local cache = 0 + local cache_bitlen = 0 + while i <= strlen do + local x = string_byte(str, i, i) + cache = cache + x * _pow2[cache_bitlen] + cache_bitlen = cache_bitlen + 8 + i = i + 1 + end + while cache_bitlen > 0 do + local bit6 = cache % 64 + buffer_size = buffer_size + 1 + buffer[buffer_size] = _byte_to_6bit_char[bit6] + cache = (cache - bit6) / 64 + cache_bitlen = cache_bitlen - 6 + end + + return table_concat(buffer) +end + +--- Decode the printable string produced by LibDeflate:EncodeForPrint. +-- "str" will have its prefixed and trailing control characters or space +-- removed before it is decoded, so it is easier to use if "str" comes form +-- user copy and paste with some prefixed or trailing spaces. +-- Then decode fails if the string contains any characters cant be produced by +-- LibDeflate:EncodeForPrint. That means, decode fails if the string contains a +-- characters NOT one of 26 lowercase letters, 26 uppercase letters, +-- 10 numbers digits, left parenthese, or right parenthese. +-- @param str [string] The string to be decoded +-- @return [string/nil] The decoded string if succeeds. nil if fails. +function LibDeflate:DecodeForPrint(str) + if type(str) ~= "string" then + error(("Usage: LibDeflate:DecodeForPrint(str):" + .." 'str' - string expected got '%s'."):format(type(str)), 2) + end + str = str:gsub("^[%c ]+", "") + str = str:gsub("[%c ]+$", "") + + local strlen = #str + if strlen == 1 then + return nil + end + local strlenMinus3 = strlen - 3 + local i = 1 + local buffer = {} + local buffer_size = 0 + while i <= strlenMinus3 do + local x1, x2, x3, x4 = string_byte(str, i, i+3) + x1 = _6bit_to_byte[x1] + x2 = _6bit_to_byte[x2] + x3 = _6bit_to_byte[x3] + x4 = _6bit_to_byte[x4] + if not (x1 and x2 and x3 and x4) then + return nil + end + i = i + 4 + local cache = x1+x2*64+x3*4096+x4*262144 + local b1 = cache % 256 + cache = (cache - b1) / 256 + local b2 = cache % 256 + local b3 = (cache - b2) / 256 + buffer_size = buffer_size + 1 + buffer[buffer_size] = + _byte_to_char[b1].._byte_to_char[b2].._byte_to_char[b3] + end + + local cache = 0 + local cache_bitlen = 0 + while i <= strlen do + local x = string_byte(str, i, i) + x = _6bit_to_byte[x] + if not x then + return nil + end + cache = cache + x * _pow2[cache_bitlen] + cache_bitlen = cache_bitlen + 6 + i = i + 1 + end + + while cache_bitlen >= 8 do + local byte = cache % 256 + buffer_size = buffer_size + 1 + buffer[buffer_size] = _byte_to_char[byte] + cache = (cache - byte) / 256 + cache_bitlen = cache_bitlen - 8 + end + + return table_concat(buffer) +end + +local function InternalClearCache() + _chat_channel_codec = nil + _addon_channel_codec = nil +end + +-- For test. Don't use the functions in this table for real application. +-- Stuffs in this table is subject to change. +LibDeflate.internals = { + LoadStringToTable = LoadStringToTable, + IsValidDictionary = IsValidDictionary, + IsEqualAdler32 = IsEqualAdler32, + _byte_to_6bit_char = _byte_to_6bit_char, + _6bit_to_byte = _6bit_to_byte, + InternalClearCache = InternalClearCache, +} + +--[[-- Commandline options +@class table +@name CommandlineOptions +@usage lua LibDeflate.lua [OPTION] [INPUT] [OUTPUT] +\-0 store only. no compression. +\-1 fastest compression. +\-9 slowest and best compression. +\-d do decompression instead of compression. +\--dict specify the file that contains +the entire preset dictionary. +\-h give this help. +\--strategy specify a special compression strategy. +\-v print the version and copyright info. +\--zlib use zlib format instead of raw deflate. +]] + +-- currently no plan to support stdin and stdout. +-- Because Lua in Windows does not set stdout with binary mode. +if io and os and debug and _ENV.arg then + local io = io + local os = os + local debug = debug + local arg = _ENV.arg + local debug_info = debug.getinfo(1) + if debug_info.source == arg[0] + or debug_info.short_src == arg[0] then + -- We are indeed runnning THIS file from the commandline. + local input + local output + local i = 1 + local status + local is_zlib = false + local is_decompress = false + local level + local strategy + local dictionary + while (arg[i]) do + local a = arg[i] + if a == "-h" then + print(LibDeflate._COPYRIGHT + .."\nUsage: lua LibDeflate.lua [OPTION] [INPUT] [OUTPUT]\n" + .." -0 store only. no compression.\n" + .." -1 fastest compression.\n" + .." -9 slowest and best compression.\n" + .." -d do decompression instead of compression.\n" + .." --dict specify the file that contains" + .." the entire preset dictionary.\n" + .." -h give this help.\n" + .." --strategy " + .." specify a special compression strategy.\n" + .." -v print the version and copyright info.\n" + .." --zlib use zlib format instead of raw deflate.\n") + os.exit(0) + elseif a == "-v" then + print(LibDeflate._COPYRIGHT) + os.exit(0) + elseif a:find("^%-[0-9]$") then + level = tonumber(a:sub(2, 2)) + elseif a == "-d" then + is_decompress = true + elseif a == "--dict" then + i = i + 1 + local dict_filename = arg[i] + if not dict_filename then + io.stderr:write("You must speicify the dict filename") + os.exit(1) + end + local dict_file, dict_status = io.open(dict_filename, "rb") + if not dict_file then + io.stderr:write( + ("LibDeflate: Cannot read the dictionary file '%s': %s") + :format(dict_filename, dict_status)) + os.exit(1) + end + local dict_str = dict_file:read("*all") + dict_file:close() + -- In your lua program, you should pass in adler32 as a CONSTANT + -- , so it actually prevent you from modifying dictionary + -- unintentionally during the program development. I do this + -- here just because no convenient way to verify in commandline. + dictionary = LibDeflate:CreateDictionary(dict_str, + #dict_str, LibDeflate:Adler32(dict_str)) + elseif a == "--strategy" then + -- Not sure if I should check error here + -- If I do, redudant code. + i = i + 1 + strategy = arg[i] + elseif a == "--zlib" then + is_zlib = true + elseif a:find("^%-") then + io.stderr:write(("LibDeflate: Invalid argument: %s") + :format(a)) + os.exit(1) + else + if not input then + input, status = io.open(a, "rb") + if not input then + io.stderr:write( + ("LibDeflate: Cannot read the file '%s': %s") + :format(a, tostring(status))) + os.exit(1) + end + elseif not output then + output, status = io.open(a, "wb") + if not output then + io.stderr:write( + ("LibDeflate: Cannot write the file '%s': %s") + :format(a, tostring(status))) + os.exit(1) + end + end + end + i = i + 1 + end -- while (arg[i]) + + if not input or not output then + io.stderr:write("LibDeflate:" + .." You must specify both input and output files.") + os.exit(1) + end + + local input_data = input:read("*all") + local configs = { + level = level, + strategy = strategy, + } + local output_data + if not is_decompress then + if not is_zlib then + if not dictionary then + output_data = + LibDeflate:CompressDeflate(input_data, configs) + else + output_data = + LibDeflate:CompressDeflateWithDict(input_data, dictionary + , configs) + end + else + if not dictionary then + output_data = + LibDeflate:CompressZlib(input_data, configs) + else + output_data = + LibDeflate:CompressZlibWithDict(input_data, dictionary + , configs) + end + end + else + if not is_zlib then + if not dictionary then + output_data = LibDeflate:DecompressDeflate(input_data) + else + output_data = LibDeflate:DecompressDeflateWithDict( + input_data, dictionary) + end + else + if not dictionary then + output_data = LibDeflate:DecompressZlib(input_data) + else + output_data = LibDeflate:DecompressZlibWithDict( + input_data, dictionary) + end + end + end + + if not output_data then + io.stderr:write("LibDeflate: Decompress fails.") + os.exit(1) + end + + output:write(output_data) + if input and input ~= io.stdin then + input:close() + end + if output and output ~= io.stdout then + output:close() + end + + io.stderr:write(("Successfully writes %d bytes"):format( + output_data:len())) + os.exit(0) + end +end + +return LibDeflate \ No newline at end of file diff --git a/compress/apis/tar.lua b/compress/apis/tar.lua index c64a9be..83188b1 100644 --- a/compress/apis/tar.lua +++ b/compress/apis/tar.lua @@ -1,8 +1,7 @@ -- see: https://github.com/luarocks/luarocks/blob/master/src/luarocks/tools/tar.lua -- A pure-Lua implementation of untar (unpacking .tar archives) - -local tar = { } +local Util = require('opus.util') local fs = _G.fs @@ -54,7 +53,6 @@ local function checksum_header(block) end local function nullterm(s) -_G._zz = s return s:match("^[^%z]*") end @@ -82,7 +80,7 @@ local function read_header_block(block) return header end -function tar.untar(filename, destdir, verbose) +local function untar(filename, destdir, verbose) assert(type(filename) == "string") assert(type(destdir) == "string") @@ -165,7 +163,7 @@ function tar.untar(filename, destdir, verbose) end local function create_header_block(filename, abspath) - local block = ('\0'):rep(512) + local block = ('\0'):rep(blocksize) local function number_to_octal(n) return ('%o'):format(n) @@ -185,7 +183,7 @@ local function create_header_block(filename, abspath) end -- the bare minimum for this program to untar -function tar.tar(filename, root, files) +local function tar(filename, root, files) assert(type(filename) == "string") assert(type(root) == "string") assert(type(files) == "table") @@ -197,15 +195,18 @@ function tar.tar(filename, root, files) local abs = fs.combine(root, file) local block = create_header_block(file, abs) tar_handle:write(block) - local f = require('opus.util').readFile(abs, 'rb') + local f = Util.readFile(abs, 'rb') tar_handle:write(f) - local padding = #f % 512 + local padding = #f % blocksize if padding > 0 then - tar_handle:write(('\0'):rep(512 - padding)) + tar_handle:write(('\0'):rep(blocksize - padding)) end end tar_handle:close() return true end -return tar \ No newline at end of file +return { + tar = tar, + untar = untar, +} diff --git a/compress/compress.lua b/compress/compress.lua new file mode 100644 index 0000000..f98d294 --- /dev/null +++ b/compress/compress.lua @@ -0,0 +1,53 @@ +local LZW = require('compress.lzw') +local Tar = require('compress.tar') +local Util = require('opus.util') + +local fs = _G.fs +local shell = _ENV.shell + +local TMP_FILE = '.out.tar' + +local args = { ... } +local files = { } + +if not args[2] then + error('Syntax: tar OUTFILE DIR') +end + +local file = shell.resolve(args[1]) +local dir = shell.resolve(args[2]) + +local filetype = 'tar' +if file:match('(.+)%.tar$') then + filetype = 'tar' +elseif file:match('(.+)%.lzw$') then + filetype = 'lzw' +end + +local function recurse(rel) + local abs = fs.combine(dir, rel) + for _,f in ipairs(fs.list(abs)) do + local fullName = fs.combine(abs, f) + if fs.native.isDir(fullName) then -- skip virtual dirs + recurse(fs.combine(rel, f)) + else + table.insert(files, fs.combine(rel, f)) + end + end +end +recurse('') + +if filetype == 'tar' then + Tar.tar(file, dir, files) + +elseif filetype == 'lzw' then + fs.mount(TMP_FILE, 'ramfs', 'file') + Tar.tar(TMP_FILE, dir, files) + + local c = Util.readFile(TMP_FILE) + fs.delete(TMP_FILE) + + c = LZW.compress(c) + Util.writeFile(file, c) +end + diff --git a/compress/uncompress.lua b/compress/uncompress.lua new file mode 100644 index 0000000..0d64203 --- /dev/null +++ b/compress/uncompress.lua @@ -0,0 +1,66 @@ +local DEFLATE = require('compress.deflatelua') +local LZW = require('compress.lzw') +local Tar = require('compress.tar') +local Util = require('opus.util') + +local fs = _G.fs +local io = _G.io +local shell = _ENV.shell + +local TMP_FILE = '.out.tar' + +local args = { ... } + +if not args[2] then + error('Syntax: tar FILE DESTDIR') +end + +local inFile = shell.resolve(args[1]) +local outDir = shell.resolve(args[2]) + +local s, m = pcall(function() + if inFile:match('(.+)%.[gG][zZ]$') then + local fh = io.open(inFile, 'rb') or error('Error opening ' .. inFile) + + fs.mount(TMP_FILE, 'ramfs', 'file') + local ofh = io.open(TMP_FILE, 'wb') + + DEFLATE.gunzip {input=fh, output=ofh, disable_crc=true} + + fh:close() + ofh:close() + + local s, m = Tar.untar(TMP_FILE, outDir, true) + + if not s then + error(m) + end + + elseif inFile:match('(.+)%.lzw$') then + local c = Util.readFile(inFile) + if not c then + error('Unable to open ' .. inFile) + end + + fs.mount(TMP_FILE, 'ramfs', 'file') + Util.writeFile(TMP_FILE, LZW.decompress(c)) + + local s, m = Tar.untar(TMP_FILE, outDir, true) + + if not s then + error(m) + end + + else + local s, m = Tar.untar(inFile, outDir) + if not s then + error(m) + end + end +end) + +fs.delete(TMP_FILE) + +if not s then + error(m) +end diff --git a/compress/untar.lua b/compress/untar.lua deleted file mode 100644 index 080c9d4..0000000 --- a/compress/untar.lua +++ /dev/null @@ -1,46 +0,0 @@ --- see: https://github.com/luarocks/luarocks/blob/master/src/luarocks/tools/tar.lua - --- TODO: support normal tar syntax -- tar xvf - -local DEFLATE = require('compress.deflatelua') -local tar = require('compress.tar') - -local fs = _G.fs -local io = _G.io -local shell = _ENV.shell - -local args = { ... } - -if not args[2] then - error('Syntax: tar FILE DESTDIR') -end - -local inFile = shell.resolve(args[1]) -local outDir = shell.resolve(args[2]) - -if inFile:match('(.+)%.[gG][zZ]$') then - local TMP_FILE = '.out.tar' - - local fh = io.open(inFile, 'rb') or error('Error opening ' .. inFile) - - fs.mount(TMP_FILE, 'ramfs', 'file') - local ofh = io.open(TMP_FILE, 'wb') - - DEFLATE.gunzip {input=fh, output=ofh, disable_crc=true} - - fh:close() - ofh:close() - - local s, m = tar.untar(TMP_FILE, outDir, true) - - fs.delete(TMP_FILE) - - if not s then - error(m) - end -else - local s, m = tar.untar(inFile, outDir) - if not s then - error(m) - end -end diff --git a/debugger/debug.lua b/debugger/debug.lua index e2cfbe3..d362227 100644 --- a/debugger/debug.lua +++ b/debugger/debug.lua @@ -58,6 +58,8 @@ local function startClient() return cmd, param end + _ENV.arg = { table.unpack(args) } + _ENV.arg[0] = filename -- breakpoint table is shared across processes dbg.breakpoints = breakpoints dbg.debugger = debugger @@ -111,13 +113,23 @@ local function message(...) client:resume('debugger', ...) end +UI.InverseButton = require('opus.class')(UI.Button) +UI.InverseButton.defaults = { + UIElement = 'InverseButton', + backgroundColor = 'primary', + backgroundFocusColor = 'gray', + textFocusColor = 'primary', + textColor = 'gray', +} + local page = UI.Page { - backgroundColor = 'black', + backgroundColor = 'gray', container = UI.Window { y = 1, ey = '50%', tabs = UI.Tabs { ey = -2, + barBackgroundColor = 'tertiary', locals = UI.Tab { title = 'Locals', index = 1, @@ -241,6 +253,8 @@ local page = UI.Page { menuBar = UI.MenuBar { y = -1, + backgroundColor = 'primary', + buttonClass = 'InverseButton', buttons = { { text = 'Continue', event = 'cmd', cmd = 'c' }, { text = 'Step', event = 'cmd', cmd = 's' }, @@ -254,6 +268,9 @@ local page = UI.Page { source = UI.ScrollingGrid { y = '50%', ey = -2, disableHeader = true, + backgroundColor = 'gray', + backgroundSelectedColor = 'lightGray', + unfocusedBackgroundSelectedColor = 'lightGray', columns = { { key = 'marker', width = 1 }, { key = 'line', textColor = 'cyan', width = 4 }, @@ -302,7 +319,7 @@ local page = UI.Page { }, statusBar = UI.StatusBar { ex = -12, y = -1, - backgroundColor = 'black', + backgroundColor = 'gray', textColor = 'orange', }, UI.FlatButton { @@ -385,6 +402,7 @@ local page = UI.Page { eventHandler = function(self, event) if event.type == 'cmd' then self.statusBar:setStatus('Running...') + self:sync() message(event.element.cmd) elseif event.type == 'restart' then diff --git a/ignore/chars.lua b/ignore/chars.lua deleted file mode 100644 index 3091711..0000000 --- a/ignore/chars.lua +++ /dev/null @@ -1,15 +0,0 @@ -local w, h = term.getSize() - -term.clear() -term.setCursorPos(1, 1) - -local t = { } -for i = 1, 8 do - table.insert(t, '---') -end - -for i = 1, 255 do - table.insert(t, string.format('%d %c', i, i)) -end - -textutils.pagedTabulate(t) diff --git a/ignore/glasses.lua b/ignore/glasses.lua deleted file mode 100644 index 831d542..0000000 --- a/ignore/glasses.lua +++ /dev/null @@ -1,196 +0,0 @@ -local class = require('opus.class') -local UI = require('opus.ui') -local Event = require('opus.event') -local Peripheral = require('opus.peripheral') - ---[[-- Glasses device --]]-- -local Glasses = class() -function Glasses:init(args) - - local defaults = { - backgroundColor = colors.black, - textColor = colors.white, - textScale = .5, - backgroundOpacity = .5, - multiplier = 2.6665, --- multiplier = 2.333, - } - defaults.width, defaults.height = term.getSize() - - UI:setProperties(defaults, args) - UI:setProperties(self, defaults) - - self.bridge = Peripheral.get({ - type = 'openperipheral_bridge', - method = 'addBox', - }) - self.bridge.clear() - - self.setBackgroundColor = function(...) end - self.setTextColor = function(...) end - - self.t = { } - for i = 1, self.height do - self.t[i] = { - text = string.rep(' ', self.width+1), - --text = self.bridge.addText(0, 40+i*4, string.rep(' ', self.width+1), 0xffffff), - bg = { }, - textFields = { }, - } - end -end - -function Glasses:setBackgroundBox(boxes, ax, bx, y, bgColor) - local colors = { - [ colors.black ] = 0x000000, - [ colors.brown ] = 0x7F664C, - [ colors.blue ] = 0x253192, - [ colors.red ] = 0xFF0000, - [ colors.gray ] = 0x272727, - [ colors.lime ] = 0x426A0D, - [ colors.green ] = 0x2D5628, - [ colors.white ] = 0xFFFFFF - } - - local function overlap(box, ax, bx) - if bx < box.ax or ax > box.bx then - return false - end - return true - end - - for _,box in pairs(boxes) do - if overlap(box, ax, bx) then - if box.bgColor == bgColor then - ax = math.min(ax, box.ax) - bx = math.max(bx, box.bx) - box.ax = box.bx + 1 - elseif ax == box.ax then - box.ax = bx + 1 - elseif ax > box.ax then - if bx < box.bx then - table.insert(boxes, { -- split - ax = bx + 1, - bx = box.bx, - bgColor = box.bgColor - }) - box.bx = ax - 1 - break - else - box.ax = box.bx + 1 - end - elseif ax < box.ax then - if bx > box.bx then - box.ax = box.bx + 1 -- delete - else - box.ax = bx + 1 - end - end - end - end - if bgColor ~= colors.black then - table.insert(boxes, { - ax = ax, - bx = bx, - bgColor = bgColor - }) - end - - local deleted - repeat - deleted = false - for k,box in pairs(boxes) do - if box.ax > box.bx then - if box.box then - box.box.delete() - end - table.remove(boxes, k) - deleted = true - break - end - if not box.box then - box.box = self.bridge.addBox( - math.floor(self.x + (box.ax - 1) * self.multiplier), - self.y + y * 4, - math.ceil((box.bx - box.ax + 1) * self.multiplier), - 4, - colors[bgColor], - self.backgroundOpacity) - else - box.box.setX(self.x + math.floor((box.ax - 1) * self.multiplier)) - box.box.setWidth(math.ceil((box.bx - box.ax + 1) * self.multiplier)) - end - end - until not deleted -end - -function Glasses:write(x, y, text, bg) - - if x < 1 then - error(' less ', 6) - end - if y <= #self.t then - local line = self.t[y] - local str = line.text - str = str:sub(1, x-1) .. text .. str:sub(x + #text) - self.t[y].text = str - - for _,tf in pairs(line.textFields) do - tf.delete() - end - line.textFields = { } - - local function split(st) - local words = { } - local offset = 0 - while true do - local b,e,w = st:find('(%S+)') - if not b then - break - end - table.insert(words, { - offset = b + offset - 1, - text = w, - }) - offset = offset + e - st = st:sub(e + 1) - end - return words - end - - local words = split(str) - for _,word in pairs(words) do - local tf = self.bridge.addText(self.x + word.offset * self.multiplier, - self.y+y*4, '', 0xffffff) - tf.setScale(self.textScale) - tf.setZ(1) - tf.setText(word.text) - table.insert(line.textFields, tf) - end - - self:setBackgroundBox(line.bg, x, x + #text - 1, y, bg) - end -end - -function Glasses:clear(bg) - for _,line in pairs(self.t) do - for _,tf in pairs(line.textFields) do - tf.delete() - end - line.textFields = { } - line.text = string.rep(' ', self.width+1) --- self.t[i].text.setText('') - end -end - -function Glasses:reset() - self:clear() - self.bridge.clear() - self.bridge.sync() -end - -function Glasses:sync() - self.bridge.sync() -end - -return Glasses diff --git a/ignore/rabbitRancher.lua b/ignore/rabbitRancher.lua deleted file mode 100644 index 6650c11..0000000 --- a/ignore/rabbitRancher.lua +++ /dev/null @@ -1,173 +0,0 @@ ---[[ - Breed rabbits with a rabbit. -]] - -local neural = require('neural.interface') -local Point = require('point') -local Sound = require('sound') -local Util = require('util') - -local os = _G.os - -local BREEDING = 'Rabbit' -local WALK_SPEED = 1.3 -local MAX_GROWN = 100 -local BREED_DELAY = 120 - -neural.assertModules({ - 'plethora:sensor', - 'plethora:scanner', - 'plethora:laser', - 'plethora:kinetic', - 'plethora:introspection', -}) - -local ID = neural.getID() -local fed = { } - -local function resupply() - local slot = neural.getEquipment().list()[1] - if slot and slot.count > 32 then - return - end - print('resupplying') - - local dispenser = Util.find(neural.scan(), 'name', 'minecraft:wooden_pressure_plate') - if dispenser then - if math.abs(dispenser.x) > 1 or math.abs(dispenser.z) > 1 then - neural.walkTo({ x = dispenser.x, y = 0, z = dispenser.z }, WALK_SPEED) - end - neural.lookAt(dispenser) - neural.getEquipment().suck(1, 64) - end -end - -local function breed(entity) - print('breeding') - entity.lastFed = os.clock() - fed[entity.id] = entity - - neural.walkTo(entity, WALK_SPEED, .5) - entity = neural.getMetaByID(entity.id) - if entity and not entity.isChild then - neural.lookAt(entity) - neural.use(1) - os.sleep(.1) - end -end - -local function kill(entity) - print('killing') - neural.walkTo(entity, WALK_SPEED, 2) - entity = neural.getMetaByID(entity.id) - if entity and not entity.isChild then - neural.lookAt(entity) - neural.fireAt({ x = entity.x, y = 0, z = entity.z }) - Sound.play('entity.firework.launch') - os.sleep(.2) - end -end - -local function getEntities() - return Util.filter(neural.sense(), function(entity) - if entity.name == BREEDING and entity.y > -.5 and entity.id ~= ID then - return true - end - end) -end - -local function getHungry(entities) - for _,v in pairs(entities) do - if not fed[v.id] or - os.clock() - fed[v.id].lastFed > BREED_DELAY then - return v - end - end -end - -local function randomEntity(entities) - local r = math.random(1, Util.size(entities)) - local i = 1 - for _, v in pairs(entities) do - i = i + 1 - if i > r then - return v - end - end -end - -local function dropOff() - print('dropping') - - if neural.getEquipment().list()[2] then - local b = Util.find(neural.scan(), 'name', 'minecraft:hopper') - if b then - neural.walkTo({ x = b.x, y = 0, z = b.z }, 2) - - b = Util.find(neural.scan(), 'name', 'minecraft:hopper') - if b and math.abs(b.x) < 1 and math.abs(b.z) < 1 then - print('dropped') - neural.getEquipment().drop(2) - end - end - end -end - -local function pickup(id) - local b = neural.getMetaByID(id) - if b then - neural.walkTo(b, 2) - - local main = neural.getEquipment().list()[1] - local amount = neural.getEquipment().suck(not main and 2 or nil) - print('sucked: ' .. amount) - if amount > 0 then - Sound.play('entity.item.pickup') - return true - end - end -end - -local function drops() - local sensed = Util.reduce(neural.sense(), function(acc, s) - if Util.round(s.y) == 0 and s.name == 'Item' then - acc[s.id] = s - end - return acc - end, { }) - - local pt = { x = 0, y = 0, z = 0 } - while true do - local b = Point.closest(pt, sensed) - if not b then - break - end - sensed[b.id] = nil - - if pickup(b.id) then - pt = b - else - dropOff() - break - end - end -end - -while true do - resupply() - - local entities = getEntities() - - if Util.size(entities) > MAX_GROWN then - kill(randomEntity(entities)) - else - local entity = getHungry(entities) - if entity then - breed(entity) - else - print('sleeping') - os.sleep(5) - end - drops() - end -end \ No newline at end of file diff --git a/lfs/apis/init.lua b/lfs/apis/init.lua index 56d5e7b..9ed01a0 100644 --- a/lfs/apis/init.lua +++ b/lfs/apis/init.lua @@ -234,4 +234,4 @@ function lfs.touch(filename, atime, mtime) end end -return lfs +return lfs \ No newline at end of file -- 2.49.1 From 4f74ab2840ee0bf1c9d58b42ee1591e8154662b9 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Fri, 5 Jun 2020 21:38:45 -0600 Subject: [PATCH 61/90] added moonscript package --- common/.package | 2 +- compress/apis/lzw.lua | 142 ---- compress/apis/tar.lua | 212 ------ compress/compress.lua | 34 +- compress/uncompress.lua | 80 +- debugger/apis/init.lua | 27 +- debugger/debug.lua | 39 +- lpeg/.package | 9 + lpeg/etc/fstab | 1 + moonscript/.package | 11 + moonscript/README.txt | 5 + moonscript/T.lua | 97 +++ moonscript/T.moon | 70 ++ moonscript/argparse.lua | 1527 +++++++++++++++++++++++++++++++++++++++ moonscript/etc/fstab | 4 + 15 files changed, 1799 insertions(+), 461 deletions(-) delete mode 100644 compress/apis/lzw.lua delete mode 100644 compress/apis/tar.lua create mode 100644 lpeg/.package create mode 100644 lpeg/etc/fstab create mode 100644 moonscript/.package create mode 100644 moonscript/README.txt create mode 100644 moonscript/T.lua create mode 100644 moonscript/T.moon create mode 100644 moonscript/argparse.lua create mode 100644 moonscript/etc/fstab diff --git a/common/.package b/common/.package index 4ffb026..b70d53a 100644 --- a/common/.package +++ b/common/.package @@ -13,7 +13,7 @@ * and more... ]], license = 'MIT', - required = { + required = { 'core', }, } diff --git a/compress/apis/lzw.lua b/compress/apis/lzw.lua deleted file mode 100644 index 3d4c4b5..0000000 --- a/compress/apis/lzw.lua +++ /dev/null @@ -1,142 +0,0 @@ --- see: https://github.com/Rochet2/lualzw --- MIT License - Copyright (c) 2016 Rochet2 - -local char = string.char -local type = type -local sub = string.sub -local tconcat = table.concat - -local SIGC = 'LZWC' - -local basedictcompress = {} -local basedictdecompress = {} -for i = 0, 255 do - local ic, iic = char(i), char(i, 0) - basedictcompress[ic] = iic - basedictdecompress[iic] = ic -end - -local function dictAddA(str, dict, a, b) - if a >= 256 then - a, b = 0, b+1 - if b >= 256 then - dict = {} - b = 1 - end - end - dict[str] = char(a,b) - a = a+1 - return dict, a, b -end - -local function compress(input) - if type(input) ~= "string" then - error ("string expected, got "..type(input)) - end - local len = #input - if len <= 1 then - return input - end - - local dict = {} - local a, b = 0, 1 - - local result = { SIGC } - local resultlen = 1 - local n = 2 - local word = "" - for i = 1, len do - local c = sub(input, i, i) - local wc = word..c - if not (basedictcompress[wc] or dict[wc]) then - local write = basedictcompress[word] or dict[word] - if not write then - error "algorithm error, could not fetch word" - end - result[n] = write - resultlen = resultlen + #write - n = n+1 - if len <= resultlen then - return input - end - dict, a, b = dictAddA(wc, dict, a, b) - word = c - else - word = wc - end - end - result[n] = basedictcompress[word] or dict[word] - resultlen = resultlen+#result[n] - if len <= resultlen then - return input - end - return tconcat(result) -end - -local function dictAddB(str, dict, a, b) - if a >= 256 then - a, b = 0, b+1 - if b >= 256 then - dict = {} - b = 1 - end - end - dict[char(a,b)] = str - a = a+1 - return dict, a, b -end - -local function decompress(input) - if type(input) ~= "string" then - error( "string expected, got "..type(input)) - end - - if #input <= 1 then - return input - end - - local control = sub(input, 1, 4) - if control ~= SIGC then - return input - end - input = sub(input, 5) - local len = #input - - if len < 2 then - error("invalid input - not a compressed string") - end - - local dict = {} - local a, b = 0, 1 - - local result = {} - local n = 1 - local last = sub(input, 1, 2) - result[n] = basedictdecompress[last] or dict[last] - n = n+1 - for i = 3, len, 2 do - local code = sub(input, i, i+1) - local lastStr = basedictdecompress[last] or dict[last] - if not lastStr then - error( "could not find last from dict. Invalid input?") - end - local toAdd = basedictdecompress[code] or dict[code] - if toAdd then - result[n] = toAdd - n = n+1 - dict, a, b = dictAddB(lastStr..sub(toAdd, 1, 1), dict, a, b) - else - local tmp = lastStr..sub(lastStr, 1, 1) - result[n] = tmp - n = n+1 - dict, a, b = dictAddB(tmp, dict, a, b) - end - last = code - end - return tconcat(result) -end - -return { - compress = compress, - decompress = decompress, -} diff --git a/compress/apis/tar.lua b/compress/apis/tar.lua deleted file mode 100644 index 83188b1..0000000 --- a/compress/apis/tar.lua +++ /dev/null @@ -1,212 +0,0 @@ - --- see: https://github.com/luarocks/luarocks/blob/master/src/luarocks/tools/tar.lua --- A pure-Lua implementation of untar (unpacking .tar archives) -local Util = require('opus.util') - -local fs = _G.fs - -local blocksize = 512 - -local function get_typeflag(flag) - if flag == "0" or flag == "\0" then return "file" - elseif flag == "1" then return "link" - elseif flag == "2" then return "symlink" -- "reserved" in POSIX, "symlink" in GNU - elseif flag == "3" then return "character" - elseif flag == "4" then return "block" - elseif flag == "5" then return "directory" - elseif flag == "6" then return "fifo" - elseif flag == "7" then return "contiguous" -- "reserved" in POSIX, "contiguous" in GNU - elseif flag == "x" then return "next file" - elseif flag == "g" then return "global extended header" - elseif flag == "L" then return "long name" - elseif flag == "K" then return "long link name" - end - return "unknown" -end - -local function octal_to_number(octal) - local exp = 0 - local number = 0 - octal = octal:gsub("%s", "") - for i = #octal,1,-1 do - local digit = tonumber(octal:sub(i,i)) - if not digit then - break - end - number = number + (digit * 8^exp) - exp = exp + 1 - end - return number -end - -local function checksum_header(block) - local sum = 256 - for i = 1,148 do - local b = block:byte(i) or 0 - sum = sum + b - end - for i = 157,500 do - local b = block:byte(i) or 0 - sum = sum + b - end - return sum -end - -local function nullterm(s) - return s:match("^[^%z]*") -end - -local function read_header_block(block) - local header = {} - header.name = nullterm(block:sub(1,100)) - header.mode = nullterm(block:sub(101,108)):gsub(" ", "") - header.uid = octal_to_number(nullterm(block:sub(109,116))) - header.gid = octal_to_number(nullterm(block:sub(117,124))) - header.size = octal_to_number(nullterm(block:sub(125,136))) - header.mtime = octal_to_number(nullterm(block:sub(137,148))) - header.chksum = octal_to_number(nullterm(block:sub(149,156))) - header.typeflag = get_typeflag(block:sub(157,157)) - header.linkname = nullterm(block:sub(158,257)) - header.magic = block:sub(258,263) - header.version = block:sub(264,265) - header.uname = nullterm(block:sub(266,297)) - header.gname = nullterm(block:sub(298,329)) - header.devmajor = octal_to_number(nullterm(block:sub(330,337))) - header.devminor = octal_to_number(nullterm(block:sub(338,345))) - header.prefix = block:sub(346,500) - if not checksum_header(block) == header.chksum then - return false, "Failed header checksum" - end - return header -end - -local function untar(filename, destdir, verbose) - assert(type(filename) == "string") - assert(type(destdir) == "string") - - local tar_handle = io.open(filename, "rb") - if not tar_handle then return nil, "Error opening file "..filename end - - local long_name, long_link_name - local ok, err - - local make_dir = function(a) - if not fs.exists(a) then - fs.makeDir(a) - end - return true - end - - while true do - local block - repeat - block = tar_handle:read(blocksize) - until (not block) or checksum_header(block) > 256 - if not block then break end - if #block < blocksize then - ok, err = nil, "Invalid block size -- corrupted file?" - break - end - local header - header, err = read_header_block(block) - if not header then - ok = false - break - end - - local file_data = tar_handle:read(math.ceil(header.size / blocksize) * blocksize):sub(1,header.size) - - if header.typeflag == "long name" then - long_name = nullterm(file_data) - elseif header.typeflag == "long link name" then - long_link_name = nullterm(file_data) - else - if long_name then - header.name = long_name - long_name = nil - end - if long_link_name then - header.name = long_link_name - long_link_name = nil - end - end - local pathname = fs.combine(destdir, header.name) - - if header.typeflag == "directory" then - ok, err = make_dir(pathname) - if not ok then - break - end - elseif header.typeflag == "file" then - local dirname = fs.getDir(pathname) - if dirname ~= "" then - ok, err = make_dir(dirname) - if not ok then - break - end - end - local file_handle - if verbose then - print(pathname) - end - file_handle, err = io.open(pathname, "wb") - if not file_handle then - ok = nil - break - end - file_handle:write(file_data) - file_handle:close() - end - end - tar_handle:close() - return ok, err -end - -local function create_header_block(filename, abspath) - local block = ('\0'):rep(blocksize) - - local function number_to_octal(n) - return ('%o'):format(n) - end - - local function ins(pos, istr) - block = block:sub(1, pos - 1) .. istr .. block:sub(pos + #istr) - end - - ins(1, filename) -- header - ins(125, number_to_octal(fs.getSize(abspath))) - ins(157, '0') -- typeflag - - ins(149, number_to_octal(checksum_header(block))) - - return block -end - --- the bare minimum for this program to untar -local function tar(filename, root, files) - assert(type(filename) == "string") - assert(type(root) == "string") - assert(type(files) == "table") - - local tar_handle = io.open(filename, "wb") - if not tar_handle then return nil, "Error opening file "..filename end - - for _, file in pairs(files) do - local abs = fs.combine(root, file) - local block = create_header_block(file, abs) - tar_handle:write(block) - local f = Util.readFile(abs, 'rb') - tar_handle:write(f) - local padding = #f % blocksize - if padding > 0 then - tar_handle:write(('\0'):rep(blocksize - padding)) - end - end - tar_handle:close() - return true -end - -return { - tar = tar, - untar = untar, -} diff --git a/compress/compress.lua b/compress/compress.lua index f98d294..4ca69a1 100644 --- a/compress/compress.lua +++ b/compress/compress.lua @@ -1,14 +1,10 @@ -local LZW = require('compress.lzw') -local Tar = require('compress.tar') +local LZW = require('opus.compress.lzw') +local Tar = require('opus.compress.tar') local Util = require('opus.util') -local fs = _G.fs local shell = _ENV.shell -local TMP_FILE = '.out.tar' - local args = { ... } -local files = { } if not args[2] then error('Syntax: tar OUTFILE DIR') @@ -24,30 +20,10 @@ elseif file:match('(.+)%.lzw$') then filetype = 'lzw' end -local function recurse(rel) - local abs = fs.combine(dir, rel) - for _,f in ipairs(fs.list(abs)) do - local fullName = fs.combine(abs, f) - if fs.native.isDir(fullName) then -- skip virtual dirs - recurse(fs.combine(rel, f)) - else - table.insert(files, fs.combine(rel, f)) - end - end -end -recurse('') - if filetype == 'tar' then - Tar.tar(file, dir, files) + Tar.tar(file, dir) elseif filetype == 'lzw' then - fs.mount(TMP_FILE, 'ramfs', 'file') - Tar.tar(TMP_FILE, dir, files) - - local c = Util.readFile(TMP_FILE) - fs.delete(TMP_FILE) - - c = LZW.compress(c) - Util.writeFile(file, c) + local c = Tar.tar_string(dir) + Util.writeFile(file, LZW.compress(c), 'wb') end - diff --git a/compress/uncompress.lua b/compress/uncompress.lua index 0d64203..abed47a 100644 --- a/compress/uncompress.lua +++ b/compress/uncompress.lua @@ -1,14 +1,11 @@ local DEFLATE = require('compress.deflatelua') -local LZW = require('compress.lzw') -local Tar = require('compress.tar') +local LZW = require('opus.compress.lzw') +local Tar = require('opus.compress.tar') local Util = require('opus.util') -local fs = _G.fs local io = _G.io local shell = _ENV.shell -local TMP_FILE = '.out.tar' - local args = { ... } if not args[2] then @@ -18,49 +15,40 @@ end local inFile = shell.resolve(args[1]) local outDir = shell.resolve(args[2]) -local s, m = pcall(function() - if inFile:match('(.+)%.[gG][zZ]$') then - local fh = io.open(inFile, 'rb') or error('Error opening ' .. inFile) +if inFile:match('(.+)%.[gG][zZ]$') then + -- uncompress a file created with: tar czf ... + local fh = io.open(inFile, 'rb') or error('Error opening ' .. inFile) - fs.mount(TMP_FILE, 'ramfs', 'file') - local ofh = io.open(TMP_FILE, 'wb') - - DEFLATE.gunzip {input=fh, output=ofh, disable_crc=true} - - fh:close() - ofh:close() - - local s, m = Tar.untar(TMP_FILE, outDir, true) - - if not s then - error(m) - end - - elseif inFile:match('(.+)%.lzw$') then - local c = Util.readFile(inFile) - if not c then - error('Unable to open ' .. inFile) - end - - fs.mount(TMP_FILE, 'ramfs', 'file') - Util.writeFile(TMP_FILE, LZW.decompress(c)) - - local s, m = Tar.untar(TMP_FILE, outDir, true) - - if not s then - error(m) - end - - else - local s, m = Tar.untar(inFile, outDir) - if not s then - error(m) - end + local t = { } + local function writer(b) + table.insert(t, b) end -end) -fs.delete(TMP_FILE) + DEFLATE.gunzip {input=fh, output=writer, disable_crc=true} -if not s then - error(m) + fh:close() + + local s, m = Tar.untar_string(string.char(table.unpack(t)), outDir, true) + + if not s then + error(m) + end + +elseif inFile:match('(.+)%.tar%.lzw$') then + local c = Util.readFile(inFile, 'rb') + if not c then + error('Unable to open ' .. inFile) + end + + local s, m = Tar.untar_string(LZW.decompress(c), outDir, true) + + if not s then + error(m) + end + +else + local s, m = Tar.untar(inFile, outDir, true) + if not s then + error(m) + end end diff --git a/debugger/apis/init.lua b/debugger/apis/init.lua index a7b97d3..1e860b8 100644 --- a/debugger/apis/init.lua +++ b/debugger/apis/init.lua @@ -9,33 +9,12 @@ local dbg = { breakpoints = nil, } -local romFiles = { - load = function(self) - local function recurse(dir) - local files = fs.list(dir) - for _,f in ipairs(files) do - local fullName = fs.combine(dir, f) - if fs.isDir(fullName) then - recurse(fullName) - else - self.files[f] = fullName - end - end - end - recurse('rom/apis') - end, - get = function(self, file) - return self.files[file] - end, - files = { }, -} -romFiles:load() - local function breakpointHook(info) if dbg.breakpoints then - local src = romFiles:get(info.short_src) or info.short_src + local src = info.short_src for _,v in pairs(dbg.breakpoints) do - if v.line == info.currentline and v.file == src then + if v.line == info.currentline + and (v.file == src or v.bfile == src) then return not v.disabled end end diff --git a/debugger/debug.lua b/debugger/debug.lua index d362227..c0f57d2 100644 --- a/debugger/debug.lua +++ b/debugger/debug.lua @@ -50,7 +50,7 @@ local function startClient() os.sleep(0) -- not sure why, but we need a sleep before :resume -- directly resuming debugger routine to prevent -- serialization of the snapshot - dbg.debugger:resume('debuggerX', dbg.debugger.uid, snapshot) + dbg.debugger:resume('debuggerX', 'break', snapshot) local e, cmd, param repeat e, cmd, param = os.pullEvent('debugger') @@ -65,15 +65,27 @@ local function startClient() dbg.debugger = debugger dbg.stopIn(fn) local s, m = dbg.call(fn, table.unpack(args)) + + dbg.debugger:resume('debuggerX', 'disconnect') + if not s then error(m, -1) end + print('Process ended normally') + print('Press enter to exit') + while true do + local e, code = os.pullEventRaw('key') + if e == 'terminate' or e == 'key' and code == _G.keys.enter then + break + end + end end, }) client = kernel.find(clientId) end local romFiles = { + files = { }, load = function(self) local function recurse(dir) local files = fs.list(dir) @@ -87,11 +99,14 @@ local romFiles = { end end recurse('rom/apis') + self.reversed = Util.transpose(self.files) end, get = function(self, file) return self.files[file] end, - files = { }, + lookup = function(self, file) + return self.reversed[file] + end, } romFiles:load() @@ -137,7 +152,7 @@ local page = UI.Page { disableHeader = true, unfocusedBackgroundSelectedColor = 'black', columns = { - { heading = 'Key', key = 'name' }, + { heading = 'localname', key = 'name' }, { heading = 'Value', key = 'value', textColor = 'yellow' }, }, autospace = true, @@ -214,6 +229,13 @@ local page = UI.Page { { heading = 'Name', key = 'short' }, { heading = 'Path', key = 'path', textColor = 'lightGray' }, }, + getDisplayValues = function(_, row) + return { + line = row.line, + short = fs.getName(row.file), + path = fs.getDir(row.file), + } + end, getRowTextColor = function(self, row, selected) return row.disabled and 'lightGray' or UI.Grid.getRowTextColor(self, row, selected) @@ -438,8 +460,7 @@ local page = UI.Page { table.insert(breakpoints, { file = event.file, line = event.line, - short = fs.getName(event.file), - path = fs.getDir(event.file), + bfile = romFiles:lookup(event.file), }) self:emit({ type = 'update_breakpoints' }) @@ -487,8 +508,12 @@ local page = UI.Page { end, } -Event.on('debuggerX', function(_, uid, data) - if uid == debugger.uid then +Event.on('debuggerX', function(_, cmd, data) + if cmd == 'disconnect' then + page.statusBar:setStatus('Finished') + page:sync() + + elseif cmd == 'break' then kernel.raise(debugger.uid) -- local tab diff --git a/lpeg/.package b/lpeg/.package new file mode 100644 index 0000000..d134966 --- /dev/null +++ b/lpeg/.package @@ -0,0 +1,9 @@ +{ + title = 'LPeg', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/lpeg', + description = [[A pure Lua port of LPeg, Roberto Ierusalimschy's Parsing Expression Grammars library. + +This version of LuLPeg emulates LPeg v0.12. +see: https://github.com/pygy/LuLPeg/]], + license = 'MIT', +} \ No newline at end of file diff --git a/lpeg/etc/fstab b/lpeg/etc/fstab new file mode 100644 index 0000000..b4336b2 --- /dev/null +++ b/lpeg/etc/fstab @@ -0,0 +1 @@ +rom/modules/main/lpeg.lua urlfs https://raw.githubusercontent.com/pygy/LuLPeg/master/lulpeg.lua diff --git a/moonscript/.package b/moonscript/.package new file mode 100644 index 0000000..e347a82 --- /dev/null +++ b/moonscript/.package @@ -0,0 +1,11 @@ +{ + title = 'moonscript', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/moonscript', + description = [[MoonScript is a programmer friendly language that compiles into Lua. It gives you the power of the fastest scripting language combined with a rich set of features. It runs on Lua 5.1 and above, including alternative runtimes like LuaJIT. + +See https://moonscript.org.]], + license = 'MIT', + required = { + 'lpeg', + }, +} diff --git a/moonscript/README.txt b/moonscript/README.txt new file mode 100644 index 0000000..d306970 --- /dev/null +++ b/moonscript/README.txt @@ -0,0 +1,5 @@ +running the compiler works fine... +moonc T.moon <-- OK + +working on getting the moon command to work properly +moon T.moon <-- NOPE diff --git a/moonscript/T.lua b/moonscript/T.lua new file mode 100644 index 0000000..8825f17 --- /dev/null +++ b/moonscript/T.lua @@ -0,0 +1,97 @@ +local Event = require('opus.event') +local UI = require('opus.ui') +local kernel = _G.kernel +local multishell = _ENV.multishell +local tasks = multishell and multishell.getTabs and multishell.getTabs() or kernel.routines +UI:configure('Tasks', ...) +local page = UI.Page({ + menuBar = UI.MenuBar({ + buttons = { + { + text = 'Activate', + event = 'activate' + }, + { + text = 'Terminate', + event = 'terminate' + }, + { + text = 'Inspect', + event = 'inspect' + } + } + }), + grid = UI.ScrollingGrid({ + y = 2, + columns = { + { + heading = 'ID', + key = 'uid', + width = 3 + }, + { + heading = 'Title', + key = 'title' + }, + { + heading = 'Status', + key = 'status' + }, + { + heading = 'Time', + key = 'timestamp' + } + }, + values = tasks, + sortColumn = 'uid', + autospace = true, + getDisplayValues = function(self, row) + local elapsed = os.clock() - row.timestamp + return { + uid = row.uid, + title = row.title, + status = row.isDead and 'error' or coroutine.status(row.co), + timestamp = elapsed < 60 and string.format("%ds", math.floor(elapsed)) or string.format("%sm", math.floor(elapsed / 6) / 10) + } + end + }), + accelerators = { + ['control-q'] = 'quit', + [' '] = 'activate', + t = 'terminate' + }, + eventHandler = function(self, event) + local t = self.grid:getSelected() + local _exp_0 = event.type + if 'activate' == _exp_0 or 'grid_select' == _exp_0 then + if t then + return multishell.setFocus(t.uid) + end + elseif 'terminate' == _exp_0 then + if t then + return multishell.terminate(t.uid) + end + elseif 'inspect' == _exp_0 then + if t then + return multishell.openTab(_ENV, { + path = 'sys/apps/Lua.lua', + args = { + t + }, + focused = true + }) + end + elseif 'quit' == _exp_0 then + return UI:quit() + else + return UI.Page.eventHandler(self, event) + end + end +}) +Event.onInterval(1, function() + page.grid:update() + page.grid:draw() + return page:sync() +end) +UI:setPage(page) +return UI:start() diff --git a/moonscript/T.moon b/moonscript/T.moon new file mode 100644 index 0000000..6cbf5f4 --- /dev/null +++ b/moonscript/T.moon @@ -0,0 +1,70 @@ +Event = require('opus.event') +UI = require('opus.ui') + +kernel = _G.kernel +multishell = _ENV.multishell +tasks = multishell and multishell.getTabs and multishell.getTabs() or kernel.routines + +UI\configure 'Tasks', ... + +page = UI.Page { + menuBar: UI.MenuBar { + buttons: { + { text: 'Activate', event: 'activate' }, + { text: 'Terminate', event: 'terminate' }, + { text: 'Inspect', event: 'inspect' }, + }, + }, + grid: UI.ScrollingGrid { + y: 2, + columns: { + { heading: 'ID', key: 'uid', width: 3 }, + { heading: 'Title', key: 'title' }, + { heading: 'Status', key: 'status' }, + { heading: 'Time', key: 'timestamp' }, + }, + values: tasks, + sortColumn: 'uid', + autospace: true, + getDisplayValues: (row) => + elapsed = os.clock! - row.timestamp + { + uid: row.uid, + title: row.title, + status: row.isDead and 'error' or coroutine.status(row.co), + timestamp: elapsed < 60 and + string.format("%ds", math.floor(elapsed)) or + string.format("%sm", math.floor(elapsed/6)/10), + } + }, + accelerators: { + [ 'control-q' ]: 'quit', + [ ' ' ]: 'activate', + t: 'terminate', + }, + eventHandler: (event) => + t = self.grid\getSelected! + switch event.type + when 'activate', 'grid_select' + multishell.setFocus t.uid if t + when 'terminate' + multishell.terminate t.uid if t + when 'inspect' + multishell.openTab _ENV, { + path: 'sys/apps/Lua.lua', + args: { t }, + focused: true, + } if t + when 'quit' + UI\quit! + else + UI.Page.eventHandler(@, event) +} + +Event.onInterval 1, () -> + page.grid\update! + page.grid\draw! + page\sync! + +UI\setPage page +UI\start! diff --git a/moonscript/argparse.lua b/moonscript/argparse.lua new file mode 100644 index 0000000..de89237 --- /dev/null +++ b/moonscript/argparse.lua @@ -0,0 +1,1527 @@ +-- The MIT License (MIT) + +-- Copyright (c) 2013 - 2018 Peter Melnichenko + +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: + +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. + +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +local function deep_update(t1, t2) + for k, v in pairs(t2) do + if type(v) == "table" then + v = deep_update({}, v) + end + + t1[k] = v + end + + return t1 +end + +-- A property is a tuple {name, callback}. +-- properties.args is number of properties that can be set as arguments +-- when calling an object. +local function class(prototype, properties, parent) + -- Class is the metatable of its instances. + local cl = {} + cl.__index = cl + + if parent then + cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype) + else + cl.__prototype = prototype + end + + if properties then + local names = {} + + -- Create setter methods and fill set of property names. + for _, property in ipairs(properties) do + local name, callback = property[1], property[2] + + cl[name] = function(self, value) + if not callback(self, value) then + self["_" .. name] = value + end + + return self + end + + names[name] = true + end + + function cl.__call(self, ...) + -- When calling an object, if the first argument is a table, + -- interpret keys as property names, else delegate arguments + -- to corresponding setters in order. + if type((...)) == "table" then + for name, value in pairs((...)) do + if names[name] then + self[name](self, value) + end + end + else + local nargs = select("#", ...) + + for i, property in ipairs(properties) do + if i > nargs or i > properties.args then + break + end + + local arg = select(i, ...) + + if arg ~= nil then + self[property[1]](self, arg) + end + end + end + + return self + end + end + + -- If indexing class fails, fallback to its parent. + local class_metatable = {} + class_metatable.__index = parent + + function class_metatable.__call(self, ...) + -- Calling a class returns its instance. + -- Arguments are delegated to the instance. + local object = deep_update({}, self.__prototype) + setmetatable(object, self) + return object(...) + end + + return setmetatable(cl, class_metatable) +end + +local function typecheck(name, types, value) + for _, type_ in ipairs(types) do + if type(value) == type_ then + return true + end + end + + error(("bad property '%s' (%s expected, got %s)"):format(name, table.concat(types, " or "), type(value))) +end + +local function typechecked(name, ...) + local types = {...} + return {name, function(_, value) typecheck(name, types, value) end} +end + +local multiname = {"name", function(self, value) + typecheck("name", {"string"}, value) + + for alias in value:gmatch("%S+") do + self._name = self._name or alias + table.insert(self._aliases, alias) + end + + -- Do not set _name as with other properties. + return true +end} + +local function parse_boundaries(str) + if tonumber(str) then + return tonumber(str), tonumber(str) + end + + if str == "*" then + return 0, math.huge + end + + if str == "+" then + return 1, math.huge + end + + if str == "?" then + return 0, 1 + end + + if str:match "^%d+%-%d+$" then + local min, max = str:match "^(%d+)%-(%d+)$" + return tonumber(min), tonumber(max) + end + + if str:match "^%d+%+$" then + local min = str:match "^(%d+)%+$" + return tonumber(min), math.huge + end +end + +local function boundaries(name) + return {name, function(self, value) + typecheck(name, {"number", "string"}, value) + + local min, max = parse_boundaries(value) + + if not min then + error(("bad property '%s'"):format(name)) + end + + self["_min" .. name], self["_max" .. name] = min, max + end} +end + +local actions = {} + +local option_action = {"action", function(_, value) + typecheck("action", {"function", "string"}, value) + + if type(value) == "string" and not actions[value] then + error(("unknown action '%s'"):format(value)) + end +end} + +local option_init = {"init", function(self) + self._has_init = true +end} + +local option_default = {"default", function(self, value) + if type(value) ~= "string" then + self._init = value + self._has_init = true + return true + end +end} + +local add_help = {"add_help", function(self, value) + typecheck("add_help", {"boolean", "string", "table"}, value) + + if self._has_help then + table.remove(self._options) + self._has_help = false + end + + if value then + local help = self:flag() + :description "Show this help message and exit." + :action(function() + print(self:get_help()) + os.exit(0) + end) + + if value ~= true then + help = help(value) + end + + if not help._name then + help "-h" "--help" + end + + self._has_help = true + end +end} + +local Parser = class({ + _arguments = {}, + _options = {}, + _commands = {}, + _mutexes = {}, + _groups = {}, + _require_command = true, + _handle_options = true +}, { + args = 3, + typechecked("name", "string"), + typechecked("description", "string"), + typechecked("epilog", "string"), + typechecked("usage", "string"), + typechecked("help", "string"), + typechecked("require_command", "boolean"), + typechecked("handle_options", "boolean"), + typechecked("action", "function"), + typechecked("command_target", "string"), + typechecked("help_vertical_space", "number"), + typechecked("usage_margin", "number"), + typechecked("usage_max_width", "number"), + typechecked("help_usage_margin", "number"), + typechecked("help_description_margin", "number"), + typechecked("help_max_width", "number"), + add_help +}) + +local Command = class({ + _aliases = {} +}, { + args = 3, + multiname, + typechecked("description", "string"), + typechecked("epilog", "string"), + typechecked("target", "string"), + typechecked("usage", "string"), + typechecked("help", "string"), + typechecked("require_command", "boolean"), + typechecked("handle_options", "boolean"), + typechecked("action", "function"), + typechecked("command_target", "string"), + typechecked("help_vertical_space", "number"), + typechecked("usage_margin", "number"), + typechecked("usage_max_width", "number"), + typechecked("help_usage_margin", "number"), + typechecked("help_description_margin", "number"), + typechecked("help_max_width", "number"), + typechecked("hidden", "boolean"), + add_help +}, Parser) + +local Argument = class({ + _minargs = 1, + _maxargs = 1, + _mincount = 1, + _maxcount = 1, + _defmode = "unused", + _show_default = true +}, { + args = 5, + typechecked("name", "string"), + typechecked("description", "string"), + option_default, + typechecked("convert", "function", "table"), + boundaries("args"), + typechecked("target", "string"), + typechecked("defmode", "string"), + typechecked("show_default", "boolean"), + typechecked("argname", "string", "table"), + typechecked("hidden", "boolean"), + option_action, + option_init +}) + +local Option = class({ + _aliases = {}, + _mincount = 0, + _overwrite = true +}, { + args = 6, + multiname, + typechecked("description", "string"), + option_default, + typechecked("convert", "function", "table"), + boundaries("args"), + boundaries("count"), + typechecked("target", "string"), + typechecked("defmode", "string"), + typechecked("show_default", "boolean"), + typechecked("overwrite", "boolean"), + typechecked("argname", "string", "table"), + typechecked("hidden", "boolean"), + option_action, + option_init +}, Argument) + +function Parser:_inherit_property(name, default) + local element = self + + while true do + local value = element["_" .. name] + + if value ~= nil then + return value + end + + if not element._parent then + return default + end + + element = element._parent + end +end + +function Argument:_get_argument_list() + local buf = {} + local i = 1 + + while i <= math.min(self._minargs, 3) do + local argname = self:_get_argname(i) + + if self._default and self._defmode:find "a" then + argname = "[" .. argname .. "]" + end + + table.insert(buf, argname) + i = i+1 + end + + while i <= math.min(self._maxargs, 3) do + table.insert(buf, "[" .. self:_get_argname(i) .. "]") + i = i+1 + + if self._maxargs == math.huge then + break + end + end + + if i < self._maxargs then + table.insert(buf, "...") + end + + return buf +end + +function Argument:_get_usage() + local usage = table.concat(self:_get_argument_list(), " ") + + if self._default and self._defmode:find "u" then + if self._maxargs > 1 or (self._minargs == 1 and not self._defmode:find "a") then + usage = "[" .. usage .. "]" + end + end + + return usage +end + +function actions.store_true(result, target) + result[target] = true +end + +function actions.store_false(result, target) + result[target] = false +end + +function actions.store(result, target, argument) + result[target] = argument +end + +function actions.count(result, target, _, overwrite) + if not overwrite then + result[target] = result[target] + 1 + end +end + +function actions.append(result, target, argument, overwrite) + result[target] = result[target] or {} + table.insert(result[target], argument) + + if overwrite then + table.remove(result[target], 1) + end +end + +function actions.concat(result, target, arguments, overwrite) + if overwrite then + error("'concat' action can't handle too many invocations") + end + + result[target] = result[target] or {} + + for _, argument in ipairs(arguments) do + table.insert(result[target], argument) + end +end + +function Argument:_get_action() + local action, init + + if self._maxcount == 1 then + if self._maxargs == 0 then + action, init = "store_true", nil + else + action, init = "store", nil + end + else + if self._maxargs == 0 then + action, init = "count", 0 + else + action, init = "append", {} + end + end + + if self._action then + action = self._action + end + + if self._has_init then + init = self._init + end + + if type(action) == "string" then + action = actions[action] + end + + return action, init +end + +-- Returns placeholder for `narg`-th argument. +function Argument:_get_argname(narg) + local argname = self._argname or self:_get_default_argname() + + if type(argname) == "table" then + return argname[narg] + else + return argname + end +end + +function Argument:_get_default_argname() + return "<" .. self._name .. ">" +end + +function Option:_get_default_argname() + return "<" .. self:_get_default_target() .. ">" +end + +-- Returns labels to be shown in the help message. +function Argument:_get_label_lines() + return {self._name} +end + +function Option:_get_label_lines() + local argument_list = self:_get_argument_list() + + if #argument_list == 0 then + -- Don't put aliases for simple flags like `-h` on different lines. + return {table.concat(self._aliases, ", ")} + end + + local longest_alias_length = -1 + + for _, alias in ipairs(self._aliases) do + longest_alias_length = math.max(longest_alias_length, #alias) + end + + local argument_list_repr = table.concat(argument_list, " ") + local lines = {} + + for i, alias in ipairs(self._aliases) do + local line = (" "):rep(longest_alias_length - #alias) .. alias .. " " .. argument_list_repr + + if i ~= #self._aliases then + line = line .. "," + end + + table.insert(lines, line) + end + + return lines +end + +function Command:_get_label_lines() + return {table.concat(self._aliases, ", ")} +end + +function Argument:_get_description() + if self._default and self._show_default then + if self._description then + return ("%s (default: %s)"):format(self._description, self._default) + else + return ("default: %s"):format(self._default) + end + else + return self._description or "" + end +end + +function Command:_get_description() + return self._description or "" +end + +function Option:_get_usage() + local usage = self:_get_argument_list() + table.insert(usage, 1, self._name) + usage = table.concat(usage, " ") + + if self._mincount == 0 or self._default then + usage = "[" .. usage .. "]" + end + + return usage +end + +function Argument:_get_default_target() + return self._name +end + +function Option:_get_default_target() + local res + + for _, alias in ipairs(self._aliases) do + if alias:sub(1, 1) == alias:sub(2, 2) then + res = alias:sub(3) + break + end + end + + res = res or self._name:sub(2) + return (res:gsub("-", "_")) +end + +function Option:_is_vararg() + return self._maxargs ~= self._minargs +end + +function Parser:_get_fullname() + local parent = self._parent + local buf = {self._name} + + while parent do + table.insert(buf, 1, parent._name) + parent = parent._parent + end + + return table.concat(buf, " ") +end + +function Parser:_update_charset(charset) + charset = charset or {} + + for _, command in ipairs(self._commands) do + command:_update_charset(charset) + end + + for _, option in ipairs(self._options) do + for _, alias in ipairs(option._aliases) do + charset[alias:sub(1, 1)] = true + end + end + + return charset +end + +function Parser:argument(...) + local argument = Argument(...) + table.insert(self._arguments, argument) + return argument +end + +function Parser:option(...) + local option = Option(...) + + if self._has_help then + table.insert(self._options, #self._options, option) + else + table.insert(self._options, option) + end + + return option +end + +function Parser:flag(...) + return self:option():args(0)(...) +end + +function Parser:command(...) + local command = Command():add_help(true)(...) + command._parent = self + table.insert(self._commands, command) + return command +end + +function Parser:mutex(...) + local elements = {...} + + for i, element in ipairs(elements) do + local mt = getmetatable(element) + assert(mt == Option or mt == Argument, ("bad argument #%d to 'mutex' (Option or Argument expected)"):format(i)) + end + + table.insert(self._mutexes, elements) + return self +end + +function Parser:group(name, ...) + assert(type(name) == "string", ("bad argument #1 to 'group' (string expected, got %s)"):format(type(name))) + + local group = {name = name, ...} + + for i, element in ipairs(group) do + local mt = getmetatable(element) + assert(mt == Option or mt == Argument or mt == Command, + ("bad argument #%d to 'group' (Option or Argument or Command expected)"):format(i + 1)) + end + + table.insert(self._groups, group) + return self +end + +local usage_welcome = "Usage: " + +function Parser:get_usage() + if self._usage then + return self._usage + end + + local usage_margin = self:_inherit_property("usage_margin", #usage_welcome) + local max_usage_width = self:_inherit_property("usage_max_width", 70) + local lines = {usage_welcome .. self:_get_fullname()} + + local function add(s) + if #lines[#lines]+1+#s <= max_usage_width then + lines[#lines] = lines[#lines] .. " " .. s + else + lines[#lines+1] = (" "):rep(usage_margin) .. s + end + end + + -- Normally options are before positional arguments in usage messages. + -- However, vararg options should be after, because they can't be reliable used + -- before a positional argument. + -- Mutexes come into play, too, and are shown as soon as possible. + -- Overall, output usages in the following order: + -- 1. Mutexes that don't have positional arguments or vararg options. + -- 2. Options that are not in any mutexes and are not vararg. + -- 3. Positional arguments - on their own or as a part of a mutex. + -- 4. Remaining mutexes. + -- 5. Remaining options. + + local elements_in_mutexes = {} + local added_elements = {} + local added_mutexes = {} + local argument_to_mutexes = {} + + local function add_mutex(mutex, main_argument) + if added_mutexes[mutex] then + return + end + + added_mutexes[mutex] = true + local buf = {} + + for _, element in ipairs(mutex) do + if not element._hidden and not added_elements[element] then + if getmetatable(element) == Option or element == main_argument then + table.insert(buf, element:_get_usage()) + added_elements[element] = true + end + end + end + + if #buf == 1 then + add(buf[1]) + elseif #buf > 1 then + add("(" .. table.concat(buf, " | ") .. ")") + end + end + + local function add_element(element) + if not element._hidden and not added_elements[element] then + add(element:_get_usage()) + added_elements[element] = true + end + end + + for _, mutex in ipairs(self._mutexes) do + local is_vararg = false + local has_argument = false + + for _, element in ipairs(mutex) do + if getmetatable(element) == Option then + if element:_is_vararg() then + is_vararg = true + end + else + has_argument = true + argument_to_mutexes[element] = argument_to_mutexes[element] or {} + table.insert(argument_to_mutexes[element], mutex) + end + + elements_in_mutexes[element] = true + end + + if not is_vararg and not has_argument then + add_mutex(mutex) + end + end + + for _, option in ipairs(self._options) do + if not elements_in_mutexes[option] and not option:_is_vararg() then + add_element(option) + end + end + + -- Add usages for positional arguments, together with one mutex containing them, if they are in a mutex. + for _, argument in ipairs(self._arguments) do + -- Pick a mutex as a part of which to show this argument, take the first one that's still available. + local mutex + + if elements_in_mutexes[argument] then + for _, argument_mutex in ipairs(argument_to_mutexes[argument]) do + if not added_mutexes[argument_mutex] then + mutex = argument_mutex + end + end + end + + if mutex then + add_mutex(mutex, argument) + else + add_element(argument) + end + end + + for _, mutex in ipairs(self._mutexes) do + add_mutex(mutex) + end + + for _, option in ipairs(self._options) do + add_element(option) + end + + if #self._commands > 0 then + if self._require_command then + add("") + else + add("[]") + end + + add("...") + end + + return table.concat(lines, "\n") +end + +local function split_lines(s) + if s == "" then + return {} + end + + local lines = {} + + if s:sub(-1) ~= "\n" then + s = s .. "\n" + end + + for line in s:gmatch("([^\n]*)\n") do + table.insert(lines, line) + end + + return lines +end + +local function autowrap_line(line, max_length) + -- Algorithm for splitting lines is simple and greedy. + local result_lines = {} + + -- Preserve original indentation of the line, put this at the beginning of each result line. + -- If the first word looks like a list marker ('*', '+', or '-'), add spaces so that starts + -- of the second and the following lines vertically align with the start of the second word. + local indentation = line:match("^ *") + + if line:find("^ *[%*%+%-]") then + indentation = indentation .. " " .. line:match("^ *[%*%+%-]( *)") + end + + -- Parts of the last line being assembled. + local line_parts = {} + + -- Length of the current line. + local line_length = 0 + + -- Index of the next character to consider. + local index = 1 + + while true do + local word_start, word_finish, word = line:find("([^ ]+)", index) + + if not word_start then + -- Ignore trailing spaces, if any. + break + end + + local preceding_spaces = line:sub(index, word_start - 1) + index = word_finish + 1 + + if (#line_parts == 0) or (line_length + #preceding_spaces + #word <= max_length) then + -- Either this is the very first word or it fits as an addition to the current line, add it. + table.insert(line_parts, preceding_spaces) -- For the very first word this adds the indentation. + table.insert(line_parts, word) + line_length = line_length + #preceding_spaces + #word + else + -- Does not fit, finish current line and put the word into a new one. + table.insert(result_lines, table.concat(line_parts)) + line_parts = {indentation, word} + line_length = #indentation + #word + end + end + + if #line_parts > 0 then + table.insert(result_lines, table.concat(line_parts)) + end + + if #result_lines == 0 then + -- Preserve empty lines. + result_lines[1] = "" + end + + return result_lines +end + +-- Automatically wraps lines within given array, +-- attempting to limit line length to `max_length`. +-- Existing line splits are preserved. +local function autowrap(lines, max_length) + local result_lines = {} + + for _, line in ipairs(lines) do + local autowrapped_lines = autowrap_line(line, max_length) + + for _, autowrapped_line in ipairs(autowrapped_lines) do + table.insert(result_lines, autowrapped_line) + end + end + + return result_lines +end + +function Parser:_get_element_help(element) + local label_lines = element:_get_label_lines() + local description_lines = split_lines(element:_get_description()) + + local result_lines = {} + + -- All label lines should have the same length (except the last one, it has no comma). + -- If too long, start description after all the label lines. + -- Otherwise, combine label and description lines. + + local usage_margin_len = self:_inherit_property("help_usage_margin", 3) + local usage_margin = (" "):rep(usage_margin_len) + local description_margin_len = self:_inherit_property("help_description_margin", 25) + local description_margin = (" "):rep(description_margin_len) + + local help_max_width = self:_inherit_property("help_max_width") + + if help_max_width then + local description_max_width = math.max(help_max_width - description_margin_len, 10) + description_lines = autowrap(description_lines, description_max_width) + end + + if #label_lines[1] >= (description_margin_len - usage_margin_len) then + for _, label_line in ipairs(label_lines) do + table.insert(result_lines, usage_margin .. label_line) + end + + for _, description_line in ipairs(description_lines) do + table.insert(result_lines, description_margin .. description_line) + end + else + for i = 1, math.max(#label_lines, #description_lines) do + local label_line = label_lines[i] + local description_line = description_lines[i] + + local line = "" + + if label_line then + line = usage_margin .. label_line + end + + if description_line and description_line ~= "" then + line = line .. (" "):rep(description_margin_len - #line) .. description_line + end + + table.insert(result_lines, line) + end + end + + return table.concat(result_lines, "\n") +end + +local function get_group_types(group) + local types = {} + + for _, element in ipairs(group) do + types[getmetatable(element)] = true + end + + return types +end + +function Parser:_add_group_help(blocks, added_elements, label, elements) + local buf = {label} + + for _, element in ipairs(elements) do + if not element._hidden and not added_elements[element] then + added_elements[element] = true + table.insert(buf, self:_get_element_help(element)) + end + end + + if #buf > 1 then + table.insert(blocks, table.concat(buf, ("\n"):rep(self:_inherit_property("help_vertical_space", 0) + 1))) + end +end + +function Parser:get_help() + if self._help then + return self._help + end + + local blocks = {self:get_usage()} + + local help_max_width = self:_inherit_property("help_max_width") + + if self._description then + local description = self._description + + if help_max_width then + description = table.concat(autowrap(split_lines(description), help_max_width), "\n") + end + + table.insert(blocks, description) + end + + -- 1. Put groups containing arguments first, then other arguments. + -- 2. Put remaining groups containing options, then other options. + -- 3. Put remaining groups containing commands, then other commands. + -- Assume that an element can't be in several groups. + local groups_by_type = { + [Argument] = {}, + [Option] = {}, + [Command] = {} + } + + for _, group in ipairs(self._groups) do + local group_types = get_group_types(group) + + for _, mt in ipairs({Argument, Option, Command}) do + if group_types[mt] then + table.insert(groups_by_type[mt], group) + break + end + end + end + + local default_groups = { + {name = "Arguments", type = Argument, elements = self._arguments}, + {name = "Options", type = Option, elements = self._options}, + {name = "Commands", type = Command, elements = self._commands} + } + + local added_elements = {} + + for _, default_group in ipairs(default_groups) do + local type_groups = groups_by_type[default_group.type] + + for _, group in ipairs(type_groups) do + self:_add_group_help(blocks, added_elements, group.name .. ":", group) + end + + local default_label = default_group.name .. ":" + + if #type_groups > 0 then + default_label = "Other " .. default_label:gsub("^.", string.lower) + end + + self:_add_group_help(blocks, added_elements, default_label, default_group.elements) + end + + if self._epilog then + local epilog = self._epilog + + if help_max_width then + epilog = table.concat(autowrap(split_lines(epilog), help_max_width), "\n") + end + + table.insert(blocks, epilog) + end + + return table.concat(blocks, "\n\n") +end + +local function get_tip(context, wrong_name) + local context_pool = {} + local possible_name + local possible_names = {} + + for name in pairs(context) do + if type(name) == "string" then + for i = 1, #name do + possible_name = name:sub(1, i - 1) .. name:sub(i + 1) + + if not context_pool[possible_name] then + context_pool[possible_name] = {} + end + + table.insert(context_pool[possible_name], name) + end + end + end + + for i = 1, #wrong_name + 1 do + possible_name = wrong_name:sub(1, i - 1) .. wrong_name:sub(i + 1) + + if context[possible_name] then + possible_names[possible_name] = true + elseif context_pool[possible_name] then + for _, name in ipairs(context_pool[possible_name]) do + possible_names[name] = true + end + end + end + + local first = next(possible_names) + + if first then + if next(possible_names, first) then + local possible_names_arr = {} + + for name in pairs(possible_names) do + table.insert(possible_names_arr, "'" .. name .. "'") + end + + table.sort(possible_names_arr) + return "\nDid you mean one of these: " .. table.concat(possible_names_arr, " ") .. "?" + else + return "\nDid you mean '" .. first .. "'?" + end + else + return "" + end +end + +local ElementState = class({ + invocations = 0 +}) + +function ElementState:__call(state, element) + self.state = state + self.result = state.result + self.element = element + self.target = element._target or element:_get_default_target() + self.action, self.result[self.target] = element:_get_action() + return self +end + +function ElementState:error(fmt, ...) + self.state:error(fmt, ...) +end + +function ElementState:convert(argument, index) + local converter = self.element._convert + + if converter then + local ok, err + + if type(converter) == "function" then + ok, err = converter(argument) + elseif type(converter[index]) == "function" then + ok, err = converter[index](argument) + else + ok = converter[argument] + end + + if ok == nil then + self:error(err and "%s" or "malformed argument '%s'", err or argument) + end + + argument = ok + end + + return argument +end + +function ElementState:default(mode) + return self.element._defmode:find(mode) and self.element._default +end + +local function bound(noun, min, max, is_max) + local res = "" + + if min ~= max then + res = "at " .. (is_max and "most" or "least") .. " " + end + + local number = is_max and max or min + return res .. tostring(number) .. " " .. noun .. (number == 1 and "" or "s") +end + +function ElementState:set_name(alias) + self.name = ("%s '%s'"):format(alias and "option" or "argument", alias or self.element._name) +end + +function ElementState:invoke() + self.open = true + self.overwrite = false + + if self.invocations >= self.element._maxcount then + if self.element._overwrite then + self.overwrite = true + else + local num_times_repr = bound("time", self.element._mincount, self.element._maxcount, true) + self:error("%s must be used %s", self.name, num_times_repr) + end + else + self.invocations = self.invocations + 1 + end + + self.args = {} + + if self.element._maxargs <= 0 then + self:close() + end + + return self.open +end + +function ElementState:pass(argument) + argument = self:convert(argument, #self.args + 1) + table.insert(self.args, argument) + + if #self.args >= self.element._maxargs then + self:close() + end + + return self.open +end + +function ElementState:complete_invocation() + while #self.args < self.element._minargs do + self:pass(self.element._default) + end +end + +function ElementState:close() + if self.open then + self.open = false + + if #self.args < self.element._minargs then + if self:default("a") then + self:complete_invocation() + else + if #self.args == 0 then + if getmetatable(self.element) == Argument then + self:error("missing %s", self.name) + elseif self.element._maxargs == 1 then + self:error("%s requires an argument", self.name) + end + end + + self:error("%s requires %s", self.name, bound("argument", self.element._minargs, self.element._maxargs)) + end + end + + local args + + if self.element._maxargs == 0 then + args = self.args[1] + elseif self.element._maxargs == 1 then + if self.element._minargs == 0 and self.element._mincount ~= self.element._maxcount then + args = self.args + else + args = self.args[1] + end + else + args = self.args + end + + self.action(self.result, self.target, args, self.overwrite) + end +end + +local ParseState = class({ + result = {}, + options = {}, + arguments = {}, + argument_i = 1, + element_to_mutexes = {}, + mutex_to_element_state = {}, + command_actions = {} +}) + +function ParseState:__call(parser, error_handler) + self.parser = parser + self.error_handler = error_handler + self.charset = parser:_update_charset() + self:switch(parser) + return self +end + +function ParseState:error(fmt, ...) + self.error_handler(self.parser, fmt:format(...)) +end + +function ParseState:switch(parser) + self.parser = parser + + if parser._action then + table.insert(self.command_actions, {action = parser._action, name = parser._name}) + end + + for _, option in ipairs(parser._options) do + option = ElementState(self, option) + table.insert(self.options, option) + + for _, alias in ipairs(option.element._aliases) do + self.options[alias] = option + end + end + + for _, mutex in ipairs(parser._mutexes) do + for _, element in ipairs(mutex) do + if not self.element_to_mutexes[element] then + self.element_to_mutexes[element] = {} + end + + table.insert(self.element_to_mutexes[element], mutex) + end + end + + for _, argument in ipairs(parser._arguments) do + argument = ElementState(self, argument) + table.insert(self.arguments, argument) + argument:set_name() + argument:invoke() + end + + self.handle_options = parser._handle_options + self.argument = self.arguments[self.argument_i] + self.commands = parser._commands + + for _, command in ipairs(self.commands) do + for _, alias in ipairs(command._aliases) do + self.commands[alias] = command + end + end +end + +function ParseState:get_option(name) + local option = self.options[name] + + if not option then + self:error("unknown option '%s'%s", name, get_tip(self.options, name)) + else + return option + end +end + +function ParseState:get_command(name) + local command = self.commands[name] + + if not command then + if #self.commands > 0 then + self:error("unknown command '%s'%s", name, get_tip(self.commands, name)) + else + self:error("too many arguments") + end + else + return command + end +end + +function ParseState:check_mutexes(element_state) + if self.element_to_mutexes[element_state.element] then + for _, mutex in ipairs(self.element_to_mutexes[element_state.element]) do + local used_element_state = self.mutex_to_element_state[mutex] + + if used_element_state and used_element_state ~= element_state then + self:error("%s can not be used together with %s", element_state.name, used_element_state.name) + else + self.mutex_to_element_state[mutex] = element_state + end + end + end +end + +function ParseState:invoke(option, name) + self:close() + option:set_name(name) + self:check_mutexes(option, name) + + if option:invoke() then + self.option = option + end +end + +function ParseState:pass(arg) + if self.option then + if not self.option:pass(arg) then + self.option = nil + end + elseif self.argument then + self:check_mutexes(self.argument) + + if not self.argument:pass(arg) then + self.argument_i = self.argument_i + 1 + self.argument = self.arguments[self.argument_i] + end + else + local command = self:get_command(arg) + self.result[command._target or command._name] = true + + if self.parser._command_target then + self.result[self.parser._command_target] = command._name + end + + self:switch(command) + end +end + +function ParseState:close() + if self.option then + self.option:close() + self.option = nil + end +end + +function ParseState:finalize() + self:close() + + for i = self.argument_i, #self.arguments do + local argument = self.arguments[i] + if #argument.args == 0 and argument:default("u") then + argument:complete_invocation() + else + argument:close() + end + end + + if self.parser._require_command and #self.commands > 0 then + self:error("a command is required") + end + + for _, option in ipairs(self.options) do + option.name = option.name or ("option '%s'"):format(option.element._name) + + if option.invocations == 0 then + if option:default("u") then + option:invoke() + option:complete_invocation() + option:close() + end + end + + local mincount = option.element._mincount + + if option.invocations < mincount then + if option:default("a") then + while option.invocations < mincount do + option:invoke() + option:close() + end + elseif option.invocations == 0 then + self:error("missing %s", option.name) + else + self:error("%s must be used %s", option.name, bound("time", mincount, option.element._maxcount)) + end + end + end + + for i = #self.command_actions, 1, -1 do + self.command_actions[i].action(self.result, self.command_actions[i].name) + end +end + +function ParseState:parse(args) + for _, arg in ipairs(args) do + local plain = true + + if self.handle_options then + local first = arg:sub(1, 1) + + if self.charset[first] then + if #arg > 1 then + plain = false + + if arg:sub(2, 2) == first then + if #arg == 2 then + if self.options[arg] then + local option = self:get_option(arg) + self:invoke(option, arg) + else + self:close() + end + + self.handle_options = false + else + local equals = arg:find "=" + if equals then + local name = arg:sub(1, equals - 1) + local option = self:get_option(name) + + if option.element._maxargs <= 0 then + self:error("option '%s' does not take arguments", name) + end + + self:invoke(option, name) + self:pass(arg:sub(equals + 1)) + else + local option = self:get_option(arg) + self:invoke(option, arg) + end + end + else + for i = 2, #arg do + local name = first .. arg:sub(i, i) + local option = self:get_option(name) + self:invoke(option, name) + + if i ~= #arg and option.element._maxargs > 0 then + self:pass(arg:sub(i + 1)) + break + end + end + end + end + end + end + + if plain then + self:pass(arg) + end + end + + self:finalize() + return self.result +end + +function Parser:error(msg) + io.stderr:write(("%s\n\nError: %s\n"):format(self:get_usage(), msg)) + os.exit(1) +end + +-- Compatibility with strict.lua and other checkers: +local default_cmdline = rawget(_ENV, "arg") or {} + +function Parser:_parse(args, error_handler) + return ParseState(self, error_handler):parse(args or default_cmdline) +end + +function Parser:parse(args) + return self:_parse(args, self.error) +end + +local function xpcall_error_handler(err) + return tostring(err) .. "\noriginal " .. debug.traceback("", 2):sub(2) +end + +function Parser:pparse(args) + local parse_error + + local ok, result = xpcall(function() + return self:_parse(args, function(_, err) + parse_error = err + error(err, 0) + end) + end, xpcall_error_handler) + + if ok then + return true, result + elseif not parse_error then + error(result, 0) + else + return false, parse_error + end +end + +local argparse = {} + +argparse.version = "0.6.0" + +setmetatable(argparse, {__call = function(_, ...) + return Parser(default_cmdline[0]):add_help(true)(...) +end}) + +return argparse \ No newline at end of file diff --git a/moonscript/etc/fstab b/moonscript/etc/fstab new file mode 100644 index 0000000..539787f --- /dev/null +++ b/moonscript/etc/fstab @@ -0,0 +1,4 @@ +packages/moonscript gitfs leafo/moonscript/master/bin +rom/modules/main/moonscript gitfs leafo/moonscript/master/moonscript +rom/modules/main/moon gitfs leafo/moonscript/master/moon +rom/modules/main/argparse.lua linkfs packages/moonscript/argparse.lua -- 2.49.1 From de3d73de703ac1e3b57d85ecff0bffb11621a878 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Fri, 5 Jun 2020 21:44:41 -0600 Subject: [PATCH 62/90] update package list --- packages.list | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages.list b/packages.list index 41bbf6a..ac02ba3 100644 --- a/packages.list +++ b/packages.list @@ -3,6 +3,7 @@ [ 'cash' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/cash/.package', [ 'ccemux' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/ccemux/.package', [ 'common' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/common/.package', + [ 'compress' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/compress/.package', [ 'core' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/core/.package', [ 'debugger' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/debugger/.package', [ 'farms' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/farms/.package', @@ -11,6 +12,7 @@ -- [ 'glasses' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/glasses/.package', [ 'gps' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/gps/.package', [ 'lfs' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/lfs/.package', + [ 'lpeg' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/lpeg/.package', [ 'lzwfs' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/lzwfs/.package', -- [ 'mbs' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/mbs/.package', [ 'milo' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/milo/.package', @@ -18,6 +20,7 @@ [ 'miners' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/miners/.package', [ 'minify' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/minify/.package', [ 'monitor' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/monitor/.package', + [ 'moonscript' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/moonscript/.package', [ 'neural' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/neural/.package', -- [ 'pickup' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/pickup/.package', [ 'recipeBook' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/recipeBook/.package', -- 2.49.1 From 0f7534d12c67a00404c57bc9dbe545e348abb98c Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Tue, 9 Jun 2020 18:17:21 -0600 Subject: [PATCH 63/90] moonscript, busted, penlight packages + debugger speed improvements --- busted/.package | 9 + busted/depend/system.lua | 16 + busted/depend/term.lua | 5 + busted/etc/fstab | 8 + debugger/apis/init.lua | 30 +- debugger/debug.lua | 3 +- moonscript/README.txt | 10 +- moonscript/T.lua | 8 +- moonscript/T.moon | 11 +- moonscript/argparse.lua | 1527 -------------------------- moonscript/etc/fstab | 9 +- moonscript/moon | 116 ++ moonscript/moonc | 235 ++++ penlight/etc/fstab | 1 - {penlight => pl}/.package | 4 +- pl/apis/compat.lua | 188 ++++ pl/apis/path.lua | 445 ++++++++ pl/etc/fstab | 1 + {penlight => pl}/init/6.penlight.lua | 0 19 files changed, 1065 insertions(+), 1561 deletions(-) create mode 100644 busted/.package create mode 100644 busted/depend/system.lua create mode 100644 busted/depend/term.lua create mode 100644 busted/etc/fstab delete mode 100644 moonscript/argparse.lua create mode 100644 moonscript/moon create mode 100644 moonscript/moonc delete mode 100644 penlight/etc/fstab rename {penlight => pl}/.package (81%) create mode 100644 pl/apis/compat.lua create mode 100644 pl/apis/path.lua create mode 100644 pl/etc/fstab rename {penlight => pl}/init/6.penlight.lua (100%) diff --git a/busted/.package b/busted/.package new file mode 100644 index 0000000..3bfdfce --- /dev/null +++ b/busted/.package @@ -0,0 +1,9 @@ +{ + title = 'busted', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/moonscript', + description = [[]], + license = 'MIT', + required = { + 'penlight', + }, +} diff --git a/busted/depend/system.lua b/busted/depend/system.lua new file mode 100644 index 0000000..b181771 --- /dev/null +++ b/busted/depend/system.lua @@ -0,0 +1,16 @@ +return { + -- Returns the monotonic time the system has been up, in secconds. + monotime = function() + return os.clock() + end, + + -- Sleep for n seconds. + sleep = function(n) + os.sleep(n) + end, + + -- Returns the current system time, 1970 (UTC), in secconds. + gettime = function() + return os.epoch('utc') / 1000 + end, +} diff --git a/busted/depend/term.lua b/busted/depend/term.lua new file mode 100644 index 0000000..01278d4 --- /dev/null +++ b/busted/depend/term.lua @@ -0,0 +1,5 @@ +return { + isatty = function() + return false + end, +} diff --git a/busted/etc/fstab b/busted/etc/fstab new file mode 100644 index 0000000..c1141b8 --- /dev/null +++ b/busted/etc/fstab @@ -0,0 +1,8 @@ +packages/busted/busted urlfs https://raw.githubusercontent.com/Olivine-Labs/busted/master/bin/busted +rom/modules/main/busted gitfs Olivine-Labs/busted/master/busted +rom/modules/main/mediator.lua urlfs https://raw.githubusercontent.com/Olivine-Labs/mediator_lua/master/src/mediator.lua +rom/modules/main/cliargs gitfs amireh/lua_cliargs/master/src/cliargs +rom/modules/main/luassert gitfs Olivine-Labs/luassert/master/src +rom/modules/main/say.lua urlfs https://raw.githubusercontent.com/Olivine-Labs/say/master/src/init.lua +rom/modules/main/term.lua linkfs packages/busted/depend/term.lua +rom/modules/main/system.lua linkfs packages/busted/depend/system.lua diff --git a/debugger/apis/init.lua b/debugger/apis/init.lua index 1e860b8..6f19958 100644 --- a/debugger/apis/init.lua +++ b/debugger/apis/init.lua @@ -9,21 +9,25 @@ local dbg = { breakpoints = nil, } -local function breakpointHook(info) +local function breakpointHook(depth, lineNo) if dbg.breakpoints then - local src = info.short_src - for _,v in pairs(dbg.breakpoints) do - if v.line == info.currentline - and (v.file == src or v.bfile == src) then - return not v.disabled + local info + for _,v in ipairs(dbg.breakpoints) do + if v.line == lineNo then + if not info then + info = debug.getinfo(depth) + end + if (v.file == info.short_src or v.bfile == info.short_src) then + return not v.disabled + end end end end end local function functionHook(fn) - return function(info) - return info.func == fn + return function() + return debug.getinfo(3).func == fn end end @@ -42,9 +46,9 @@ local function stackSizeHook(n) end i = i + 1 end - return function(info) + return function(depth, lineNo) return not debug.getinfo(i - n) - or breakpointHook(info) + or breakpointHook(depth + 1, lineNo) end end @@ -133,12 +137,10 @@ local function get_trace(offset, stack_inspect_offset) return t end -local function hook() - local info = debug.getinfo(2) - +local function hook(_, lineNo) local h = dbg.hooks[coroutine.running()] - if h and h.eval(info) then + if h and h.eval(3, lineNo) then local inspectOffset = 0 repeat diff --git a/debugger/debug.lua b/debugger/debug.lua index c0f57d2..2a3ac39 100644 --- a/debugger/debug.lua +++ b/debugger/debug.lua @@ -1,3 +1,4 @@ +local class = require('opus.class') local Config = require('opus.config') local Event = require('opus.event') local UI = require('opus.ui') @@ -128,7 +129,7 @@ local function message(...) client:resume('debugger', ...) end -UI.InverseButton = require('opus.class')(UI.Button) +UI.InverseButton = class(UI.Button) UI.InverseButton.defaults = { UIElement = 'InverseButton', backgroundColor = 'primary', diff --git a/moonscript/README.txt b/moonscript/README.txt index d306970..d0b8bfa 100644 --- a/moonscript/README.txt +++ b/moonscript/README.txt @@ -1,5 +1,7 @@ -running the compiler works fine... -moonc T.moon <-- OK +moonscript must be run in compatibility mode: +> compat moon T.moon +> compat moonc T.moon -working on getting the moon command to work properly -moon T.moon <-- NOPE +moon and moonc were modified to allow relative paths: +> cd /packages/moonscript +> compat moonc T.moon diff --git a/moonscript/T.lua b/moonscript/T.lua index 8825f17..cd0fa6d 100644 --- a/moonscript/T.lua +++ b/moonscript/T.lua @@ -1,11 +1,13 @@ local Event = require('opus.event') local UI = require('opus.ui') -local kernel = _G.kernel -local multishell = _ENV.multishell +local kernel +kernel = _G.kernel +local multishell +multishell = _ENV.multishell local tasks = multishell and multishell.getTabs and multishell.getTabs() or kernel.routines UI:configure('Tasks', ...) local page = UI.Page({ - menuBar = UI.MenuBar({ + UI.MenuBar({ buttons = { { text = 'Activate', diff --git a/moonscript/T.moon b/moonscript/T.moon index 6cbf5f4..2fbbc27 100644 --- a/moonscript/T.moon +++ b/moonscript/T.moon @@ -1,14 +1,15 @@ Event = require('opus.event') UI = require('opus.ui') -kernel = _G.kernel -multishell = _ENV.multishell -tasks = multishell and multishell.getTabs and multishell.getTabs() or kernel.routines +import kernel from _G +import multishell from _ENV + +tasks = multishell and multishell.getTabs and multishell.getTabs! or kernel.routines UI\configure 'Tasks', ... page = UI.Page { - menuBar: UI.MenuBar { + UI.MenuBar { buttons: { { text: 'Activate', event: 'activate' }, { text: 'Terminate', event: 'terminate' }, @@ -43,7 +44,7 @@ page = UI.Page { t: 'terminate', }, eventHandler: (event) => - t = self.grid\getSelected! + t = @grid\getSelected! switch event.type when 'activate', 'grid_select' multishell.setFocus t.uid if t diff --git a/moonscript/argparse.lua b/moonscript/argparse.lua deleted file mode 100644 index de89237..0000000 --- a/moonscript/argparse.lua +++ /dev/null @@ -1,1527 +0,0 @@ --- The MIT License (MIT) - --- Copyright (c) 2013 - 2018 Peter Melnichenko - --- Permission is hereby granted, free of charge, to any person obtaining a copy of --- this software and associated documentation files (the "Software"), to deal in --- the Software without restriction, including without limitation the rights to --- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of --- the Software, and to permit persons to whom the Software is furnished to do so, --- subject to the following conditions: - --- The above copyright notice and this permission notice shall be included in all --- copies or substantial portions of the Software. - --- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS --- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR --- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER --- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN --- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -local function deep_update(t1, t2) - for k, v in pairs(t2) do - if type(v) == "table" then - v = deep_update({}, v) - end - - t1[k] = v - end - - return t1 -end - --- A property is a tuple {name, callback}. --- properties.args is number of properties that can be set as arguments --- when calling an object. -local function class(prototype, properties, parent) - -- Class is the metatable of its instances. - local cl = {} - cl.__index = cl - - if parent then - cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype) - else - cl.__prototype = prototype - end - - if properties then - local names = {} - - -- Create setter methods and fill set of property names. - for _, property in ipairs(properties) do - local name, callback = property[1], property[2] - - cl[name] = function(self, value) - if not callback(self, value) then - self["_" .. name] = value - end - - return self - end - - names[name] = true - end - - function cl.__call(self, ...) - -- When calling an object, if the first argument is a table, - -- interpret keys as property names, else delegate arguments - -- to corresponding setters in order. - if type((...)) == "table" then - for name, value in pairs((...)) do - if names[name] then - self[name](self, value) - end - end - else - local nargs = select("#", ...) - - for i, property in ipairs(properties) do - if i > nargs or i > properties.args then - break - end - - local arg = select(i, ...) - - if arg ~= nil then - self[property[1]](self, arg) - end - end - end - - return self - end - end - - -- If indexing class fails, fallback to its parent. - local class_metatable = {} - class_metatable.__index = parent - - function class_metatable.__call(self, ...) - -- Calling a class returns its instance. - -- Arguments are delegated to the instance. - local object = deep_update({}, self.__prototype) - setmetatable(object, self) - return object(...) - end - - return setmetatable(cl, class_metatable) -end - -local function typecheck(name, types, value) - for _, type_ in ipairs(types) do - if type(value) == type_ then - return true - end - end - - error(("bad property '%s' (%s expected, got %s)"):format(name, table.concat(types, " or "), type(value))) -end - -local function typechecked(name, ...) - local types = {...} - return {name, function(_, value) typecheck(name, types, value) end} -end - -local multiname = {"name", function(self, value) - typecheck("name", {"string"}, value) - - for alias in value:gmatch("%S+") do - self._name = self._name or alias - table.insert(self._aliases, alias) - end - - -- Do not set _name as with other properties. - return true -end} - -local function parse_boundaries(str) - if tonumber(str) then - return tonumber(str), tonumber(str) - end - - if str == "*" then - return 0, math.huge - end - - if str == "+" then - return 1, math.huge - end - - if str == "?" then - return 0, 1 - end - - if str:match "^%d+%-%d+$" then - local min, max = str:match "^(%d+)%-(%d+)$" - return tonumber(min), tonumber(max) - end - - if str:match "^%d+%+$" then - local min = str:match "^(%d+)%+$" - return tonumber(min), math.huge - end -end - -local function boundaries(name) - return {name, function(self, value) - typecheck(name, {"number", "string"}, value) - - local min, max = parse_boundaries(value) - - if not min then - error(("bad property '%s'"):format(name)) - end - - self["_min" .. name], self["_max" .. name] = min, max - end} -end - -local actions = {} - -local option_action = {"action", function(_, value) - typecheck("action", {"function", "string"}, value) - - if type(value) == "string" and not actions[value] then - error(("unknown action '%s'"):format(value)) - end -end} - -local option_init = {"init", function(self) - self._has_init = true -end} - -local option_default = {"default", function(self, value) - if type(value) ~= "string" then - self._init = value - self._has_init = true - return true - end -end} - -local add_help = {"add_help", function(self, value) - typecheck("add_help", {"boolean", "string", "table"}, value) - - if self._has_help then - table.remove(self._options) - self._has_help = false - end - - if value then - local help = self:flag() - :description "Show this help message and exit." - :action(function() - print(self:get_help()) - os.exit(0) - end) - - if value ~= true then - help = help(value) - end - - if not help._name then - help "-h" "--help" - end - - self._has_help = true - end -end} - -local Parser = class({ - _arguments = {}, - _options = {}, - _commands = {}, - _mutexes = {}, - _groups = {}, - _require_command = true, - _handle_options = true -}, { - args = 3, - typechecked("name", "string"), - typechecked("description", "string"), - typechecked("epilog", "string"), - typechecked("usage", "string"), - typechecked("help", "string"), - typechecked("require_command", "boolean"), - typechecked("handle_options", "boolean"), - typechecked("action", "function"), - typechecked("command_target", "string"), - typechecked("help_vertical_space", "number"), - typechecked("usage_margin", "number"), - typechecked("usage_max_width", "number"), - typechecked("help_usage_margin", "number"), - typechecked("help_description_margin", "number"), - typechecked("help_max_width", "number"), - add_help -}) - -local Command = class({ - _aliases = {} -}, { - args = 3, - multiname, - typechecked("description", "string"), - typechecked("epilog", "string"), - typechecked("target", "string"), - typechecked("usage", "string"), - typechecked("help", "string"), - typechecked("require_command", "boolean"), - typechecked("handle_options", "boolean"), - typechecked("action", "function"), - typechecked("command_target", "string"), - typechecked("help_vertical_space", "number"), - typechecked("usage_margin", "number"), - typechecked("usage_max_width", "number"), - typechecked("help_usage_margin", "number"), - typechecked("help_description_margin", "number"), - typechecked("help_max_width", "number"), - typechecked("hidden", "boolean"), - add_help -}, Parser) - -local Argument = class({ - _minargs = 1, - _maxargs = 1, - _mincount = 1, - _maxcount = 1, - _defmode = "unused", - _show_default = true -}, { - args = 5, - typechecked("name", "string"), - typechecked("description", "string"), - option_default, - typechecked("convert", "function", "table"), - boundaries("args"), - typechecked("target", "string"), - typechecked("defmode", "string"), - typechecked("show_default", "boolean"), - typechecked("argname", "string", "table"), - typechecked("hidden", "boolean"), - option_action, - option_init -}) - -local Option = class({ - _aliases = {}, - _mincount = 0, - _overwrite = true -}, { - args = 6, - multiname, - typechecked("description", "string"), - option_default, - typechecked("convert", "function", "table"), - boundaries("args"), - boundaries("count"), - typechecked("target", "string"), - typechecked("defmode", "string"), - typechecked("show_default", "boolean"), - typechecked("overwrite", "boolean"), - typechecked("argname", "string", "table"), - typechecked("hidden", "boolean"), - option_action, - option_init -}, Argument) - -function Parser:_inherit_property(name, default) - local element = self - - while true do - local value = element["_" .. name] - - if value ~= nil then - return value - end - - if not element._parent then - return default - end - - element = element._parent - end -end - -function Argument:_get_argument_list() - local buf = {} - local i = 1 - - while i <= math.min(self._minargs, 3) do - local argname = self:_get_argname(i) - - if self._default and self._defmode:find "a" then - argname = "[" .. argname .. "]" - end - - table.insert(buf, argname) - i = i+1 - end - - while i <= math.min(self._maxargs, 3) do - table.insert(buf, "[" .. self:_get_argname(i) .. "]") - i = i+1 - - if self._maxargs == math.huge then - break - end - end - - if i < self._maxargs then - table.insert(buf, "...") - end - - return buf -end - -function Argument:_get_usage() - local usage = table.concat(self:_get_argument_list(), " ") - - if self._default and self._defmode:find "u" then - if self._maxargs > 1 or (self._minargs == 1 and not self._defmode:find "a") then - usage = "[" .. usage .. "]" - end - end - - return usage -end - -function actions.store_true(result, target) - result[target] = true -end - -function actions.store_false(result, target) - result[target] = false -end - -function actions.store(result, target, argument) - result[target] = argument -end - -function actions.count(result, target, _, overwrite) - if not overwrite then - result[target] = result[target] + 1 - end -end - -function actions.append(result, target, argument, overwrite) - result[target] = result[target] or {} - table.insert(result[target], argument) - - if overwrite then - table.remove(result[target], 1) - end -end - -function actions.concat(result, target, arguments, overwrite) - if overwrite then - error("'concat' action can't handle too many invocations") - end - - result[target] = result[target] or {} - - for _, argument in ipairs(arguments) do - table.insert(result[target], argument) - end -end - -function Argument:_get_action() - local action, init - - if self._maxcount == 1 then - if self._maxargs == 0 then - action, init = "store_true", nil - else - action, init = "store", nil - end - else - if self._maxargs == 0 then - action, init = "count", 0 - else - action, init = "append", {} - end - end - - if self._action then - action = self._action - end - - if self._has_init then - init = self._init - end - - if type(action) == "string" then - action = actions[action] - end - - return action, init -end - --- Returns placeholder for `narg`-th argument. -function Argument:_get_argname(narg) - local argname = self._argname or self:_get_default_argname() - - if type(argname) == "table" then - return argname[narg] - else - return argname - end -end - -function Argument:_get_default_argname() - return "<" .. self._name .. ">" -end - -function Option:_get_default_argname() - return "<" .. self:_get_default_target() .. ">" -end - --- Returns labels to be shown in the help message. -function Argument:_get_label_lines() - return {self._name} -end - -function Option:_get_label_lines() - local argument_list = self:_get_argument_list() - - if #argument_list == 0 then - -- Don't put aliases for simple flags like `-h` on different lines. - return {table.concat(self._aliases, ", ")} - end - - local longest_alias_length = -1 - - for _, alias in ipairs(self._aliases) do - longest_alias_length = math.max(longest_alias_length, #alias) - end - - local argument_list_repr = table.concat(argument_list, " ") - local lines = {} - - for i, alias in ipairs(self._aliases) do - local line = (" "):rep(longest_alias_length - #alias) .. alias .. " " .. argument_list_repr - - if i ~= #self._aliases then - line = line .. "," - end - - table.insert(lines, line) - end - - return lines -end - -function Command:_get_label_lines() - return {table.concat(self._aliases, ", ")} -end - -function Argument:_get_description() - if self._default and self._show_default then - if self._description then - return ("%s (default: %s)"):format(self._description, self._default) - else - return ("default: %s"):format(self._default) - end - else - return self._description or "" - end -end - -function Command:_get_description() - return self._description or "" -end - -function Option:_get_usage() - local usage = self:_get_argument_list() - table.insert(usage, 1, self._name) - usage = table.concat(usage, " ") - - if self._mincount == 0 or self._default then - usage = "[" .. usage .. "]" - end - - return usage -end - -function Argument:_get_default_target() - return self._name -end - -function Option:_get_default_target() - local res - - for _, alias in ipairs(self._aliases) do - if alias:sub(1, 1) == alias:sub(2, 2) then - res = alias:sub(3) - break - end - end - - res = res or self._name:sub(2) - return (res:gsub("-", "_")) -end - -function Option:_is_vararg() - return self._maxargs ~= self._minargs -end - -function Parser:_get_fullname() - local parent = self._parent - local buf = {self._name} - - while parent do - table.insert(buf, 1, parent._name) - parent = parent._parent - end - - return table.concat(buf, " ") -end - -function Parser:_update_charset(charset) - charset = charset or {} - - for _, command in ipairs(self._commands) do - command:_update_charset(charset) - end - - for _, option in ipairs(self._options) do - for _, alias in ipairs(option._aliases) do - charset[alias:sub(1, 1)] = true - end - end - - return charset -end - -function Parser:argument(...) - local argument = Argument(...) - table.insert(self._arguments, argument) - return argument -end - -function Parser:option(...) - local option = Option(...) - - if self._has_help then - table.insert(self._options, #self._options, option) - else - table.insert(self._options, option) - end - - return option -end - -function Parser:flag(...) - return self:option():args(0)(...) -end - -function Parser:command(...) - local command = Command():add_help(true)(...) - command._parent = self - table.insert(self._commands, command) - return command -end - -function Parser:mutex(...) - local elements = {...} - - for i, element in ipairs(elements) do - local mt = getmetatable(element) - assert(mt == Option or mt == Argument, ("bad argument #%d to 'mutex' (Option or Argument expected)"):format(i)) - end - - table.insert(self._mutexes, elements) - return self -end - -function Parser:group(name, ...) - assert(type(name) == "string", ("bad argument #1 to 'group' (string expected, got %s)"):format(type(name))) - - local group = {name = name, ...} - - for i, element in ipairs(group) do - local mt = getmetatable(element) - assert(mt == Option or mt == Argument or mt == Command, - ("bad argument #%d to 'group' (Option or Argument or Command expected)"):format(i + 1)) - end - - table.insert(self._groups, group) - return self -end - -local usage_welcome = "Usage: " - -function Parser:get_usage() - if self._usage then - return self._usage - end - - local usage_margin = self:_inherit_property("usage_margin", #usage_welcome) - local max_usage_width = self:_inherit_property("usage_max_width", 70) - local lines = {usage_welcome .. self:_get_fullname()} - - local function add(s) - if #lines[#lines]+1+#s <= max_usage_width then - lines[#lines] = lines[#lines] .. " " .. s - else - lines[#lines+1] = (" "):rep(usage_margin) .. s - end - end - - -- Normally options are before positional arguments in usage messages. - -- However, vararg options should be after, because they can't be reliable used - -- before a positional argument. - -- Mutexes come into play, too, and are shown as soon as possible. - -- Overall, output usages in the following order: - -- 1. Mutexes that don't have positional arguments or vararg options. - -- 2. Options that are not in any mutexes and are not vararg. - -- 3. Positional arguments - on their own or as a part of a mutex. - -- 4. Remaining mutexes. - -- 5. Remaining options. - - local elements_in_mutexes = {} - local added_elements = {} - local added_mutexes = {} - local argument_to_mutexes = {} - - local function add_mutex(mutex, main_argument) - if added_mutexes[mutex] then - return - end - - added_mutexes[mutex] = true - local buf = {} - - for _, element in ipairs(mutex) do - if not element._hidden and not added_elements[element] then - if getmetatable(element) == Option or element == main_argument then - table.insert(buf, element:_get_usage()) - added_elements[element] = true - end - end - end - - if #buf == 1 then - add(buf[1]) - elseif #buf > 1 then - add("(" .. table.concat(buf, " | ") .. ")") - end - end - - local function add_element(element) - if not element._hidden and not added_elements[element] then - add(element:_get_usage()) - added_elements[element] = true - end - end - - for _, mutex in ipairs(self._mutexes) do - local is_vararg = false - local has_argument = false - - for _, element in ipairs(mutex) do - if getmetatable(element) == Option then - if element:_is_vararg() then - is_vararg = true - end - else - has_argument = true - argument_to_mutexes[element] = argument_to_mutexes[element] or {} - table.insert(argument_to_mutexes[element], mutex) - end - - elements_in_mutexes[element] = true - end - - if not is_vararg and not has_argument then - add_mutex(mutex) - end - end - - for _, option in ipairs(self._options) do - if not elements_in_mutexes[option] and not option:_is_vararg() then - add_element(option) - end - end - - -- Add usages for positional arguments, together with one mutex containing them, if they are in a mutex. - for _, argument in ipairs(self._arguments) do - -- Pick a mutex as a part of which to show this argument, take the first one that's still available. - local mutex - - if elements_in_mutexes[argument] then - for _, argument_mutex in ipairs(argument_to_mutexes[argument]) do - if not added_mutexes[argument_mutex] then - mutex = argument_mutex - end - end - end - - if mutex then - add_mutex(mutex, argument) - else - add_element(argument) - end - end - - for _, mutex in ipairs(self._mutexes) do - add_mutex(mutex) - end - - for _, option in ipairs(self._options) do - add_element(option) - end - - if #self._commands > 0 then - if self._require_command then - add("") - else - add("[]") - end - - add("...") - end - - return table.concat(lines, "\n") -end - -local function split_lines(s) - if s == "" then - return {} - end - - local lines = {} - - if s:sub(-1) ~= "\n" then - s = s .. "\n" - end - - for line in s:gmatch("([^\n]*)\n") do - table.insert(lines, line) - end - - return lines -end - -local function autowrap_line(line, max_length) - -- Algorithm for splitting lines is simple and greedy. - local result_lines = {} - - -- Preserve original indentation of the line, put this at the beginning of each result line. - -- If the first word looks like a list marker ('*', '+', or '-'), add spaces so that starts - -- of the second and the following lines vertically align with the start of the second word. - local indentation = line:match("^ *") - - if line:find("^ *[%*%+%-]") then - indentation = indentation .. " " .. line:match("^ *[%*%+%-]( *)") - end - - -- Parts of the last line being assembled. - local line_parts = {} - - -- Length of the current line. - local line_length = 0 - - -- Index of the next character to consider. - local index = 1 - - while true do - local word_start, word_finish, word = line:find("([^ ]+)", index) - - if not word_start then - -- Ignore trailing spaces, if any. - break - end - - local preceding_spaces = line:sub(index, word_start - 1) - index = word_finish + 1 - - if (#line_parts == 0) or (line_length + #preceding_spaces + #word <= max_length) then - -- Either this is the very first word or it fits as an addition to the current line, add it. - table.insert(line_parts, preceding_spaces) -- For the very first word this adds the indentation. - table.insert(line_parts, word) - line_length = line_length + #preceding_spaces + #word - else - -- Does not fit, finish current line and put the word into a new one. - table.insert(result_lines, table.concat(line_parts)) - line_parts = {indentation, word} - line_length = #indentation + #word - end - end - - if #line_parts > 0 then - table.insert(result_lines, table.concat(line_parts)) - end - - if #result_lines == 0 then - -- Preserve empty lines. - result_lines[1] = "" - end - - return result_lines -end - --- Automatically wraps lines within given array, --- attempting to limit line length to `max_length`. --- Existing line splits are preserved. -local function autowrap(lines, max_length) - local result_lines = {} - - for _, line in ipairs(lines) do - local autowrapped_lines = autowrap_line(line, max_length) - - for _, autowrapped_line in ipairs(autowrapped_lines) do - table.insert(result_lines, autowrapped_line) - end - end - - return result_lines -end - -function Parser:_get_element_help(element) - local label_lines = element:_get_label_lines() - local description_lines = split_lines(element:_get_description()) - - local result_lines = {} - - -- All label lines should have the same length (except the last one, it has no comma). - -- If too long, start description after all the label lines. - -- Otherwise, combine label and description lines. - - local usage_margin_len = self:_inherit_property("help_usage_margin", 3) - local usage_margin = (" "):rep(usage_margin_len) - local description_margin_len = self:_inherit_property("help_description_margin", 25) - local description_margin = (" "):rep(description_margin_len) - - local help_max_width = self:_inherit_property("help_max_width") - - if help_max_width then - local description_max_width = math.max(help_max_width - description_margin_len, 10) - description_lines = autowrap(description_lines, description_max_width) - end - - if #label_lines[1] >= (description_margin_len - usage_margin_len) then - for _, label_line in ipairs(label_lines) do - table.insert(result_lines, usage_margin .. label_line) - end - - for _, description_line in ipairs(description_lines) do - table.insert(result_lines, description_margin .. description_line) - end - else - for i = 1, math.max(#label_lines, #description_lines) do - local label_line = label_lines[i] - local description_line = description_lines[i] - - local line = "" - - if label_line then - line = usage_margin .. label_line - end - - if description_line and description_line ~= "" then - line = line .. (" "):rep(description_margin_len - #line) .. description_line - end - - table.insert(result_lines, line) - end - end - - return table.concat(result_lines, "\n") -end - -local function get_group_types(group) - local types = {} - - for _, element in ipairs(group) do - types[getmetatable(element)] = true - end - - return types -end - -function Parser:_add_group_help(blocks, added_elements, label, elements) - local buf = {label} - - for _, element in ipairs(elements) do - if not element._hidden and not added_elements[element] then - added_elements[element] = true - table.insert(buf, self:_get_element_help(element)) - end - end - - if #buf > 1 then - table.insert(blocks, table.concat(buf, ("\n"):rep(self:_inherit_property("help_vertical_space", 0) + 1))) - end -end - -function Parser:get_help() - if self._help then - return self._help - end - - local blocks = {self:get_usage()} - - local help_max_width = self:_inherit_property("help_max_width") - - if self._description then - local description = self._description - - if help_max_width then - description = table.concat(autowrap(split_lines(description), help_max_width), "\n") - end - - table.insert(blocks, description) - end - - -- 1. Put groups containing arguments first, then other arguments. - -- 2. Put remaining groups containing options, then other options. - -- 3. Put remaining groups containing commands, then other commands. - -- Assume that an element can't be in several groups. - local groups_by_type = { - [Argument] = {}, - [Option] = {}, - [Command] = {} - } - - for _, group in ipairs(self._groups) do - local group_types = get_group_types(group) - - for _, mt in ipairs({Argument, Option, Command}) do - if group_types[mt] then - table.insert(groups_by_type[mt], group) - break - end - end - end - - local default_groups = { - {name = "Arguments", type = Argument, elements = self._arguments}, - {name = "Options", type = Option, elements = self._options}, - {name = "Commands", type = Command, elements = self._commands} - } - - local added_elements = {} - - for _, default_group in ipairs(default_groups) do - local type_groups = groups_by_type[default_group.type] - - for _, group in ipairs(type_groups) do - self:_add_group_help(blocks, added_elements, group.name .. ":", group) - end - - local default_label = default_group.name .. ":" - - if #type_groups > 0 then - default_label = "Other " .. default_label:gsub("^.", string.lower) - end - - self:_add_group_help(blocks, added_elements, default_label, default_group.elements) - end - - if self._epilog then - local epilog = self._epilog - - if help_max_width then - epilog = table.concat(autowrap(split_lines(epilog), help_max_width), "\n") - end - - table.insert(blocks, epilog) - end - - return table.concat(blocks, "\n\n") -end - -local function get_tip(context, wrong_name) - local context_pool = {} - local possible_name - local possible_names = {} - - for name in pairs(context) do - if type(name) == "string" then - for i = 1, #name do - possible_name = name:sub(1, i - 1) .. name:sub(i + 1) - - if not context_pool[possible_name] then - context_pool[possible_name] = {} - end - - table.insert(context_pool[possible_name], name) - end - end - end - - for i = 1, #wrong_name + 1 do - possible_name = wrong_name:sub(1, i - 1) .. wrong_name:sub(i + 1) - - if context[possible_name] then - possible_names[possible_name] = true - elseif context_pool[possible_name] then - for _, name in ipairs(context_pool[possible_name]) do - possible_names[name] = true - end - end - end - - local first = next(possible_names) - - if first then - if next(possible_names, first) then - local possible_names_arr = {} - - for name in pairs(possible_names) do - table.insert(possible_names_arr, "'" .. name .. "'") - end - - table.sort(possible_names_arr) - return "\nDid you mean one of these: " .. table.concat(possible_names_arr, " ") .. "?" - else - return "\nDid you mean '" .. first .. "'?" - end - else - return "" - end -end - -local ElementState = class({ - invocations = 0 -}) - -function ElementState:__call(state, element) - self.state = state - self.result = state.result - self.element = element - self.target = element._target or element:_get_default_target() - self.action, self.result[self.target] = element:_get_action() - return self -end - -function ElementState:error(fmt, ...) - self.state:error(fmt, ...) -end - -function ElementState:convert(argument, index) - local converter = self.element._convert - - if converter then - local ok, err - - if type(converter) == "function" then - ok, err = converter(argument) - elseif type(converter[index]) == "function" then - ok, err = converter[index](argument) - else - ok = converter[argument] - end - - if ok == nil then - self:error(err and "%s" or "malformed argument '%s'", err or argument) - end - - argument = ok - end - - return argument -end - -function ElementState:default(mode) - return self.element._defmode:find(mode) and self.element._default -end - -local function bound(noun, min, max, is_max) - local res = "" - - if min ~= max then - res = "at " .. (is_max and "most" or "least") .. " " - end - - local number = is_max and max or min - return res .. tostring(number) .. " " .. noun .. (number == 1 and "" or "s") -end - -function ElementState:set_name(alias) - self.name = ("%s '%s'"):format(alias and "option" or "argument", alias or self.element._name) -end - -function ElementState:invoke() - self.open = true - self.overwrite = false - - if self.invocations >= self.element._maxcount then - if self.element._overwrite then - self.overwrite = true - else - local num_times_repr = bound("time", self.element._mincount, self.element._maxcount, true) - self:error("%s must be used %s", self.name, num_times_repr) - end - else - self.invocations = self.invocations + 1 - end - - self.args = {} - - if self.element._maxargs <= 0 then - self:close() - end - - return self.open -end - -function ElementState:pass(argument) - argument = self:convert(argument, #self.args + 1) - table.insert(self.args, argument) - - if #self.args >= self.element._maxargs then - self:close() - end - - return self.open -end - -function ElementState:complete_invocation() - while #self.args < self.element._minargs do - self:pass(self.element._default) - end -end - -function ElementState:close() - if self.open then - self.open = false - - if #self.args < self.element._minargs then - if self:default("a") then - self:complete_invocation() - else - if #self.args == 0 then - if getmetatable(self.element) == Argument then - self:error("missing %s", self.name) - elseif self.element._maxargs == 1 then - self:error("%s requires an argument", self.name) - end - end - - self:error("%s requires %s", self.name, bound("argument", self.element._minargs, self.element._maxargs)) - end - end - - local args - - if self.element._maxargs == 0 then - args = self.args[1] - elseif self.element._maxargs == 1 then - if self.element._minargs == 0 and self.element._mincount ~= self.element._maxcount then - args = self.args - else - args = self.args[1] - end - else - args = self.args - end - - self.action(self.result, self.target, args, self.overwrite) - end -end - -local ParseState = class({ - result = {}, - options = {}, - arguments = {}, - argument_i = 1, - element_to_mutexes = {}, - mutex_to_element_state = {}, - command_actions = {} -}) - -function ParseState:__call(parser, error_handler) - self.parser = parser - self.error_handler = error_handler - self.charset = parser:_update_charset() - self:switch(parser) - return self -end - -function ParseState:error(fmt, ...) - self.error_handler(self.parser, fmt:format(...)) -end - -function ParseState:switch(parser) - self.parser = parser - - if parser._action then - table.insert(self.command_actions, {action = parser._action, name = parser._name}) - end - - for _, option in ipairs(parser._options) do - option = ElementState(self, option) - table.insert(self.options, option) - - for _, alias in ipairs(option.element._aliases) do - self.options[alias] = option - end - end - - for _, mutex in ipairs(parser._mutexes) do - for _, element in ipairs(mutex) do - if not self.element_to_mutexes[element] then - self.element_to_mutexes[element] = {} - end - - table.insert(self.element_to_mutexes[element], mutex) - end - end - - for _, argument in ipairs(parser._arguments) do - argument = ElementState(self, argument) - table.insert(self.arguments, argument) - argument:set_name() - argument:invoke() - end - - self.handle_options = parser._handle_options - self.argument = self.arguments[self.argument_i] - self.commands = parser._commands - - for _, command in ipairs(self.commands) do - for _, alias in ipairs(command._aliases) do - self.commands[alias] = command - end - end -end - -function ParseState:get_option(name) - local option = self.options[name] - - if not option then - self:error("unknown option '%s'%s", name, get_tip(self.options, name)) - else - return option - end -end - -function ParseState:get_command(name) - local command = self.commands[name] - - if not command then - if #self.commands > 0 then - self:error("unknown command '%s'%s", name, get_tip(self.commands, name)) - else - self:error("too many arguments") - end - else - return command - end -end - -function ParseState:check_mutexes(element_state) - if self.element_to_mutexes[element_state.element] then - for _, mutex in ipairs(self.element_to_mutexes[element_state.element]) do - local used_element_state = self.mutex_to_element_state[mutex] - - if used_element_state and used_element_state ~= element_state then - self:error("%s can not be used together with %s", element_state.name, used_element_state.name) - else - self.mutex_to_element_state[mutex] = element_state - end - end - end -end - -function ParseState:invoke(option, name) - self:close() - option:set_name(name) - self:check_mutexes(option, name) - - if option:invoke() then - self.option = option - end -end - -function ParseState:pass(arg) - if self.option then - if not self.option:pass(arg) then - self.option = nil - end - elseif self.argument then - self:check_mutexes(self.argument) - - if not self.argument:pass(arg) then - self.argument_i = self.argument_i + 1 - self.argument = self.arguments[self.argument_i] - end - else - local command = self:get_command(arg) - self.result[command._target or command._name] = true - - if self.parser._command_target then - self.result[self.parser._command_target] = command._name - end - - self:switch(command) - end -end - -function ParseState:close() - if self.option then - self.option:close() - self.option = nil - end -end - -function ParseState:finalize() - self:close() - - for i = self.argument_i, #self.arguments do - local argument = self.arguments[i] - if #argument.args == 0 and argument:default("u") then - argument:complete_invocation() - else - argument:close() - end - end - - if self.parser._require_command and #self.commands > 0 then - self:error("a command is required") - end - - for _, option in ipairs(self.options) do - option.name = option.name or ("option '%s'"):format(option.element._name) - - if option.invocations == 0 then - if option:default("u") then - option:invoke() - option:complete_invocation() - option:close() - end - end - - local mincount = option.element._mincount - - if option.invocations < mincount then - if option:default("a") then - while option.invocations < mincount do - option:invoke() - option:close() - end - elseif option.invocations == 0 then - self:error("missing %s", option.name) - else - self:error("%s must be used %s", option.name, bound("time", mincount, option.element._maxcount)) - end - end - end - - for i = #self.command_actions, 1, -1 do - self.command_actions[i].action(self.result, self.command_actions[i].name) - end -end - -function ParseState:parse(args) - for _, arg in ipairs(args) do - local plain = true - - if self.handle_options then - local first = arg:sub(1, 1) - - if self.charset[first] then - if #arg > 1 then - plain = false - - if arg:sub(2, 2) == first then - if #arg == 2 then - if self.options[arg] then - local option = self:get_option(arg) - self:invoke(option, arg) - else - self:close() - end - - self.handle_options = false - else - local equals = arg:find "=" - if equals then - local name = arg:sub(1, equals - 1) - local option = self:get_option(name) - - if option.element._maxargs <= 0 then - self:error("option '%s' does not take arguments", name) - end - - self:invoke(option, name) - self:pass(arg:sub(equals + 1)) - else - local option = self:get_option(arg) - self:invoke(option, arg) - end - end - else - for i = 2, #arg do - local name = first .. arg:sub(i, i) - local option = self:get_option(name) - self:invoke(option, name) - - if i ~= #arg and option.element._maxargs > 0 then - self:pass(arg:sub(i + 1)) - break - end - end - end - end - end - end - - if plain then - self:pass(arg) - end - end - - self:finalize() - return self.result -end - -function Parser:error(msg) - io.stderr:write(("%s\n\nError: %s\n"):format(self:get_usage(), msg)) - os.exit(1) -end - --- Compatibility with strict.lua and other checkers: -local default_cmdline = rawget(_ENV, "arg") or {} - -function Parser:_parse(args, error_handler) - return ParseState(self, error_handler):parse(args or default_cmdline) -end - -function Parser:parse(args) - return self:_parse(args, self.error) -end - -local function xpcall_error_handler(err) - return tostring(err) .. "\noriginal " .. debug.traceback("", 2):sub(2) -end - -function Parser:pparse(args) - local parse_error - - local ok, result = xpcall(function() - return self:_parse(args, function(_, err) - parse_error = err - error(err, 0) - end) - end, xpcall_error_handler) - - if ok then - return true, result - elseif not parse_error then - error(result, 0) - else - return false, parse_error - end -end - -local argparse = {} - -argparse.version = "0.6.0" - -setmetatable(argparse, {__call = function(_, ...) - return Parser(default_cmdline[0]):add_help(true)(...) -end}) - -return argparse \ No newline at end of file diff --git a/moonscript/etc/fstab b/moonscript/etc/fstab index 539787f..4f45064 100644 --- a/moonscript/etc/fstab +++ b/moonscript/etc/fstab @@ -1,4 +1,5 @@ -packages/moonscript gitfs leafo/moonscript/master/bin -rom/modules/main/moonscript gitfs leafo/moonscript/master/moonscript -rom/modules/main/moon gitfs leafo/moonscript/master/moon -rom/modules/main/argparse.lua linkfs packages/moonscript/argparse.lua +packages/moonscript/repo gitfs leafo/moonscript/master +rom/modules/main/moonscript linkfs packages/moonscript/repo/moonscript +rom/modules/main/moon linkfs packages/moonscript/repo/moon +rom/modules/main/moonutil gitfs natnat-mc/moonutil/master/moonutil +rom/modules/main/argparse urlfs https://raw.githubusercontent.com/mpeterv/argparse/master/src/argparse.lua diff --git a/moonscript/moon b/moonscript/moon new file mode 100644 index 0000000..bd17e3c --- /dev/null +++ b/moonscript/moon @@ -0,0 +1,116 @@ +#!/usr/bin/env lua +local argparse = require("argparse") +local moonscript = require("moonscript.base") +local util = require("moonscript.util") +local errors = require("moonscript.errors") +local unpack = util.unpack +local argparser = argparse()({ + name = "moon" +}) +argparser:argument("script") +argparser:argument("args"):args("*") +argparser:option("-c --coverage", "Collect and print code coverage") +argparser:option("-d", "Disable stack trace rewriting") +argparser:option("-v --version", "Print version information") +local base = 0 +local _list_0 = arg +for _index_0 = 1, #_list_0 do + local flag = _list_0[_index_0] + base = base + 1 + if flag:sub(1, 1) ~= "-" then + break + end +end +local args = { + unpack(arg, 1, base) +} +local opts = argparser:parse(args) +local print_err +print_err = function(...) + local msg = table.concat((function(...) + local _accum_0 = { } + local _len_0 = 1 + local _list_1 = { + ... + } + for _index_0 = 1, #_list_1 do + local v = _list_1[_index_0] + _accum_0[_len_0] = tostring(v) + _len_0 = _len_0 + 1 + end + return _accum_0 + end)(...), "\t") + return io.stderr:write(msg .. "\n") +end +local run +run = function() + if opts.version then + require("moonscript.version").print_version() + os.exit() + end + local script_fname = shell.resolve(opts.script) + args = { + unpack(arg, base + 1) + } + args[-1] = arg[0] + args[0] = opts.script + local moonscript_chunk, lua_parse_error + local passed, err = pcall(function() + moonscript_chunk, lua_parse_error = moonscript.loadfile(script_fname, { + implicitly_return_root = false + }) + end) + if not (passed) then + print_err(err) + os.exit(1) + end + if not (moonscript_chunk) then + if lua_parse_error then + print_err(lua_parse_error) + else + print_err("Can't file file: " .. tostring(script_fname)) + end + os.exit(1) + end + util.getfenv(moonscript_chunk).arg = args + local run_chunk + run_chunk = function() + moonscript.insert_loader() + moonscript_chunk(unpack(args)) + return moonscript.remove_loader() + end + if opts.d then + return run_chunk() + end + local err, trace, cov + if opts.coverage then + print("starting coverage") + local coverage = require("moonscript.cmd.coverage") + cov = coverage.CodeCoverage() + cov:start() + end + xpcall(run_chunk, function(_err) + err = _err + trace = debug.traceback("", 2) + end) + if err then + local truncated = errors.truncate_traceback(util.trim(trace)) + local rewritten = errors.rewrite_traceback(truncated, err) + if rewritten then + print_err(rewritten) + else + print_err(table.concat({ + err, + util.trim(trace) + }, "\n")) + end + return os.exit(1) + else + if cov then + cov:stop() + return cov:print_results() + end + end +end +return run() +-- vim: set filetype=lua: \ No newline at end of file diff --git a/moonscript/moonc b/moonscript/moonc new file mode 100644 index 0000000..9d9c2ef --- /dev/null +++ b/moonscript/moonc @@ -0,0 +1,235 @@ +#!/usr/bin/env lua + +local argparse = require "argparse" +local lfs = require "lfs" + +local parser = argparse() + +parser:flag("-l --lint", "Perform a lint on the file instead of compiling") +parser:flag("-v --version", "Print version") +parser:flag("-w --watch", "Watch file/directory for updates") +parser:option("--transform", "Transform syntax tree with module") +parser:mutex( + parser:option("-t --output-to", "Specify where to place compiled files"), + parser:option("-o", "Write output to file"), + parser:flag("-p", "Write output to standard output"), + parser:flag("-T", "Write parse tree instead of code (to stdout)"), + parser:flag("-b", "Write parse and compile time instead of code(to stdout)"), + parser:flag("-X", "Write line rewrite map instead of code (to stdout)") +) +parser:flag("-", + "Read from standard in, print to standard out (Must be only argument)") + +local read_stdin = arg[1] == "--" -- luacheck: ignore 113 + +if not read_stdin then + parser:argument("file/directory"):args("+") +end + +local opts = parser:parse() + +if opts.version then + local v = require "moonscript.version" + v.print_version() + os.exit() +end + +function log_msg(...) + if not opts.p then + io.stderr:write(table.concat({...}, " ") .. "\n") + end +end + +local moonc = require("moonscript.cmd.moonc") +local util = require "moonscript.util" +local normalize_dir = moonc.normalize_dir +local compile_and_write = moonc.compile_and_write +local path_to_target = moonc.path_to_target + +local function scan_directory(root, collected) + root = normalize_dir(root) + collected = collected or {} + + for fname in lfs.dir(root) do + if not fname:match("^%.") then + local full_path = root..fname + + if lfs.attributes(full_path, "mode") == "directory" then + scan_directory(full_path, collected) + elseif fname:match("%.moon$") then + table.insert(collected, full_path) + end + end + end + + return collected +end + +local function remove_dups(tbl, key_fn) + local hash = {} + local final = {} + + for _, v in ipairs(tbl) do + local dup_key = key_fn and key_fn(v) or v + if not hash[dup_key] then + table.insert(final, v) + hash[dup_key] = true + end + end + + return final +end + +-- creates tuples of input and target +local function get_files(fname, files) + files = files or {} + + if lfs.attributes(fname, "mode") == "directory" then + for _, sub_fname in ipairs(scan_directory(fname)) do + table.insert(files, { + sub_fname, + path_to_target(sub_fname, opts.output_to, fname) + }) + end + else + if fname:sub(1, 1) ~= '/' then + fname = lfs.currentdir() .. '/' .. fname + end + + table.insert(files, { + fname, + path_to_target(fname, opts.output_to) + }) + end + + return files +end + +if read_stdin then + local parse = require "moonscript.parse" + local compile = require "moonscript.compile" + + local text = io.stdin:read("*a") + local tree, err = parse.string(text) + + if not tree then error(err) end + local code, err, pos = compile.tree(tree) + + if not code then + error(compile.format_error(err, pos, text)) + end + + print(code) + os.exit() +end + +local inputs = opts["file/directory"] + +local files = {} +for _, input in ipairs(inputs) do + get_files(input, files) +end + +files = remove_dups(files, function(f) + return f[2] +end) + +-- returns an iterator that returns files that have been updated +local function create_watcher(files) + local watchers = require("moonscript.cmd.watchers") + + if watchers.InotifyWacher:available() then + return watchers.InotifyWacher(files):each_update() + end + + return watchers.SleepWatcher(files):each_update() +end + +if opts.watch then + -- build function to check for lint or compile in watch + local handle_file + if opts.lint then + local lint = require "moonscript.cmd.lint" + handle_file = lint.lint_file + else + handle_file = compile_and_write + end + + local watcher = create_watcher(files) + -- catches interrupt error for ctl-c + local protected = function() + local status, file = true, watcher() + if status then + return file + elseif file ~= "interrupted!" then + error(file) + end + end + + for fname in protected do + local target = path_to_target(fname, opts.t) + + if opts.o then + target = opts.o + end + + local success, err = handle_file(fname, target) + if opts.lint then + if success then + io.stderr:write(success .. "\n\n") + elseif err then + io.stderr:write(fname .. "\n" .. err .. "\n\n") + end + elseif not success then + io.stderr:write(table.concat({ + "", + "Error: " .. fname, + err, + "\n", + }, "\n")) + elseif success == "build" then + log_msg("Built", fname, "->", target) + end + end + + io.stderr:write("\nQuitting...\n") +elseif opts.lint then + local has_linted_with_error; + local lint = require "moonscript.cmd.lint" + for _, tuple in pairs(files) do + local fname = tuple[1] + local res, err = lint.lint_file(fname) + if res then + has_linted_with_error = true + io.stderr:write(res .. "\n\n") + elseif err then + has_linted_with_error = true + io.stderr:write(fname .. "\n" .. err.. "\n\n") + end + end + if has_linted_with_error then + os.exit(1) + end +else + for _, tuple in ipairs(files) do + local fname, target = util.unpack(tuple) + if opts.o then + target = opts.o + end + + local success, err = compile_and_write(fname, target, { + print = opts.p, + fname = fname, + benchmark = opts.b, + show_posmap = opts.X, + show_parse_tree = opts.T, + transform_module = opts.transform + }) + + if not success then + io.stderr:write(fname .. "\t" .. err .. "\n") + os.exit(1) + end + end +end + diff --git a/penlight/etc/fstab b/penlight/etc/fstab deleted file mode 100644 index 2a1632b..0000000 --- a/penlight/etc/fstab +++ /dev/null @@ -1 +0,0 @@ -#rom/modules/main/pl gitfs Tieske/Penlight/master/lua/pl \ No newline at end of file diff --git a/penlight/.package b/pl/.package similarity index 81% rename from penlight/.package rename to pl/.package index 4543cf2..58081b7 100644 --- a/penlight/.package +++ b/pl/.package @@ -1,6 +1,6 @@ { - title = 'Penlight apis', - repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/penlight', + title = 'Penlight', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/pl', description = [[See: https://github.com/Tieske/Penlight Penlight brings together a set of generally useful pure Lua modules, focusing on input data handling (such as reading configuration files), functional programming (such as map, reduce, placeholder expressions, etc), and OS path management. Much of the functionality is inspired by the Python standard libraries.]], diff --git a/pl/apis/compat.lua b/pl/apis/compat.lua new file mode 100644 index 0000000..46b84f4 --- /dev/null +++ b/pl/apis/compat.lua @@ -0,0 +1,188 @@ +---------------- +--- Lua 5.1/5.2/5.3 compatibility. +-- Injects `table.pack`, `table.unpack`, and `package.searchpath` in the global +-- environment, to make sure they are available for Lua 5.1 and LuaJIT. +-- +-- All other functions are exported as usual in the returned module table. +-- +-- NOTE: everything in this module is also available in `pl.utils`. +-- @module pl.compat +local compat = {} + +--- boolean flag this is Lua 5.1 (or LuaJIT). +-- @field lua51 +compat.lua51 = _VERSION == 'Lua 5.1' + +--- boolean flag this is LuaJIT. +-- @field jit +compat.jit = (tostring(assert):match('builtin') ~= nil) + +--- boolean flag this is LuaJIT with 5.2 compatibility compiled in. +-- @field jit52 +if compat.jit then + -- 'goto' is a keyword when 52 compatibility is enabled in LuaJit + compat.jit52 = not loadstring("local goto = 1") +end + +--- the directory separator character for the current platform. +-- @field dir_separator +compat.dir_separator = _ENV.package.config:sub(1,1) + +--- boolean flag this is a Windows platform. +-- @field is_windows +compat.is_windows = compat.dir_separator == '\\' + +--- execute a shell command, in a compatible and platform independent way. +-- This is a compatibility function that returns the same for Lua 5.1 and +-- Lua 5.2+. +-- +-- NOTE: Windows systems can use signed 32bit integer exitcodes. Posix systems +-- only use exitcodes 0-255, anything else is undefined. +-- @param cmd a shell command +-- @return true if successful +-- @return actual return code +function compat.execute(cmd) + local res1,res2,res3 = os.execute(cmd) + if res2 == "No error" and res3 == 0 and compat.is_windows then + -- os.execute bug in Lua 5.2+ not reporting -1 properly on Windows + res3 = -1 + end + if compat.lua51 and not compat.jit52 then + if compat.is_windows then + return res1==0,res1 + else + res1 = res1 > 255 and res1 / 256 or res1 + return res1==0,res1 + end + else + if compat.is_windows then + return res3==0,res3 + else + return not not res1,res3 + end + end +end + +---------------- +-- Load Lua code as a text or binary chunk (in a Lua 5.2 compatible way). +-- @param ld code string or loader +-- @param[opt] source name of chunk for errors +-- @param[opt] mode 'b', 't' or 'bt' +-- @param[opt] env environment to load the chunk in +-- @function compat.load + +--------------- +-- Get environment of a function (in a Lua 5.1 compatible way). +-- Not 100% compatible, so with Lua 5.2 it may return nil for a function with no +-- global references! +-- Based on code by [Sergey Rozhenko](http://lua-users.org/lists/lua-l/2010-06/msg00313.html) +-- @param f a function or a call stack reference +-- @function compat.getfenv + +--------------- +-- Set environment of a function (in a Lua 5.1 compatible way). +-- @param f a function or a call stack reference +-- @param env a table that becomes the new environment of `f` +-- @function compat.setfenv + +if compat.lua51 then -- define Lua 5.2 style load() + if not compat.jit then -- but LuaJIT's load _is_ compatible + local lua51_load = load + function compat.load(str,src,mode,env) + local chunk,err + if type(str) == 'string' then + if str:byte(1) == 27 and not (mode or 'bt'):find 'b' then + return nil,"attempt to load a binary chunk" + end + chunk,err = loadstring(str,src) + else + chunk,err = lua51_load(str,src) + end + if chunk and env then setfenv(chunk,env) end + return chunk,err + end + else + compat.load = load + end + compat.setfenv, compat.getfenv = setfenv, getfenv +else + compat.load = load + -- setfenv/getfenv replacements for Lua 5.2 + -- by Sergey Rozhenko + -- http://lua-users.org/lists/lua-l/2010-06/msg00313.html + -- Roberto Ierusalimschy notes that it is possible for getfenv to return nil + -- in the case of a function with no globals: + -- http://lua-users.org/lists/lua-l/2010-06/msg00315.html + function compat.setfenv(f, t) + f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func) + local name + local up = 0 + repeat + up = up + 1 + name = debug.getupvalue(f, up) + until name == '_ENV' or name == nil + if name then + debug.upvaluejoin(f, up, function() return name end, 1) -- use unique upvalue + debug.setupvalue(f, up, t) + end + if f ~= 0 then return f end + end + + function compat.getfenv(f) + local f = f or 0 + f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func) + local name, val + local up = 0 + repeat + up = up + 1 + name, val = debug.getupvalue(f, up) + until name == '_ENV' or name == nil + return val + end +end + +--- Global exported functions (for Lua 5.1 & LuaJIT) +-- @section lua52 + +--- pack an argument list into a table. +-- @param ... any arguments +-- @return a table with field n set to the length +-- @function table.pack +if not table.pack then + function table.pack (...) -- luacheck: ignore + return {n=select('#',...); ...} + end +end + +--- unpack a table and return the elements. +-- +-- NOTE: this version does NOT honor the n field, and hence it is not nil-safe. +-- See `utils.unpack` for a version that is nil-safe. +-- @param t table to unpack +-- @param[opt] i index from which to start unpacking, defaults to 1 +-- @param[opt] t index of the last element to unpack, defaults to #t +-- @return multiple return values from the table +-- @function table.unpack +-- @see utils.unpack +if not table.unpack then + table.unpack = unpack -- luacheck: ignore +end + +--- return the full path where a Lua module name would be matched. +-- @param mod module name, possibly dotted +-- @param path a path in the same form as package.path or package.cpath +-- @see path.package_path +-- @function package.searchpath +if not package.searchpath then + local sep = package.config:sub(1,1) + function package.searchpath (mod,path) -- luacheck: ignore + mod = mod:gsub('%.',sep) + for m in path:gmatch('[^;]+') do + local nm = m:gsub('?',mod) + local f = io.open(nm,'r') + if f then f:close(); return nm end + end + end +end + +return compat \ No newline at end of file diff --git a/pl/apis/path.lua b/pl/apis/path.lua new file mode 100644 index 0000000..835fdfc --- /dev/null +++ b/pl/apis/path.lua @@ -0,0 +1,445 @@ +--- Path manipulation and file queries. +-- +-- This is modelled after Python's os.path library (10.1); see @{04-paths.md|the Guide}. +-- +-- Dependencies: `pl.utils`, `lfs` +-- @module pl.path + +-- imports and locals +local _G = _G +local sub = string.sub +local getenv = os.getenv +local tmpnam = os.tmpname +local attributes, currentdir, link_attrib +local package = package +local append, concat, remove = table.insert, table.concat, table.remove +local utils = require 'pl.utils' +local assert_string,raise = utils.assert_string,utils.raise + +local attrib +local path = {} + +local lfs = require('lfs') + attributes = lfs.attributes + currentdir = lfs.currentdir + link_attrib = lfs.symlinkattributes + + +attrib = attributes +path.attrib = attrib +path.link_attrib = link_attrib + +--- Lua iterator over the entries of a given directory. +-- Behaves like `lfs.dir` +path.dir = lfs.dir + +--- Creates a directory. +path.mkdir = lfs.mkdir + +--- Removes a directory. +path.rmdir = lfs.rmdir + +---- Get the working directory. +path.currentdir = currentdir + +--- Changes the working directory. +path.chdir = lfs.chdir + + +--- is this a directory? +-- @string P A file path +function path.isdir(P) + assert_string(1,P) + if P:match("\\$") then + P = P:sub(1,-2) + end + return attrib(P,'mode') == 'directory' +end + +--- is this a file?. +-- @string P A file path +function path.isfile(P) + assert_string(1,P) + return attrib(P,'mode') == 'file' +end + +-- is this a symbolic link? +-- @string P A file path +function path.islink(P) + assert_string(1,P) + if link_attrib then + return link_attrib(P,'mode')=='link' + else + return false + end +end + +--- return size of a file. +-- @string P A file path +function path.getsize(P) + assert_string(1,P) + return attrib(P,'size') +end + +--- does a path exist?. +-- @string P A file path +-- @return the file path if it exists, nil otherwise +function path.exists(P) + assert_string(1,P) + return attrib(P,'mode') ~= nil and P +end + +--- Return the time of last access as the number of seconds since the epoch. +-- @string P A file path +function path.getatime(P) + assert_string(1,P) + return attrib(P,'access') +end + +--- Return the time of last modification +-- @string P A file path +function path.getmtime(P) + assert_string(1,P) + return attrib(P,'modification') +end + +---Return the system's ctime. +-- @string P A file path +function path.getctime(P) + assert_string(1,P) + return path.attrib(P,'change') +end + + +local function at(s,i) + return sub(s,i,i) +end + +path.is_windows = utils.is_windows + +local other_sep +-- !constant sep is the directory separator for this platform. +if path.is_windows then + path.sep = '\\'; other_sep = '/' + path.dirsep = ';' +else + path.sep = '/' + path.dirsep = ':' +end +local sep = path.sep + +--- are we running Windows? +-- @class field +-- @name path.is_windows + +--- path separator for this platform. +-- @class field +-- @name path.sep + +--- separator for PATH for this platform +-- @class field +-- @name path.dirsep + +--- given a path, return the directory part and a file part. +-- if there's no directory part, the first value will be empty +-- @string P A file path +function path.splitpath(P) + assert_string(1,P) + local i = #P + local ch = at(P,i) + while i > 0 and ch ~= sep and ch ~= other_sep do + i = i - 1 + ch = at(P,i) + end + if i == 0 then + return '',P + else + return sub(P,1,i-1), sub(P,i+1) + end +end + +--- return an absolute path. +-- @string P A file path +-- @string[opt] pwd optional start path to use (default is current dir) +function path.abspath(P,pwd) + assert_string(1,P) + if pwd then assert_string(2,pwd) end + local use_pwd = pwd ~= nil + if not use_pwd and not currentdir then return P end + P = P:gsub('[\\/]$','') + pwd = pwd or currentdir() + if not path.isabs(P) then + P = path.join(pwd,P) + elseif path.is_windows and not use_pwd and at(P,2) ~= ':' and at(P,2) ~= '\\' then + P = pwd:sub(1,2)..P -- attach current drive to path like '\\fred.txt' + end + return path.normpath(P) +end + +--- given a path, return the root part and the extension part. +-- if there's no extension part, the second value will be empty +-- @string P A file path +-- @treturn string root part +-- @treturn string extension part (maybe empty) +function path.splitext(P) + assert_string(1,P) + local i = #P + local ch = at(P,i) + while i > 0 and ch ~= '.' do + if ch == sep or ch == other_sep then + return P,'' + end + i = i - 1 + ch = at(P,i) + end + if i == 0 then + return P,'' + else + return sub(P,1,i-1),sub(P,i) + end +end + +--- return the directory part of a path +-- @string P A file path +function path.dirname(P) + assert_string(1,P) + local p1 = path.splitpath(P) + return p1 +end + +--- return the file part of a path +-- @string P A file path +function path.basename(P) + assert_string(1,P) + local _,p2 = path.splitpath(P) + return p2 +end + +--- get the extension part of a path. +-- @string P A file path +function path.extension(P) + assert_string(1,P) + local _,p2 = path.splitext(P) + return p2 +end + +--- is this an absolute path?. +-- @string P A file path +function path.isabs(P) + assert_string(1,P) + if path.is_windows then + return at(P,1) == '/' or at(P,1)=='\\' or at(P,2)==':' + else + return at(P,1) == '/' + end +end + +--- return the path resulting from combining the individual paths. +-- if the second (or later) path is absolute, we return the last absolute path (joined with any non-absolute paths following). +-- empty elements (except the last) will be ignored. +-- @string p1 A file path +-- @string p2 A file path +-- @string ... more file paths +function path.join(p1,p2,...) + assert_string(1,p1) + assert_string(2,p2) + if select('#',...) > 0 then + local p = path.join(p1,p2) + local args = {...} + for i = 1,#args do + assert_string(i,args[i]) + p = path.join(p,args[i]) + end + return p + end + if path.isabs(p2) then return p2 end + local endc = at(p1,#p1) + if endc ~= path.sep and endc ~= other_sep and endc ~= "" then + p1 = p1..path.sep + end + return p1..p2 +end + +--- normalize the case of a pathname. On Unix, this returns the path unchanged; +-- for Windows, it converts the path to lowercase, and it also converts forward slashes +-- to backward slashes. +-- @string P A file path +function path.normcase(P) + assert_string(1,P) + if path.is_windows then + return (P:lower():gsub('/','\\')) + else + return P + end +end + +--- normalize a path name. +-- `A//B`, `A/./B`, and `A/foo/../B` all become `A/B`. +-- @string P a file path +function path.normpath(P) + assert_string(1,P) + -- Split path into anchor and relative path. + local anchor = '' + if path.is_windows then + if P:match '^\\\\' then -- UNC + anchor = '\\\\' + P = P:sub(3) + elseif at(P, 1) == '/' or at(P, 1) == '\\' then + anchor = '\\' + P = P:sub(2) + elseif at(P, 2) == ':' then + anchor = P:sub(1, 2) + P = P:sub(3) + if at(P, 1) == '/' or at(P, 1) == '\\' then + anchor = anchor..'\\' + P = P:sub(2) + end + end + P = P:gsub('/','\\') + else + -- According to POSIX, in path start '//' and '/' are distinct, + -- but '///+' is equivalent to '/'. + if P:match '^//' and at(P, 3) ~= '/' then + anchor = '//' + P = P:sub(3) + elseif at(P, 1) == '/' then + anchor = '/' + P = P:match '^/*(.*)$' + end + end + local parts = {} + for part in P:gmatch('[^'..sep..']+') do + if part == '..' then + if #parts ~= 0 and parts[#parts] ~= '..' then + remove(parts) + else + append(parts, part) + end + elseif part ~= '.' then + append(parts, part) + end + end + P = anchor..concat(parts, sep) + if P == '' then P = '.' end + return P +end + +--- relative path from current directory or optional start point +-- @string P a path +-- @string[opt] start optional start point (default current directory) +function path.relpath (P,start) + assert_string(1,P) + if start then assert_string(2,start) end + local split,min,append = utils.split, math.min, table.insert + P = path.abspath(P,start) + start = start or currentdir() + local compare + if path.is_windows then + P = P:gsub("/","\\") + start = start:gsub("/","\\") + compare = function(v) return v:lower() end + else + compare = function(v) return v end + end + local startl, Pl = split(start,sep), split(P,sep) + local n = min(#startl,#Pl) + if path.is_windows and n > 0 and at(Pl[1],2) == ':' and Pl[1] ~= startl[1] then + return P + end + local k = n+1 -- default value if this loop doesn't bail out! + for i = 1,n do + if compare(startl[i]) ~= compare(Pl[i]) then + k = i + break + end + end + local rell = {} + for i = 1, #startl-k+1 do rell[i] = '..' end + if k <= #Pl then + for i = k,#Pl do append(rell,Pl[i]) end + end + return table.concat(rell,sep) +end + + +--- Replace a starting '~' with the user's home directory. +-- In windows, if HOME isn't set, then USERPROFILE is used in preference to +-- HOMEDRIVE HOMEPATH. This is guaranteed to be writeable on all versions of Windows. +-- @string P A file path +function path.expanduser(P) + assert_string(1,P) + if at(P,1) == '~' then + local home = getenv('HOME') + if not home then -- has to be Windows + home = getenv 'USERPROFILE' or (getenv 'HOMEDRIVE' .. getenv 'HOMEPATH') + end + return home..sub(P,2) + else + return P + end +end + + +---Return a suitable full path to a new temporary file name. +-- unlike os.tmpname(), it always gives you a writeable path (uses TEMP environment variable on Windows) +function path.tmpname () + local res = tmpnam() + -- On Windows if Lua is compiled using MSVC14 os.tmpname + -- already returns an absolute path within TEMP env variable directory, + -- no need to prepend it. + if path.is_windows and not res:find(':') then + res = getenv('TEMP')..res + end + return res +end + +--- return the largest common prefix path of two paths. +-- @string path1 a file path +-- @string path2 a file path +function path.common_prefix (path1,path2) + assert_string(1,path1) + assert_string(2,path2) + -- get them in order! + if #path1 > #path2 then path2,path1 = path1,path2 end + local compare + if path.is_windows then + path1 = path1:gsub("/", "\\") + path2 = path2:gsub("/", "\\") + compare = function(v) return v:lower() end + else + compare = function(v) return v end + end + for i = 1,#path1 do + if compare(at(path1,i)) ~= compare(at(path2,i)) then + local cp = path1:sub(1,i-1) + if at(path1,i-1) ~= sep then + cp = path.dirname(cp) + end + return cp + end + end + if at(path2,#path1+1) ~= sep then path1 = path.dirname(path1) end + return path1 + --return '' +end + +--- return the full path where a particular Lua module would be found. +-- Both package.path and package.cpath is searched, so the result may +-- either be a Lua file or a shared library. +-- @string mod name of the module +-- @return on success: path of module, lua or binary +-- @return on error: nil,error string +function path.package_path(mod) + assert_string(1,mod) + local res + mod = mod:gsub('%.',sep) + res = package.searchpath(mod,package.path) + if res then return res,true end + res = package.searchpath(mod,package.cpath) + if res then return res,false end + return raise 'cannot find module on path' +end + + +---- finis ----- +return path \ No newline at end of file diff --git a/pl/etc/fstab b/pl/etc/fstab new file mode 100644 index 0000000..02aa462 --- /dev/null +++ b/pl/etc/fstab @@ -0,0 +1 @@ +packages/pl/apis gitfs Tieske/Penlight/master/lua/pl \ No newline at end of file diff --git a/penlight/init/6.penlight.lua b/pl/init/6.penlight.lua similarity index 100% rename from penlight/init/6.penlight.lua rename to pl/init/6.penlight.lua -- 2.49.1 From fae3b6a65415daaa1608ffff4f5a5e7ba59f43da Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Tue, 9 Jun 2020 18:18:52 -0600 Subject: [PATCH 64/90] package list updated --- packages.list | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages.list b/packages.list index ac02ba3..2fde8e1 100644 --- a/packages.list +++ b/packages.list @@ -1,5 +1,6 @@ { [ 'builder' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/builder/.package', + [ 'busted' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/busted/.package', [ 'cash' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/cash/.package', [ 'ccemux' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/ccemux/.package', [ 'common' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/common/.package', @@ -22,6 +23,7 @@ [ 'monitor' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/monitor/.package', [ 'moonscript' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/moonscript/.package', [ 'neural' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/neural/.package', + [ 'penlight' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/pl/.package', -- [ 'pickup' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/pickup/.package', [ 'recipeBook' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/recipeBook/.package', [ 'screenSaver'] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/screenSaver/.package', -- 2.49.1 From e9f9999f41309c3f0083a3c4c44c444d66cf20e1 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Wed, 10 Jun 2020 19:47:42 -0600 Subject: [PATCH 65/90] better file select for debugger - native support for moonscript --- debugger/debug.lua | 41 ++++++++++++++++++++++++++++++++-- moonscript/autorun/startup.lua | 37 ++++++++++++++++++++++++++++++ moonscript/moon | 2 +- 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 moonscript/autorun/startup.lua diff --git a/debugger/debug.lua b/debugger/debug.lua index 2a3ac39..adf4f24 100644 --- a/debugger/debug.lua +++ b/debugger/debug.lua @@ -214,7 +214,7 @@ local page = UI.Page { breaks = UI.Tab { title = 'Breakpoints', index = 2, - menuBar = UI.MenuBar { + UI.MenuBar { buttons = { { text = 'Toggle', event = 'toggle' }, { text = 'Remove', event = 'remove' }, @@ -274,7 +274,7 @@ local page = UI.Page { }, }, - menuBar = UI.MenuBar { + UI.MenuBar { y = -1, backgroundColor = 'primary', buttonClass = 'InverseButton', @@ -362,6 +362,43 @@ local page = UI.Page { y = '50%', modal = true, enable = function() end, + getFiles = function() + local paths = { } + for _,v in pairs(Util.split(client.env.package.path, '(.-);')) do + v = v:match('(.+)%?') or '' + if v:sub(1, 1) ~= '/' then + v = fs.combine(fs.getDir(filename), v) + end + if fs.exists(v) and fs.isDir(v) then + paths[fs.combine(v, '')] = true + end + end + + local t = { } + for k in pairs(paths) do + local function recurse(dir) + local files = fs.list(dir) + for _,f in ipairs(files) do + local fullName = fs.combine(dir, f) + if fs.isDir(fullName) then + -- skip virtual dirs + if f ~= '.git' and fs.native.isDir(fullName) then + recurse(fullName) + end + elseif fullName:match('(.+)%.lua$') then + table.insert(t, { + name = f, + dir = dir, + lname = f:lower(), + fullName = fullName, + }) + end + end + end + recurse(k) + end + return t + end, show = function(self) UI.QuickSelect.enable(self) self:focusFirst() diff --git a/moonscript/autorun/startup.lua b/moonscript/autorun/startup.lua new file mode 100644 index 0000000..f50e546 --- /dev/null +++ b/moonscript/autorun/startup.lua @@ -0,0 +1,37 @@ +local Map = require('opus.map') + +local fs = _G.fs +local shell = _ENV.shell + +local commands = Map.transpose { + 'packages/moonscript/moon', + 'packages/moonscript/moonc' +} + +local function compatEnv(source) + local env = Map.shallowCopy(source._G) + Map.merge(env, source) + env._G = env + _G.requireInjector(env, 'packages/moon') + return env +end + +shell.registerHandler(function(args, env) + if args[1]:match('(.+)%.moon$') then + return { + title = fs.getName(args[1]):match('([^%.]+)'), + path = 'packages/moonscript/moon', + args = args, + load = loadfile, + env = compatEnv(env), + } + end + local command = shell.resolveProgram(args[1]) or '' + return commands[command] and { + title = fs.getName(command), + path = command, + args = { table.unpack(args, 2) }, + load = loadfile, + env = compatEnv(env), + } +end) diff --git a/moonscript/moon b/moonscript/moon index bd17e3c..f4605ac 100644 --- a/moonscript/moon +++ b/moonscript/moon @@ -48,7 +48,7 @@ run = function() require("moonscript.version").print_version() os.exit() end - local script_fname = shell.resolve(opts.script) + local script_fname = shell.resolveProgram(opts.script) args = { unpack(arg, base + 1) } -- 2.49.1 From 1a166bdb22e18855f74ca163d337fcb68861825e Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Thu, 11 Jun 2020 13:29:02 -0600 Subject: [PATCH 66/90] compability fixes --- busted/.package | 2 +- debugger/debug.lua | 2 +- ignore/pal.lua | 77 ++++++++++++++++++++++++++++++++++ moonscript/autorun/startup.lua | 2 +- pl/.package | 3 ++ 5 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 ignore/pal.lua diff --git a/busted/.package b/busted/.package index 3bfdfce..0a4d3e8 100644 --- a/busted/.package +++ b/busted/.package @@ -1,7 +1,7 @@ { title = 'busted', repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/moonscript', - description = [[]], + description = [[WIP]], license = 'MIT', required = { 'penlight', diff --git a/debugger/debug.lua b/debugger/debug.lua index adf4f24..2ba1d11 100644 --- a/debugger/debug.lua +++ b/debugger/debug.lua @@ -32,7 +32,7 @@ local debugger = kernel.getCurrent() local client local function startClient() - local env = kernel.makeEnv(_ENV) + local env = kernel.makeEnv(_ENV, fs.getDir(filename)) currentFile = nil local clientId = multishell.openTab(nil, { diff --git a/ignore/pal.lua b/ignore/pal.lua new file mode 100644 index 0000000..b47c7dd --- /dev/null +++ b/ignore/pal.lua @@ -0,0 +1,77 @@ +local pals = { + { -- molokai + 0x101010, + 0x960050, + 0x66aa11, + 0xc47f2c, + 0x30309b, + 0x7e40a5, + 0x3579a8, + 0x9999aa, + 0x303030, + 0xff0090, + 0x80ff00, + 0xffba68, + 0x5f5fee, + 0xbb88dd, + 0x4eb4fa, + 0xd0d0d0, + }, + { -- solarized + 0xffffd7, + 0xd75f00, -- orange + 0x585858, + 0x0087ff, -- light blue + 0x1c1c1c, + 0x8a8a8a, + 0xd70000, -- light red + 0x808080, -- gray + 0xe4e4e4, -- light gray + 0x00afaf, -- cyan + 0x626262, + 0x5f5faf, -- blue + 0xaf8700, -- brown + 0x5f8700, -- green + 0xaf005f, -- dark red + 0x262626, -- black + }, + { + 0xf7f7f7, + 0xc4a500, -- mustard + 0xf79aff, -- magenta + 0x8dcff0, -- light blue + 0xfee14d, -- yellow + 0xc4f137, -- lime + 0x207383, -- dark green + 0x7a7a7a, + 0xa1a1a1, + 0x6ad9cf, -- greenish blue + 0xba8acc, -- purple + 0x62a3c4, -- blue gray + 0xd6837c, -- orange/brown + 0x7da900, -- green + 0xb84131, -- redish brown + 0x1b1b1b, + } +} +term.setPaletteColor(2^0,0xFFFFFF) +term.setPaletteColor(2^1,0xFF6300) +term.setPaletteColor(2^2,0xFF00DE) +term.setPaletteColor(2^3,0x00C3FF) +term.setPaletteColor(2^4,0xFFFF00) +term.setPaletteColor(2^5,0x91FF00) +term.setPaletteColor(2^6,0xFF6DA8) +term.setPaletteColor(2^7,0x585757) +term.setPaletteColor(2^8,0xA9A9A9) +term.setPaletteColor(2^9,0x00FFFF) +term.setPaletteColor(2^10,0x7700FF) +term.setPaletteColor(2^11,0x0000FF) +term.setPaletteColor(2^12,0x4C2700) +term.setPaletteColor(2^13,0x00FF00) +term.setPaletteColor(2^14,0xFF0000) +term.setPaletteColor(2^15,0x000000) + +local pal = pals[tonumber(({...})[1])] +for k,v in pairs(pal) do + term.setPaletteColour(2^(k - 1), v) +end diff --git a/moonscript/autorun/startup.lua b/moonscript/autorun/startup.lua index f50e546..0f685cd 100644 --- a/moonscript/autorun/startup.lua +++ b/moonscript/autorun/startup.lua @@ -12,7 +12,7 @@ local function compatEnv(source) local env = Map.shallowCopy(source._G) Map.merge(env, source) env._G = env - _G.requireInjector(env, 'packages/moon') + _G.requireInjector(env, 'packages/moonscript') return env end diff --git a/pl/.package b/pl/.package index 58081b7..e11e194 100644 --- a/pl/.package +++ b/pl/.package @@ -5,4 +5,7 @@ Penlight brings together a set of generally useful pure Lua modules, focusing on input data handling (such as reading configuration files), functional programming (such as map, reduce, placeholder expressions, etc), and OS path management. Much of the functionality is inspired by the Python standard libraries.]], license = 'MIT', + required = { + 'lfs', + }, } -- 2.49.1 From 27c7d2dd18a0eeb320db92bcdabda31a02a41352 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Sat, 13 Jun 2020 12:19:13 -0600 Subject: [PATCH 67/90] moonscript support fixes --- debugger/example.lua | 80 ------------------------- moonscript/README.txt | 7 --- moonscript/T.lua | 99 ------------------------------- moonscript/autorun/startup.lua | 20 ++++--- moonscript/etc/fstab | 4 +- moonscript/{moon => moon.works} | 0 moonscript/{moonc => moonc.works} | 0 packages.list | 2 +- 8 files changed, 17 insertions(+), 195 deletions(-) delete mode 100644 debugger/example.lua delete mode 100644 moonscript/README.txt delete mode 100644 moonscript/T.lua rename moonscript/{moon => moon.works} (100%) rename moonscript/{moonc => moonc.works} (100%) diff --git a/debugger/example.lua b/debugger/example.lua deleted file mode 100644 index 20ce2f8..0000000 --- a/debugger/example.lua +++ /dev/null @@ -1,80 +0,0 @@ ---[[ - -- a very simple debugger implementation - local dbg = require('debugger') - dbg.read = function(snapshot) - print(('%s: %d'):format(snapshot.info.source, snapshot.info.currentline)) - write('> ') - return read() - end - dbg.stopIn(debug.getinfo(1).func) -]] - -local function m2(a) - return a -end - -local function method(times) - local a = 2 - for _ = 1, times do - a = a * a - end - return m2(a) -end - -local Event = require('opus.event') - -Event.on('event1', function() - print('event1') -end) - -Event.on('event2', function() - print('event2') -end) - -Event.onTimeout(10, function() - Event.exitPullEvents() -end) - -local function xx() - os.queueEvent('event1') - os.queueEvent('event2') - - Event.pullEvents() -end -xx() - -local chunk = load([[ - local j = 5 - for i = 1, 5 do - j = j * i - end - --table.insert(j, 5) - return j]], nil, nil, _ENV) - -local j = chunk() -print(j) - -local co = coroutine.create(function(args) - print('in coroutine') - return 'hi' -end) - -local _, t = coroutine.resume(co, 'test') -while coroutine.status(co) ~= 'dead' do - coroutine.resume(co, os.pullEvent()) - --print('alive') -end -print(coroutine.status(co)) - -print(t) - -local i = 2 -print(i) -local res = method(i) - -dofile("rom/modules/main/cc/expect.lua") - -print(res) -print('result: ' .. res) - -table.insert(res, 5) diff --git a/moonscript/README.txt b/moonscript/README.txt deleted file mode 100644 index d0b8bfa..0000000 --- a/moonscript/README.txt +++ /dev/null @@ -1,7 +0,0 @@ -moonscript must be run in compatibility mode: -> compat moon T.moon -> compat moonc T.moon - -moon and moonc were modified to allow relative paths: -> cd /packages/moonscript -> compat moonc T.moon diff --git a/moonscript/T.lua b/moonscript/T.lua deleted file mode 100644 index cd0fa6d..0000000 --- a/moonscript/T.lua +++ /dev/null @@ -1,99 +0,0 @@ -local Event = require('opus.event') -local UI = require('opus.ui') -local kernel -kernel = _G.kernel -local multishell -multishell = _ENV.multishell -local tasks = multishell and multishell.getTabs and multishell.getTabs() or kernel.routines -UI:configure('Tasks', ...) -local page = UI.Page({ - UI.MenuBar({ - buttons = { - { - text = 'Activate', - event = 'activate' - }, - { - text = 'Terminate', - event = 'terminate' - }, - { - text = 'Inspect', - event = 'inspect' - } - } - }), - grid = UI.ScrollingGrid({ - y = 2, - columns = { - { - heading = 'ID', - key = 'uid', - width = 3 - }, - { - heading = 'Title', - key = 'title' - }, - { - heading = 'Status', - key = 'status' - }, - { - heading = 'Time', - key = 'timestamp' - } - }, - values = tasks, - sortColumn = 'uid', - autospace = true, - getDisplayValues = function(self, row) - local elapsed = os.clock() - row.timestamp - return { - uid = row.uid, - title = row.title, - status = row.isDead and 'error' or coroutine.status(row.co), - timestamp = elapsed < 60 and string.format("%ds", math.floor(elapsed)) or string.format("%sm", math.floor(elapsed / 6) / 10) - } - end - }), - accelerators = { - ['control-q'] = 'quit', - [' '] = 'activate', - t = 'terminate' - }, - eventHandler = function(self, event) - local t = self.grid:getSelected() - local _exp_0 = event.type - if 'activate' == _exp_0 or 'grid_select' == _exp_0 then - if t then - return multishell.setFocus(t.uid) - end - elseif 'terminate' == _exp_0 then - if t then - return multishell.terminate(t.uid) - end - elseif 'inspect' == _exp_0 then - if t then - return multishell.openTab(_ENV, { - path = 'sys/apps/Lua.lua', - args = { - t - }, - focused = true - }) - end - elseif 'quit' == _exp_0 then - return UI:quit() - else - return UI.Page.eventHandler(self, event) - end - end -}) -Event.onInterval(1, function() - page.grid:update() - page.grid:draw() - return page:sync() -end) -UI:setPage(page) -return UI:start() diff --git a/moonscript/autorun/startup.lua b/moonscript/autorun/startup.lua index 0f685cd..f3343e4 100644 --- a/moonscript/autorun/startup.lua +++ b/moonscript/autorun/startup.lua @@ -1,7 +1,6 @@ local Map = require('opus.map') local fs = _G.fs -local shell = _ENV.shell local commands = Map.transpose { 'packages/moonscript/moon', @@ -16,21 +15,28 @@ local function compatEnv(source) return env end -shell.registerHandler(function(args, env) - if args[1]:match('(.+)%.moon$') then +local function fix(env, args) + if #args > 0 then + args[1] = env.shell.resolve(args[1]) + end + return args +end + +_ENV.shell.registerHandler(function(env, command, args) + if command:match('(.+)%.moon$') then return { - title = fs.getName(args[1]):match('([^%.]+)'), + title = fs.getName(command):match('([^%.]+)'), path = 'packages/moonscript/moon', - args = args, + args = { env.shell.resolveProgram(command), table.unpack(args) }, load = loadfile, env = compatEnv(env), } end - local command = shell.resolveProgram(args[1]) or '' + command = env.shell.resolveProgram(command) or '' return commands[command] and { title = fs.getName(command), path = command, - args = { table.unpack(args, 2) }, + args = fix(env, args), load = loadfile, env = compatEnv(env), } diff --git a/moonscript/etc/fstab b/moonscript/etc/fstab index 4f45064..2fe25e2 100644 --- a/moonscript/etc/fstab +++ b/moonscript/etc/fstab @@ -1,5 +1,7 @@ packages/moonscript/repo gitfs leafo/moonscript/master +packages/moonscript/moon linkfs packages/moonscript/repo/bin/moon +packages/moonscript/moonc linkfs packages/moonscript/repo/bin/moonc rom/modules/main/moonscript linkfs packages/moonscript/repo/moonscript rom/modules/main/moon linkfs packages/moonscript/repo/moon -rom/modules/main/moonutil gitfs natnat-mc/moonutil/master/moonutil +#rom/modules/main/moonutil gitfs natnat-mc/moonutil/master/moonutil rom/modules/main/argparse urlfs https://raw.githubusercontent.com/mpeterv/argparse/master/src/argparse.lua diff --git a/moonscript/moon b/moonscript/moon.works similarity index 100% rename from moonscript/moon rename to moonscript/moon.works diff --git a/moonscript/moonc b/moonscript/moonc.works similarity index 100% rename from moonscript/moonc rename to moonscript/moonc.works diff --git a/packages.list b/packages.list index 2fde8e1..b738872 100644 --- a/packages.list +++ b/packages.list @@ -23,7 +23,7 @@ [ 'monitor' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/monitor/.package', [ 'moonscript' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/moonscript/.package', [ 'neural' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/neural/.package', - [ 'penlight' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/pl/.package', + [ 'pl' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/pl/.package', -- [ 'pickup' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/pickup/.package', [ 'recipeBook' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/recipeBook/.package', [ 'screenSaver'] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/screenSaver/.package', -- 2.49.1 From cd0e6af55f8d2831619e44e207c59bad1e3d42c6 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Mon, 15 Jun 2020 19:55:56 -0600 Subject: [PATCH 68/90] remove alternates - add ccemux peripherals upon system startup --- cash/.package | 7 ------- ccemux/autorun/startup.lua | 9 --------- ccemux/bin/emustartup.lua | 20 ++++++++++++++++++++ ccemux/bin/emustartup.moon | 16 ++++++++++++++++ ccemux/system/ccemux.lua | 17 +++++++++++++++++ mbs/.package | 11 ----------- monitor/mwm.lua | 3 +-- moonscript/etc/fstab | 3 ++- packages.list | 3 +-- 9 files changed, 57 insertions(+), 32 deletions(-) create mode 100644 ccemux/bin/emustartup.lua create mode 100644 ccemux/bin/emustartup.moon diff --git a/cash/.package b/cash/.package index 93cb0fb..473194e 100644 --- a/cash/.package +++ b/cash/.package @@ -21,11 +21,4 @@ Restorable history Partial CCKernel2 support Full compatibility with CraftOS shell.lua]], license = 'MIT', - install = [[ - require('opus.alternate').set('shell', 'packages/cash/cash.lua') - require('opus.util').writeFile('.cashrc', 'set TERMINATE_QUIT=yes') - ]], - uninstall = [[ - require('opus.alternate').remove('shell', 'packages/cash/cash.lua') - ]], } diff --git a/ccemux/autorun/startup.lua b/ccemux/autorun/startup.lua index cb3d466..dfa1e40 100644 --- a/ccemux/autorun/startup.lua +++ b/ccemux/autorun/startup.lua @@ -1,20 +1,11 @@ local ccemux = _G.ccemux local fs = _G.fs -local peripheral = _G.peripheral local textutils = _G.textutils if ccemux then -- add a System setup tab fs.mount('sys/apps/system/ccemux.lua', 'linkfs', 'packages/ccemux/system/ccemux.lua') - local Config = require('opus.config') - - for k,v in pairs(Config.load('ccemux')) do - if not peripheral.getType(k) then - ccemux.attach(k, v.type, v.args) - end - end - _G.kernel.hook('clipboard_copy', function(_, args) local data = args[1] if type(data) == 'table' then diff --git a/ccemux/bin/emustartup.lua b/ccemux/bin/emustartup.lua new file mode 100644 index 0000000..d4c2874 --- /dev/null +++ b/ccemux/bin/emustartup.lua @@ -0,0 +1,20 @@ +local ccemux +ccemux = _G.ccemux +local fs +fs = _G.fs +local peripheral +peripheral = _G.peripheral +local unserialize +unserialize = _G.textutils.unserialize +local CONFIG = 'usr/config/ccemux' +if ccemux and fs.exists(CONFIG) then + local f = fs.open(CONFIG, 'r') + local c = unserialize(f.readAll()) + f.close() + for k, v in pairs(c) do + if not peripheral.getType(k) then + ccemux.attach(k, v.type, v.args) + print(k) + end + end +end diff --git a/ccemux/bin/emustartup.moon b/ccemux/bin/emustartup.moon new file mode 100644 index 0000000..0e8ebd2 --- /dev/null +++ b/ccemux/bin/emustartup.moon @@ -0,0 +1,16 @@ +import ccemux from _G +import fs from _G +import peripheral from _G +import unserialize from _G.textutils + +CONFIG = 'usr/config/ccemux' + +if ccemux and fs.exists CONFIG + f = fs.open(CONFIG, 'r') + c = unserialize(f.readAll()) + f.close() + + for k,v in pairs c + if not peripheral.getType(k) + ccemux.attach(k, v.type, v.args) + print k diff --git a/ccemux/system/ccemux.lua b/ccemux/system/ccemux.lua index e4360ea..b7fb882 100644 --- a/ccemux/system/ccemux.lua +++ b/ccemux/system/ccemux.lua @@ -1,5 +1,6 @@ local Config = require('opus.config') local UI = require('opus.ui') +local Util = require('opus.util') local ccemux = _G.ccemux @@ -62,7 +63,23 @@ function tab:updatePeripherals(config) self.grid:update() end +function tab.bootCheck() + local startupFile = 'packages/ccemux/bin/emustartup.lua' + + local c = Util.readTable('.startup.boot') + if c then + for _,v in pairs(c.preload) do + if v == startupFile then + return + end + end + end + table.insert(c.preload, startupFile) + Util.writeTable('.startup.boot', c) +end + function tab:enable() + self:bootCheck() local config = Config.load('ccemux') local choices = { } diff --git a/mbs/.package b/mbs/.package index 101cef8..95bbaaf 100644 --- a/mbs/.package +++ b/mbs/.package @@ -8,15 +8,4 @@ https://github.com/SquidDev-CC/mbs MBS is a series of utilities for improving the default CraftOS experience. ]], license = 'MIT', - install = [[ - local Alt = require('opus.alternate') - Alt.set('shell', '.mbs/bin/shell.lua') - Alt.add('lua', '.mbs/bin/lua.lua') - ]], - uninstall = [[ - local Alt = require('opus.alternate') - Alt.remove('shell', '.mbs/bin/shell.lua') - Alt.remove('lua', '.mbs/bin/lua.lua') - fs.delete('.mbs') - ]], } diff --git a/monitor/mwm.lua b/monitor/mwm.lua index fa19aa1..bb88753 100644 --- a/monitor/mwm.lua +++ b/monitor/mwm.lua @@ -1,4 +1,3 @@ -local Alt = require('opus.alternate') local Terminal = require('opus.terminal') local trace = require('opus.trace') local Util = require('opus.util') @@ -496,7 +495,7 @@ local function addShell() process.co = coroutine.create(function() print('To run a program on the monitor, type "fg "') print('To quit, type "exit"') - os.run(shell.makeEnv(_ENV), Alt.get('shell')) + os.run(shell.makeEnv(_ENV), shell.resolveProgram('shell')) multishell.stop() end) diff --git a/moonscript/etc/fstab b/moonscript/etc/fstab index 2fe25e2..d1466b8 100644 --- a/moonscript/etc/fstab +++ b/moonscript/etc/fstab @@ -4,4 +4,5 @@ packages/moonscript/moonc linkfs packages/moonscript/repo/bin/moonc rom/modules/main/moonscript linkfs packages/moonscript/repo/moonscript rom/modules/main/moon linkfs packages/moonscript/repo/moon #rom/modules/main/moonutil gitfs natnat-mc/moonutil/master/moonutil -rom/modules/main/argparse urlfs https://raw.githubusercontent.com/mpeterv/argparse/master/src/argparse.lua +#rom/modules/main/argparse urlfs https://raw.githubusercontent.com/mpeterv/argparse/master/src/argparse.lua +rom/modules/main/argparse urlfs https://raw.githubusercontent.com/luarocks/argparse/master/src/argparse.lua diff --git a/packages.list b/packages.list index b738872..181e646 100644 --- a/packages.list +++ b/packages.list @@ -1,6 +1,6 @@ { [ 'builder' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/builder/.package', - [ 'busted' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/busted/.package', +-- [ 'busted' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/busted/.package', [ 'cash' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/cash/.package', [ 'ccemux' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/ccemux/.package', [ 'common' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/common/.package', @@ -10,7 +10,6 @@ [ 'farms' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/farms/.package', -- [ 'forestry' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/forestry/.package', [ 'games' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/games/.package', --- [ 'glasses' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/glasses/.package', [ 'gps' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/gps/.package', [ 'lfs' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/lfs/.package', [ 'lpeg' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/lpeg/.package', -- 2.49.1 From cb1126e216afd0d4c6d3880ca38003776514b86b Mon Sep 17 00:00:00 2001 From: devomaa Date: Wed, 1 Jul 2020 07:40:31 +0300 Subject: [PATCH 69/90] Duplicate "stdin" (#40) --- shellex/cat.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/shellex/cat.lua b/shellex/cat.lua index 4eefa3b..0010d49 100644 --- a/shellex/cat.lua +++ b/shellex/cat.lua @@ -4,7 +4,10 @@ local fs = require("shellex.filesystem") local args = shell.parse(...) local ec = 0 if #args == 0 then - args = {"-"} + while true do + local input = read() + print(input) + end end for i = 1, #args do @@ -28,4 +31,4 @@ for i = 1, #args do end end -return ec \ No newline at end of file +return ec -- 2.49.1 From d6f61acace30cad78b029e68cdb008754529d500 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Sat, 25 Jul 2020 18:52:13 -0600 Subject: [PATCH 70/90] run lua commands from shell and other tweaks --- common/Events.lua | 27 +++++++---------------- common/autorun/common.lua | 46 +++++++++++++++++++++++++++++++++++++++ common/etc/fstab | 4 +++- debugger/etc/apps.db | 1 + milo/MiloLocal.lua | 4 +++- 5 files changed, 61 insertions(+), 21 deletions(-) diff --git a/common/Events.lua b/common/Events.lua index f2539af..4d0d29c 100644 --- a/common/Events.lua +++ b/common/Events.lua @@ -30,15 +30,8 @@ local page = UI.Page { getDisplayValues = function(_, row) row = Util.shallowCopy(row) - local function tovalue(s) - if type(s) == 'table' then - return 'table' - end - return s - end - for k,v in pairs(row) do - row[k] = tovalue(v) + row[k] = type(v) == 'table' and 'table' or v end return row @@ -95,30 +88,25 @@ local page = UI.Page { end, } -local updated = false local timerId = os.startTimer(1) Event.addRoutine(function() while true do local _, id = os.pullEvent('timer') if id == timerId then - if updated then - while #page.grid.values > 100 do -- page.grid.height do - table.remove(page.grid.values, 100) -- #page.grid.values) - end - updated = false - page.grid:update() - page.grid:draw() - page:sync() + while #page.grid.values > 100 do + table.remove(page.grid.values) end - timerId = os.startTimer(1) + timerId = nil + page.grid:update() + page.grid:draw() + page:sync() end end end) local hookFunction = function(event, e) if not page.filtered[event] and not page.paused and not (event == 'timer' and e[1] == timerId) then - updated = true table.insert(page.grid.values, 1, { event = event, p1 = e[1], @@ -127,6 +115,7 @@ local hookFunction = function(event, e) p4 = e[4], p5 = e[5], }) + timerId = timerId or os.startTimer(.1) end end diff --git a/common/autorun/common.lua b/common/autorun/common.lua index 2c9b53e..0e1cbb1 100644 --- a/common/autorun/common.lua +++ b/common/autorun/common.lua @@ -6,3 +6,49 @@ end _ENV.shell.setCompletionFunction("packages/common/edit.lua", c) _ENV.shell.setCompletionFunction("packages/common/hexedit.lua", c) + +_ENV.shell.registerHandler(function(env, command, args) + if command:match('^!') then + return { + title = 'lua', + path = table.concat({ command:match('^!(.+)'), table.unpack(args) }, ' '), + args = args, + load = function(s) + return function() + local fn, m + local wrapped + + fn = load('return (' ..s.. ')', 'lua', nil, env) + + if fn then + fn = load('return {' ..s.. '}', 'lua', nil, env) + wrapped = true + end + + if fn then + fn, m = pcall(fn) + if #m <= 1 and wrapped then + m = m[1] + end + else + fn, m = load(s, 'lua', nil, env) + if fn then + fn, m = pcall(fn) + end + end + + if fn then + if m or wrapped then + require('opus.util').print(m or 'nil') + else + print() + end + else + _G.printError(m) + end + end + end, + env = env, + } + end +end) diff --git a/common/etc/fstab b/common/etc/fstab index e636640..2b43563 100644 --- a/common/etc/fstab +++ b/common/etc/fstab @@ -3,4 +3,6 @@ packages/common/hexedit.lua urlfs https://pastebin.com/raw/Ds9ajsp4 packages/common/colors.lua urlfs https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/ignore/colors.lua packages/common/cowsay.lua urlfs https://pastebin.com/raw/n00VQJsw packages/common/calc.lua urlfs https://pastebin.com/raw/nAinUn1h -packages/common/write.lua urlfs https://pastebin.com/raw/RSyhCjqv \ No newline at end of file +packages/common/write.lua urlfs https://pastebin.com/raw/RSyhCjqv +# pretty output +rom/modules/main/inpsect.lua urlfs https://raw.githubusercontent.com/kikito/inspect.lua/master/inspect.lua diff --git a/debugger/etc/apps.db b/debugger/etc/apps.db index 7f8a171..186c7ce 100644 --- a/debugger/etc/apps.db +++ b/debugger/etc/apps.db @@ -3,5 +3,6 @@ run = "fileui --exec=debug.lua --title=debug", title = "Debug", category = "Apps", + iconExt = "\30\50\31\50\128\31\102\152\144\30\102\31\50\159\155\30\50\128\10\30\50\31\102\130\152\30\101\31\53\143\143\30\102\31\50\155\30\50\31\102\129\10\30\50\31\102\130\31\50\128\31\101\134\137\31\50\128\31\102\129", }, } diff --git a/milo/MiloLocal.lua b/milo/MiloLocal.lua index 7ba699b..ed1c282 100644 --- a/milo/MiloLocal.lua +++ b/milo/MiloLocal.lua @@ -219,7 +219,9 @@ local s, m = pcall(function() UI:start() end) -turtle.setStatus('idle') +if turtle.setStatus then + turtle.setStatus('idle') +end _G._syslog = oldDebug if not s then error(m) end -- 2.49.1 From 31a5d6c2d081df1901bc1c5844c405e3a154ca94 Mon Sep 17 00:00:00 2001 From: Drew Lemmy Date: Fri, 7 Aug 2020 23:51:16 +0100 Subject: [PATCH 71/90] Update swshop package description (#45) Because the package says 'by Lemmmy', people get confused and think that I made the swshop package, so I keep getting questions about how to operate it etc. This should clear up the confusion. --- swshop/.package | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/swshop/.package b/swshop/.package index afeb083..f3c73fb 100644 --- a/swshop/.package +++ b/swshop/.package @@ -2,8 +2,8 @@ required = { 'milo', }, - title = 'Switchcraft basic shop', + title = 'SwitchCraft basic shop', repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/swshop', - description = 'Modification of the k store by Lemmmy\nRun installPlugin.lua after install', + description = 'Krist-based shop for SwitchCraft\nRun installPlugin.lua after install', license = 'MIT', } -- 2.49.1 From dcdd126bdf447111202a71a1af53b6599a45e3a4 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Fri, 7 Aug 2020 20:21:25 -0400 Subject: [PATCH 72/90] Fix handling of invalid items --- swshop/swshop.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/swshop/swshop.lua b/swshop/swshop.lua index eaf1dc9..0fadd48 100644 --- a/swshop/swshop.lua +++ b/swshop/swshop.lua @@ -48,6 +48,7 @@ local function getItemDetails(item) return key, v end end + return nil, {} end end -- 2.49.1 From 287b3627657a098d9dc749067c9d8f3a2d55aa22 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Fri, 7 Aug 2020 20:50:53 -0400 Subject: [PATCH 73/90] Fix Shoplogs menu not working --- swshop/Shoplogs.lua | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/swshop/Shoplogs.lua b/swshop/Shoplogs.lua index 5a478e3..62185cc 100644 --- a/swshop/Shoplogs.lua +++ b/swshop/Shoplogs.lua @@ -100,22 +100,6 @@ function page.txSlide:show(data) UI.SlideOut.show(self) end -function page.menuBar:eventHandler(event) - if event.type == "purge" then - page.grid.values = {} - page.grid:update() - page.grid:draw() - fs.delete(logFile) - - elseif event.type == "set_timezone" then - page.tzSlide:show() - - else - return UI.MenuBar.eventHandler(self, event) - end - return true -end - function page.txSlide:eventHandler(event) if event.type == 'tx_close' then self:hide() @@ -175,6 +159,15 @@ function page:eventHandler(event) self.grid:update() self.grid:draw() + elseif event.type == "purge" then + self.grid.values = {} + self.grid:update() + self.grid:draw() + fs.delete(logFile) + + elseif event.type == "set_timezone" then + self.tzSlide:show() + else return UI.Page.eventHandler(self, event) end -- 2.49.1 From 153c1ac4a2bcdbafdb6a6b50fd07d0869310803a Mon Sep 17 00:00:00 2001 From: Anavrins Date: Thu, 13 Aug 2020 21:18:49 -0400 Subject: [PATCH 74/90] UI-ification of the ores/xray app --- neural/ores.lua | 307 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 205 insertions(+), 102 deletions(-) diff --git a/neural/ores.lua b/neural/ores.lua index 9bbf2de..c47a89e 100644 --- a/neural/ores.lua +++ b/neural/ores.lua @@ -6,7 +6,12 @@ -- Updated to use new(ish) canvas3d local Config = require('opus.config') -local GPS = require("opus.gps") +local GPS = require('opus.gps') +local UI = require('opus.ui') +local Util = require('opus.util') +local itemDB = require('core.itemDB') +local Event = require('opus.event') +local Angle = require('neural.angle') local keys = _G.keys local os = _G.os @@ -31,111 +36,209 @@ elseif not modules.scan then showRequirements('Scanner module') end --- size of displayed block -local BLOCK_SIZE = .5 - -local function getPoint() - return GPS.locate() -end - -local targets = Config.load('ores', { - ["minecraft:emerald_ore"] = { "minecraft:emerald_ore", 0 }, - ["minecraft:diamond_ore"] = { "minecraft:diamond_ore", 0 }, - ["minecraft:gold_ore"] = { "minecraft:gold_ore", 0 }, - ["minecraft:redstone_ore"] = { "minecraft:redstone_ore", 0 }, - ["minecraft:lit_redstone_ore"] = { "minecraft:redstone_ore", 0 }, - ["minecraft:iron_ore"] = { "minecraft:iron_ore", 0 }, - ["minecraft:lapis_ore"] = { "minecraft:lapis_ore", 0 }, - ["minecraft:coal_ore"] = { "minecraft:coal_ore", 0 }, - ["minecraft:quartz_ore"] = { "minecraft:quartz_ore", 0 }, - ["minecraft:glowstone"] = { "minecraft:glowstone", 0 }, -}) - local projecting = { } -local offset = getPoint() or showRequirements('GPS') +local offset = GPS.locate() or showRequirements('GPS') local canvas = modules.canvas3d().create({ -(offset.x % 1) + .5, -(offset.y % 1) + .5, - -(offset.z % 1) + .5 }) - -local function update() - while true do - -- order matters - local scanned = modules.scan() - local pos = getPoint() - - if pos then - if math.abs(pos.x - offset.x) + - math.abs(pos.y - offset.y) + - math.abs(pos.z - offset.z) > 64 then - for _, b in pairs(projecting) do - b.box.remove() - end - projecting = { } - offset = pos - canvas.recenter({ - -(offset.x % 1) + .5, - -(offset.y % 1) + .5, - -(offset.z % 1) + .5 }) - end - - local blocks = { } - for _, b in pairs(scanned) do - if targets[b.name..(b.metadata ~= 0 and ":"..b.metadata or "")] then - -- track block's world position - b.id = table.concat({ - math.floor(pos.x + b.x), - math.floor(pos.y + b.y), - math.floor(pos.z + b.z) }, ':') - blocks[b.id] = b - end - end - - for _, b in pairs(blocks) do - if not projecting[b.id] then - projecting[b.id] = b - - local target = targets[b.name..(b.metadata ~= 0 and ":"..b.metadata or "")] - - local x = b.x - math.floor(offset.x) + math.floor(pos.x) - local y = b.y - math.floor(offset.y) + math.floor(pos.y) - local z = b.z - math.floor(offset.z) + math.floor(pos.z) - - --[[ - b.box = canvas.addFrame({ x, y, z }) - b.box.setDepthTested(false) - b.box.addItem({ .25, .25 }, target[1], target[2], 2) - --]] - - b.box = canvas.addItem({ x, y, z }, target[1], target[2], BLOCK_SIZE) - b.box.setDepthTested(false) - end - end - - for _, b in pairs(projecting) do - if not blocks[b.id] then - b.box.remove() - projecting[b.id] = nil - end - end - end - - os.sleep(.5) - end -end - -parallel.waitForAny( - function() - print('Ore visualization started') - print('Press enter to exit') - while true do - local e, key = os.pullEventRaw('key') - if key == keys.enter or e == 'terminate' then - break - end - end - end, - update + -(offset.z % 1) + .5 } ) +local page = UI.Page { + notification = UI.Notification {}, + + menuBar = UI.MenuBar { + buttons = { + { text = 'Scan', event = 'scan' }, + { text = 'Size', event = 'size' }, + -- { text = 'Laser', event = 'laser' }, -- If you dare + } + }, + + sizeSlide = UI.MiniSlideOut { + x = -13, + sizeEntry = UI.TextEntry { + shadowText = "Block Size", + accelerators = {enter = 'size_accept'} + } + }, + + grid = UI.CheckboxGrid { + y = 2, + columns = { + { heading = 'Count', key = 'count', width = 6, align = 'right' }, + { heading = 'Name', key = 'displayName' }, + }, + sortColumn = 'displayName', + } + +} + +function page:scan() + local rawBlocks = modules.scan() + self.totals = Util.reduce(rawBlocks, function(acc, b) + if b.name == 'minecraft:air' then return acc end + b.key = table.concat({ b.name, b.metadata }, ':') + local entry = acc[b.key] + if not entry then + b.displayName = itemDB:getName(b.key) + b.count = 1 + acc[b.key] = b + else + entry.count = entry.count + 1 + end + + return acc + end, + Util.reduce(self.targets, function(acc, b) + local key = table.concat({ b[1], b[2] }, ':') + acc[key] = { + displayName = itemDB:getName(key), + name = b[1], + metadata = b[2], + key = key, + count = 0, + checked = true, + } + return acc + end, { }) + ) + self.grid:setValues(self.totals) + self:draw() +end + +function page:shootLaser() + if not modules.fire then self.notification:error("No laser found") return end + self.notification:info("Shooting...") + self:sync() + local targets = Util.filter(modules.scan(), function(b) + return self.targets[table.concat({ b.name, b.metadata }, ':')] + end) + + Util.each(targets, function(b) + local yaw, pitch = Angle.towards(b.x, b.y, b.z) + if pitch < 40 then -- Avoid shooting the block below you + modules.fire(yaw, pitch, 3) + end + end) + self.notification:success("Done!") +end + +function page:loadConfigs() + self.blockSize, self.targets = unpack(Config.load('ores', {.5, {}})) +end + +function page:saveConfigs() + Config.update('ores', {self.blockSize, self.targets}) +end + +function page.grid:getRowTextColor(row, selected) + return row.checked and colors.yellow or UI.CheckboxGrid.getRowTextColor(self, row, selected) +end + +function page:eventHandler(event) + if event.type == "scan" then + self.notification:info("Scanning...") + self:sync() + self:scan() + self.notification:success("Done!") + elseif event.type == "size" then + self.sizeSlide:show() + elseif event.type == "size_accept" then + self.blockSize = tonumber(self.sizeSlide.sizeEntry.value) or self.blockSize + self.blockSize = math.max(self.blockSize, 0) + self.sizeSlide.sizeEntry:reset() + self.sizeSlide:hide() + + elseif event.type == "laser" then + self:shootLaser() + + elseif event.type == "grid_select" then + local block = event.selected + local key = table.concat({ block.name, block.metadata }, ':') + if block.checked then + self.targets[key] = {block.name, block.metadata} + else + self.targets[key] = nil + end + page:saveConfigs() + else return UI.Page.eventHandler(self, event) + end + return true +end + +page:loadConfigs() +page:scan() + +Event.addRoutine( + function() + while true do + -- order matters + local scanned = modules.scan() + local pos = GPS.locate() + + if pos then + if math.abs(pos.x - offset.x) + + math.abs(pos.y - offset.y) + + math.abs(pos.z - offset.z) > 64 then + for _, b in pairs(projecting) do + b.box.remove() + end + projecting = { } + offset = pos + canvas.recenter({ + -(offset.x % 1) + .5, + -(offset.y % 1) + .5, + -(offset.z % 1) + .5 }) + end + + local blocks = { } + for _, b in pairs(scanned) do + if page.targets[table.concat({b.name, b.metadata or 0}, ":")] then + -- track block's world position + b.id = table.concat({ + math.floor(pos.x + b.x), + math.floor(pos.y + b.y), + math.floor(pos.z + b.z) }, ':') + blocks[b.id] = b + end + end + + for _, b in pairs(blocks) do + if not projecting[b.id] then + projecting[b.id] = b + + local target = page.targets[table.concat({b.name, b.metadata or 0}, ":")] + + local x = b.x - math.floor(offset.x) + math.floor(pos.x) + local y = b.y - math.floor(offset.y) + math.floor(pos.y) + local z = b.z - math.floor(offset.z) + math.floor(pos.z) + + --[[ + b.box = canvas.addFrame({ x, y, z }) + b.box.setDepthTested(false) + b.box.addItem({ .25, .25 }, target[1], target[2], 2) + --]] + + b.box = canvas.addItem({ x, y, z }, target[1], target[2], page.blockSize) + b.box.setDepthTested(false) + end + end + + for _, b in pairs(projecting) do + if not blocks[b.id] then + b.box.remove() + projecting[b.id] = nil + end + end + end + + os.sleep(.5) + end + end +) + +UI:setPage(page) +UI:start() + canvas.clear() -- 2.49.1 From db007af3eac36ddc60983d0de9af87ecaed10a17 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Fri, 14 Aug 2020 01:51:45 -0400 Subject: [PATCH 75/90] Add config path args to MiloRemote Allowing to connect to multiple Milo system from one NI Example: > MiloRemote base > MiloRemote shop --- milo/MiloRemote.lua | 14 +++++++++----- milo/plugins/remote/setup.lua | 6 +++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/milo/MiloRemote.lua b/milo/MiloRemote.lua index fda2876..0e702db 100644 --- a/milo/MiloRemote.lua +++ b/milo/MiloRemote.lua @@ -12,8 +12,12 @@ local fs = _G.fs local peripheral = _G.peripheral local shell = _ENV.shell +local configName = ({...})[1] +local configPath = 'miloRemote' .. (configName and "_"..configName or "") + local context = { - state = Config.load('miloRemote', { displayMode = 0, deposit = true }), + state = Config.load(configPath, { displayMode = 0, deposit = true }), + configPath = configPath, responseHandlers = { }, } @@ -158,7 +162,7 @@ function page.grid:eventHandler(event) if event.type == 'grid_sort' then context.state.sortColumn = event.sortColumn context.state.inverseSort = event.inverseSort - Config.update('miloRemote', context.state) + Config.update(configPath, context.state) end return UI.Grid.eventHandler(self, event) end @@ -181,7 +185,7 @@ function page:eventHandler(event) self.statusBar:draw() context:setStatus(depositMode[context.state.deposit].help) context:notifyInfo(depositMode[context.state.deposit].help) - Config.update('miloRemote', context.state) + Config.update(configPath, context.state) elseif event.type == 'focus_change' then context:setStatus(event.focused.help) @@ -243,7 +247,7 @@ function page:eventHandler(event) context:setStatus(event.button.help) context:notifyInfo(event.button.help) self.grid:draw() - Config.update('miloRemote', context.state) + Config.update(configPath, context.state) elseif event.type == 'text_change' and event.element == self.statusBar.filter then self.filter = event.text or '' @@ -445,7 +449,7 @@ end function context:setState(key, value) self.state[key] = value - Config.update('miloRemote', self.state) + Config.update(configPath, self.state) end context.responseHandlers['received'] = function(response) diff --git a/milo/plugins/remote/setup.lua b/milo/plugins/remote/setup.lua index 856278d..2c266bf 100644 --- a/milo/plugins/remote/setup.lua +++ b/milo/plugins/remote/setup.lua @@ -5,10 +5,10 @@ local Util = require('opus.util') local colors = _G.colors local fs = _G.fs -local STARTUP_FILE = 'usr/autorun/miloRemote.lua' - local context = ({ ... })[1] +local STARTUP_FILE = 'usr/autorun/'..context.configPath..'.lua' + local setup = UI.SlideOut { titleBar = UI.TitleBar { title = 'Remote Setup', @@ -59,7 +59,7 @@ function setup:eventHandler(event) self.statusBar:setStatus(event.focused.help) elseif event.type == 'form_complete' then - Config.update('miloRemote', context.state) + Config.update(context.configPath, context.state) self:hide() context.page:refresh('list') context.page.grid:draw() -- 2.49.1 From 86db60ba9762d291d133f2c15dd271a741f29d17 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Sat, 15 Aug 2020 02:28:54 -0400 Subject: [PATCH 76/90] Fix Ores crashing near water Fixes https://github.com/kepler155c/opus/issues/45 --- neural/ores.lua | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/neural/ores.lua b/neural/ores.lua index c47a89e..fe31b0b 100644 --- a/neural/ores.lua +++ b/neural/ores.lua @@ -44,15 +44,20 @@ local canvas = modules.canvas3d().create({ -(offset.z % 1) + .5 } ) +local menuBarButtons = { + { text = 'Scan', event = 'scan' }, + { text = 'Size', event = 'size' }, +} + +if modules.fire then + table.insert(menuBarButtons, { text = 'Laser', event = 'laser' }) +end + local page = UI.Page { notification = UI.Notification {}, menuBar = UI.MenuBar { - buttons = { - { text = 'Scan', event = 'scan' }, - { text = 'Size', event = 'size' }, - -- { text = 'Laser', event = 'laser' }, -- If you dare - } + buttons = menuBarButtons }, sizeSlide = UI.MiniSlideOut { @@ -220,8 +225,21 @@ Event.addRoutine( b.box.addItem({ .25, .25 }, target[1], target[2], 2) --]] - b.box = canvas.addItem({ x, y, z }, target[1], target[2], page.blockSize) - b.box.setDepthTested(false) + pcall(function() + b.box = canvas.addItem({ + pos.x - offset.x + b.x + -(pos.x % 1) + .5, + pos.y - offset.y + b.y + -(pos.y % 1) + .5, + pos.z - offset.z + b.z + -(pos.z % 1) + .5 }, + target[1], target[2], page.blockSize) + end) + if not b.box then + b.box = canvas.addBox( + pos.x - offset.x + b.x + -(pos.x % 1) + .5, + pos.y - offset.y + b.y + -(pos.y % 1) + .5, + pos.z - offset.z + b.z + -(pos.z % 1) + .5, + .5, .5, .5, 0xFFFFFF7F) + end + b.box.setDepthTested(false) end end -- 2.49.1 From 7f880b1563587aa813d61ed24db9fbb4e777f0eb Mon Sep 17 00:00:00 2001 From: Anavrins Date: Thu, 20 Aug 2020 03:23:59 -0400 Subject: [PATCH 77/90] Typos and broken links fix --- common/etc/fstab | 4 ++-- screenSaver/etc/fstab | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/etc/fstab b/common/etc/fstab index 2b43563..0ff0a83 100644 --- a/common/etc/fstab +++ b/common/etc/fstab @@ -1,8 +1,8 @@ -packages/common/ascii.lua urlfs http://pastebin.com/raw/u3kcnyjd +packages/common/ascii.lua urlfs https://pastebin.com/raw/U3KcNyJd packages/common/hexedit.lua urlfs https://pastebin.com/raw/Ds9ajsp4 packages/common/colors.lua urlfs https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/ignore/colors.lua packages/common/cowsay.lua urlfs https://pastebin.com/raw/n00VQJsw packages/common/calc.lua urlfs https://pastebin.com/raw/nAinUn1h packages/common/write.lua urlfs https://pastebin.com/raw/RSyhCjqv # pretty output -rom/modules/main/inpsect.lua urlfs https://raw.githubusercontent.com/kikito/inspect.lua/master/inspect.lua +rom/modules/main/inspect.lua urlfs https://raw.githubusercontent.com/kikito/inspect.lua/master/inspect.lua diff --git a/screenSaver/etc/fstab b/screenSaver/etc/fstab index 31905fb..99599c9 100644 --- a/screenSaver/etc/fstab +++ b/screenSaver/etc/fstab @@ -2,7 +2,7 @@ sys/apps/system/saver.lua linkfs packages/screenSaver/system/saver.lua packages/screenSaver/savers/timespace.lua urlfs https://raw.githubusercontent.com/Allen2277/Computercraft/master/Time%20Space%20Screensaver packages/screenSaver/savers/visualizer.lua urlfs https://raw.githubusercontent.com/Allen2277/Computercraft/master/ScreenSaver packages/screenSaver/savers/random.lua urlfs https://pastebin.com/raw/XXW0r5zt -packages/screenSaver/savers/melting.lua urlfs http://pastebin.com/raw/raUv6Pap +packages/screenSaver/savers/melting.lua urlfs https://pastebin.com/raw/raUv6Pap packages/screenSaver/savers/bubbles.lua urlfs https://pastebin.com/raw/JCR8YTww #packages/screenSaver/savers/fire.lua urlfs https://pastebin.com/raw/4CY4AYj3 packages/screenSaver/savers/rain.lua urlfs https://pastebin.com/raw/P86Hm99N @@ -11,4 +11,4 @@ packages/screenSaver/savers/fireworks.lua urlfs https://pastebin.com/raw/Yn5sWt3 packages/screenSaver/savers/starfield.lua urlfs https://pastebin.com/raw/AQm9R6nT #packages/screenSaver/savers/nyan.lua urlfs https://pastebin.com/raw/YJnT6Adu packages/screenSaver/savers/bounce.lua urlfs https://pastebin.com/raw/WLrfdyNy -packages/screenSaver/savers/antfarm.lua urlfs https://pastebin.com/raw/h9x3h7aw \ No newline at end of file +packages/screenSaver/savers/antfarm.lua urlfs https://pastebin.com/raw/h9x3h7aw -- 2.49.1 From 64a218810a19d8e81c680a51ed20e3a6691896fa Mon Sep 17 00:00:00 2001 From: Anavrins Date: Sat, 22 Aug 2020 20:18:07 -0400 Subject: [PATCH 78/90] Fix GPS server Was sending 4 replies per nodes and caused resolving to fail --- gps/gpsServer.lua | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/gps/gpsServer.lua b/gps/gpsServer.lua index d66b575..6d29d73 100644 --- a/gps/gpsServer.lua +++ b/gps/gpsServer.lua @@ -250,16 +250,15 @@ local function server(mode) end Event.on('modem_message', function(_, side, channel, computerId, message, distance) - if distance and modems[side] then + local modem = modems[side] + if distance and modem then if mode == 'gps' and channel == GPS.CHANNEL_GPS and message == "PING" then - for _, modem in pairs(modems) do - modem.transmit(computerId, GPS.CHANNEL_GPS, { modem.x, modem.y, modem.z }) - end - getPosition(computerId, modems[side], distance) + modem.transmit(computerId, GPS.CHANNEL_GPS, { modem.x, modem.y, modem.z }) + getPosition(computerId, modem, distance) end if mode == 'snmp' and channel == 999 then - getPosition(computerId, modems[side], distance, message) + getPosition(computerId, modem, distance, message) end end end) -- 2.49.1 From 88cacb482338d348d9c2eb7425a5ba496204e27a Mon Sep 17 00:00:00 2001 From: Anavrins Date: Mon, 24 Aug 2020 23:11:25 -0400 Subject: [PATCH 79/90] More customization on canvasServer --- neural/canvasServer.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/neural/canvasServer.lua b/neural/canvasServer.lua index 556dffe..6eeae18 100644 --- a/neural/canvasServer.lua +++ b/neural/canvasServer.lua @@ -43,14 +43,14 @@ local function update(scanned) -- items are centered at the mid-point of the cube -- boxes are aligned to the top corner - sigh if b.path then - box = canvas.addBox(x + .4, y + .4, z + .4, .2, .2, .2, 0xa0902080) + box = canvas.addBox(x + .4, y + .4, z + .4, .2, .2, .2, b.color or 0xa0902080) elseif b.name then pcall(function() - box = canvas.addItem({ x + .5, y + .5, z + .5 }, b.name, b.damage or b.metadata, .5) + box = canvas.addItem({ x + .5, y + .5, z + .5 }, b.name, b.damage or b.metadata, b.size or .5) end) end if not box then - box = canvas.addBox(x, y, z, 1, 1, 1, 0x8080ff30) + box = canvas.addBox(x, y, z, 1, 1, 1, b.color or 0x8080ff30) end if box then box.setDepthTested(false) -- 2.49.1 From c7a347f723782b110f0be98976eb44b53089299f Mon Sep 17 00:00:00 2001 From: Anavrins Date: Sun, 20 Sep 2020 23:08:18 -0400 Subject: [PATCH 80/90] Cleanup --- miloApps/apps/furniController.lua | 104 +++++++++++++++--------------- 1 file changed, 51 insertions(+), 53 deletions(-) diff --git a/miloApps/apps/furniController.lua b/miloApps/apps/furniController.lua index d6ad9ec..12fab79 100644 --- a/miloApps/apps/furniController.lua +++ b/miloApps/apps/furniController.lua @@ -17,158 +17,156 @@ -- 13-16: Furnace Outputs for 2 to 4 furni.lua arrays -- Only user option, slot 4, what to use it for? -local useSlot4=nil + +local useSlot4 = nil -- Begin main program code -- Find modem with -only- turtles attached (these should be our furni/furniController arrays) -local devModem="" -for nDev,modem in ipairs({peripheral.find("modem")}) do +local devModem = "" +for _, modem in ipairs({peripheral.find("modem")}) do if #modem.getNamesRemote() > 1 then -- We're only looking for 2 or more devices on a network segment, ignore anything less - nTurtles=0 - for nDev,dev in ipairs(modem.getNamesRemote()) do - if modem.getTypeRemote(dev) == "turtle" then nTurtles=nTurtles+1 end + local nTurtles = 0 + for _, dev in ipairs(modem.getNamesRemote()) do + if modem.getTypeRemote(dev) == "turtle" then nTurtles = nTurtles + 1 end end - if nTurtles==#modem.getNamesRemote() then - devModem=modem + if nTurtles == #modem.getNamesRemote() then + devModem = modem break end end end + if devModem ~= "" then - print("Found turtle-only modem, detected",#devModem.getNamesRemote(),"turtles",(#devModem.getNamesRemote() > 4 and "(Max 4 will be mapped)" or "(All will be mapped)")) + print("Found turtle-only modem, detected", #devModem.getNamesRemote(), "turtles", (#devModem.getNamesRemote() > 4 and "(Max 4 will be mapped)" or "(All will be mapped)")) else print("Did not find a modem with only turtles connected. Please check your networking cables/modems and devices, then try again.") return -- We don't have a modem, halt end -local devTurtles={} -for nDev,dev in ipairs(devModem.getNamesRemote()) do + +local devTurtles = {} +for _, dev in ipairs(devModem.getNamesRemote()) do if #devTurtles >= 4 then break end - devTurtles[#devTurtles+1]=peripheral.wrap(dev) - print("Mapping",devTurtles[#devTurtles].getLabel(),"("..dev..")","as device",#devTurtles) - devTurtles[#devTurtles].inputSlot=#devTurtles+4 -- Set the input for this device on second row - devTurtles[#devTurtles].fuelSlot=#devTurtles+8 -- Set the fuel for this device on third row - devTurtles[#devTurtles].outputSlot=#devTurtles+12 -- Set the output for this device on fourth row + devTurtles[#devTurtles+1] = peripheral.wrap(dev) + print("Mapping", devTurtles[#devTurtles].getLabel(), "("..dev..")", "as device", #devTurtles) + devTurtles[#devTurtles].inputSlot = #devTurtles + 4 -- Set the input for this device on second row + devTurtles[#devTurtles].fuelSlot = #devTurtles + 8 -- Set the fuel for this device on third row + devTurtles[#devTurtles].outputSlot = #devTurtles + 12 -- Set the output for this device on fourth row if devTurtles[#devTurtles].isOn() then - print("Resynchronizing",devTurtles[#devTurtles].getLabel()) + print("Resynchronizing", devTurtles[#devTurtles].getLabel()) devTurtles[#devTurtles].reboot() else - print("Powering",devTurtles[#devTurtles].getLabel(),"on") + print("Powering", devTurtles[#devTurtles].getLabel(), "on") devTurtles[#devTurtles].turnOn() end end -local turtle.list = function () -- Supplement a local .list()-like function - local tList={} - for i=1,16 do - local slotData=turtle.getItemDetail(i) -- DWGFJTLR +local function listInventory() -- Supplement a local .list()-like function + local tList = {} + for i = 1, 16 do + local slotData = turtle.getItemDetail(i) -- DWGFJTLR if slotData ~= nil then slotData.maxStack=turtle.getItemCount(i) + turtle.getItemSpace(i) - tList[i]=slotData + tList[i] = slotData end end return tList end -function manageLocalResources() +local function manageLocalResources() --print("Input found") - local itemCount=turtle.getItemCount(1)/#devTurtles + local itemCount = turtle.getItemCount(1) / #devTurtles if itemCount < 1 then itemCount = 1 end -- Patch for single items fed into master slot if turtle.getItemCount(1) > 0 then -- Input - for nDev,dev in ipairs(devTurtles) do - local devList=dev.list() + for _, dev in ipairs(devTurtles) do if turtle.select(1) and ((turtle.compareTo(dev.inputSlot) and turtle.getItemSpace(dev.inputSlot) > 0) or turtle.getItemCount(dev.inputSlot) == 0) then -- Move Input - turtle.transferTo(dev.inputSlot,itemCount) + turtle.transferTo(dev.inputSlot, itemCount) end if useSlot4 == "input" and turtle.select(4) and ((turtle.compareTo(dev.inputSlot) and turtle.getItemSpace(dev.inputSlot) > 0) or turtle.getItemCount(dev.inputSlot) == 0) then -- Move Input - turtle.transferTo(dev.inputSlot,itemCount) + turtle.transferTo(dev.inputSlot, itemCount) end --Slot 4 input end end if turtle.select(2) and turtle.getSelectedSlot() == 2 then -- Fuel - for nDev,dev in ipairs(devTurtles) do - local devList=dev.list() + for _, dev in ipairs(devTurtles) do if turtle.compareTo(dev.fuelSlot) or turtle.getItemCount(dev.fuelSlot) == 0 then turtle.transferTo(dev.fuelSlot,64) end -- Move fuel into slot end end if useSlot4 == "fuel" and turtle.select(4) and turtle.getSelectedSlot() == 4 then -- Fuel - for nDev,dev in ipairs(devTurtles) do - local devList=dev.list() + for _, dev in ipairs(devTurtles) do if turtle.compareTo(dev.fuelSlot) or turtle.getItemCount(dev.fuelSlot) == 0 then turtle.transferTo(dev.fuelSlot,64) end -- Move fuel into slot end end -- Slot 4 fuel - for nDev,dev in ipairs(devTurtles) do - local devList=dev.list() + for _, dev in ipairs(devTurtles) do if turtle.getItemCount(3) == 0 and turtle.getItemCount(dev.outputSlot) > 0 then turtle.select(dev.outputSlot) - turtle.transferTo(3,64) + turtle.transferTo(3, 64) elseif turtle.getItemCount(dev.outputSlot) > 0 and turtle.select(3) and turtle.compareTo(dev.outputSlot) then -- Move output --turtle.setStatus("moving") turtle.select(dev.outputSlot) - turtle.transferTo(3,64) + turtle.transferTo(3, 64) end if useSlot4 == "output" then -- Slot 4 output if turtle.getItemCount(4) == 0 and turtle.getItemCount(dev.outputSlot) > 0 then turtle.select(dev.outputSlot) - turtle.transferTo(4,64) + turtle.transferTo(4, 64) elseif turtle.getItemCount(dev.outputSlot) > 0 and turtle.select(4) and turtle.compareTo(dev.outputSlot) then turtle.select(dev.outputSlot) - turtle.transferTo(4,64) + turtle.transferTo(4, 64) end end end end -function manageRemoteResources() - local localList=turtle.list() - for nDev,dev in ipairs(devTurtles) do - local devList=dev.list() +local function manageRemoteResources() + local localList = listInventory() + for _, dev in ipairs(devTurtles) do + local devList = dev.list() --print(textutils.serialize(devList)) if devList[2] == nil then if localList[dev.fuelSlot] ~= nil then turtle.setStatus("fueling") - dev.pullItems(devModem.getNameLocal(),dev.fuelSlot,64,2) + dev.pullItems(devModem.getNameLocal(), dev.fuelSlot, 64, 2) end else if localList[dev.fuelSlot] ~= nil and localList[dev.fuelSlot].name == devList[2].name then turtle.setStatus("fueling") - dev.pullItems(devModem.getNameLocal(),dev.fuelSlot,64,2) + dev.pullItems(devModem.getNameLocal(), dev.fuelSlot, 64, 2) end end if devList[1] == nil then if localList[dev.inputSlot] ~= nil then turtle.setStatus("pushing") - dev.pullItems(devModem.getNameLocal(),dev.inputSlot,64,1) + dev.pullItems(devModem.getNameLocal(), dev.inputSlot, 64, 1) end else if localList[dev.inputSlot] ~= nil and localList[dev.inputSlot].name == devList[1].name then turtle.setStatus("pushing") - dev.pullItems(devModem.getNameLocal(),dev.inputSlot,64,1) + dev.pullItems(devModem.getNameLocal(), dev.inputSlot, 64, 1) end end -- Move input if devList[3] ~= nil then if localList[dev.outputSlot] == nil then turtle.setStatus("pulling") - dev.pushItems(devModem.getNameLocal(),3,64,dev.outputSlot) + dev.pushItems(devModem.getNameLocal(), 3, 64, dev.outputSlot) end else if localList[dev.outputSlot] ~= nil then if devList[3] ~= nil and localList[dev.outputSlot].name == devList[3].name then turtle.setStatus("pulling") - dev.pushItems(devModem.getNameLocal(),3,64,dev.outputSlot) + dev.pushItems(devModem.getNameLocal(), 3, 64, dev.outputSlot) end end end end end -local lastList={} while true do -- Main Loop - local turtleList=turtle.list() - if turtleList[1]~=nil or turtleList[13]~=nil or turtleList[14]~=nil or turtleList[15]~=nil or turtleList[16]~=nil or ((useSlot4=="input" or useSlot4=="fuel") and turtleList[4]~=nil) then + local turtleList = listInventory() + if turtleList[1] ~= nil or turtleList[13] ~= nil or turtleList[14] ~= nil or turtleList[15] ~= nil or turtleList[16] ~= nil or ((useSlot4 == "input" or useSlot4 == "fuel") and turtleList[4] ~= nil) then manageLocalResources() end manageRemoteResources() turtle.setStatus("sleeping") - os.sleep(1) + sleep(1) end -- 2.49.1 From 13601c97a9b730adfc9d0ff0b9dd6993e83ba1c8 Mon Sep 17 00:00:00 2001 From: Drew Lemmy Date: Tue, 29 Sep 2020 18:51:35 +0100 Subject: [PATCH 81/90] fix: too many milo parallel tasks hurting server (#46) --- milo/apis/storage.lua | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/milo/apis/storage.lua b/milo/apis/storage.lua index c8d6642..fb086ea 100644 --- a/milo/apis/storage.lua +++ b/milo/apis/storage.lua @@ -9,6 +9,8 @@ local device = _G.device local os = _G.os local parallel = _G.parallel +local SCAN_CHUNK_SIZE = 16 + local Storage = class() local function loadOld(storage) @@ -263,7 +265,18 @@ function Storage:listItems(throttle) end if #t > 0 then - parallel.waitForAll(table.unpack(t)) + local chunk = {} + + -- Split the work into chunks to avoid spamming too many coroutines. + for i = 1, #t do + table.insert(chunk, t[i]) + + -- When we reach the chunk limit, execute them and start a new chunk + if #chunk >= SCAN_CHUNK_SIZE then + parallel.waitForAll(table.unpack(chunk)) + chunk = {} + end + end end for _, adapter in self:onlineAdapters() do -- 2.49.1 From 5e275e36adfb41788565fa7747a2cbdc371125ba Mon Sep 17 00:00:00 2001 From: Drew Lemmy Date: Tue, 29 Sep 2020 22:18:57 +0100 Subject: [PATCH 82/90] fix: parallel fix 2 (#47) --- milo/apis/storage.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/milo/apis/storage.lua b/milo/apis/storage.lua index fb086ea..2cdbc3d 100644 --- a/milo/apis/storage.lua +++ b/milo/apis/storage.lua @@ -277,6 +277,10 @@ function Storage:listItems(throttle) chunk = {} end end + + if #chunk > 0 then + parallel.waitForAll(table.unpack(chunk)) + end end for _, adapter in self:onlineAdapters() do -- 2.49.1 From 0272d2a3038477551de024cfbb7a8aef3b62d468 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Sat, 26 Dec 2020 19:22:46 -0500 Subject: [PATCH 83/90] fix: missing damage value in tl3 --- swshop/swshop.lua | 1 + turtle/init/6.tl3.lua | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/swshop/swshop.lua b/swshop/swshop.lua index 0fadd48..5634e11 100644 --- a/swshop/swshop.lua +++ b/swshop/swshop.lua @@ -79,6 +79,7 @@ local function handleTransaction(transaction) to = transaction.to, from = transaction.from, value = transaction.value, + txid = transaction.id, id = metadata.name, time = math.floor(os.epoch('utc')/1000), recipient = recipient, diff --git a/turtle/init/6.tl3.lua b/turtle/init/6.tl3.lua index d6a352b..118e11c 100644 --- a/turtle/init/6.tl3.lua +++ b/turtle/init/6.tl3.lua @@ -798,15 +798,15 @@ function turtle.getSlot(indexOrId, slots) if detail then return { name = detail.name, - damage = detail.damage, + damage = detail.damage or 0, count = detail.count, - key = detail.name .. ':' .. detail.damage, + key = detail.name .. ':' .. (detail.damage or 0), index = indexOrId, -- deprecate qty = detail.count, - dmg = detail.damage, + dmg = detail.damage or 0, id = detail.name, } end -- 2.49.1 From c85bb530626767b80eaaedbbc0b554bc7d8c6228 Mon Sep 17 00:00:00 2001 From: RubenHetKonijn <64010592+RubenHetKonijn@users.noreply.github.com> Date: Sun, 3 Jan 2021 03:41:42 +0100 Subject: [PATCH 84/90] Make the block scanner requirement more clear (#49) --- neural/ores.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neural/ores.lua b/neural/ores.lua index fe31b0b..a55fe4b 100644 --- a/neural/ores.lua +++ b/neural/ores.lua @@ -33,7 +33,7 @@ if not modules then elseif not modules.canvas then showRequirements('Overlay glasses') elseif not modules.scan then - showRequirements('Scanner module') + showRequirements('Block Scanner module') end local projecting = { } -- 2.49.1 From 1d2187b957b7a2d9eb33f11fe4068e25d1a13719 Mon Sep 17 00:00:00 2001 From: Anavrins Date: Sun, 19 Jun 2022 20:00:56 -0400 Subject: [PATCH 85/90] Closes #56 --- milo/core/machines.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/milo/core/machines.lua b/milo/core/machines.lua index 049c5a1..cdc5ce1 100644 --- a/milo/core/machines.lua +++ b/milo/core/machines.lua @@ -28,7 +28,7 @@ local networkPage = UI.Page { y = 2, ey = -3, values = context.storage.nodes, columns = { - { key = 'suffix', width = 4, align = 'right' }, + { key = 'suffix', width = 5, align = 'right' }, { heading = 'Name', key = 'displayName' }, { heading = 'Type', key = 'mtype', width = 4 }, { heading = 'Pri', key = 'priority', width = 3 }, -- 2.49.1 From af570c776943ddceee933ef92aa5fe8431856b80 Mon Sep 17 00:00:00 2001 From: Kan18 <24967425+Kan18@users.noreply.github.com> Date: Fri, 1 Jul 2022 03:02:56 -0400 Subject: [PATCH 86/90] Fix storageGen.lua (#59) --- milo/autorun/milo.lua | 1 + milo/{apps => }/storageGen.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) rename milo/{apps => }/storageGen.lua (97%) diff --git a/milo/autorun/milo.lua b/milo/autorun/milo.lua index bbb626f..ecf0d82 100644 --- a/milo/autorun/milo.lua +++ b/milo/autorun/milo.lua @@ -6,6 +6,7 @@ fs.delete('packages/milo/Milo.lua') fs.delete('packages/milo/plugins/listing.lua') fs.delete('packages/milo/apis/milo.lua') fs.delete('packages/milo/plugins/manipulator.lua') +fs.delete('packages/milo/apps') if peripheral.find('workbench') and shell.openForegroundTab then shell.openForegroundTab('MiloLocal') diff --git a/milo/apps/storageGen.lua b/milo/storageGen.lua similarity index 97% rename from milo/apps/storageGen.lua rename to milo/storageGen.lua index c4ea024..542f410 100644 --- a/milo/apps/storageGen.lua +++ b/milo/storageGen.lua @@ -86,7 +86,7 @@ end function page:saveConfig(path) local config = Util.readTable(path) or {} Util.each(self.storages, function(dev, name) - if self.typeGrid.values[dev.type] and self.typeGrid.values[dev.type].checked and not config[name] then + if self.typeGrid.values[dev.type] and self.typeGrid.values[dev.type].checked and (not config[name] or config[name].mtype == 'ignore') then config[name] = { name = name, category = 'storage', -- 2.49.1 From b3052fe57b0491500523a4c94737ca963be89c6e Mon Sep 17 00:00:00 2001 From: EmmaKnijn <64010592+EmmaKnijn@users.noreply.github.com> Date: Sun, 17 Jul 2022 23:43:14 +0200 Subject: [PATCH 87/90] Adress a couple of issues (#58) --- README.md | 16 ++++++++++++++++ milo/plugins/jobMonitor.lua | 3 ++- milo/plugins/statsView.lua | 4 ++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6a47fa4..a81bf32 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,18 @@ # opus-apps Applications for Opus OS + +## Installing an application +To install an application, follow these steps +1. Start your OpusOS Computer +2. Go to the System tab on the main menu +3. Find the packages app and open it +4. Select the package you want +5. Select the package and press the `+` button +6. Your application should get installed! + +## Updating your applications +To update your applications, follow these steps +1. Start your OpusOS Computer +2. Go to the System tab on the main menu +3. Find the packages app and open it +4. Press the `Update All` button diff --git a/milo/plugins/jobMonitor.lua b/milo/plugins/jobMonitor.lua index a548fcf..d24d164 100644 --- a/milo/plugins/jobMonitor.lua +++ b/milo/plugins/jobMonitor.lua @@ -157,7 +157,8 @@ local function createPage(node) row.remaining = math.max(0, row.requested - row.crafted) --_syslog('%d %d %d %d', row.remaining, row.requested, row.total, row.crafted) row.status = (row.status or '') .. - string.format(' %d of %d', row.crafted + row.total, row.total + row.requested) + row.total = row.total or 0 + string.format(' %d of %d', row.crafted + row.total, row.total + row.requested) else row.displayName = ' ' .. row.displayName row.status = (row.status or '') .. string.format(' %d of %d', row.count, row.total) diff --git a/milo/plugins/statsView.lua b/milo/plugins/statsView.lua index 799c1a2..db996ff 100644 --- a/milo/plugins/statsView.lua +++ b/milo/plugins/statsView.lua @@ -151,7 +151,7 @@ local function createPage(node) { heading = 'Size', key = 'size', width = 5 }, { heading = 'Used', key = 'used', width = 5 }, { heading = 'Perc', key = 'perc', width = 5 }, - -- TODO: add % to each number + }, sortColumn = 'name', }, @@ -224,7 +224,7 @@ local function createPage(node) name = n.displayName or n.name, size = n.adapter.__size, used = n.adapter.__used, - perc = math.floor(n.adapter.__used / n.adapter.__size * 100), + perc = tostring(math.floor(n.adapter.__used / n.adapter.__size * 100)) .. "%", updated = updated, }) totals.usedSlots = totals.usedSlots + n.adapter.__used -- 2.49.1 From cba2f7013fec4be05731e74f750489d343e8e802 Mon Sep 17 00:00:00 2001 From: Kan18 <24967425+Kan18@users.noreply.github.com> Date: Sun, 17 Jul 2022 19:44:54 -0400 Subject: [PATCH 88/90] fix jobMonitor.lua (#61) --- milo/plugins/jobMonitor.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/milo/plugins/jobMonitor.lua b/milo/plugins/jobMonitor.lua index d24d164..58c52d1 100644 --- a/milo/plugins/jobMonitor.lua +++ b/milo/plugins/jobMonitor.lua @@ -156,9 +156,9 @@ local function createPage(node) if row.requested then row.remaining = math.max(0, row.requested - row.crafted) --_syslog('%d %d %d %d', row.remaining, row.requested, row.total, row.crafted) - row.status = (row.status or '') .. row.total = row.total or 0 - string.format(' %d of %d', row.crafted + row.total, row.total + row.requested) + row.status = (row.status or '') .. + string.format(' %d of %d', row.crafted + row.total, row.total + row.requested) else row.displayName = ' ' .. row.displayName row.status = (row.status or '') .. string.format(' %d of %d', row.count, row.total) -- 2.49.1 From aa3cd6d08dd612b4afb35d649b45e580eff05336 Mon Sep 17 00:00:00 2001 From: xAnavrins Date: Sun, 17 Jul 2022 19:53:06 -0400 Subject: [PATCH 89/90] Better error visibility in multiMiner --- common/multiMiner.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/multiMiner.lua b/common/multiMiner.lua index a271261..ce71e8c 100644 --- a/common/multiMiner.lua +++ b/common/multiMiner.lua @@ -89,7 +89,7 @@ local function hijackTurtle(remoteId) local socket, msg = Socket.connect(remoteId, 188) if not socket then - error(msg) + error(msg, 0) end socket:write('turtle') @@ -101,7 +101,7 @@ local function hijackTurtle(remoteId) socket:write({ method, ... }) local resp = socket:read() if not resp then - error('timed out: ' .. method) + error('T/O: ' .. method, 0) end return table.unpack(resp) end -- 2.49.1 From 2461d060e092d68b17e6e02f199ebc0d5dd48ca7 Mon Sep 17 00:00:00 2001 From: xAnavrins Date: Mon, 18 Jul 2022 00:07:55 -0400 Subject: [PATCH 90/90] Updates to swshop part 1 Updated URL to new krist node Form now validates if address own provided domain Support for third-party sync nodes Update k.lua and w.lua Closes #60 --- .gitignore | 1 + swshop/apis/k.lua | 17 ++++++--- swshop/apis/w.lua | 32 ++++++++++------ swshop/shopConfig.lua | 88 ++++++++++++++++++++++++++++++++----------- swshop/swshop.lua | 10 +++-- 5 files changed, 105 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index a5e9d77..b4752c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /etc/fstab /etc/recipes2.db +/.vscode diff --git a/swshop/apis/k.lua b/swshop/apis/k.lua index 4adf49e..d6e3208 100644 --- a/swshop/apis/k.lua +++ b/swshop/apis/k.lua @@ -4,9 +4,9 @@ local jua local json local await -local endpoint = "krist.ceriat.net" -local wsEndpoint = "ws://"..endpoint -local httpEndpoint = "http://"..endpoint +local endpoint = "krist.dev" +local wsEndpoint = "wss://"..endpoint +local httpEndpoint = "https://"..endpoint local function asserttype(var, name, vartype, optional) if not (type(var) == vartype or optional and type(var) == "nil") then @@ -14,6 +14,12 @@ local function asserttype(var, name, vartype, optional) end end +function setNodeUrl(url) + endpoint = url + wsEndpoint = "wss://"..endpoint + httpEndpoint = "https://"..endpoint +end + function init(juai, jsoni, wi, ri) asserttype(juai, "jua", "table") asserttype(jsoni, "json", "table") @@ -53,7 +59,7 @@ local function authorize_websocket(cb, privatekey) asserttype(privatekey, "privatekey", "string", true) api_request(function(success, data) - cb(success and data and data.ok, data.url and data.url:gsub("wss:", "ws:") or data) + cb(success and data and data.ok, data.url and data.url:gsub("ws:", "wss:") or data) end, "/ws/start", { privatekey = privatekey }) @@ -175,7 +181,7 @@ local wsEventNameLookup = { ownNames = "name", ownWebhooks = "webhook", motd = "motd", - keepalive = "keepalive", + keepalive = "keepalive" } local wsEvents = {} @@ -367,6 +373,7 @@ function parseMeta(meta) end return { + setNodeUrl = setNodeUrl, init = init, address = address, addressTransactions = addressTransactions, diff --git a/swshop/apis/w.lua b/swshop/apis/w.lua index 7fe1e3e..c0da9a4 100644 --- a/swshop/apis/w.lua +++ b/swshop/apis/w.lua @@ -79,10 +79,12 @@ function init(jua) jua.on("websocket_failure", function(event, url) local id = findID(url) - if id and callbackRegistry[id].failure then - callbackRegistry[id].failure(id) + if id then + if callbackRegistry[id].failure then + callbackRegistry[id].failure(id) + end + table.remove(callbackRegistry, id) end - table.remove(callbackRegistry, id) end) jua.on("websocket_message", function(event, url, data) @@ -94,10 +96,12 @@ function init(jua) jua.on("websocket_closed", function(event, url) local id = findID(url) - if id and callbackRegistry[id].closed then - callbackRegistry[id].closed(id) + if id then + if callbackRegistry[id].closed then + callbackRegistry[id].closed(id) + end + table.remove(callbackRegistry, id) end - table.remove(callbackRegistry, id) end) else jua.on("socket_connect", function(event, id) @@ -107,10 +111,12 @@ function init(jua) end) jua.on("socket_error", function(event, id, msg) - if id and callbackRegistry[id].failure then - callbackRegistry[id].failure(id, msg) + if id then + if callbackRegistry[id].failure then + callbackRegistry[id].failure(id, msg) + end + table.remove(callbackRegistry, id) end - table.remove(callbackRegistry, id) end) jua.on("socket_message", function(event, id) @@ -121,10 +127,12 @@ function init(jua) end) jua.on("socket_closed", function(event, id) - if id and callbackRegistry[id].closed then - callbackRegistry[id].closed(id) + if id then + if callbackRegistry[id].closed then + callbackRegistry[id].closed(id) + end + table.remove(callbackRegistry, id) end - table.remove(callbackRegistry, id) end) end end diff --git a/swshop/shopConfig.lua b/swshop/shopConfig.lua index 8c1ffba..fd7dd35 100644 --- a/swshop/shopConfig.lua +++ b/swshop/shopConfig.lua @@ -5,6 +5,8 @@ local colors = _G.colors local device = _G.device local os = _G.os +local defaultKristNode = "https://krist.dev" + --[[ Configuration Page ]]-- local wizardPage = UI.WizardPage { title = 'Store Front', @@ -13,28 +15,21 @@ local wizardPage = UI.WizardPage { x = 2, ex = -2, y = 1, ey = -2, manualControls = true, [1] = UI.TextEntry { - formLabel = 'Domain', formKey = 'domain', - help = 'Krist wallet domain (minus .kst)', - limit = 64, - shadowText = 'example', - required = true, - }, - [2] = UI.TextEntry { formLabel = 'Header', formKey = 'header', help = 'Text to show in header', limit = 64, shadowText = "xxxx's shop", required = false, }, - [3] = UI.Checkbox { + [2] = UI.Checkbox { formLabel = 'Single shop', formKey = 'refundInvalid', help = 'Only this shop uses this domain', }, - [4] = UI.Checkbox { + [3] = UI.Checkbox { formLabel = 'Show out of stock', formKey = 'showOutOfStock', help = 'Show out of stock items in red', }, - [5] = UI.Chooser { + [4] = UI.Chooser { formLabel = 'RS Signal', formKey = 'rsSide', formIndex = 6, width = 10, choices = { @@ -47,7 +42,7 @@ local wizardPage = UI.WizardPage { }, required = true, }, - [6] = UI.Chooser { + [5] = UI.Chooser { width = 9, formIndex = 7, formLabel = 'Font Size', formKey = 'textScale', @@ -88,14 +83,33 @@ function wizardPage:isValidFor(node) end -- [[Password View]] -- -local passwordPage = UI.WizardPage { +local kristPage = UI.WizardPage { title = 'Krist Settings', index = 3, form = UI.Form { x = 2, ex = -2, y = 1, ey = -2, manualControls = true, - passEntry = UI.TextEntry { + + nodeEntry = UI.TextEntry { formIndex = 1, + formLabel = 'Sync Node', formKey = 'syncNode', + shadowText = 'http(s)://domain:port', + help = 'Sync node the shop will use', + limit = 256, + required = true, + }, + + domainEntry = UI.TextEntry { + formIndex = 3, + formLabel = 'Domain', formKey = 'domain', + help = 'Krist wallet domain (minus .kst)', + limit = 64, + shadowText = 'example', + required = true, + }, + + passEntry = UI.TextEntry { + formIndex = 4, formLabel = 'Password', formKey = 'password', shadowText = 'Password', help = 'Krist wallet password', @@ -103,15 +117,17 @@ local passwordPage = UI.WizardPage { required = true, pass = true, }, + pkeyCheck = UI.Checkbox { - formIndex = 2, - formLabel = 'Is private key', formKey = 'isPrivateKey', + formIndex = 5, + formLabel = 'Private key', formKey = 'isPrivateKey', help = 'Password is in private key format', ispkey = true, }, + preview = UI.TextEntry { - formIndex = 4, - formLabel = 'Using address', formKey = 'address', + formIndex = 7, + formLabel = 'Address', formKey = 'address', backgroundColor = 'primary', textColor = colors.yellow, inactive = true, @@ -127,7 +143,7 @@ local function makeAddress(text, isPrivateKey) return Krist.makev2address(privKey) end -function passwordPage.form:eventHandler(event) +function kristPage.form:eventHandler(event) if (event.type == 'text_change' and event.element.pass) or (event.type == 'checkbox_change' and event.element.ispkey) then self.passEntry.shadowText = self.pkeyCheck.value and 'Private key' or 'Password' @@ -137,17 +153,43 @@ function passwordPage.form:eventHandler(event) return UI.Form.eventHandler(self, event) end -function passwordPage:setNode(node) +function kristPage:setNode(node) node.address = node.password and makeAddress(node.password, node.isPrivateKey) or '' + node.syncNode = node.syncNode or defaultKristNode self.form:setValues(node) end -function passwordPage:validate() - return self.form:save() +local function addressHasName(node, address, domain) + local res, err = http.get(node .. "/addresses/" .. address .. "/names") + if res then + local data = textutils.unserializeJSON(res.readAll()) + if data.ok then + for _, name in pairs(data.names) do + if name.name == domain then + return true + end + end + return false, "This address doesn't own this name" + end + return false, data.error + end + return false, "Error while checking names" end -function passwordPage:isValidFor(node) +function kristPage:validate() + local ok, err = http.checkURL(self.form.nodeEntry.value or '') + + if ok then + ok, err = addressHasName(self.form.nodeEntry.value, self.form.preview.value, self.form.domainEntry.value) + if ok then + return self.form:save() + end + end + self:emit({ type = 'general_error', message = err }) +end + +function kristPage:isValidFor(node) return node.mtype == 'shop' end -UI:getPage('nodeWizard').wizard:add({ storeFronta = wizardPage, storeFrontb = passwordPage }) +UI:getPage('nodeWizard').wizard:add({ storeFronta = wizardPage, storeFrontb = kristPage }) diff --git a/swshop/swshop.lua b/swshop/swshop.lua index 5634e11..8529f5a 100644 --- a/swshop/swshop.lua +++ b/swshop/swshop.lua @@ -21,16 +21,20 @@ local storage = Config.load('storage') Util.each(rs.getSides(), function(side) rs.setOutput(side, false) end) -r.init(jua) -w.init(jua) -k.init(jua, json, w, r) +local defaultKristNode = "https://krist.dev" local node = ({ ... })[1] or error('Node name is required') local config = storage[node] local privatekey = config.isPrivateKey and config.password or Krist.toKristWalletFormat(config.password) +local nodeurl = config.syncNode or defaultKristNode local address = Krist.makev2address(privatekey) local rsSide = config.rsSide or 'top' +r.init(jua) +w.init(jua) +k.setNodeUrl(nodeurl:gsub("https?://", "")) +k.init(jua, json, w, r) + jua.on("terminate", function() rs.setOutput(rsSide, false) jua.stop() -- 2.49.1