diff --git a/LICENSE.md b/LICENSE.md index a5e2567..8e8b7db 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016-2017 kepler155c +Copyright (c) 2016-2019 kepler155c Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/startup b/startup deleted file mode 100644 index 917ddab..0000000 --- a/startup +++ /dev/null @@ -1,96 +0,0 @@ -local colors = _G.colors -local os = _G.os -local settings = _G.settings -local term = _G.term - -local bootOptions = { - { prompt = os.version() }, - { prompt = 'Opus' , args = { '/sys/boot/opus.boot' } }, - { prompt = 'Opus Shell' , args = { '/sys/boot/opus.boot', 'sys/apps/shell' } }, -} -local bootOption = 2 -if settings then - settings.load('.settings') - bootOption = tonumber(settings.get('opus.boot_option') or 2) or 2 -end - -local function startupMenu() - while true do - term.clear() - term.setCursorPos(1, 1) - print('Select startup mode') - print() - for k,option in pairs(bootOptions) do - print(k .. ' : ' .. option.prompt) - end - print('') - term.write('> ') - local ch = tonumber(_G.read()) - if ch and bootOptions[ch] then - return ch - end - end -end - -local function splash() - local w, h = term.current().getSize() - - term.setTextColor(colors.white) - if not term.isColor() then - local str = 'Opus OS' - term.setCursorPos((w - #str) / 2, h / 2) - term.write(str) - else - term.setBackgroundColor(colors.black) - term.clear() - local opus = { - 'fffff00', - 'ffff07000', - 'ff00770b00 4444', - 'ff077777444444444', - 'f07777744444444444', - 'f0000777444444444', - '070000111744444', - '777770000', - '7777000000', - '70700000000', - '077000000000', - } - for k,line in ipairs(opus) do - term.setCursorPos((w - 18) / 2, k + (h - #opus) / 2) - term.blit(string.rep(' ', #line), string.rep('a', #line), line) - end - end - - local str = 'Press any key for menu' - term.setCursorPos((w - #str) / 2, h) - term.write(str) -end - -term.clear() -splash() - -local timerId = os.startTimer(1.5) -while true do - local e, id = os.pullEvent() - if e == 'timer' and id == timerId then - break - end - if e == 'char' then - bootOption = startupMenu() - if settings then - settings.set('opus.boot_option', bootOption) - settings.save('.settings') - end - break - end -end - -term.clear() -term.setCursorPos(1, 1) -if bootOptions[bootOption].args then - os.run(_G.getfenv(1), table.unpack(bootOptions[bootOption].args)) -else - print(bootOptions[bootOption].prompt) -end - diff --git a/startup.lua b/startup.lua new file mode 100644 index 0000000..b6381d1 --- /dev/null +++ b/startup.lua @@ -0,0 +1,181 @@ +--[[ + .startup.boot + delay + description: delays amount before starting the default selection + default: 1.5 + + preload + description : runs before menu is displayed, can be used for password + locking, drive encryption, etc. + example : { [1] = '/path/somefile.lua', [2] = 'path2/another.lua' } + + menu + description: array of menu entries (see .startup.boot for examples) +]] + +local colors = _G.colors +local fs = _G.fs +local keys = _G.keys +local os = _G.os +local settings = _G.settings +local term = _G.term +local textutils = _G.textutils + +local function loadBootOptions() + if not fs.exists('.startup.boot') then + local f = fs.open('.startup.boot', 'w') + f.write(textutils.serialize({ + delay = 1.5, + preload = { }, + menu = { + { prompt = os.version() }, + { prompt = 'Opus' , args = { '/sys/boot/opus.boot' } }, + { prompt = 'Opus Shell' , args = { '/sys/boot/opus.boot', 'sys/apps/shell.lua' } }, + { prompt = 'Opus Kiosk' , args = { '/sys/boot/kiosk.boot' } }, + }, + })) + f.close() + end + + local f = fs.open('.startup.boot', 'r') + local options = textutils.unserialize(f.readAll()) + f.close() + + return options +end + +local bootOptions = loadBootOptions() + +local bootOption = 2 +if settings then + settings.load('.settings') + bootOption = tonumber(settings.get('opus.boot_option')) or bootOption +end + +local function startupMenu() + local x, y = term.getSize() + local align, selected = 0, bootOption + + local function redraw() + local title = "Boot Options:" + term.clear() + term.setTextColor(colors.white) + term.setCursorPos((x/2)-(#title/2), (y/2)-(#bootOptions.menu/2)-1) + term.write(title) + for i, item in pairs(bootOptions.menu) do + local txt = i .. ". " .. item.prompt + term.setCursorPos((x/2)-(align/2), (y/2)-(#bootOptions.menu/2)+i) + term.write(txt) + end + end + + for _, item in pairs(bootOptions.menu) do + if #item.prompt > align then + align = #item.prompt + end + end + + redraw() + while true do + term.setCursorPos((x/2)-(align/2)-2, (y/2)-(#bootOptions.menu/2)+selected) + term.setTextColor(term.isColor() and colors.yellow or colors.lightGray) + + term.write(">") + local event, key = os.pullEvent() + if event == "mouse_scroll" then + key = key == 1 and keys.down or keys.up + elseif event == 'key_up' then + key = nil -- only process key events + end + + if key == keys.enter or key == keys.right then + return selected + elseif key == keys.down then + if selected == #bootOptions.menu then + selected = 0 + end + selected = selected + 1 + elseif key == keys.up then + if selected == 1 then + selected = #bootOptions.menu + 1 + end + selected = selected - 1 + elseif event == 'char' then + key = tonumber(key) or 0 + if bootOptions.menu[key] then + return key + end + end + + local cx, cy = term.getCursorPos() + term.setCursorPos(cx-1, cy) + term.write(" ") + end +end + +local function splash() + local w, h = term.current().getSize() + + term.setTextColor(colors.white) + if not term.isColor() then + local str = 'Opus OS' + term.setCursorPos((w - #str) / 2, h / 2) + term.write(str) + else + term.setBackgroundColor(colors.black) + term.clear() + local opus = { + 'fffff00', + 'ffff07000', + 'ff00770b00 4444', + 'ff077777444444444', + 'f07777744444444444', + 'f0000777444444444', + '070000111744444', + '777770000', + '7777000000', + '70700000000', + '077000000000', + } + for k,line in ipairs(opus) do + term.setCursorPos((w - 18) / 2, k + (h - #opus) / 2) + term.blit(string.rep(' ', #line), string.rep('a', #line), line) + end + end + + local str = 'Press any key for menu' + term.setCursorPos((w - #str) / 2, h) + term.write(str) +end + +for _, v in pairs(bootOptions.preload) do + os.run(_ENV, v) +end + +term.clear() +splash() + +local timerId = os.startTimer(bootOptions.delay) +while true do + local e, id = os.pullEvent() + if e == 'timer' and id == timerId then + break + end + if e == 'char' or e == 'key' then + bootOption = startupMenu() + if settings then + settings.set('opus.boot_option', bootOption) + settings.save('.settings') + end + break + end +end + +term.clear() +term.setCursorPos(1, 1) +if bootOptions.menu[bootOption].args then + os.run(_ENV, table.unpack(bootOptions.menu[bootOption].args)) +else + print(bootOptions.menu[bootOption].prompt) +end + diff --git a/sys/apis/crypto.lua b/sys/apis/crypto.lua deleted file mode 100644 index fc1075b..0000000 --- a/sys/apis/crypto.lua +++ /dev/null @@ -1,150 +0,0 @@ --- https://github.com/PixelToast/ComputerCraft/blob/master/apis/enc - -local Crypto = { } - -local function serialize(t) - local sType = type(t) - if sType == "table" then - local lstcnt=0 - for k,v in pairs(t) do - lstcnt = lstcnt + 1 - end - local result = "{" - local aset=1 - for k,v in pairs(t) do - if k==aset then - result = result..serialize(v).."," - aset=aset+1 - else - result = result..("["..serialize(k).."]="..serialize(v)..",") - end - end - result = result.."}" - return result - elseif sType == "string" then - return string.format("%q",t) - elseif sType == "number" or sType == "boolean" or sType == "nil" then - return tostring(t) - elseif sType == "function" then - local status,data=pcall(string.dump,t) - if status then - data2="" - for char in string.gmatch(data,".") do - data2=data2..zfill(string.byte(char)) - end - return 'f("'..data2..'")' - else - error("Invalid function: "..data) - end - else - error("Could not serialize type "..sType..".") - end -end - -local function unserialize( s ) - local func, e = loadstring( "return "..s, "serialize" ) - if not func then - return s,e - else - setfenv( func, { - f=function(S) - return loadstring(splitnum(S)) - end, - }) - return func() - end -end - -local function splitnum(S) - local Out="" - for l1=1,#S,2 do - local l2=(#S-l1)+1 - local function sure(N,n) - if (l2-n)<1 then N="0" end - return N - end - local CNum=tonumber("0x"..sure(string.sub(S,l2-1,l2-1),1) .. sure(string.sub(S,l2,l2),0)) - Out=string.char(CNum)..Out - end - return Out -end - -local function zfill(N) - N=string.format("%X",N) - Zs="" - if #N==1 then - Zs="0" - end - return Zs..N -end - -local function wrap(N) - return N-(math.floor(N/256)*256) -end - -local function checksum(S) - local sum=0 - for char in string.gmatch(S,".") do - math.randomseed(string.byte(char)+sum) - sum=sum+math.random(0,9999) - end - math.randomseed(sum) - return sum -end - -local function genkey(len,psw) - checksum(psw) - local key={} - local tKeys={} - for l1=1,len do - local num=math.random(1,len) - while tKeys[num] do - num=math.random(1,len) - end - tKeys[num]=true - key[l1]={num,math.random(0,255)} - end - return key -end - -function Crypto.encrypt(data,psw) - data=serialize(data) - local chs=checksum(data) - local key=genkey(#data,psw) - local out={} - local cnt=1 - for char in string.gmatch(data,".") do - table.insert(out,key[cnt][1],zfill(wrap(string.byte(char)+key[cnt][2])),chars) - cnt=cnt+1 - end - return string.sub(serialize({chs,table.concat(out)}),2,-3) -end - -function Crypto.decrypt(data,psw) - local oData=data - data=unserialize("{"..data.."}") - if type(data)~="table" then - return oData - end - local chs=data[1] - data=data[2] - local key=genkey((#data)/2,psw) - local sKey={} - for k,v in pairs(key) do - sKey[v[1]]={k,v[2]} - end - local str=splitnum(data) - local cnt=1 - local out={} - for char in string.gmatch(str,".") do - table.insert(out,sKey[cnt][1],string.char(wrap(string.byte(char)-sKey[cnt][2]))) - cnt=cnt+1 - end - out=table.concat(out) - if checksum(out or "")==chs then - return unserialize(out) - end - return oData,out,chs -end - -return Crypto diff --git a/sys/apis/entry.lua b/sys/apis/entry.lua deleted file mode 100644 index 992e967..0000000 --- a/sys/apis/entry.lua +++ /dev/null @@ -1,151 +0,0 @@ -local class = require('class') - -local os = _G.os - -local Entry = class() - -function Entry:init(args) - self.pos = 0 - self.scroll = 0 - self.value = '' - self.width = args.width - self.limit = 1024 -end - -function Entry:reset() - self.pos = 0 - self.scroll = 0 - self.value = '' -end - -local function nextWord(line, cx) - local result = { line:find("(%w+)", cx) } - if #result > 1 and result[2] > cx then - return result[2] + 1 - elseif #result > 0 and result[1] == cx then - result = { line:find("(%w+)", result[2] + 1) } - if #result > 0 then - return result[1] - end - end -end - -function Entry:updateScroll() - if self.pos - self.scroll > self.width then - self.scroll = self.pos - (self.width) - elseif self.pos < self.scroll then - self.scroll = self.pos - end -end - -function Entry:process(ie) - local updated = false - - if ie.code == 'left' then - if self.pos > 0 then - self.pos = math.max(self.pos - 1, 0) - updated = true - end - - elseif ie.code == 'right' then - local input = tostring(self.value) - if self.pos < #input then - self.pos = math.min(self.pos + 1, #input) - updated = true - end - - elseif ie.code == 'home' then - if self.pos ~= 0 then - self.pos = 0 - updated = true - end - - elseif ie.code == 'end' then - if self.pos ~= #tostring(self.value) then - self.pos = #tostring(self.value) - updated = true - end - - elseif ie.code == 'backspace' then - if self.pos > 0 then - local input = tostring(self.value) - self.value = input:sub(1, self.pos - 1) .. input:sub(self.pos + 1) - self.pos = self.pos - 1 - updated = true - end - - elseif ie.code == 'control-right' then - local nx = nextWord(self.value, self.pos + 1) - if nx then - self.pos = math.min(nx - 1, #self.value) - elseif self.pos < #self.value then - self.pos = #self.value - end - updated = true - - elseif ie.code == 'control-left' then - if self.pos ~= 0 then - local lx = 1 - while true do - local nx = nextWord(self.value, lx) - if not nx or nx >= self.pos then - break - end - lx = nx - end - if not lx then - self.pos = 0 - else - self.pos = lx - 1 - end - updated = true - end - - elseif ie.code == 'delete' then - local input = tostring(self.value) - if self.pos < #input then - self.value = input:sub(1, self.pos) .. input:sub(self.pos + 2) - self.update = true - updated = true - end - - elseif ie.code == 'char' then - local input = tostring(self.value) - if #input < self.limit then - self.value = input:sub(1, self.pos) .. ie.ch .. input:sub(self.pos + 1) - self.pos = self.pos + 1 - self.update = true - updated = true - end - - elseif ie.code == 'copy' then - os.queueEvent('clipboard_copy', self.value) - - elseif ie.code == 'paste' then - local input = tostring(self.value) - if #input + #ie.text > self.limit then - ie.text = ie.text:sub(1, self.limit-#input) - end - self.value = input:sub(1, self.pos) .. ie.text .. input:sub(self.pos + 1) - self.pos = self.pos + #ie.text - updated = true - - elseif ie.code == 'mouse_click' then - -- need starting x passed in instead of hardcoding 3 - self.pos = math.min(ie.x - 3 + self.scroll, #self.value) - updated = true - - elseif ie.code == 'mouse_rightclick' then - local input = tostring(self.value) - if #input > 0 then - self:reset() - updated = true - end - end - - self:updateScroll() - - return updated -end - -return Entry diff --git a/sys/apis/fs/redfs.lua b/sys/apis/fs/redfs.lua deleted file mode 100644 index bbd981a..0000000 --- a/sys/apis/fs/redfs.lua +++ /dev/null @@ -1,61 +0,0 @@ ---[[ - Mount a readonly file system from another computer across rednet. The - target computer must be running OpusOS or redserver. Dissimlar to samba - in that a snapshot of the target is taken upon mounting - making this - faster. - - Useful for mounting a non-changing directory tree. - - Syntax: - rttp:///directory/subdir - - Examples: - rttp://12/usr/etc - rttp://8/usr -]]-- - -local rttp = require('rttp') - -local fs = _G.fs - -local redfs = { } - -local function getListing(uri) - local success, response = rttp.get(uri .. '?recursive=true') - - if not success then - error(response) - end - - if response.statusCode ~= 200 then - error('Received response ' .. response.statusCode) - end - - local list = { } - for _,v in pairs(response.data) do - if not v.isDir then - list[v.path] = { - url = uri .. '/' .. v.path, - size = v.size, - } - end - end - - return list -end - -function redfs.mount(dir, uri) - if not uri then - error('redfs syntax: uri') - end - - local list = getListing(uri) - for path, entry in pairs(list) do - if not fs.exists(fs.combine(dir, path)) then - local node = fs.mount(fs.combine(dir, path), 'urlfs', entry.url) - node.size = entry.size - end - end -end - -return redfs diff --git a/sys/apis/injector.lua b/sys/apis/injector.lua deleted file mode 100644 index 15fc8b7..0000000 --- a/sys/apis/injector.lua +++ /dev/null @@ -1,178 +0,0 @@ -local PASTEBIN_URL = 'http://pastebin.com/raw' -local GIT_URL = 'https://raw.githubusercontent.com' -local DEFAULT_PATH = 'sys/apis' -local DEFAULT_BRANCH = _ENV.OPUS_BRANCH or _G.OPUS_BRANCH or 'master' -local DEFAULT_UPATH = GIT_URL .. '/kepler155c/opus/' .. DEFAULT_BRANCH .. '/sys/apis' - -local fs = _G.fs -local http = _G.http -local os = _G.os - -if not http._patched then - -- fix broken http get - local syncLocks = { } - - local function sync(obj, fn) - local key = tostring(obj) - if syncLocks[key] then - local cos = tostring(coroutine.running()) - table.insert(syncLocks[key], cos) - repeat - local _, co = os.pullEvent('sync_lock') - until co == cos - else - syncLocks[key] = { } - end - fn() - local co = table.remove(syncLocks[key], 1) - if co then - os.queueEvent('sync_lock', co) - else - syncLocks[key] = nil - end - end - - -- todo -- completely replace http.get with function that - -- checks for success on permanent redirects (minecraft 1.75 bug) - - http._patched = http.get - function http.get(url, headers) - local s, m - sync(url, function() - s, m = http._patched(url, headers) - end) - return s, m - end -end - -local function loadUrl(url) - local c - local h = http.get(url) - if h then - c = h.readAll() - h.close() - end - if c and #c > 0 then - return c - end -end - --- Add require and package to the environment -local function requireWrapper(env) - - local function standardSearcher(modname) - if env.package.loaded[modname] then - return function() - return env.package.loaded[modname] - end - end - end - - local function shellSearcher(modname) - local fname = modname:gsub('%.', '/') .. '.lua' - - if env.shell and type(env.shell.dir) == 'function' then - local path = env.shell.resolve(fname) - if fs.exists(path) and not fs.isDir(path) then - return loadfile(path, env) - end - end - end - - local function pathSearcher(modname) - local fname = modname:gsub('%.', '/') .. '.lua' - - for dir in string.gmatch(env.package.path, "[^:]+") do - local path = fs.combine(dir, fname) - if fs.exists(path) and not fs.isDir(path) then - return loadfile(path, env) - end - end - end - - -- require('BniCQPVf') - local function pastebinSearcher(modname) - if #modname == 8 and not modname:match('%W') then - local url = PASTEBIN_URL .. '/' .. modname - local c = loadUrl(url) - if c then - return load(c, modname, nil, env) - end - end - end - - -- require('kepler155c.opus.master.sys.apis.util') - local function gitSearcher(modname) - local fname = modname:gsub('%.', '/') .. '.lua' - local _, count = fname:gsub("/", "") - if count >= 3 then - local url = GIT_URL .. '/' .. fname - local c = loadUrl(url) - if c then - return load(c, modname, nil, env) - end - end - end - - local function urlSearcher(modname) - local fname = modname:gsub('%.', '/') .. '.lua' - - if fname:sub(1, 1) ~= '/' then - for entry in string.gmatch(env.package.upath, "[^;]+") do - local url = entry .. '/' .. fname - local c = loadUrl(url) - if c then - return load(c, modname, nil, env) - end - end - end - end - - -- place package and require function into env - env.package = { - path = env.LUA_PATH or _G.LUA_PATH or DEFAULT_PATH, - upath = env.LUA_UPATH or _G.LUA_UPATH or DEFAULT_UPATH, - config = '/\n:\n?\n!\n-', - loaded = { - math = math, - string = string, - table = table, - io = io, - os = os, - }, - loaders = { - standardSearcher, - shellSearcher, - pathSearcher, - pastebinSearcher, - gitSearcher, - urlSearcher, - } - } - - function env.require(modname) - for _,searcher in ipairs(env.package.loaders) do - local fn, msg = searcher(modname) - if fn then - local module, msg2 = fn(modname, env) - if not module then - error(msg2 or (modname .. ' module returned nil'), 2) - end - env.package.loaded[modname] = module - return module - end - if msg then - error(msg, 2) - end - end - error('Unable to find module ' .. modname) - end - - return env.require -- backwards compatible -end - -return function(env) - env = env or getfenv(2) - --setfenv(requireWrapper, env) - return requireWrapper(env) -end diff --git a/sys/apis/json.lua b/sys/apis/json.lua deleted file mode 100644 index 64f8825..0000000 --- a/sys/apis/json.lua +++ /dev/null @@ -1,220 +0,0 @@ --- credit ElvishJerricco --- http://pastebin.com/raw.php?i=4nRg9CHU - -local json = { } - ------------------------------------------------------------------- utils -local controls = {["\n"]="\\n", ["\r"]="\\r", ["\t"]="\\t", ["\b"]="\\b", ["\f"]="\\f", ["\""]="\\\"", ["\\"]="\\\\"} - -local function isArray(t) - local max = 0 - for k,v in pairs(t) do - if type(k) ~= "number" then - return false - elseif k > max then - max = k - end - end - return max == #t -end - -local whites = {['\n']=true; ['\r']=true; ['\t']=true; [' ']=true; [',']=true; [':']=true} -local function removeWhite(str) - while whites[str:sub(1, 1)] do - str = str:sub(2) - end - return str -end - ------------------------------------------------------------------- encoding - -local function encodeCommon(val, pretty, tabLevel, tTracking) - local str = "" - - -- Tabbing util - local function tab(s) - str = str .. ("\t"):rep(tabLevel) .. s - end - - local function arrEncoding(val, bracket, closeBracket, iterator, loopFunc) - str = str .. bracket - if pretty then - str = str .. "\n" - tabLevel = tabLevel + 1 - end - for k,v in iterator(val) do - tab("") - loopFunc(k,v) - str = str .. "," - if pretty then str = str .. "\n" end - end - if pretty then - tabLevel = tabLevel - 1 - end - if str:sub(-2) == ",\n" then - str = str:sub(1, -3) .. "\n" - elseif str:sub(-1) == "," then - str = str:sub(1, -2) - end - tab(closeBracket) - end - - -- Table encoding - if type(val) == "table" then - assert(not tTracking[val], "Cannot encode a table holding itself recursively") - tTracking[val] = true - if isArray(val) then - arrEncoding(val, "[", "]", ipairs, function(k,v) - str = str .. encodeCommon(v, pretty, tabLevel, tTracking) - end) - else - arrEncoding(val, "{", "}", pairs, function(k,v) - assert(type(k) == "string", "JSON object keys must be strings", 2) - str = str .. encodeCommon(k, pretty, tabLevel, tTracking) - str = str .. (pretty and ": " or ":") .. encodeCommon(v, pretty, tabLevel, tTracking) - end) - end - -- String encoding - elseif type(val) == "string" then - str = '"' .. val:gsub("[%c\"\\]", controls) .. '"' - -- Number encoding - elseif type(val) == "number" or type(val) == "boolean" then - str = tostring(val) - else - error("JSON only supports arrays, objects, numbers, booleans, and strings", 2) - end - return str -end - -function json.encode(val) - return encodeCommon(val, false, 0, {}) -end - -function json.encodePretty(val) - return encodeCommon(val, true, 0, {}) -end - -function json.encodeToFile(path, val) - local file = io.open(path, "w") - assert(file, "Unable to open file") - file:write(json.encodePretty(val)) - file:close() -end - ------------------------------------------------------------------- decoding - -local decodeControls = {} -for k,v in pairs(controls) do - decodeControls[v] = k -end - -local function parseBoolean(str) - if str:sub(1, 4) == "true" then - return true, removeWhite(str:sub(5)) - else - return false, removeWhite(str:sub(6)) - end -end - -local function parseNull(str) - return nil, removeWhite(str:sub(5)) -end - -local numChars = {['e']=true; ['E']=true; ['+']=true; ['-']=true; ['.']=true} -local function parseNumber(str) - local i = 1 - while numChars[str:sub(i, i)] or tonumber(str:sub(i, i)) do - i = i + 1 - end - local val = tonumber(str:sub(1, i - 1)) - str = removeWhite(str:sub(i)) - return val, str -end - -local function parseString(str) - str = str:sub(2) - local s = "" - while str:sub(1,1) ~= "\"" do - local next = str:sub(1,1) - str = str:sub(2) - assert(next ~= "\n", "Unclosed string") - - if next == "\\" then - local escape = str:sub(1,1) - str = str:sub(2) - - next = assert(decodeControls[next..escape], "Invalid escape character") - end - - s = s .. next - end - return s, removeWhite(str:sub(2)) -end - -function json.parseArray(str) - str = removeWhite(str:sub(2)) - - local val = {} - local i = 1 - while str:sub(1, 1) ~= "]" do - local v - v, str = json.parseValue(str) - val[i] = v - i = i + 1 - str = removeWhite(str) - end - str = removeWhite(str:sub(2)) - return val, str -end - -function json.parseValue(str) - local fchar = str:sub(1, 1) - if fchar == "{" then - return json.parseObject(str) - elseif fchar == "[" then - return json.parseArray(str) - elseif tonumber(fchar) ~= nil or numChars[fchar] then - return parseNumber(str) - elseif str:sub(1, 4) == "true" or str:sub(1, 5) == "false" then - return parseBoolean(str) - elseif fchar == "\"" then - return parseString(str) - elseif str:sub(1, 4) == "null" then - return parseNull(str) - end -end - -function json.parseMember(str) - local k, val - k, str = json.parseValue(str) - val, str = json.parseValue(str) - return k, val, str -end - -function json.parseObject(str) - str = removeWhite(str:sub(2)) - - local val = {} - while str:sub(1, 1) ~= "}" do - local k, v - k, v, str = json.parseMember(str) - val[k] = v - str = removeWhite(str) - end - str = removeWhite(str:sub(2)) - return val, str -end - -function json.decode(str) - str = removeWhite(str) - return json.parseValue(str) -end - -function json.decodeFromFile(path) - local file = assert(fs.open(path, "r")) - local decoded = json.decode(file.readAll()) - file.close() - return decoded -end - -return json diff --git a/sys/apis/jumper/core/bheap.lua b/sys/apis/jumper/core/bheap.lua deleted file mode 100644 index c58ce2f..0000000 --- a/sys/apis/jumper/core/bheap.lua +++ /dev/null @@ -1,175 +0,0 @@ ---- A light implementation of Binary heaps data structure. --- While running a search, some search algorithms (Astar, Dijkstra, Jump Point Search) have to maintains --- a list of nodes called __open list__. Retrieve from this list the lowest cost node can be quite slow, --- as it normally requires to skim through the full set of nodes stored in this list. This becomes a real --- problem especially when dozens of nodes are being processed (on large maps). --- --- The current module implements a binary heap --- data structure, from which the search algorithm will instantiate an open list, and cache the nodes being --- examined during a search. As such, retrieving the lower-cost node is faster and globally makes the search end --- up quickly. --- --- This module is internally used by the library on purpose. --- It should normally not be used explicitely, yet it remains fully accessible. --- - ---[[ - Notes: - This lighter implementation of binary heaps, based on : - https://github.com/Yonaba/Binary-Heaps ---]] - -if (...) then - - -- Dependency - local Utils = require((...):gsub('%.bheap$','.utils')) - - -- Local reference - local floor = math.floor - - -- Default comparison function - local function f_min(a,b) return a < b end - - -- Percolates up - local function percolate_up(heap, index) - if index == 1 then return end - local pIndex - if index <= 1 then return end - if index%2 == 0 then - pIndex = index/2 - else pIndex = (index-1)/2 - end - if not heap._sort(heap._heap[pIndex], heap._heap[index]) then - heap._heap[pIndex], heap._heap[index] = - heap._heap[index], heap._heap[pIndex] - percolate_up(heap, pIndex) - end - end - - -- Percolates down - local function percolate_down(heap,index) - local lfIndex,rtIndex,minIndex - lfIndex = 2*index - rtIndex = lfIndex + 1 - if rtIndex > heap._size then - if lfIndex > heap._size then return - else minIndex = lfIndex end - else - if heap._sort(heap._heap[lfIndex],heap._heap[rtIndex]) then - minIndex = lfIndex - else - minIndex = rtIndex - end - end - if not heap._sort(heap._heap[index],heap._heap[minIndex]) then - heap._heap[index],heap._heap[minIndex] = heap._heap[minIndex],heap._heap[index] - percolate_down(heap,minIndex) - end - end - - -- Produces a new heap - local function newHeap(template,comp) - return setmetatable({_heap = {}, - _sort = comp or f_min, _size = 0}, - template) - end - - - --- The `heap` class.
- -- This class is callable. - -- _Therefore,_ heap(...) _is used to instantiate new heaps_. - -- @type heap - local heap = setmetatable({}, - {__call = function(self,...) - return newHeap(self,...) - end}) - heap.__index = heap - - --- Checks if a `heap` is empty - -- @class function - -- @treturn bool __true__ of no item is queued in the heap, __false__ otherwise - -- @usage - -- if myHeap:empty() then - -- print('Heap is empty!') - -- end - function heap:empty() - return (self._size==0) - end - - --- Clears the `heap` (removes all items queued in the heap) - -- @class function - -- @treturn heap self (the calling `heap` itself, can be chained) - -- @usage myHeap:clear() - function heap:clear() - self._heap = {} - self._size = 0 - self._sort = self._sort or f_min - return self - end - - --- Adds a new item in the `heap` - -- @class function - -- @tparam value item a new value to be queued in the heap - -- @treturn heap self (the calling `heap` itself, can be chained) - -- @usage - -- myHeap:push(1) - -- -- or, with chaining - -- myHeap:push(1):push(2):push(4) - function heap:push(item) - if item then - self._size = self._size + 1 - self._heap[self._size] = item - percolate_up(self, self._size) - end - return self - end - - --- Pops from the `heap`. - -- Removes and returns the lowest cost item (with respect to the comparison function being used) from the `heap`. - -- @class function - -- @treturn value a value previously pushed into the heap - -- @usage - -- while not myHeap:empty() do - -- local lowestValue = myHeap:pop() - -- ... - -- end - function heap:pop() - local root - if self._size > 0 then - root = self._heap[1] - self._heap[1] = self._heap[self._size] - self._heap[self._size] = nil - self._size = self._size-1 - if self._size>1 then - percolate_down(self, 1) - end - end - return root - end - - --- Restores the `heap` property. - -- Reorders the `heap` with respect to the comparison function being used. - -- When given argument __item__ (a value existing in the `heap`), will sort from that very item in the `heap`. - -- Otherwise, the whole `heap` will be cheacked. - -- @class function - -- @tparam[opt] value item the modified value - -- @treturn heap self (the calling `heap` itself, can be chained) - -- @usage myHeap:heapify() - function heap:heapify(item) - if self._size == 0 then return end - if item then - local i = Utils.indexOf(self._heap,item) - if i then - percolate_down(self, i) - percolate_up(self, i) - end - return - end - for i = floor(self._size/2),1,-1 do - percolate_down(self,i) - end - return self - end - - return heap -end \ No newline at end of file diff --git a/sys/apis/jumper/core/node.lua b/sys/apis/jumper/core/node.lua deleted file mode 100644 index 09d1db4..0000000 --- a/sys/apis/jumper/core/node.lua +++ /dev/null @@ -1,41 +0,0 @@ ---- The Node class. --- The `node` represents a cell (or a tile) on a collision map. Basically, for each single cell (tile) --- in the collision map passed-in upon initialization, a `node` object will be generated --- and then cached within the `grid`. --- --- In the following implementation, nodes can be compared using the `<` operator. The comparison is --- made with regards of their `f` cost. From a given node being examined, the `pathfinder` will expand the search --- to the next neighbouring node having the lowest `f` cost. See `core.bheap` for more details. --- -if (...) then - - local Node = {} - Node.__index = Node - - function Node:new(x,y,z) - return setmetatable({x = x, y = y, z = z }, Node) - end - - -- Enables the use of operator '<' to compare nodes. - -- Will be used to sort a collection of nodes in a binary heap on the basis of their F-cost - function Node.__lt(A,B) return (A._f < B._f) end - - function Node:getX() return self.x end - function Node:getY() return self.y end - function Node:getZ() return self.z end - - --- Clears temporary cached attributes of a `node`. - -- Deletes the attributes cached within a given node after a pathfinding call. - -- This function is internally used by the search algorithms, so you should not use it explicitely. - function Node:reset() - self._g, self._h, self._f = nil, nil, nil - self._opened, self._closed, self._parent = nil, nil, nil - return self - end - - return setmetatable(Node, - {__call = function(_,...) - return Node:new(...) - end} - ) -end \ No newline at end of file diff --git a/sys/apis/jumper/core/path.lua b/sys/apis/jumper/core/path.lua deleted file mode 100644 index f02c41b..0000000 --- a/sys/apis/jumper/core/path.lua +++ /dev/null @@ -1,67 +0,0 @@ ---- The Path class. --- The `path` class is a structure which represents a path (ordered set of nodes) from a start location to a goal. --- An instance from this class would be a result of a request addressed to `Pathfinder:getPath`. --- --- This module is internally used by the library on purpose. --- It should normally not be used explicitely, yet it remains fully accessible. --- - -if (...) then - - local t_remove = table.remove - - local Path = {} - Path.__index = Path - - function Path:new() - return setmetatable({_nodes = {}}, Path) - end - - --- Iterates on each single `node` along a `path`. At each step of iteration, - -- returns the `node` plus a count value. Aliased as @{Path:nodes} - -- @usage - -- for node, count in p:iter() do - -- ... - -- end - function Path:nodes() - local i = 1 - return function() - if self._nodes[i] then - i = i+1 - return self._nodes[i-1],i-1 - end - end - end - - --- `Path` compression modifier. Given a `path`, eliminates useless nodes to return a lighter `path` - -- consisting of straight moves. Does the opposite of @{Path:fill} - -- @class function - -- @treturn path self (the calling `path` itself, can be chained) - -- @see Path:fill - -- @usage p:filter() - function Path:filter() - local i = 2 - local xi,yi,zi,dx,dy,dz, olddx, olddy, olddz - xi,yi,zi = self._nodes[i].x, self._nodes[i].y, self._nodes[i].z - dx, dy,dz = xi - self._nodes[i-1].x, yi-self._nodes[i-1].y, zi-self._nodes[i-1].z - while true do - olddx, olddy, olddz = dx, dy, dz - if self._nodes[i+1] then - i = i+1 - xi, yi, zi = self._nodes[i].x, self._nodes[i].y, self._nodes[i].z - dx, dy, dz = xi - self._nodes[i-1].x, yi - self._nodes[i-1].y, zi - self._nodes[i-1].z - if olddx == dx and olddy == dy and olddz == dz then - t_remove(self._nodes, i-1) - i = i - 1 - end - else break end - end - return self - end - - return setmetatable(Path, - {__call = function(_,...) - return Path:new(...) - end - }) -end \ No newline at end of file diff --git a/sys/apis/jumper/core/utils.lua b/sys/apis/jumper/core/utils.lua deleted file mode 100644 index ab5f764..0000000 --- a/sys/apis/jumper/core/utils.lua +++ /dev/null @@ -1,57 +0,0 @@ --- Various utilities for Jumper top-level modules - -if (...) then - - -- Dependencies - local _PATH = (...):gsub('%.utils$','') - local Path = require (_PATH .. '.path') - - -- Local references - local pairs = pairs - local t_insert = table.insert - - -- Raw array items count - local function arraySize(t) - local count = 0 - for _ in pairs(t) do - count = count+1 - end - return count - end - - -- Extract a path from a given start/end position - local function traceBackPath(finder, node, startNode) - local path = Path:new() - path._grid = finder._grid - while true do - if node._parent then - t_insert(path._nodes,1,node) - node = node._parent - else - t_insert(path._nodes,1,startNode) - return path - end - end - end - - -- Lookup for value in a table - local indexOf = function(t,v) - for i = 1,#t do - if t[i] == v then return i end - end - return nil - end - - -- Is i out of range - local function outOfRange(i,low,up) - return (i< low or i > up) - end - - return { - arraySize = arraySize, - indexOf = indexOf, - outOfRange = outOfRange, - traceBackPath = traceBackPath - } - -end diff --git a/sys/apis/jumper/grid.lua b/sys/apis/jumper/grid.lua deleted file mode 100644 index 6cfc53a..0000000 --- a/sys/apis/jumper/grid.lua +++ /dev/null @@ -1,101 +0,0 @@ ---- The Grid class. --- Implementation of the `grid` class. --- The `grid` is a implicit graph which represents the 2D --- world map layout on which the `pathfinder` object will run. --- During a search, the `pathfinder` object needs to save some critical values. --- These values are cached within each `node` --- object, and the whole set of nodes are tight inside the `grid` object itself. - -if (...) then - - -- Dependencies - local _PATH = (...):gsub('%.grid$','') - - -- Local references - local Utils = require (_PATH .. '.core.utils') - local Node = require (_PATH .. '.core.node') - - -- Local references - local setmetatable = setmetatable - - -- Offsets for straights moves - local straightOffsets = { - {x = 1, y = 0, z = 0} --[[W]], {x = -1, y = 0, z = 0}, --[[E]] - {x = 0, y = 1, z = 0} --[[S]], {x = 0, y = -1, z = 0}, --[[N]] - {x = 0, y = 0, z = 1} --[[U]], {x = 0, y = -0, z = -1}, --[[D]] - } - - local Grid = {} - Grid.__index = Grid - - function Grid:new(dim) - local newGrid = { } - newGrid._min_x, newGrid._max_x = dim.x, dim.ex - newGrid._min_y, newGrid._max_y = dim.y, dim.ey - newGrid._min_z, newGrid._max_z = dim.z, dim.ez - newGrid._nodes = { } - newGrid._width = (newGrid._max_x-newGrid._min_x)+1 - newGrid._height = (newGrid._max_y-newGrid._min_y)+1 - newGrid._length = (newGrid._max_z-newGrid._min_z)+1 - return setmetatable(newGrid,Grid) - end - - function Grid:isWalkableAt(x, y, z) - local node = self:getNodeAt(x,y,z) - return node and node.walkable ~= 1 - end - - function Grid:getWidth() - return self._width - end - - function Grid:getHeight() - return self._height - end - - function Grid:getNodes() - return self._nodes - end - - function Grid:getBounds() - return self._min_x, self._min_y, self._min_z, self._max_x, self._max_y, self._max_z - end - - --- Returns neighbours. The returned value is an array of __walkable__ nodes neighbouring a given `node`. - -- @treturn {node,...} an array of nodes neighbouring a given node - function Grid:getNeighbours(node) - local neighbours = {} - for i = 1,#straightOffsets do - local n = self:getNodeAt( - node.x + straightOffsets[i].x, - node.y + straightOffsets[i].y, - node.z + straightOffsets[i].z - ) - if n and self:isWalkableAt(n.x, n.y, n.z) then - neighbours[#neighbours+1] = n - end - end - - return neighbours - end - - function Grid:getNodeAt(x,y,z) - if not x or not y or not z then return end - if Utils.outOfRange(x,self._min_x,self._max_x) then return end - if Utils.outOfRange(y,self._min_y,self._max_y) then return end - if Utils.outOfRange(z,self._min_z,self._max_z) then return end - - -- inefficient - if not self._nodes[y] then self._nodes[y] = {} end - if not self._nodes[y][x] then self._nodes[y][x] = {} end - if not self._nodes[y][x][z] then self._nodes[y][x][z] = Node:new(x,y,z) end - return self._nodes[y][x][z] - end - - return setmetatable(Grid,{ - __call = function(self,...) - return self:new(...) - end - }) - -end diff --git a/sys/apis/jumper/pathfinder.lua b/sys/apis/jumper/pathfinder.lua deleted file mode 100644 index 0ff2844..0000000 --- a/sys/apis/jumper/pathfinder.lua +++ /dev/null @@ -1,104 +0,0 @@ ---[[ - The following License applies to all files within the jumper directory. - - Note that this is only a partial copy of the full jumper code base. Also, - the code was modified to support 3D maps. ---]] - ---[[ -This work is under MIT-LICENSE -Copyright (c) 2012-2013 Roland Yonaba. - --- https://opensource.org/licenses/MIT - ---]] - -local _VERSION = "" -local _RELEASEDATE = "" - -if (...) then - - -- Dependencies - local _PATH = (...):gsub('%.pathfinder$','') - local Utils = require (_PATH .. '.core.utils') - - -- Internalization - local pairs = pairs - local assert = assert - local setmetatable = setmetatable - - --- Finders (search algorithms implemented). Refers to the search algorithms actually implemented in Jumper. - --
  • [A*](http://en.wikipedia.org/wiki/A*_search_algorithm)
  • - local Finders = { - ['ASTAR'] = require (_PATH .. '.search.astar'), - } - - -- Will keep track of all nodes expanded during the search - -- to easily reset their properties for the next pathfinding call - local toClear = {} - - -- Performs a traceback from the goal node to the start node - -- Only happens when the path was found - - local Pathfinder = {} - Pathfinder.__index = Pathfinder - - function Pathfinder:new(heuristic) - local newPathfinder = {} - setmetatable(newPathfinder, Pathfinder) - self._finder = Finders.ASTAR - self._heuristic = heuristic - return newPathfinder - end - - function Pathfinder:setGrid(grid) - self._grid = grid - return self - end - - --- Calculates a `path`. Returns the `path` from start to end location - -- Both locations must exist on the collision map. The starting location can be unwalkable. - -- @treturn path a path (array of nodes) when found, otherwise nil - -- @usage local path = myFinder:getPath(1,1,5,5) - function Pathfinder:getPath(startX, startY, startZ, ih, endX, endY, endZ, oh) - self:reset() - local startNode = self._grid:getNodeAt(startX, startY, startZ) - local endNode = self._grid:getNodeAt(endX, endY, endZ) - if not startNode or not endNode then - return nil - end - - startNode.heading = ih - endNode.heading = oh - - assert(startNode, ('Invalid location [%d, %d, %d]'):format(startX, startY, startZ)) - assert(endNode and self._grid:isWalkableAt(endX, endY, endZ), - ('Invalid or unreachable location [%d, %d, %d]'):format(endX, endY, endZ)) - local _endNode = self._finder(self, startNode, endNode, toClear) - if _endNode then - return Utils.traceBackPath(self, _endNode, startNode) - end - return nil - end - - --- Resets the `pathfinder`. This function is called internally between - -- successive pathfinding calls, so you should not - -- use it explicitely, unless under specific circumstances. - -- @class function - -- @treturn pathfinder self (the calling `pathfinder` itself, can be chained) - -- @usage local path, len = myFinder:getPath(1,1,5,5) - function Pathfinder:reset() - for node in pairs(toClear) do node:reset() end - toClear = {} - return self - end - - -- Returns Pathfinder class - Pathfinder._VERSION = _VERSION - Pathfinder._RELEASEDATE = _RELEASEDATE - return setmetatable(Pathfinder,{ - __call = function(self,...) - return self:new(...) - end - }) -end diff --git a/sys/apis/jumper/search/astar.lua b/sys/apis/jumper/search/astar.lua deleted file mode 100644 index c92dabc..0000000 --- a/sys/apis/jumper/search/astar.lua +++ /dev/null @@ -1,77 +0,0 @@ --- Astar algorithm --- This actual implementation of A-star is based on --- [Nash A. & al. pseudocode](http://aigamedev.com/open/tutorials/theta-star-any-angle-paths/) - -if (...) then - - -- Internalization - local huge = math.huge - - -- Dependancies - local _PATH = (...):match('(.+)%.search.astar$') - local Heap = require (_PATH.. '.core.bheap') - - -- Updates G-cost - local function computeCost(node, neighbour, heuristic) - local mCost, heading = heuristic(neighbour, node) -- Heuristics.EUCLIDIAN(neighbour, node) - - if node._g + mCost < neighbour._g then - neighbour._parent = node - neighbour._g = node._g + mCost - neighbour.heading = heading - end - end - - -- Updates vertex node-neighbour - local function updateVertex(openList, node, neighbour, endNode, heuristic) - local oldG = neighbour._g - computeCost(node, neighbour, heuristic) - if neighbour._g < oldG then - if neighbour._opened then neighbour._opened = false end - neighbour._h = heuristic(endNode, neighbour) - neighbour._f = neighbour._g + neighbour._h - openList:push(neighbour) - neighbour._opened = true - end - end - - -- Calculates a path. - -- Returns the path from location `` to location ``. - return function (finder, startNode, endNode, toClear) - local openList = Heap() - startNode._g = 0 - startNode._h = finder._heuristic(endNode, startNode) - startNode._f = startNode._g + startNode._h - openList:push(startNode) - toClear[startNode] = true - startNode._opened = true - - while not openList:empty() do - local node = openList:pop() - node._closed = true - if node == endNode then return node end - local neighbours = finder._grid:getNeighbours(node) - for i = 1,#neighbours do - local neighbour = neighbours[i] - if not neighbour._closed then - toClear[neighbour] = true - if not neighbour._opened then - neighbour._g = huge - neighbour._parent = nil - end - updateVertex(openList, node, neighbour, endNode, finder._heuristic) - end - end - - --[[ - printf('x:%d y:%d z:%d g:%d', node.x, node.y, node.z, node._g) - for i = 1,#neighbours do - local n = neighbours[i] - printf('x:%d y:%d z:%d f:%f g:%f h:%d', n.x, n.y, n.z, n._f, n._g, n.heading or -1) - end - --]] - - end - return nil - end -end diff --git a/sys/apis/logger.lua b/sys/apis/logger.lua deleted file mode 100644 index d693f4c..0000000 --- a/sys/apis/logger.lua +++ /dev/null @@ -1,133 +0,0 @@ -local Logger = { - fn = function() end, - filteredEvents = { }, -} - -function Logger.setLogger(fn) - Logger.fn = fn -end - -function Logger.disable() - Logger.setLogger(function() end) -end - -function Logger.setDaemonLogging() - Logger.setLogger(function (text) - os.queueEvent('log', { text = text }) - end) -end - -function Logger.setMonitorLogging() - local debugMon = device.monitor - - if not debugMon then - debugMon.setTextScale(.5) - debugMon.clear() - debugMon.setCursorPos(1, 1) - Logger.setLogger(function(text) - debugMon.write(text) - debugMon.scroll(-1) - debugMon.setCursorPos(1, 1) - end) - end -end - -function Logger.setScreenLogging() - Logger.setLogger(function(text) - local x, y = term.getCursorPos() - if x ~= 1 then - local sx, sy = term.getSize() - term.setCursorPos(1, sy) - --term.scroll(1) - end - print(text) - end) -end - -function Logger.setWirelessLogging() - if device.wireless_modem then - Logger.filter('modem_message') - Logger.filter('modem_receive') - Logger.filter('rednet_message') - Logger.setLogger(function(text) - device.wireless_modem.transmit(59998, os.getComputerID(), { - type = 'log', contents = text - }) - end) - Logger.debug('Logging enabled') - return true - end -end - -function Logger.setFileLogging(fileName) - fs.delete(fileName) - Logger.setLogger(function (text) - local logFile - - local mode = 'w' - if fs.exists(fileName) then - mode = 'a' - end - local file = io.open(fileName, mode) - if file then - file:write(text) - file:write('\n') - file:close() - end - end) -end - -function Logger.log(category, value, ...) - if Logger.filteredEvents[category] then - return - end - - if type(value) == 'table' then - local str - for k,v in pairs(value) do - if not str then - str = '{ ' - else - str = str .. ', ' - end - str = str .. k .. '=' .. tostring(v) - end - if str then - value = str .. ' }' - else - value = '{ }' - end - elseif type(value) == 'string' then - local args = { ... } - if #args > 0 then - value = string.format(value, unpack(args)) - end - else - value = tostring(value) - end - Logger.fn(category .. ': ' .. value) -end - -function Logger.debug(value, ...) - Logger.log('debug', value, ...) -end - -function Logger.logNestedTable(t, indent) - for _,v in ipairs(t) do - if type(v) == 'table' then - log('table') - logNestedTable(v) --, indent+1) - else - log(v) - end - end -end - -function Logger.filter( ...) - local events = { ... } - for _,event in pairs(events) do - Logger.filteredEvents[event] = true - end -end - -return Logger \ No newline at end of file diff --git a/sys/apis/packages.lua b/sys/apis/packages.lua deleted file mode 100644 index ae7375a..0000000 --- a/sys/apis/packages.lua +++ /dev/null @@ -1,60 +0,0 @@ -local Util = require('util') - -local fs = _G.fs -local textutils = _G.textutils - -local PACKAGE_DIR = 'packages' - -local Packages = { } - -function Packages:installed() - self.cache = { } - - if fs.exists(PACKAGE_DIR) then - for _, dir in pairs(fs.list(PACKAGE_DIR)) do - local path = fs.combine(fs.combine(PACKAGE_DIR, dir), '.package') - self.cache[dir] = Util.readTable(path) - end - end - - return self.cache -end - -function Packages:list() - if self.packageList then - return self.packageList - end - self.packageList = Util.readTable('usr/config/packages') or { } - - return self.packageList -end - -function Packages:isInstalled(package) - return self:installed()[package] -end - -function Packages:getManifest(package) - local fname = 'packages/' .. package .. '/.package' - if fs.exists(fname) then - local c = Util.readTable(fname) - if c then - c.repository = c.repository:gsub('{{OPUS_BRANCH}}', _G.OPUS_BRANCH) - return c - end - end - local list = self:list() - local url = list and list[package] - - if url then - local c = Util.httpGet(url) - if c then - c = textutils.unserialize(c) - if c then - c.repository = c.repository:gsub('{{OPUS_BRANCH}}', _G.OPUS_BRANCH) - return c - end - end - end -end - -return Packages diff --git a/sys/apis/peripheral.lua b/sys/apis/peripheral.lua deleted file mode 100644 index 33a1df1..0000000 --- a/sys/apis/peripheral.lua +++ /dev/null @@ -1,242 +0,0 @@ -local Event = require('event') -local Socket = require('socket') -local Util = require('util') - -local os = _G.os - -local Peripheral = Util.shallowCopy(_G.peripheral) - -function Peripheral.getList() - if _G.device then - return _G.device - end - - local deviceList = { } - for _,side in pairs(Peripheral.getNames()) do - Peripheral.addDevice(deviceList, side) - end - - return deviceList -end - -function Peripheral.addDevice(deviceList, side) - local name = side - local ptype = Peripheral.getType(side) - - if not ptype then - return - end - - if ptype == 'modem' then - if not Peripheral.call(name, 'isWireless') then --- ptype = 'wireless_modem' --- else - ptype = 'wired_modem' - end - end - - local sides = { - front = true, - back = true, - top = true, - bottom = true, - left = true, - right = true - } - - if sides[name] then - local i = 1 - local uniqueName = ptype - while deviceList[uniqueName] do - uniqueName = ptype .. '_' .. i - i = i + 1 - end - name = uniqueName - end - - -- this can randomly fail - if not deviceList[name] then - pcall(function() - deviceList[name] = Peripheral.wrap(side) - end) - - if deviceList[name] then - Util.merge(deviceList[name], { - name = name, - type = ptype, - side = side, - }) - end - end - - return deviceList[name] -end - -function Peripheral.getBySide(side) - return Util.find(Peripheral.getList(), 'side', side) -end - -function Peripheral.getByType(typeName) - return Util.find(Peripheral.getList(), 'type', typeName) -end - -function Peripheral.getByMethod(method) - for _,p in pairs(Peripheral.getList()) do - if p[method] then - return p - end - end -end - --- match any of the passed arguments -function Peripheral.get(args) - - if type(args) == 'string' then - args = { type = args } - end - - if args.name then - return _G.device[args.name] - end - - if args.type then - local p = Peripheral.getByType(args.type) - if p then - return p - end - end - - if args.method then - local p = Peripheral.getByMethod(args.method) - if p then - return p - end - end - - if args.side then - local p = Peripheral.getBySide(args.side) - if p then - return p - end - end -end - -local function getProxy(pi) - local socket, msg = Socket.connect(pi.host, 189) - - if not socket then - error("Timed out attaching peripheral: " .. pi.uri .. '\n' .. msg) - end - - -- write the uri of the periperal we are requesting... - -- ie. type/monitor - socket:write(pi.path) - local proxy = socket:read(3) - - if not proxy then - error("Timed out attaching peripheral: " .. pi.uri) - end - - if type(proxy) == 'string' then - error(proxy) - end - - local methods = proxy.methods - proxy.methods = nil - - for _,method in pairs(methods) do - proxy[method] = function(...) - socket:write({ fn = method, args = { ... } }) - local resp = socket:read() - if not resp then - error("Timed out communicating with peripheral: " .. pi.uri) - end - return table.unpack(resp) - end - end - - if proxy.blit then - local methods = { 'clear', 'clearLine', 'setCursorPos', 'write', 'blit', - 'setTextColor', 'setTextColour', 'setBackgroundColor', - 'setBackgroundColour', 'scroll', 'setCursorBlink', } - local queue = nil - - for _,method in pairs(methods) do - proxy[method] = function(...) - if not queue then - queue = { } - Event.onTimeout(0, function() - if not socket:write({ fn = 'fastBlit', args = { queue } }) then - error("Timed out communicating with peripheral: " .. pi.uri) - end - queue = nil - socket:read() - end) - end - if not socket.connected then - error("Timed out communicating with peripheral: " .. pi.uri) - end - - table.insert(queue, { - fn = method, - args = { ... }, - }) - end - end - end - - if proxy.type == 'monitor' then - Event.addRoutine(function() - while true do - local data = socket:read() - if not data then - break - end - if data.fn and data.fn == 'event' then - os.queueEvent(table.unpack(data.data)) - end - end - end) - end - - return proxy -end - ---[[ - Parse a uri into it's components - - Examples: - monitor = { name = 'monitor' } - side/top = { side = 'top' } - method/list = { method = 'list' } - 12://name/monitor = { host = 12, name = 'monitor' } -]]-- -local function parse(uri) - local pi = Util.split(uri:gsub('^%d*://', ''), '(.-)/') - - if #pi == 1 then - pi = { - 'name', - pi[1], - } - end - - return { - host = uri:match('^(%d*)%:'), -- 12 - uri = uri, -- 12://name/monitor - path = uri:gsub('^%d*://', ''), -- name/monitor - [ pi[1] ] = pi[2], -- name = 'monitor' - } -end - -function Peripheral.lookup(uri) - local pi = parse(uri) - - if pi.host and _G.device.wireless_modem then - return getProxy(pi) - end - - return Peripheral.get(pi) -end - -return Peripheral diff --git a/sys/apis/rttp.lua b/sys/apis/rttp.lua deleted file mode 100644 index 8696e9e..0000000 --- a/sys/apis/rttp.lua +++ /dev/null @@ -1,95 +0,0 @@ -local device = _G.device -local os = _G.os - -local rttp = { } -local computerId = os.getComputerID() - -local function parse(url, default) - -- initialize default parameters - local parsed = {} - local authority - - for i,v in pairs(default or parsed) do parsed[i] = v end - -- remove whitespace - -- url = string.gsub(url, "%s", "") - -- Decode unreserved characters - url = string.gsub(url, "%%(%x%x)", function(hex) - local char = string.char(tonumber(hex, 16)) - if string.match(char, "[a-zA-Z0-9._~-]") then - return char - end - -- Hex encodings that are not unreserved must be preserved. - return nil - end) - -- get fragment - url = string.gsub(url, "#(.*)$", function(f) - parsed.fragment = f - return "" - end) - -- get scheme. Lower-case according to RFC 3986 section 3.1. - url = string.gsub(url, "^(%w[%w.+-]*):", - function(s) parsed.scheme = string.lower(s); return "" end) - -- get authority - url = string.gsub(url, "^//([^/]*)", function(n) - authority = n - return "" - end) - -- get query stringing - url = string.gsub(url, "%?(.*)", function(q) - parsed.query = q - return "" - end) - -- get params - url = string.gsub(url, "%;(.*)", function(p) - parsed.params = p - return "" - end) - - -- path is whatever was left - parsed.path = url - - -- Represents host:port, port = nil if not used. - if authority then - authority = string.gsub(authority, ":(%d+)$", - function(p) parsed.port = tonumber(p); return "" end) - if authority ~= "" then - parsed.host = authority - end - end - return parsed -end - -function rttp.get(url) - local modem = device.wireless_modem or error('Modem not found') - local parsed = parse(url, { port = 80 }) - - parsed.host = tonumber(parsed.host) or error('Invalid url') - - for i = 16384, 32767 do - if not modem.isOpen(i) then - modem.open(i) - local path = parsed.query and parsed.path .. '?' .. parsed.query or parsed.path - - modem.transmit(parsed.port, parsed.host, { - method = 'GET', - replyAddress = computerId, - replyPort = i, - path = path, - }) - local timerId = os.startTimer(3) - repeat - local event, id, dport, dhost, response = os.pullEvent() - if event == 'modem_message' and - dport == i and - dhost == computerId and - type(response) == 'table' then - modem.close(i) - return true, response - end - until event == 'timer' and id == timerId - return false, 'timeout' - end - end -end - -return rttp diff --git a/sys/apis/security.lua b/sys/apis/security.lua deleted file mode 100644 index 24d24d4..0000000 --- a/sys/apis/security.lua +++ /dev/null @@ -1,60 +0,0 @@ -local Config = require('config') - -local config = { } - -local Security = { } - -function Security.verifyPassword(password) - Config.load('os', config) - return config.password and password == config.password -end - -function Security.hasPassword() - return not not config.password -end - -function Security.getSecretKey() - Config.load('os', config) - if not config.secretKey then - config.secretKey = math.random(100000, 999999) - Config.update('os', config) - end - return config.secretKey -end - -function Security.getPublicKey() - - local exchange = { - base = 11, - primeMod = 625210769 - } - - local function modexp(base, exponent, modulo) - local remainder = base - - for _ = 1, exponent-1 do - remainder = remainder * remainder - if remainder >= modulo then - remainder = remainder % modulo - end - end - - return remainder - end - - local secretKey = Security.getSecretKey() - return modexp(exchange.base, secretKey, exchange.primeMod) -end - -function Security.updatePassword(password) - Config.load('os', config) - config.password = password - Config.update('os', config) -end - -function Security.getPassword() - Config.load('os', config) - return config.password -end - -return Security diff --git a/sys/apis/sha1.lua b/sys/apis/sha1.lua deleted file mode 100644 index 0be5024..0000000 --- a/sys/apis/sha1.lua +++ /dev/null @@ -1,280 +0,0 @@ -local sha1 = { - _VERSION = "sha.lua 0.5.0", - _URL = "https://github.com/kikito/sha.lua", - _DESCRIPTION = [[ - SHA-1 secure hash computation, and HMAC-SHA1 signature computation in Lua (5.1) - Based on code originally by Jeffrey Friedl (http://regex.info/blog/lua/sha1) - And modified by Eike Decker - (http://cube3d.de/uploads/Main/sha1.txt) - ]], - _LICENSE = [[ - MIT LICENSE - - Copyright (c) 2013 Enrique Garcia Cota + Eike Decker + Jeffrey Friedl - - https://opensource.org/licenses/MIT - ]] -} - ------------------------------------------------------------------------------------ - --- loading this file (takes a while but grants a boost of factor 13) -local PRELOAD_CACHE = false - -local BLOCK_SIZE = 64 -- 512 bits - --- local storing of global functions (minor speedup) -local floor,modf = math.floor,math.modf -local char,format,rep = string.char,string.format,string.rep - --- merge 4 bytes to an 32 bit word -local function bytes_to_w32(a,b,c,d) return a*0x1000000+b*0x10000+c*0x100+d end --- split a 32 bit word into four 8 bit numbers -local function w32_to_bytes(i) - return floor(i/0x1000000)%0x100,floor(i/0x10000)%0x100,floor(i/0x100)%0x100,i%0x100 -end - --- shift the bits of a 32 bit word. Don't use negative values for "bits" -local function w32_rot(bits,a) - local b2 = 2^(32-bits) - local a,b = modf(a/b2) - return a+b*b2*(2^(bits)) -end - --- caching function for functions that accept 2 arguments, both of values between --- 0 and 255. The function to be cached is passed, all values are calculated --- during loading and a function is returned that returns the cached values (only) -local function cache2arg(fn) - if not PRELOAD_CACHE then return fn end - local lut = {} - for i=0,0xffff do - local a,b = floor(i/0x100),i%0x100 - lut[i] = fn(a,b) - end - return function(a,b) - return lut[a*0x100+b] - end -end - --- splits an 8-bit number into 8 bits, returning all 8 bits as booleans -local function byte_to_bits(b) - local b = function(n) - local b = floor(b/n) - return b%2==1 - end - return b(1),b(2),b(4),b(8),b(16),b(32),b(64),b(128) -end - --- builds an 8bit number from 8 booleans -local function bits_to_byte(a,b,c,d,e,f,g,h) - local function n(b,x) return b and x or 0 end - return n(a,1)+n(b,2)+n(c,4)+n(d,8)+n(e,16)+n(f,32)+n(g,64)+n(h,128) -end - --- bitwise "and" function for 2 8bit number -local band = cache2arg (function(a,b) - local A,B,C,D,E,F,G,H = byte_to_bits(b) - local a,b,c,d,e,f,g,h = byte_to_bits(a) - return bits_to_byte( - A and a, B and b, C and c, D and d, - E and e, F and f, G and g, H and h) -end) - --- bitwise "or" function for 2 8bit numbers -local bor = cache2arg(function(a,b) - local A,B,C,D,E,F,G,H = byte_to_bits(b) - local a,b,c,d,e,f,g,h = byte_to_bits(a) - return bits_to_byte( - A or a, B or b, C or c, D or d, - E or e, F or f, G or g, H or h) -end) - --- bitwise "xor" function for 2 8bit numbers -local bxor = cache2arg(function(a,b) - local A,B,C,D,E,F,G,H = byte_to_bits(b) - local a,b,c,d,e,f,g,h = byte_to_bits(a) - return bits_to_byte( - A ~= a, B ~= b, C ~= c, D ~= d, - E ~= e, F ~= f, G ~= g, H ~= h) -end) - --- bitwise complement for one 8bit number -local function bnot(x) - return 255-(x % 256) -end - --- creates a function to combine to 32bit numbers using an 8bit combination function -local function w32_comb(fn) - return function(a,b) - local aa,ab,ac,ad = w32_to_bytes(a) - local ba,bb,bc,bd = w32_to_bytes(b) - return bytes_to_w32(fn(aa,ba),fn(ab,bb),fn(ac,bc),fn(ad,bd)) - end -end - --- create functions for and, xor and or, all for 2 32bit numbers -local w32_and = w32_comb(band) -local w32_xor = w32_comb(bxor) -local w32_or = w32_comb(bor) - --- xor function that may receive a variable number of arguments -local function w32_xor_n(a,...) - local aa,ab,ac,ad = w32_to_bytes(a) - for i=1,select('#',...) do - local ba,bb,bc,bd = w32_to_bytes(select(i,...)) - aa,ab,ac,ad = bxor(aa,ba),bxor(ab,bb),bxor(ac,bc),bxor(ad,bd) - end - return bytes_to_w32(aa,ab,ac,ad) -end - --- combining 3 32bit numbers through binary "or" operation -local function w32_or3(a,b,c) - local aa,ab,ac,ad = w32_to_bytes(a) - local ba,bb,bc,bd = w32_to_bytes(b) - local ca,cb,cc,cd = w32_to_bytes(c) - return bytes_to_w32( - bor(aa,bor(ba,ca)), bor(ab,bor(bb,cb)), bor(ac,bor(bc,cc)), bor(ad,bor(bd,cd)) - ) -end - --- binary complement for 32bit numbers -local function w32_not(a) - return 4294967295-(a % 4294967296) -end - --- adding 2 32bit numbers, cutting off the remainder on 33th bit -local function w32_add(a,b) return (a+b) % 4294967296 end - --- adding n 32bit numbers, cutting off the remainder (again) -local function w32_add_n(a,...) - for i=1,select('#',...) do - a = (a+select(i,...)) % 4294967296 - end - return a -end --- converting the number to a hexadecimal string -local function w32_to_hexstring(w) return format("%08x",w) end - -local function hex_to_binary(hex) - return hex:gsub('..', function(hexval) - return string.char(tonumber(hexval, 16)) - end) -end - --- building the lookuptables ahead of time (instead of littering the source code --- with precalculated values) -local xor_with_0x5c = {} -local xor_with_0x36 = {} -for i=0,0xff do - xor_with_0x5c[char(i)] = char(bxor(i,0x5c)) - xor_with_0x36[char(i)] = char(bxor(i,0x36)) -end - ------------------------------------------------------------------------------ - --- calculating the SHA1 for some text -function sha1.sha1(msg) - local H0,H1,H2,H3,H4 = 0x67452301,0xEFCDAB89,0x98BADCFE,0x10325476,0xC3D2E1F0 - local msg_len_in_bits = #msg * 8 - - local first_append = char(0x80) -- append a '1' bit plus seven '0' bits - - local non_zero_message_bytes = #msg +1 +8 -- the +1 is the appended bit 1, the +8 are for the final appended length - local current_mod = non_zero_message_bytes % 64 - local second_append = current_mod>0 and rep(char(0), 64 - current_mod) or "" - - -- now to append the length as a 64-bit number. - local B1, R1 = modf(msg_len_in_bits / 0x01000000) - local B2, R2 = modf( 0x01000000 * R1 / 0x00010000) - local B3, R3 = modf( 0x00010000 * R2 / 0x00000100) - local B4 = 0x00000100 * R3 - - local L64 = char( 0) .. char( 0) .. char( 0) .. char( 0) -- high 32 bits - .. char(B1) .. char(B2) .. char(B3) .. char(B4) -- low 32 bits - - msg = msg .. first_append .. second_append .. L64 - - assert(#msg % 64 == 0) - - local chunks = #msg / 64 - - local W = { } - local start, A, B, C, D, E, f, K, TEMP - local chunk = 0 - - while chunk < chunks do - -- - -- break chunk up into W[0] through W[15] - -- - start,chunk = chunk * 64 + 1,chunk + 1 - - for t = 0, 15 do - W[t] = bytes_to_w32(msg:byte(start, start + 3)) - start = start + 4 - end - - -- - -- build W[16] through W[79] - -- - for t = 16, 79 do - -- For t = 16 to 79 let Wt = S1(Wt-3 XOR Wt-8 XOR Wt-14 XOR Wt-16). - W[t] = w32_rot(1, w32_xor_n(W[t-3], W[t-8], W[t-14], W[t-16])) - end - - A,B,C,D,E = H0,H1,H2,H3,H4 - - for t = 0, 79 do - if t <= 19 then - -- (B AND C) OR ((NOT B) AND D) - f = w32_or(w32_and(B, C), w32_and(w32_not(B), D)) - K = 0x5A827999 - elseif t <= 39 then - -- B XOR C XOR D - f = w32_xor_n(B, C, D) - K = 0x6ED9EBA1 - elseif t <= 59 then - -- (B AND C) OR (B AND D) OR (C AND D - f = w32_or3(w32_and(B, C), w32_and(B, D), w32_and(C, D)) - K = 0x8F1BBCDC - else - -- B XOR C XOR D - f = w32_xor_n(B, C, D) - K = 0xCA62C1D6 - end - - -- TEMP = S5(A) + ft(B,C,D) + E + Wt + Kt; - A,B,C,D,E = w32_add_n(w32_rot(5, A), f, E, W[t], K), - A, w32_rot(30, B), C, D - end - -- Let H0 = H0 + A, H1 = H1 + B, H2 = H2 + C, H3 = H3 + D, H4 = H4 + E. - H0,H1,H2,H3,H4 = w32_add(H0, A),w32_add(H1, B),w32_add(H2, C),w32_add(H3, D),w32_add(H4, E) - end - local f = w32_to_hexstring - return f(H0) .. f(H1) .. f(H2) .. f(H3) .. f(H4) -end - - -function sha1.binary(msg) - return hex_to_binary(sha1.sha1(msg)) -end - -function sha1.hmac(key, text) - assert(type(key) == 'string', "key passed to sha1.hmac should be a string") - assert(type(text) == 'string', "text passed to sha1.hmac should be a string") - - if #key > BLOCK_SIZE then - key = sha1.binary(key) - end - - local key_xord_with_0x36 = key:gsub('.', xor_with_0x36) .. string.rep(string.char(0x36), BLOCK_SIZE - #key) - local key_xord_with_0x5c = key:gsub('.', xor_with_0x5c) .. string.rep(string.char(0x5c), BLOCK_SIZE - #key) - - return sha1.sha1(key_xord_with_0x5c .. sha1.binary(key_xord_with_0x36 .. text)) -end - -function sha1.hmac_binary(key, text) - return hex_to_binary(sha1.hmac(key, text)) -end - -setmetatable(sha1, {__call = function(_,msg) return sha1.sha1(msg) end }) - -return sha1 \ No newline at end of file diff --git a/sys/apis/socket.lua b/sys/apis/socket.lua deleted file mode 100644 index 7926234..0000000 --- a/sys/apis/socket.lua +++ /dev/null @@ -1,222 +0,0 @@ -local Crypto = require('crypto') -local Logger = require('logger') -local Security = require('security') -local Util = require('util') - -local device = _G.device -local os = _G.os - -local socketClass = { } - -function socketClass:read(timeout) - local data, distance = _G.transport.read(self) - if data then - return data, distance - end - - if not self.connected then - Logger.log('socket', 'read: No connection') - return - end - - local timerId = os.startTimer(timeout or 5) - - while true do - local e, id = os.pullEvent() - - if e == 'transport_' .. self.uid then - data, distance = _G.transport.read(self) - if data then - os.cancelTimer(timerId) - return data, distance - end - if not self.connected then - break - end - - elseif e == 'timer' and id == timerId then - if timeout or not self.connected then - break - end - timerId = os.startTimer(5) - self:ping() - end - end -end - -function socketClass:write(data) - if self.connected then - _G.transport.write(self, { - type = 'DATA', - seq = self.wseq, - data = data, - }) - return true - end -end - -function socketClass:ping() - if self.connected then - _G.transport.ping(self) - return true - end -end - -function socketClass:close() - if self.connected then - Logger.log('socket', 'closing socket ' .. self.sport) - self.transmit(self.dport, self.dhost, { - type = 'DISC', - }) - self.connected = false - end - device.wireless_modem.close(self.sport) - _G.transport.close(self) -end - -local Socket = { } - -local function loopback(port, sport, msg) - os.queueEvent('modem_message', 'loopback', port, sport, msg, 0) -end - -local function newSocket(isLoopback) - for _ = 16384, 32767 do - local i = math.random(16384, 32767) - if not device.wireless_modem.isOpen(i) then - local socket = { - shost = os.getComputerID(), - sport = i, - transmit = device.wireless_modem.transmit, - wseq = math.random(100, 100000), - rseq = math.random(100, 100000), - timers = { }, - messages = { }, - } - setmetatable(socket, { __index = socketClass }) - - device.wireless_modem.open(socket.sport) - - if isLoopback then - socket.transmit = loopback - end - return socket - end - end - error('No ports available') -end - -function Socket.connect(host, port) - if not device.wireless_modem then - return false, 'Wireless modem not found' - end - - local socket = newSocket(host == os.getComputerID()) - socket.dhost = tonumber(host) - Logger.log('socket', 'connecting to ' .. port) - - socket.transmit(port, socket.sport, { - type = 'OPEN', - shost = socket.shost, - dhost = socket.dhost, - t = Crypto.encrypt({ ts = os.time(), seq = socket.seq }, Security.getPublicKey()), - rseq = socket.wseq, - wseq = socket.rseq, - }) - - local timerId = os.startTimer(3) - repeat - local e, id, sport, dport, msg = os.pullEvent() - if e == 'modem_message' and - sport == socket.sport and - type(msg) == 'table' and - msg.dhost == socket.shost then - - os.cancelTimer(timerId) - - if msg.type == 'CONN' then - - socket.dport = dport - socket.connected = true - Logger.log('socket', 'connection established to %d %d->%d', - host, socket.sport, socket.dport) - - _G.transport.open(socket) - - return socket - elseif msg.type == 'REJE' then - return false, 'Password not set on target or not trusted' - end - end - until e == 'timer' and id == timerId - - socket:close() - - return false, 'Connection timed out' -end - -local function trusted(msg, port) - if port == 19 or msg.shost == os.getComputerID() then - -- no auth for trust server or loopback - return true - end - - if not Security.hasPassword() then - -- no password has been set on this computer - --return true - end - - local trustList = Util.readTable('usr/.known_hosts') or { } - local pubKey = trustList[msg.shost] - - if pubKey then - local data = Crypto.decrypt(msg.t or '', pubKey) - - --local sharedKey = modexp(pubKey, exchange.secretKey, public.primeMod) - return data.ts and tonumber(data.ts) and math.abs(os.time() - data.ts) < 24 - end -end - -function Socket.server(port) - device.wireless_modem.open(port) - Logger.log('socket', 'Waiting for connections on port ' .. port) - - while true do - local _, _, sport, dport, msg = os.pullEvent('modem_message') - - if sport == port and - msg and - type(msg) == 'table' and - msg.dhost == os.getComputerID() and - msg.type == 'OPEN' then - - local socket = newSocket(msg.shost == os.getComputerID()) - socket.dport = dport - socket.dhost = msg.shost - socket.wseq = msg.wseq - socket.rseq = msg.rseq - - if trusted(msg, port) then - socket.connected = true - socket.transmit(socket.dport, socket.sport, { - type = 'CONN', - dhost = socket.dhost, - shost = socket.shost, - }) - Logger.log('socket', 'Connection established %d->%d', socket.sport, socket.dport) - - _G.transport.open(socket) - return socket - end - - socket.transmit(socket.dport, socket.sport, { - type = 'REJE', - dhost = socket.dhost, - shost = socket.shost, - }) - socket:close() - end - end -end - -return Socket diff --git a/sys/apis/sync.lua b/sys/apis/sync.lua deleted file mode 100644 index 7987cb4..0000000 --- a/sys/apis/sync.lua +++ /dev/null @@ -1,61 +0,0 @@ -local Sync = { - syncLocks = { } -} - -local os = _G.os - -function Sync.sync(obj, fn) - local key = tostring(obj) - if Sync.syncLocks[key] then - local cos = tostring(coroutine.running()) - table.insert(Sync.syncLocks[key], cos) - repeat - local _, co = os.pullEvent('sync_lock') - until co == cos - else - Sync.syncLocks[key] = { } - end - local s, m = pcall(fn) - local co = table.remove(Sync.syncLocks[key], 1) - if co then - os.queueEvent('sync_lock', co) - else - Sync.syncLocks[key] = nil - end - if not s then - error(m) - end -end - -function Sync.lock(obj) - local key = tostring(obj) - if Sync.syncLocks[key] then - local cos = tostring(coroutine.running()) - table.insert(Sync.syncLocks[key], cos) - repeat - local _, co = os.pullEvent('sync_lock') - until co == cos - else - Sync.syncLocks[key] = { } - end -end - -function Sync.release(obj) - local key = tostring(obj) - if not Sync.syncLocks[key] then - error('Sync.release: Lock was not obtained', 2) - end - local co = table.remove(Sync.syncLocks[key], 1) - if co then - os.queueEvent('sync_lock', co) - else - Sync.syncLocks[key] = nil - end -end - -function Sync.isLocked(obj) - local key = tostring(obj) - return not not Sync.syncLocks[key] -end - -return Sync diff --git a/sys/apis/terminal.lua b/sys/apis/terminal.lua deleted file mode 100644 index fbc860c..0000000 --- a/sys/apis/terminal.lua +++ /dev/null @@ -1,335 +0,0 @@ -local colors = _G.colors -local term = _G.term -local _gsub = string.gsub -local _rep = string.rep -local _sub = string.sub - -local Terminal = { } - --- add scrolling functions to a window -function Terminal.scrollable(win, maxScroll) - local lines = { } - local scrollPos = 0 - local oblit, oreposition = win.blit, win.reposition - - local palette = { } - for n = 1, 16 do - palette[2 ^ (n - 1)] = _sub("0123456789abcdef", n, n) - end - - maxScroll = maxScroll or 100 - - -- should only do if window is visible... - local function redraw() - local _, h = win.getSize() - local x, y = win.getCursorPos() - for i = 1, h do - local line = lines[i + scrollPos] - if line and line.dirty then - win.setCursorPos(1, i) - oblit(line.text, line.fg, line.bg) - line.dirty = false - end - end - win.setCursorPos(x, y) - end - - local function scrollTo(p, forceRedraw) - local _, h = win.getSize() - local ms = #lines - h -- max scroll - p = math.min(math.max(p, 0), ms) -- normalize - - if p ~= scrollPos or forceRedraw then - scrollPos = p - for _, line in pairs(lines) do - line.dirty = true - end - end - end - - function win.write(text) - local _, h = win.getSize() - - text = tostring(text) or '' - scrollTo(#lines - h) - win.blit(text, - _rep(palette[win.getTextColor()], #text), - _rep(palette[win.getBackgroundColor()], #text)) - local x, y = win.getCursorPos() - win.setCursorPos(x + #text, y) - end - - function win.clearLine() - local w, h = win.getSize() - local _, y = win.getCursorPos() - - scrollTo(#lines - h) - lines[y + scrollPos] = { - text = _rep(' ', w), - fg = _rep(palette[win.getTextColor()], w), - bg = _rep(palette[win.getBackgroundColor()], w), - dirty = true, - } - redraw() - end - - function win.blit(text, fg, bg) - local x, y = win.getCursorPos() - local w, h = win.getSize() - - if y > 0 and y <= h and x <= w then - local width = #text - - -- fix ffs - if x < 1 then - text = _sub(text, 2 - x) - if bg then - bg = _sub(bg, 2 - x) - end - if bg then - fg = _sub(fg, 2 - x) - end - width = width + x - 1 - x = 1 - end - - if x + width - 1 > w then - text = _sub(text, 1, w - x + 1) - if bg then - bg = _sub(bg, 1, w - x + 1) - end - if bg then - fg = _sub(fg, 1, w - x + 1) - end - width = #text - end - - if width > 0 then - local function replace(sstr, pos, rstr) - if pos == 1 and width == w then - return rstr - elseif pos == 1 then - return rstr .. _sub(sstr, pos+width) - elseif pos + width > w then - return _sub(sstr, 1, pos-1) .. rstr - end - return _sub(sstr, 1, pos-1) .. rstr .. _sub(sstr, pos+width) - end - - local line = lines[y + scrollPos] - line.dirty = true - line.text = replace(line.text, x, text, width) - if fg then - line.fg = replace(line.fg, x, fg, width) - end - if bg then - line.bg = replace(line.bg, x, bg, width) - end - end - end - redraw() - end - - function win.clear() - local w, h = win.getSize() - - local text = _rep(' ', w) - local fg = _rep(palette[win.getTextColor()], w) - local bg = _rep(palette[win.getBackgroundColor()], w) - lines = { } - for y = 1, h do - lines[y] = { - dirty = true, - text = text, - fg = fg, - bg = bg, - } - end - scrollPos = 0 - redraw() - end - - -- doesn't support negative scrolling... - function win.scroll(n) - local w = win.getSize() - - for _ = 1, n do - lines[#lines + 1] = { - text = _rep(' ', w), - fg = _rep(palette[win.getTextColor()], w), - bg = _rep(palette[win.getBackgroundColor()], w), - } - end - - while #lines > maxScroll do - table.remove(lines, 1) - end - - scrollTo(maxScroll, true) - redraw() - end - - function win.scrollUp() - scrollTo(scrollPos - 1) - redraw() - end - - function win.scrollDown() - scrollTo(scrollPos + 1) - redraw() - end - - function win.reposition(x, y, nw, nh) - local w, h = win.getSize() - local D = (nh or h) - h - - if D > 0 then - for _ = 1, D do - lines[#lines + 1] = { - text = _rep(' ', w), - fg = _rep(palette[win.getTextColor()], w), - bg = _rep(palette[win.getBackgroundColor()], w), - } - end - elseif D < 0 then - for _ = D, -1 do - lines[#lines] = nil - end - end - return oreposition(x, y, nw, nh) - end - - win.clear() -end - --- get windows contents -function Terminal.getContents(win, parent) - local oblit, oscp = parent.blit, parent.setCursorPos - local lines = { } - - parent.blit = function(text, fg, bg) - lines[#lines + 1] = { - text = text, - fg = fg, - bg = bg, - } - end - parent.setCursorPos = function() end - - win.setVisible(true) - win.redraw() - - parent.blit = oblit - parent.setCursorPos = oscp - - return lines -end - -function Terminal.toGrayscale(ct) - local scolors = { - [ colors.white ] = colors.white, - [ colors.orange ] = colors.lightGray, - [ colors.magenta ] = colors.lightGray, - [ colors.lightBlue ] = colors.lightGray, - [ colors.yellow ] = colors.lightGray, - [ colors.lime ] = colors.lightGray, - [ colors.pink ] = colors.lightGray, - [ colors.gray ] = colors.gray, - [ colors.lightGray ] = colors.lightGray, - [ colors.cyan ] = colors.lightGray, - [ colors.purple ] = colors.gray, - [ colors.blue ] = colors.gray, - [ colors.brown ] = colors.gray, - [ colors.green ] = colors.lightGray, - [ colors.red ] = colors.gray, - [ colors.black ] = colors.black, - } - - local methods = { 'setBackgroundColor', 'setBackgroundColour', - 'setTextColor', 'setTextColour' } - for _,v in pairs(methods) do - local fn = ct[v] - ct[v] = function(c) - fn(scolors[c]) - end - end - - local bcolors = { - [ '1' ] = '8', - [ '2' ] = '8', - [ '3' ] = '8', - [ '4' ] = '8', - [ '5' ] = '8', - [ '6' ] = '8', - [ '9' ] = '8', - [ 'a' ] = '7', - [ 'b' ] = '7', - [ 'c' ] = '7', - [ 'd' ] = '8', - [ 'e' ] = '7', - } - - local function translate(s) - if s then - s = _gsub(s, "%w", bcolors) - end - return s - end - - local fn = ct.blit - ct.blit = function(text, fg, bg) - fn(text, translate(fg), translate(bg)) - end -end - -function Terminal.getNullTerm(ct) - local nt = Terminal.copy(ct) - - local methods = { 'blit', 'clear', 'clearLine', 'scroll', - 'setCursorBlink', 'setCursorPos', 'write' } - for _,v in pairs(methods) do - nt[v] = function() end - end - - return nt -end - -function Terminal.copy(it, ot) - ot = ot or { } - for k,v in pairs(it) do - if type(v) == 'function' then - ot[k] = v - end - end - return ot -end - -function Terminal.mirror(ct, dt) - for k,f in pairs(ct) do - ct[k] = function(...) - local ret = { f(...) } - if dt[k] then - dt[k](...) - end - return table.unpack(ret) - end - end -end - -function Terminal.readPassword(prompt) - if prompt then - term.write(prompt) - end - local fn = term.current().write - term.current().write = function() end - local s - pcall(function() s = _G.read(prompt) end) - term.current().write = fn - - if s == '' then - return - end - return s -end - -return Terminal diff --git a/sys/apis/turtle/pathfind.lua b/sys/apis/turtle/pathfind.lua deleted file mode 100644 index ce0fa69..0000000 --- a/sys/apis/turtle/pathfind.lua +++ /dev/null @@ -1,235 +0,0 @@ -local Grid = require('jumper.grid') -local Pathfinder = require('jumper.pathfinder') -local Point = require('point') -local Util = require('util') - -local turtle = _G.turtle - -local function addBlock(grid, b, dim) - if Point.inBox(b, dim) then - local node = grid:getNodeAt(b.x, b.y, b.z) - if node then - node.walkable = 1 - end - end -end - --- map shrinks/grows depending upon blocks encountered --- the map will encompass any blocks encountered, the turtle position, and the destination -local function mapDimensions(dest, blocks, boundingBox, dests) - local box = Point.makeBox(turtle.point, turtle.point) - - Point.expandBox(box, dest) - - for _,d in pairs(dests) do - Point.expandBox(box, d) - end - - for _,b in pairs(blocks) do - Point.expandBox(box, b) - end - - -- expand one block out in all directions - if boundingBox then - box.x = math.max(box.x - 1, boundingBox.x) - box.z = math.max(box.z - 1, boundingBox.z) - box.y = math.max(box.y - 1, boundingBox.y) - box.ex = math.min(box.ex + 1, boundingBox.ex) - box.ez = math.min(box.ez + 1, boundingBox.ez) - box.ey = math.min(box.ey + 1, boundingBox.ey) - else - box.x = box.x - 1 - box.z = box.z - 1 - box.y = box.y - 1 - box.ex = box.ex + 1 - box.ez = box.ez + 1 - box.ey = box.ey + 1 - end - - return box -end - -local function nodeToPoint(node) - return { x = node.x, y = node.y, z = node.z, heading = node.heading } -end - -local function heuristic(n, node) - return Point.calculateMoves(node, n) --- { x = node.x, y = node.y, z = node.z, heading = node.heading }, --- { x = n.x, y = n.y, z = n.z, heading = n.heading }) -end - -local function dimsAreEqual(d1, d2) - return d1.ex == d2.ex and - d1.ey == d2.ey and - d1.ez == d2.ez and - d1.x == d2.x and - d1.y == d2.y and - d1.z == d2.z -end - --- turtle sensor returns blocks in relation to the world - not turtle orientation --- so cannot figure out block location unless we know our orientation in the world --- really kinda dumb since it returns the coordinates as offsets of our location --- instead of true coordinates -local function addSensorBlocks(blocks, sblocks) - for _,b in pairs(sblocks) do - if b.type ~= 'AIR' then - local pt = { x = turtle.point.x, y = turtle.point.y + b.y, z = turtle.point.z } - pt.x = pt.x - b.x - pt.z = pt.z - b.z -- this will only work if we were originally facing west - local found = false - for _,ob in pairs(blocks) do - if pt.x == ob.x and pt.y == ob.y and pt.z == ob.z then - found = true - break - end - end - if not found then - table.insert(blocks, pt) - end - end - end -end - -local function selectDestination(pts, box, grid) - while #pts > 0 do - local pt = Point.closest(turtle.point, pts) - if box and not Point.inBox(pt, box) then - Util.removeByValue(pts, pt) - else - if grid:isWalkableAt(pt.x, pt.y, pt.z) then - return pt - end - Util.removeByValue(pts, pt) - end - end -end - -local function pathTo(dest, options) - local blocks = options.blocks or turtle.getState().blocks or { } - local dests = options.dest or { dest } -- support alternative destinations - local box = options.box or turtle.getState().box - local lastDim - local grid - - if box then - box = Point.normalizeBox(box) - end - - -- Creates a pathfinder object - local finder = Pathfinder(heuristic) - - while turtle.point.x ~= dest.x or turtle.point.z ~= dest.z or turtle.point.y ~= dest.y do - - -- map expands as we encounter obstacles - local dim = mapDimensions(dest, blocks, box, dests) - - -- reuse map if possible - if not lastDim or not dimsAreEqual(dim, lastDim) then - -- Creates a grid object - grid = Grid(dim) - finder:setGrid(grid) - - lastDim = dim - end - for _,b in pairs(blocks) do - addBlock(grid, b, dim) - end - - dest = selectDestination(dests, box, grid) - if not dest then - return false, 'failed to reach destination' - end - if turtle.point.x == dest.x and turtle.point.z == dest.z and turtle.point.y == dest.y then - break - end - - -- Define start and goal locations coordinates - local startPt = turtle.point - - -- Calculates the path, and its length - local path = finder:getPath( - startPt.x, startPt.y, startPt.z, turtle.point.heading, - dest.x, dest.y, dest.z, dest.heading) - - if not path then - Util.removeByValue(dests, dest) - else - path:filter() - - for node in path:nodes() do - local pt = nodeToPoint(node) - - if turtle.isAborted() then - return false, 'aborted' - end - ---if this is the next to last node ---and we are traveling up or down, then the ---heading for this node should be the heading of the last node ---or, maybe.. ---if last node is up or down (or either?) - - -- use single turn method so the turtle doesn't turn around - -- when encountering obstacles - -- if not turtle.gotoSingleTurn(pt.x, pt.y, pt.z, pt.heading) then - if not turtle.goto(pt) then - local bpt = Point.nearestTo(turtle.point, pt) - - table.insert(blocks, bpt) - -- really need to check if the block we ran into was a turtle. - -- if so, this block should be temporary (1-2 secs) - - --local side = turtle.getSide(turtle.point, pt) - --if turtle.isTurtleAtSide(side) then - -- pt.timestamp = os.clock() + ? - --end - -- if dim has not changed, then need to update grid with - -- walkable = nil (after time has elapsed) - - --if device.turtlesensorenvironment then - -- addSensorBlocks(blocks, device.turtlesensorenvironment.sonicScan()) - --end - break - end - end - end - end - - if dest.heading then - turtle.setHeading(dest.heading) - end - return dest -end - -return { - pathfind = function(dest, options) - options = options or { } - --if not options.blocks and turtle.gotoPoint(dest) then - -- return dest - --end - return pathTo(dest, options) - end, - - -- set a global bounding box - -- box can be overridden by passing box in pathfind options - setBox = function(box) - turtle.getState().box = box - end, - - setBlocks = function(blocks) - turtle.getState().blocks = blocks - end, - - addBlock = function(block) - if turtle.getState().blocks then - table.insert(turtle.getState().blocks, block) - end - end, - - reset = function() - turtle.getState().box = nil - turtle.getState().blocks = nil - end, -} diff --git a/sys/apis/ui.lua b/sys/apis/ui.lua deleted file mode 100644 index dffb370..0000000 --- a/sys/apis/ui.lua +++ /dev/null @@ -1,3478 +0,0 @@ -local Canvas = require('ui.canvas') -local class = require('class') -local Event = require('event') -local Input = require('input') -local Peripheral = require('peripheral') -local Sound = require('sound') -local Transition = require('ui.transition') -local Util = require('util') - -local _rep = string.rep -local _sub = string.sub -local colors = _G.colors -local device = _G.device -local fs = _G.fs -local os = _G.os -local term = _G.term -local window = _G.window - ---[[ - Using the shorthand window definition, elements are created from - the bottom up. Once reaching the top, setParent is called top down. - - On :init(), elements do not know the parent or can calculate sizing. -]] - -local function safeValue(v) - local t = type(v) - if t == 'string' or t == 'number' then - return v - end - return tostring(v) -end - --- need to add offsets to this test -local function getPosition(element) - local x, y = 1, 1 - repeat - x = element.x + x - 1 - y = element.y + y - 1 - element = element.parent - until not element - return x, y -end - ---[[-- Top Level Manager --]]-- -local Manager = class() -function Manager:init() - local function keyFunction(event, code, held) - local ie = Input:translate(event, code, held) - - if ie and self.currentPage then - local target = self.currentPage.focused or self.currentPage - self:inputEvent(target, - { type = 'key', key = ie.code == 'char' and ie.ch or ie.code, element = target }) - self.currentPage:sync() - end - end - - local handlers = { - char = keyFunction, - key_up = keyFunction, - key = keyFunction, - - term_resize = function(_, side) - if self.currentPage then - -- the parent doesn't have any children set... - -- that's why we have to resize both the parent and the current page - -- kinda makes sense - if self.currentPage.parent.device.side == side then - self.currentPage.parent:resize() - - self.currentPage:resize() - self.currentPage:draw() - self.currentPage:sync() - end - end - end, - - mouse_scroll = function(_, direction, x, y) - if self.currentPage then - local event = self.currentPage:pointToChild(x, y) - local directions = { - [ -1 ] = 'up', - [ 1 ] = 'down' - } - -- revisit - should send out scroll_up and scroll_down events - -- let the element convert them to up / down - self:inputEvent(event.element, - { type = 'key', key = directions[direction] }) - self.currentPage:sync() - end - end, - - -- this should be moved to the device ! - monitor_touch = function(_, side, x, y) - Input:translate('mouse_click', 1, x, y) - local ie = Input:translate('mouse_up', 1, x, y) - if self.currentPage then - if self.currentPage.parent.device.side == side then - self:click(ie.code, 1, x, y) - end - end - end, - - mouse_click = function(_, button, x, y) - Input:translate('mouse_click', button, x, y) - - if self.currentPage then - if not self.currentPage.parent.device.side then - local event = self.currentPage:pointToChild(x, y) - if event.element.focus and not event.element.inactive then - self.currentPage:setFocus(event.element) - self.currentPage:sync() - end - end - end - end, - - mouse_up = function(_, button, x, y) - local ie = Input:translate('mouse_up', button, x, y) - - if ie.code == 'control-shift-mouse_click' then -- hack - local event = self.currentPage:pointToChild(x, y) - _ENV.multishell.openTab({ - path = 'sys/apps/Lua.lua', - args = { event.element }, - focused = true }) - - elseif ie and self.currentPage then - --if not self.currentPage.parent.device.side then - self:click(ie.code, button, x, y) - --end - end - end, - - mouse_drag = function(_, button, x, y) - local ie = Input:translate('mouse_drag', button, x, y) - if ie and self.currentPage then - local event = self.currentPage:pointToChild(x, y) - event.type = ie.code - self:inputEvent(event.element, event) - self.currentPage:sync() - end - end, - - paste = function(_, text) - Input:translate('paste') - self:emitEvent({ type = 'paste', text = text }) - self.currentPage:sync() - end, - } - - -- use 1 handler to single thread all events - Event.on({ - 'char', 'key_up', 'key', 'term_resize', - 'mouse_scroll', 'monitor_touch', 'mouse_click', - 'mouse_up', 'mouse_drag', 'paste' }, - function(event, ...) - handlers[event](event, ...) - end) -end - -function Manager:configure(appName, ...) - local options = { - device = { arg = 'd', type = 'string', - desc = 'Device type' }, - textScale = { arg = 't', type = 'number', - desc = 'Text scale' }, - } - local defaults = Util.loadTable('usr/config/' .. appName) or { } - if not defaults.device then - defaults.device = { } - end - - Util.getOptions(options, { ... }, true) - local optionValues = { - name = options.device.value, - textScale = options.textScale.value, - } - - Util.merge(defaults.device, optionValues) - - if defaults.device.name then - - local dev - - if defaults.device.name == 'terminal' then - dev = term.current() - else - dev = Peripheral.lookup(defaults.device.name) --- device[defaults.device.name] - end - - if not dev then - error('Invalid display device') - end - self:setDefaultDevice(self.Device({ - device = dev, - textScale = defaults.device.textScale, - })) - end - - if defaults.theme then - for k,v in pairs(defaults.theme) do - if self[k] and self[k].defaults then - Util.merge(self[k].defaults, v) - end - end - end -end - -function Manager:disableEffects() - self.defaultDevice.effectsEnabled = false -end - -function Manager:loadTheme(filename) - if fs.exists(filename) then - local theme, err = Util.loadTable(filename) - if not theme then - error(err) - end - for k,v in pairs(theme) do - if self[k] and self[k].defaults then - Util.merge(self[k].defaults, v) - end - end - end -end - -function Manager:emitEvent(event) - if self.currentPage and self.currentPage.focused then - return self.currentPage.focused:emit(event) - end -end - -function Manager:inputEvent(parent, event) - while parent do - if parent.accelerators then - local acc = parent.accelerators[event.key] - if acc then - if parent:emit({ type = acc, element = parent }) then - return true - end - end - end - if parent.eventHandler then - if parent:eventHandler(event) then - return true - end - end - parent = parent.parent - end -end - -function Manager:click(code, button, x, y) - if self.currentPage then - - local target = self.currentPage - - -- need to add offsets into this check - --[[ - if x < target.x or y < target.y or - x > target.x + target.width - 1 or - y > target.y + target.height - 1 then - target:emit({ type = 'mouse_out' }) - - target = self.currentPage - end - --]] - - local clickEvent = target:pointToChild(x, y) - - if code == 'mouse_doubleclick' then - if self.doubleClickElement ~= clickEvent.element then - return - end - else - self.doubleClickElement = clickEvent.element - end - - clickEvent.button = button - clickEvent.type = code - clickEvent.key = code - - if clickEvent.element.focus then - self.currentPage:setFocus(clickEvent.element) - end - if not self:inputEvent(clickEvent.element, clickEvent) then - --[[ - if button == 3 then - -- if the double-click was not captured - -- send through a single-click - clickEvent.button = 1 - clickEvent.type = events[1] - clickEvent.key = events[1] - self:inputEvent(clickEvent.element, clickEvent) - end - ]] - end - - self.currentPage:sync() - end -end - -function Manager:setDefaultDevice(dev) - self.defaultDevice = dev - self.term = dev -end - -function Manager:addPage(name, page) - if not self.pages then - self.pages = { } - end - self.pages[name] = page -end - -function Manager:setPages(pages) - self.pages = pages -end - -function Manager:getPage(pageName) - local page = self.pages[pageName] - - if not page then - error('UI:getPage: Invalid page: ' .. tostring(pageName), 2) - end - - return page -end - -function Manager:setPage(pageOrName, ...) - local page = pageOrName - - if type(pageOrName) == 'string' then - page = self.pages[pageOrName] or error('Invalid page: ' .. pageOrName) - end - - if page == self.currentPage then - page:draw() - else - local needSync - if self.currentPage then - if self.currentPage.focused then - self.currentPage.focused.focused = false - self.currentPage.focused:focus() - end - self.currentPage:disable() - page.previousPage = self.currentPage - else - needSync = true - end - self.currentPage = page - self.currentPage:clear(page.backgroundColor) - page:enable(...) - page:draw() - if self.currentPage.focused then - self.currentPage.focused.focused = true - self.currentPage.focused:focus() - end - if needSync then - page:sync() -- first time a page has been set - end - end -end - -function Manager:getCurrentPage() - return self.currentPage -end - -function Manager:setPreviousPage() - if self.currentPage.previousPage then - local previousPage = self.currentPage.previousPage.previousPage - self:setPage(self.currentPage.previousPage) - self.currentPage.previousPage = previousPage - end -end - -function Manager:getDefaults(element, args) - local defaults = Util.deepCopy(element.defaults) - if args then - Manager:mergeProperties(defaults, args) - end - return defaults -end - -function Manager:mergeProperties(obj, args) - if args then - for k,v in pairs(args) do - if k == 'accelerators' then - if obj.accelerators then - Util.merge(obj.accelerators, args.accelerators) - else - obj[k] = v - end - else - obj[k] = v - end - end - end -end - -function Manager:pullEvents(...) - Event.pullEvents(...) - self.term:reset() -end - -function Manager:exitPullEvents() - Event.exitPullEvents() -end - -local UI = Manager() - ---[[-- Basic drawable area --]]-- -UI.Window = class() -UI.Window.uid = 1 -UI.Window.defaults = { - UIElement = 'Window', - x = 1, - y = 1, - -- z = 0, -- eventually... - offx = 0, - offy = 0, - cursorX = 1, - cursorY = 1, -} -function UI.Window:init(args) - -- merge defaults for all subclasses - local defaults = args - local m = getmetatable(self) -- get the class for this instance - repeat - defaults = UI:getDefaults(m, defaults) - m = m._base - until not m - UI:mergeProperties(self, defaults) - - -- each element has a unique ID - self.uid = UI.Window.uid - UI.Window.uid = UI.Window.uid + 1 - - -- at this time, the object has all the properties set - - -- postInit is a special constructor. the element does not need to implement - -- the method. But we need to guarantee that each subclass which has this - -- method is called. - m = self - local lpi - repeat - if m.postInit and m.postInit ~= lpi then - m.postInit(self) - lpi = m.postInit - end - m = m._base - until not m -end - -function UI.Window:postInit() - if self.parent then - -- this will cascade down the whole tree of elements starting at the - -- top level window (which has a device as a parent) - self:setParent() - end -end - -function UI.Window:initChildren() - local children = self.children - - -- insert any UI elements created using the shorthand - -- window definition into the children array - for k,child in pairs(self) do - if k ~= 'parent' then -- reserved - if type(child) == 'table' and child.UIElement and not child.parent then - if not children then - children = { } - end - table.insert(children, child) - end - end - end - if children then - for _,child in pairs(children) do - if not child.parent then - child.parent = self - child:setParent() - -- child:reposition() -- maybe - if self.enabled then - child:enable() - end - end - end - self.children = children - end -end - -local function setSize(self) - if self.x < 0 then - self.x = self.parent.width + self.x + 1 - end - if self.y < 0 then - self.y = self.parent.height + self.y + 1 - end - - if self.ex then - local ex = self.ex - if self.ex <= 1 then - ex = self.parent.width + self.ex + 1 - end - if self.width then - self.x = ex - self.width + 1 - else - self.width = ex - self.x + 1 - end - end - if self.ey then - local ey = self.ey - if self.ey <= 1 then - ey = self.parent.height + self.ey + 1 - end - if self.height then - self.y = ey - self.height + 1 - else - self.height = ey - self.y + 1 - end - end - - if not self.width then - self.width = self.parent.width - self.x + 1 - end - if not self.height then - self.height = self.parent.height - self.y + 1 - end -end - --- bad name... should be called something like postInit --- normally used to determine sizes since the parent is --- only known at this point -function UI.Window:setParent() - self.oh, self.ow = self.height, self.width - self.ox, self.oy = self.x, self.y - - setSize(self) - - self:initChildren() -end - -function UI.Window:resize() - self.height, self.width = self.oh, self.ow - self.x, self.y = self.ox, self.oy - - setSize(self) - - if self.children then - for _,child in ipairs(self.children) do - child:resize() - end - end -end - -function UI.Window:add(children) - UI:mergeProperties(self, children) - self:initChildren() -end - -function UI.Window:getCursorPos() - return self.cursorX, self.cursorY -end - -function UI.Window:setCursorPos(x, y) - self.cursorX = x - self.cursorY = y - self.parent:setCursorPos(self.x + x - 1, self.y + y - 1) -end - -function UI.Window:setCursorBlink(blink) - self.parent:setCursorBlink(blink) -end - -function UI.Window:draw() - self:clear(self.backgroundColor) - if self.children then - for _,child in pairs(self.children) do - if child.enabled then - child:draw() - end - end - end -end - -function UI.Window:sync() - if self.parent then - self.parent:sync() - end -end - -function UI.Window:enable() - self.enabled = true - if self.children then - for _,child in pairs(self.children) do - child:enable() - end - end -end - -function UI.Window:disable() - self.enabled = false - if self.children then - for _,child in pairs(self.children) do - child:disable() - end - end -end - -function UI.Window:setTextScale(textScale) - self.textScale = textScale - self.parent:setTextScale(textScale) -end - -function UI.Window:clear(bg, fg) - if self.canvas then - self.canvas:clear(bg or self.backgroundColor, fg or self.textColor) - else - self:clearArea(1 + self.offx, 1 + self.offy, self.width, self.height, bg) - end -end - -function UI.Window:clearLine(y, bg) - self:write(1, y, _rep(' ', self.width), bg) -end - -function UI.Window:clearArea(x, y, width, height, bg) - if width > 0 then - local filler = _rep(' ', width) - for i = 0, height - 1 do - self:write(x, y + i, filler, bg) - end - end -end - -function UI.Window:write(x, y, text, bg, tc) - bg = bg or self.backgroundColor - tc = tc or self.textColor - x = x - self.offx - y = y - self.offy - if y <= self.height and y > 0 then - if self.canvas then - self.canvas:write(x, y, text, bg, tc) - else - self.parent:write( - self.x + x - 1, self.y + y - 1, tostring(text), bg, tc) - end - end -end - -function UI.Window:centeredWrite(y, text, bg, fg) - if #text >= self.width then - self:write(1, y, text, bg, fg) - else - local space = math.floor((self.width-#text) / 2) - local filler = _rep(' ', space + 1) - local str = _sub(filler, 1, space) .. text - str = str .. _sub(filler, self.width - #str + 1) - self:write(1, y, str, bg, fg) - end -end - -function UI.Window:print(text, bg, fg) - local marginLeft = self.marginLeft or 0 - local marginRight = self.marginRight or 0 - local width = self.width - marginLeft - marginRight - - local function nextWord(line, cx) - local result = { line:find("(%w+)", cx) } - if #result > 1 and result[2] > cx then - return _sub(line, cx, result[2] + 1) - elseif #result > 0 and result[1] == cx then - result = { line:find("(%w+)", result[2]) } - if #result > 0 then - return _sub(line, cx, result[1] + 1) - end - end - if cx <= #line then - return _sub(line, cx, #line) - end - end - - local function pieces(f, bg, fg) - local pos = 1 - local t = { } - while true do - local s = string.find(f, '\027', pos, true) - if not s then - break - end - if pos < s then - table.insert(t, _sub(f, pos, s - 1)) - end - local seq = _sub(f, s) - seq = seq:match("\027%[([%d;]+)m") - local e = { } - for color in string.gmatch(seq, "%d+") do - color = tonumber(color) - if color == 0 then - e.fg = fg - e.bg = bg - elseif color > 20 then - e.bg = 2 ^ (color - 21) - else - e.fg = 2 ^ (color - 1) - end - end - table.insert(t, e) - pos = s + #seq + 3 - end - if pos <= #f then - table.insert(t, _sub(f, pos)) - end - return t - end - - local lines = Util.split(text) - for k,line in pairs(lines) do - local fragments = pieces(line, bg, fg) - for _, fragment in ipairs(fragments) do - local lx = 1 - if type(fragment) == 'table' then -- ansi sequence - fg = fragment.fg - bg = fragment.bg - else - while true do - local word = nextWord(fragment, lx) - if not word then - break - end - local w = word - if self.cursorX + #word > width then - self.cursorX = marginLeft + 1 - self.cursorY = self.cursorY + 1 - w = word:gsub('^ ', '') - end - self:write(self.cursorX, self.cursorY, w, bg, fg) - self.cursorX = self.cursorX + #w - lx = lx + #word - end - end - end - if lines[k + 1] then - self.cursorX = marginLeft + 1 - self.cursorY = self.cursorY + 1 - end - end - - return self.cursorX, self.cursorY -end - -function UI.Window:setFocus(focus) - if self.parent then - self.parent:setFocus(focus) - end -end - -function UI.Window:capture(child) - if self.parent then - self.parent:capture(child) - end -end - -function UI.Window:release(child) - if self.parent then - self.parent:release(child) - end -end - -function UI.Window:pointToChild(x, y) - x = x + self.offx - self.x + 1 - y = y + self.offy - self.y + 1 - if self.children then - for _,child in pairs(self.children) do - if child.enabled and not child.inactive and - x >= child.x and x < child.x + child.width and - y >= child.y and y < child.y + child.height then - local c = child:pointToChild(x, y) - if c then - return c - end - end - end - end - return { - element = self, - x = x, - y = y - } -end - -function UI.Window:getFocusables() - local focusable = { } - - local function focusSort(a, b) - if a.y == b.y then - return a.x < b.x - end - return a.y < b.y - end - - local function getFocusable(parent, x, y) - for _,child in Util.spairs(parent.children, focusSort) do - if child.enabled and child.focus and not child.inactive then - table.insert(focusable, child) - end - if child.children then - getFocusable(child, child.x + x, child.y + y) - end - end - end - - if self.children then - getFocusable(self, self.x, self.y) - end - - return focusable -end - -function UI.Window:focusFirst() - local focusables = self:getFocusables() - local focused = focusables[1] - if focused then - self:setFocus(focused) - end -end - -function UI.Window:refocus() - local el = self - while el do - local focusables = el:getFocusables() - if focusables[1] then - self:setFocus(focusables[1]) - break - end - el = el.parent - end -end - -function UI.Window:scrollIntoView() - local parent = self.parent - - if self.x <= parent.offx then - parent.offx = math.max(0, self.x - 1) - parent:draw() - elseif self.x + self.width > parent.width + parent.offx then - parent.offx = self.x + self.width - parent.width - 1 - parent:draw() - end - - if self.y <= parent.offy then - parent.offy = math.max(0, self.y - 1) - parent:draw() - elseif self.y + self.height > parent.height + parent.offy then - parent.offy = self.y + self.height - parent.height - 1 - parent:draw() - end -end - -function UI.Window:getCanvas() - local el = self - repeat - if el.canvas then - return el.canvas - end - el = el.parent - until not el -end - -function UI.Window:addLayer(bg, fg) - local canvas = self:getCanvas() - canvas = canvas:addLayer(self, bg, fg) - canvas:clear(bg or self.backgroundColor, fg or self.textColor) - return canvas -end - -function UI.Window:addTransition(effect, args) - if self.parent then - args = args or { } - if not args.x then -- not good - args.x, args.y = getPosition(self) - args.width = self.width - args.height = self.height - end - - args.canvas = args.canvas or self.canvas - self.parent:addTransition(effect, args) - end -end - -function UI.Window:emit(event) - local parent = self - while parent do - if parent.eventHandler then - if parent:eventHandler(event) then - return true - end - end - parent = parent.parent - end -end - -function UI.Window:find(uid) - if self.children then - return Util.find(self.children, 'uid', uid) - end -end - -function UI.Window:eventHandler(event) - return false -end - ---[[-- Terminal for computer / advanced computer / monitor --]]-- -UI.Device = class(UI.Window) -UI.Device.defaults = { - UIElement = 'Device', - backgroundColor = colors.black, - textColor = colors.white, - textScale = 1, - effectsEnabled = true, -} -function UI.Device:postInit() - self.device = self.device or term.current() - - if self.deviceType then - self.device = device[self.deviceType] - end - - if not self.device.setTextScale then - self.device.setTextScale = function() end - end - - self.device.setTextScale(self.textScale) - self.width, self.height = self.device.getSize() - - self.isColor = self.device.isColor() - - self.canvas = Canvas({ - x = 1, y = 1, width = self.width, height = self.height, - isColor = self.isColor, - }) - self.canvas:clear(self.backgroundColor, self.textColor) -end - -function UI.Device:resize() - self.device.setTextScale(self.textScale) - self.width, self.height = self.device.getSize() - self.lines = { } - self.canvas:resize(self.width, self.height) - self.canvas:clear(self.backgroundColor, self.textColor) -end - -function UI.Device:setCursorPos(x, y) - self.cursorX = x - self.cursorY = y -end - -function UI.Device:getCursorBlink() - return self.cursorBlink -end - -function UI.Device:setCursorBlink(blink) - self.cursorBlink = blink - self.device.setCursorBlink(blink) -end - -function UI.Device:setTextScale(textScale) - self.textScale = textScale - self.device.setTextScale(self.textScale) -end - -function UI.Device:reset() - self.device.setBackgroundColor(colors.black) - self.device.setTextColor(colors.white) - self.device.clear() - self.device.setCursorPos(1, 1) -end - -function UI.Device:addTransition(effect, args) - if not self.transitions then - self.transitions = { } - end - - args = args or { } - args.ex = args.x + args.width - 1 - args.ey = args.y + args.height - 1 - args.canvas = args.canvas or self.canvas - - if type(effect) == 'string' then - effect = Transition[effect] - if not effect then - error('Invalid transition') - end - end - - table.insert(self.transitions, { update = effect(args), args = args }) -end - -function UI.Device:runTransitions(transitions, canvas) - for _,t in ipairs(transitions) do - canvas:punch(t.args) -- punch out the effect areas - end - canvas:blitClipped(self.device) -- and blit the remainder - canvas:reset() - - while true do - for _,k in ipairs(Util.keys(transitions)) do - local transition = transitions[k] - if not transition.update(self.device) then - transitions[k] = nil - end - end - if Util.empty(transitions) then - break - end - os.sleep(0) - end -end - -function UI.Device:sync() - local transitions - if self.transitions and self.effectsEnabled then - transitions = self.transitions - self.transitions = nil - end - - if self:getCursorBlink() then - self.device.setCursorBlink(false) - end - - if transitions then - self:runTransitions(transitions, self.canvas) - else - self.canvas:render(self.device) - end - - if self:getCursorBlink() then - self.device.setCursorPos(self.cursorX, self.cursorY) - self.device.setCursorBlink(true) - end -end - ---[[-- StringBuffer --]]-- --- justs optimizes string concatenations -UI.StringBuffer = class() -function UI.StringBuffer:init(bufSize) - self.bufSize = bufSize - self.buffer = {} -end - -function UI.StringBuffer:insert(s, width) - local len = #tostring(s or '') - if len > width then - s = _sub(s, 1, width) - end - table.insert(self.buffer, s) - if len < width then - table.insert(self.buffer, _rep(' ', width - len)) - end -end - -function UI.StringBuffer:insertRight(s, width) - local len = #tostring(s or '') - if len > width then - s = _sub(s, 1, width) - end - if len < width then - table.insert(self.buffer, _rep(' ', width - len)) - end - table.insert(self.buffer, s) -end - -function UI.StringBuffer:get(sep) - return Util.widthify(table.concat(self.buffer, sep or ''), self.bufSize) -end - -function UI.StringBuffer:clear() - self.buffer = { } -end - --- For manipulating text in a fixed width string -local SB = { } -function SB:new(width) - return setmetatable({ - width = width, - buf = _rep(' ', width) - }, { __index = SB }) -end -function SB:insert(x, str, width) - if x < 1 then - x = self.width + x + 1 - end - width = width or #str - if x + width - 1 > self.width then - width = self.width - x - end - if width > 0 then - self.buf = _sub(self.buf, 1, x - 1) .. _sub(str, 1, width) .. _sub(self.buf, x + width) - end -end -function SB:fill(x, ch, width) - width = width or self.width - x + 1 - self:insert(x, _rep(ch, width)) -end -function SB:center(str) - self:insert(math.max(1, math.ceil((self.width - #str + 1) / 2)), str) -end -function SB:get() - return self.buf -end - ---[[-- Page (focus manager) --]]-- -UI.Page = class(UI.Window) -UI.Page.defaults = { - UIElement = 'Page', - accelerators = { - down = 'focus_next', - enter = 'focus_next', - tab = 'focus_next', - ['shift-tab' ] = 'focus_prev', - up = 'focus_prev', - }, - backgroundColor = colors.cyan, - textColor = colors.white, -} -function UI.Page:postInit() - self.parent = self.parent or UI.defaultDevice - self.__target = self -end - -function UI.Page:setParent() - UI.Window.setParent(self) - if self.z then - self.canvas = self:addLayer(self.backgroundColor, self.textColor) - self.canvas:clear(self.backgroundColor, self.textColor) - else - self.canvas = self.parent.canvas - end -end - -function UI.Page:enable() - self.canvas.visible = true - UI.Window.enable(self) - - if not self.focused or not self.focused.enabled then - self:focusFirst() - end -end - -function UI.Page:disable() - if self.z then - self.canvas.visible = false - end -end - -function UI.Page:capture(child) - self.__target = child -end - -function UI.Page:release(child) - if self.__target == child then - self.__target = self - end -end - -function UI.Page:pointToChild(x, y) - if self.__target == self then - return UI.Window.pointToChild(self, x, y) - end - x = x + self.offx - self.x + 1 - y = y + self.offy - self.y + 1 - return self.__target:pointToChild(x, y) -end - -function UI.Page:getFocusables() - if self.__target == self or self.__target.pageType ~= 'modal' then - return UI.Window.getFocusables(self) - end - return self.__target:getFocusables() -end - -function UI.Page:getFocused() - return self.focused -end - -function UI.Page:focusPrevious() - local function getPreviousFocus(focused) - local focusables = self:getFocusables() - local k = Util.contains(focusables, focused) - if k then - if k > 1 then - return focusables[k - 1] - end - return focusables[#focusables] - end - end - - local focused = getPreviousFocus(self.focused) - if focused then - self:setFocus(focused) - end -end - -function UI.Page:focusNext() - local function getNextFocus(focused) - local focusables = self:getFocusables() - local k = Util.contains(focusables, focused) - if k then - if k < #focusables then - return focusables[k + 1] - end - return focusables[1] - end - end - - local focused = getNextFocus(self.focused) - if focused then - self:setFocus(focused) - end -end - -function UI.Page:setFocus(child) - if not child or not child.focus then - return - end - - if self.focused and self.focused ~= child then - self.focused.focused = false - self.focused:focus() - self.focused:emit({ type = 'focus_lost', focused = child }) - end - - self.focused = child - if not child.focused then - child.focused = true - child:emit({ type = 'focus_change', focused = child }) - --self:emit({ type = 'focus_change', focused = child }) - end - - child:focus() -end - -function UI.Page:eventHandler(event) - if self.focused then - if event.type == 'focus_next' then - self:focusNext() - return true - elseif event.type == 'focus_prev' then - self:focusPrevious() - return true - end - end -end - ---[[-- Grid --]]-- -UI.Grid = class(UI.Window) -UI.Grid.defaults = { - UIElement = 'Grid', - index = 1, - inverseSort = false, - disableHeader = false, - marginRight = 0, - textColor = colors.white, - textSelectedColor = colors.white, - backgroundColor = colors.black, - backgroundSelectedColor = colors.gray, - headerBackgroundColor = colors.cyan, - headerTextColor = colors.white, - headerSortColor = colors.yellow, - unfocusedTextSelectedColor = colors.white, - unfocusedBackgroundSelectedColor = colors.gray, - focusIndicator = '>', - sortIndicator = ' ', - inverseSortIndicator = '^', - values = { }, - columns = { }, - accelerators = { - enter = 'key_enter', - [ 'control-c' ] = 'copy', - down = 'scroll_down', - up = 'scroll_up', - home = 'scroll_top', - [ 'end' ] = 'scroll_bottom', - pageUp = 'scroll_pageUp', - [ 'control-b' ] = 'scroll_pageUp', - pageDown = 'scroll_pageDown', - [ 'control-f' ] = 'scroll_pageDown', - }, -} -function UI.Grid:setParent() - UI.Window.setParent(self) - - for _,c in pairs(self.columns) do - c.cw = c.width - if not c.heading then - c.heading = '' - end - end - - self:update() - - if not self.pageSize then - if self.disableHeader then - self.pageSize = self.height - else - self.pageSize = self.height - 1 - end - end -end - -function UI.Grid:resize() - UI.Window.resize(self) - - if self.disableHeader then - self.pageSize = self.height - else - self.pageSize = self.height - 1 - end - self:adjustWidth() -end - -function UI.Grid:adjustWidth() - local t = { } -- cols without width - local w = self.width - #self.columns - 1 - self.marginRight -- width remaining - - for _,c in pairs(self.columns) do - if c.width then - c.cw = c.width - w = w - c.cw - else - table.insert(t, c) - end - end - - if #t == 0 then - return - end - - if #t == 1 then - t[1].cw = #(t[1].heading or '') - t[1].cw = math.max(t[1].cw, w) - return - end - - if not self.autospace then - for k,c in ipairs(t) do - c.cw = math.floor(w / (#t - k + 1)) - w = w - c.cw - end - - else - for _,c in ipairs(t) do - c.cw = #(c.heading or '') - w = w - c.cw - end - -- adjust the size to the length of the value - for key,row in pairs(self.values) do - if w <= 0 then - break - end - row = self:getDisplayValues(row, key) - for _,col in pairs(t) do - local value = row[col.key] - if value then - value = tostring(value) - if #value > col.cw then - w = w + col.cw - col.cw = math.min(#value, w) - w = w - col.cw - if w <= 0 then - break - end - end - end - end - end - - -- last column does not get padding (right alignment) - if not self.columns[#self.columns].width then - Util.removeByValue(t, self.columns[#self.columns]) - end - - -- got some extra room - add some padding - if w > 0 then - for k,c in ipairs(t) do - local padding = math.floor(w / (#t - k + 1)) - c.cw = c.cw + padding - w = w - padding - end - end - end -end - -function UI.Grid:setPageSize(pageSize) - self.pageSize = pageSize -end - -function UI.Grid:getValues() - return self.values -end - -function UI.Grid:setValues(t) - self.values = t - self:update() -end - -function UI.Grid:setInverseSort(inverseSort) - self.inverseSort = inverseSort - self:update() - self:setIndex(self.index) -end - -function UI.Grid:setSortColumn(column) - self.sortColumn = column -end - -function UI.Grid:getDisplayValues(row, key) - return row -end - -function UI.Grid:getSelected() - if self.sorted then - return self.values[self.sorted[self.index]], self.sorted[self.index] - end -end - -function UI.Grid:setSelected(name, value) - if self.sorted then - for k,v in pairs(self.sorted) do - if self.values[v][name] == value then - self:setIndex(k) - return - end - end - end - self:setIndex(1) -end - -function UI.Grid:focus() - self:drawRows() -end - -function UI.Grid:draw() - if not self.disableHeader then - self:drawHeadings() - end - - if self.index <= 0 then - self:setIndex(1) - elseif self.index > #self.sorted then - self:setIndex(#self.sorted) - end - self:drawRows() -end - --- Something about the displayed table has changed --- resort the table -function UI.Grid:update() - local function sort(a, b) - if not a[self.sortColumn] then - return false - elseif not b[self.sortColumn] then - return true - end - return self:sortCompare(a, b) - end - - local function inverseSort(a, b) - return not sort(a, b) - end - - local order - if self.sortColumn then - order = sort - if self.inverseSort then - order = inverseSort - end - end - - self.sorted = Util.keys(self.values) - if order then - table.sort(self.sorted, function(a,b) - return order(self.values[a], self.values[b]) - end) - end - - self:adjustWidth() -end - -function UI.Grid:drawHeadings() - local x = 1 - for _,col in ipairs(self.columns) do - local ind = ' ' - if col.key == self.sortColumn then - if self.inverseSort then - ind = self.inverseSortIndicator - else - ind = self.sortIndicator - end - end - self:write(x, - 1, - Util.widthify(ind .. col.heading, col.cw + 1), - self.headerBackgroundColor, - col.key == self.sortColumn and self.headerSortColor or self.headerTextColor) - x = x + col.cw + 1 - end -end - -function UI.Grid:sortCompare(a, b) - a = safeValue(a[self.sortColumn]) - b = safeValue(b[self.sortColumn]) - if type(a) == type(b) then - return a < b - end - return tostring(a) < tostring(b) -end - -function UI.Grid:drawRows() - local y = 1 - local startRow = math.max(1, self:getStartRow()) - local sb = UI.StringBuffer(self.width) - - if not self.disableHeader then - y = y + 1 - end - - local lastRow = math.min(startRow + self.pageSize - 1, #self.sorted) - for index = startRow, lastRow do - - local sindex = self.sorted[index] - local rawRow = self.values[sindex] - local key = sindex - local row = self:getDisplayValues(rawRow, key) - - sb:clear() - - local ind = ' ' - if self.focused and index == self.index and not self.inactive then - ind = self.focusIndicator - end - - for _,col in pairs(self.columns) do - if col.justify == 'right' then - sb:insertRight(ind .. safeValue(row[col.key] or ''), col.cw + 1) - else - sb:insert(ind .. safeValue(row[col.key] or ''), col.cw + 1) - end - ind = ' ' - end - - local selected = index == self.index and not self.inactive - - self:write(1, y, sb:get(), - self:getRowBackgroundColor(rawRow, selected), - self:getRowTextColor(rawRow, selected)) - - y = y + 1 - end - - if y <= self.height then - self:clearArea(1, y, self.width, self.height - y + 1) - end -end - -function UI.Grid:getRowTextColor(row, selected) - if selected then - if self.focused then - return self.textSelectedColor - end - return self.unfocusedTextSelectedColor - end - return self.textColor -end - -function UI.Grid:getRowBackgroundColor(row, selected) - if selected then - if self.focused then - return self.backgroundSelectedColor - end - return self.unfocusedBackgroundSelectedColor - end - return self.backgroundColor -end - -function UI.Grid:getIndex() - return self.index -end - -function UI.Grid:setIndex(index) - index = math.max(1, index) - self.index = math.min(index, #self.sorted) - - local selected = self:getSelected() - if selected ~= self.selected then - self:drawRows() - self.selected = selected - if selected then - self:emit({ type = 'grid_focus_row', selected = selected, element = self }) - end - end -end - -function UI.Grid:getStartRow() - return math.floor((self.index - 1) / self.pageSize) * self.pageSize + 1 -end - -function UI.Grid:getPage() - return math.floor(self.index / self.pageSize) + 1 -end - -function UI.Grid:getPageCount() - local tableSize = Util.size(self.values) - local pc = math.floor(tableSize / self.pageSize) - if tableSize % self.pageSize > 0 then - pc = pc + 1 - end - return pc -end - -function UI.Grid:nextPage() - self:setPage(self:getPage() + 1) -end - -function UI.Grid:previousPage() - self:setPage(self:getPage() - 1) -end - -function UI.Grid:setPage(pageNo) - -- 1 based paging - self:setIndex((pageNo-1) * self.pageSize + 1) -end - -function UI.Grid:eventHandler(event) - if event.type == 'mouse_click' or - event.type == 'mouse_rightclick' or - event.type == 'mouse_doubleclick' then - if not self.disableHeader then - if event.y == 1 then - local col = 2 - for _,c in ipairs(self.columns) do - if event.x < col + c.cw then - self:emit({ - type = 'grid_sort', - sortColumn = c.key, - inverseSort = self.sortColumn == c.key and not self.inverseSort, - element = self, - }) - break - end - col = col + c.cw + 1 - end - return true - end - end - local row = self:getStartRow() + event.y - 1 - if not self.disableHeader then - row = row - 1 - end - if row > 0 and row <= Util.size(self.values) then - self:setIndex(row) - if event.type == 'mouse_doubleclick' then - self:emit({ type = 'key_enter' }) - elseif event.type == 'mouse_rightclick' then - self:emit({ type = 'grid_select_right', selected = self.selected, element = self }) - end - return true - end - return false - - elseif event.type == 'grid_sort' then - self.sortColumn = event.sortColumn - self:setInverseSort(event.inverseSort) - self:draw() - elseif event.type == 'scroll_down' then - self:setIndex(self.index + 1) - elseif event.type == 'scroll_up' then - self:setIndex(self.index - 1) - elseif event.type == 'scroll_top' then - self:setIndex(1) - elseif event.type == 'scroll_bottom' then - self:setIndex(Util.size(self.values)) - elseif event.type == 'scroll_pageUp' then - self:setIndex(self.index - self.pageSize) - elseif event.type == 'scroll_pageDown' then - self:setIndex(self.index + self.pageSize) - elseif event.type == 'key_enter' then - if self.selected then - self:emit({ type = 'grid_select', selected = self.selected, element = self }) - end - elseif event.type == 'copy' then - if self.selected then - os.queueEvent('clipboard_copy', self.selected) - end - else - return false - end - return true -end - ---[[-- ScrollingGrid --]]-- -UI.ScrollingGrid = class(UI.Grid) -UI.ScrollingGrid.defaults = { - UIElement = 'ScrollingGrid', - scrollOffset = 0, - marginRight = 1, -} -function UI.ScrollingGrid:postInit() - self.scrollBar = UI.ScrollBar() -end - -function UI.ScrollingGrid:drawRows() - UI.Grid.drawRows(self) - self.scrollBar:draw() -end - -function UI.ScrollingGrid:getViewArea() - local y = 1 - if not self.disableHeader then - y = 2 - end - return { - static = true, -- the container doesn't scroll - y = y, -- scrollbar Y - height = self.pageSize, -- viewable height - totalHeight = Util.size(self.values), -- total height - offsetY = self.scrollOffset, -- scroll offset - } -end - -function UI.ScrollingGrid:getStartRow() - local ts = Util.size(self.values) - if ts < self.pageSize then - self.scrollOffset = 0 - end - return self.scrollOffset + 1 -end - -function UI.ScrollingGrid:setIndex(index) - if index < self.scrollOffset + 1 then - self.scrollOffset = index - 1 - elseif index - self.scrollOffset > self.pageSize then - self.scrollOffset = index - self.pageSize - end - - if self.scrollOffset < 0 then - self.scrollOffset = 0 - else - local ts = Util.size(self.values) - if self.pageSize + self.scrollOffset + 1 > ts then - self.scrollOffset = math.max(0, ts - self.pageSize) - end - end - UI.Grid.setIndex(self, index) -end - ---[[-- Menu --]]-- -UI.Menu = class(UI.Grid) -UI.Menu.defaults = { - UIElement = 'Menu', - disableHeader = true, - columns = { { heading = 'Prompt', key = 'prompt', width = 20 } }, -} -function UI.Menu:postInit() - self.values = self.menuItems - self.pageSize = #self.menuItems -end - -function UI.Menu:setParent() - UI.Grid.setParent(self) - self.itemWidth = 1 - for _,v in pairs(self.values) do - if #v.prompt > self.itemWidth then - self.itemWidth = #v.prompt - end - end - self.columns[1].width = self.itemWidth - - if self.centered then - self:center() - else - self.width = self.itemWidth + 2 - end -end - -function UI.Menu:center() - self.x = (self.width - self.itemWidth + 2) / 2 - self.width = self.itemWidth + 2 -end - -function UI.Menu:eventHandler(event) - if event.type == 'key' then - if event.key == 'enter' then - local selected = self.menuItems[self.index] - self:emit({ - type = selected.event or 'menu_select', - selected = selected - }) - return true - end - elseif event.type == 'mouse_click' then - if event.y <= #self.menuItems then - UI.Grid.setIndex(self, event.y) - local selected = self.menuItems[self.index] - self:emit({ - type = selected.event or 'menu_select', - selected = selected - }) - return true - end - end - return UI.Grid.eventHandler(self, event) -end - ---[[-- Viewport --]]-- -UI.Viewport = class(UI.Window) -UI.Viewport.defaults = { - UIElement = 'Viewport', - backgroundColor = colors.cyan, - accelerators = { - down = 'scroll_down', - up = 'scroll_up', - home = 'scroll_top', - [ 'end' ] = 'scroll_bottom', - pageUp = 'scroll_pageUp', - [ 'control-b' ] = 'scroll_pageUp', - pageDown = 'scroll_pageDown', - [ 'control-f' ] = 'scroll_pageDown', - }, -} -function UI.Viewport:setScrollPosition(offset) - local oldOffset = self.offy - self.offy = math.max(offset, 0) - local max = self.ymax or self.height - if self.children then - for _, child in ipairs(self.children) do - if child ~= self.scrollBar then -- hack ! - max = math.max(child.y + child.height - 1, max) - end - end - end - self.offy = math.min(self.offy, math.max(max, self.height) - self.height) - if self.offy ~= oldOffset then - self:draw() - end -end - -function UI.Viewport:reset() - self.offy = 0 -end - -function UI.Viewport:getViewArea() - return { - y = (self.offy or 0) + 1, - height = self.height, - totalHeight = self.ymax, - offsetY = self.offy or 0, - } -end - -function UI.Viewport:eventHandler(event) - if event.type == 'scroll_down' then - self:setScrollPosition(self.offy + 1) - elseif event.type == 'scroll_up' then - self:setScrollPosition(self.offy - 1) - elseif event.type == 'scroll_top' then - self:setScrollPosition(0) - elseif event.type == 'scroll_bottom' then - self:setScrollPosition(10000000) - elseif event.type == 'scroll_pageUp' then - self:setScrollPosition(self.offy - self.height) - elseif event.type == 'scroll_pageDown' then - self:setScrollPosition(self.offy + self.height) - else - return false - end - return true -end - ---[[-- TitleBar --]]-- -UI.TitleBar = class(UI.Window) -UI.TitleBar.defaults = { - UIElement = 'TitleBar', - height = 1, - textColor = colors.white, - backgroundColor = colors.cyan, - title = '', - frameChar = '-', - closeInd = '*', -} -function UI.TitleBar:draw() - local sb = SB:new(self.width) - sb:fill(2, self.frameChar, sb.width - 3) - sb:center(string.format(' %s ', self.title)) - if self.previousPage or self.event then - sb:insert(-1, self.closeInd) - else - sb:insert(-2, self.frameChar) - end - self:write(1, 1, sb:get()) -end - -function UI.TitleBar:eventHandler(event) - if event.type == 'mouse_click' then - if (self.previousPage or self.event) and event.x == self.width then - if self.event then - self:emit({ type = self.event, element = self }) - elseif type(self.previousPage) == 'string' or - type(self.previousPage) == 'table' then - UI:setPage(self.previousPage) - else - UI:setPreviousPage() - end - return true - end - end -end - ---[[-- Button --]]-- -UI.Button = class(UI.Window) -UI.Button.defaults = { - UIElement = 'Button', - text = 'button', - backgroundColor = colors.lightGray, - backgroundFocusColor = colors.gray, - textFocusColor = colors.white, - textInactiveColor = colors.gray, - textColor = colors.black, - centered = true, - height = 1, - focusIndicator = ' ', - event = 'button_press', - accelerators = { - space = 'button_activate', - enter = 'button_activate', - mouse_click = 'button_activate', - } -} -function UI.Button:setParent() - if not self.width and not self.ex then - self.width = #self.text + 2 - end - UI.Window.setParent(self) -end - -function UI.Button:draw() - local fg = self.textColor - local bg = self.backgroundColor - local ind = ' ' - if self.focused then - bg = self.backgroundFocusColor - fg = self.textFocusColor - ind = self.focusIndicator - elseif self.inactive then - fg = self.textInactiveColor - end - local text = ind .. self.text .. ' ' - if self.centered then - self:clear(bg) - self:centeredWrite(1 + math.floor(self.height / 2), text, bg, fg) - else - self:write(1, 1, Util.widthify(text, self.width), bg, fg) - end -end - -function UI.Button:focus() - if self.focused then - self:scrollIntoView() - end - self:draw() -end - -function UI.Button:eventHandler(event) - if event.type == 'button_activate' then - self:emit({ type = self.event, button = self }) - return true - end - return false -end - ---[[-- MenuItem --]]-- -UI.MenuItem = class(UI.Button) -UI.MenuItem.defaults = { - UIElement = 'MenuItem', - textColor = colors.black, - backgroundColor = colors.lightGray, - textFocusColor = colors.white, - backgroundFocusColor = colors.lightGray, -} - ---[[-- MenuBar --]]-- -UI.MenuBar = class(UI.Window) -UI.MenuBar.defaults = { - UIElement = 'MenuBar', - buttons = { }, - height = 1, - backgroundColor = colors.lightGray, - textColor = colors.black, - spacing = 2, - lastx = 1, - showBackButton = false, - buttonClass = 'MenuItem', -} -UI.MenuBar.spacer = { spacer = true, text = 'spacer', inactive = true } - -function UI.MenuBar:postInit() - self:addButtons(self.buttons) -end - -function UI.MenuBar:addButtons(buttons) - if not self.children then - self.children = { } - end - - for _,button in pairs(buttons) do - if button.UIElement then - table.insert(self.children, button) - else - local buttonProperties = { - x = self.lastx, - width = #button.text + self.spacing, - centered = false, - } - self.lastx = self.lastx + buttonProperties.width - UI:mergeProperties(buttonProperties, button) - - button = UI[self.buttonClass](buttonProperties) - if button.name then - self[button.name] = button - else - table.insert(self.children, button) - end - - if button.dropdown then - button.dropmenu = UI.DropMenu { buttons = button.dropdown } - end - end - end - if self.parent then - self:initChildren() - end -end - -function UI.MenuBar:getActive(menuItem) - return not menuItem.inactive -end - -function UI.MenuBar:eventHandler(event) - if event.type == 'button_press' and event.button.dropmenu then - if event.button.dropmenu.enabled then - event.button.dropmenu:hide() - return true - else - local x, y = getPosition(event.button) - if x + event.button.dropmenu.width > self.width then - x = self.width - event.button.dropmenu.width + 1 - end - for _,c in pairs(event.button.dropmenu.children) do - if not c.spacer then - c.inactive = not self:getActive(c) - end - end - event.button.dropmenu:show(x, y + 1) - end - return true - end -end - ---[[-- DropMenuItem --]]-- -UI.DropMenuItem = class(UI.Button) -UI.DropMenuItem.defaults = { - UIElement = 'DropMenuItem', - textColor = colors.black, - backgroundColor = colors.white, - textFocusColor = colors.white, - textInactiveColor = colors.lightGray, - backgroundFocusColor = colors.lightGray, -} -function UI.DropMenuItem:eventHandler(event) - if event.type == 'button_activate' then - self.parent:hide() - end - return UI.Button.eventHandler(self, event) -end - ---[[-- DropMenu --]]-- -UI.DropMenu = class(UI.MenuBar) -UI.DropMenu.defaults = { - UIElement = 'DropMenu', - backgroundColor = colors.white, - buttonClass = 'DropMenuItem', -} -function UI.DropMenu:setParent() - UI.MenuBar.setParent(self) - - local maxWidth = 1 - for y,child in ipairs(self.children) do - child.x = 1 - child.y = y - if #(child.text or '') > maxWidth then - maxWidth = #child.text - end - end - for _,child in ipairs(self.children) do - child.width = maxWidth + 2 - if child.spacer then - child.text = string.rep('-', child.width - 2) - end - end - - self.height = #self.children + 1 - self.width = maxWidth + 2 - self.ow = self.width - - self.canvas = self:addLayer() -end - -function UI.DropMenu:enable() - self.enabled = false -end - -function UI.DropMenu:show(x, y) - self.x, self.y = x, y - self.canvas:move(x, y) - self.canvas:setVisible(true) - - self.enabled = true - for _,child in pairs(self.children) do - child:enable() - end - - self:draw() - self:capture(self) - self:focusFirst() -end - -function UI.DropMenu:hide() - self:disable() - self.canvas:setVisible(false) - self:release(self) -end - -function UI.DropMenu:eventHandler(event) - if event.type == 'focus_lost' and self.enabled then - if not Util.contains(self.children, event.focused) then - self:hide() - end - elseif event.type == 'mouse_out' and self.enabled then - self:hide() - self:refocus() - else - return UI.MenuBar.eventHandler(self, event) - end - return true -end - ---[[-- TabBarMenuItem --]]-- -UI.TabBarMenuItem = class(UI.Button) -UI.TabBarMenuItem.defaults = { - UIElement = 'TabBarMenuItem', - event = 'tab_select', - textColor = colors.black, - selectedBackgroundColor = colors.cyan, - unselectedBackgroundColor = colors.lightGray, - backgroundColor = colors.lightGray, -} -function UI.TabBarMenuItem:draw() - if self.selected then - self.backgroundColor = self.selectedBackgroundColor - self.backgroundFocusColor = self.selectedBackgroundColor - else - self.backgroundColor = self.unselectedBackgroundColor - self.backgroundFocusColor = self.unselectedBackgroundColor - end - UI.Button.draw(self) -end - ---[[-- TabBar --]]-- -UI.TabBar = class(UI.MenuBar) -UI.TabBar.defaults = { - UIElement = 'TabBar', - buttonClass = 'TabBarMenuItem', - selectedBackgroundColor = colors.cyan, -} -function UI.TabBar:enable() - UI.MenuBar.enable(self) - if not Util.find(self.children, 'selected', true) then - local menuItem = self:getFocusables()[1] - if menuItem then - menuItem.selected = true - end - end -end - -function UI.TabBar:eventHandler(event) - if event.type == 'tab_select' then - local selected, si = Util.find(self:getFocusables(), 'uid', event.button.uid) - local previous, pi = Util.find(self:getFocusables(), 'selected', true) - - if si ~= pi then - selected.selected = true - previous.selected = false - self:emit({ type = 'tab_change', current = si, last = pi, tab = selected }) - end - UI.MenuBar.draw(self) - end - return UI.MenuBar.eventHandler(self, event) -end - -function UI.TabBar:selectTab(text) - local menuItem = Util.find(self.children, 'text', text) - if menuItem then - menuItem.selected = true - end -end - ---[[-- Tabs --]]-- -UI.Tabs = class(UI.Window) -UI.Tabs.defaults = { - UIElement = 'Tabs', -} -function UI.Tabs:postInit() - self:add(self) -end - -function UI.Tabs:add(children) - local buttons = { } - for _,child in pairs(children) do - if type(child) == 'table' and child.UIElement and child.tabTitle then - child.y = 2 - table.insert(buttons, { - text = child.tabTitle, - event = 'tab_select', - tabUid = child.uid, - }) - end - end - - if not self.tabBar then - self.tabBar = UI.TabBar({ - buttons = buttons, - }) - else - self.tabBar:addButtons(buttons) - end - - if self.parent then - return UI.Window.add(self, children) - end -end - -function UI.Tabs:enable() - self.enabled = true - self.tabBar:enable() - - local menuItem = Util.find(self.tabBar.children, 'selected', true) - - for _,child in pairs(self.children) do - if child.uid == menuItem.tabUid then - child:enable() - self:emit({ type = 'tab_activate', activated = child }) - elseif child.tabTitle then - child:disable() - end - end -end - -function UI.Tabs:eventHandler(event) - if event.type == 'tab_change' then - local tab = self:find(event.tab.tabUid) - if event.current > event.last then - tab:addTransition('slideLeft') - else - tab:addTransition('slideRight') - end - - for _,child in pairs(self.children) do - if child.uid == event.tab.tabUid then - child:enable() - elseif child.tabTitle then - child:disable() - end - end - self:emit({ type = 'tab_activate', activated = tab }) - tab:draw() - end -end - ---[[-- Wizard --]]-- -UI.Wizard = class(UI.Window) -UI.Wizard.defaults = { - UIElement = 'Wizard', - pages = { }, -} -function UI.Wizard:postInit() - self.cancelButton = UI.Button { - x = 2, y = -1, - text = 'Cancel', - event = 'cancel', - } - self.previousButton = UI.Button { - x = -18, y = -1, - text = '< Back', - event = 'previousView', - } - self.nextButton = UI.Button { - x = -9, y = -1, - text = 'Next >', - event = 'nextView', - } - - Util.merge(self, self.pages) - for _, child in pairs(self.pages) do - child.ey = -2 - end -end - -function UI.Wizard:add(pages) - Util.merge(self.pages, pages) - Util.merge(self, pages) - - for _, child in pairs(self.pages) do - child.ey = child.ey or -2 - end - - if self.parent then - self:initChildren() - end -end - -function UI.Wizard:getPage(index) - return Util.find(self.pages, 'index', index) -end - -function UI.Wizard:enable(...) - self.enabled = true - self.index = 1 - local initial = self:getPage(1) - for _,child in pairs(self.children) do - if child == initial or not child.index then - child:enable(...) - else - child:disable() - end - end - self:emit({ type = 'enable_view', next = initial }) -end - -function UI.Wizard:isViewValid() - local currentView = self:getPage(self.index) - return not currentView.validate and true or currentView:validate() -end - -function UI.Wizard:eventHandler(event) - if event.type == 'nextView' then - local currentView = self:getPage(self.index) - if self:isViewValid() then - self.index = self.index + 1 - local nextView = self:getPage(self.index) - currentView:emit({ type = 'enable_view', next = nextView, current = currentView }) - end - - elseif event.type == 'previousView' then - local currentView = self:getPage(self.index) - local nextView = self:getPage(self.index - 1) - if nextView then - self.index = self.index - 1 - currentView:emit({ type = 'enable_view', prev = nextView, current = currentView }) - end - return true - - elseif event.type == 'wizard_complete' then - if self:isViewValid() then - self:emit({ type = 'accept' }) - end - - elseif event.type == 'enable_view' then - if event.current then - if event.next then - self:addTransition('slideLeft') - elseif event.prev then - self:addTransition('slideRight') - end - event.current:disable() - end - - local current = event.next or event.prev - if not current then error('property "index" is required on wizard pages') end - - if self:getPage(self.index - 1) then - self.previousButton:enable() - else - self.previousButton:disable() - end - - if self:getPage(self.index + 1) then - self.nextButton.text = 'Next >' - self.nextButton.event = 'nextView' - else - self.nextButton.text = 'Accept' - self.nextButton.event = 'wizard_complete' - end - -- a new current view - current:enable() - self:draw() - end -end - ---[[-- SlideOut --]]-- -UI.SlideOut = class(UI.Window) -UI.SlideOut.defaults = { - UIElement = 'SlideOut', - pageType = 'modal', -} -function UI.SlideOut:setParent() - UI.Window.setParent(self) - self.canvas = self:addLayer() -end - -function UI.SlideOut:enable() - self.enabled = false -end - -function UI.SlideOut:show(...) - self:addTransition('expandUp') - self.canvas:setVisible(true) - self.enabled = true - for _,child in pairs(self.children) do - child:enable(...) - end - self:draw() - self:capture(self) - self:focusFirst() -end - -function UI.SlideOut:disable() - self.canvas:setVisible(false) - self.enabled = false - if self.children then - for _,child in pairs(self.children) do - child:disable() - end - end -end - -function UI.SlideOut:hide() - self:disable() - self:release(self) - self:refocus() -end - -function UI.SlideOut:eventHandler(event) - if event.type == 'slide_show' then - self:show() - return true - - elseif event.type == 'slide_hide' then - self:hide() - return true - end -end - ---[[-- Embedded --]]-- -UI.Embedded = class(UI.Window) -UI.Embedded.defaults = { - UIElement = 'Embedded', - backgroundColor = colors.black, - textColor = colors.white, - accelerators = { - up = 'scroll_up', - down = 'scroll_down', - } -} - -function UI.Embedded:setParent() - UI.Window.setParent(self) - self.win = window.create(UI.term.device, 1, 1, self.width, self.height, false) - Canvas.scrollingWindow(self.win, self.x, self.y) - self.win.setParent(UI.term.device) - self.win.setMaxScroll(100) - - local canvas = self:getCanvas() - self.win.canvas.parent = canvas - table.insert(canvas.layers, self.win.canvas) - self.canvas = self.win.canvas - - self.win.setCursorPos(1, 1) - self.win.setBackgroundColor(self.backgroundColor) - self.win.setTextColor(self.textColor) - self.win.clear() -end - -function UI.Embedded:draw() - self.canvas:dirty() -end - -function UI.Embedded:enable() - self.canvas:setVisible(true) - UI.Window.enable(self) -end - -function UI.Embedded:disable() - self.canvas:setVisible(false) - UI.Window.disable(self) -end - -function UI.Embedded:eventHandler(event) - if event.type == 'scroll_up' then - self.win.scrollUp() - return true - elseif event.type == 'scroll_down' then - self.win.scrollDown() - return true - end -end - -function UI.Embedded:focus() - -- allow scrolling -end - ---[[-- Notification --]]-- -UI.Notification = class(UI.Window) -UI.Notification.defaults = { - UIElement = 'Notification', - backgroundColor = colors.gray, - height = 3, -} -function UI.Notification:draw() -end - -function UI.Notification:enable() - self.enabled = false -end - -function UI.Notification:error(value, timeout) - self.backgroundColor = colors.red - Sound.play('entity.villager.no', .5) - self:display(value, timeout) -end - -function UI.Notification:info(value, timeout) - self.backgroundColor = colors.gray - self:display(value, timeout) -end - -function UI.Notification:success(value, timeout) - self.backgroundColor = colors.green - self:display(value, timeout) -end - -function UI.Notification:cancel() - if self.canvas then - Event.cancelNamedTimer('notificationTimer') - self.enabled = false - self.canvas:removeLayer() - self.canvas = nil - end -end - -function UI.Notification:display(value, timeout) - self.enabled = true - local lines = Util.wordWrap(value, self.width - 2) - self.height = #lines + 1 - self.y = self.parent.height - self.height + 1 - if self.canvas then - self.canvas:removeLayer() - end - - self.canvas = self:addLayer(self.backgroundColor, self.textColor) - self:addTransition('expandUp', { ticks = self.height }) - self.canvas:setVisible(true) - self:clear() - for k,v in pairs(lines) do - self:write(2, k, v) - end - - Event.addNamedTimer('notificationTimer', timeout or 3, false, function() - self:cancel() - self:sync() - end) -end - ---[[-- Throttle --]]-- -UI.Throttle = class(UI.Window) -UI.Throttle.defaults = { - UIElement = 'Throttle', - backgroundColor = colors.gray, - bordercolor = colors.cyan, - height = 4, - width = 10, - timeout = .075, - ctr = 0, - image = { - ' //) (O )~@ &~&-( ?Q ', - ' //) (O )- @ \\-( ?) && ', - ' //) (O ), @ \\-(?) && ', - ' //) (O ). @ \\-d ) (@ ' - } -} -function UI.Throttle:setParent() - self.x = math.ceil((self.parent.width - self.width) / 2) - self.y = math.ceil((self.parent.height - self.height) / 2) - UI.Window.setParent(self) -end - -function UI.Throttle:enable() - self.c = os.clock() - self.enabled = false -end - -function UI.Throttle:disable() - if self.canvas then - self.enabled = false - self.canvas:removeLayer() - self.canvas = nil - self.ctr = 0 - end -end - -function UI.Throttle:update() - local cc = os.clock() - if cc > self.c + self.timeout then - os.sleep(0) - self.c = os.clock() - self.enabled = true - if not self.canvas then - self.canvas = self:addLayer(self.backgroundColor, self.borderColor) - self.canvas:setVisible(true) - self:clear(self.borderColor) - end - local image = self.image[self.ctr + 1] - local width = self.width - 2 - for i = 0, #self.image do - self:write(2, i + 1, image:sub(width * i + 1, width * i + width), - self.backgroundColor, self.textColor) - end - - self.ctr = (self.ctr + 1) % #self.image - - self:sync() - end -end - ---[[-- StatusBar --]]-- -UI.StatusBar = class(UI.Window) -UI.StatusBar.defaults = { - UIElement = 'StatusBar', - backgroundColor = colors.lightGray, - textColor = colors.gray, - height = 1, - ey = -1, -} -function UI.StatusBar:adjustWidth() - -- Can only have 1 adjustable width - if self.columns then - local w = self.width - #self.columns - 1 - for _,c in pairs(self.columns) do - if c.width then - c.cw = c.width -- computed width - w = w - c.width - end - end - for _,c in pairs(self.columns) do - if not c.width then - c.cw = w - end - end - end -end - -function UI.StatusBar:resize() - UI.Window.resize(self) - self:adjustWidth() -end - -function UI.StatusBar:setParent() - UI.Window.setParent(self) - self:adjustWidth() -end - -function UI.StatusBar:setStatus(status) - if self.values ~= status then - self.values = status - self:draw() - end -end - -function UI.StatusBar:setValue(name, value) - if not self.values then - self.values = { } - end - self.values[name] = value -end - -function UI.StatusBar:getValue(name) - if self.values then - return self.values[name] - end -end - -function UI.StatusBar:timedStatus(status, timeout) - timeout = timeout or 3 - self:write(2, 1, Util.widthify(status, self.width-2), self.backgroundColor) - Event.addNamedTimer('statusTimer', timeout, false, function() - if self.parent.enabled then - self:draw() - self:sync() - end - end) -end - -function UI.StatusBar:getColumnWidth(name) - local c = Util.find(self.columns, 'key', name) - return c and c.cw -end - -function UI.StatusBar:setColumnWidth(name, width) - local c = Util.find(self.columns, 'key', name) - if c then - c.cw = width - end -end - -function UI.StatusBar:draw() - if not self.values then - self:clear() - elseif type(self.values) == 'string' then - self:write(1, 1, Util.widthify(' ' .. self.values, self.width)) - else - local s = '' - for _,c in ipairs(self.columns) do - s = s .. ' ' .. Util.widthify(tostring(self.values[c.key] or ''), c.cw) - end - self:write(1, 1, Util.widthify(s, self.width)) - end -end - ---[[-- ProgressBar --]]-- -UI.ProgressBar = class(UI.Window) -UI.ProgressBar.defaults = { - UIElement = 'ProgressBar', - progressColor = colors.lime, - backgroundColor = colors.gray, - height = 1, - value = 0, -} -function UI.ProgressBar:draw() - self:clear() - local width = math.ceil(self.value / 100 * self.width) - self:clearArea(1, 1, width, self.height, self.progressColor) -end - ---[[-- VerticalMeter --]]-- -UI.VerticalMeter = class(UI.Window) -UI.VerticalMeter.defaults = { - UIElement = 'VerticalMeter', - backgroundColor = colors.gray, - meterColor = colors.lime, - width = 1, - value = 0, -} -function UI.VerticalMeter:draw() - local height = self.height - math.ceil(self.value / 100 * self.height) - self:clear() - self:clearArea(1, height + 1, self.width, self.height, self.meterColor) -end - ---[[-- TextEntry --]]-- -UI.TextEntry = class(UI.Window) -UI.TextEntry.defaults = { - UIElement = 'TextEntry', - value = '', - shadowText = '', - focused = false, - textColor = colors.white, - shadowTextColor = colors.gray, - backgroundColor = colors.black, -- colors.lightGray, - backgroundFocusColor = colors.black, --lightGray, - height = 1, - limit = 6, - pos = 0, - accelerators = { - [ 'control-c' ] = 'copy', - } -} -function UI.TextEntry:postInit() - self.value = tostring(self.value) -end - -function UI.TextEntry:setValue(value) - self.value = value -end - -function UI.TextEntry:setPosition(pos) - self.pos = pos -end - -function UI.TextEntry:updateScroll() - if not self.scroll then - self.scroll = 0 - end - - if not self.pos then - self.pos = #tostring(self.value) - self.scroll = 0 - elseif self.pos > #tostring(self.value) then - self.pos = #tostring(self.value) - self.scroll = 0 - end - - if self.pos - self.scroll > self.width - 2 then - self.scroll = self.pos - (self.width - 2) - elseif self.pos < self.scroll then - self.scroll = self.pos - end -end - -function UI.TextEntry:draw() - local bg = self.backgroundColor - local tc = self.textColor - if self.focused then - bg = self.backgroundFocusColor - end - - self:updateScroll() - local text = tostring(self.value) - if #text > 0 then - if self.scroll and self.scroll > 0 then - text = text:sub(1 + self.scroll) - end - if self.mask then - text = _rep('*', #text) - end - else - tc = self.shadowTextColor - text = self.shadowText - end - - self:write(1, 1, ' ' .. Util.widthify(text, self.width - 2) .. ' ', bg, tc) - if self.focused then - self:setCursorPos(self.pos-self.scroll+2, 1) - end -end - -function UI.TextEntry:reset() - self.pos = 0 - self.value = '' - self:draw() - self:updateCursor() -end - -function UI.TextEntry:updateCursor() - self:updateScroll() - self:setCursorPos(self.pos-self.scroll+2, 1) -end - -function UI.TextEntry:focus() - self:draw() - if self.focused then - self:setCursorBlink(true) - else - self:setCursorBlink(false) - end -end - ---[[ - A few lines below from theoriginalbit - http://www.computercraft.info/forums2/index.php?/topic/16070-read-and-limit-length-of-the-input-field/ ---]] -function UI.TextEntry:eventHandler(event) - if event.type == 'key' then - local ch = event.key - if ch == 'left' then - if self.pos > 0 then - self.pos = math.max(self.pos-1, 0) - self:draw() - end - elseif ch == 'right' then - local input = tostring(self.value) - if self.pos < #input then - self.pos = math.min(self.pos+1, #input) - self:draw() - end - elseif ch == 'home' then - self.pos = 0 - self:draw() - elseif ch == 'end' then - self.pos = #tostring(self.value) - self:draw() - elseif ch == 'backspace' then - if self.pos > 0 then - local input = tostring(self.value) - self.value = input:sub(1, self.pos-1) .. input:sub(self.pos+1) - self.pos = self.pos - 1 - self:draw() - self:emit({ type = 'text_change', text = self.value, element = self }) - end - elseif ch == 'delete' then - local input = tostring(self.value) - if self.pos < #input then - self.value = input:sub(1, self.pos) .. input:sub(self.pos+2) - self:draw() - self:emit({ type = 'text_change', text = self.value, element = self }) - end - elseif #ch == 1 then - local input = tostring(self.value) - if #input < self.limit then - self.value = input:sub(1, self.pos) .. ch .. input:sub(self.pos+1) - self.pos = self.pos + 1 - self:draw() - self:emit({ type = 'text_change', text = self.value, element = self }) - end - else - return false - end - return true - - elseif event.type == 'copy' then - os.queueEvent('clipboard_copy', self.value) - - elseif event.type == 'paste' then - local input = tostring(self.value) - local text = event.text - if #input + #text > self.limit then - text = text:sub(1, self.limit-#input) - end - self.value = input:sub(1, self.pos) .. text .. input:sub(self.pos+1) - self.pos = self.pos + #text - self:draw() - self:updateCursor() - self:emit({ type = 'text_change', text = self.value, element = self }) - return true - - elseif event.type == 'mouse_click' then - if self.focused and event.x > 1 then - self.pos = event.x + self.scroll - 2 - self:updateCursor() - return true - end - elseif event.type == 'mouse_rightclick' then - local input = tostring(self.value) - if #input > 0 then - self:reset() - self:emit({ type = 'text_change', text = self.value, element = self }) - end - end - - return false -end - ---[[-- Chooser --]]-- -UI.Chooser = class(UI.Window) -UI.Chooser.defaults = { - UIElement = 'Chooser', - choices = { }, - nochoice = 'Select', - backgroundFocusColor = colors.lightGray, - textInactiveColor = colors.gray, - leftIndicator = '<', - rightIndicator = '>', - height = 1, -} -function UI.Chooser:setParent() - if not self.width and not self.ex then - self.width = 1 - for _,v in pairs(self.choices) do - if #v.name > self.width then - self.width = #v.name - end - end - self.width = self.width + 4 - end - UI.Window.setParent(self) -end - -function UI.Chooser:draw() - local bg = self.backgroundColor - if self.focused then - bg = self.backgroundFocusColor - end - local fg = self.inactive and self.textInactiveColor or self.textColor - local choice = Util.find(self.choices, 'value', self.value) - local value = self.nochoice - if choice then - value = choice.name - end - self:write(1, 1, self.leftIndicator, self.backgroundColor, colors.black) - self:write(2, 1, ' ' .. Util.widthify(tostring(value), self.width-4) .. ' ', bg, fg) - self:write(self.width, 1, self.rightIndicator, self.backgroundColor, colors.black) -end - -function UI.Chooser:focus() - self:draw() -end - -function UI.Chooser:eventHandler(event) - if event.type == 'key' then - if event.key == 'right' or event.key == 'space' then - local _,k = Util.find(self.choices, 'value', self.value) - local choice - if k and k < #self.choices then - choice = self.choices[k+1] - else - choice = self.choices[1] - end - self.value = choice.value - self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice }) - self:draw() - return true - elseif event.key == 'left' then - local _,k = Util.find(self.choices, 'value', self.value) - local choice - if k and k > 1 then - choice = self.choices[k-1] - else - choice = self.choices[#self.choices] - end - self.value = choice.value - self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice }) - self:draw() - return true - end - elseif event.type == 'mouse_click' then - if event.x == 1 then - self:emit({ type = 'key', key = 'left' }) - return true - elseif event.x == self.width then - self:emit({ type = 'key', key = 'right' }) - return true - end - end -end - ---[[-- Chooser --]]-- -UI.Checkbox = class(UI.Window) -UI.Checkbox.defaults = { - UIElement = 'Checkbox', - nochoice = 'Select', - checkedIndicator = 'X', - leftMarker = '[', - rightMarker = ']', - value = false, - textColor = colors.white, - backgroundColor = colors.black, - backgroundFocusColor = colors.lightGray, - height = 1, - width = 3, - accelerators = { - space = 'checkbox_toggle', - mouse_click = 'checkbox_toggle', - } -} -function UI.Checkbox:draw() - local bg = self.backgroundColor - if self.focused then - bg = self.backgroundFocusColor - end - if type(self.value) == 'string' then - self.value = nil -- TODO: fix form - end - local text = string.format('[%s]', not self.value and ' ' or self.checkedIndicator) - self:write(1, 1, text, bg) - self:write(1, 1, self.leftMarker, self.backgroundColor, self.textColor) - self:write(2, 1, not self.value and ' ' or self.checkedIndicator, bg) - self:write(3, 1, self.rightMarker, self.backgroundColor, self.textColor) -end - -function UI.Checkbox:focus() - self:draw() -end - -function UI.Checkbox:reset() - self.value = false -end - -function UI.Checkbox:eventHandler(event) - if event.type == 'checkbox_toggle' then - self.value = not self.value - self:emit({ type = 'checkbox_change', checked = self.value, element = self }) - self:draw() - return true - end -end - ---[[-- Text --]]-- -UI.Text = class(UI.Window) -UI.Text.defaults = { - UIElement = 'Text', - value = '', - height = 1, -} -function UI.Text:setParent() - if not self.width and not self.ex then - self.width = #tostring(self.value) - end - UI.Window.setParent(self) -end - -function UI.Text:draw() - self:write(1, 1, Util.widthify(self.value or '', self.width), self.backgroundColor) -end - ---[[-- ScrollBar --]]-- -UI.ScrollBar = class(UI.Window) -UI.ScrollBar.defaults = { - UIElement = 'ScrollBar', - lineChar = '|', - sliderChar = '#', - upArrowChar = '^', - downArrowChar = 'v', - scrollbarColor = colors.lightGray, - value = '', - width = 1, - x = -1, - ey = -1, -} -function UI.ScrollBar:draw() - local parent = self.parent - local view = parent:getViewArea() - - if view.totalHeight > view.height then - local maxScroll = view.totalHeight - view.height - local percent = view.offsetY / maxScroll - local sliderSize = math.max(1, Util.round(view.height / view.totalHeight * (view.height - 2))) - local x = self.width - - local row = view.y - if not view.static then -- does the container scroll ? - self.y = row -- if so, move the scrollbar onscreen - row = 1 - end - - for i = 1, view.height - 2 do - self:write(x, row + i, self.lineChar, nil, self.scrollbarColor) - end - - local y = Util.round((view.height - 2 - sliderSize) * percent) - for i = 1, sliderSize do - self:write(x, row + y + i, self.sliderChar, nil, self.scrollbarColor) - end - - local color = self.scrollbarColor - if view.offsetY > 0 then - color = colors.white - end - self:write(x, row, self.upArrowChar, nil, color) - - color = self.scrollbarColor - if view.offsetY + view.height < view.totalHeight then - color = colors.white - end - self:write(x, row + view.height - 1, self.downArrowChar, nil, color) - end -end - -function UI.ScrollBar:eventHandler(event) - if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then - if event.x == 1 then - local view = self.parent:getViewArea() - if view.totalHeight > view.height then - if event.y == view.y then - self:emit({ type = 'scroll_up'}) - elseif event.y == self.height then - self:emit({ type = 'scroll_down'}) - -- else - -- ... percentage ... - end - end - return true - end - end -end - ---[[-- TextArea --]]-- -UI.TextArea = class(UI.Viewport) -UI.TextArea.defaults = { - UIElement = 'TextArea', - marginRight = 2, - value = '', -} -function UI.TextArea:postInit() - self.scrollBar = UI.ScrollBar() -end - -function UI.TextArea:setText(text) - self.offy = 0 - self.ymax = nil - self.value = text - self:draw() -end - -function UI.TextArea:focus() - -- allow keyboard scrolling -end - -function UI.TextArea:draw() - self:clear() --- self:setCursorPos(1, 1) - self.cursorX, self.cursorY = 1, 1 - self:print(self.value) - self.ymax = self.cursorY + 1 - - for _,child in pairs(self.children) do - if child.enabled then - child:draw() - end - end -end - ---[[-- Form --]]-- -UI.Form = class(UI.Window) -UI.Form.defaults = { - UIElement = 'Form', - values = { }, - margin = 2, - event = 'form_complete', -} -function UI.Form:postInit() - self:createForm() -end - -function UI.Form:reset() - for _,child in pairs(self.children) do - if child.reset then - child:reset() - end - end -end - -function UI.Form:setValues(values) - self:reset() - self.values = values - for _,child in pairs(self.children) do - if child.formKey then - -- this should be child:setValue(self.values[child.formKey]) - -- so chooser can set default choice if null - -- null should be valid as well - child.value = self.values[child.formKey] or '' - end - end -end - -function UI.Form:createForm() - self.children = self.children or { } - - if not self.labelWidth then - self.labelWidth = 1 - for _, child in pairs(self) do - if type(child) == 'table' and child.UIElement then - if child.formLabel then - self.labelWidth = math.max(self.labelWidth, #child.formLabel + 2) - end - end - end - end - - local y = self.margin - for _, child in pairs(self) do - if type(child) == 'table' and child.UIElement then - if child.formKey then - child.value = self.values[child.formKey] or '' - end - if child.formLabel then - child.x = self.labelWidth + self.margin - 1 - child.y = y - if not child.width and not child.ex then - child.ex = -self.margin - end - - table.insert(self.children, UI.Text { - x = self.margin, - y = y, - textColor = colors.black, - width = #child.formLabel, - value = child.formLabel, - }) - end - if child.formKey or child.formLabel then - y = y + 1 - end - end - end - - if not self.manualControls then - table.insert(self.children, UI.Button { - y = -self.margin, x = -12 - self.margin, - text = 'Ok', - event = 'form_ok', - }) - table.insert(self.children, UI.Button { - y = -self.margin, x = -7 - self.margin, - text = 'Cancel', - event = 'form_cancel', - }) - end -end - -function UI.Form:validateField(field) - if field.required then - if not field.value or #tostring(field.value) == 0 then - return false, 'Field is required' - end - end - if field.validate == 'numeric' then - if #tostring(field.value) > 0 then - if not tonumber(field.value) then - return false, 'Invalid number' - end - end - end - return true -end - -function UI.Form:save() - for _,child in pairs(self.children) do - if child.formKey then - local s, m = self:validateField(child) - if not s then - self:setFocus(child) - self:emit({ type = 'form_invalid', message = m, field = child }) - return false - end - end - end - for _,child in pairs(self.children) do - if child.formKey then - if (child.pruneEmpty and type(child.value) == 'string' and #child.value == 0) or - (child.pruneEmpty and type(child.value) == 'boolean' and not child.value) then - self.values[child.formKey] = nil - elseif child.validate == 'numeric' then - self.values[child.formKey] = tonumber(child.value) - else - self.values[child.formKey] = child.value - end - end - end - - return true -end - -function UI.Form:eventHandler(event) - if event.type == 'form_ok' then - if not self:save() then - return false - end - self:emit({ type = self.event, UIElement = self }) - else - return UI.Window.eventHandler(self, event) - end - return true -end - ---[[-- Dialog --]]-- -UI.Dialog = class(UI.Page) -UI.Dialog.defaults = { - UIElement = 'Dialog', - x = 7, - y = 4, - z = 2, - height = 7, - textColor = colors.black, - backgroundColor = colors.white, -} -function UI.Dialog:postInit() - self.titleBar = UI.TitleBar({ previousPage = true, title = self.title }) -end - -function UI.Dialog:setParent() - if not self.width then - self.width = self.parent.width - 11 - end - if self.width > self.parent.width then - self.width = self.parent.width - end - self.x = math.floor((self.parent.width - self.width) / 2) + 1 - self.y = math.floor((self.parent.height - self.height) / 2) + 1 - UI.Page.setParent(self) -end - -function UI.Dialog:disable() - self.previousPage.canvas.palette = self.oldPalette - UI.Page.disable(self) -end - -function UI.Dialog:enable(...) - self.oldPalette = self.previousPage.canvas.palette - self.previousPage.canvas:applyPalette(Canvas.darkPalette) - self:addTransition('grow') - UI.Page.enable(self, ...) -end - -function UI.Dialog:eventHandler(event) - if event.type == 'cancel' then - UI:setPreviousPage() - end - return UI.Page.eventHandler(self, event) -end - ---[[-- Image --]]-- -UI.Image = class(UI.Window) -UI.Image.defaults = { - UIElement = 'Image', - event = 'button_press', -} -function UI.Image:setParent() - if self.image then - self.height = #self.image - end - if self.image and not self.width then - self.width = #self.image[1] - end - UI.Window.setParent(self) -end - -function UI.Image:draw() - self:clear() - if self.image then - for y = 1, #self.image do - local line = self.image[y] - for x = 1, #line do - local ch = line[x] - if type(ch) == 'number' then - if ch > 0 then - self:write(x, y, ' ', ch) - end - else - self:write(x, y, ch) - end - end - end - end -end - -function UI.Image:setImage(image) - self.image = image -end - ---[[-- NftImage --]]-- -UI.NftImage = class(UI.Window) -UI.NftImage.defaults = { - UIElement = 'NftImage', - event = 'button_press', -} -function UI.NftImage:setParent() - if self.image then - self.height = self.image.height - end - if self.image and not self.width then - self.width = self.image.width - end - UI.Window.setParent(self) -end - -function UI.NftImage:draw() - if self.image then - for y = 1, self.image.height do - for x = 1, #self.image.text[y] do - self:write(x, y, self.image.text[y][x], self.image.bg[y][x], self.image.fg[y][x]) - end - end - else - self:clear() - end -end - -function UI.NftImage:setImage(image) - self.image = image -end - -UI:loadTheme('usr/config/ui.theme') -if Util.getVersion() >= 1.76 then - UI:loadTheme('sys/etc/ext.theme') -end - -UI:setDefaultDevice(UI.Device({ device = term.current() })) - -return UI diff --git a/sys/apis/ui/canvas.lua b/sys/apis/ui/canvas.lua deleted file mode 100644 index ea45ecf..0000000 --- a/sys/apis/ui/canvas.lua +++ /dev/null @@ -1,498 +0,0 @@ -local class = require('class') -local Region = require('ui.region') -local Util = require('util') - -local _rep = string.rep -local _sub = string.sub -local _gsub = string.gsub -local colors = _G.colors - -local Canvas = class() - -Canvas.colorPalette = { } -Canvas.darkPalette = { } -Canvas.grayscalePalette = { } - -for n = 1, 16 do - Canvas.colorPalette[2 ^ (n - 1)] = _sub("0123456789abcdef", n, n) - Canvas.grayscalePalette[2 ^ (n - 1)] = _sub("088888878877787f", n, n) - Canvas.darkPalette[2 ^ (n - 1)] = _sub("8777777f77fff77f", n, n) -end - -function Canvas:init(args) - self.x = 1 - self.y = 1 - self.layers = { } - - Util.merge(self, args) - - self.ex = self.x + self.width - 1 - self.ey = self.y + self.height - 1 - - if not self.palette then - if self.isColor then - self.palette = Canvas.colorPalette - else - self.palette = Canvas.grayscalePalette - end - end - - self.lines = { } - for i = 1, self.height do - self.lines[i] = { } - end -end - -function Canvas:move(x, y) - self.x, self.y = x, y - self.ex = self.x + self.width - 1 - self.ey = self.y + self.height - 1 -end - -function Canvas:resize(w, h) - for i = self.height, h do - self.lines[i] = { } - end - - while #self.lines > h do - table.remove(self.lines, #self.lines) - end - - if w ~= self.width then - for i = 1, self.height do - self.lines[i] = { dirty = true } - end - end - - self.ex = self.x + w - 1 - self.ey = self.y + h - 1 - self.width = w - self.height = h -end - -function Canvas:copy() - local b = Canvas({ - x = self.x, - y = self.y, - width = self.width, - height = self.height, - isColor = self.isColor, - }) - for i = 1, self.height do - b.lines[i].text = self.lines[i].text - b.lines[i].fg = self.lines[i].fg - b.lines[i].bg = self.lines[i].bg - end - return b -end - -function Canvas:addLayer(layer) - local canvas = Canvas({ - x = layer.x, - y = layer.y, - width = layer.width, - height = layer.height, - isColor = self.isColor, - }) - canvas.parent = self - table.insert(self.layers, canvas) - return canvas -end - -function Canvas:removeLayer() - for k, layer in pairs(self.parent.layers) do - if layer == self then - self:setVisible(false) - table.remove(self.parent.layers, k) - break - end - end -end - -function Canvas:setVisible(visible) - self.visible = visible - if not visible then - self.parent:dirty() - -- set parent's lines to dirty for each line in self - end -end - -function Canvas:write(x, y, text, bg, fg) - if bg then - bg = _rep(self.palette[bg], #text) - end - if fg then - fg = _rep(self.palette[fg], #text) - end - self:writeBlit(x, y, text, bg, fg) -end - -function Canvas:writeBlit(x, y, text, bg, fg) - if y > 0 and y <= #self.lines and x <= self.width then - local width = #text - - -- fix ffs - if x < 1 then - text = _sub(text, 2 - x) - if bg then - bg = _sub(bg, 2 - x) - end - if fg then - fg = _sub(fg, 2 - x) - end - width = width + x - 1 - x = 1 - end - - if x + width - 1 > self.width then - text = _sub(text, 1, self.width - x + 1) - if bg then - bg = _sub(bg, 1, self.width - x + 1) - end - if fg then - fg = _sub(fg, 1, self.width - x + 1) - end - width = #text - end - - if width > 0 then - - local function replace(sstr, pos, rstr, width) - if pos == 1 and width == self.width then - return rstr - elseif pos == 1 then - return rstr .. _sub(sstr, pos+width) - elseif pos + width > self.width then - return _sub(sstr, 1, pos-1) .. rstr - end - return _sub(sstr, 1, pos-1) .. rstr .. _sub(sstr, pos+width) - end - - local line = self.lines[y] - line.dirty = true - line.text = replace(line.text, x, text, width) - if fg then - line.fg = replace(line.fg, x, fg, width) - end - if bg then - line.bg = replace(line.bg, x, bg, width) - end - end - end -end - -function Canvas:writeLine(y, text, fg, bg) - self.lines[y].dirty = true - self.lines[y].text = text - self.lines[y].fg = fg - self.lines[y].bg = bg -end - -function Canvas:reset() - self.regions = nil -end - -function Canvas:clear(bg, fg) - local text = _rep(' ', self.width) - fg = _rep(self.palette[fg or colors.white], self.width) - bg = _rep(self.palette[bg or colors.black], self.width) - for i = 1, self.height do - self:writeLine(i, text, fg, bg) - end -end - -function Canvas:punch(rect) - if not self.regions then - self.regions = Region.new(self.x, self.y, self.ex, self.ey) - end - self.regions:subRect(rect.x, rect.y, rect.ex, rect.ey) -end - -function Canvas:blitClipped(device) - for _,region in ipairs(self.regions.region) do - self:blit(device, - { x = region[1] - self.x + 1, - y = region[2] - self.y + 1, - ex = region[3]- self.x + 1, - ey = region[4] - self.y + 1 }, - { x = region[1], y = region[2] }) - end -end - -function Canvas:redraw(device) - self:reset() - if #self.layers > 0 then - for _,layer in pairs(self.layers) do - self:punch(layer) - end - self:blitClipped(device) - else - self:blit(device) - end - self:clean() -end - -function Canvas:isDirty() - for _, line in pairs(self.lines) do - if line.dirty then - return true - end - end -end - -function Canvas:dirty() - for _, line in pairs(self.lines) do - line.dirty = true - end -end - -function Canvas:clean() - for _, line in pairs(self.lines) do - line.dirty = false - end -end - -function Canvas:render(device, layers) --- redrawAll ? - layers = layers or self.layers - if #layers > 0 then - self.regions = Region.new(self.x, self.y, self.ex, self.ey) - local l = Util.shallowCopy(layers) - for _, canvas in ipairs(layers) do - table.remove(l, 1) - if canvas.visible then - self:punch(canvas) - canvas:render(device, l) - end - end - self:blitClipped(device) - self:reset() - else - self:blit(device) - end - self:clean() -end - -function Canvas:blit(device, src, tgt) - src = src or { x = 1, y = 1, ex = self.ex - self.x + 1, ey = self.ey - self.y + 1 } - tgt = tgt or self - - for i = 0, src.ey - src.y do - local line = self.lines[src.y + i] - if line and line.dirty then - local t, fg, bg = line.text, line.fg, line.bg - if src.x > 1 or src.ex < self.ex then - t = _sub(t, src.x, src.ex) - fg = _sub(fg, src.x, src.ex) - bg = _sub(bg, src.x, src.ex) - end - --if tgt.y + i > self.ey then -- wrong place to do clipping ?? - -- break - --end - device.setCursorPos(tgt.x, tgt.y + i) - device.blit(t, fg, bg) - end - end -end - -function Canvas:applyPalette(palette) - - local lookup = { } - for n = 1, 16 do - lookup[self.palette[2 ^ (n - 1)]] = palette[2 ^ (n - 1)] - end - - for _, l in pairs(self.lines) do - l.fg = _gsub(l.fg, '%w', lookup) - l.bg = _gsub(l.bg, '%w', lookup) - l.dirty = true - end - - self.palette = palette -end - -function Canvas.convertWindow(win, parent, wx, wy) - local w, h = win.getSize() - - win.canvas = Canvas({ - x = wx, - y = wy, - width = w, - height = h, - isColor = win.isColor(), - }) - - function win.clear() - win.canvas:clear(win.getBackgroundColor(), win.getTextColor()) - end - - function win.clearLine() - local _, y = win.getCursorPos() - win.canvas:write(1, - y, - _rep(' ', win.canvas.width), - win.getBackgroundColor(), - win.getTextColor()) - end - - function win.write(str) - local x, y = win.getCursorPos() - win.canvas:write(x, - y, - str, - win.getBackgroundColor(), - win.getTextColor()) - win.setCursorPos(x + #str, y) - end - - function win.blit(text, fg, bg) - local x, y = win.getCursorPos() - win.canvas:writeBlit(x, y, text, bg, fg) - end - - function win.redraw() - win.canvas:redraw(parent) - end - - function win.scroll(n) - table.insert(win.canvas.lines, table.remove(win.canvas.lines, 1)) - win.canvas.lines[#win.canvas.lines].text = _rep(' ', win.canvas.width) - win.canvas:dirty() - end - - function win.reposition(x, y, width, height) - win.canvas.x, win.canvas.y = x, y - win.canvas:resize(width or win.canvas.width, height or win.canvas.height) - end - - win.clear() -end - -function Canvas.scrollingWindow(win, wx, wy) - local w, h = win.getSize() - local scrollPos = 0 - local maxScroll = h - - -- canvas lines are are a sliding window within the local lines table - local lines = { } - - local parent - local canvas = Canvas({ - x = wx, - y = wy, - width = w, - height = h, - isColor = win.isColor(), - }) - win.canvas = canvas - - local function scrollTo(p, forceRedraw) - local ms = #lines - canvas.height -- max scroll - p = math.min(math.max(p, 0), ms) -- normalize - - if p ~= scrollPos or forceRedraw then - scrollPos = p - for i = 1, canvas.height do - canvas.lines[i] = lines[i + scrollPos] - end - canvas:dirty() - end - end - - function win.blit(text, fg, bg) - local x, y = win.getCursorPos() - win.canvas:writeBlit(x, y, text, bg, fg) - win.redraw() - end - - function win.clear() - lines = { } - for i = 1, canvas.height do - lines[i] = canvas.lines[i] - end - scrollPos = 0 - canvas:clear(win.getBackgroundColor(), win.getTextColor()) - win.redraw() - end - - function win.clearLine() - local _, y = win.getCursorPos() - - scrollTo(#lines - canvas.height) - win.canvas:write(1, - y, - _rep(' ', win.canvas.width), - win.getBackgroundColor(), - win.getTextColor()) - win.redraw() - end - - function win.redraw() - if parent and canvas.visible then - local x, y = win.getCursorPos() - for i = 1, canvas.height do - local line = canvas.lines[i] - if line and line.dirty then - parent.setCursorPos(canvas.x, canvas.y + i - 1) - parent.blit(line.text, line.fg, line.bg) - line.dirty = false - end - end - win.setCursorPos(x, y) - end - end - - -- doesn't support negative scrolling... - function win.scroll(n) - for _ = 1, n do - lines[#lines + 1] = { - text = _rep(' ', canvas.width), - fg = _rep(canvas.palette[win.getTextColor()], canvas.width), - bg = _rep(canvas.palette[win.getBackgroundColor()], canvas.width), - } - end - - while #lines > maxScroll do - table.remove(lines, 1) - end - - scrollTo(maxScroll, true) - win.redraw() - end - - function win.scrollDown() - scrollTo(scrollPos + 1) - win.redraw() - end - - function win.scrollUp() - scrollTo(scrollPos - 1) - win.redraw() - end - - function win.setMaxScroll(ms) - maxScroll = ms - end - - function win.setParent(p) - parent = p - end - - function win.write(str) - str = tostring(str) or '' - - local x, y = win.getCursorPos() - scrollTo(#lines - canvas.height) - win.blit(str, - _rep(canvas.palette[win.getTextColor()], #str), - _rep(canvas.palette[win.getBackgroundColor()], #str)) - win.setCursorPos(x + #str, y) - end - - function win.reposition(x, y, width, height) - win.canvas.x, win.canvas.y = x, y - win.canvas:resize(width or win.canvas.width, height or win.canvas.height) - end - - win.clear() -end -return Canvas diff --git a/sys/apis/ui/fileui.lua b/sys/apis/ui/fileui.lua deleted file mode 100644 index 02e2bc7..0000000 --- a/sys/apis/ui/fileui.lua +++ /dev/null @@ -1,145 +0,0 @@ -local UI = require('ui') -local Util = require('util') - -local colors = _G.colors -local fs = _G.fs - -return function(args) - - local columns = { - { heading = 'Name', key = 'name' }, - } - - if UI.term.width > 28 then - table.insert(columns, - { heading = 'Size', key = 'size', width = 5 } - ) - end - - args = args or { } - - local selectFile = UI.Dialog { - x = args.x or 3, - y = args.y or 2, - z = args.z or 2, --- rex = args.rex or -3, --- rey = args.rey or -3, - height = args.height, - width = args.width, - title = 'Select File', - grid = UI.ScrollingGrid { - x = 2, - y = 2, - ex = -2, - ey = -4, - path = '', - sortColumn = 'name', - columns = columns, - }, - path = UI.TextEntry { - x = 2, - y = -2, - ex = -11, - limit = 256, - accelerators = { - enter = 'path_enter', - } - }, - cancel = UI.Button { - text = 'Cancel', - x = -9, - y = -2, - event = 'cancel', - }, - } - - function selectFile:enable(path, fn) - self:setPath(path) - self.fn = fn - UI.Dialog.enable(self) - end - - function selectFile:setPath(path) - self.grid.dir = path - while not fs.isDir(self.grid.dir) do - self.grid.dir = fs.getDir(self.grid.dir) - end - - self.path.value = self.grid.dir - end - - function selectFile.grid:draw() - local files = fs.listEx(self.dir) - if #self.dir > 0 then - table.insert(files, { - name = '..', - isDir = true, - }) - end - self:setValues(files) - self:setIndex(1) - UI.Grid.draw(self) - end - - function selectFile.grid:getDisplayValues(row) - if row.size then - row = Util.shallowCopy(row) - row.size = Util.toBytes(row.size) - end - return row - end - - function selectFile.grid:getRowTextColor(file) - if file.isDir then - return colors.cyan - end - if file.isReadOnly then - return colors.pink - end - return colors.white - end - - function selectFile.grid:sortCompare(a, b) - if self.sortColumn == 'size' then - return a.size < b.size - end - if a.isDir == b.isDir then - return a.name:lower() < b.name:lower() - end - return a.isDir - end - - function selectFile:eventHandler(event) - - if event.type == 'grid_select' then - self.grid.dir = fs.combine(self.grid.dir, event.selected.name) - self.path.value = self.grid.dir - if event.selected.isDir then - self.grid:draw() - self.path:draw() - else - UI:setPreviousPage() - self.fn(self.path.value) - end - - elseif event.type == 'path_enter' then - if fs.isDir(self.path.value) then - self:setPath(self.path.value) - self.grid:draw() - self.path:draw() - else - UI:setPreviousPage() - self.fn(self.path.value) - end - - elseif event.type == 'cancel' then - UI:setPreviousPage() - self.fn() - else - return UI.Dialog.eventHandler(self, event) - end - return true - end - - return selectFile -end diff --git a/sys/apis/ui/glasses.lua b/sys/apis/ui/glasses.lua deleted file mode 100644 index cd8a26f..0000000 --- a/sys/apis/ui/glasses.lua +++ /dev/null @@ -1,196 +0,0 @@ -local class = require('class') -local UI = require('ui') -local Event = require('event') -local Peripheral = require('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/sys/apis/ui/transition.lua b/sys/apis/ui/transition.lua deleted file mode 100644 index 16e2127..0000000 --- a/sys/apis/ui/transition.lua +++ /dev/null @@ -1,90 +0,0 @@ -local Tween = require('ui.tween') - -local Transition = { } - -function Transition.slideLeft(args) - local ticks = args.ticks or 6 - local easing = args.easing or 'outQuint' - local pos = { x = args.ex } - local tween = Tween.new(ticks, pos, { x = args.x }, easing) - local lastScreen = args.canvas:copy() - - return function(device) - local finished = tween:update(1) - local x = math.floor(pos.x) - lastScreen:dirty() - lastScreen:blit(device, { - x = args.ex - x + args.x, - y = args.y, - ex = args.ex, - ey = args.ey }, - { x = args.x, y = args.y }) - args.canvas:blit(device, { - x = args.x, - y = args.y, - ex = args.ex - x + args.x, - ey = args.ey }, - { x = x, y = args.y }) - return not finished - end -end - -function Transition.slideRight(args) - local ticks = args.ticks or 6 - local easing = args.easing or'outQuint' - local pos = { x = args.x } - local tween = Tween.new(ticks, pos, { x = args.ex }, easing) - local lastScreen = args.canvas:copy() - - return function(device) - local finished = tween:update(1) - local x = math.floor(pos.x) - lastScreen:dirty() - lastScreen:blit(device, { - x = args.x, - y = args.y, - ex = args.ex - x + args.x, - ey = args.ey }, - { x = x, y = args.y }) - args.canvas:blit(device, { - x = args.ex - x + args.x, - y = args.y, - ex = args.ex, - ey = args.ey }, - { x = args.x, y = args.y }) - return not finished - end -end - -function Transition.expandUp(args) - local ticks = args.ticks or 3 - local easing = args.easing or 'linear' - local pos = { y = args.ey + 1 } - local tween = Tween.new(ticks, pos, { y = args.y }, easing) - - return function(device) - local finished = tween:update(1) - args.canvas:blit(device, nil, { x = args.x, y = math.floor(pos.y) }) - return not finished - end -end - -function Transition.grow(args) - local ticks = args.ticks or 3 - local easing = args.easing or 'linear' - local tween = Tween.new(ticks, - { x = args.width / 2 - 1, y = args.height / 2 - 1, w = 1, h = 1 }, - { x = 1, y = 1, w = args.width, h = args.height }, easing) - - return function(device) - local finished = tween:update(1) - local subj = tween.subject - local rect = { x = math.floor(subj.x), y = math.floor(subj.y) } - rect.ex = math.floor(rect.x + subj.w - 1) - rect.ey = math.floor(rect.y + subj.h - 1) - args.canvas:blit(device, rect, { x = args.x + rect.x - 1, y = args.y + rect.y - 1}) - return not finished - end -end - -return Transition diff --git a/sys/apps/Files.lua b/sys/apps/Files.lua index 464ab75..b39ddd9 100644 --- a/sys/apps/Files.lua +++ b/sys/apps/Files.lua @@ -1,9 +1,9 @@ -_G.requireInjector(_ENV) - -local Config = require('config') -local Event = require('event') -local UI = require('ui') -local Util = require('util') +local Alt = require('opus.alternate') +local Config = require('opus.config') +local Event = require('opus.event') +local pastebin = require('opus.http.pastebin') +local UI = require('opus.ui') +local Util = require('opus.util') local colors = _G.colors local fs = _G.fs @@ -11,402 +11,540 @@ local multishell = _ENV.multishell local os = _G.os local shell = _ENV.shell +local FILE = 1 + UI:configure('Files', ...) -local config = { - showHidden = false, - showDirSizes = false, +local config = Config.load('Files', { + showHidden = false, + showDirSizes = false, +}) +config.associations = config.associations or { + nft = 'pain', + txt = 'edit', } -Config.load('Files', config) - local copied = { } local marked = { } local directories = { } local cutMode = false local function formatSize(size) - if size >= 1000000 then - return string.format('%dM', math.floor(size/1000000, 2)) - elseif size >= 1000 then - return string.format('%dK', math.floor(size/1000, 2)) - end - return size + if size >= 1000000 then + return string.format('%dM', math.floor(size/1000000, 2)) + elseif size >= 1000 then + return string.format('%dK', math.floor(size/1000, 2)) + end + return size end local Browser = UI.Page { - menuBar = UI.MenuBar { - buttons = { - { text = '^-', event = 'updir' }, - { text = 'File', dropdown = { - { text = 'Run', event = 'run' }, - { text = 'Edit e', event = 'edit' }, - { text = 'Shell s', event = 'shell' }, - UI.MenuBar.spacer, - { text = 'Quit q', event = 'quit' }, - } }, - { text = 'Edit', dropdown = { - { text = 'Cut ^x', event = 'cut' }, - { text = 'Copy ^c', event = 'copy' }, - { text = 'Copy path ', event = 'copy_path' }, - { text = 'Paste ^v', event = 'paste' }, - UI.MenuBar.spacer, - { text = 'Mark m', event = 'mark' }, - { text = 'Unmark all u', event = 'unmark' }, - UI.MenuBar.spacer, - { text = 'Delete del', event = 'delete' }, - } }, - { text = 'View', dropdown = { - { text = 'Refresh r', event = 'refresh' }, - { text = 'Hidden ^h', event = 'toggle_hidden' }, - { text = 'Dir Size ^s', event = 'toggle_dirSize' }, - } }, - }, - }, - grid = UI.ScrollingGrid { - columns = { - { heading = 'Name', key = 'name' }, - { key = 'flags', width = 2 }, - { heading = 'Size', key = 'fsize', width = 5 }, - }, - sortColumn = 'name', - y = 2, ey = -2, - }, - statusBar = UI.StatusBar { - columns = { - { key = 'status' }, - { key = 'totalSize', width = 6 }, - }, - }, - accelerators = { - q = 'quit', - e = 'edit', - s = 'shell', - r = 'refresh', - space = 'mark', - backspace = 'updir', - m = 'move', - u = 'unmark', - d = 'delete', - delete = 'delete', - [ 'control-h' ] = 'toggle_hidden', - [ 'control-s' ] = 'toggle_dirSize', - [ 'control-x' ] = 'cut', - [ 'control-c' ] = 'copy', - paste = 'paste', - }, + menuBar = UI.MenuBar { + buttons = { + { text = '^-', event = 'updir' }, + { text = 'File', dropdown = { + { text = 'Run', event = 'run', flags = FILE }, + { text = 'Edit e', event = 'edit', flags = FILE }, + { text = 'Cloud edit c', event = 'cedit', flags = FILE }, + { text = 'Pastebin put p', event = 'pastebin', flags = FILE }, + { text = 'Shell s', event = 'shell' }, + { spacer = true }, + { text = 'Quit ^q', event = 'quit' }, + } }, + { text = 'Edit', dropdown = { + { text = 'Cut ^x', event = 'cut' }, + { text = 'Copy ^c', event = 'copy' }, + { text = 'Copy path ', event = 'copy_path' }, + { text = 'Paste ^v', event = 'paste' }, + { spacer = true }, + { text = 'Mark m', event = 'mark' }, + { text = 'Unmark all u', event = 'unmark' }, + { spacer = true }, + { text = 'Delete del', event = 'delete' }, + } }, + { text = 'View', dropdown = { + { text = 'Refresh r', event = 'refresh' }, + { text = 'Hidden ^h', event = 'toggle_hidden' }, + { text = 'Dir Size ^s', event = 'toggle_dirSize' }, + } }, + { text = '\187', + x = -3, + dropdown = { + { text = 'Associations', event = 'associate' }, + } }, + }, + }, + grid = UI.ScrollingGrid { + columns = { + { heading = 'Name', key = 'name' }, + { key = 'flags', width = 2 }, + { heading = 'Size', key = 'fsize', width = 5 }, + }, + sortColumn = 'name', + y = 2, ey = -2, + }, + statusBar = UI.StatusBar { + columns = { + { key = 'status' }, + { key = 'totalSize', width = 6 }, + }, + }, + notification = UI.Notification { }, + associations = UI.SlideOut { + backgroundColor = colors.cyan, + menuBar = UI.MenuBar { + buttons = { + { text = 'Save', event = 'save' }, + { text = 'Cancel', event = 'cancel' }, + }, + }, + grid = UI.ScrollingGrid { + x = 2, ex = -6, y = 3, ey = -5, + columns = { + { heading = 'Extension', key = 'name' }, + { heading = 'Program', key = 'value' }, + }, + autospace = true, + sortColumn = 'name', + accelerators = { + delete = 'remove_entry', + }, + }, + remove = UI.Button { + x = -4, y = 6, + text = '-', event = 'remove_entry', help = 'Remove', + }, + form = UI.Form { + x = 3, y = -3, ey = -2, + margin = 1, + manualControls = true, + [1] = UI.TextEntry { + width = 20, + formLabel = 'Extension', formKey = 'name', + shadowText = 'extension', + required = true, + limit = 64, + }, + [2] = UI.TextEntry { + width = 20, + formLabel = 'Program', formKey = 'value', + shadowText = 'program', + required = true, + limit = 128, + }, + add = UI.Button { + x = -11, y = 1, + text = 'Add', event = 'add_association', + }, + }, + statusBar = UI.StatusBar { + backgroundColor = colors.cyan, + }, + }, + accelerators = { + [ 'control-q' ] = 'quit', + c = 'cedit', + e = 'edit', + s = 'shell', + p = 'pastebin', + r = 'refresh', + [ ' ' ] = 'mark', + m = 'mark', + backspace = 'updir', + u = 'unmark', + d = 'delete', + delete = 'delete', + [ 'control-h' ] = 'toggle_hidden', + [ 'control-s' ] = 'toggle_dirSize', + [ 'control-x' ] = 'cut', + [ 'control-c' ] = 'copy', + paste = 'paste', + }, } function Browser:enable() - UI.Page.enable(self) - self:setFocus(self.grid) + UI.Page.enable(self) + self:setFocus(self.grid) end function Browser.menuBar:getActive(menuItem) - local file = Browser.grid:getSelected() - if file then - if menuItem.event == 'edit' or menuItem.event == 'run' then - return not file.isDir - end - end - return true + local file = Browser.grid:getSelected() + if menuItem.flags == FILE then + return file and not file.isDir + end + return true end function Browser.grid:sortCompare(a, b) - if self.sortColumn == 'fsize' then - return a.size < b.size - elseif self.sortColumn == 'flags' then - return a.flags < b.flags - end - if a.isDir == b.isDir then - return a.name:lower() < b.name:lower() - end - return a.isDir + if self.sortColumn == 'fsize' then + return a.size < b.size + elseif self.sortColumn == 'flags' then + return a.flags < b.flags + end + if a.isDir == b.isDir then + return a.name:lower() < b.name:lower() + end + return a.isDir end function Browser.grid:getRowTextColor(file) - if file.marked then - return colors.green - end - if file.isDir then - return colors.cyan - end - if file.isReadOnly then - return colors.pink - end - return colors.white + if file.marked then + return colors.green + end + if file.isDir then + return colors.cyan + end + if file.isReadOnly then + return colors.pink + end + return colors.white end function Browser.grid:eventHandler(event) - if event.type == 'copy' then -- let copy be handled by parent - return false - end - return UI.ScrollingGrid.eventHandler(self, event) + if event.type == 'copy' then -- let copy be handled by parent + return false + end + return UI.ScrollingGrid.eventHandler(self, event) end function Browser.statusBar:draw() - if self.parent.dir then - local info = '#:' .. Util.size(self.parent.dir.files) - local numMarked = Util.size(marked) - if numMarked > 0 then - info = info .. ' M:' .. numMarked - end - self:setValue('info', info) - self:setValue('totalSize', formatSize(self.parent.dir.totalSize)) - UI.StatusBar.draw(self) - end + if self.parent.dir then + local info = '#:' .. Util.size(self.parent.dir.files) + local numMarked = Util.size(marked) + if numMarked > 0 then + info = info .. ' M:' .. numMarked + end + self:setValue('info', info) + self:setValue('totalSize', formatSize(self.parent.dir.totalSize)) + UI.StatusBar.draw(self) + end end function Browser:setStatus(status, ...) - self.statusBar:timedStatus(string.format(status, ...)) + self.notification:info(string.format(status, ...)) end function Browser:unmarkAll() - for _,m in pairs(marked) do - m.marked = false - end - Util.clear(marked) + for _,m in pairs(marked) do + m.marked = false + end + Util.clear(marked) end function Browser:getDirectory(directory) - local s, dir = pcall(function() + local s, dir = pcall(function() - local dir = directories[directory] - if not dir then - dir = { - name = directory, - size = 0, - files = { }, - totalSize = 0, - index = 1 - } - directories[directory] = dir - end + local dir = directories[directory] + if not dir then + dir = { + name = directory, + size = 0, + files = { }, + totalSize = 0, + index = 1 + } + directories[directory] = dir + end - self:updateDirectory(dir) + self:updateDirectory(dir) - return dir - end) + return dir + end) - return s, dir + return s, dir end function Browser:updateDirectory(dir) - dir.size = 0 - dir.totalSize = 0 - Util.clear(dir.files) + dir.size = 0 + dir.totalSize = 0 + Util.clear(dir.files) - local files = fs.listEx(dir.name) - if files then - dir.size = #files - for _, file in pairs(files) do - file.fullName = fs.combine(dir.name, file.name) - file.flags = '' - if not file.isDir then - dir.totalSize = dir.totalSize + file.size - file.fsize = formatSize(file.size) - else - if config.showDirSizes then - file.size = fs.getSize(file.fullName, true) + local files = fs.listEx(dir.name) + if files then + dir.size = #files + for _, file in pairs(files) do + file.fullName = fs.combine(dir.name, file.name) + file.flags = '' + if not file.isDir then + dir.totalSize = dir.totalSize + file.size + file.fsize = formatSize(file.size) + else + if config.showDirSizes then + file.size = fs.getSize(file.fullName, true) - dir.totalSize = dir.totalSize + file.size - file.fsize = formatSize(file.size) - end - file.flags = 'D' - end - if file.isReadOnly then - file.flags = file.flags .. 'R' - end - if config.showHidden or file.name:sub(1, 1) ~= '.' then - dir.files[file.fullName] = file - end - end - end + dir.totalSize = dir.totalSize + file.size + file.fsize = formatSize(file.size) + end + file.flags = 'D' + end + if file.isReadOnly then + file.flags = file.flags .. 'R' + end + if config.showHidden or file.name:sub(1, 1) ~= '.' then + dir.files[file.fullName] = file + end + end + end -- self.grid:update() -- self.grid:setIndex(dir.index) - self.grid:setValues(dir.files) + self.grid:setValues(dir.files) end function Browser:setDir(dirName, noStatus) - self:unmarkAll() + self:unmarkAll() - if self.dir then - self.dir.index = self.grid:getIndex() - end - local DIR = fs.combine('', dirName) - shell.setDir(DIR) - local s, dir = self:getDirectory(DIR) - if s then - self.dir = dir - elseif noStatus then - error(dir) - else - self:setStatus(dir) - self:setDir('', true) - return - end + if self.dir then + self.dir.index = self.grid:getIndex() + end + local DIR = fs.combine('', dirName) + shell.setDir(DIR) + local s, dir = self:getDirectory(DIR) + if s then + self.dir = dir + elseif noStatus then + error(dir) + else + self:setStatus(dir) + self:setDir('', true) + return + end - if not noStatus then - self.statusBar:setValue('status', '/' .. self.dir.name) - self.statusBar:draw() - end - self.grid:setIndex(self.dir.index) + if not noStatus then + self.statusBar:setValue('status', '/' .. self.dir.name) + self.statusBar:draw() + end + self.grid:setIndex(self.dir.index) end function Browser:run(...) - if multishell then - local tabId = shell.openTab(...) - multishell.setFocus(tabId) - else - shell.run(...) - Event.terminate = false - self:draw() - end + if multishell then + local tabId = shell.openTab(...) + multishell.setFocus(tabId) + else + shell.run(...) + Event.terminate = false + self:draw() + end end function Browser:hasMarked() - if Util.size(marked) == 0 then - local file = self.grid:getSelected() - if file then - file.marked = true - marked[file.fullName] = file - self.grid:draw() - end - end - return Util.size(marked) > 0 + if Util.size(marked) == 0 then + local file = self.grid:getSelected() + if file then + file.marked = true + marked[file.fullName] = file + self.grid:draw() + end + end + return Util.size(marked) > 0 end function Browser:eventHandler(event) - local file = self.grid:getSelected() + local file = self.grid:getSelected() - if event.type == 'quit' then - Event.exitPullEvents() + if event.type == 'quit' then + Event.exitPullEvents() - elseif event.type == 'edit' and file then - self:run('edit', file.name) + elseif event.type == 'edit' and file then + self:run('edit', file.name) - elseif event.type == 'shell' then - self:run('sys/apps/shell') + elseif event.type == 'cedit' and file then + self:run('cedit', file.name) + self:setStatus('Started cloud edit') - elseif event.type == 'refresh' then - self:updateDirectory(self.dir) - self.grid:draw() - self:setStatus('Refreshed') + elseif event.type == 'shell' then + self:run(Alt.get('shell')) - elseif event.type == 'toggle_hidden' then - config.showHidden = not config.showHidden - Config.update('Files', config) + elseif event.type == 'refresh' then + self:updateDirectory(self.dir) + self.grid:draw() + self:setStatus('Refreshed') - self:updateDirectory(self.dir) - self.grid:draw() - if not config.showHidden then - self:setStatus('Hiding hidden') - else - self:setStatus('Displaying hidden') - end + elseif event.type == 'associate' then + self.associations:show() - elseif event.type == 'toggle_dirSize' then - config.showDirSizes = not config.showDirSizes - Config.update('Files', config) + elseif event.type == 'pastebin' then + if file and not file.isDir then + local s, m = pastebin.put(file.fullName) + if s then + os.queueEvent('clipboard_copy', s) + self.notification:success(string.format('Uploaded as %s', s), 0) + else + self.notification:error(m) + end + end - self:updateDirectory(self.dir) - self.grid:draw() - if config.showDirSizes then - self:setStatus('Displaying dir sizes') - end + elseif event.type == 'toggle_hidden' then + config.showHidden = not config.showHidden + Config.update('Files', config) - elseif event.type == 'mark' and file then - file.marked = not file.marked - if file.marked then - marked[file.fullName] = file - else - marked[file.fullName] = nil - end - self.grid:draw() - self.statusBar:draw() + self:updateDirectory(self.dir) + self.grid:draw() + if not config.showHidden then + self:setStatus('Hiding hidden') + else + self:setStatus('Displaying hidden') + end - elseif event.type == 'unmark' then - self:unmarkAll() - self.grid:draw() - self:setStatus('Marked files cleared') + elseif event.type == 'toggle_dirSize' then + config.showDirSizes = not config.showDirSizes + Config.update('Files', config) - elseif event.type == 'grid_select' or event.type == 'run' then - if file then - if file.isDir then - self:setDir(file.fullName) - else - self:run(file.name) - end - end + self:updateDirectory(self.dir) + self.grid:draw() + if config.showDirSizes then + self:setStatus('Displaying dir sizes') + end - elseif event.type == 'updir' then - local dir = (self.dir.name:match("(.*/)")) - self:setDir(dir or '/') + elseif event.type == 'mark' and file then + file.marked = not file.marked + if file.marked then + marked[file.fullName] = file + else + marked[file.fullName] = nil + end + self.grid:draw() + self.statusBar:draw() - elseif event.type == 'delete' then - if self:hasMarked() then - local width = self.statusBar:getColumnWidth('status') - self.statusBar:setColumnWidth('status', UI.term.width) - self.statusBar:setValue('status', 'Delete marked? (y/n)') - self.statusBar:draw() - self.statusBar:sync() - local _, ch = os.pullEvent('char') - if ch == 'y' or ch == 'Y' then - for _,m in pairs(marked) do - pcall(function() - fs.delete(m.fullName) - end) - end - end - marked = { } - self.statusBar:setColumnWidth('status', width) - self.statusBar:setValue('status', '/' .. self.dir.name) - self:updateDirectory(self.dir) + elseif event.type == 'unmark' then + self:unmarkAll() + self.grid:draw() + self:setStatus('Marked files cleared') - self.statusBar:draw() - self.grid:draw() - self:setFocus(self.grid) - end + elseif event.type == 'grid_select' or event.type == 'run' then + if file then + if file.isDir then + self:setDir(file.fullName) + else + local ext = file.name:match('%.(%w+)$') + if ext and config.associations[ext] then + self:run(config.associations[ext], '/' .. file.fullName) + else + self:run(file.name) + end + end + end - elseif event.type == 'copy' or event.type == 'cut' then - if self:hasMarked() then - cutMode = event.type == 'cut' - Util.clear(copied) - Util.merge(copied, marked) - --self:unmarkAll() - self.grid:draw() - self:setStatus('Copied %d file(s)', Util.size(copied)) - end + elseif event.type == 'updir' then + local dir = (self.dir.name:match("(.*/)")) + self:setDir(dir or '/') - elseif event.type == 'copy_path' then - if file then - os.queueEvent('clipboard_copy', file.fullName) - end + elseif event.type == 'delete' then + if self:hasMarked() then + local width = self.statusBar:getColumnWidth('status') + self.statusBar:setColumnWidth('status', UI.term.width) + self.statusBar:setValue('status', 'Delete marked? (y/n)') + self.statusBar:draw() + self.statusBar:sync() + local _, ch = os.pullEvent('char') + if ch == 'y' or ch == 'Y' then + for _,m in pairs(marked) do + pcall(function() + fs.delete(m.fullName) + end) + end + end + marked = { } + self.statusBar:setColumnWidth('status', width) + self.statusBar:setValue('status', '/' .. self.dir.name) + self:updateDirectory(self.dir) - elseif event.type == 'paste' then - for _,m in pairs(copied) do - local s, m = pcall(function() - if cutMode then - fs.move(m.fullName, fs.combine(self.dir.name, m.name)) - else - fs.copy(m.fullName, fs.combine(self.dir.name, m.name)) - end - end) - end - self:updateDirectory(self.dir) - self.grid:draw() - self:setStatus('Pasted ' .. Util.size(copied) .. ' file(s)') + self.statusBar:draw() + self.grid:draw() + self:setFocus(self.grid) + end - else - return UI.Page.eventHandler(self, event) - end - self:setFocus(self.grid) - return true + elseif event.type == 'copy' or event.type == 'cut' then + if self:hasMarked() then + cutMode = event.type == 'cut' + Util.clear(copied) + Util.merge(copied, marked) + --self:unmarkAll() + self.grid:draw() + self:setStatus('Copied %d file(s)', Util.size(copied)) + end + + elseif event.type == 'copy_path' then + if file then + os.queueEvent('clipboard_copy', file.fullName) + end + + elseif event.type == 'paste' then + for _,m in pairs(copied) do + local s, m = pcall(function() + if cutMode then + fs.move(m.fullName, fs.combine(self.dir.name, m.name)) + else + fs.copy(m.fullName, fs.combine(self.dir.name, m.name)) + end + end) + end + self:updateDirectory(self.dir) + self.grid:draw() + self:setStatus('Pasted ' .. Util.size(copied) .. ' file(s)') + + else + return UI.Page.eventHandler(self, event) + end + self:setFocus(self.grid) + return true +end + +--[[ Associations slide out ]] -- +function Browser.associations:show() + self.grid.values = { } + for k, v in pairs(config.associations) do + table.insert(self.grid.values, { + name = k, + value = v, + }) + end + self.grid:update() + UI.SlideOut.show(self) + self:setFocus(self.form[1]) +end + +function Browser.associations:eventHandler(event) + if 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 == 'add_association' then + if self.form:save() then + local entry = Util.find(self.grid.values, 'name', self.form[1].value) or { } + entry.name = self.form[1].value + entry.value = self.form[2].value + table.insert(self.grid.values, entry) + self.form[1]:reset() + self.form[2]:reset() + self.grid:update() + self.grid:draw() + end + + elseif event.type == 'cancel' then + self:hide() + + elseif event.type == 'save' then + config.associations = { } + for _, v in pairs(self.grid.values) do + config.associations[v.name] = v.value + end + Config.update('Files', config) + self:hide() + + else + return UI.SlideOut.eventHandler(self, event) + end + return true end --[[-- Startup logic --]]-- -local args = { ... } +local args = Util.parse(...) Browser:setDir(args[1] or shell.dir()) diff --git a/sys/apps/Help.lua b/sys/apps/Help.lua index c4b0c9f..ffd0486 100644 --- a/sys/apps/Help.lua +++ b/sys/apps/Help.lua @@ -1,10 +1,8 @@ -_G.requireInjector(_ENV) +local UI = require('opus.ui') +local Util = require('opus.util') -local UI = require('ui') -local Util = require('util') - -local colors = _G.colors -local help = _G.help +local colors = _G.colors +local help = _G.help UI:configure('Help', ...) @@ -23,6 +21,7 @@ local page = UI.Page { filter = UI.TextEntry { x = 10, y = 2, ex = -3, limit = 32, + transform = 'lowercase', }, grid = UI.ScrollingGrid { y = 4, @@ -33,7 +32,7 @@ local page = UI.Page { sortColumn = 'name', }, accelerators = { - q = 'quit', + [ 'control-q' ] = 'quit', enter = 'grid_select', }, } @@ -42,21 +41,30 @@ local topicPage = UI.Page { backgroundColor = colors.black, titleBar = UI.TitleBar { title = 'text', - previousPage = true, + event = 'back', }, helpText = UI.TextArea { backgroundColor = colors.black, x = 2, ex = -1, y = 3, ey = -2, }, accelerators = { - q = 'back', + [ 'control-q' ] = 'back', backspace = 'back', }, } +function topicPage:enable(name) + local f = help.lookup(name) + + self.titleBar.title = name + self.helpText:setText(f and Util.readFile(f) or 'No help available for ' .. name) + + return UI.Page.enable(self) +end + function topicPage:eventHandler(event) if event.type == 'back' then - UI:setPreviousPage() + UI:setPage(page) end return UI.Page.eventHandler(self, event) end @@ -68,12 +76,8 @@ function page:eventHandler(event) elseif event.type == 'grid_select' then if self.grid:getSelected() then local name = self.grid:getSelected().name - local f = help.lookup(name) - topicPage.titleBar.title = name - topicPage.helpText:setText(Util.readFile(f)) - - UI:setPage(topicPage) + UI:setPage(topicPage, name) end elseif event.type == 'text_change' then @@ -95,5 +99,6 @@ function page:eventHandler(event) end end -UI:setPage(page) +local args = Util.parse(...) +UI:setPage(args[1] and topicPage or page, args[1]) UI:pullEvents() diff --git a/sys/apps/Installer.lua b/sys/apps/Installer.lua deleted file mode 100644 index da576a5..0000000 --- a/sys/apps/Installer.lua +++ /dev/null @@ -1,457 +0,0 @@ -local colors = _G.colors -local fs = _G.fs -local http = _G.http -local install = _ENV.install -local os = _G.os - -local injector -if not install.testing then - _G.OPUS_BRANCH = 'master-1.8' - local url ='https://raw.githubusercontent.com/kepler155c/opus/master-1.8/sys/apis/injector.lua' - injector = load(http.get(url).readAll(), 'injector.lua', nil, _ENV)() -else - injector = _G.requireInjector -end - -injector(_ENV) - -if not install.testing then - if package then - for _ = 1, 4 do - table.remove(package.loaders, 1) - end - end -end - -local Git = require('git') -local UI = require('ui') -local Util = require('util') - -local currentFile = '' -local currentProgress = 0 -local cancelEvent - -local args = { ... } -local steps = install.steps[args[1] or 'install'] - -if not steps then - error('Invalid install type') -end - -local mode = steps[#steps] - -if UI.term.width < 32 then - cancelEvent = 'quit' -end - -local page = UI.Page { - backgroundColor = colors.cyan, - titleBar = UI.TitleBar { - event = cancelEvent, - }, - wizard = UI.Wizard { - y = 2, ey = -2, - }, - notification = UI.Notification(), - accelerators = { - q = 'quit', - }, -} - -local pages = { - splash = UI.Viewport { }, - review = UI.Viewport { }, - license = UI.Viewport { - backgroundColor = colors.black, - }, - branch = UI.Window { - grid = UI.ScrollingGrid { - ey = -3, - columns = { - { heading = 'Branch', key = 'branch' }, - { heading = 'Description', key = 'description' }, - }, - values = install.branches, - autospace = true, - }, - }, - files = UI.Window { - grid = UI.ScrollingGrid { - ey = -3, - columns = { - { heading = 'Files', key = 'file' }, - }, - sortColumn = 'file', - }, - }, - install = UI.Window { - progressBar = UI.ProgressBar { - y = -1, - }, - }, - uninstall = UI.Window { - progressBar = UI.ProgressBar { - y = -1, - }, - }, -} - -local function getFileList() - if install.gitRepo then - local gitFiles = Git.list(string.format('%s/%s', install.gitRepo, install.gitBranch or 'master')) - install.files = { } - install.diskspace = 0 - for path, entry in pairs(gitFiles) do - install.files[path] = entry.url - install.diskspace = install.diskspace + entry.size - end - end - - if not install.files or Util.empty(install.files) then - error('File list is missing or empty') - end -end - ---[[ Splash ]]-- -function pages.splash:enable() - page.titleBar.title = 'Installer v1.0' - UI.Viewport.enable(self) -end - -function pages.splash:draw() - self:clear() - self:setCursorPos(1, 1) - self:print( - string.format('%s v%s\n', install.title, install.version), nil, colors.yellow) - self:print( - string.format('By: %s\n\n%s\n', install.author, install.description)) - - self.ymax = self.cursorY -end - ---[[ License ]]-- -function pages.license:enable() - page.titleBar.title = 'License Review' - page.wizard.nextButton.text = 'Accept' - UI.Viewport.enable(self) -end - -function pages.license:draw() - self:clear() - self:setCursorPos(1, 1) - self:print( - string.format('Copyright (c) %s %s\n\n', install.copyrightYear, - install.copyrightHolders), - nil, colors.yellow) - self:print(install.license) - - self.ymax = self.cursorY + 1 -end - ---[[ Review ]]-- -function pages.review:enable() - if mode == 'uninstall' then - page.nextButton.text = 'Remove' - page.titleBar.title = 'Remove Installed Files' - else - page.wizard.nextButton.text = 'Begin' - page.titleBar.title = 'Download and Install' - end - UI.Viewport.enable(self) -end - -function pages.review:draw() - self:clear() - self:setCursorPos(1, 1) - - local text = 'Ready to begin installation.\n\nProceeding will download and install the files to the hard drive.' - if mode == 'uninstall' then - text = 'Ready to begin.\n\nProceeding will remove the files previously installed.' - end - self:print(text) - - self.ymax = self.cursorY + 1 -end - ---[[ Files ]]-- -function pages.files:enable() - page.titleBar.title = 'Review Files' - self.grid.values = { } - for k,v in pairs(install.files) do - table.insert(self.grid.values, { file = k, code = v }) - end - self.grid:update() - UI.Window.enable(self) -end - -function pages.files:draw() - self:clear() - - local function formatSize(size) - if size >= 1000000 then - return string.format('%dM', math.floor(size/1000000, 2)) - elseif size >= 1000 then - return string.format('%dK', math.floor(size/1000, 2)) - end - return size - end - - if install.diskspace then - - local bg = self.backgroundColor - - local diskFree = fs.getFreeSpace('/') - if install.diskspace > diskFree then - bg = colors.red - end - - local text = string.format('Space Required: %s, Free: %s', - formatSize(install.diskspace), formatSize(diskFree)) - - if #text > self.width then - text = string.format('Space: %s Free: %s', - formatSize(install.diskspace), formatSize(diskFree)) - end - - self:write(1, self.height, Util.widthify(text, self.width), bg) - end - self.grid:draw() -end - ---[[ -function pages.files:view(url) - local s, m = pcall(function() - page.notification:info('Downloading') - page:sync() - Util.download(url, '/.source') - end) - page.notification:disable() - if s then - shell.run('edit /.source') - fs.delete('/.source') - page:draw() - page.notification:cancel() - else - page.notification:error(m:gsub('.*: (.*)', '%1')) - end -end - -function pages.files:eventHandler(event) - if event.type == 'grid_select' then - self:view(event.selected.code) - return true - end -end ---]] - -local function drawCommon(self) - if currentFile then - self:write(1, 3, 'File:') - self:write(1, 4, Util.widthify(currentFile, self.width)) - else - self:write(1, 3, 'Finished') - end - if self.failed then - self:write(1, 5, Util.widthify(self.failed, self.width), colors.red) - end - self:write(1, self.height - 1, 'Progress') - - self.progressBar.value = currentProgress - self.progressBar:draw() - self:sync() -end - ---[[ Branch ]]-- -function pages.branch:enable() - page.titleBar.title = 'Select Branch' - UI.Window.enable(self) -end - -function pages.branch:eventHandler(event) - -- user is navigating to next view (not previous) - if event.type == 'enable_view' and event.next then - install.gitBranch = self.grid:getSelected().branch - getFileList() - end -end - ---[[ Install ]]-- -function pages.install:enable() - page.wizard.cancelButton:disable() - page.wizard.previousButton:disable() - page.wizard.nextButton:disable() - - page.titleBar.title = 'Installing...' - page.titleBar.event = nil - - UI.Window.enable(self) - - page:draw() - page:sync() - - local i = 0 - local numFiles = Util.size(install.files) - for filename,url in pairs(install.files) do - currentFile = filename - currentProgress = i / numFiles * 100 - self:draw(self) - self:sync() - local s, m = pcall(function() - Util.download(url, fs.combine(install.directory or '', filename)) - end) - if not s then - self.failed = m:gsub('.*: (.*)', '%1') - break - end - i = i + 1 - end - - if not self.failed then - currentProgress = 100 - currentFile = nil - - if install.postInstall then - local s, m = pcall(function() install.postInstall(page, UI) end) - if not s then - self.failed = m:gsub('.*: (.*)', '%1') - end - end - end - - page.wizard.nextButton.text = 'Exit' - page.wizard.nextButton.event = 'quit' - if not self.failed and install.rebootAfter then - page.wizard.nextButton.text = 'Reboot' - page.wizard.nextButton.event = 'reboot' - end - - page.wizard.nextButton:enable() - page:draw() - page:sync() - - if not self.failed and Util.key(args, 'automatic') then - if install.rebootAfter then - os.reboot() - else - UI:exitPullEvents() - end - end -end - -function pages.install:draw() - self:clear() - local text = 'The files are being installed' - if #text > self.width then - text = 'Installing files' - end - self:write(1, 1, text, nil, colors.yellow) - - drawCommon(self) -end - ---[[ Uninstall ]]-- -function pages.uninstall:enable() - page.wizard.cancelButton:disable() - page.wizard.previousButton:disable() - page.wizard.nextButton:disable() - - page.titleBar.title = 'Uninstalling...' - page.titleBar.event = nil - - page:draw() - page:sync() - - UI.Window.enable(self) - - local function pruneDir(dir) - if #dir > 0 then - if fs.exists(dir) then - local files = fs.list(dir) - if #files == 0 then - fs.delete(dir) - pruneDir(fs.getDir(dir)) - end - end - end - end - - local i = 0 - local numFiles = Util.size(install.files) - for k in pairs(install.files) do - currentFile = k - currentProgress = i / numFiles * 100 - self:draw() - self:sync() - fs.delete(k) - pruneDir(fs.getDir(k)) - i = i + 1 - end - - currentProgress = 100 - currentFile = nil - - page.wizard.nextButton.text = 'Exit' - page.wizard.nextButton.event = 'quit' - page.wizard.nextButton:enable() - - page:draw() - page:sync() -end - -function pages.uninstall:draw() - self:clear() - self:write(1, 1, 'Uninstalling files', nil, colors.yellow) - drawCommon(self) -end - -function page:eventHandler(event) - if event.type == 'cancel' then - UI:exitPullEvents() - - elseif event.type == 'reboot' then - os.reboot() - - elseif event.type == 'quit' then - UI:exitPullEvents() - - else - return UI.Page.eventHandler(self, event) - end - return true -end - -function page:enable() - UI.Page.enable(self) - self:setFocus(page.wizard.nextButton) - if UI.term.width < 32 then - page.wizard.cancelButton:disable() - page.wizard.previousButton.x = 2 - end -end - -getFileList() - -local wizardPages = { } -for k,v in ipairs(steps) do - if not pages[v] then - error('Invalid step: ' .. v) - end - wizardPages[k] = pages[v] - wizardPages[k].index = k - wizardPages[k].x = 2 - wizardPages[k].y = 2 - wizardPages[k].ey = -3 - wizardPages[k].ex = -2 -end -page.wizard:add(wizardPages) - -if Util.key(steps, 'install') and install.preInstall then - install.preInstall(page, UI) -end - -UI:setPage(page) -local s, m = pcall(function() UI:pullEvents() end) -if not s then - UI.term:reset() - _G.printError(m) -end diff --git a/sys/apps/Lua.lua b/sys/apps/Lua.lua index f4653a2..45cec04 100644 --- a/sys/apps/Lua.lua +++ b/sys/apps/Lua.lua @@ -1,8 +1,9 @@ +-- Lua may be called from outside of shell - inject a require _G.requireInjector(_ENV) -local History = require('history') -local UI = require('ui') -local Util = require('util') +local History = require('opus.history') +local UI = require('opus.ui') +local Util = require('opus.util') local colors = _G.colors local os = _G.os @@ -19,6 +20,7 @@ _G.requireInjector(sandboxEnv) UI:configure('Lua', ...) local command = '' +local counter = 1 local history = History.load('usr/.lua_history', 25) local page = UI.Page { @@ -32,7 +34,7 @@ local page = UI.Page { prompt = UI.TextEntry { y = 2, shadowText = 'enter command', - limit = 256, + limit = 1024, accelerators = { enter = 'command_enter', up = 'history_back', @@ -41,35 +43,40 @@ local page = UI.Page { [ 'control-space' ] = 'autocomplete', }, }, - grid = UI.ScrollingGrid { - y = 3, ey = -2, - columns = { - { heading = 'Key', key = 'name' }, - { heading = 'Value', key = 'value' }, + tabs = UI.Tabs { + y = 3, + [1] = UI.Tab { + tabTitle = 'Formatted', + grid = UI.ScrollingGrid { + columns = { + { heading = 'Key', key = 'name' }, + { heading = 'Value', key = 'value' }, + }, + sortColumn = 'name', + autospace = true, + }, + }, + [2] = UI.Tab { + tabTitle = 'Output', + output = UI.Embedded { + visible = true, + maxScroll = 1000, + backgroundColor = colors.black, + }, }, - sortColumn = 'name', - autospace = true, - }, - titleBar = UI.TitleBar { - title = 'Output', - y = -1, - event = 'show_output', - closeInd = '^' - }, - output = UI.Embedded { - y = -6, - backgroundColor = colors.gray, }, } +page.grid = page.tabs[1].grid +page.output = page.tabs[2].output + function page:setPrompt(value, focus) self.prompt:setValue(value) - self.prompt.scroll = 0 - self.prompt:setPosition(#value) - self.prompt:updateScroll() if value:sub(-1) == ')' then self.prompt:setPosition(#value - 1) + else + self.prompt:setPosition(#value) end self.prompt:draw() @@ -79,9 +86,8 @@ function page:setPrompt(value, focus) end function page:enable() - self:setFocus(self.prompt) UI.Page.enable(self) - self.output:disable() + self:setFocus(self.prompt) end local function autocomplete(env, oLine, x) @@ -140,23 +146,16 @@ function page:eventHandler(event) self:draw() + elseif event.type == 'tab_select' then + self:setFocus(self.prompt) + elseif event.type == 'show_output' then - self.output:enable() - - self.titleBar.oy = -7 - self.titleBar.event = 'hide_output' - self.titleBar.closeInd = 'v' - self.titleBar:resize() - - self.grid.ey = -8 - self.grid:resize() - - self:draw() + self.tabs:selectTab(self.tabs[2]) elseif event.type == 'autocomplete' then local sz = #self.prompt.value - local pos = self.prompt.pos - self:setPrompt(autocomplete(sandboxEnv, self.prompt.value, self.prompt.pos)) + local pos = self.prompt.entry.pos + self:setPrompt(autocomplete(sandboxEnv, self.prompt.value, self.prompt.entry.pos)) self.prompt:setPosition(pos + #self.prompt.value - sz) self.prompt:updateCursor() @@ -181,8 +180,6 @@ function page:eventHandler(event) local s = tostring(self.prompt.value) if #s > 0 then - history:add(s) - history:back() self:executeStatement(s) else local t = { } @@ -212,10 +209,6 @@ end function page:setResult(result) local t = { } - local oterm = term.redirect(self.output.win) - Util.print(result) - term.redirect(oterm) - local function safeValue(v) if type(v) == 'string' or type(v) == 'number' then return v @@ -300,24 +293,45 @@ end function page:rawExecute(s) local fn, m + local wrapped fn = load('return (' ..s.. ')', 'lua', nil, sandboxEnv) if fn then fn = load('return {' ..s.. '}', 'lua', nil, sandboxEnv) + wrapped = true end + local t = os.clock() if fn then fn, m = pcall(fn) - if #m == 1 then + if #m <= 1 and wrapped then m = m[1] end - return fn, m + else + fn, m = load(s, 'lua', nil, sandboxEnv) + if fn then + t = os.clock() + fn, m = pcall(fn) + end end - fn, m = load(s, 'lua', nil, sandboxEnv) if fn then - fn, m = pcall(fn) + t = os.clock() - t + + local bg, fg = term.getBackgroundColor(), term.getTextColor() + term.setTextColor(colors.cyan) + term.setBackgroundColor(colors.black) + term.write(string.format('out [%.2f]: ', t)) + term.setBackgroundColor(bg) + term.setTextColor(fg) + if m or wrapped then + Util.print(m or 'nil') + else + print() + end + else + _G.printError(m) end return fn, m @@ -326,15 +340,26 @@ end function page:executeStatement(statement) command = statement + history:add(statement) + history:back() + local s, m local oterm = term.redirect(self.output.win) + self.output.win.scrollBottom() + local bg, fg = term.getBackgroundColor(), term.getTextColor() + term.setBackgroundColor(colors.black) + term.setTextColor(colors.green) + term.write(string.format('in [%d]: ', counter)) + term.setBackgroundColor(bg) + term.setTextColor(fg) + print(tostring(statement)) + pcall(function() s, m = self:rawExecute(command) end) - if not s then - _G.printError(m) - end + term.redirect(oterm) + counter = counter + 1 if s and m then self:setResult(m) @@ -351,7 +376,7 @@ function page:executeStatement(statement) end end -local args = { ... } +local args = Util.parse(...) if args[1] then command = 'args[1]' sandboxEnv.args = args diff --git a/sys/apps/Network.lua b/sys/apps/Network.lua index 0fceb8c..6ae002a 100644 --- a/sys/apps/Network.lua +++ b/sys/apps/Network.lua @@ -1,14 +1,11 @@ -_G.requireInjector(_ENV) - -local Config = require('config') -local Event = require('event') -local Socket = require('socket') -local UI = require('ui') -local Util = require('util') +local Config = require('opus.config') +local Event = require('opus.event') +local Socket = require('opus.socket') +local UI = require('opus.ui') +local Util = require('opus.util') local colors = _G.colors local device = _G.device -local multishell = _ENV.multishell local network = _G.network local os = _G.os local shell = _ENV.shell @@ -17,16 +14,17 @@ UI:configure('Network', ...) local gridColumns = { { heading = 'Label', key = 'label' }, - { heading = 'Dist', key = 'distance' }, + { heading = 'Dist', key = 'distance', align = 'right' }, { heading = 'Status', key = 'status' }, } -local trusted = Util.readTable('usr/.known_hosts') local config = Config.load('network', { }) if UI.term.width >= 30 then - table.insert(gridColumns, { heading = 'Fuel', key = 'fuel', width = 5 }) - table.insert(gridColumns, { heading = 'Uptime', key = 'uptime' }) + table.insert(gridColumns, { heading = 'Fuel', key = 'fuel', width = 5, align = 'right' }) +end +if UI.term.width >= 40 then + table.insert(gridColumns, { heading = 'Uptime', key = 'uptime', align = 'right' }) end local page = UI.Page { @@ -35,22 +33,19 @@ local page = UI.Page { { text = 'Connect', dropdown = { { text = 'Telnet t', event = 'telnet' }, { text = 'VNC v', event = 'vnc' }, - UI.MenuBar.spacer, + { spacer = true }, { text = 'Reboot r', event = 'reboot' }, } }, - --{ text = 'Chat', event = 'chat' }, { text = 'Trust', dropdown = { { text = 'Establish', event = 'trust' }, - { text = 'Remove', event = 'untrust' }, } }, - { text = 'Help', event = 'help', noCheck = true }, { - text = '\206', + text = '\187', x = -3, dropdown = { - { text = 'Show all', event = 'show_all', noCheck = true }, - UI.MenuBar.spacer, - { text = 'Show trusted', event = 'show_trusted', noCheck = true }, + { text = 'Port Status', event = 'ports', modem = true }, + { spacer = true }, + { text = 'Help', event = 'help', noCheck = true }, }, }, }, @@ -62,12 +57,53 @@ local page = UI.Page { sortColumn = 'label', autospace = true, }, + ports = UI.SlideOut { + titleBar = UI.TitleBar { + title = 'Ports', + event = 'ports_hide', + }, + grid = UI.ScrollingGrid { + y = 2, + columns = { + { heading = 'Port', key = 'port' }, + { heading = 'State', key = 'state' }, + { heading = 'Connection', key = 'connection' }, + }, + sortColumn = 'port', + autospace = true, + }, + }, + help = UI.SlideOut { + backgroundColor = colors.cyan, + x = 5, ex = -5, height = 8, y = -8, + titleBar = UI.TitleBar { + title = 'Network Help', + event = 'slide_hide', + }, + text = UI.TextArea { + x = 2, y = 2, + backgroundColor = colors.cyan, + value = [[ + +In order to connect to another computer: + +1. The target computer must have a password set (run 'password' from the shell prompt). + +2. From this computer, click trust and enter the password for that computer. + +This only needs to be done once. + ]], + }, + accelerators = { + q = 'slide_hide', + } + }, notification = UI.Notification { }, accelerators = { t = 'telnet', v = 'vnc', r = 'reboot', - q = 'quit', + [ 'control-q' ] = 'quit', c = 'clear', }, } @@ -91,23 +127,71 @@ local function sendCommand(host, command) end end +function page.ports:eventHandler(event) + if event.type == 'grid_select' then + shell.openForegroundTab('Sniff ' .. event.selected.port) + end + return UI.SlideOut.eventHandler(self, event) +end + +function page.ports.grid:update() + local transport = network:getTransport() + + local function findConnection(port) + if transport then + for _,socket in pairs(transport.sockets) do + if socket.sport == port then + return socket + end + end + end + end + + local connections = { } + + pcall(function() -- guard against modem removal + if device.wireless_modem then + for i = 0, 65535 do + if device.wireless_modem.isOpen(i) then + local conn = { + port = i + } + local socket = findConnection(i) + if socket then + conn.state = 'CONNECTED' + local host = socket.dhost + if network[host] then + host = network[host].label + end + conn.connection = host .. ':' .. socket.dport + else + conn.state = 'LISTEN' + end + table.insert(connections, conn) + end + end + end + end) + + self.values = connections + UI.Grid.update(self) +end + function page:eventHandler(event) local t = self.grid:getSelected() if t then if event.type == 'telnet' then - multishell.openTab({ - path = 'sys/apps/telnet.lua', - focused = true, - args = { t.id }, - title = t.label, - }) + shell.openForegroundTab('telnet ' .. t.id) + elseif event.type == 'vnc' then - multishell.openTab({ - path = 'sys/apps/vnc.lua', - focused = true, - args = { t.id }, + shell.openForegroundTab('vnc.lua ' .. t.id) + os.queueEvent('overview_shortcut', { title = t.label, + category = "VNC", + icon = "\010\030 \009\009\031e\\\031 \031e/\031dn\010\030 \009\009 \031e\\/\031 \031bc", + run = "vnc.lua " .. t.id, }) + elseif event.type == 'clear' then Util.clear(network) page.grid:update() @@ -116,18 +200,6 @@ function page:eventHandler(event) elseif event.type == 'trust' then shell.openForegroundTab('trust ' .. t.id) - elseif event.type == 'untrust' then - local trustList = Util.readTable('usr/.known_hosts') or { } - trustList[t.id] = nil - Util.writeTable('usr/.known_hosts', trustList) - - elseif event.type == 'chat' then - multishell.openTab({ - path = 'sys/apps/shell', - args = { 'chat join opusChat-' .. t.id .. ' guest-' .. os.getComputerID() }, - title = 'Chatroom', - focused = true, - }) elseif event.type == 'reboot' then sendCommand(t.id, 'reboot') @@ -135,32 +207,23 @@ function page:eventHandler(event) sendCommand(t.id, 'shutdown') end end + if event.type == 'help' then - UI:setPage(UI.Dialog { - title = 'Network Help', - height = 10, - backgroundColor = colors.white, - text = UI.TextArea { - x = 2, y = 2, - backgroundColor = colors.white, - value = [[ -In order to connect to another computer: + self.help:show() - 1. The target computer must have a password set (run 'password' from the shell prompt). - 2. From this computer, click trust and enter the password for that computer. + elseif event.type == 'ports' then + self.ports.grid:update() + self.ports:show() -This only needs to be done once. - ]], - }, - accelerators = { - q = 'cancel', - } - }) + self.portsHandler = Event.onInterval(3, function() + self.ports.grid:update() + self.ports.grid:draw() + self:sync() + end) - elseif event.type == 'show_all' then - config.showTrusted = false - self.grid:setValues(network) - Config.update('network', config) + elseif event.type == 'ports_hide' then + Event.off(self.portsHandler) + self.ports:hide() elseif event.type == 'show_trusted' then config.showTrusted = true @@ -174,16 +237,15 @@ end function page.menuBar:getActive(menuItem) local t = page.grid:getSelected() - if menuItem.event == 'untrust' then - local trustList = Util.readTable('usr/.known_hosts') or { } - return t and trustList[t.id] + if menuItem.modem then + return not not device.wireless_modem end return menuItem.noCheck or not not t end function page.grid:getRowTextColor(row, selected) if not row.active then - return colors.orange + return colors.lightGray end return UI.Grid.getRowTextColor(self, row, selected) end @@ -193,31 +255,23 @@ function page.grid:getDisplayValues(row) if row.uptime then if row.uptime < 60 then row.uptime = string.format("%ds", math.floor(row.uptime)) + elseif row.uptime < 3600 then + row.uptime = string.format("%sm", math.floor(row.uptime / 60)) else - row.uptime = string.format("%sm", math.floor(row.uptime/6)/10) + row.uptime = string.format("%sh", math.floor(row.uptime / 3600)) end end if row.fuel then - row.fuel = Util.toBytes(row.fuel) + row.fuel = row.fuel > 0 and Util.toBytes(row.fuel) or '' end if row.distance then - row.distance = Util.round(row.distance, 1) + row.distance = Util.toBytes(Util.round(row.distance, 1)) end return row end Event.onInterval(1, function() - local t = { } - if config.showTrusted then - for k,v in pairs(network) do - if trusted[k] then - t[k] = v - end - end - page.grid:setValues(t) - else - page.grid:update() - end + page.grid:update() page.grid:draw() page:sync() end) diff --git a/sys/apps/Overview.lua b/sys/apps/Overview.lua index 14740fc..92c09bf 100644 --- a/sys/apps/Overview.lua +++ b/sys/apps/Overview.lua @@ -1,18 +1,18 @@ -_G.requireInjector(_ENV) - -local class = require('class') -local Config = require('config') -local Event = require('event') -local FileUI = require('ui.fileui') -local NFT = require('nft') -local Packages = require('packages') -local SHA1 = require('sha1') -local Tween = require('ui.tween') -local UI = require('ui') -local Util = require('util') +local Alt = require('opus.alternate') +local class = require('opus.class') +local Config = require('opus.config') +local Event = require('opus.event') +local NFT = require('opus.nft') +local Packages = require('opus.packages') +local SHA = require('opus.crypto.sha2') +local Tween = require('opus.ui.tween') +local UI = require('opus.ui') +local Util = require('opus.util') local colors = _G.colors +local device = _G.device local fs = _G.fs +local os = _G.os local pocket = _G.pocket local shell = _ENV.shell local term = _G.term @@ -23,6 +23,9 @@ if not _ENV.multishell then end local REGISTRY_DIR = 'usr/.registry' +local DEFAULT_ICON = NFT.parse("\0308\0317\153\153\153\153\153\ +\0307\0318\153\153\153\153\153\ +\0308\0317\153\153\153\153\153") UI:configure('Overview', ...) @@ -32,85 +35,21 @@ local config = { } Config.load('Overview', config) -local applications = { } local extSupport = Util.getVersion() >= 1.76 -local function loadApplications() - local requirements = { - turtle = not not turtle, - advancedTurtle = turtle and term.isColor(), - advanced = term.isColor(), - pocket = not not pocket, - advancedPocket = pocket and term.isColor(), - advancedComputer = not turtle and not pocket and term.isColor(), - } - - applications = Util.readTable('sys/etc/app.db') - - for dir in pairs(Packages:installed()) do - local path = fs.combine('packages/' .. dir, 'etc/apps') - if fs.exists(path) then - local dbs = fs.list(path) - for _, db in pairs(dbs) do - local apps = Util.readTable(fs.combine(path, db)) or { } - Util.merge(applications, apps) - end - end - end - - if fs.exists(REGISTRY_DIR) then - local files = fs.list(REGISTRY_DIR) - for _,file in pairs(files) do - local app = Util.readTable(fs.combine(REGISTRY_DIR, file)) - if app and app.key then - app.filename = fs.combine(REGISTRY_DIR, file) - applications[app.key] = app - end - end - end - - Util.each(applications, function(v, k) v.key = k end) - applications = Util.filter(applications, function(a) - if a.disabled then - return false - end - - if a.requires then - return requirements[a.requires] - end - - return true -- Util.startsWith(a.run, 'http') or shell.resolveProgram(a.run) - end) -end - -loadApplications() - -local defaultIcon = NFT.parse("\03180\031711\03180\ -\031800\03171\03180\ -\03171\031800\03171") +local applications = { } +local buttons = { } local sx, sy = term.current().getSize() local maxRecent = math.ceil(sx * sy / 62) -local function elipse(s, len) +local function ellipsis(s, len) if #s > len then s = s:sub(1, len - 2) .. '..' end return s end -local buttons = { } -local categories = { } -for _,f in pairs(applications) do - if not categories[f.category] then - categories[f.category] = true - table.insert(buttons, { text = f.category }) - end -end -table.sort(buttons, function(a, b) return a.text < b.text end) -table.insert(buttons, 1, { text = 'Recent' }) -table.insert(buttons, { text = '+', event = 'new' }) - local function parseIcon(iconText) local icon @@ -136,7 +75,7 @@ function UI.VerticalTabBar:setParent() self.x = 1 self.width = 8 self.height = nil - self.ey = -1 + self.ey = -2 UI.TabBar.setParent(self) for k,c in pairs(self.children) do c.x = 1 @@ -149,20 +88,62 @@ end local cx = 9 local cy = 1 -if sx < 30 then - UI.VerticalTabBar = UI.TabBar - cx = 1 - cy = 2 -end local page = UI.Page { - tabBar = UI.VerticalTabBar { - buttons = buttons, - }, container = UI.Viewport { x = cx, y = cy, }, + tray = UI.Window { + y = -1, width = 8, + backgroundColor = colors.lightGray, + newApp = UI.Button { + text = '+', event = 'new', + }, + --[[ + volume = UI.Button { + x = 3, + text = '\15', event = 'volume', + },]] + }, + editor = UI.SlideOut { + y = -12, height = 12, + backgroundColor = colors.cyan, + titleBar = UI.TitleBar { + title = 'Edit Application', + event = 'slide_hide', + }, + form = UI.Form { + y = 2, ey = -2, + [1] = UI.TextEntry { + formLabel = 'Title', formKey = 'title', limit = 11, help = 'Application title', + required = true, + }, + [2] = UI.TextEntry { + formLabel = 'Run', formKey = 'run', limit = 100, help = 'Full path to application', + required = true, + }, + [3] = UI.TextEntry { + formLabel = 'Category', formKey = 'category', limit = 11, help = 'Category of application', + required = true, + }, + iconFile = UI.TextEntry { + x = 11, ex = -12, y = 7, + limit = 128, help = 'Path to icon file', + shadowText = 'Path to icon file', + }, + loadIcon = UI.Button { + x = 11, y = 9, + text = 'Load', event = 'loadIcon', help = 'Load icon file', + }, + image = UI.NftImage { + backgroundColor = colors.black, + y = 7, x = 2, height = 3, width = 8, + }, + }, + notification = UI.Notification(), + statusBar = UI.StatusBar(), + }, notification = UI.Notification(), accelerators = { r = 'refresh', @@ -175,8 +156,75 @@ local page = UI.Page { }, } -if extSupport then - page.container.backgroundColor = colors.black +local function loadApplications() + local requirements = { + turtle = not not turtle, + advancedTurtle = turtle and term.isColor(), + advanced = term.isColor(), + pocket = not not pocket, + advancedPocket = pocket and term.isColor(), + advancedComputer = not turtle and not pocket and term.isColor(), + neuralInterface = not not device.neuralInterface, + } + + applications = Util.readTable('sys/etc/apps.db') + + for dir in pairs(Packages:installed()) do + local path = fs.combine('packages/' .. dir, 'etc/apps.db') + if fs.exists(path) then + local apps = Util.readTable(path) or { } + Util.merge(applications, apps) + end + end + + if fs.exists(REGISTRY_DIR) then + local files = fs.list(REGISTRY_DIR) + for _,file in pairs(files) do + local app = Util.readTable(fs.combine(REGISTRY_DIR, file)) + if app and app.key then + app.filename = fs.combine(REGISTRY_DIR, file) + applications[app.key] = app + end + end + end + + Util.each(applications, function(v, k) v.key = k end) + applications = Util.filter(applications, function(a) + if a.disabled then + return false + end + + if a.requires then + return requirements[a.requires] + end + + return true -- Util.startsWith(a.run, 'http') or shell.resolveProgram(a.run) + end) + + local categories = { } + buttons = { } + for _,f in pairs(applications) do + if not categories[f.category] then + categories[f.category] = true + table.insert(buttons, { + text = f.category, + selected = config.currentCategory == f.category + }) + end + end + table.sort(buttons, function(a, b) return a.text < b.text end) + table.insert(buttons, 1, { text = 'Recent' }) + + Util.removeByValue(page.children, page.tabBar) + + page:add { + tabBar = UI.VerticalTabBar { + buttons = buttons, + }, + } + + --page.tabBar:selectTab(config.currentCategory or 'Apps') + page.container:setCategory(config.currentCategory or 'Apps') end UI.Icon = class(UI.Window) @@ -188,6 +236,7 @@ UI.Icon.defaults = { function UI.Icon:eventHandler(event) if event.type == 'mouse_click' then self:setFocus(self.button) + --self:emit({ type = self.button.event, button = self.button }) return true elseif event.type == 'mouse_doubleclick' then self:emit({ type = self.button.event, button = self.button }) @@ -201,7 +250,7 @@ end function page.container:setCategory(categoryName, animate) -- reset the viewport window self.children = { } - self.offy = 0 + self:reset() local function filter(it, f) local ot = { } @@ -242,10 +291,10 @@ function page.container:setCategory(categoryName, animate) icon = parseIcon(program.icon) end if not icon then - icon = defaultIcon + icon = DEFAULT_ICON end - local title = elipse(program.title, 8) + local title = ellipsis(program.title, 8) local width = math.max(icon.width + 2, #title + 2) table.insert(self.children, UI.Icon({ @@ -283,10 +332,10 @@ function page.container:setCategory(categoryName, animate) for k,child in ipairs(self.children) do if r == 1 then child.x = math.random(1, self.width) - child.y = math.random(1, self.height) + child.y = math.random(1, self.height - 3) elseif r == 2 then child.x = self.width - child.y = self.height + child.y = self.height - 3 elseif r == 3 then child.x = math.floor(self.width / 2) child.y = math.floor(self.height / 2) @@ -298,7 +347,7 @@ function page.container:setCategory(categoryName, animate) child.y = row if k == #self.children then child.x = self.width - child.y = self.height + child.y = self.height - 3 end end child.tween = Tween.new(6, child, { x = col, y = row }, 'linear') @@ -318,10 +367,10 @@ function page.container:setCategory(categoryName, animate) end self:initChildren() - if animate then -- need to fix transitions under layers - local function transition(args) + if animate then + local function transition() local i = 1 - return function(device) + return function() self:clear() for _,child in pairs(self.children) do child.tween:update(1) @@ -329,7 +378,6 @@ function page.container:setCategory(categoryName, animate) child.y = math.floor(child.y) child:draw() end - args.canvas:blit(device, args, args) i = i + 1 return i < 7 end @@ -373,13 +421,13 @@ function page:eventHandler(event) shell.switchTab(shell.openTab(event.button.app.run)) elseif event.type == 'shell' then - shell.switchTab(shell.openTab('sys/apps/shell')) + shell.switchTab(shell.openTab(Alt.get('shell'))) elseif event.type == 'lua' then - shell.switchTab(shell.openTab('sys/apps/Lua.lua')) + shell.switchTab(shell.openTab(Alt.get('lua'))) elseif event.type == 'files' then - shell.switchTab(shell.openTab('sys/apps/Files.lua')) + shell.switchTab(shell.openTab(Alt.get('files'))) elseif event.type == 'focus_change' then if event.focused.parent.UIElement == 'Icon' then @@ -395,9 +443,13 @@ function page:eventHandler(event) elseif event.type == 'delete' then local focused = page:getFocused() if focused.app then - focused.app.disabled = true - local filename = focused.app.filename or fs.combine(REGISTRY_DIR, focused.app.key) - Util.writeTable(filename, focused.app) + if focused.app.filename then + fs.delete(focused.app.filename) + else + focused.app.disabled = true + local filename = focused.app.filename or fs.combine(REGISTRY_DIR, focused.app.key) + Util.writeTable(filename, focused.app) + end loadApplications() page:refresh() page:draw() @@ -409,12 +461,12 @@ function page:eventHandler(event) if config.currentCategory ~= 'Recent' then category = config.currentCategory or 'Apps' end - UI:setPage('editor', { category = category }) + self.editor:show({ category = category }) elseif event.type == 'edit' then local focused = page:getFocused() if focused.app then - UI:setPage('editor', focused.app) + self.editor:show(focused.app) end else @@ -423,40 +475,7 @@ function page:eventHandler(event) return true end -local formWidth = math.max(UI.term.width - 8, 26) - -local editor = UI.Dialog { - height = 11, - width = formWidth, - title = 'Edit Application', - form = UI.Form { - y = 2, - height = 9, - title = UI.TextEntry { - formLabel = 'Title', formKey = 'title', limit = 11, help = 'Application title', - required = true, - }, - run = UI.TextEntry { - formLabel = 'Run', formKey = 'run', limit = 100, help = 'Full path to application', - required = true, - }, - category = UI.TextEntry { - formLabel = 'Category', formKey = 'category', limit = 11, help = 'Category of application', - required = true, - }, - loadIcon = UI.Button { - x = 11, y = 6, - text = 'Icon', event = 'loadIcon', help = 'Select icon' - }, - image = UI.NftImage { - y = 6, x = 2, height = 3, width = 8, - }, - }, - statusBar = UI.StatusBar(), - iconFile = '', -} - -function editor:enable(app) +function page.editor:show(app) if app then self.form:setValues(app) @@ -469,96 +488,101 @@ function editor:enable(app) end self.form.image:setImage(icon) end - UI.Dialog.enable(self) + UI.SlideOut.show(self) self:focusFirst() end -function editor.form.image:draw() +function page.editor.form.image:draw() self:clear() UI.NftImage.draw(self) end -function editor:updateApplications(app) +function page.editor:updateApplications(app) if not app.key then - app.key = SHA1.sha1(app.title) + app.key = SHA.compute(app.title) end local filename = app.filename or fs.combine(REGISTRY_DIR, app.key) Util.writeTable(filename, app) loadApplications() end -function editor:eventHandler(event) +function page.editor:eventHandler(event) if event.type == 'form_cancel' or event.type == 'cancel' then - UI:setPreviousPage() + self:hide() elseif event.type == 'focus_change' then self.statusBar:setStatus(event.focused.help or '') self.statusBar:draw() elseif event.type == 'loadIcon' then - local fileui = FileUI({ - x = self.x, - y = self.y, - z = 2, - width = self.width, - height = self.height, - }) - UI:setPage(fileui, fs.getDir(self.iconFile), function(fileName) - if fileName then - self.iconFile = fileName - local s, m = pcall(function() - local iconLines = Util.readFile(fileName) - if not iconLines then - error('Must be an NFT image - 3 rows, 8 cols max') - end - local icon, m = parseIcon(iconLines) - if not icon then - error(m) - end - if extSupport then - self.form.values.iconExt = iconLines - else - self.form.values.icon = iconLines - end - self.form.image:setImage(icon) - self.form.image:draw() - end) - if not s and m then - local msg = m:gsub('.*: (.*)', '%1') - page.notification:error(msg) - end + local s, m = pcall(function() + local iconLines = Util.readFile(self.form.iconFile.value) + if not iconLines then + error('Must be an NFT image - 3 rows, 8 cols max') end + local icon, m = parseIcon(iconLines) + if not icon then + error(m) + end + if extSupport then + self.form.values.iconExt = iconLines + else + self.form.values.icon = iconLines + end + self.form.image:setImage(icon) + self.form.image:draw() end) + if not s and m then + local msg = m:gsub('.*: (.*)', '%1') + self.notification:error(msg) + end elseif event.type == 'form_invalid' then - page.notification:error(event.message) + self.notification:error(event.message) elseif event.type == 'form_complete' then local values = self.form.values - UI:setPreviousPage() + self:hide() self:updateApplications(values) - page:refresh() - page:draw() + --page:refresh() + --page:draw() + config.currentCategory = values.category + Config.update('Overview', config) + os.queueEvent('overview_refresh') else - return UI.Dialog.eventHandler(self, event) + return UI.SlideOut.eventHandler(self, event) end return true end UI:setPages({ - editor = editor, main = page, }) -Event.on('os_register_app', function() +local function reload() loadApplications() page:refresh() page:draw() page:sync() +end + +Event.on('overview_shortcut', function(_, app) + if not app.key then + app.key = SHA.compute(app.title) + end + local filename = app.filename or fs.combine(REGISTRY_DIR, app.key) + if not fs.exists(filename) then + Util.writeTable(filename, app) + reload() + end end) -page.tabBar:selectTab(config.currentCategory or 'Apps') -page.container:setCategory(config.currentCategory or 'Apps') +Event.on('overview_refresh', function() + reload() +end) + +loadApplications() + UI:setPage(page) UI:pullEvents() diff --git a/sys/apps/PackageManager.lua b/sys/apps/PackageManager.lua index fd05cf0..3a3dd8b 100644 --- a/sys/apps/PackageManager.lua +++ b/sys/apps/PackageManager.lua @@ -1,18 +1,16 @@ -_G.requireInjector(_ENV) - -local Ansi = require('ansi') -local Packages = require('packages') -local UI = require('ui') +local Ansi = require('opus.ansi') +local Packages = require('opus.packages') +local UI = require('opus.ui') +local Util = require('opus.util') local colors = _G.colors -local shell = _ENV.shell local term = _G.term UI:configure('PackageManager', ...) local page = UI.Page { grid = UI.ScrollingGrid { - y = 2, ey = 7, x = 2, ex = -6, + x = 2, ex = 14, y = 2, ey = -5, values = { }, columns = { { heading = 'Package', key = 'name' }, @@ -22,42 +20,79 @@ local page = UI.Page { help = 'Select a package', }, add = UI.Button { - x = -4, y = 4, - text = '+', + x = 2, y = -3, + text = 'Install', event = 'action', help = 'Install or update', }, remove = UI.Button { - x = -4, y = 6, - text = '-', + x = 12, y = -3, + text = 'Remove ', event = 'action', operation = 'uninstall', operationText = 'Remove', help = 'Remove', }, - description = UI.TextArea { - x = 2, y = 9, ey = -2, - --backgroundColor = colors.white, + updateall = UI.Button { + ex = -2, y = -3, width = 12, + text = 'Update All', + event = 'updateall', + help = 'Update all installed packages', + }, + description = UI.TextArea { + x = 16, y = 3, ey = -5, + marginRight = 0, marginLeft = 0, }, - statusBar = UI.StatusBar { }, action = UI.SlideOut { backgroundColor = colors.cyan, titleBar = UI.TitleBar { event = 'hide-action', }, button = UI.Button { - ex = -4, y = 4, width = 7, - text = 'Begin', event = 'begin', + x = -10, y = 3, + text = ' Begin ', event = 'begin', }, output = UI.Embedded { - y = 6, ey = -2, x = 2, ex = -2, - }, - statusBar = UI.StatusBar { - backgroundColor = colors.cyan, + y = 5, ey = -2, x = 2, ex = -2, + visible = true, }, }, + statusBar = UI.StatusBar { }, + accelerators = { + [ 'control-q' ] = 'quit', + }, } +function page:loadPackages() + self.grid.values = { } + self.statusBar:setStatus('Downloading...') + self:sync() + + for k in pairs(Packages:list()) do + local manifest = Packages:getManifest(k) + if not manifest then + manifest = { + invalid = true, + description = 'Unable to download manifest', + title = '', + } + end + table.insert(self.grid.values, { + installed = not not Packages:isInstalled(k), + name = k, + manifest = manifest, + }) + end + self.grid:update() + self.grid:setIndex(1) + self.grid:emit({ + type = 'grid_focus_row', + selected = self.grid:getSelected(), + element = self.grid, + }) + self.statusBar:setStatus('Updated packages') +end + function page.grid:getRowTextColor(row, selected) if row.installed then return colors.yellow @@ -66,25 +101,26 @@ function page.grid:getRowTextColor(row, selected) end function page.action:show() + self.output.win:clear() UI.SlideOut.show(self) - self.output:draw() - self.output.win.redraw() + --self.output:draw() + --self.output.win.redraw() end function page:run(operation, name) local oterm = term.redirect(self.action.output.win) self.action.output:clear() local cmd = string.format('package %s %s', operation, name) - --for _ = 1, 3 do - -- print(cmd .. '\n') - -- os.sleep(1) - --end term.setCursorPos(1, 1) term.clear() term.setTextColor(colors.yellow) print(cmd .. '\n') term.setTextColor(colors.white) - shell.run(cmd) + local s, m = Util.run(_ENV, '/sys/apps/package.lua', operation, name) + + if not s and m then + _G.printError(m) + end term.redirect(oterm) self.action.output:draw() end @@ -92,6 +128,10 @@ end function page:updateSelection(selected) self.add.operation = selected.installed and 'update' or 'install' self.add.operationText = selected.installed and 'Update' or 'Install' + self.add.text = selected.installed and 'Update' or 'Install' + self.remove.inactive = not selected.installed + self.add:draw() + self.remove:draw() end function page:eventHandler(event) @@ -107,13 +147,20 @@ function page:eventHandler(event) self.description:draw() self:updateSelection(event.selected) + elseif event.type == 'updateall' then + self.operation = 'updateall' + self.action.button.text = ' Begin ' + self.action.button.event = 'begin' + self.action.titleBar.title = 'Update All' + self.action:show() + elseif event.type == 'action' then local selected = self.grid:getSelected() if selected then self.operation = event.button.operation self.action.button.text = event.button.operationText self.action.titleBar.title = selected.manifest.title - self.action.button.text = 'Begin' + self.action.button.text = ' Begin ' self.action.button.event = 'begin' self.action:show() end @@ -122,12 +169,17 @@ function page:eventHandler(event) self.action:hide() elseif event.type == 'begin' then - local selected = self.grid:getSelected() - self:run(self.operation, selected.name) - selected.installed = Packages:isInstalled(selected.name) + if self.operation == 'updateall' then + self:run(self.operation, '') + else + local selected = self.grid:getSelected() + self:run(self.operation, selected.name) + selected.installed = Packages:isInstalled(selected.name) - self:updateSelection(selected) - self.action.button.text = 'Done' + self:updateSelection(selected) + end + + self.action.button.text = ' Done ' self.action.button.event = 'hide-action' self.action.button:draw() @@ -137,22 +189,11 @@ function page:eventHandler(event) UI.Page.eventHandler(self, event) end -for k in pairs(Packages:list()) do - local manifest = Packages:getManifest(k) - if not manifest then - manifest = { - invalid = true, - description = 'Unable to download manifest', - title = '', - } - end - table.insert(page.grid.values, { - installed = not not Packages:isInstalled(k), - name = k, - manifest = manifest, - }) -end -page.grid:update() - UI:setPage(page) +page.statusBar:setStatus('Downloading...') +page:sync() +Packages:downloadList() +page:loadPackages() +page:sync() + UI:pullEvents() diff --git a/sys/apps/ShellLauncher.lua b/sys/apps/ShellLauncher.lua new file mode 100644 index 0000000..f2eebb7 --- /dev/null +++ b/sys/apps/ShellLauncher.lua @@ -0,0 +1,30 @@ +local Alt = require('opus.alternate') + +local kernel = _G.kernel +local os = _G.os +local shell = _ENV.shell + +local launcherTab = kernel.getCurrent() +launcherTab.noFocus = true + +kernel.hook('kernel_focus', function(_, eventData) + local focusTab = eventData and eventData[1] + if focusTab == launcherTab.uid then + local previousTab = eventData[2] + local nextTab = launcherTab + if not previousTab then + for _, v in pairs(kernel.routines) do + if not v.hidden and v.uid > nextTab.uid then + nextTab = v + end + end + end + if nextTab == launcherTab then + shell.switchTab(shell.openTab(Alt.get('shell'))) + else + shell.switchTab(nextTab.uid) + end + end +end) + +os.pullEventRaw('kernel_halt') \ No newline at end of file diff --git a/sys/apps/Sniff.lua b/sys/apps/Sniff.lua new file mode 100644 index 0000000..2bda4f0 --- /dev/null +++ b/sys/apps/Sniff.lua @@ -0,0 +1,387 @@ +local UI = require('opus.ui') +local Event = require('opus.event') +local Util = require('opus.util') + +local colors = _G.colors +local device = _G.device +local textutils = _G.textutils +local peripheral = _G.peripheral +local multishell = _ENV.multishell + +local gridColumns = {} +table.insert(gridColumns, { heading = '#', key = 'id', width = 5, align = 'right' }) +table.insert(gridColumns, { heading = 'Port', key = 'portid', width = 5, align = 'right' }) +table.insert(gridColumns, { heading = 'Reply', key = 'replyid', width = 5, align = 'right' }) +if UI.defaultDevice.width > 50 then + table.insert(gridColumns, { heading = 'Dist', key = 'distance', width = 6, align = 'right' }) +end +table.insert(gridColumns, { heading = 'Msg', key = 'packetStr' }) + +local page = UI.Page { + paused = false, + index = 1, + notification = UI.Notification { }, + accelerators = { ['control-q'] = 'quit' }, + + menuBar = UI.MenuBar { + buttons = { + { text = 'Pause', event = 'pause_click', name = 'pauseButton' }, + { text = 'Clear', event = 'clear_click' }, + { text = 'Config', event = 'config_click' }, + }, + }, + + packetGrid = UI.ScrollingGrid { + y = 2, + maxPacket = 300, + inverseSort = true, + sortColumn = 'id', + columns = gridColumns, + accelerators = { ['space'] = 'pause_click' }, + }, + + configSlide = UI.SlideOut { + y = -11, + titleBar = UI.TitleBar { title = 'Sniffer Config', event = 'config_close' }, + accelerators = { ['backspace'] = 'config_close' }, + configTabs = UI.Tabs { + y = 2, + filterTab = UI.Tab { + tabTitle = 'Filter', + filterGridText = UI.Text { + x = 2, y = 2, + value = 'ID filter', + }, + filterGrid = UI.ScrollingGrid { + x = 2, y = 3, + width = 10, height = 4, + disableHeader = true, + columns = { + { key = 'id', width = 5 }, + }, + }, + filterEntry = UI.TextEntry { + x = 2, y = 8, + width = 7, + shadowText = 'ID', + limit = 5, + accelerators = { enter = 'filter_add' }, + }, + filterAdd = UI.Button { + x = 10, y = 8, + text = '+', + event = 'filter_add', + }, + filterAllCheck = UI.Checkbox { + x = 14, y = 8, + value = false, + }, + filterAddText = UI.Text { + x = 18, y = 8, + value = "Use ID filter", + }, + rangeText = UI.Text { + x = 15, y = 2, + value = "Distance filter", + }, + rangeEntry = UI.TextEntry { + x = 15, y = 3, + width = 10, + limit = 8, + shadowText = 'Range', + transform = 'number', + }, + }, + modemTab = UI.Tab { + tabTitle = 'Modem', + channelGrid = UI.ScrollingGrid { + x = 2, y = 2, + width = 12, height = 5, + autospace = true, + columns = {{ heading = 'Open Ports', key = 'port' }}, + }, + modemGrid = UI.ScrollingGrid { + x = 15, y = 2, + ex = -2, height = 5, + autospace = true, + columns = { + { heading = 'Side', key = 'side' }, + { heading = 'Type', key = 'type' }, + }, + }, + channelEntry = UI.TextEntry { + x = 2, y = 8, + width = 7, + shadowText = 'ID', + limit = 5, + accelerators = { enter = 'channel_add' }, + }, + channelAdd = UI.Button { + x = 10, y = 8, + text = '+', + event = 'channel_add', + }, + }, + }, + }, + + packetSlide = UI.SlideOut { + titleBar = UI.TitleBar { + title = 'Packet Information', + event = 'packet_close', + }, + backgroundColor = colors.cyan, + accelerators = { + ['backspace'] = 'packet_close', + ['left'] = 'prev_packet', + ['right'] = 'next_packet', + }, + packetMeta = UI.Grid { + x = 2, y = 2, + ex = 23, height = 4, + inactive = true, + columns = { + { key = 'text' }, + { key = 'value', align = 'right', textColor = colors.yellow }, + }, + values = { + port = { text = 'Port' }, + reply = { text = 'Reply' }, + dist = { text = 'Distance' }, + } + }, + packetButton = UI.Button { + x = 25, y = 5, + text = 'Open in Lua', + event = 'packet_lua', + }, + packetData = UI.TextArea { + y = 7, ey = -1, + backgroundColor = colors.black, + }, + }, +} + +local filterConfig = page.configSlide.configTabs.filterTab +local modemConfig = page.configSlide.configTabs.modemTab + +function filterConfig:eventHandler(event) + if event.type == 'filter_add' then + local id = tonumber(self.filterEntry.value) + if id then self.filterGrid.values[id] = { id = id } + self.filterGrid:update() + self.filterEntry:reset() + self:draw() + end + + elseif event.type == 'grid_select' then + self.filterGrid.values[event.selected.id] = nil + self.filterGrid:update() + self.filterGrid:draw() + + else return UI.Tab.eventHandler(self, event) + end + return true +end + +function modemConfig:loadChannel() + for chan = 0, 65535 do + self.currentModem.openChannels[chan] = self.currentModem.device.isOpen(chan) and { port = chan } or nil + end + self.channelGrid:setValues(self.currentModem.openChannels) + self.currentModem.loaded = true +end + +function modemConfig:enable() + if not self.currentModem.loaded then + self:loadChannel() + end + + UI.Tab.enable(self) +end + +function modemConfig:eventHandler(event) + if event.type == 'channel_add' then + local id = tonumber(modemConfig.channelEntry.value) + if id then + self.currentModem.openChannels[id] = { port = id } + self.currentModem.device.open(id) + self.channelGrid:setValues(self.currentModem.openChannels) + self.channelGrid:update() + self.channelEntry:reset() + self:draw() + end + + elseif event.type == 'grid_select' then + if event.element == self.channelGrid then + self.currentModem.openChannels[event.selected.port] = nil + self.currentModem.device.close(event.selected.port) + self.channelGrid:setValues(self.currentModem.openChannels) + page.configSlide.configTabs.modemTab.channelGrid:update() + page.configSlide.configTabs.modemTab.channelGrid:draw() + + elseif event.element == self.modemGrid then + self.currentModem = event.selected + page.notification:info("Loading channel list") + page:sync() + modemConfig:loadChannel() + page.notification:success("Now using modem on " .. self.currentModem.side) + self.channelGrid:draw() + end + + else return UI.Tab.eventHandler(self, event) + end + return true +end + +function page.packetSlide:setPacket(packet) + self.currentPacket = packet + local p, res = pcall(textutils.serialize, page.packetSlide.currentPacket.message) + self.packetData.textColor = p and colors.white or colors.red + self.packetData:setText(res) + self.packetMeta.values.port.value = page.packetSlide.currentPacket.portid + self.packetMeta.values.reply.value = page.packetSlide.currentPacket.replyid + self.packetMeta.values.dist.value = Util.round(page.packetSlide.currentPacket.distance, 2) +end + +function page.packetSlide:show(packet) + self:setPacket(packet) + + UI.SlideOut.show(self) +end + +function page.packetSlide:eventHandler(event) + if event.type == 'packet_close' then + self:hide() + page:setFocus(page.packetGrid) + + elseif event.type == 'packet_lua' then + multishell.openTab({ path = 'sys/apps/Lua.lua', args = { self.currentPacket.message }, focused = true }) + + elseif event.type == 'prev_packet' then + local c = self.currentPacket + local n = page.packetGrid.values[c.id - 1] + if n then + self:setPacket(n) + self:draw() + end + + elseif event.type == 'next_packet' then + local c = self.currentPacket + local n = page.packetGrid.values[c.id + 1] + if n then + self:setPacket(n) + self:draw() + end + + else return UI.SlideOut.eventHandler(self, event) + end + return true +end + +function page.packetGrid:getDisplayValues(row) + local row = Util.shallowCopy(row) + row.distance = Util.toBytes(Util.round(row.distance), 2) + return row +end + +function page.packetGrid:addPacket(packet) + if not page.paused and (packet.distance <= (filterConfig.rangeEntry.value or math.huge)) and (not filterConfig.filterAllCheck.value or filterConfig.filterGrid.values[packet.portid]) then + page.index = page.index + 1 + local _, res = pcall(textutils.serialize, packet.message) + packet.packetStr = res:gsub("\n%s*", "") + table.insert(self.values, packet) + end + if #self.values > self.maxPacket then + local t = { } + for i = 10, #self.values do + t[i - 9] = self.values[i] + end + self:setValues(t) + end + + self:update() + self:draw() + page:sync() +end + +function page:enable() + modemConfig.modems = {} + peripheral.find('modem', function(side, dev) + modemConfig.modems[side] = { + type = dev.isWireless() and 'Wireless' or 'Wired', + side = side, + openChannels = { }, + device = dev, + loaded = false + } + end) + modemConfig.currentModem = device.wireless_modem and + modemConfig.modems[device.wireless_modem.side] or + device.wired_modem and + modemConfig.modems[device.wired_modem.side] or + nil + + modemConfig.modemGrid.values = modemConfig.modems + modemConfig.modemGrid:update() + modemConfig.modemGrid:setSelected(modemConfig.currentModem) + + UI.Page.enable(self) +end + + +function page:eventHandler(event) + if event.type == 'pause_click' then + self.paused = not self.paused + self.menuBar.pauseButton.text = self.paused and 'Resume' or 'Pause' + self.notification:success(self.paused and 'Paused' or 'Resumed', 2) + self.menuBar:draw() + + elseif event.type == 'clear_click' then + self.packetGrid:setValues({ }) + self.notification:success('Cleared', 2) + self.packetGrid:draw() + + elseif event.type == 'config_click' then + self.configSlide:show() + self:setFocus(filterConfig.filterEntry) + + elseif event.type == 'config_close' then + self.configSlide:hide() + self:setFocus(self.packetGrid) + + elseif event.type == 'grid_select' then + self.packetSlide:show(event.selected) + + elseif event.type == 'quit' then + Event.exitPullEvents() + + else return UI.Page.eventHandler(self, event) + end + return true +end + +Event.on('modem_message', function(_, side, chan, reply, msg, dist) + if modemConfig.currentModem.side == side then + page.packetGrid:addPacket({ + id = page.index, + portid = chan, + replyid = reply, + message = msg, + distance = dist or -1, + }) + end +end) + +local args = Util.parse(...) +if args[1] then + local id = tonumber(args[1]) + if id then + filterConfig.filterGrid.values[id] = { id = id } + filterConfig.filterAllCheck:setValue(true) + filterConfig.filterGrid:update() + end +end + +UI:setPage(page) +UI:pullEvents() diff --git a/sys/apps/System.lua b/sys/apps/System.lua index ce2a2b1..b18200d 100644 --- a/sys/apps/System.lua +++ b/sys/apps/System.lua @@ -1,316 +1,41 @@ -_G.requireInjector(_ENV) +local UI = require('opus.ui') +local Util = require('opus.util') -local Config = require('config') -local Security = require('security') -local SHA1 = require('sha1') -local UI = require('ui') -local Util = require('util') - -local fs = _G.fs -local os = _G.os -local settings = _G.settings -local shell = _ENV.shell -local turtle = _G.turtle +local fs = _G.fs +local shell = _ENV.shell UI:configure('System', ...) -local env = { - path = shell.path(), - aliases = shell.aliases(), - lua_path = _ENV.LUA_PATH, -} -Config.load('shell', env) - local systemPage = UI.Page { tabs = UI.Tabs { - pathTab = UI.Window { - tabTitle = 'Path', - entry = UI.TextEntry { - x = 2, y = 2, ex = -2, - limit = 256, - value = shell.path(), - shadowText = 'enter system path', - accelerators = { - enter = 'update_path', - }, - }, - grid = UI.Grid { - y = 4, - disableHeader = true, - columns = { { key = 'value' } }, - autospace = true, - }, - }, - - aliasTab = UI.Window { - tabTitle = 'Alias', - alias = UI.TextEntry { - x = 2, y = 2, ex = -2, - limit = 32, - shadowText = 'Alias', - }, - path = UI.TextEntry { - y = 3, x = 2, ex = -2, - limit = 256, - shadowText = 'Program path', - accelerators = { - enter = 'new_alias', - }, - }, - grid = UI.Grid { - y = 5, - sortColumn = 'alias', - columns = { - { heading = 'Alias', key = 'alias' }, - { heading = 'Program', key = 'path' }, - }, - accelerators = { - delete = 'delete_alias', - }, - }, - }, - - passwordTab = UI.Window { - tabTitle = 'Password', - oldPass = UI.TextEntry { - x = 2, y = 2, ex = -2, - limit = 32, - mask = true, - shadowText = 'old password', - inactive = not Security.getPassword(), - }, - newPass = UI.TextEntry { - y = 3, x = 2, ex = -2, - limit = 32, - mask = true, - shadowText = 'new password', - accelerators = { - enter = 'new_password', - }, - }, - button = UI.Button { - x = 2, y = 5, - text = 'Update', - event = 'update_password', - }, - info = UI.TextArea { - x = 2, ex = -2, - y = 7, - inactive = true, - value = 'Add a password to enable other computers to connect to this one.', - } - }, - - infoTab = UI.Window { - tabTitle = 'Info', - labelText = UI.Text { - x = 3, y = 2, - value = 'Label' - }, - label = UI.TextEntry { - x = 9, y = 2, ex = -4, - limit = 32, - value = os.getComputerLabel(), - accelerators = { - enter = 'update_label', - }, - }, + settings = UI.Tab { + tabTitle = 'Category', grid = UI.ScrollingGrid { - y = 3, - values = { - { name = '', value = '' }, - { name = 'CC version', value = Util.getVersion() }, - { name = 'Lua version', value = _VERSION }, - { name = 'MC version', value = Util.getMinecraftVersion() }, - { name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) }, - { name = 'Computer ID', value = tostring(os.getComputerID()) }, - { name = 'Day', value = tostring(os.day()) }, - }, - inactive = true, + y = 2, columns = { - { key = 'name', width = 12 }, - { key = 'value' }, + { heading = 'Name', key = 'name' }, + { heading = 'Description', key = 'description' }, }, + sortColumn = 'name', + autospace = true, }, }, }, notification = UI.Notification(), accelerators = { - q = 'quit', + [ 'control-q' ] = 'quit', }, } -if turtle then - pcall(function() - local Home = require('turtle.home') --- TODO: dont rely on turtle.home - local values = { } - Config.load('gps', values.home and { values.home } or { }) - - systemPage.tabs:add({ - gpsTab = UI.Window { - tabTitle = 'GPS', - labelText = UI.Text { - x = 3, y = 2, - value = 'On restart, return to this location' - }, - grid = UI.Grid { - x = 3, ex = -3, y = 4, - height = 2, - values = values, - inactive = true, - columns = { - { heading = 'x', key = 'x' }, - { heading = 'y', key = 'y' }, - { heading = 'z', key = 'z' }, - }, - }, - button1 = UI.Button { - x = 3, y = 7, - text = 'Set home', - event = 'gps_set', - }, - button2 = UI.Button { - ex = -3, y = 7, width = 7, - text = 'Clear', - event = 'gps_clear', - }, - }, - }) - function systemPage.tabs.gpsTab:eventHandler(event) - if event.type == 'gps_set' then - systemPage.notification:info('Determining location', 10) - systemPage:sync() - if Home.set() then - Config.load('gps', values) - self.grid:setValues(values.home and { values.home } or { }) - self.grid:draw() - systemPage.notification:success('Location set') - else - systemPage.notification:error('Unable to determine location') - end - return true - elseif event.type == 'gps_clear' then - fs.delete('usr/config/gps') - self.grid:setValues({ }) - self.grid:draw() - return true - end +function systemPage.tabs.settings:eventHandler(event) + if event.type == 'grid_select' then + local tab = event.selected.tab + if not systemPage.tabs[tab.tabTitle] then + systemPage.tabs:add({ [ tab.tabTitle ] = tab }) + tab:disable() end - end) -end - -if settings then - local values = { } - for _,v in pairs(settings.getNames()) do - local value = settings.get(v) - if not value then - value = false - end - table.insert(values, { - name = v, - value = value, - }) - end - - systemPage.tabs:add({ - settingsTab = UI.Window { - tabTitle = 'Settings', - grid = UI.Grid { - y = 1, - values = values, - autospace = true, - sortColumn = 'name', - columns = { - { heading = 'Setting', key = 'name' }, - { heading = 'Value', key = 'value' }, - }, - }, - } - }) - function systemPage.tabs.settingsTab:eventHandler(event) - if event.type == 'grid_select' then - if not event.selected.value or type(event.selected.value) == 'boolean' then - event.selected.value = not event.selected.value - end - settings.set(event.selected.name, event.selected.value) - settings.save('.settings') - self.grid:draw() - return true - end - end -end - -function systemPage.tabs.pathTab.grid:draw() - self.values = { } - for _,v in ipairs(Util.split(env.path, '(.-):')) do - table.insert(self.values, { value = v }) - end - self:update() - UI.Grid.draw(self) -end - -function systemPage.tabs.pathTab:eventHandler(event) - if event.type == 'update_path' then - env.path = self.entry.value - self.grid:setIndex(self.grid:getIndex()) - self.grid:draw() - Config.update('shell', env) - systemPage.notification:success('reboot to take effect') - return true - end -end - -function systemPage.tabs.aliasTab.grid:draw() - self.values = { } - for k,v in pairs(env.aliases) do - table.insert(self.values, { alias = k, path = v }) - end - self:update() - UI.Grid.draw(self) -end - -function systemPage.tabs.aliasTab:eventHandler(event) - if event.type == 'delete_alias' then - env.aliases[self.grid:getSelected().alias] = nil - self.grid:setIndex(self.grid:getIndex()) - self.grid:draw() - Config.update('shell', env) - systemPage.notification:success('reboot to take effect') - return true - - elseif event.type == 'new_alias' then - env.aliases[self.alias.value] = self.path.value - self.alias:reset() - self.path:reset() - self:draw() - self:setFocus(self.alias) - Config.update('shell', env) - systemPage.notification:success('reboot to take effect') - return true - end -end - -function systemPage.tabs.passwordTab:eventHandler(event) - if event.type == 'update_password' then - if #self.newPass.value == 0 then - systemPage.notification:error('Invalid password') - elseif Security.getPassword() and not Security.verifyPassword(SHA1.sha1(self.oldPass.value)) then - systemPage.notification:error('Passwords do not match') - else - Security.updatePassword(SHA1.sha1(self.newPass.value)) - self.oldPass.inactive = false - systemPage.notification:success('Password updated') - end - - return true - end -end - -function systemPage.tabs.infoTab:eventHandler(event) - if event.type == 'update_label' then - os.setComputerLabel(self.label.value) - systemPage.notification:success('Label updated') + systemPage.tabs:selectTab(tab) + self.parent:draw() return true end end @@ -318,13 +43,43 @@ end function systemPage:eventHandler(event) if event.type == 'quit' then UI:exitPullEvents() + + elseif event.type == 'success_message' then + self.notification:success(event.message) + + elseif event.type == 'info_message' then + self.notification:info(event.message) + + elseif event.type == 'error_message' then + self.notification:error(event.message) + elseif event.type == 'tab_activate' then event.activated:focusFirst() + else return UI.Page.eventHandler(self, event) end return true end +local function loadDirectory(dir) + local plugins = { } + for _, file in pairs(fs.list(dir)) do + local s, m = Util.run(_ENV, fs.combine(dir, file)) + if not s and m then + _G.printError('Error loading: ' .. file) + error(m or 'Unknown error') + elseif s and m then + table.insert(plugins, { tab = m, name = m.tabTitle, description = m.description }) + end + end + return plugins +end + +local programDir = fs.getDir(shell.getRunningProgram()) +local plugins = loadDirectory(fs.combine(programDir, 'system'), { }) + +systemPage.tabs.settings.grid:setValues(plugins) + UI:setPage(systemPage) UI:pullEvents() diff --git a/sys/apps/Tasks.lua b/sys/apps/Tasks.lua index 5bcd043..f67d71b 100644 --- a/sys/apps/Tasks.lua +++ b/sys/apps/Tasks.lua @@ -1,8 +1,5 @@ -_G.requireInjector(_ENV) - -local Event = require('event') -local UI = require('ui') -local Util = require('util') +local Event = require('opus.event') +local UI = require('opus.ui') local kernel = _G.kernel local multishell = _ENV.multishell @@ -29,7 +26,7 @@ local page = UI.Page { autospace = true, }, accelerators = { - q = 'quit', + [ 'control-q' ] = 'quit', space = 'activate', t = 'terminate', }, @@ -51,15 +48,15 @@ function page:eventHandler(event) end function page.grid:getDisplayValues(row) - row = Util.shallowCopy(row) local elapsed = os.clock()-row.timestamp - if elapsed < 60 then - row.timestamp = string.format("%ds", math.floor(elapsed)) - else - row.timestamp = string.format("%sm", math.floor(elapsed/6)/10) - end - row.status = row.isDead and 'error' or coroutine.status(row.co) - return row + 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 Event.onInterval(1, function() diff --git a/sys/apps/Welcome.lua b/sys/apps/Welcome.lua new file mode 100644 index 0000000..04b60f9 --- /dev/null +++ b/sys/apps/Welcome.lua @@ -0,0 +1,161 @@ +local Ansi = require('opus.ansi') +local Security = require('opus.security') +local SHA = require('opus.crypto.sha2') +local UI = require('opus.ui') + +local colors = _G.colors +local os = _G.os +local shell = _ENV.shell + +local splashIntro = [[First Time Setup + +%sThanks for installing Opus OS. The next screens will prompt you for basic settings for this computer.]] +local labelIntro = [[Set a friendly name for this computer. + +%sNo spaces recommended.]] +local passwordIntro = [[A password is required for wireless access. + +%sLeave blank to skip.]] +local packagesIntro = [[Setup Complete + +%sOpen the package manager to add software to this computer.]] +local contributorsIntro = [[Contributors%s + +Anavrins: Encryption/security/custom apps +Community: Several selected applications +hugeblank: Startup screen improvements +LDDestroier: Art design + custom apps +Lemmmy: Application improvements + +%sContribute at:%s +https://github.com/kepler155c/opus]] + +local page = UI.Page { + wizard = UI.Wizard { + ey = -2, + pages = { + splash = UI.WizardPage { + index = 1, + intro = UI.TextArea { + textColor = colors.yellow, + inactive = true, + x = 3, ex = -3, y = 2, ey = -2, + value = string.format(splashIntro, Ansi.white), + }, + }, + label = UI.WizardPage { + index = 2, + labelText = UI.Text { + x = 3, y = 2, + value = 'Label' + }, + label = UI.TextEntry { + x = 9, y = 2, ex = -3, + limit = 32, + value = os.getComputerLabel(), + }, + intro = UI.TextArea { + textColor = colors.yellow, + inactive = true, + x = 3, ex = -3, y = 4, ey = -3, + value = string.format(labelIntro, Ansi.white), + }, + }, + password = UI.WizardPage { + index = 3, + passwordLabel = UI.Text { + x = 3, y = 2, + value = 'Password' + }, + newPass = UI.TextEntry { + x = 12, ex = -3, y = 2, + limit = 32, + mask = true, + shadowText = 'password', + }, +--[[ + groupLabel = UI.Text { + x = 3, y = 3, + value = 'Group' + }, + group = UI.TextEntry { + x = 12, ex = -3, y = 3, + limit = 32, + shadowText = 'network group', + }, +]] + intro = UI.TextArea { + textColor = colors.yellow, + inactive = true, + x = 3, ex = -3, y = 5, ey = -3, + value = string.format(passwordIntro, Ansi.white), + }, + }, + packages = UI.WizardPage { + index = 4, + button = UI.Button { + x = 3, y = -3, + text = 'Open Package Manager', + event = 'packages', + }, + intro = UI.TextArea { + textColor = colors.yellow, + inactive = true, + x = 3, ex = -3, y = 2, ey = -4, + value = string.format(packagesIntro, Ansi.white), + }, + }, + contributors = UI.WizardPage { + index = 5, + intro = UI.TextArea { + textColor = colors.yellow, + inactive = true, + x = 3, ex = -3, y = 2, ey = -2, + value = string.format(contributorsIntro, Ansi.white, Ansi.yellow, Ansi.white), + }, + }, + }, + }, + notification = UI.Notification { }, +} + +function page.wizard.pages.label:validate() + os.setComputerLabel(self.label.value) + return true +end + +function page.wizard.pages.password:validate() + if type(self.newPass.value) == "string" and #self.newPass.value > 0 then + Security.updatePassword(SHA.compute(self.newPass.value)) + end + --[[ + if #self.group.value > 0 then + local config = Config.load('os') + config.group = self.group.value + Config.update('os', config) + end + ]] + return true +end + +function page:eventHandler(event) + if event.type == 'skip' then + self.wizard:emit({ type = 'nextView' }) + + elseif event.type == 'view_enabled' then + event.view:focusFirst() + + elseif event.type == 'packages' then + shell.openForegroundTab('PackageManager') + + elseif event.type == 'wizard_complete' or event.type == 'cancel' then + UI.exitPullEvents() + + else + return UI.Page.eventHandler(self, event) + end + return true +end + +UI:setPage(page) +UI:pullEvents() diff --git a/sys/apps/autorun.lua b/sys/apps/autorun.lua new file mode 100644 index 0000000..1274666 --- /dev/null +++ b/sys/apps/autorun.lua @@ -0,0 +1,70 @@ +local Packages = require('opus.packages') + +local colors = _G.colors +local fs = _G.fs +local keys = _G.keys +local multishell = _ENV.multishell +local os = _G.os +local shell = _ENV.shell +local term = _G.term + +local success = true + +local function runDir(directory) + if not fs.exists(directory) then + return true + end + + local files = fs.list(directory) + table.sort(files) + + for _,file in ipairs(files) do + os.sleep(0) + local result, err = shell.run(directory .. '/' .. file) + + if result then + if term.isColor() then + term.setTextColor(colors.green) + end + term.write('[PASS] ') + term.setTextColor(colors.white) + term.write(fs.combine(directory, file)) + print() + else + if term.isColor() then + term.setTextColor(colors.red) + end + term.write('[FAIL] ') + term.setTextColor(colors.white) + term.write(fs.combine(directory, file)) + if err then + _G.printError('\n' .. err) + end + print() + success = false + end + end +end + +runDir('sys/autorun') +for _, package in pairs(Packages:installedSorted()) do + local packageDir = 'packages/' .. package.name .. '/autorun' + runDir(packageDir) +end +runDir('usr/autorun') + +if not success then + if multishell then + multishell.setFocus(multishell.getCurrent()) + end + _G.printError('A startup program has errored') + print('Press enter to continue') + + while true do + local e, code = os.pullEventRaw('key') + if e == 'terminate' or e == 'key' and code == keys.enter then + break + end + end +end + diff --git a/sys/apps/cedit.lua b/sys/apps/cedit.lua new file mode 100644 index 0000000..2231e73 --- /dev/null +++ b/sys/apps/cedit.lua @@ -0,0 +1,38 @@ +local Config = require('opus.config') + +local multishell = _ENV.multishell +local os = _G.os +local read = _G.read +local shell = _ENV.shell + +local args = { ... } +if not args[1] then + error('Syntax: cedit ') +end + +if not _G.http.websocket then + error('Requires CC: Tweaked') +end + +if not _G.cloud_catcher then + local key = Config.load('cloud').key + + if not key then + print('Visit https://cloud-catcher.squiddev.cc') + print('Paste key: ') + key = read() + if #key == 0 then + return + end + end + + -- open an unfocused tab + local id = shell.openTab('cloud ' .. key) + print('Connecting...') + while not _G.cloud_catcher do + os.sleep(.2) + end + multishell.setTitle(id, 'Cloud') +end + +shell.run('cloud edit ' .. table.unpack({ ... })) diff --git a/sys/apps/cshell.lua b/sys/apps/cshell.lua new file mode 100644 index 0000000..9a0e1af --- /dev/null +++ b/sys/apps/cshell.lua @@ -0,0 +1,23 @@ +local Config = require('opus.config') + +local read = _G.read +local shell = _ENV.shell + +if not _G.http.websocket then + error('Requires CC: Tweaked') +end + +if not _G.cloud_catcher then + local key = Config.load('cloud').key + + if not key then + print('Visit https://cloud-catcher.squiddev.cc') + print('Paste key: ') + key = read() + if #key == 0 then + return + end + end + print('Connecting...') + shell.run('cloud ' .. key) +end diff --git a/sys/apps/mount.lua b/sys/apps/mount.lua index d7bde84..a25eb38 100644 --- a/sys/apps/mount.lua +++ b/sys/apps/mount.lua @@ -3,4 +3,4 @@ local args = { ... } local target = table.remove(args, 1) target = shell.resolve(target) -fs.mount(target, unpack(args)) +fs.mount(target, table.unpack(args)) diff --git a/sys/apps/netdaemon.lua b/sys/apps/netdaemon.lua index ae51a19..1d3fd66 100644 --- a/sys/apps/netdaemon.lua +++ b/sys/apps/netdaemon.lua @@ -1,7 +1,7 @@ _G.requireInjector(_ENV) -local Event = require('event') -local Util = require('util') +local Event = require('opus.event') +local Util = require('opus.util') local device = _G.device local fs = _G.fs @@ -14,9 +14,12 @@ if not device.wireless_modem then end print('Net daemon starting') +-- don't close as multiple computers may be sharing the +-- wireless modem +--device.wireless_modem.closeAll() -for _,file in pairs(fs.list('sys/network')) do - local fn, msg = Util.run(_ENV, 'sys/network/' .. file) +for _,file in pairs(fs.list('sys/apps/network')) do + local fn, msg = Util.run(_ENV, 'sys/apps/network/' .. file) if not fn then printError(msg) end diff --git a/sys/apps/network/keygen.lua b/sys/apps/network/keygen.lua new file mode 100644 index 0000000..65f04b8 --- /dev/null +++ b/sys/apps/network/keygen.lua @@ -0,0 +1,39 @@ +local ECC = require('opus.crypto.ecc') +local Event = require('opus.event') +local Util = require('opus.util') + +local network = _G.network +local os = _G.os + +local keyPairs = { } + +local function generateKeyPair() + local key = { } + for _ = 1, 32 do + table.insert(key, ("%02x"):format(math.random(0, 0xFF))) + end + local privateKey = Util.hexToByteArray(table.concat(key)) + return privateKey, ECC.publicKey(privateKey) +end + +getmetatable(network).__index.getKeyPair = function() + local keys = table.remove(keyPairs) + os.queueEvent('generate_keypair') + if not keys then + return generateKeyPair() + end + return table.unpack(keys) +end + +-- Generate key pairs in the background as this is a time-consuming process +Event.on('generate_keypair', function() + while true do + os.sleep(5) + local timer = Util.timer() + table.insert(keyPairs, { generateKeyPair() }) + _G._syslog('Generated keypair in ' .. timer()) + if #keyPairs >= 3 then + break + end + end +end) diff --git a/sys/apps/network/proxy.lua b/sys/apps/network/proxy.lua new file mode 100644 index 0000000..9e487b0 --- /dev/null +++ b/sys/apps/network/proxy.lua @@ -0,0 +1,64 @@ +local Event = require('opus.event') +local Socket = require('opus.socket') +local Util = require('opus.util') + +local function getProxy(path) + local x = Util.split(path, '(.-)/') + local proxy = _G + for _, v in pairs(x) do + proxy = proxy[v] + if not proxy then + break + end + end + return proxy +end + +local function proxyConnection(socket) + local path = socket:read(2) + if path then + local api = getProxy(path) + + if not api then + print('proxy: invalid API') + socket:close() + return + end + + local methods = { } + for k,v in pairs(api) do + if type(v) == 'function' then + table.insert(methods, k) + end + end + socket:write(methods) + + while true do + local data = socket:read() + if not data then + print('proxy: lost connection from ' .. socket.dhost) + break + end + socket:write({ api[data[1]](table.unpack(data, 2)) }) + end + end +end + +Event.addRoutine(function() + print('proxy: listening on port 188') + while true do + local socket = Socket.server(188) + + print('proxy: connection from ' .. socket.dhost) + + Event.addRoutine(function() + local s, m = pcall(proxyConnection, socket) + print('proxy: closing connection to ' .. socket.dhost) + socket:close() + if not s and m then + print('Proxy error') + _G.printError(m) + end + end) + end +end) diff --git a/sys/network/samba.lua b/sys/apps/network/samba.lua similarity index 86% rename from sys/network/samba.lua rename to sys/apps/network/samba.lua index c87c1c0..50cbfd0 100644 --- a/sys/network/samba.lua +++ b/sys/apps/network/samba.lua @@ -1,5 +1,5 @@ -local Event = require('event') -local Socket = require('socket') +local Event = require('opus.event') +local Socket = require('opus.socket') local fs = _G.fs @@ -48,7 +48,7 @@ local function sambaConnection(socket) end local ret local s, m = pcall(function() - ret = fn(unpack(msg.args)) + ret = fn(table.unpack(msg.args)) end) if not s and m then _G.printError('samba: ' .. m) @@ -67,8 +67,13 @@ Event.addRoutine(function() Event.addRoutine(function() print('samba: connection from ' .. socket.dhost) - sambaConnection(socket) + local s, m = pcall(sambaConnection, socket) print('samba: closing connection to ' .. socket.dhost) + socket:close() + if not s and m then + print('Samba error') + _G.printError(m) + end end) end end) diff --git a/sys/network/snmp.lua b/sys/apps/network/snmp.lua similarity index 66% rename from sys/network/snmp.lua rename to sys/apps/network/snmp.lua index 50297b1..0fc9a34 100644 --- a/sys/network/snmp.lua +++ b/sys/apps/network/snmp.lua @@ -1,7 +1,7 @@ -local Event = require('event') -local GPS = require('gps') -local Socket = require('socket') -local Util = require('util') +local Event = require('opus.event') +local GPS = require('opus.gps') +local Socket = require('opus.socket') +local Util = require('opus.util') local device = _G.device local kernel = _G.kernel @@ -106,8 +106,12 @@ Event.addRoutine(function() Event.addRoutine(function() print('snmp: connection from ' .. socket.dhost) - snmpConnection(socket) + local s, m = pcall(snmpConnection, socket) print('snmp: closing connection to ' .. socket.dhost) + if not s and m then + print('snmp error') + _G.printError(m) + end end) end end) @@ -117,16 +121,25 @@ print('discovery: listening on port 999') Event.on('modem_message', function(_, _, sport, id, info, distance) if sport == 999 and tonumber(id) and type(info) == 'table' then - if not network[id] then - network[id] = { } - end - Util.merge(network[id], info) - network[id].distance = distance - network[id].timestamp = os.clock() + if type(info.label) == 'string' and type(info.id) == 'number' then - if not network[id].active then - network[id].active = true - os.queueEvent('network_attach', network[id]) + if not network[id] then + network[id] = { } + end + Util.merge(network[id], info) + network[id].distance = type(distance) == 'number' and distance + network[id].timestamp = os.clock() + + if not network[id].label then + network[id].label = 'unknown' + end + + if not network[id].active then + network[id].active = true + os.queueEvent('network_attach', network[id]) + end + else + print('discovery: Invalid alive message ' .. id) end end end) @@ -136,27 +149,53 @@ local info = { } local infoTimer = os.clock() +local function getSlots() + return Util.reduce(turtle.getInventory(), function(acc, v) + if v.count > 0 then + acc[v.index .. ',' .. v.count] = v.key + end + return acc + end, { }) +end + local function sendInfo() if os.clock() - infoTimer >= 1 then -- don't flood infoTimer = os.clock() info.label = os.getComputerLabel() info.uptime = math.floor(os.clock()) - if turtle then + info.group = network.getGroup() + if turtle and turtle.getStatus then info.fuel = turtle.getFuelLevel() info.status = turtle.getStatus() info.point = turtle.point - info.inventory = turtle.getInventory() + info.inv = getSlots() info.slotIndex = turtle.getSelectedSlot() end if device.neuralInterface then info.status = device.neuralInterface.status - pcall(function() - if not info.status and device.neuralInterface.getMetaOwner then - info.status = 'health: ' .. - math.floor(device.neuralInterface.getMetaOwner().health / - device.neuralInterface.getMetaOwner().maxHealth * 100) - end - end) + if not info.status and device.neuralInterface.getMetaOwner then + pcall(function() + local meta = device.neuralInterface.getMetaOwner() + local states = { + isWet = 'Swimming', + isElytraFlying = 'Flying', + isBurning = 'Burning', + isDead = 'Deceased', + isOnLadder = 'Climbing', + isRiding = 'Riding', + isSneaking = 'Sneaking', + isSprinting = 'Running', + } + for k,v in pairs(states) do + if meta[k] then + info.status = v + break + end + end + info.status = info.status or 'health: ' .. + math.floor(meta.health / meta.maxHealth * 100) + end) + end end device.wireless_modem.transmit(999, os.getComputerID(), info) end @@ -180,3 +219,5 @@ Event.on('turtle_response', function() sendInfo() end end) + +Event.onTimeout(1, sendInfo) diff --git a/sys/network/telnet.lua b/sys/apps/network/telnet.lua similarity index 66% rename from sys/network/telnet.lua rename to sys/apps/network/telnet.lua index c0d5f07..9aec83c 100644 --- a/sys/network/telnet.lua +++ b/sys/apps/network/telnet.lua @@ -1,12 +1,13 @@ -local Event = require('event') -local Socket = require('socket') -local Util = require('util') +local Alt = require('opus.alternate') +local Event = require('opus.event') +local Socket = require('opus.socket') +local Util = require('opus.util') local kernel = _G.kernel local term = _G.term local window = _G.window -local function telnetHost(socket) +local function telnetHost(socket, mode) local methods = { 'clear', 'clearLine', 'setCursorPos', 'write', 'blit', 'setTextColor', 'setTextColour', 'setBackgroundColor', 'setBackgroundColour', 'scroll', 'setCursorBlink', } @@ -43,10 +44,10 @@ local function telnetHost(socket) local shellThread = kernel.run({ terminal = win, window = win, - title = 'Telnet client', + title = mode .. ' client', hidden = true, co = coroutine.create(function() - Util.run(_ENV, 'sys/apps/shell', table.unpack(termInfo.program)) + Util.run(_ENV, Alt.get('shell'), table.unpack(termInfo.program)) if socket.queue then socket:write(socket.queue) end @@ -68,6 +69,23 @@ local function telnetHost(socket) end) end +Event.addRoutine(function() + print('ssh: listening on port 22') + while true do + local socket = Socket.server(22, { ENCRYPT = true }) + + print('ssh: connection from ' .. socket.dhost) + + Event.addRoutine(function() + local s, m = pcall(telnetHost, socket, 'SSH') + if not s and m then + print('ssh error') + _G.printError(m) + end + end) + end +end) + Event.addRoutine(function() print('telnet: listening on port 23') while true do @@ -76,7 +94,11 @@ Event.addRoutine(function() print('telnet: connection from ' .. socket.dhost) Event.addRoutine(function() - telnetHost(socket) + local s, m = pcall(telnetHost, socket, 'Telnet') + if not s and m then + print('Telnet error') + _G.printError(m) + end end) end end) diff --git a/sys/network/transport.lua b/sys/apps/network/transport.lua similarity index 59% rename from sys/network/transport.lua rename to sys/apps/network/transport.lua index 2eeb801..2732d12 100644 --- a/sys/network/transport.lua +++ b/sys/apps/network/transport.lua @@ -5,17 +5,23 @@ * background read buffering ]]-- -local Event = require('event') +local Crypto = require('opus.crypto.chacha20') +local Event = require('opus.event') +local network = _G.network local os = _G.os local computerId = os.getComputerID() local transport = { timers = { }, sockets = { }, + encryptQueue = { }, UID = 0, } -_G.transport = transport + +getmetatable(network).__index.getTransport = function() + return transport +end function transport.open(socket) transport.UID = transport.UID + 1 @@ -28,24 +34,26 @@ end function transport.read(socket) local data = table.remove(socket.messages, 1) if data then - return unpack(data) + if socket.options.ENCRYPT then + return table.unpack(Crypto.decrypt(data[1], socket.enckey)), data[2] + end + return table.unpack(data) end end -function transport.write(socket, data) - --_debug('>> ' .. Util.tostring({ type = 'DATA', seq = socket.wseq })) - socket.transmit(socket.dport, socket.dhost, data) - - --local timerId = os.startTimer(3) - - --transport.timers[timerId] = socket - --socket.timers[socket.wseq] = timerId - - socket.wseq = socket.wseq + 1 +function transport.write(socket, msg) + if socket.options.ENCRYPT then + if #transport.encryptQueue == 0 then + os.queueEvent('transport_encrypt') + end + table.insert(transport.encryptQueue, { socket.sport, msg }) + else + socket.transmit(socket.dport, socket.dhost, msg) + end + socket.wseq = socket.wrng:nextInt(5) end function transport.ping(socket) - --_debug('>> ' .. Util.tostring({ type = 'DATA', seq = socket.wseq })) if os.clock() - socket.activityTimer > 10 then socket.activityTimer = os.clock() socket.transmit(socket.dport, socket.dhost, { @@ -53,7 +61,7 @@ function transport.ping(socket) seq = -1, }) - local timerId = os.startTimer(5) + local timerId = os.startTimer(3) transport.timers[timerId] = socket socket.timers[-1] = timerId end @@ -63,6 +71,19 @@ function transport.close(socket) transport.sockets[socket.sport] = nil end +Event.on('transport_encrypt', function() + while #transport.encryptQueue > 0 do + local entry = table.remove(transport.encryptQueue, 1) + local socket = transport.sockets[entry[1]] + + if socket and socket.connected then + local msg = entry[2] + msg.data = Crypto.encrypt({ msg.data }, socket.enckey) + socket.transmit(socket.dport, socket.dhost, msg) + end + end +end) + Event.on('timer', function(_, timerId) local socket = transport.timers[timerId] @@ -78,18 +99,19 @@ Event.on('modem_message', function(_, _, dport, dhost, msg, distance) local socket = transport.sockets[dport] if socket and socket.connected then - --if msg.type then _debug('<< ' .. Util.tostring(msg)) end if socket.co and coroutine.status(socket.co) == 'dead' then - _G._debug('socket coroutine dead') + _G._syslog('socket coroutine dead') socket:close() elseif msg.type == 'DISC' then -- received disconnect from other end - if socket.connected then - os.queueEvent('transport_' .. socket.uid) + if msg.seq == socket.rseq then + if socket.connected then + os.queueEvent('transport_' .. socket.uid) + end + socket.connected = false + socket:close() end - socket.connected = false - socket:close() elseif msg.type == 'ACK' then local ackTimerId = socket.timers[msg.seq] @@ -108,28 +130,20 @@ Event.on('modem_message', function(_, _, dport, dhost, msg, distance) }) elseif msg.type == 'DATA' and msg.data then - socket.activityTimer = os.clock() if msg.seq ~= socket.rseq then - print('transport seq error - closing socket ' .. socket.sport) - _debug(msg.data) - _debug('current ' .. socket.rseq) - _debug('expected ' .. msg.seq) --- socket:close() --- os.queueEvent('transport_' .. socket.uid) + print('transport seq error ' .. socket.sport) + _syslog(msg.data) + _syslog('expected ' .. socket.rseq) + _syslog('got ' .. msg.seq) else - socket.rseq = socket.rseq + 1 + socket.activityTimer = os.clock() + socket.rseq = socket.rrng:nextInt(5) + table.insert(socket.messages, { msg.data, distance }) - -- use resume instead ?? if not socket.messages[2] then -- table size is 1 os.queueEvent('transport_' .. socket.uid) end - - --_debug('>> ' .. Util.tostring({ type = 'ACK', seq = msg.seq })) - --socket.transmit(socket.dport, socket.dhost, { - -- type = 'ACK', - -- seq = msg.seq, - --}) end end end diff --git a/sys/apps/network/trust.lua b/sys/apps/network/trust.lua new file mode 100644 index 0000000..c31b6ec --- /dev/null +++ b/sys/apps/network/trust.lua @@ -0,0 +1,46 @@ +local Crypto = require('opus.crypto.chacha20') +local Event = require('opus.event') +local Security = require('opus.security') +local Socket = require('opus.socket') +local Util = require('opus.util') + +local trustId = '01c3ba27fe01383a03a1785276d99df27c3edcef68fbf231ca' + +local function trustConnection(socket) + local data = socket:read(2) + if data then + local password = Security.getPassword() + if not password then + socket:write({ msg = 'No password has been set' }) + else + local s + s, data = pcall(Crypto.decrypt, data, password) + if s and data and data.pk and data.dh == socket.dhost then + local trustList = Util.readTable('usr/.known_hosts') or { } + trustList[data.dh] = data.pk + Util.writeTable('usr/.known_hosts', trustList) + + socket:write({ success = true, msg = 'Trust accepted' }) + else + socket:write({ msg = 'Invalid password' }) + end + end + end +end + +Event.addRoutine(function() + print('trust: listening on port 19') + + while true do + local socket = Socket.server(19, { identifier = trustId }) + + print('trust: connection from ' .. socket.dhost) + + local s, m = pcall(trustConnection, socket) + socket:close() + if not s and m then + print('Trust error') + _G.printError(m) + end + end +end) diff --git a/sys/network/vnc.lua b/sys/apps/network/vnc.lua similarity index 69% rename from sys/network/vnc.lua rename to sys/apps/network/vnc.lua index 7c4febc..c833ead 100644 --- a/sys/network/vnc.lua +++ b/sys/apps/network/vnc.lua @@ -1,6 +1,6 @@ -local Event = require('event') -local Socket = require('socket') -local Util = require('util') +local Event = require('opus.event') +local Socket = require('opus.socket') +local Util = require('opus.util') local os = _G.os local terminal = _G.device.terminal @@ -63,7 +63,31 @@ Event.addRoutine(function() -- no new process - only 1 connection allowed -- due to term size issues - vncHost(socket) + local s, m = pcall(vncHost, socket) socket:close() + if not s and m then + print('vnc error') + _G.printError(m) + end + end +end) + +Event.addRoutine(function() + + print('svnc: listening on port 5901') + + while true do + local socket = Socket.server(5901, { ENCRYPT = true }) + + print('svnc: connection from ' .. socket.dhost) + + -- no new process - only 1 connection allowed + -- due to term size issues + local s, m = pcall(vncHost, socket) + socket:close() + if not s and m then + print('vnc error') + _G.printError(m) + end end end) diff --git a/sys/apps/package.lua b/sys/apps/package.lua index 1f0d212..b5fdba3 100644 --- a/sys/apps/package.lua +++ b/sys/apps/package.lua @@ -1,8 +1,7 @@ -_G.requireInjector(_ENV) - -local Git = require('git') -local Packages = require('packages') -local Util = require('util') +local BulkGet = require('opus.bulkget') +local Git = require('opus.git') +local Packages = require('opus.packages') +local Util = require('opus.util') local fs = _G.fs local term = _G.term @@ -10,6 +9,12 @@ local term = _G.term local args = { ... } local action = table.remove(args, 1) +local function makeSandbox() + local sandbox = setmetatable(Util.shallowCopy(_ENV), { __index = _G }) + _G.requireInjector(sandbox) + return sandbox +end + local function Syntax(msg) _G.printError(msg) print('\nSyntax: Package list | install [name] ... | update [name] | uninstall [name]') @@ -36,24 +41,61 @@ local function progress(max) end end -local function install(name) - local manifest = Packages:getManifest(name) or error('Invalid package') - local packageDir = fs.combine('packages', name) - local method = args[2] or 'local' - if method == 'remote' then - Util.writeTable(packageDir .. '/.install', { - mount = string.format('%s gitfs %s', packageDir, manifest.repository), - }) - Util.writeTable(fs.combine(packageDir, '.package'), manifest) - else - local list = Git.list(manifest.repository) - local showProgress = progress(Util.size(list)) - for path, entry in pairs(list) do - Util.download(entry.url, fs.combine(packageDir, path)) - showProgress() +local function runScript(script) + if script then + local s, m = pcall(function() + local fn, m = load(script, 'script', nil, makeSandbox()) + if not fn then + error(m) + end + fn() + end) + if not s and m then + _G.printError(m) end end - return +end + +local function install(name, isUpdate, ignoreDeps) + local manifest = Packages:downloadManifest(name) or error('Invalid package') + + if not ignoreDeps then + if manifest.required then + for _, v in pairs(manifest.required) do + if isUpdate or not Packages:isInstalled(v) then + install(v, isUpdate) + end + end + end + end + + print(string.format('%s: %s', + isUpdate and 'Updating' or 'Installing', + name)) + + local packageDir = fs.combine('packages', name) + + local list = Git.list(manifest.repository) + local showProgress = progress(Util.size(list)) + + local getList = { } + for path, entry in pairs(list) do + table.insert(getList, { + path = fs.combine(packageDir, path), + url = entry.url + }) + end + + BulkGet.download(getList, function(_, s, m) + if not s then + error(m) + end + showProgress() + end) + + if not isUpdate then + runScript(manifest.install) + end end if action == 'list' then @@ -69,7 +111,23 @@ if action == 'install' then error('Package is already installed') end install(name) - print('installation complete') + print('installation complete\n') + _G.printError('Reboot is required') + return +end + +if action == 'refresh' then + print('Downloading...') + Packages:downloadList() + print('refresh complete') + return +end + +if action == 'updateall' then + for name in pairs(Packages:installed()) do + install(name, true, true) + end + print('updateall complete') return end @@ -78,7 +136,7 @@ if action == 'update' then if not Packages:isInstalled(name) then error('Package is not installed') end - install(name) + install(name, true) print('update complete') return end @@ -88,6 +146,10 @@ if action == 'uninstall' then if not Packages:isInstalled(name) then error('Package is not installed') end + + local manifest = Packages:getManifest(name) + runScript(manifest.uninstall) + local packageDir = fs.combine('packages', name) fs.delete(packageDir) print('removed: ' .. packageDir) diff --git a/sys/apps/password.lua b/sys/apps/password.lua index eb3c5b0..e1585b0 100644 --- a/sys/apps/password.lua +++ b/sys/apps/password.lua @@ -1,12 +1,10 @@ -_G.requireInjector(_ENV) - -local Security = require('security') -local SHA1 = require('sha1') -local Terminal = require('terminal') +local Security = require('opus.security') +local SHA = require('opus.crypto.sha2') +local Terminal = require('opus.terminal') local password = Terminal.readPassword('Enter new password: ') if password then - Security.updatePassword(SHA1.sha1(password)) + Security.updatePassword(SHA.compute(password)) print('Password updated') end diff --git a/sys/apps/pastebin.lua b/sys/apps/pastebin.lua new file mode 100644 index 0000000..aaade9a --- /dev/null +++ b/sys/apps/pastebin.lua @@ -0,0 +1,114 @@ +local function printUsage() + print( "Usages:" ) + print( "pastebin put " ) + print( "pastebin get " ) + print( "pastebin run " ) +end + +if not http then + printError( "Pastebin requires http API" ) + printError( "Set http_enable to true in ComputerCraft.cfg" ) + return +end + +local pastebin = require('opus.http.pastebin') + +local tArgs = { ... } +local sCommand = tArgs[1] + +if sCommand == "put" then + -- Upload a file to pastebin.com + + if #tArgs < 2 then + printUsage() + return + end + + -- Determine file to upload + local sFile = tArgs[2] + local sPath = shell.resolve( sFile ) + if not fs.exists( sPath ) or fs.isDir( sPath ) then + print( "No such file" ) + return + end + + print( "Connecting to pastebin.com... " ) + + local resp, msg = pastebin.put(sPath) + + if resp then + print( "Uploaded as " .. resp ) + print( "Run \"pastebin get "..resp.."\" to download anywhere" ) + + else + printError( msg ) + end + +elseif sCommand == "get" then + -- Download a file from pastebin.com + + if #tArgs < 3 then + printUsage() + return + end + + local sCode = pastebin.parseCode(tArgs[2]) + if not sCode then + return false, "Invalid pastebin code. The code is the ID at the end of the pastebin.com URL." + end + + -- Determine file to download + local sFile = tArgs[3] + local sPath = shell.resolve( sFile ) + if fs.exists( sPath ) then + printError( "File already exists" ) + return + end + + print( "Connecting to pastebin.com... " ) + + local resp, msg = pastebin.get(sCode, sPath) + + if resp then + print( "Downloaded as " .. sPath ) + else + printError( msg ) + end + +elseif sCommand == "run" then + -- Download and run a file from pastebin.com + + if #tArgs < 2 then + printUsage() + return + end + + local sCode = pastebin.parseCode(tArgs[2]) + if not sCode then + return false, "Invalid pastebin code. The code is the ID at the end of the pastebin.com URL." + end + + print( "Connecting to pastebin.com... " ) + + local res, msg = pastebin.download(sCode) + if not res then + printError( msg ) + return res, msg + end + + res, msg = load(res, sCode, "t", _ENV) + if not res then + printError( msg ) + return res, msg + end + + res, msg = pcall(res, table.unpack(tArgs, 3)) + if not res then + printError( msg ) + end +else + + printUsage() + return +end + diff --git a/sys/apps/shell b/sys/apps/shell.lua similarity index 80% rename from sys/apps/shell rename to sys/apps/shell.lua index 31ce233..d64f046 100644 --- a/sys/apps/shell +++ b/sys/apps/shell.lua @@ -3,6 +3,7 @@ local parentShell = _ENV.shell _ENV.shell = { } local fs = _G.fs +local settings = _G.settings local shell = _ENV.shell local sandboxEnv = setmetatable({ }, { __index = _G }) @@ -13,7 +14,8 @@ sandboxEnv.shell = shell _G.requireInjector(_ENV) -local Util = require('util') +local trace = require('opus.trace') +local Util = require('opus.util') local DIR = (parentShell and parentShell.dir()) or "" local PATH = (parentShell and parentShell.path()) or ".:/rom/programs" @@ -65,11 +67,12 @@ local function run(env, ...) end if isUrl then - tProgramStack[#tProgramStack + 1] = path:match("^https?://([^/:]+:?[0-9]*/?.*)$") + tProgramStack[#tProgramStack + 1] = path -- path:match("^https?://([^/:]+:?[0-9]*/?.*)$") else tProgramStack[#tProgramStack + 1] = path end + env[ "arg" ] = { [0] = path, table.unpack(args) } local r = { fn(table.unpack(args)) } tProgramStack[#tProgramStack] = nil @@ -86,7 +89,9 @@ function shell.run(...) end local env = setmetatable(Util.shallowCopy(sandboxEnv), { __index = _G }) - local r = { pcall(run, env, ...) } + _G.requireInjector(env) + + local r = { trace(run, env, ...) } if _ENV.multishell then _ENV.multishell.setTitle(_ENV.multishell.getCurrent(), oldTitle or 'shell') @@ -312,9 +317,9 @@ function shell.newTab(tabInfo, ...) tabInfo.args = args tabInfo.title = fs.getName(path):match('([^%.]+)') - if path ~= 'sys/apps/shell' then + if path ~= 'sys/apps/shell.lua' then table.insert(tabInfo.args, 1, tabInfo.path) - tabInfo.path = 'sys/apps/shell' + tabInfo.path = 'sys/apps/shell.lua' end return _ENV.multishell.openTab(tabInfo) end @@ -327,10 +332,10 @@ function shell.openTab( ... ) local sCommand = tWords[1] if sCommand then local sPath = shell.resolveProgram(sCommand) - if sPath == "sys/apps/shell" then + if sPath == "sys/apps/shell.lua" then return _ENV.multishell.launch(Util.shallowCopy(sandboxEnv), sPath, table.unpack(tWords, 2)) else - return _ENV.multishell.launch(Util.shallowCopy(sandboxEnv), "sys/apps/shell", sCommand, table.unpack(tWords, 2)) + return _ENV.multishell.launch(Util.shallowCopy(sandboxEnv), "sys/apps/shell.lua", sCommand, table.unpack(tWords, 2)) end end end @@ -350,34 +355,35 @@ end local tArgs = { ... } if #tArgs > 0 then local env = setmetatable(Util.shallowCopy(sandboxEnv), { __index = _G }) + _G.requireInjector(env) + return run(env, ...) end -local Config = require('config') -local Entry = require('entry') -local History = require('history') -local Input = require('input') -local Terminal = require('terminal') +local Config = require('opus.config') +local Entry = require('opus.entry') +local History = require('opus.history') +local Input = require('opus.input') +local Sound = require('opus.sound') +local Terminal = require('opus.terminal') local colors = _G.colors local os = _G.os local term = _G.term local textutils = _G.textutils -local terminal = term.current() ---Terminal.scrollable(terminal, 100) -terminal.noAutoScroll = true +local oldTerm +local terminal = term.current() +local _rep = string.rep +local _sub = string.sub + +if not terminal.scrollUp then + terminal = Terminal.window(term.current()) + terminal.setMaxScroll(200) + oldTerm = term.redirect(terminal) +end local config = { - standard = { - textColor = colors.white, - commandTextColor = colors.lightGray, - directoryTextColor = colors.gray, - directoryBackgroundColor = colors.black, - promptTextColor = colors.gray, - promptBackgroundColor = colors.black, - directoryColor = colors.gray, - }, color = { textColor = colors.white, commandTextColor = colors.yellow, @@ -386,15 +392,34 @@ local config = { promptTextColor = colors.blue, promptBackgroundColor = colors.black, directoryColor = colors.green, + fileColor = colors.white, + backgroundColor = colors.black, }, displayDirectory = true, } Config.load('shellprompt', config) -local _colors = config.standard -if term.isColor() then - _colors = config.color +local _colors = config.color +-- temp +if not _colors.backgroundColor then + _colors.backgroundColor = colors.black + _colors.fileColor = colors.white +end + +local palette = { } +for n = 1, 16 do + palette[2 ^ (n - 1)] = _sub("0123456789abcdef", n, n) +end + +if not term.isColor() then + _colors = { } + for k, v in pairs(config.color) do + _colors[k] = Terminal.colorToGrayscale(v) + end + for n = 1, 16 do + palette[2 ^ (n - 1)] = _sub("088888878877787f", n, n) + end end local function autocompleteArgument(program, words) @@ -526,9 +551,9 @@ local function autocomplete(line) end if #tDirs > 0 then - textutils.tabulate(_colors.directoryColor, tDirs, colors.white, tFiles) + textutils.tabulate(_colors.directoryColor, tDirs, _colors.fileColor, tFiles) else - textutils.tabulate(colors.white, tFiles) + textutils.tabulate(_colors.fileColor, tFiles) end term.setTextColour(_colors.promptTextColor) @@ -536,7 +561,7 @@ local function autocomplete(line) term.write("$ " ) term.setTextColour(_colors.commandTextColor) - term.setBackgroundColor(colors.black) + term.setBackgroundColor(_colors.backgroundColor) return line end end @@ -544,21 +569,38 @@ end local function shellRead(history) local lastLen = 0 local entry = Entry({ - width = term.getSize() - 3 + width = term.getSize() - 3, + offset = 3, }) history:reset() term.setCursorBlink(true) + local function updateCursor() + term.setCursorPos(3 + entry.pos - entry.scroll, select(2, term.getCursorPos())) + end + local function redraw() + if terminal.scrollBottom then + terminal.scrollBottom() + end local _,cy = term.getCursorPos() term.setCursorPos(3, cy) local filler = #entry.value < lastLen and string.rep(' ', lastLen - #entry.value) or '' - local str = string.sub(entry.value, entry.scroll + 1) - term.write(string.sub(str, 1, entry.width) .. filler) - term.setCursorPos(3 + entry.pos - entry.scroll, cy) + local str = string.sub(entry.value, entry.scroll + 1, entry.width + entry.scroll) .. filler + local fg = _rep(palette[_colors.commandTextColor], #str) + local bg = _rep(palette[_colors.backgroundColor], #str) + if entry.mark.active then + local sx = entry.mark.x - entry.scroll + 1 + local ex = entry.mark.ex - entry.scroll + 1 + bg = string.rep('f', sx - 1) .. + string.rep('7', ex - sx) .. + string.rep('f', #str - ex + 1) + end + term.blit(str, fg, bg) + updateCursor() lastLen = #entry.value end @@ -567,11 +609,11 @@ local function shellRead(history) local ie = Input:translate(event, p1, p2, p3) if ie then - if ie.code == 'scroll_up' then - --terminal.scrollUp() + if ie.code == 'scroll_up' and terminal.scrollUp then + terminal.scrollUp() - elseif ie.code == 'scroll_down' then - --terminal.scrollDown() + elseif ie.code == 'scroll_down' and terminal.scrollDown then + terminal.scrollDown() elseif ie.code == 'terminate' then bExit = true @@ -580,14 +622,15 @@ local function shellRead(history) elseif ie.code == 'enter' then break - elseif ie.code == 'up' or ie.code == 'down' then - if ie.code == 'up' then + elseif ie.code == 'up' or ie.code == 'control-p' or + ie.code == 'down' or ie.code == 'control-n' then + entry:reset() + if ie.code == 'up' or ie.code == 'control-p' then entry.value = history:back() or '' else entry.value = history:forward() or '' end - entry.pos = string.len(entry.value) - entry.scroll = 0 + entry.pos = #entry.value entry:updateScroll() redraw() @@ -597,23 +640,30 @@ local function shellRead(history) if cline then entry.value = cline entry.pos = #entry.value + entry:unmark() entry:updateScroll() redraw() + else + Sound.play('entity.villager.no') end end - elseif entry:process(ie) then - redraw() + else + entry:process(ie) + if entry.textChanged then + redraw() + elseif entry.posChanged then + updateCursor() + end end elseif event == "term_resize" then entry.width = term.getSize() - 3 + entry:updateScroll() redraw() end end - --local _, cy = term.getCursorPos() - --term.setCursorPos( w + 1, cy ) print() term.setCursorBlink( false ) return entry.value @@ -621,6 +671,13 @@ end local history = History.load('usr/.shell_history', 25) +term.setBackgroundColor(_colors.backgroundColor) +term.clear() + +if settings.get("motd.enabled") then + shell.run("motd") +end + while not bExit do if config.displayDirectory then term.setTextColour(_colors.directoryTextColor) @@ -631,7 +688,7 @@ while not bExit do term.setBackgroundColor(_colors.promptBackgroundColor) term.write("$ " ) term.setTextColour(_colors.commandTextColor) - term.setBackgroundColor(colors.black) + term.setBackgroundColor(_colors.backgroundColor) local sLine = shellRead(history) if bExit then -- terminated break @@ -648,3 +705,7 @@ while not bExit do end end end + +if oldTerm then + term.redirect(oldTerm) +end diff --git a/sys/apps/system/aliases.lua b/sys/apps/system/aliases.lua new file mode 100644 index 0000000..6b04c5c --- /dev/null +++ b/sys/apps/system/aliases.lua @@ -0,0 +1,72 @@ +local Config = require('opus.config') +local UI = require('opus.ui') + +local kernel = _G.kernel + +local aliasTab = UI.Tab { + tabTitle = 'Aliases', + description = 'Shell aliases', + alias = UI.TextEntry { + x = 2, y = 2, ex = -2, + limit = 32, + shadowText = 'Alias', + }, + path = UI.TextEntry { + y = 3, x = 2, ex = -2, + limit = 256, + shadowText = 'Program path', + accelerators = { + enter = 'new_alias', + }, + }, + grid = UI.Grid { + y = 5, + sortColumn = 'alias', + columns = { + { heading = 'Alias', key = 'alias' }, + { heading = 'Program', key = 'path' }, + }, + accelerators = { + delete = 'delete_alias', + }, + }, +} + +function aliasTab.grid:draw() + self.values = { } + local env = Config.load('shell') + for k in pairs(kernel.getShell().aliases()) do + kernel.getShell().clearAlias(k) + end + for k,v in pairs(env.aliases) do + table.insert(self.values, { alias = k, path = v }) + kernel.getShell().setAlias(k, v) + end + self:update() + UI.Grid.draw(self) +end + +function aliasTab:eventHandler(event) + if event.type == 'delete_alias' then + local env = Config.load('shell', { aliases = { } }) + env.aliases[self.grid:getSelected().alias] = nil + Config.update('shell', env) + self.grid:setIndex(self.grid:getIndex()) + self.grid:draw() + self:emit({ type = 'success_message', message = 'Aliases updated' }) + return true + + elseif event.type == 'new_alias' then + local env = Config.load('shell', { aliases = { } }) + env.aliases[self.alias.value] = self.path.value + Config.update('shell', env) + self.alias:reset() + self.path:reset() + self:draw() + self:setFocus(self.alias) + self:emit({ type = 'success_message', message = 'Aliases updated' }) + return true + end +end + +return aliasTab diff --git a/sys/apps/system/alternate.lua b/sys/apps/system/alternate.lua new file mode 100644 index 0000000..6894428 --- /dev/null +++ b/sys/apps/system/alternate.lua @@ -0,0 +1,80 @@ +local Array = require('opus.array') +local Config = require('opus.config') +local UI = require('opus.ui') + +local colors = _G.colors + +local tab = UI.Tab { + tabTitle = 'Preferred', + description = 'Select preferred applications', + apps = UI.ScrollingGrid { + x = 2, y = 2, + ex = 12, ey = -3, + columns = { + { key = 'name' }, + }, + sortColumn = 'name', + disableHeader = true, + }, + choices = UI.Grid { + x = 14, y = 2, + ex = -2, ey = -3, + disableHeader = true, + columns = { + { key = 'file' }, + } + }, + statusBar = UI.StatusBar { + values = 'Double-click to set as preferred' + }, +} + +function tab.choices:getRowTextColor(row) + if row == self.values[1] then + return colors.yellow + end + return UI.Grid.getRowTextColor(self, row) +end + +function tab:updateChoices() + local app = self.apps:getSelected().name + local choices = { } + for _, v in pairs(self.config[app]) do + table.insert(choices, { file = v }) + end + self.choices:setValues(choices) + self.choices:draw() +end + +function tab:enable() + self.config = Config.load('alternate') + + local apps = { } + for k, _ in pairs(self.config) do + table.insert(apps, { name = k }) + end + self.apps:setValues(apps) + + self:updateChoices() + + UI.Tab.enable(self) +end + +function tab:eventHandler(event) + if event.type == 'grid_focus_row' and event.element == self.apps then + self:updateChoices() + + elseif event.type == 'grid_select' and event.element == self.choices then + local app = self.apps:getSelected().name + Array.removeByValue(self.config[app], event.selected.file) + table.insert(self.config[app], 1, event.selected.file) + self:updateChoices() + Config.update('alternate', self.config) + + else + return UI.Tab.eventHandler(self, event) + end + return true +end + +return tab diff --git a/sys/apps/system/cloud.lua b/sys/apps/system/cloud.lua new file mode 100644 index 0000000..221c828 --- /dev/null +++ b/sys/apps/system/cloud.lua @@ -0,0 +1,57 @@ +local Ansi = require('opus.ansi') +local Config = require('opus.config') +local UI = require('opus.ui') + +local colors = _G.colors + +-- -t80x30 + +if _G.http.websocket then + local config = Config.load('cloud') + + local tab = UI.Tab { + tabTitle = 'Cloud', + description = 'Cloud Catcher options', + key = UI.TextEntry { + x = 3, ex = -3, y = 2, + limit = 32, + value = config.key, + shadowText = 'Cloud key', + accelerators = { + enter = 'update_key', + }, + }, + button = UI.Button { + x = 3, y = 4, + text = 'Update', + event = 'update_key', + }, + labelText = UI.TextArea { + x = 3, ex = -3, y = 6, + textColor = colors.yellow, + marginLeft = 0, marginRight = 0, + value = string.format( +[[Use a non-changing cloud key. Note that only a single computer can use this session at one time. +To obtain a key, visit: +%shttps://cloud-catcher.squiddev.cc%s then bookmark: +%shttps://cloud-catcher.squiddev.cc/?id=KEY + ]], + Ansi.white, Ansi.reset, Ansi.white), + }, + } + + function tab:eventHandler(event) + if event.type == 'update_key' then + if #self.key.value > 0 then + config.key = self.key.value + else + config.key = nil + end + Config.update('cloud', config) + self:emit({ type = 'success_message', message = 'Updated' }) + end + end + + return tab +end + diff --git a/sys/apps/system/diskusage.lua b/sys/apps/system/diskusage.lua new file mode 100644 index 0000000..98b8516 --- /dev/null +++ b/sys/apps/system/diskusage.lua @@ -0,0 +1,152 @@ +local UI = require('opus.ui') +local Event = require('opus.event') +local NFT = require('opus.nft') + +local colors = _G.colors +local fs = _G.fs +local os = _G.os +local peripheral = _G.peripheral + +local NftImages = { + blank = '\0308\0317\153\153\153\153\153\153\153\153\010\0307\0318\153\153\153\153\153\153\153\153\010\0308\0317\153\153\153\153\153\153\153\153\010\0307\0318\153\153\153\153\153\153\153\153\010\0308\0317\153\153\153\153\153\153\153\153', + drive = '\030 \031 \030b\031b\128\0308\0318\128\128\030f\149\030b\149\031 \139\010\030 \031 \030b\031b\128\128\128\128\128\128\010\030 \031 \030b\031b\128\0300\0317____\030b\031b\128\010\030 \031 \030b\031b\128\0300\0317____\030b\031b\128', + ram = '\030 \031 \128\0318\144\144\144\144\144\031 \128\010\0308\031 \157\0307\0317\128\128\128\128\128\030 \0318\145\010\030 \0318\136\0307\0317\128\0307\0310RAM\0307\128\030 \0318\132\010\0308\031 \157\0307\0317\128\128\128\128\128\030 \0318\145\010\030 \031 \128\0318\129\129\129\129\129\031 \128', + rom = '\030 \031 \128\0318\144\144\144\144\144\031 \128\010\0308\031 \157\0307\0317\128\128\128\128\128\030 \0318\145\010\030 \0318\136\0307\0317\128\0307\0310ROM\0307\128\030 \0318\132\010\0308\031 \157\0307\0317\128\128\128\128\128\030 \0318\145\010\030 \031 \128\0318\129\129\129\129\129\031 \128', + hdd = '\030 \031 \0307\0317\128\0300\135\131\139\0307\128\010\030 \031 \0300\0317\149\0310\128\0307\131\0300\128\0307\149\010\030 \031 \0307\0310\130\0300\0317\144\0308\0310\133\0307\159\129\010\030 \031 \0308\0317\149\129\142\159\0307\128\010\030 \031 \030 \0317\143\143\143\143\143', +} + +local tab = UI.Tab { + tabTitle = 'Disks Usage', + description = 'Visualise HDD and disks usage', + + drives = UI.ScrollingGrid { + x = 2, y = 1, + ex = '47%', ey = -7, + columns = { + { heading = 'Drive', key = 'name' }, + { heading = 'Side' ,key = 'side', textColor = colors.yellow } + }, + sortColumn = 'name', + }, + infos = UI.Grid { + x = '52%', y = 2, + ex = -2, ey = -4, + disableHeader = true, + unfocusedBackgroundSelectedColor = colors.black, + inactive = true, + backgroundSelectedColor = colors.black, + columns = { + { key = 'name' }, + { key = 'value', align = 'right', textColor = colors.yellow }, + } + }, + + progress = UI.ProgressBar { + x = 11, y = -2, + ex = -2, + }, + percentage = UI.Text { + x = 11, y = -3, + ex = '47%', + align = 'center', + }, + icon = UI.NftImage { + x = 2, y = -5, + image = NFT.parse(NftImages.blank) + }, +} + +local function getDrives() + local unique = { ['hdd'] = true, ['virt'] = true } + local drives = { { name = 'hdd', side = '' } } + + for _, drive in pairs(fs.list('/')) do + local side = fs.getDrive(drive) + if side and not unique[side] then + unique[side] = true + table.insert(drives, { name = drive, side = side }) + end + end + return drives +end + +local function getDriveInfo(p) + local files, dirs, total = 0, 0, 0 + + if p == "hdd" then p = "/" end + p = fs.combine(p, '') + local drive = fs.getDrive(p) + + local function recurse(path) + if fs.getDrive(path) == drive then + if fs.isDir(path) then + if path ~= p then + total = total + 500 + dirs = dirs + 1 + end + for _, v in pairs(fs.list(path)) do + recurse(fs.combine(path, v)) + end + else + local sz = fs.getSize(path) + files = files + 1 + if drive == 'rom' then + total = total + sz + else + total = total + math.max(500, sz) + end + end + end + end + + recurse(p) + + local info = {} + table.insert(info, { name = 'Type', value = peripheral.getType(drive) or drive }) + table.insert(info, { name = 'Used', value = total }) + table.insert(info, { name = 'Total', value = total + fs.getFreeSpace(p) }) + table.insert(info, { name = 'Free', value = fs.getFreeSpace(p) }) + table.insert(info, { }) + table.insert(info, { name = 'Files', value = files }) + table.insert(info, { name = 'Dirs', value = dirs }) + return info, math.floor((total / (total + fs.getFreeSpace(p))) * 100) +end + +function tab:updateInfo() + local selected = self.drives:getSelected() + local info, percent = getDriveInfo(selected and selected.name or self.drives.values[1].name) + self.infos:setValues(info) + self.progress.value = percent + self.percentage.value = ('%#3d%%'):format(percent) + self.icon.image = NFT.parse(NftImages[info[1].value] or NftImages.blank) + self:draw() +end + +function tab:updateDrives() + local drives = getDrives() + self.drives:setValues(drives) +end + +function tab:enable() + self:updateDrives() + self:updateInfo() + UI.Tab.enable(self) +end + +function tab:eventHandler(event) + if event.type == 'grid_focus_row' then + self:updateInfo() + else + return UI.Tab.eventHandler(self, event) + end + return true +end + +Event.on({ 'disk', 'disk_eject' }, function() + os.sleep(1) + tab:updateDrives() + tab:updateInfo() + tab:sync() +end) + +return tab diff --git a/sys/apps/system/kiosk.lua b/sys/apps/system/kiosk.lua new file mode 100644 index 0000000..becee72 --- /dev/null +++ b/sys/apps/system/kiosk.lua @@ -0,0 +1,62 @@ +local UI = require('opus.ui') + +local colors = _G.colors +local peripheral = _G.peripheral +local settings = _G.settings + +local tab = UI.Tab { + tabTitle = 'Kiosk', + description = 'Kiosk options', + form = UI.Form { + x = 2, ex = -2, + manualControls = true, + monitor = UI.Chooser { + formLabel = 'Monitor', formKey = 'monitor', + }, + textScale = UI.Chooser { + formLabel = 'Font Size', formKey = 'textScale', + nochoice = 'Small', + choices = { + { name = 'Small', value = '.5' }, + { name = 'Large', value = '1' }, + }, + help = 'Adjust text scaling', + }, + labelText = UI.TextArea { + x = 2, ex = -2, y = 5, + textColor = colors.yellow, + value = 'Settings apply to kiosk mode selected during startup' + }, + }, +} + +function tab:enable() + local choices = { } + + peripheral.find('monitor', function(side) + table.insert(choices, { name = side, value = side }) + end) + + self.form.monitor.choices = choices + self.form.monitor.value = settings.get('kiosk.monitor') + + self.form.textScale.value = settings.get('kiosk.textscale') + + UI.Tab.enable(self) +end + +function tab:eventHandler(event) + if event.type == 'choice_change' then + if self.form.monitor.value then + settings.set('kiosk.monitor', self.form.monitor.value) + end + if self.form.textScale.value then + settings.set('kiosk.textscale', self.form.textScale.value) + end + settings.save('.settings') + end +end + +if peripheral.find('monitor') then + return tab +end diff --git a/sys/apps/system/label.lua b/sys/apps/system/label.lua new file mode 100644 index 0000000..e4a73f2 --- /dev/null +++ b/sys/apps/system/label.lua @@ -0,0 +1,49 @@ +local UI = require('opus.ui') +local Util = require('opus.util') + +local fs = _G.fs +local os = _G.os + +local labelTab = UI.Tab { + tabTitle = 'Label', + description = 'Set the computer label', + labelText = UI.Text { + x = 3, y = 2, + value = 'Label' + }, + label = UI.TextEntry { + x = 9, y = 2, ex = -4, + limit = 32, + value = os.getComputerLabel(), + accelerators = { + enter = 'update_label', + }, + }, + grid = UI.ScrollingGrid { + y = 3, + values = { + { name = '', value = '' }, + { name = 'CC version', value = Util.getVersion() }, + { name = 'Lua version', value = _VERSION }, + { name = 'MC version', value = Util.getMinecraftVersion() }, + { name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) }, + { name = 'Computer ID', value = tostring(os.getComputerID()) }, + { name = 'Day', value = tostring(os.day()) }, + }, + inactive = true, + columns = { + { key = 'name', width = 12 }, + { key = 'value' }, + }, + }, +} + +function labelTab:eventHandler(event) + if event.type == 'update_label' then + os.setComputerLabel(self.label.value) + self:emit({ type = 'success_message', message = 'Label updated' }) + return true + end +end + +return labelTab diff --git a/sys/apps/system/launcher.lua b/sys/apps/system/launcher.lua new file mode 100644 index 0000000..6bd290c --- /dev/null +++ b/sys/apps/system/launcher.lua @@ -0,0 +1,84 @@ +local Config = require('opus.config') +local UI = require('opus.ui') + +local colors = _G.colors +local fs = _G.fs + +local config = Config.load('multishell') + +local tab = UI.Tab { + tabTitle = 'Launcher', + description = 'Set the application launcher', + launcherLabel = UI.Text { + x = 3, y = 2, + value = 'Launcher', + }, + launcher = UI.Chooser { + x = 13, y = 2, width = 12, + choices = { + { name = 'Overview', value = 'sys/apps/Overview.lua' }, + { name = 'Shell', value = 'sys/apps/ShellLauncher.lua' }, + { name = 'Custom', value = 'custom' }, + }, + }, + custom = UI.TextEntry { + x = 13, ex = -3, y = 3, + limit = 128, + shadowText = 'File name', + }, + button = UI.Button { + x = 3, y = 5, + text = 'Update', + event = 'update', + }, + labelText = UI.TextArea { + x = 3, ex = -3, y = 7, + textColor = colors.yellow, + value = 'Choose an application launcher', + }, +} + +function tab:enable() + local launcher = config.launcher and 'custom' or 'sys/apps/Overview.lua' + + for _, v in pairs(self.launcher.choices) do + if v.value == config.launcher then + launcher = v.value + break + end + end + + UI.Tab.enable(self) + + self.launcher.value = launcher + self.custom.enabled = launcher == 'custom' +end + +function tab:eventHandler(event) + if event.type == 'choice_change' then + self.custom.enabled = event.value == 'custom' + if self.custom.enabled then + self.custom.value = config.launcher + end + self:draw() + + elseif event.type == 'update' then + local launcher + + if self.launcher.value ~= 'custom' then + launcher = self.launcher.value + elseif fs.exists(self.custom.value) and not fs.isDir(self.custom.value) then + launcher = self.custom.value + end + + if launcher then + config.launcher = launcher + Config.update('multishell', config) + self:emit({ type = 'success_message', message = 'Updated' }) + else + self:emit({ type = 'error_message', message = 'Invalid file' }) + end + end +end + +return tab diff --git a/sys/apps/system/network.lua b/sys/apps/system/network.lua new file mode 100644 index 0000000..414c080 --- /dev/null +++ b/sys/apps/system/network.lua @@ -0,0 +1,60 @@ +local Ansi = require('opus.ansi') +local Config = require('opus.config') +local UI = require('opus.ui') + +local device = _G.device + +local tab = UI.Tab { + tabTitle = 'Network', + description = 'Networking options', + info = UI.TextArea { + x = 3, y = 4, + value = string.format( +[[%sSet the primary modem used for wireless communications.%s + +Reboot to take effect.]], Ansi.yellow, Ansi.reset) + }, + label = UI.Text { + x = 3, y = 2, + value = 'Modem', + }, + modem = UI.Chooser { + x = 10, ex = -3, y = 2, + nochoice = 'auto', + }, +} + +function tab:enable() + local width = 7 + local choices = { + { name = 'auto', value = 'auto' }, + { name = 'disable', value = 'none' }, + } + + for k,v in pairs(device) do + if v.isWireless and v.isWireless() and k ~= 'wireless_modem' then + table.insert(choices, { name = k, value = v.name }) + width = math.max(width, #k) + end + end + + self.modem.choices = choices + --self.modem.width = width + 4 + + local config = Config.load('os') + self.modem.value = config.wirelessModem or 'auto' + + UI.Tab.enable(self) +end + +function tab:eventHandler(event) + if event.type == 'choice_change' then + local config = Config.load('os') + config.wirelessModem = self.modem.value + Config.update('os', config) + self:emit({ type = 'success_message', message = 'reboot to take effect' }) + return true + end +end + +return tab diff --git a/sys/apps/system/password.lua b/sys/apps/system/password.lua new file mode 100644 index 0000000..d822996 --- /dev/null +++ b/sys/apps/system/password.lua @@ -0,0 +1,44 @@ +local Security = require('opus.security') +local SHA = require('opus.crypto.sha2') +local UI = require('opus.ui') + +local colors = _G.colors + +local passwordTab = UI.Tab { + tabTitle = 'Password', + description = 'Wireless network password', + newPass = UI.TextEntry { + x = 3, ex = -3, y = 3, + limit = 32, + mask = true, + shadowText = 'new password', + accelerators = { + enter = 'new_password', + }, + }, + button = UI.Button { + x = 3, y = 5, + text = 'Update', + event = 'update_password', + }, + info = UI.TextArea { + x = 3, ex = -3, y = 7, + textColor = colors.yellow, + inactive = true, + value = 'Add a password to enable other computers to connect to this one.', + } +} +function passwordTab:eventHandler(event) + if event.type == 'update_password' then + if not self.newPass.value or #self.newPass.value == 0 then + self:emit({ type = 'error_message', message = 'Invalid password' }) + + else + Security.updatePassword(SHA.compute(self.newPass.value)) + self:emit({ type = 'success_message', message = 'Password updated' }) + end + return true + end +end + +return passwordTab diff --git a/sys/apps/system/path.lua b/sys/apps/system/path.lua new file mode 100644 index 0000000..bf30f7e --- /dev/null +++ b/sys/apps/system/path.lua @@ -0,0 +1,103 @@ +local Config = require('opus.config') +local UI = require('opus.ui') +local Util = require('opus.util') + +local tab = UI.Tab { + tabTitle = 'Path', + description = 'Set the shell path', + tabClose = true, + entry = UI.TextEntry { + x = 2, y = 2, ex = -2, + limit = 256, + shadowText = 'enter new path', + accelerators = { + enter = 'update_path', + }, + help = 'add a new path', + }, + grid = UI.Grid { + y = 4, ey = -3, + disableHeader = true, + columns = { { key = 'value' } }, + autospace = true, + sortColumn = 'index', + help = 'double-click to remove, shift-arrow to move', + accelerators = { + delete = 'remove', + }, + }, + statusBar = UI.StatusBar { }, + accelerators = { + [ 'shift-up' ] = 'move_up', + [ 'shift-down' ] = 'move_down', + }, +} + +function tab:updateList(path) + self.grid.values = { } + for k,v in ipairs(Util.split(path, '(.-):')) do + table.insert(self.grid.values, { index = k, value = v }) + end + self.grid:update() +end + +function tab:enable() + local env = Config.load('shell') + self:updateList(env.path) + UI.Tab.enable(self) +end + +function tab:save() + local t = { } + for _, v in ipairs(self.grid.values) do + table.insert(t, v.value) + end + local env = Config.load('shell') + env.path = table.concat(t, ':') + self:updateList(env.path) + Config.update('shell', env) +end + +function tab:eventHandler(event) + if event.type == 'update_path' then + table.insert(self.grid.values, { + value = self.entry.value, + }) + self:save() + self.entry:reset() + self.entry:draw() + self.grid:draw() + return true + + elseif event.type == 'grid_select' or event.type == 'remove' then + local selected = self.grid:getSelected() + if selected then + table.remove(self.grid.values, selected.index) + self:save() + self.grid:draw() + end + + elseif event.type == 'focus_change' then + self.statusBar:setStatus(event.focused.help) + + elseif event.type == 'move_up' then + local entry = self.grid:getSelected() + if entry.index > 1 then + table.insert(self.grid.values, entry.index - 1, table.remove(self.grid.values, entry.index)) + self.grid:setIndex(entry.index - 1) + self:save() + self.grid:draw() + end + + elseif event.type == 'move_down' then + local entry = self.grid:getSelected() + if entry.index < #self.grid.values then + table.insert(self.grid.values, entry.index + 1, table.remove(self.grid.values, entry.index)) + self.grid:setIndex(entry.index + 1) + self:save() + self.grid:draw() + end + end +end + +return tab diff --git a/sys/apps/system/requires.lua b/sys/apps/system/requires.lua new file mode 100644 index 0000000..1e41216 --- /dev/null +++ b/sys/apps/system/requires.lua @@ -0,0 +1,104 @@ +local Config = require('opus.config') +local UI = require('opus.ui') +local Util = require('opus.util') + +local tab = UI.Tab { + tabTitle = 'Requires', + description = 'Require path', + tabClose = true, + entry = UI.TextEntry { + x = 2, y = 2, ex = -2, + limit = 256, + shadowText = 'Enter new require path', + accelerators = { + enter = 'update_path', + }, + help = 'add a new path (reboot required)', + }, + grid = UI.Grid { + y = 4, ey = -3, + disableHeader = true, + columns = { { key = 'value' } }, + autospace = true, + sortColumn = 'index', + help = 'double-click to remove, shift-arrow to move', + accelerators = { + delete = 'remove', + }, + }, + statusBar = UI.StatusBar { }, + accelerators = { + [ 'shift-up' ] = 'move_up', + [ 'shift-down' ] = 'move_down', + }, +} + +function tab:updateList(lua_path) + self.grid.values = { } + for k,v in ipairs(Util.split(lua_path, '(.-);')) do + table.insert(self.grid.values, { index = k, value = v }) + end + self.grid:update() +end + +function tab:enable() + local env = Config.load('shell') + self:updateList(env.lua_path) + UI.Tab.enable(self) +end + +function tab:save() + local t = { } + for _, v in ipairs(self.grid.values) do + table.insert(t, v.value) + end + local env = Config.load('shell') + env.lua_path = table.concat(t, ';') + self:updateList(env.lua_path) + Config.update('shell', env) +end + +function tab:eventHandler(event) + if event.type == 'update_path' then + table.insert(self.grid.values, { + value = self.entry.value, + }) + self:save() + self.entry:reset() + self.entry:draw() + self.grid:draw() + return true + + elseif event.type == 'grid_select' or event.type == 'remove' then + local selected = self.grid:getSelected() + if selected then + table.remove(self.grid.values, selected.index) + self:save() + self.grid:draw() + end + + elseif event.type == 'focus_change' then + self.statusBar:setStatus(event.focused.help) + + elseif event.type == 'move_up' then + local entry = self.grid:getSelected() + if entry.index > 1 then + table.insert(self.grid.values, entry.index - 1, table.remove(self.grid.values, entry.index)) + self.grid:setIndex(entry.index - 1) + self:save() + self.grid:draw() + end + + elseif event.type == 'move_down' then + local entry = self.grid:getSelected() + if entry.index < #self.grid.values then + table.insert(self.grid.values, entry.index + 1, table.remove(self.grid.values, entry.index)) + self.grid:setIndex(entry.index + 1) + self:save() + self.grid:draw() + end + end +end + +--this needs rework - see 4.user.lua +--return tab diff --git a/sys/apps/system/settings.lua b/sys/apps/system/settings.lua new file mode 100644 index 0000000..8e071ac --- /dev/null +++ b/sys/apps/system/settings.lua @@ -0,0 +1,49 @@ +local UI = require('opus.ui') + +local settings = _G.settings + +if settings then + local settingsTab = UI.Tab { + tabTitle = 'Settings', + description = 'Computercraft configurable settings', + grid = UI.Grid { + y = 2, + autospace = true, + sortColumn = 'name', + columns = { + { heading = 'Setting', key = 'name' }, + { heading = 'Value', key = 'value' }, + }, + }, + } + + function settingsTab:enable() + local values = { } + for _,v in pairs(settings.getNames()) do + local value = settings.get(v) + if not value then + value = false + end + table.insert(values, { + name = v, + value = value, + }) + end + self.grid:setValues(values) + UI.Tab.enable(self) + end + + function settingsTab:eventHandler(event) + if event.type == 'grid_select' then + if not event.selected.value or type(event.selected.value) == 'boolean' then + event.selected.value = not event.selected.value + end + settings.set(event.selected.name, event.selected.value) + settings.save('.settings') + self.grid:draw() + return true + end + end + + return settingsTab +end diff --git a/sys/apps/system/shell.lua b/sys/apps/system/shell.lua new file mode 100644 index 0000000..13dfda6 --- /dev/null +++ b/sys/apps/system/shell.lua @@ -0,0 +1,142 @@ +local Config = require('opus.config') +local UI = require('opus.ui') +local Util = require('opus.util') + +local colors = _G.colors +local os = _G.os + +local config = Config.load('shellprompt') + +local allColors = { } +for k,v in pairs(colors) do + if type(v) == 'number' then + table.insert(allColors, { name = k, value = v }) + end +end + +local defaults = { + textColor = colors.white, + commandTextColor = colors.yellow, + directoryTextColor = colors.orange, + directoryBackgroundColor = colors.black, + promptTextColor = colors.blue, + promptBackgroundColor = colors.black, + directoryColor = colors.green, + fileColor = colors.white, + backgroundColor = colors.black, +} +local _colors = config.color or Util.shallowCopy(defaults) + +local allSettings = { } +for k, v in pairs(defaults) do + table.insert(allSettings, { name = k }) +end + +-- temp +if not _colors.backgroundColor then + _colors.backgroundColor = colors.black + _colors.fileColor = colors.white +end + +local tab = UI.Tab { + tabTitle = 'Shell', + description = 'Shell options', + grid1 = UI.ScrollingGrid { + y = 2, ey = -10, x = 3, ex = -16, + disableHeader = true, + columns = { { key = 'name' } }, + values = allSettings, + sortColumn = 'name', + }, + grid2 = UI.ScrollingGrid { + y = 2, ey = -10, x = -14, ex = -3, + disableHeader = true, + columns = { { key = 'name' } }, + values = allColors, + sortColumn = 'name', + }, + directoryLabel = UI.Text { + x = 2, y = -2, + value = 'Display directory', + }, + directory = UI.Checkbox { + x = 20, y = -2, + value = config.displayDirectory + }, + reset = UI.Button { + x = -18, y = -2, + text = 'Reset', + event = 'reset', + }, + button = UI.Button { + x = -9, y = -2, + text = 'Update', + event = 'update', + }, + display = UI.Window { + x = 3, ex = -3, y = -8, height = 5, + }, +} + +function tab.grid2:getRowTextColor(row) + local selected = tab.grid1:getSelected() + if _colors[selected.name] == row.value then + return colors.yellow + end + return UI.Grid.getRowTextColor(self, row) +end + +function tab.display:draw() + self:clear(_colors.backgroundColor) + local offset = 0 + if config.displayDirectory then + self:write(1, 1, + '==' .. os.getComputerLabel() .. ':/dir/etc', + _colors.directoryBackgroundColor, _colors.directoryTextColor) + offset = 1 + end + + self:write(1, 1 + offset, '$ ', + _colors.promptBackgroundColor, _colors.promptTextColor) + + self:write(3, 1 + offset, 'ls /', + _colors.backgroundColor, _colors.commandTextColor) + + self:write(1, 2 + offset, 'sys usr', + _colors.backgroundColor, _colors.directoryColor) + + self:write(1, 3 + offset, 'startup', + _colors.backgroundColor, _colors.fileColor) +end + +function tab:eventHandler(event) + if event.type =='checkbox_change' then + config.displayDirectory = not not event.checked + self.display:draw() + + elseif event.type == 'grid_focus_row' and event.element == self.grid1 then + self.grid2:draw() + + elseif event.type == 'grid_select' and event.element == self.grid2 then + _colors[tab.grid1:getSelected().name] = event.selected.value + self.display:draw() + self.grid2:draw() + + elseif event.type == 'reset' then + config.color = defaults + config.displayDirectory = true + self.directory.value = true + _colors = Util.shallowCopy(defaults) + + Config.update('shellprompt', config) + self:draw() + + elseif event.type == 'update' then + config.color = _colors + Config.update('shellprompt', config) + + end + return UI.Tab.eventHandler(self, event) +end + +return tab diff --git a/sys/apps/telnet.lua b/sys/apps/telnet.lua index 25c6c05..ab9f024 100644 --- a/sys/apps/telnet.lua +++ b/sys/apps/telnet.lua @@ -1,16 +1,15 @@ -_G.requireInjector(_ENV) - -local Event = require('event') -local Socket = require('socket') -local Terminal = require('terminal') -local Util = require('util') +local Event = require('opus.event') +local Socket = require('opus.socket') +local Terminal = require('opus.terminal') +local Util = require('opus.util') local multishell = _ENV.multishell local os = _G.os local read = _G.read +local shell = _ENV.shell local term = _G.term -local args = { ... } +local args, options = Util.parse(...) local remoteId = tonumber(table.remove(args, 1) or '') if not remoteId then @@ -23,13 +22,25 @@ if not remoteId then end if multishell then - multishell.setTitle(multishell.getCurrent(), 'Telnet ' .. remoteId) + multishell.setTitle(multishell.getCurrent(), + (options.s and 'Secure ' or 'Telnet ') .. remoteId) end -local socket, msg = Socket.connect(remoteId, 23) +local socket, msg, reason -if not socket then - error(msg) +while true do + socket, msg, reason = Socket.connect(remoteId, options.s and 22 or 23) + + if socket then + break + elseif reason ~= 'NOTRUST' then + error(msg) + end + + local s, m = shell.run('trust ' .. remoteId) + if not s then + error(m) + end end local ct = Util.shallowCopy(term.current()) diff --git a/sys/apps/trust.lua b/sys/apps/trust.lua index b8cb705..9d036c9 100644 --- a/sys/apps/trust.lua +++ b/sys/apps/trust.lua @@ -1,10 +1,8 @@ -_G.requireInjector(_ENV) - -local Crypto = require('crypto') -local Security = require('security') -local SHA1 = require('sha1') -local Socket = require('socket') -local Terminal = require('terminal') +local Crypto = require('opus.crypto.chacha20') +local Security = require('opus.security') +local SHA = require('opus.crypto.sha2') +local Socket = require('opus.socket') +local Terminal = require('opus.terminal') local os = _G.os @@ -29,15 +27,16 @@ if not password then end print('connecting...') -local socket, msg = Socket.connect(remoteId, 19) +local trustId = '01c3ba27fe01383a03a1785276d99df27c3edcef68fbf231ca' +local socket, msg = Socket.connect(remoteId, 19, { identifier = trustId }) if not socket then error(msg) end -local publicKey = Security.getPublicKey() +local identifier = Security.getIdentifier() -socket:write(Crypto.encrypt({ pk = publicKey, dh = os.getComputerID() }, SHA1.sha1(password))) +socket:write(Crypto.encrypt({ pk = identifier, dh = os.getComputerID() }, SHA.compute(password))) local data = socket:read(2) socket:close() diff --git a/sys/apps/vnc.lua b/sys/apps/vnc.lua index 5b17700..c9d865c 100644 --- a/sys/apps/vnc.lua +++ b/sys/apps/vnc.lua @@ -1,17 +1,16 @@ -_G.requireInjector(_ENV) - -local Event = require('event') -local Socket = require('socket') -local Terminal = require('terminal') -local Util = require('util') +local Event = require('opus.event') +local Socket = require('opus.socket') +local Terminal = require('opus.terminal') +local Util = require('opus.util') local colors = _G.colors local multishell = _ENV.multishell local os = _G.os +local shell = _ENV.shell local term = _G.term local remoteId -local args = { ... } +local args, options = Util.parse(...) if #args == 1 then remoteId = tonumber(args[1]) else @@ -24,11 +23,20 @@ if not remoteId then end if multishell then - multishell.setTitle(multishell.getCurrent(), 'VNC-' .. remoteId) + multishell.setTitle(multishell.getCurrent(), + (options.s and 'SVNC-' or 'VNC-') .. remoteId) end local function connect() - local socket, msg = Socket.connect(remoteId, 5900) + local socket, msg, reason = Socket.connect(remoteId, options.s and 5901 or 5900) + + if reason == 'NOTRUST' then + local s, m = shell.run('trust ' .. remoteId) + if not s then + return s, m + end + socket, msg = Socket.connect(remoteId, 5900) + end if not socket then return false, msg @@ -62,7 +70,7 @@ local function connect() break end for _,v in ipairs(data) do - ct[v.f](unpack(v.args)) + ct[v.f](table.unpack(v.args)) end end end) diff --git a/sys/autorun/clipboard.lua b/sys/autorun/clipboard.lua index 2dd8017..992af47 100644 --- a/sys/autorun/clipboard.lua +++ b/sys/autorun/clipboard.lua @@ -1,25 +1,22 @@ -_G.requireInjector(_ENV) - -local Util = require('util') +local Util = require('opus.util') local kernel = _G.kernel local keyboard = _G.device.keyboard local os = _G.os local textutils = _G.textutils -local data - kernel.hook('clipboard_copy', function(_, args) - data = args[1] + keyboard.clipboard = args[1] end) keyboard.addHotkey('shift-paste', function() + local data = keyboard.clipboard + if type(data) == 'table' then local s, m = pcall(textutils.serialize, data) - data = (s and m) or Util.tostring(data) + data = s and m or Util.tostring(data) end - -- replace the event paste data with our internal data - -- args[1] = Util.tostring(data or '') + if data then os.queueEvent('paste', data) end diff --git a/sys/autorun/complete.lua b/sys/autorun/complete.lua new file mode 100644 index 0000000..31a6bd9 --- /dev/null +++ b/sys/autorun/complete.lua @@ -0,0 +1,22 @@ +local function completeMultipleChoice(sText, tOptions, bAddSpaces) + local tResults = { } + for n = 1,#tOptions do + local sOption = tOptions[n] + if #sOption + (bAddSpaces and 1 or 0) > #sText and string.sub(sOption, 1, #sText) == sText then + local sResult = string.sub(sOption, #sText + 1) + if bAddSpaces then + table.insert(tResults, sResult .. " ") + else + table.insert(tResults, sResult) + end + end + end + return tResults +end + +_ENV.shell.setCompletionFunction("sys/apps/package.lua", + function(_, index, text) + if index == 1 then + return completeMultipleChoice(text, { "install ", "update ", "uninstall ", "updateall ", "refresh" }) + end + end) diff --git a/sys/autorun/gps.lua b/sys/autorun/gps.lua deleted file mode 100644 index 8a51fea..0000000 --- a/sys/autorun/gps.lua +++ /dev/null @@ -1,43 +0,0 @@ -local modem = _G.device.wireless_modem -local turtle = _G.turtle - -if turtle and modem then - local s, m = turtle.run(function() - - _G.requireInjector(_ENV) - - local Config = require('config') - local config = { - destructive = false, - } - Config.load('gps', config) - - if config.home then - - local s = turtle.enableGPS(2) - if not s then - s = turtle.enableGPS(2) - end - if not s and config.destructive then - turtle.setPolicy('turtleSafe') - s = turtle.enableGPS(2) - end - - if not s then - error('Unable to get GPS position') - end - - if config.destructive then - turtle.setPolicy('turtleSafe') - end - - if not turtle.pathfind(config.home) then - error('Failed to return home') - end - end - end) - - if not s and m then - error(m) - end -end diff --git a/sys/autorun/gpshost.lua b/sys/autorun/gpshost.lua deleted file mode 100644 index 188d009..0000000 --- a/sys/autorun/gpshost.lua +++ /dev/null @@ -1,19 +0,0 @@ -if _G.device.wireless_modem then - - _G.requireInjector(_ENV) - local Config = require('config') - - local kernel = _G.kernel - - local config = { } - Config.load('gps', config) - - if config.host and type(config.host) == 'table' then - kernel.run({ - title = 'GPS Daemon', - hidden = true, - path = '/rom/programs/gps', - args = { 'host', config.host.x, config.host.y, config.host.z }, - }) - end -end diff --git a/sys/autorun/hotkeys.lua b/sys/autorun/hotkeys.lua index 9c9dfce..cbd93bf 100644 --- a/sys/autorun/hotkeys.lua +++ b/sys/autorun/hotkeys.lua @@ -1,11 +1,13 @@ -_G.requireInjector(_ENV) - -local Util = require('util') +local Util = require('opus.util') local kernel = _G.kernel local keyboard = _G.device.keyboard local multishell = _ENV.multishell +if not multishell or not multishell.getTabs then + return +end + -- overview keyboard.addHotkey('control-o', function() for _,tab in pairs(multishell.getTabs()) do @@ -21,10 +23,12 @@ keyboard.addHotkey('control-backspace', function() local tab = kernel.find(uid) if not tab.isOverview then multishell.terminate(uid) - tab = Util.shallowCopy(tab) - tab.isDead = false - tab.focused = true - multishell.openTab(tab) + multishell.openTab({ + path = tab.path, + env = tab.env, + args = tab.args, + focused = true, + }) end end) @@ -38,7 +42,7 @@ keyboard.addHotkey('control-tab', function() return a.uid < b.uid end for _,tab in Util.spairs(tabs, compareTab) do - if not tab.hidden then + if not tab.hidden and not tab.noFocus then table.insert(visibleTabs, tab) end end diff --git a/sys/autorun/log.lua b/sys/autorun/log.lua index 224d6e9..91bb193 100644 --- a/sys/autorun/log.lua +++ b/sys/autorun/log.lua @@ -1,5 +1,3 @@ -_G.requireInjector(_ENV) - --[[ Adds a task and the control-d hotkey to view the kernel log. --]] @@ -13,23 +11,27 @@ local term = _G.term local function systemLog() local routine = kernel.getCurrent() - local w, h = kernel.window.getSize() - kernel.window.reposition(1, 2, w, h - 1) + if multishell and multishell.openTab then + local w, h = kernel.window.getSize() + kernel.window.reposition(1, 2, w, h - 1) - routine.terminal = kernel.window - routine.window = kernel.window - term.redirect(kernel.window) + routine.terminal = kernel.window + routine.window = kernel.window + term.redirect(kernel.window) + end kernel.hook('mouse_scroll', function(_, eventData) local dir, y = eventData[1], eventData[3] if y > 1 then local currentTab = kernel.getFocused() - if currentTab.terminal.scrollUp and not currentTab.terminal.noAutoScroll then - if dir == -1 then - currentTab.terminal.scrollUp() - else - currentTab.terminal.scrollDown() + if currentTab == routine then + if currentTab.terminal.scrollUp and not currentTab.terminal.noAutoScroll then + if dir == -1 then + currentTab.terminal.scrollUp() + else + currentTab.terminal.scrollDown() + end end end end @@ -48,8 +50,16 @@ local function systemLog() keyboard.removeHotkey('control-d') end -multishell.openTab({ - title = 'System Log', - fn = systemLog, - hidden = true, -}) +if multishell and multishell.openTab then + multishell.openTab({ + title = 'System Log', + fn = systemLog, + noTerminate = true, + hidden = true, + }) +else + kernel.run({ + title = 'Syslog', + fn = systemLog, + }) +end diff --git a/sys/autorun/upgraded.lua b/sys/autorun/upgraded.lua new file mode 100644 index 0000000..5922c3a --- /dev/null +++ b/sys/autorun/upgraded.lua @@ -0,0 +1,20 @@ +local fs = _G.fs + +local function deleteIfExists(path) + if fs.exists(path) then + fs.delete(path) + print("Deleted outdated file at: "..path) + end +end +-- cleanup outdated files +deleteIfExists('sys/apps/shell') +deleteIfExists('sys/etc/app.db') +deleteIfExists('sys/extensions') +deleteIfExists('sys/network') +deleteIfExists('startup') +deleteIfExists('sys/apps/system/turtle.lua') +deleteIfExists('sys/autorun/gps.lua') +deleteIfExists('sys/autorun/gpshost.lua') +deleteIfExists('sys/apps/network/redserver.lua') +deleteIfExists('sys/apis') +deleteIfExists('sys/autorun/apps.lua') diff --git a/sys/autorun/welcome.lua b/sys/autorun/welcome.lua new file mode 100644 index 0000000..b7fc3cc --- /dev/null +++ b/sys/autorun/welcome.lua @@ -0,0 +1,51 @@ +local Config = require('opus.config') +local Util = require('opus.util') + +local fs = _G.fs +local os = _G.os +local shell = _ENV.shell + +local config = Config.load('os') +if not config.welcomed then + config.welcomed = true + config.securityUpdate = true + config.readNotes = 1 + Config.update('os', config) + if shell.openForegroundTab then + shell.openForegroundTab('Welcome') + end +end + +if not config.securityUpdate then + config.securityUpdate = true + config.secretKey = nil + config.password = nil + config.readNotes = 1 + Config.update('os', config) + + fs.delete('usr/.known_hosts') + + Util.writeFile('sys/notes_1.txt', [[ +An important security update has been applied. + +Unfortunately, this update has reset the +password on the system. You can set a new +password in System->System->Password. + +All computers that you connect to will also +need to be updated as well. + +Also, I have changed the location for apis. +This will require you to update all installed +packages. Sorry ! + +Thanks for your patience. And... thanks to +Anavrins for the much improved security. +]]) +end + +if fs.exists('sys/notes_1.txt') and shell.openForegroundTab then + shell.openForegroundTab('edit sys/notes_1.txt') + os.sleep(2) + fs.delete('sys/notes_1.txt') +end diff --git a/sys/boot/kiosk.boot b/sys/boot/kiosk.boot new file mode 100644 index 0000000..61155fe --- /dev/null +++ b/sys/boot/kiosk.boot @@ -0,0 +1,40 @@ +local os = _G.os +local parallel = _G.parallel +local peripheral = _G.peripheral +local settings = _G.settings +local term = _G.term + +local name = settings.get('kiosk.monitor') + +if not name then + peripheral.find('monitor', function(s) + name = s + end) +end + +local mon = name and peripheral.wrap(name) + +if mon then + print("Opus OS is running in Kiosk mode, and the screen will be redirected to the monitor. To undo this, go to the boot option menu by pressing a key while booting, then select the option 2.") + term.redirect(mon) + mon.setTextScale(tonumber(settings.get('kiosk.textscale')) or 1) + + parallel.waitForAny( + function() + os.run(_ENV, '/sys/boot/opus.boot') + end, + + function() + while true do + local event, side, x, y = os.pullEventRaw('monitor_touch') + + if event == 'monitor_touch' and side == name then + os.queueEvent('mouse_click', 1, x, y) + os.queueEvent('mouse_up', 1, x, y) + end + end + end + ) +else + os.run(_ENV, '/sys/boot/opus.boot') +end diff --git a/sys/boot/opus.boot b/sys/boot/opus.boot index 990def6..2a02d14 100644 --- a/sys/boot/opus.boot +++ b/sys/boot/opus.boot @@ -1,61 +1,30 @@ --- Loads the Opus environment regardless if the file system is local or not -local fs = _G.fs -local http = _G.http - -_G.OPUS_BRANCH = 'master-1.8' -local GIT_REPO = 'kepler155c/opus/' .. _G.OPUS_BRANCH -local BASE = 'https://raw.githubusercontent.com/' .. GIT_REPO +local fs = _G.fs local sandboxEnv = setmetatable({ }, { __index = _G }) for k,v in pairs(_ENV) do sandboxEnv[k] = v end -_G._debug = function() end - -local function makeEnv() +local function run(file, ...) local env = setmetatable({ }, { __index = _G }) for k,v in pairs(sandboxEnv) do env[k] = v end - return env -end -local function run(file, ...) - local s, m = loadfile(file, makeEnv()) + local s, m = loadfile(file, env) if s then return s(...) end error('Error loading ' .. file .. '\n' .. m) end -local function runUrl(file, ...) - local url = BASE .. '/' .. file - - local u = http.get(url) - if u then - local fn = load(u.readAll(), url, nil, makeEnv()) - u.close() - if fn then - return fn(...) - end - end - error('Failed to download ' .. url) -end +_G._syslog = function() end +_G.OPUS_BRANCH = 'develop-1.8' -- Install require shim -if fs.exists('sys/apis/injector.lua') then - _G.requireInjector = run('sys/apis/injector.lua') -else - -- not local, run the file system directly from git - _G.requireInjector = runUrl('sys/apis/injector.lua') - runUrl('sys/extensions/2.vfs.lua') +_G.requireInjector = run('sys/modules/opus/injector.lua') - -- install file system - fs.mount('', 'gitfs', GIT_REPO) -end - -local s, m = pcall(run, 'sys/apps/shell', 'sys/kernel.lua', ...) +local s, m = pcall(run, 'sys/apps/shell.lua', 'sys/kernel.lua', ...) if not s then print('\nError loading Opus OS\n') diff --git a/sys/etc/app.db b/sys/etc/app.db deleted file mode 100644 index 353b20b..0000000 --- a/sys/etc/app.db +++ /dev/null @@ -1,211 +0,0 @@ -{ - [ "0a999012ffb87b3edac99adbdfc498b12831a1e2" ] = { - title = "Packages", - category = "System", - run = "PackageManager.lua", - iconExt = "\030c\0317\151\131\131\131\0307\031c\148\ -\030c\0317\151\131\0310\143\0317\131\0307\031c\148\ -\0307\031c\138\030f\0317\151\131\131\131", - }, - [ "53ebc572b4a44802ba114729f07bdaaf5409a9d7" ] = { - title = "Network", - category = "Apps", - icon = "\0304 \030 \ -\030f \0304 \0307 \030 \031 \031f)\ -\030f \0304 \0307 \030 \031f)", - iconExt = "\030 \031f \0305\031f\140\030f\0315\137\144\ -\030 \031f\030f\0314\131\131\0304\031f\148\030 \0305\155\150\149\ -\030 \031f\030f\0310\147\0300\031f\141\0304\149\0307\0318\149\030 ", - run = "Network.lua", - }, - c7116629a6a855cb774d9c7c8ad822fd83c71fb5 = { - title = "Reboot", - category = "System", - icon = "\0304\031f \030f\0310o..\0304\031f \ -\0304\031f \030f\0310.o.\0304\031f \ -\0304\031f - ", - iconExt = "\0307\031f\135\0300\0317\159\0307\0310\144\031f\139\ -\0300\0317\131\0307\0310\147\0300\0317\156\131\ -\130\143\143\129", - run = "rom/programs/reboot", - }, - fb91e24fa52d8d2b32937bf04d843f730319a902 = { - title = "Update", - category = "System", - icon = "\0301\03171\03180\030 \031 \ -\0301\03181\030 \031 \ -\0301\03170\03180\03171\0307\031f>", - iconExt = "\031f\128\0313\152\131\131\132\031f\128\ -\0313\139\159\129\0303\031f\159\129\139\ -\031f\128\0313\136\0303\031f\143\143\030f\0313\134\031f\128", - run = "http://pastebin.com/raw/UzGHLbNC", - }, - c47ae15370cfe1ed2781eedc1dc2547d12d9e972 = { - title = "Help", - category = "Apps", - icon = " \031f?\031 \ -\031f?\031 \ - \031f?", - iconExt = "\0300\031f\129\030f\0310\131\0300\031f\148\030f\0310\148\ -\030 \031 \0300\031f\131\030f\0310\142\129\ -\030 \031 \0300\031f\131\030f\128", - run = "Help.lua", - }, - b0832074630eb731d7fbe8074de48a90cd9bb220 = { - title = "Lua", - category = "Apps", - icon = "\030f \ -\030f\0310lua>\031 \ -\030f ", - iconExt = "\0300\031f\151\030f\128\0300\159\159\159\030f\0310\144\0304\031f\159\030f\128\ -\0300\031f\149\030f\128\0300\149\149\151\145\030f\128\0314\153\ -\130\131\130\131\130\131\0314\130\031f\128", - run = "sys/apps/Lua.lua", - }, - bc0792d8dc81e8aa30b987246a5ce97c40cd6833 = { - title = "System", - category = "System", - icon = " \0307\031f| \ -\0307\031f---o\030 \031 \ - \0307\031f| ", - iconExt = "\0318\138\0308\031f\130\0318\128\031f\129\030f\0318\133\ -\0318\143\0308\128\0317\143\0318\128\030f\143\ -\0318\138\135\143\139\133", - run = "System.lua", - }, - [ "2a4d562b1d9a9c90bdede6fac8ce4f7402462b86" ] = { - title = "Tasks", - category = "System", - icon = "\030f\031f \0315/\ -\030f\031f \0315/\\/ \ -\030f\0315/\031f ", - iconExt = "\031f\128\128\0305\159\030f\128\0305\159\030f\0315\134\031f\128\ -\031f\128\0315\152\129\137\0305\031f\158\139\030f\0317 \ -\0315\134\031f\128\128\128\128\0305\154\030f\0317 ", - run = "Tasks.lua", - }, - [ "a0365977708b7387ee9ce2c13e5820e6e11732cb" ] = { - title = "Pain", - category = "Apps", - icon = "\030 \031f\0307\031f\159\030 \159\030 \ -\030 \031f\0308\031f\135\0307\0318\144\140\030f\0317\159\143\031c\139\0302\135\030f\0312\157\ -\030 \031f\030f\0318\143\133\0312\136\0302\031f\159\159\143\131\030f\0312\132", - run = "http://pastebin.com/raw/wJQ7jav0", - }, - [ "48d6857f6b2869d031f463b13aa34df47e18c548" ] = { - title = "Breakout", - category = "Games", - icon = "\0301\031f \0309 \030c \030b \030e \030c \0306 \ -\030 \031f \ -\030 \031f \0300 \0310 ", - iconExt = "\030 \031f\030f\0319\144\030d\031f\159\030b\159\030f\0311\144\031b\144\030c\031f\159\030f\0311\144\ -\030 \031f\030f\0311\130\031b\129\0319\130\031e\130\0310\144\031d\129\0316\129\ -\030 \031f\030f\0310\136\140\140\030 ", - run = "https://gist.github.com/LDDestroier/c7528d95bc0103545c2a/raw", - }, - [ "53a5d150062b1e03206b9e15854b81060e3c7552" ] = { - title = "Minesweeper", - category = "Games", - icon = "\030f\031f \03131\0308\031f \030f\031d2\ -\030f\031f \031d2\03131\0308\031f \030f\03131\ -\030f\03131\0308\031f \030f\03131\031e3", - run = "https://pastebin.com/raw/nsKrHTbN", - }, - [ "01c933b2a36ad8ed2d54089cb2903039046c1216" ] = { - title = "Enchat", - icon = "\030e\031f\151\030f\031e\156\0311\140\0314\140\0315\140\031d\140\031b\140\031a\132\ -\030f\0314\128\030e\031f\132\030f\031e\132\0318nchat\ -\030f\031e\138\141\0311\140\0314\140\0315\132\0317v\03183\031a\132", - category = "Apps", - run = "https://raw.githubusercontent.com/LDDestroier/enchat/master/enchat3.lua", - }, - [ "6ce6c512ea433a7fc5c8841628e7696cd0ff7f2b" ] = { - title = "Files", - category = "Apps", - icon = "\0300\0317==\031 \0307 \ -\0300\0317====\ -\0300\0317====", - iconExt = "\030 \031f\0300\031f\136\140\132\0308\130\030f\0318\144\ -\030 \031f\030f\0310\157\0300\031f\147\030f\0310\142\143\149\ -\030 \031f\0300\031f\136\140\132\140\030f\0310\149", - run = "Files.lua", - }, - [ "7fddb7d8d1d60b1eeefa9af01082e0811d4b484d" ] = { - title = "Shutdown", - category = "System", - icon = "\0304\031f \ -\0304\031f \030f\0310zz\031 \ -\0304\031f \030f ", - iconExt = "\030e\031f\135\030f\031e\148\030e\128\031f\151\139\ -\030e\031e\128\030f\031f\128\031e\143\031f\128\030e\031e\128\ -\031e\139\030e\031f\130\131\129\030f\031e\135", - run = "/rom/programs/shutdown", - }, - bdc1fd5d3c0f3dcfd55d010426e61bf9451e680d = { - title = "Shell", - category = "Apps", - icon = "\0304 \030 \ -\0304 \030f\0314> \0310_\031 \ -\0304 \030f \030 ", - iconExt = "\030f\0314\151\131\131\131\131\ -\030f\0314\149\030f\0314> \0310_ \ -\030f\0314\149\030f ", - run = "shell", - }, - b77aad5fb24921ef76ac8f3e500ed93fddae8f2a = { - title = "Redirection", - category = "Games", - icon = "\0307 \0308 \0307 \ -\0308\031b> \030b\0310>\0308\0318 \ -\0307 ", - run = "rom/programs/fun/advanced/redirection", - requires = 'advanced', - }, - f39d173d91c22348565c20283b89d4d1cabd3b7e = { - title = "Falling", - category = "Games", - icon = "\030f \0302 \ -\0309 \0302 \0301 \ -\030e \0309 \0301 ", - run = "rom/programs/pocket/falling", - requires = 'advancedPocket', - }, - db56e2e1db9f7accfc37f2b132d27505c66ba521 = { - title = "Adventure", - category = "Games", - icon = "\030f\0310You \031 \ -\030f\0310Ther\030 \031 \ -\030f\0314?\031f \031 \030 ", - run = "rom/programs/fun/adventure", - }, - [ "76b849f460640bc789c433894382fb5acbac42a2" ] = { - title = "Worm", - category = "Games", - icon = "\030d \030 \030e \030 \ -\030d \030 \ -\030d ", - iconExt = "\030 \031f\0305\031f\151\030f\0315\135\131\0305\031f\146\ -\030 \031f\030f\0315\130\141\0305\031f\139\030f\0315\130\ -\030 \031f\0305\031f\146\143\030f\0315\158\031e\130", - run = "/rom/programs/fun/worm", - }, - [ "9f46ca3ef617166776ef6014a58d4e66859caa62" ] = { - title = "DJ", - category = "Games", - icon = " \030f \ -\030f \0307 \ -\030f \0307 \0300 ", - iconExt = "\031f\128\0307\143\131\131\131\131\143\030f\128\ -\0307\031f\129\0317\128\0319\136\0309\031b\136\132\0307\0319\132\0317\128\031f\130\ -\0317\130\143\0307\128\128\128\128\030f\143\129", - run = "/rom/programs/fun/dj", - }, - [ "76b849f460640bc789c433894382fb5acbac42a2" ] = { - title = "Tron", - category = "Games", - iconExt = "\030 \031f\030b\031f\143\030f\128\128\030b\143\143\143\030f\128\128\ -\030 \031f\0309\031b\140\030b\031f\151\030f\031b\131\0307\148\0317\128\030b\151\030f\031b\131\148\ -\030 \031f\030f\031b\131\031f\128\031b\131\0317\131\031f\128\0317\131\031b\131\031f\128", - run = "https://raw.githubusercontent.com/LDDestroier/CC/master/tron.lua", - }, -} diff --git a/sys/etc/apps.db b/sys/etc/apps.db new file mode 100644 index 0000000..e753fd1 --- /dev/null +++ b/sys/etc/apps.db @@ -0,0 +1,136 @@ +{ + [ "0a999012ffb87b3edac99adbdfc498b12831a1e2" ] = { + title = "Packages", + category = "System", + run = "PackageManager.lua", + iconExt = "\030c\0317\151\131\131\131\0307\031c\148\010\030c\0317\151\131\0310\143\0317\131\0307\031c\148\010\0307\031c\138\030 \0317\151\131\131\131", + }, + [ "b2efeaa1a7d6d2185ea02473cf758203dfcea3fe" ] = { + title = "Cloud", + category = "Apps", + run = "cshell.lua", + iconExt = "\0300\031 \159\131\135\0310\128\128\031 \139\131\030 \0310\144\010\0300\128\031f\137\144\0310\128\030a\136\149\133\0300\128\010\0300\031 \144\031f\134\136\132\031a\142\138\138\030 \0310\159", + }, + [ "53ebc572b4a44802ba114729f07bdaaf5409a9d7" ] = { + title = "Network", + category = "Apps", + icon = "\0304 \030 \010\030f \0304 \0307 \030 \031 \031f)\010\030f \0304 \0307 \030 \031f)", + iconExt = "\030 \031 \128\128\128\128\0305\140\030 \0315\137\144\010\0314\131\131\0304\031f\148\030 \031 \128\0305\155\150\149\010\147\0300\031f\141\0304\149\0307\0318\149\030 \031 \128\128\128", + run = "Network.lua", + }, + [ "c7116629a6a855cb774d9c7c8ad822fd83c71fb5" ] = { + title = "Reboot", + category = "System", + icon = "\0304\031f \030f\0310o..\0304\031f \010\0304\031f \030f\0310.o.\0304\031f \010\0304\031f - ", + iconExt = "\0307\031 \135\0300\0317\159\0307\0310\144\031 \139\010\0300\0317\131\0307\0310\147\0300\0317\156\131\010\030 \130\143\143\129", + run = "rom/programs/reboot", + }, + [ "fb91e24fa52d8d2b32937bf04d843f730319a902" ] = { + title = "Update", + category = "System", + icon = "\0301\03171\03180\030 \031 \010\0301\03181\030 \031 \010\0301\03170\03180\03171\0307\031f>", + iconExt = "\030 \031 \128\0313\152\131\131\132\031 \128\010\030 \0313\139\159\129\0303\031 \159\129\139\010\030 \031 \128\0313\136\0303\031 \143\143\030 \0313\134\031 \128", + run = "update update", + }, + [ "c47ae15370cfe1ed2781eedc1dc2547d12d9e972" ] = { + title = "Help", + category = "Apps", + icon = "\030 \0310 ? \010\030 \0310? \010\030 \0310\009 ?", + iconExt = "\0300\031 \129\030 \0310\131\0300\031 \148\030 \0310\148\010\030 \031 \128\0300\131\030 \0310\142\129\010\030 \031 \128\0300\131\030 \128\128", + run = "Help.lua", + }, + [ "b0832074630eb731d7fbe8074de48a90cd9bb220" ] = { + title = "Lua", + category = "Apps", + icon = "\030 \010\030 \0310lua>\031 \010\030 ", + iconExt = "\0300\031 \151\030 \128\0300\159\159\159\030 \0310\144\0304\031 \159\030 \128\010\0300\031 \149\030 \128\0300\149\149\151\145\030 \128\0314\153\010\030 \130\131\130\131\130\131\0314\130\031 \128", + run = "Lua.lua", + }, + [ "bc0792d8dc81e8aa30b987246a5ce97c40cd6833" ] = { + title = "System", + category = "System", + icon = "\030 \0307\031f| \010\0307\031f---o\030 \031 \010\030 \009 \0307\031f| ", + iconExt = "\030 \0318\138\0308\031 \130\0318\128\031 \129\030 \0318\133\010\030 \0318\143\0308\128\0317\143\0318\128\030 \143\010\030 \0318\138\135\143\139\133", + run = "System.lua", + }, + [ "2a4d562b1d9a9c90bdede6fac8ce4f7402462b86" ] = { + title = "Tasks", + category = "System", + icon = "\030 \031f \0315/\010\030 \031f \0315/\\/ \010\030 \0315/\031f ", + iconExt = "\030 \031 \128\128\0305\159\030 \128\0305\159\030 \0315\134\031 \128\010\030 \031 \128\0315\152\129\137\0305\031 \158\139\030 \128\010\030 \0315\134\031 \128\128\128\128\0305\154\030 \128", + run = "Tasks.lua", + }, + [ "a0365977708b7387ee9ce2c13e5820e6e11732cb" ] = { + title = "Pain", + category = "Apps", + iconExt = "\0307\031 \159\030 \128\128\128\128\128\128\128\010\0308\031 \135\0307\0318\144\140\030 \0317\159\143\031c\139\0302\135\030 \0312\157\010\030 \0318\143\133\0312\136\0302\031 \159\159\143\131\030 \0312\132", + run = "pain", + }, + [ "6a381ca189cbddd63737cbaf6e8b593844ce467ba52b1c5e5e05d8f29864385d" ] = { + title = "Sniffer", + category = "Apps", + iconExt = "\030 \031 \128\128\128\128\0315\149\0305\031 \154\030 \0315\137\010\0304\031 \159\0314\128\128\030 \144\0315\130\0305\031 \155\140\010\0314\151\0304\031f\148\030f\0314\151\0304\031f\148\030 \031 \128\128\128", + run = "Sniff.lua", + }, + [ "01c933b2a36ad8ed2d54089cb2903039046c1216" ] = { + title = "Enchat", + iconExt = "\030e\031f\151\030f\031e\156\0311\140\0314\140\0315\140\031d\140\031b\140\031a\132\010\030f\0314\128\030e\031f\132\030f\031e\132\0318nchat\010\030f\031e\138\141\0311\140\0314\140\0315\132\0317v\03183\031a\132", + category = "Apps", + run = "Enchat", + }, + [ "6ce6c512ea433a7fc5c8841628e7696cd0ff7f2b" ] = { + title = "Files", + category = "Apps", + icon = "\0300\0317==\031 \0307 \010\0300\0317====\010\0300\0317====", + iconExt = "\0300\031f\136\140\132\0308\031 \130\030 \0318\144\010\157\0300\031f\147\030f\0310\142\143\030 \149\010\0300\031f\136\140\132\140\030 \0310\149", + run = "Files.lua", + }, + [ "7fddb7d8d1d60b1eeefa9af01082e0811d4b484d" ] = { + title = "Shutdown", + category = "System", + icon = "\0304\031f \010\0304\031f \030f\0310zz\031 \010\0304\031f \030f ", + iconExt = "\030e\031 \135\030 \031e\148\030e\128\031 \151\139\010\030e\031e\128\030 \031 \128\031e\143\031 \128\030e\031e\128\010\030 \031e\139\030e\031 \130\131\129\030 \031e\135", + run = "/rom/programs/shutdown", + }, + [ "bdc1fd5d3c0f3dcfd55d010426e61bf9451e680d" ] = { + title = "Shell", + category = "Apps", + icon = "\0304 \030 \010\0304 \030f\0314> \0310_\031 \010\0304 \030f \030 ", + iconExt = "\030f\0314\151\131\131\131\131\010\030f\0314\149\030f\0314> \0310_ \010\030f\0314\149\030f ", + run = "shell", + }, + [ "b77aad5fb24921ef76ac8f3e500ed93fddae8f2a" ] = { + title = "Redirection", + category = "Games", + icon = "\0307 \0308 \0307 \010\0308\031b> \030b\0310>\0308\0318 \010\0307 ", + run = "rom/programs/fun/advanced/redirection", + requires = 'advanced', + }, + [ "f39d173d91c22348565c20283b89d4d1cabd3b7e" ] = { + title = "Falling", + category = "Games", + icon = "\030f \0302 \010\0309 \0302 \0301 \010\030e \0309 \0301 ", + run = "rom/programs/pocket/falling", + requires = 'advancedPocket', + }, + [ "db56e2e1db9f7accfc37f2b132d27505c66ba521" ] = { + title = "Adventure", + category = "Games", + icon = "\030f\0310You \031 \010\030f\0310Ther\030 \031 \010\030f\0314?\031f \031 \030 ", + run = "rom/programs/fun/adventure", + }, + [ "76b849f460640bc789c433894382fb5acbac42a2" ] = { + title = "Worm", + category = "Games", + icon = "\030d \030 \030e \030 \010\030d \030 \010\030d ", + iconExt = "\0305\031 \151\030 \0315\135\131\0305\031 \146\010\030 \0315\130\141\0305\031 \139\030 \0315\130\010\0305\031 \146\143\030 \0315\158\031e\130", + run = "/rom/programs/fun/worm", + }, + [ "9f46ca3ef617166776ef6014a58d4e66859caa62" ] = { + title = "DJ", + category = "Games", + icon = " \030f \010\030f \0307 \010\030f \0307 \0300 ", + iconExt = "\030 \031 \128\0307\143\131\131\131\131\143\030 \128\010\0307\031 \129\0317\128\0319\136\0309\031b\136\132\0307\0319\132\0317\128\031 \130\010\030 \0317\130\143\0307\128\128\128\128\030 \143\129", + run = "/rom/programs/fun/dj", + }, +} diff --git a/sys/etc/ext.theme b/sys/etc/ext.theme deleted file mode 100644 index 492529e..0000000 --- a/sys/etc/ext.theme +++ /dev/null @@ -1,28 +0,0 @@ -{ - ScrollBar = { - lineChar = '|', - sliderChar = '\127', - upArrowChar = '\30', - downArrowChar = '\31', - }, - Button = { - --focusIndicator = '\183', - }, - Checkbox = { - checkedIndicator = '\4', - leftMarker = '\124', - rightMarker = '\124', - }, - Chooser = { - leftIndicator = '\17', - rightIndicator = '\16', - }, - Grid = { - focusIndicator = '\183', - inverseSortIndicator = '\24', - }, - TitleBar = { - frameChar = '\140', - closeInd = '\215', - }, -} diff --git a/sys/etc/fstab b/sys/etc/fstab new file mode 100644 index 0000000..60fb9e1 --- /dev/null +++ b/sys/etc/fstab @@ -0,0 +1,6 @@ +sys/apps/pain.lua urlfs https://github.com/LDDestroier/CC/raw/master/pain.lua +sys/apps/update.lua urlfs http://pastebin.com/raw/UzGHLbNC +sys/apps/Enchat.lua urlfs https://raw.githubusercontent.com/LDDestroier/enchat/master/enchat3.lua +sys/apps/cloud.lua urlfs https://cloud-catcher.squiddev.cc/cloud.lua +sys/apps/nfttrans.lua urlfs https://pastebin.com/raw/e8XrzeDY +rom/modules/main/opus linkfs sys/modules/opus \ No newline at end of file diff --git a/sys/extensions/1.device.lua b/sys/extensions/1.device.lua deleted file mode 100644 index 003ae14..0000000 --- a/sys/extensions/1.device.lua +++ /dev/null @@ -1,186 +0,0 @@ -_G.requireInjector(_ENV) - -local Peripheral = require('peripheral') - -_G.device = Peripheral.getList() - -_G.device.terminal = _G.kernel.terminal -_G.device.terminal.side = 'terminal' -_G.device.terminal.type = 'terminal' -_G.device.terminal.name = 'terminal' - -_G.device.keyboard = { - side = 'keyboard', - type = 'keyboard', - name = 'keyboard', - hotkeys = { }, - state = { }, -} - -_G.device.mouse = { - side = 'mouse', - type = 'mouse', - name = 'mouse', - state = { }, -} - -local Input = require('input') -local Util = require('util') - -local device = _G.device -local kernel = _G.kernel -local keyboard = _G.device.keyboard -local mouse = _G.device.mouse -local os = _G.os - -local drivers = { } - -kernel.hook('peripheral', function(_, eventData) - local side = eventData[1] - if side then - local dev = Peripheral.addDevice(device, side) - if dev then - if drivers[dev.type] then - local e = drivers[dev.type](dev) - if type(e) == 'table' then - for _, v in pairs(e) do - os.queueEvent('device_attach', v.name) - end - elseif e then - os.queueEvent('device_attach', e.name) - end - end - - os.queueEvent('device_attach', dev.name, dev) - end - end -end) - -kernel.hook('peripheral_detach', function(_, eventData) - local side = eventData[1] - if side then - local dev = Util.find(device, 'side', side) - if dev then - os.queueEvent('device_detach', dev.name, dev) - if dev._children then - for _,v in pairs(dev._children) do - os.queueEvent('peripheral_detach', v.name) - end - end - device[dev.name] = nil - end - end -end) - -kernel.hook({ 'key', 'key_up', 'char', 'paste' }, function(event, eventData) - local code = eventData[1] - - -- maintain global keyboard state - if event == 'key' then - keyboard.state[code] = true - elseif event == 'key_up' then - if not keyboard.state[code] then - return true -- ensure key ups are only generated if a key down was sent - end - keyboard.state[code] = nil - end - - -- and fire hotkeys - local hotkey = Input:translate(event, eventData[1], eventData[2]) - - if hotkey and keyboard.hotkeys[hotkey.code] then - keyboard.hotkeys[hotkey.code](event, eventData) - end -end) - -kernel.hook({ 'mouse_click', 'mouse_up', 'mouse_drag' }, function(event, eventData) - local button = eventData[1] - if event == 'mouse_click' then - mouse.state[button] = true - else - if not mouse.state[button] then - return true -- ensure mouse ups are only generated if a mouse down was sent - end - if event == 'mouse_up' then - mouse.state[button] = nil - end - end -end) - -kernel.hook('kernel_focus', function() - Util.clear(keyboard.state) - Util.clear(mouse.state) -end) - -function keyboard.addHotkey(code, fn) - keyboard.hotkeys[code] = fn -end - -function keyboard.removeHotkey(code) - keyboard.hotkeys[code] = nil -end - -kernel.hook('monitor_touch', function(event, eventData) - local monitor = Peripheral.getBySide(eventData[1]) - if monitor and monitor.eventChannel then - monitor.eventChannel(event, table.unpack(eventData)) - return true -- stop propagation - end -end) - -local function createDevice(name, devType, method, manipulator) - local dev = { - name = name, - side = name, - type = devType, - } - local methods = { - 'drop', 'getDocs', 'getItem', 'getItemMeta', 'getTransferLocations', - 'list', 'pullItems', 'pushItems', 'size', 'suck', - } - if manipulator[method] then - for _,k in pairs(methods) do - dev[k] = function(...) - return manipulator[method]()[k](...) - end - end - if not manipulator._children then - manipulator._children = { dev } - else - table.insert(manipulator._children, dev) - end - device[name] = dev - end -end - -drivers['manipulator'] = function(dev) - if dev.getName then - local name - pcall(function() - name = dev.getName() - end) - if name then - if dev.getInventory then - createDevice(name .. ':inventory', 'inventory', 'getInventory', dev) - end - if dev.getEquipment then - createDevice(name .. ':equipment', 'equipment', 'getEquipment', dev) - end - if dev.getEnder then - createDevice(name .. ':enderChest', 'enderChest', 'getEnder', dev) - end - - return dev._children - end - end -end - --- initialize drivers -for _,v in pairs(device) do - if drivers[v.type] then - local s, m = pcall(drivers[v.type], v) - if not s and m then - _G.printError(m) - end - end -end diff --git a/sys/extensions/4.user.lua b/sys/extensions/4.user.lua deleted file mode 100644 index 662fc6f..0000000 --- a/sys/extensions/4.user.lua +++ /dev/null @@ -1,50 +0,0 @@ -_G.requireInjector(_ENV) - -local Util = require('util') - -local fs = _G.fs -local shell = _ENV.shell - -if not fs.exists('usr/apps') then - fs.makeDir('usr/apps') -end -if not fs.exists('usr/autorun') then - fs.makeDir('usr/autorun') -end ---if not fs.exists('usr/config/fstab') then --- Util.writeFile('usr/config/fstab', --- 'usr gitfs kepler155c/opus-apps/' .. _G.OPUS_BRANCH) ---end - -if not fs.exists('usr/config/shell') then - Util.writeTable('usr/config/shell', { - aliases = shell.aliases(), - path = 'usr/apps:sys/apps:' .. shell.path(), - lua_path = 'sys/apis:usr/apis', - }) -end - -if not fs.exists('usr/config/packages') then - local packages = { - [ 'develop-1.8' ] = 'https://pastebin.com/raw/WhEiNGZE', - [ 'master-1.8' ] = 'https://pastebin.com/raw/pexZpAxt', - } - - if packages[_G.OPUS_BRANCH] then - Util.download(packages[_G.OPUS_BRANCH], 'usr/config/packages') - end -end - -local config = Util.readTable('usr/config/shell') -if config.aliases then - for k in pairs(shell.aliases()) do - shell.clearAlias(k) - end - for k,v in pairs(config.aliases) do - shell.setAlias(k, v) - end -end -shell.setPath(config.path) -_G.LUA_PATH = config.lua_path - -fs.loadTab('usr/config/fstab') diff --git a/sys/extensions/6.packages.lua b/sys/extensions/6.packages.lua deleted file mode 100644 index 8fb598a..0000000 --- a/sys/extensions/6.packages.lua +++ /dev/null @@ -1,45 +0,0 @@ -_G.requireInjector(_ENV) - -local Packages = require('packages') -local Util = require('util') - -local shell = _ENV.shell -local fs = _G.fs - -local appPaths = Util.split(shell.path(), '(.-):') -local luaPaths = Util.split(_G.LUA_PATH, '(.-):') - -local function addPath(t, e) - local function hasEntry() - for _,v in ipairs(t) do - if v == e then - return true - end - end - end - if not hasEntry() then - table.insert(t, 1, e) - end -end - --- dependency graph --- https://github.com/mpeterv/depgraph/blob/master/src/depgraph/init.lua - -for name in pairs(Packages:installed()) do - local packageDir = fs.combine('packages', name) - if fs.exists(fs.combine(packageDir, '.install')) then - local install = Util.readTable(fs.combine(packageDir, '.install')) - if install and install.mount then - fs.mount(table.unpack(Util.matches(install.mount))) - end - end - - addPath(appPaths, packageDir) - local apiPath = fs.combine(fs.combine('packages', name), 'apis') - if fs.exists(apiPath) then - addPath(luaPaths, apiPath) - end -end - -shell.setPath(table.concat(appPaths, ':')) -_G.LUA_PATH = table.concat(luaPaths, ':') diff --git a/sys/extensions/6.tl3.lua b/sys/extensions/6.tl3.lua deleted file mode 100644 index d82930a..0000000 --- a/sys/extensions/6.tl3.lua +++ /dev/null @@ -1,1195 +0,0 @@ -if not _G.turtle then - return -end - -_G.requireInjector(_ENV) - -local Pathing = require('turtle.pathfind') -local GPS = require('gps') -local Point = require('point') -local synchronized = require('sync').sync -local Util = require('util') - -local os = _G.os -local peripheral = _G.peripheral -local turtle = _G.turtle - -local function noop() end -local headings = Point.headings -local state = { } - -turtle.pathfind = Pathing.pathfind -turtle.point = { x = 0, y = 0, z = 0, heading = 0 } - -function turtle.getPoint() return turtle.point end -function turtle.getState() return state end -function turtle.isAborted() return state.abort end -function turtle.getStatus() return state.status end -function turtle.setStatus(s) state.status = s end - -local function _defaultMove(action) - while not action.move() do - if not state.digPolicy(action) and not state.attackPolicy(action) then - return false - end - end - return true -end - -function turtle.setPoint(pt, isGPS) - turtle.point.x = pt.x - turtle.point.y = pt.y - turtle.point.z = pt.z - if pt.heading then - turtle.point.heading = pt.heading - end - turtle.point.gps = isGPS - return true -end - -function turtle.resetState() - state.abort = false - state.status = 'idle' - state.attackPolicy = noop - state.digPolicy = noop - state.movePolicy = _defaultMove - state.moveCallback = noop - Pathing.reset() - return true -end - -function turtle.reset() - turtle.point.x = 0 - turtle.point.y = 0 - turtle.point.z = 0 - turtle.point.heading = 0 -- should be facing - turtle.point.gps = false - - turtle.resetState() - return true -end - -turtle.reset() - -local actions = { - up = { - detect = turtle.native.detectUp, - dig = turtle.native.digUp, - move = turtle.native.up, - attack = turtle.native.attackUp, - place = turtle.native.placeUp, - drop = turtle.native.dropUp, - suck = turtle.native.suckUp, - compare = turtle.native.compareUp, - inspect = turtle.native.inspectUp, - side = 'top' - }, - down = { - detect = turtle.native.detectDown, - dig = turtle.native.digDown, - move = turtle.native.down, - attack = turtle.native.attackDown, - place = turtle.native.placeDown, - drop = turtle.native.dropDown, - suck = turtle.native.suckDown, - compare = turtle.native.compareDown, - inspect = turtle.native.inspectDown, - side = 'bottom' - }, - forward = { - detect = turtle.native.detect, - dig = turtle.native.dig, - move = turtle.native.forward, - attack = turtle.native.attack, - place = turtle.native.place, - drop = turtle.native.drop, - suck = turtle.native.suck, - compare = turtle.native.compare, - inspect = turtle.native.inspect, - side = 'front' - }, - back = { - detect = noop, - dig = noop, - move = turtle.native.back, - attack = noop, - place = noop, - suck = noop, - compare = noop, - side = 'back' - }, -} - -function turtle.getAction(direction) - return actions[direction] -end - -function turtle.getHeadingInfo(heading) - heading = heading or turtle.point.heading - return headings[heading] -end - --- hackish way to support unlimited fuel -if type(turtle.getFuelLevel()) ~= 'number' then - function turtle.getFuelLevel() - return 10000000 - end -end - --- [[ Basic turtle actions ]] -- -local function inventoryAction(fn, name, qty) - local slots = turtle.getFilledSlots() - local s - for _,slot in pairs(slots) do - if slot.key == name or slot.name == name then - turtle.native.select(slot.index) - if not qty then - s = fn() - else - s = fn(math.min(qty, slot.count)) - qty = qty - slot.count - if qty < 0 then - break - end - end - end - end - if not s then - return false, 'No items found' - end - return s -end - --- [[ Attack ]] -- -local function _attack(action) - if action.attack() then - repeat until not action.attack() - return true - end - return false -end - -turtle.attackPolicies = { - none = noop, - - attack = function(action) - return _attack(action) - end, -} - -function turtle.attack() return _attack(actions.forward) end -function turtle.attackUp() return _attack(actions.up) end -function turtle.attackDown() return _attack(actions.down) end - -function turtle.setAttackPolicy(policy) state.attackPolicy = policy end - --- [[ Place ]] -- -local function _place(action, indexOrId) - - local slot - - if indexOrId then - slot = turtle.getSlot(indexOrId) - if not slot then - return false, 'No items to place' - end - end - - if slot and slot.qty == 0 then - return false, 'No items to place' - end - - return Util.tryTimes(3, function() - if slot then - turtle.select(slot.index) - end - local result = { action.place() } - if result[1] then - return true - end - if not state.digPolicy(action) then - state.attackPolicy(action) - end - return unpack(result) - end) -end - -function turtle.place(slot) return _place(actions.forward, slot) end -function turtle.placeUp(slot) return _place(actions.up, slot) end -function turtle.placeDown(slot) return _place(actions.down, slot) end - -local function _drop(action, qtyOrName, qty) - if not qtyOrName or type(qtyOrName) == 'number' then - return action.drop(qtyOrName or 64) - end - return inventoryAction(action.drop, qtyOrName, qty) -end - -function turtle.drop(count, slot) return _drop(actions.forward, count, slot) end -function turtle.dropUp(count, slot) return _drop(actions.up, count, slot) end -function turtle.dropDown(count, slot) return _drop(actions.down, count, slot) end - -function turtle.refuel(qtyOrName, qty) - if not qtyOrName or type(qtyOrName) == 'number' then - return turtle.native.refuel(qtyOrName or 64) - end - return inventoryAction(turtle.native.refuel, qtyOrName, qty or 64) -end - -function turtle.isTurtleAtSide(side) - local sideType = peripheral.getType(side) - return sideType and sideType == 'turtle' -end - -turtle.digPolicies = { - none = noop, - - dig = function(action) - return action.dig() - end, - - turtleSafe = function(action) - if action.side == 'back' then - return false - end - if not turtle.isTurtleAtSide(action.side) then - return action.dig() - end - return Util.tryTimes(6, function() --- if not turtle.isTurtleAtSide(action.side) then --- return true --action.dig() --- end - os.sleep(.25) - if not action.detect() then - return true - end - end) - end, - - digAndDrop = function(action) - if action.detect() then - local slots = turtle.getInventory() - if action.dig() then - turtle.reconcileInventory(slots) - return true - end - end - return false - end -} - -turtle.movePolicies = { - none = noop, - default = _defaultMove, - assured = function(action) - if not _defaultMove(action) then - if action.side == 'back' then - return false - end - local oldStatus = state.status - print('assured move: stuck') - state.status = 'stuck' - repeat - os.sleep(1) - until _defaultMove(action) - state.status = oldStatus - end - return true - end, -} - -turtle.policies = { - none = { dig = turtle.digPolicies.none, attack = turtle.attackPolicies.none }, - digOnly = { dig = turtle.digPolicies.dig, attack = turtle.attackPolicies.none }, - attackOnly = { dig = turtle.digPolicies.none, attack = turtle.attackPolicies.attack }, - digAttack = { dig = turtle.digPolicies.dig, attack = turtle.attackPolicies.attack }, - turtleSafe = { dig = turtle.digPolicies.turtleSafe, attack = turtle.attackPolicies.attack }, - - attack = { attack = turtle.attackPolicies.attack }, - - defaultMove = { move = turtle.movePolicies.default }, - assuredMove = { move = turtle.movePolicies.assured }, -} - -function turtle.setPolicy(...) - local args = { ... } - for _, policy in pairs(args) do - if type(policy) == 'string' then - policy = turtle.policies[policy] - end - if not policy then - error('Invalid policy') - -- return false, 'Invalid policy' - end - if policy.dig then - state.digPolicy = policy.dig - end - if policy.attack then - state.attackPolicy = policy.attack - end - if policy.move then - state.movePolicy = policy.move - end - end - return true -end - -function turtle.setDigPolicy(policy) state.digPolicy = policy end -function turtle.setMoveCallback(cb) state.moveCallback = cb end -function turtle.clearMoveCallback() state.moveCallback = noop end -function turtle.getMoveCallback() return state.moveCallback end - --- [[ Heading ]] -- -function turtle.getHeading() - return turtle.point.heading -end - -function turtle.turnRight() - turtle.setHeading((turtle.point.heading + 1) % 4) - return turtle.point -end - -function turtle.turnLeft() - turtle.setHeading((turtle.point.heading - 1) % 4) - return turtle.point -end - -function turtle.turnAround() - turtle.setHeading((turtle.point.heading + 2) % 4) - return turtle.point -end - -function turtle.setHeading(heading) - if not heading then - return false, 'Invalid heading' - end - - local fi = Point.facings[heading] - if not fi then - return false, 'Invalid heading' - end - - heading = fi.heading % 4 - if heading ~= turtle.point.heading then - while heading < turtle.point.heading do - heading = heading + 4 - end - if heading - turtle.point.heading == 3 then - turtle.native.turnLeft() - turtle.point.heading = (turtle.point.heading - 1) % 4 - state.moveCallback('turn', turtle.point) - else - local turns = heading - turtle.point.heading - while turns > 0 do - turns = turns - 1 - turtle.native.turnRight() - turtle.point.heading = (turtle.point.heading + 1) % 4 - state.moveCallback('turn', turtle.point) - end - end - end - - return turtle.point -end - -function turtle.headTowardsX(dx) - if turtle.point.x ~= dx then - if turtle.point.x > dx then - turtle.setHeading(2) - else - turtle.setHeading(0) - end - end -end - -function turtle.headTowardsZ(dz) - if turtle.point.z ~= dz then - if turtle.point.z > dz then - turtle.setHeading(3) - else - turtle.setHeading(1) - end - end -end - -function turtle.headTowards(pt) - local xd = math.abs(turtle.point.x - pt.x) - local zd = math.abs(turtle.point.z - pt.z) - if xd > zd then - turtle.headTowardsX(pt.x) - else - turtle.headTowardsZ(pt.z) - end -end - --- [[ move ]] -- -function turtle.up() - if state.movePolicy(actions.up) then - turtle.point.y = turtle.point.y + 1 - state.moveCallback('up', turtle.point) - return true, turtle.point - end -end - -function turtle.down() - if state.movePolicy(actions.down) then - turtle.point.y = turtle.point.y - 1 - state.moveCallback('down', turtle.point) - return true, turtle.point - end -end - -function turtle.forward() - if state.movePolicy(actions.forward) then - turtle.point.x = turtle.point.x + headings[turtle.point.heading].xd - turtle.point.z = turtle.point.z + headings[turtle.point.heading].zd - state.moveCallback('forward', turtle.point) - return true, turtle.point - end -end - -function turtle.back() - if state.movePolicy(actions.back) then - turtle.point.x = turtle.point.x - headings[turtle.point.heading].xd - turtle.point.z = turtle.point.z - headings[turtle.point.heading].zd - state.moveCallback('back', turtle.point) - return true, turtle.point - end -end - -local function moveTowardsX(dx) - if not tonumber(dx) then error('moveTowardsX: Invalid arguments') end - local direction = dx - turtle.point.x - local move - - if direction == 0 then - return true - end - - if direction > 0 and turtle.point.heading == 0 or - direction < 0 and turtle.point.heading == 2 then - move = turtle.forward - else - move = turtle.back - end - - repeat - if not move() then - return false - end - until turtle.point.x == dx - return true -end - -local function moveTowardsZ(dz) - local direction = dz - turtle.point.z - local move - - if direction == 0 then - return true - end - - if direction > 0 and turtle.point.heading == 1 or - direction < 0 and turtle.point.heading == 3 then - move = turtle.forward - else - move = turtle.back - end - - repeat - if not move() then - return false - end - until turtle.point.z == dz - return true -end - --- [[ go ]] -- --- 1 turn goto (going backwards if possible) -function turtle.gotoSingleTurn(dx, dy, dz, dh) - dx = dx or turtle.point.x - dy = dy or turtle.point.y - dz = dz or turtle.point.z - - local function gx() - if turtle.point.x ~= dx then - moveTowardsX(dx) - end - if turtle.point.z ~= dz then - if dh and dh % 2 == 1 then - turtle.setHeading(dh) - else - turtle.headTowardsZ(dz) - end - end - end - - local function gz() - if turtle.point.z ~= dz then - moveTowardsZ(dz) - end - if turtle.point.x ~= dx then - if dh and dh % 2 == 0 then - turtle.setHeading(dh) - else - turtle.headTowardsX(dx) - end - end - end - - repeat - local x, z - local y = turtle.point.y - - repeat - x, z = turtle.point.x, turtle.point.z - - if turtle.point.heading % 2 == 0 then - gx() - gz() - else - gz() - gx() - end - until x == turtle.point.x and z == turtle.point.z - - if turtle.point.y ~= dy then - turtle.gotoY(dy) - end - - if turtle.point.x == dx and turtle.point.z == dz and turtle.point.y == dy then - return true - end - - until x == turtle.point.x and z == turtle.point.z and y == turtle.point.y - - return false -end - -local function gotoEx(dx, dy, dz) - -- determine the heading to ensure the least amount of turns - -- first check is 1 turn needed - remaining require 2 turns - if turtle.point.heading == 0 and turtle.point.x <= dx or - turtle.point.heading == 2 and turtle.point.x >= dx or - turtle.point.heading == 1 and turtle.point.z <= dz or - turtle.point.heading == 3 and turtle.point.z >= dz then - -- maintain current heading - -- nop - elseif dz > turtle.point.z and turtle.point.heading == 0 or - dz < turtle.point.z and turtle.point.heading == 2 or - dx < turtle.point.x and turtle.point.heading == 1 or - dx > turtle.point.x and turtle.point.heading == 3 then - turtle.turnRight() - else - turtle.turnLeft() - end - - if (turtle.point.heading % 2) == 1 then - if not turtle.gotoZ(dz) then return false end - if not turtle.gotoX(dx) then return false end - else - if not turtle.gotoX(dx) then return false end - if not turtle.gotoZ(dz) then return false end - end - - if dy then - if not turtle.gotoY(dy) then return false end - end - - return true -end - --- fallback goto - will turn around if was previously moving backwards -local function gotoMultiTurn(dx, dy, dz) - if gotoEx(dx, dy, dz) then - return true - end - - local moved - repeat - local x, y, z = turtle.point.x, turtle.point.y, turtle.point.z - - -- try going the other way - if (turtle.point.heading % 2) == 1 then - turtle.headTowardsX(dx) - else - turtle.headTowardsZ(dz) - end - - if gotoEx(dx, dy, dz) then - return true - end - - if dy then - turtle.gotoY(dy) - end - - moved = x ~= turtle.point.x or y ~= turtle.point.y or z ~= turtle.point.z - until not moved - - return false -end - --- go backwards - turning around if necessary to fight mobs / break blocks -function turtle.goback() - local hi = headings[turtle.point.heading] - return turtle._goto({ - x = turtle.point.x - hi.xd, - y = turtle.point.y, - z = turtle.point.z - hi.zd, - heading = turtle.point.heading, - }) -end - -function turtle.gotoYfirst(pt) - if turtle._gotoY(pt.y) then - if turtle._goto(pt) then - turtle.setHeading(pt.heading) - return true - end - end -end - -function turtle.gotoYlast(pt) - if turtle._goto({ x = pt.x, z = pt.z, heading = pt.heading }) then - if turtle.gotoY(pt.y) then - turtle.setHeading(pt.heading) - return true - end - end -end - -function turtle._goto(pt) - local dx, dy, dz, dh = pt.x, pt.y, pt.z, pt.heading - if not turtle.gotoSingleTurn(dx, dy, dz, dh) then - if not gotoMultiTurn(dx, dy, dz) then - return false, 'Failed to reach location' - end - end - turtle.setHeading(dh) - return pt -end - --- avoid lint errors -turtle['goto'] = turtle._goto - -function turtle.gotoX(dx) - turtle.headTowardsX(dx) - - while turtle.point.x ~= dx do - if not turtle.forward() then - return false - end - end - return true -end - -function turtle.gotoZ(dz) - turtle.headTowardsZ(dz) - - while turtle.point.z ~= dz do - if not turtle.forward() then - return false - end - end - return true -end - -function turtle.gotoY(dy) - while turtle.point.y > dy do - if not turtle.down() then - return false - end - end - - while turtle.point.y < dy do - if not turtle.up() then - return false - end - end - return true -end - --- [[ Slot management ]] -- -function turtle.getSlot(indexOrId, slots) - if type(indexOrId) == 'string' then - slots = slots or turtle.getInventory() - local _,c = string.gsub(indexOrId, ':', '') - if c == 2 then -- combined id and dmg .. ie. minecraft:coal:0 - return Util.find(slots, 'iddmg', indexOrId) - end - return Util.find(slots, 'id', indexOrId) - end - - local detail = turtle.getItemDetail(indexOrId) - if detail then - return { - name = detail.name, - damage = detail.damage, - count = detail.count, - key = detail.name .. ':' .. detail.damage, - - index = indexOrId, - - -- deprecate - qty = detail.count, - dmg = detail.damage, - id = detail.name, - iddmg = detail.name .. ':' .. detail.damage, - } - end - - -- inconsistent return value - -- null is returned if indexOrId is a string and no item is present - return { - qty = 0, -- deprecate - count = 0, - index = indexOrId, - } -end - -function turtle.select(indexOrId) - if type(indexOrId) == 'number' then - return turtle.native.select(indexOrId) - end - - local s = turtle.getSlot(indexOrId) - if s then - turtle.native.select(s.index) - return s - end - - return false, 'Inventory does not contain item' -end - -function turtle.getInventory(slots) - slots = slots or { } - for i = 1, 16 do - slots[i] = turtle.getSlot(i) - end - return slots -end - -function turtle.getSummedInventory() - local slots = turtle.getFilledSlots() - local t = { } - for _,slot in pairs(slots) do - local entry = t[slot.iddmg] - if not entry then - entry = { - count = 0, - damage = slot.damage, - name = slot.name, - key = slot.key, - - -- deprecate - qty = 0, - dmg = slot.dmg, - id = slot.id, - iddmg = slot.iddmg, - } - t[slot.iddmg] = entry - end - entry.qty = entry.qty + slot.qty - entry.count = entry.qty - end - return t -end - -function turtle.has(item, count) - if item:match('.*:%d') then - local slot = turtle.getSummedInventory()[item] - return slot and slot.count >= (count or 1) - end - local slot = turtle.getSlot(item) - return slot and slot.count > 0 -end - -function turtle.getFilledSlots(startSlot) - startSlot = startSlot or 1 - - local slots = { } - for i = startSlot, 16 do - local count = turtle.getItemCount(i) - if count > 0 then - slots[i] = turtle.getSlot(i) - end - end - return slots -end - -function turtle.eachFilledSlot(fn) - local slots = turtle.getFilledSlots() - for _,slot in pairs(slots) do - fn(slot) - end -end - -function turtle.emptyInventory(dropAction) - dropAction = dropAction or turtle.native.drop - turtle.eachFilledSlot(function(slot) - turtle.select(slot.index) - dropAction() - end) - turtle.select(1) -end - -function turtle.reconcileInventory(slots, dropAction) - dropAction = dropAction or turtle.native.drop - for _,s in pairs(slots) do - local qty = turtle.getItemCount(s.index) - if qty > s.qty then - turtle.select(s.index) - dropAction(qty-s.qty, s) - end - end -end - -function turtle.selectSlotWithItems(startSlot) - startSlot = startSlot or 1 - for i = startSlot, 16 do - if turtle.getItemCount(i) > 0 then - turtle.select(i) - return i - end - end -end - -function turtle.selectSlotWithQuantity(qty, startSlot) - startSlot = startSlot or 1 - - for i = startSlot, 16 do - if turtle.getItemCount(i) == qty then - turtle.select(i) - return i - end - end -end - -function turtle.selectOpenSlot(startSlot) - return turtle.selectSlotWithQuantity(0, startSlot) -end - -function turtle.condense() - local slots = turtle.getInventory() - - for i = 16, 1, -1 do - if slots[i].count > 0 then - for j = 1, i - 1 do - if slots[j].count == 0 or slots[i].key == slots[j].key then - turtle.select(i) - turtle.transferTo(j, 64) - local transferred = slots[i].qty - turtle.getItemCount(i) - slots[j].count = slots[j].count + transferred - slots[i].count = slots[i].count - transferred - slots[j].key = slots[i].key - if slots[i].count == 0 then - break - end - end - end - end - end - return true -end - -function turtle.getItemCount(idOrName) - if type(idOrName) == 'number' then - return turtle.native.getItemCount(idOrName) - end - local slots = turtle.getFilledSlots() - local count = 0 - for _,slot in pairs(slots) do - if slot.iddmg == idOrName or slot.name == idOrName then - count = count + slot.qty - end - end - return count -end - -function turtle.equip(side, item) - if item then - if not turtle.select(item) then - return false, 'Unable to equip ' .. item - end - end - - if side == 'left' then - return turtle.equipLeft() - end - return turtle.equipRight() -end - -function turtle.isEquipped(item) - if peripheral.getType('left') == item then - return 'left' - elseif peripheral.getType('right') == item then - return 'right' - end -end - --- [[ ]] -- -function turtle.run(fn, ...) - local args = { ... } - local s, m - - if type(fn) == 'string' then - fn = turtle[fn] - end - - synchronized(turtle, function() - turtle.resetState() - s, m = pcall(function() fn(unpack(args)) end) - turtle.resetState() - if not s and m then - _G.printError(m) - end - end) - - return s, m -end - -function turtle.abort(abort) - state.abort = abort - if abort then - os.queueEvent('turtle_abort') - end -end - --- [[ Pathing ]] -- -function turtle.setPersistent(isPersistent) - if isPersistent then - Pathing.setBlocks({ }) - else - Pathing.setBlocks() - end -end - -function turtle.setPathingBox(box) - Pathing.setBox(box) -end - -function turtle.addWorldBlock(pt) - Pathing.addBlock(pt) -end - -local movementStrategy = turtle.pathfind - -function turtle.setMovementStrategy(strategy) - if strategy == 'pathing' then - movementStrategy = turtle.pathfind - elseif strategy == 'goto' then - movementStrategy = turtle._goto - else - error('Invalid movement strategy') - end -end - -function turtle.faceAgainst(pt, options) -- 4 sided - options = options or { } - options.dest = { } - - for i = 0, 3 do - local hi = Point.facings[i] - table.insert(options.dest, { - x = pt.x + hi.xd, - z = pt.z + hi.zd, - y = pt.y + hi.yd, - heading = (hi.heading + 2) % 4, - }) - end - - return movementStrategy(Point.closest(turtle.point, options.dest), options) -end - --- move against this point --- if the point does not contain a heading, then the turtle --- will face the block (if on same plane) --- if above or below, the heading is undetermined unless specified -function turtle.moveAgainst(pt, options) -- 6 sided - options = options or { } - options.dest = { } - - for i = 0, 5 do - local hi = turtle.getHeadingInfo(i) - local heading, direction - if i < 4 then - heading = (hi.heading + 2) % 4 - direction = 'forward' - elseif i == 4 then - direction = 'down' - elseif i == 5 then - direction = 'up' - end - - table.insert(options.dest, { - x = pt.x + hi.xd, - z = pt.z + hi.zd, - y = pt.y + hi.yd, - direction = direction, - heading = pt.heading or heading, - }) - end - - return movementStrategy(Point.closest(turtle.point, options.dest), options) -end - -local actionsAt = { - detect = { - up = turtle.detectUp, - down = turtle.detectDown, - forward = turtle.detect, - }, - dig = { - up = turtle.digUp, - down = turtle.digDown, - forward = turtle.dig, - }, - move = { - up = turtle.moveUp, - down = turtle.moveDown, - forward = turtle.move, - }, - attack = { - up = turtle.attackUp, - down = turtle.attackDown, - forward = turtle.attack, - }, - place = { - up = turtle.placeUp, - down = turtle.placeDown, - forward = turtle.place, - }, - drop = { - up = turtle.dropUp, - down = turtle.dropDown, - forward = turtle.drop, - }, - suck = { - up = turtle.suckUp, - down = turtle.suckDown, - forward = turtle.suck, - }, - compare = { - up = turtle.compareUp, - down = turtle.compareDown, - forward = turtle.compare, - }, - inspect = { - up = turtle.inspectUp, - down = turtle.inspectDown, - forward = turtle.inspect, - }, -} - --- pt = { x,y,z,heading,direction } --- direction should only be up or down if provided --- heading can be provided to tell which way to face during action --- ex: place a block at the point from above facing east -local function _actionAt(action, pt, ...) - if not pt.heading and not pt.direction then - local msg - pt, msg = turtle.moveAgainst(pt) - if pt then - return action[pt.direction](...) - end - return pt, msg - end - - local reversed = - { [0] = 2, [1] = 3, [2] = 0, [3] = 1, [4] = 5, [5] = 4, } - local dir = reversed[headings[pt.direction or pt.heading].heading] - local apt = { x = pt.x + headings[dir].xd, - y = pt.y + headings[dir].yd, - z = pt.z + headings[dir].zd, } - local direction - - -- ex: place a block at this point, from above, facing east - if dir < 4 then - apt.heading = (dir + 2) % 4 - direction = 'forward' - elseif dir == 4 then - apt.heading = pt.heading - direction = 'down' - elseif dir == 5 then - apt.heading = pt.heading - direction = 'up' - end - - if movementStrategy(apt) then - return action[direction](...) - end -end - -local function _actionDownAt(action, pt, ...) - pt = Util.shallowCopy(pt) - pt.direction = Point.DOWN - return _actionAt(action, pt, ...) -end - -local function _actionUpAt(action, pt, ...) - pt = Util.shallowCopy(pt) - pt.direction = Point.UP - return _actionAt(action, pt, ...) -end - -local function _actionForwardAt(action, pt, ...) - if turtle.faceAgainst(pt) then - return action.forward(...) - end -end - -function turtle.detectAt(pt) return _actionAt(actionsAt.detect, pt) end -function turtle.detectDownAt(pt) return _actionDownAt(actionsAt.detect, pt) end -function turtle.detectForwardAt(pt) return _actionForwardAt(actionsAt.detect, pt) end -function turtle.detectUpAt(pt) return _actionUpAt(actionsAt.detect, pt) end - -function turtle.digAt(pt) return _actionAt(actionsAt.dig, pt) end -function turtle.digDownAt(pt) return _actionDownAt(actionsAt.dig, pt) end -function turtle.digForwardAt(pt) return _actionForwardAt(actionsAt.dig, pt) end -function turtle.digUpAt(pt) return _actionUpAt(actionsAt.dig, pt) end - -function turtle.attackAt(pt) return _actionAt(actionsAt.attack, pt) end -function turtle.attackDownAt(pt) return _actionDownAt(actionsAt.attack, pt) end -function turtle.attackForwardAt(pt) return _actionForwardAt(actionsAt.attack, pt) end -function turtle.attackUpAt(pt) return _actionUpAt(actionsAt.attack, pt) end - -function turtle.placeAt(pt, arg, dir) return _actionAt(actionsAt.place, pt, arg, dir) end -function turtle.placeDownAt(pt, arg) return _actionDownAt(actionsAt.place, pt, arg) end -function turtle.placeForwardAt(pt, arg) return _actionForwardAt(actionsAt.place, pt, arg) end -function turtle.placeUpAt(pt, arg) return _actionUpAt(actionsAt.place, pt, arg) end - -function turtle.dropAt(pt, ...) return _actionAt(actionsAt.drop, pt, ...) end -function turtle.dropDownAt(pt, ...) return _actionDownAt(actionsAt.drop, pt, ...) end -function turtle.dropForwardAt(pt, ...) return _actionForwardAt(actionsAt.drop, pt, ...) end -function turtle.dropUpAt(pt, ...) return _actionUpAt(actionsAt.drop, pt, ...) end - -function turtle.suckAt(pt, qty) return _actionAt(actionsAt.suck, pt, qty or 64) end -function turtle.suckDownAt(pt, qty) return _actionDownAt(actionsAt.suck, pt, qty or 64) end -function turtle.suckForwardAt(pt, qty) return _actionForwardAt(actionsAt.suck, pt, qty or 64) end -function turtle.suckUpAt(pt, qty) return _actionUpAt(actionsAt.suck, pt, qty or 64) end - -function turtle.compareAt(pt) return _actionAt(actionsAt.compare, pt) end -function turtle.compareDownAt(pt) return _actionDownAt(actionsAt.compare, pt) end -function turtle.compareForwardAt(pt) return _actionForwardAt(actionsAt.compare, pt) end -function turtle.compareUpAt(pt) return _actionUpAt(actionsAt.compare, pt) end - -function turtle.inspectAt(pt) return _actionAt(actionsAt.inspect, pt) end -function turtle.inspectDownAt(pt) return _actionDownAt(actionsAt.inspect, pt) end -function turtle.inspectForwardAt(pt) return _actionForwardAt(actionsAt.inspect, pt) end -function turtle.inspectUpAt(pt) return _actionUpAt(actionsAt.inspect, pt) end - --- [[ GPS ]] -- -function turtle.enableGPS(timeout) - local pt = GPS.getPointAndHeading(timeout) - if pt then - turtle.setPoint(pt, true) - return turtle.point - end -end - -function turtle.addFeatures(...) - for _,feature in pairs({ ... }) do - require('turtle.' .. feature) - end -end diff --git a/sys/help/CloudCatcher b/sys/help/CloudCatcher new file mode 100644 index 0000000..7494099 --- /dev/null +++ b/sys/help/CloudCatcher @@ -0,0 +1,5 @@ +Cloud Catcher is a web terminal for ComputerCraft, allowing you to interact with any in-game computer in the browser, as well as edit files remotely! + +To get started, visit https://cloud-catcher.squiddev.cc for a session key. + +Within Files, press 'c' to edit a file using Cloud Catcher. diff --git a/sys/help/Opus b/sys/help/Opus new file mode 100644 index 0000000..e4c1fc3 --- /dev/null +++ b/sys/help/Opus @@ -0,0 +1,39 @@ +Shortcut Keys +============= + * Control-o: Show the Overview + * Control-tab: Cycle to next tab + * Control-shift-tab: Cycle to previous tab + * Control-d: Show/toggle logging screen + * Control-c: Copy (in most applications) + * Control-shift-v: Paste from internal clipboard + * Control-shift-doubleclick: Open in Lua the clicked UI element + +Wireless Networking +=================== +To establish one-way trust between two computers with modems, run the following in a shell prompt: + +On the target computer, run: +> password + +On the source computer, run: +> trust + +Running Custom Programs +======================= +To create a program that runs at startup, create a file in /usr/autorun to start the program. Example: + +In file: /usr/autorun/startup.lua +shell.openForegroundTab('myprogram') + +There are 3 different ways to open tabs: + + * shell.openTab : background tab + * shell.openForegroundTab : focused tab + * shell.openHiddenTab : appears only in the Tasks application + +Copy / Paste +============ +Opus can paste from both the system clipboard and the internal clipboard. + + * control-v: for normal clipboard + * control-shift-v: for internal clipboard diff --git a/sys/help/Opus-Applications b/sys/help/Opus-Applications new file mode 100644 index 0000000..a7747a3 --- /dev/null +++ b/sys/help/Opus-Applications @@ -0,0 +1,30 @@ +Opus applications are grouped into packages with a common theme. + +To install a package, use either the System -> Packages program or the package command line program. + +Shell usage: + +> package list +> package install +> package update +> package uninstall + +Package definitions are located in usr/apps/packages. This file can be modified to add custom packages. + +Current stable packages +======================= + +* core +Programming and miscellaneous applications. Also contains drivers needed for other packages. + +* builder +A program for creating structures from schematic files using a turtle (requires core). + +* farms +Various programs for farming resources (wood, crops, animals). + +* milo +An A/E like storage implementation (requires core). + +* miners +Mining programs. diff --git a/sys/help/Overview b/sys/help/Overview new file mode 100644 index 0000000..d39a4dd --- /dev/null +++ b/sys/help/Overview @@ -0,0 +1,15 @@ +Overview is the main application launcher. + +Shortcut keys +============= + * s: Shell + * l: Lua application + * f: Files + * e: Edit an application (or right-click) + * control-n: Add a new application + * delete: Delete an application + +Adding a new application +======================== +The run entry can be either a disk file or a URL. +Icons must be in NFT format with a height of 3 and a width of 3 to 8 characters. \ No newline at end of file diff --git a/sys/help/pastebin b/sys/help/pastebin new file mode 100644 index 0000000..57e4d0a --- /dev/null +++ b/sys/help/pastebin @@ -0,0 +1,15 @@ +pastebin is a program for uploading files to and downloading files from pastebin.com. This is useful for sharing programs with other players. +The HTTP API must be enabled in ComputerCraft.cfg to use this program. + +ex: +"pastebin put foo" will upload the file "foo" to pastebin.com, and print the URL. +"pastebin get xq5gc7LB foo" will download the file from the URL http://pastebin.com/xq5gc7LB, and save it as "foo". +"pastebin run CxaWmPrX" will download the file from the URL http://pastebin.com/CxaWmPrX, and immediately run it. + +Functions in the pastebin API: +pastebin.get( code, filepath ) +pastebin.put( filepath ) +pastebin.download( code ) +pastebin.upload( pastename, text ) +pastebin.parseCode( code ) + diff --git a/sys/init/1.device.lua b/sys/init/1.device.lua new file mode 100644 index 0000000..b61a767 --- /dev/null +++ b/sys/init/1.device.lua @@ -0,0 +1,108 @@ +_G.requireInjector(_ENV) + +local Peripheral = require('opus.peripheral') + +_G.device = Peripheral.getList() + +_G.device.terminal = _G.kernel.terminal +_G.device.terminal.side = 'terminal' +_G.device.terminal.type = 'terminal' +_G.device.terminal.name = 'terminal' + +_G.device.keyboard = { + side = 'keyboard', + type = 'keyboard', + name = 'keyboard', + hotkeys = { }, + state = { }, +} + +_G.device.mouse = { + side = 'mouse', + type = 'mouse', + name = 'mouse', + state = { }, +} + +local Input = require('opus.input') +local Util = require('opus.util') + +local device = _G.device +local kernel = _G.kernel +local keyboard = _G.device.keyboard +local keys = _G.keys +local mouse = _G.device.mouse +local os = _G.os + +kernel.hook('peripheral', function(_, eventData) + local side = eventData[1] + if side then + local dev = Peripheral.addDevice(device, side) + if dev then + os.queueEvent('device_attach', dev.name) + end + end +end) + +kernel.hook('peripheral_detach', function(_, eventData) + local side = eventData[1] + if side then + for _, dev in pairs(Util.findAll(device, 'side', side)) do + os.queueEvent('device_detach', dev.name) + device[dev.name] = nil + end + end +end) + +local modifiers = Util.transpose { + keys.leftCtrl, keys.rightCtrl, + keys.leftShift, keys.rightShift, + keys.leftAlt, keys.rightAlt, +} + +kernel.hook({ 'key', 'char', 'paste' }, function(event, eventData) + local code = eventData[1] + + -- maintain global keyboard modifier state + if event == 'key' and modifiers[code] then + keyboard.state[code] = true + end + + -- and fire hotkeys + local hotkey = Input:translate(event, eventData[1], eventData[2]) + + if hotkey and keyboard.hotkeys[hotkey.code] then + keyboard.hotkeys[hotkey.code](event, eventData) + return true + end +end) + +kernel.hook('key_up', function(_, eventData) + local code = eventData[1] + + if modifiers[code] then + keyboard.state[code] = nil + end +end) + +kernel.hook({ 'mouse_click', 'mouse_up', 'mouse_drag' }, function(event, eventData) + local button = eventData[1] + if event == 'mouse_click' then + mouse.state[button] = true + else + if not mouse.state[button] then + return true -- ensure mouse ups are only generated if a mouse down was sent + end + if event == 'mouse_up' then + mouse.state[button] = nil + end + end +end) + +function keyboard.addHotkey(code, fn) + keyboard.hotkeys[code] = fn +end + +function keyboard.removeHotkey(code) + keyboard.hotkeys[code] = nil +end diff --git a/sys/extensions/2.vfs.lua b/sys/init/2.vfs.lua similarity index 91% rename from sys/extensions/2.vfs.lua rename to sys/init/2.vfs.lua index 3558c60..5b3c5f9 100644 --- a/sys/extensions/2.vfs.lua +++ b/sys/init/2.vfs.lua @@ -1,11 +1,13 @@ +local fs = _G.fs + if fs.native then return end _G.requireInjector(_ENV) -local Util = require('util') +local Util = require('opus.util') -local fs = _G.fs +-- TODO: support getDrive for virtual nodes fs.native = Util.shallowCopy(fs) @@ -14,7 +16,7 @@ local nativefs = { } for k,fn in pairs(fs) do if type(fn) == 'function' then - nativefs[k] = function(node, ...) + nativefs[k] = function(_, ...) return fn(...) end end @@ -88,6 +90,13 @@ function nativefs.exists(node, dir) return fs.native.exists(dir) end +function nativefs.getDrive(node, dir) + if node.mountPoint == dir then + return fs.native.getDrive(dir) or 'virt' + end + return fs.native.getDrive(dir) +end + function nativefs.delete(node, dir) if node.mountPoint == dir then fs.unmount(dir) @@ -113,6 +122,7 @@ local function splitpath(path) end local function getNode(dir) + if not dir then error('Invalid directory', 2) end local cd = fs.combine(dir, '') local parts = splitpath(cd) local node = fs.nodes @@ -176,6 +186,8 @@ function fs.listEx(dir) end function fs.copy(s, t) + if not s then error('copy: bad argument #1') end + if not t then error('copy: bad argument #2') end local sp = getNode(s) local tp = getNode(t) if sp == tp and sp.fs.copy then @@ -208,6 +220,8 @@ function fs.find(spec) -- not optimized -- local files = node.fs.find(node, spec) local files = { } -- method from https://github.com/N70/deltaOS/blob/dev/vfs + + -- REVISIT - see globbing in shellex package local function recurse_spec(results, path, spec) local segment = spec:match('([^/]*)'):gsub('/', '') local pattern = '^' .. segment:gsub("[%.%[%]%(%)%%%+%-%?%^%$]","%%%1"):gsub("%z","%%z"):gsub("%*","[^/]-") .. '$' @@ -244,7 +258,7 @@ end local function getfstype(fstype) local vfs = fstypes[fstype] if not vfs then - vfs = require('fs.' .. fstype) + vfs = require('opus.fs.' .. fstype) fs.registerType(fstype, vfs) end return vfs @@ -291,7 +305,8 @@ function fs.loadTab(path) local mounts = Util.readFile(path) if mounts then for _,l in ipairs(Util.split(mounts)) do - if l:sub(1, 1) ~= '#' then + l = Util.trim(l) + if #l > 0 and l:sub(1, 1) ~= '#' then local s, m = pcall(function() fs.mount(table.unpack(Util.matches(l))) end) @@ -328,8 +343,8 @@ function fs.unmount(path) end end -function fs.registerType(name, fs) - fstypes[name] = fs +function fs.registerType(name, vfs) + fstypes[name] = vfs end function fs.getTypes() diff --git a/sys/init/3.modules.lua b/sys/init/3.modules.lua new file mode 100644 index 0000000..c01ae43 --- /dev/null +++ b/sys/init/3.modules.lua @@ -0,0 +1,138 @@ +local Util = require('opus.util') + +local device = _G.device +local kernel = _G.kernel +local os = _G.os + +local containers = { + manipulator = true, + neuralInterface = true, +} + +local cache = { } + +-- manipulators will throw an error on listModules +-- if the user has logged off +local function getModules(dev, side) + local list = { } + local s, m = pcall(function() + if dev and dev.listModules then + for _, module in pairs(dev.listModules()) do + list[module] = Util.shallowCopy(dev) + list[module].name = module + list[module].type = module + list[module].side = side + end + end + end) + if not s and m then + _G._syslog(m) + end + return list +end + +-- if a device has been reattached, reuse the existing +-- table so any references to the table are retained +local function addDevice(dev, args, doQueue) + local name = args.name + + if not cache[name] then + cache[name] = { } + end + device[name] = cache[name] + Util.merge(device[name], dev) + Util.merge(device[name], args) + + if doQueue then + os.queueEvent('device_attach', name) + end +end + +-- directly access the peripheral as the methods in getInventory, etc. +-- can become invalid without any way to tell +local function damnManipulator(container, method, args, doQueue) + local dev = { } + local methods = { + 'drop', 'getDocs', 'getItem', 'getItemMeta', 'getTransferLocations', + 'list', 'pullItems', 'pushItems', 'size', 'suck', + } + -- the user might not be logged in when the compputer is started + -- and there's no way to know when they have logged in. + -- these methods will error if the user is not logged in + if container[method] then + for _,k in pairs(methods) do + dev[k] = function(...) + return device[container.name][method]()[k](...) + end + end + + addDevice(dev, args, doQueue) + end +end + +local function addContainer(v, doQueue) + -- add devices like plethora:scanner + for name, dev in pairs(getModules(v, v.side)) do + -- neural and attached modules have precedence over manipulator modules + if not device[name] or v.type ~= 'manipulator' then + addDevice(dev, { name = dev.name, type = dev.name, side = dev.side }, doQueue) + end + end + + if v.getName then + local s, m = pcall(function() + local name = v.getName() + if name then + damnManipulator(v, 'getInventory', { + name = name .. ':inventory', + type = 'inventory', + side = v.side + }, doQueue) + damnManipulator(v, 'getEquipment', { + name = name .. ':equipment', + type = 'equipment', + side = v.side + }, doQueue) + damnManipulator(v, 'getEnder', { + name = name .. ':enderChest', + type = 'enderChest', + side = v.side + }, doQueue) + end + end) + if not s and m then + _G._syslog(m) + end + end +end + +for k,v in pairs(device) do + if containers[v.type] then + cache[k] = v + addContainer(v) + end +end + +-- register modules as peripherals +kernel.hook('device_attach', function(_, eventData) + local name = eventData[1] + local dev = device[name] + + if dev and containers[dev.type] then + -- so... basically, if you get a handle to device.neuralInterface + -- (or manipulator) - that handle will still be valid after + -- a module is removed + if cache[name] then + device[name] = cache[name] + for k,v in pairs(device[name]) do + if type(v) == 'function' then + device[name][k] = nil + end + end + Util.merge(device[name], dev) + else + cache[name] = dev + end + addContainer(dev, true) + end +end) diff --git a/sys/init/3.relay.lua b/sys/init/3.relay.lua new file mode 100644 index 0000000..1b24f07 --- /dev/null +++ b/sys/init/3.relay.lua @@ -0,0 +1,26 @@ +local device = _G.device +local kernel = _G.kernel + +local function register(v) + if v and v.isWireless and v.isAccessPoint and v.getNamesRemote then + v._children = { } + for _, name in pairs(v.getNamesRemote()) do + local dev = v.getMethodsRemote(name) + if dev then + dev.name = name + dev.side = v.side + dev.type = v.getTypeRemote(name) + device[name] = dev + end + end + end +end + +for _,v in pairs(device) do + register(v) +end + +-- register oc devices as peripherals +kernel.hook('device_attach', function(_, eventData) + register(device[eventData[1]]) +end) diff --git a/sys/init/3.sys.lua b/sys/init/3.sys.lua new file mode 100644 index 0000000..7e18d18 --- /dev/null +++ b/sys/init/3.sys.lua @@ -0,0 +1,3 @@ +local fs = _G.fs + +fs.loadTab('sys/etc/fstab') diff --git a/sys/extensions/4.label.lua b/sys/init/4.label.lua similarity index 68% rename from sys/extensions/4.label.lua rename to sys/init/4.label.lua index 0a34862..dfdb89c 100644 --- a/sys/extensions/4.label.lua +++ b/sys/init/4.label.lua @@ -1,14 +1,22 @@ -local os = _G.os +local os = _G.os +local peripheral = _G.peripheral -- Default label if not os.getComputerLabel() then local id = os.getComputerID() + if _G.turtle then os.setComputerLabel('turtle_' .. id) + elseif _G.pocket then os.setComputerLabel('pocket_' .. id) + elseif _G.commands then os.setComputerLabel('command_' .. id) + + elseif peripheral.find('neuralInterface') then + os.setComputerLabel('neural_' .. id) + else os.setComputerLabel('computer_' .. id) end diff --git a/sys/init/4.user.lua b/sys/init/4.user.lua new file mode 100644 index 0000000..4decc1f --- /dev/null +++ b/sys/init/4.user.lua @@ -0,0 +1,49 @@ +local Util = require('opus.util') + +local fs = _G.fs +local shell = _ENV.shell + +if not fs.exists('usr/apps') then + fs.makeDir('usr/apps') +end +if not fs.exists('usr/autorun') then + fs.makeDir('usr/autorun') +end + +-- TODO: Temporary +local upgrade = Util.readTable('usr/config/shell') +if upgrade and (not upgrade.upgraded or upgrade.upgraded ~= 1) then + fs.delete('usr/config/shell') +end + +if not fs.exists('usr/config/shell') then + Util.writeTable('usr/config/shell', { + aliases = shell.aliases(), + path = '/usr/apps', + lua_path = package.path, + upgraded = 1, + }) +end + +local config = Util.readTable('usr/config/shell') +if config.aliases then + for k in pairs(shell.aliases()) do + shell.clearAlias(k) + end + for k,v in pairs(config.aliases) do + shell.setAlias(k, v) + end +end + +local path = config.path and Util.split(config.path, '(.-):') or { } +table.insert(path, '/sys/apps') +for _, v in pairs(Util.split(shell.path(), '(.-):')) do + table.insert(path, v) +end + +shell.setPath(table.concat(path, ':')) + +--_G.LUA_PATH = config.lua_path +--_G.settings.set('mbs.shell.require_path', config.lua_path) + +fs.loadTab('usr/config/fstab') diff --git a/sys/extensions/5.network.lua b/sys/init/5.network.lua similarity index 71% rename from sys/extensions/5.network.lua rename to sys/init/5.network.lua index 3963ae2..b02937a 100644 --- a/sys/extensions/5.network.lua +++ b/sys/init/5.network.lua @@ -1,12 +1,20 @@ _G.requireInjector(_ENV) -local Config = require('config') +local Config = require('opus.config') local device = _G.device local kernel = _G.kernel local os = _G.os -_G.network = { } +do + local config = Config.load('os') + _G.network = setmetatable({ }, { __index = { + getGroup = function() return config.group end, + setGroup = function(name) + config.group = name + end + }}) +end local function startNetwork() kernel.run({ @@ -18,8 +26,12 @@ end local function setModem(dev) if not device.wireless_modem and dev.isWireless() then - local config = Config.load('os', { }) - if not config.wirelessModem or dev.name == config.wirelessModem then + local config = Config.load('os') + + if not config.wirelessModem or + config.wirelessModem == 'auto' or + dev.name == config.wirelessModem then + device.wireless_modem = dev os.queueEvent('device_attach', 'wireless_modem') return dev @@ -27,7 +39,7 @@ local function setModem(dev) end end --- create a psuedo-device named 'wireleess_modem' +-- create a psuedo-device named 'wireless_modem' kernel.hook('device_attach', function(_, eventData) local dev = device[eventData[1]] if dev and dev.type == 'modem' then diff --git a/sys/init/6.packages.lua b/sys/init/6.packages.lua new file mode 100644 index 0000000..7d06abe --- /dev/null +++ b/sys/init/6.packages.lua @@ -0,0 +1,34 @@ +local Packages = require('opus.packages') +local Util = require('opus.util') + +local fs = _G.fs +local help = _G.help +local shell = _ENV.shell + +local appPaths = Util.split(shell.path(), '(.-):') +local helpPaths = Util.split(help.path(), '(.-):') + +table.insert(helpPaths, '/sys/help') + +for name in pairs(Packages:installed()) do + local packageDir = fs.combine('packages', name) + + table.insert(appPaths, 1, '/' .. packageDir) + local apiPath = fs.combine(packageDir, 'apis') + if fs.exists(apiPath) then + fs.mount(fs.combine('rom/modules/main', name), 'linkfs', apiPath) + end + + local helpPath = '/' .. fs.combine(packageDir, 'help') + if fs.exists(helpPath) then + table.insert(helpPaths, helpPath) + end + + local fstabPath = fs.combine(packageDir, 'etc/fstab') + if fs.exists(fstabPath) then + fs.loadTab(fstabPath) + end +end + +help.setPath(table.concat(helpPaths, ':')) +shell.setPath(table.concat(appPaths, ':')) diff --git a/sys/extensions/7.multishell.lua b/sys/init/7.multishell.lua similarity index 78% rename from sys/extensions/7.multishell.lua rename to sys/init/7.multishell.lua index 4da07e8..d8b722a 100644 --- a/sys/extensions/7.multishell.lua +++ b/sys/init/7.multishell.lua @@ -1,8 +1,8 @@ _G.requireInjector(_ENV) -local Config = require('config') -local Packages = require('packages') -local Util = require('util') +local Config = require('opus.config') +local trace = require('opus.trace') +local Util = require('opus.util') local colors = _G.colors local fs = _G.fs @@ -11,7 +11,6 @@ local keys = _G.keys local os = _G.os local printError = _G.printError local shell = _ENV.shell -local term = _G.term local window = _G.window local parentTerm = _G.device.terminal @@ -23,7 +22,7 @@ local multishell = { } shell.setEnv('multishell', multishell) -multishell.term = parentTerm --deprecated +multishell.term = parentTerm --deprecated use device.terminal local config = { standard = { @@ -104,6 +103,15 @@ function multishell.launch( tProgramEnv, sProgramPath, ... ) }) 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 + function multishell.openTab(tab) if not tab.title and tab.path then tab.title = fs.getName(tab.path):match('([^%.]+)') @@ -120,15 +128,20 @@ function multishell.openTab(tab) if tab.fn then result, err = Util.runFunction(routine.env, tab.fn, table.unpack(tab.args or { } )) elseif tab.path then - result, err = Util.run(routine.env, tab.path, table.unpack(tab.args or { } )) + result, err = xprun(routine.env, tab.path, table.unpack(tab.args or { } )) else err = 'multishell: invalid tab' end - if not result and err and err ~= 'Terminated' then - if err then + if not result and err and err ~= 'Terminated' or (err and err ~= 0) then + tab.terminal.setBackgroundColor(colors.black) + if tonumber(err) then + tab.terminal.setTextColor(colors.orange) + print('Process exited with error code: ' .. err) + elseif err then printError(tostring(err)) end + tab.terminal.setTextColor(colors.white) print('\nPress enter to close') routine.isDead = true routine.hidden = false @@ -173,23 +186,7 @@ function multishell.getCount() return #kernel.routines end -kernel.hook('kernel_focus', function(_, eventData) - local previous = eventData[2] - if previous then - local routine = kernel.find(previous) - if routine and routine.window then - routine.window.setVisible(false) - if routine.hidden then - kernel.lower(previous) - end - end - end - - local focused = kernel.find(eventData[1]) - if focused and focused.window then - focused.window.setVisible(true) - end - +kernel.hook('kernel_focus', function() redrawMenu() end) @@ -267,7 +264,7 @@ kernel.hook('multishell_redraw', function() write(currentTab.sx - 1, ' ' .. currentTab.title:sub(1, currentTab.width - 1) .. ' ', _colors.focusBackgroundColor, _colors.focusTextColor) - if not currentTab.isOverview then + if not currentTab.noTerminate then write(w, closeInd, _colors.backgroundColor, _colors.focusTextColor) end end @@ -336,69 +333,22 @@ kernel.hook('mouse_scroll', function(_, eventData) eventData[3] = eventData[3] - 1 end) -local function startup() - local success = true - - local function runDir(directory, open) - if not fs.exists(directory) then - return true - end - - local files = fs.list(directory) - table.sort(files) - - for _,file in ipairs(files) do - os.sleep(0) - local result, err = open(directory .. '/' .. file) - - if result then - if term.isColor() then - term.setTextColor(colors.green) - end - term.write('[PASS] ') - term.setTextColor(colors.white) - term.write(fs.combine(directory, file)) - print() - else - if term.isColor() then - term.setTextColor(colors.red) - end - term.write('[FAIL] ') - term.setTextColor(colors.white) - term.write(fs.combine(directory, file)) - if err then - _G.printError('\n' .. err) - end - print() - success = false - end - end - end - - runDir('sys/autorun', shell.run) - for name in pairs(Packages:installed()) do - local packageDir = 'packages/' .. name .. '/autorun' - runDir(packageDir, shell.run) - end - runDir('usr/autorun', shell.run) - - if not success then - multishell.setFocus(multishell.getCurrent()) - printError('\nA startup program has errored') - os.pullEvent('terminate') - end -end - kernel.hook('kernel_ready', function() + local env = Util.shallowCopy(shell.getEnv()) + _G.requireInjector(env) + overviewId = multishell.openTab({ - path = 'sys/apps/Overview.lua', + path = config.launcher or 'sys/apps/Overview.lua', isOverview = true, + noTerminate = true, focused = true, title = '+', + env = env, }) multishell.openTab({ - fn = startup, + path = 'sys/apps/shell.lua', + args = { 'sys/apps/autorun.lua' }, title = 'Autorun', }) end) diff --git a/sys/kernel.lua b/sys/kernel.lua index b686bd8..b2ef8a8 100644 --- a/sys/kernel.lua +++ b/sys/kernel.lua @@ -1,7 +1,8 @@ _G.requireInjector(_ENV) -local Terminal = require('terminal') -local Util = require('util') +local Array = require('opus.array') +local Terminal = require('opus.terminal') +local Util = require('opus.util') _G.kernel = { UID = 0, @@ -18,9 +19,9 @@ local window = _G.window local w, h = term.getSize() kernel.terminal = term.current() -kernel.window = window.create(kernel.terminal, 1, 1, w, h, false) -Terminal.scrollable(kernel.window) +kernel.window = Terminal.window(kernel.terminal, 1, 1, w, h, false) +kernel.window.setMaxScroll(100) local focusedRoutineEvents = Util.transpose { 'char', 'key', 'key_up', @@ -28,18 +29,13 @@ local focusedRoutineEvents = Util.transpose { 'paste', 'terminate', } -_G._debug = function(pattern, ...) +_G._syslog = function(pattern, ...) local oldTerm = term.redirect(kernel.window) + kernel.window.scrollBottom() Util.print(pattern, ...) term.redirect(oldTerm) end -if not _G.debug then -- don't clobber lua debugger - function _G.debug(...) - _G._debug(...) - end -end - -- any function that runs in a kernel hook does not run in -- a separate coroutine or have a window. an error in a hook -- function will crash the system. @@ -60,7 +56,7 @@ end function kernel.unhook(event, fn) local eventHooks = kernel.hooks[event] if eventHooks then - Util.removeByValue(eventHooks, fn) + Array.removeByValue(eventHooks, fn) if #eventHooks == 0 then kernel.hooks[event] = nil end @@ -69,6 +65,23 @@ end local Routine = { } +local function switch(routine, previous) + if routine then + if previous and previous.window then + previous.window.setVisible(false) + if previous.hidden then + kernel.lower(previous.uid) + end + end + + if routine and routine.window then + routine.window.setVisible(true) + end + + os.queueEvent('kernel_focus', routine.uid, previous and previous.uid) + end +end + function Routine:resume(event, ...) if not self.co or coroutine.status(self.co) == 'dead' then return @@ -92,12 +105,12 @@ function Routine:resume(event, ...) term.redirect(previousTerm) if not ok and self.haltOnError then - error(result) + error(result, -1) end if coroutine.status(self.co) == 'dead' then - Util.removeByValue(kernel.routines, self) + Array.removeByValue(kernel.routines, self) if #kernel.routines > 0 then - os.queueEvent('kernel_focus', kernel.routines[1].uid) + switch(kernel.routines[1]) end if self.haltOnExit then kernel.halt() @@ -115,6 +128,10 @@ function kernel.getCurrent() return kernel.running end +function kernel.getShell() + return shell +end + function kernel.newRoutine(args) kernel.UID = kernel.UID + 1 @@ -123,6 +140,7 @@ function kernel.newRoutine(args) timestamp = os.clock(), terminal = kernel.window, window = kernel.window, + title = 'untitled', }, { __index = Routine }) Util.merge(routine, args) @@ -162,15 +180,22 @@ function kernel.run(args) end function kernel.raise(uid) + if kernel.getFocused() and kernel.getFocused().pinned then + return false + end + local routine = Util.find(kernel.routines, 'uid', uid) if routine then local previous = kernel.routines[1] if routine ~= previous then - Util.removeByValue(kernel.routines, routine) + Array.removeByValue(kernel.routines, routine) table.insert(kernel.routines, 1, routine) end - os.queueEvent('kernel_focus', routine.uid, previous and previous.uid) + + switch(routine, previous) +-- local previous = eventData[2] +-- local routine = kernel.find(previous) return true end return false @@ -187,7 +212,7 @@ function kernel.lower(uid) end end - Util.removeByValue(kernel.routines, routine) + Array.removeByValue(kernel.routines, routine) table.insert(kernel.routines, routine) return true end @@ -205,7 +230,17 @@ end function kernel.event(event, eventData) local stopPropagation - local eventHooks = kernel.hooks[event] + local eventHooks = kernel.hooks['*'] + if eventHooks then + for i = #eventHooks, 1, -1 do + stopPropagation = eventHooks[i](event, eventData) + if stopPropagation then + break + end + end + end + + eventHooks = kernel.hooks[event] if eventHooks then for i = #eventHooks, 1, -1 do stopPropagation = eventHooks[i](event, eventData) @@ -254,7 +289,7 @@ local function init(...) local runLevel = #args > 0 and 6 or 7 print('Starting Opus OS') - local dir = 'sys/extensions' + local dir = 'sys/init' local files = fs.list(dir) table.sort(files) for _,file in ipairs(files) do @@ -272,13 +307,19 @@ local function init(...) if args[1] then kernel.hook('kernel_ready', function() + + term.redirect(kernel.window) + shell.run('sys/apps/autorun.lua') + + local shellWindow = window.create(kernel.terminal, 1, 1, w, h, false) local s, m = kernel.run({ title = args[1], - path = 'sys/apps/shell', + path = 'sys/apps/shell.lua', args = args, haltOnExit = true, haltOnError = true, - terminal = kernel.terminal, + terminal = shellWindow, + window = shellWindow, }) if s then kernel.raise(s.uid) diff --git a/sys/modules/opus/alternate.lua b/sys/modules/opus/alternate.lua new file mode 100644 index 0000000..101de58 --- /dev/null +++ b/sys/modules/opus/alternate.lua @@ -0,0 +1,48 @@ +local Array = require('opus.array') +local Config = require('opus.config') +local Util = require('opus.util') + +local function getConfig() + return Config.load('alternate', { + shell = { + 'sys/apps/shell.lua', + 'rom/programs/shell.lua', + }, + lua = { + 'sys/apps/Lua.lua', + 'rom/programs/lua.lua', + }, + files = { + 'sys/apps/Files.lua', + } + }) +end + +local Alt = { } + +function Alt.get(key) + return getConfig()[key][1] +end + +function Alt.set(key, value) + local config = getConfig() + Array.removeByValue(config[key], value) + table.insert(config[key], 1, value) + Config.update('alternate', config) +end + +function Alt.remove(key, value) + local config = getConfig() + Array.removeByValue(config[key], value) + Config.update('alternate', config) +end + +function Alt.add(key, value) + local config = getConfig() + if not Util.contains(config[key], value) then + table.insert(config[key], value) + Config.update('alternate', config) + end +end + +return Alt diff --git a/sys/apis/ansi.lua b/sys/modules/opus/ansi.lua similarity index 100% rename from sys/apis/ansi.lua rename to sys/modules/opus/ansi.lua diff --git a/sys/modules/opus/array.lua b/sys/modules/opus/array.lua new file mode 100644 index 0000000..f0aa73f --- /dev/null +++ b/sys/modules/opus/array.lua @@ -0,0 +1,22 @@ +local Array = { } + +function Array.filter(it, f) + local ot = { } + for _,v in pairs(it) do + if f(v) then + table.insert(ot, v) + end + end + return ot +end + +function Array.removeByValue(t, e) + for k,v in pairs(t) do + if v == e then + table.remove(t, k) + break + end + end +end + +return Array diff --git a/sys/modules/opus/bulkget.lua b/sys/modules/opus/bulkget.lua new file mode 100644 index 0000000..29eadcf --- /dev/null +++ b/sys/modules/opus/bulkget.lua @@ -0,0 +1,33 @@ +local Util = require('opus.util') + +local parallel = _G.parallel + +local BulkGet = { } + +function BulkGet.download(list, callback) + local t = { } + local failed = false + + for _ = 1, 5 do + table.insert(t, function() + while true do + local entry = table.remove(list) + if not entry then + break + end + local s, m = Util.download(entry.url, entry.path) + if not s then + failed = true + end + callback(entry, s, m) + if failed then + break + end + end + end) + end + + parallel.waitForAll(table.unpack(t)) +end + +return BulkGet diff --git a/sys/modules/opus/cbor.lua b/sys/modules/opus/cbor.lua new file mode 100644 index 0000000..95e41ca --- /dev/null +++ b/sys/modules/opus/cbor.lua @@ -0,0 +1,581 @@ +-- Concise Binary Object Representation (CBOR) +-- RFC 7049 + +local function softreq(pkg, field) + local ok, mod = pcall(require, pkg); + if not ok then return end + if field then return mod[field]; end + return mod; +end +local dostring = function (s) + local ok, f = pcall(loadstring or load, s); -- luacheck: read globals loadstring + if ok and f then return f(); end +end + +local setmetatable = setmetatable; +local getmetatable = getmetatable; +local dbg_getmetatable = debug and debug.getmetatable; +local assert = assert; +local error = error; +local type = type; +local pairs = pairs; +local ipairs = ipairs; +local tostring = tostring; +local s_char = string.char; +local t_concat = table.concat; +local t_sort = table.sort; +local m_floor = math.floor; +local m_abs = math.abs; +local m_huge = math.huge; +local m_max = math.max; +local maxint = math.maxinteger or 9007199254740992; +local minint = math.mininteger or -9007199254740992; +local NaN = 0/0; +local m_frexp = math.frexp; +local m_ldexp = math.ldexp or function (x, exp) return x * 2.0 ^ exp; end; +local m_type = math.type or function (n) return n % 1 == 0 and n <= maxint and n >= minint and "integer" or "float" end; +local s_pack = string.pack or softreq("struct", "pack"); +local s_unpack = string.unpack or softreq("struct", "unpack"); +local b_rshift = softreq("bit32", "rshift") or softreq("bit", "rshift") or + dostring "return function(a,b) return a >> b end" or + function (a, b) return m_max(0, m_floor(a / (2 ^ b))); end; + +-- sanity check +if s_pack and s_pack(">I2", 0) ~= "\0\0" then + s_pack = nil; +end +if s_unpack and s_unpack(">I2", "\1\2\3\4") ~= 0x102 then + s_unpack = nil; +end + +local _ENV = nil; -- luacheck: ignore 211 + +local encoder = {}; + +local function encode(obj, opts) + return encoder[type(obj)](obj, opts); +end + +-- Major types 0, 1 and length encoding for others +local function integer(num, m) + if m == 0 and num < 0 then + -- negative integer, major type 1 + num, m = - num - 1, 32; + end + if num < 24 then + return s_char(m + num); + elseif num < 2 ^ 8 then + return s_char(m + 24, num); + elseif num < 2 ^ 16 then + return s_char(m + 25, b_rshift(num, 8), num % 0x100); + elseif num < 2 ^ 32 then + return s_char(m + 26, + b_rshift(num, 24) % 0x100, + b_rshift(num, 16) % 0x100, + b_rshift(num, 8) % 0x100, + num % 0x100); + elseif num < 2 ^ 64 then + local high = m_floor(num / 2 ^ 32); + num = num % 2 ^ 32; + return s_char(m + 27, + b_rshift(high, 24) % 0x100, + b_rshift(high, 16) % 0x100, + b_rshift(high, 8) % 0x100, + high % 0x100, + b_rshift(num, 24) % 0x100, + b_rshift(num, 16) % 0x100, + b_rshift(num, 8) % 0x100, + num % 0x100); + end + error "int too large"; +end + +if s_pack then + function integer(num, m) + local fmt; + m = m or 0; + if num < 24 then + fmt, m = ">B", m + num; + elseif num < 256 then + fmt, m = ">BB", m + 24; + elseif num < 65536 then + fmt, m = ">BI2", m + 25; + elseif num < 4294967296 then + fmt, m = ">BI4", m + 26; + else + fmt, m = ">BI8", m + 27; + end + return s_pack(fmt, m, num); + end +end + +local simple_mt = {}; +function simple_mt:__tostring() return self.name or ("simple(%d)"):format(self.value); end +function simple_mt:__tocbor() return self.cbor or integer(self.value, 224); end + +local function simple(value, name, cbor) + assert(value >= 0 and value <= 255, "bad argument #1 to 'simple' (integer in range 0..255 expected)"); + return setmetatable({ value = value, name = name, cbor = cbor }, simple_mt); +end + +local tagged_mt = {}; +function tagged_mt:__tostring() return ("%d(%s)"):format(self.tag, tostring(self.value)); end +function tagged_mt:__tocbor() return integer(self.tag, 192) .. encode(self.value); end + +local function tagged(tag, value) + assert(tag >= 0, "bad argument #1 to 'tagged' (positive integer expected)"); + return setmetatable({ tag = tag, value = value }, tagged_mt); +end + +local null = simple(22, "null"); -- explicit null +local undefined = simple(23, "undefined"); -- undefined or nil +local BREAK = simple(31, "break", "\255"); + +-- Number types dispatch +function encoder.number(num) + return encoder[m_type(num)](num); +end + +-- Major types 0, 1 +function encoder.integer(num) + if num < 0 then + return integer(-1 - num, 32); + end + return integer(num, 0); +end + +-- Major type 7 +function encoder.float(num) + if num ~= num then -- NaN shortcut + return "\251\127\255\255\255\255\255\255\255"; + end + local sign = (num > 0 or 1 / num > 0) and 0 or 1; + num = m_abs(num) + if num == m_huge then + return s_char(251, sign * 128 + 128 - 1) .. "\240\0\0\0\0\0\0"; + end + local fraction, exponent = m_frexp(num) + if fraction == 0 then + return s_char(251, sign * 128) .. "\0\0\0\0\0\0\0"; + end + fraction = fraction * 2; + exponent = exponent + 1024 - 2; + if exponent <= 0 then + fraction = fraction * 2 ^ (exponent - 1) + exponent = 0; + else + fraction = fraction - 1; + end + return s_char(251, + sign * 2 ^ 7 + m_floor(exponent / 2 ^ 4) % 2 ^ 7, + exponent % 2 ^ 4 * 2 ^ 4 + + m_floor(fraction * 2 ^ 4 % 0x100), + m_floor(fraction * 2 ^ 12 % 0x100), + m_floor(fraction * 2 ^ 20 % 0x100), + m_floor(fraction * 2 ^ 28 % 0x100), + m_floor(fraction * 2 ^ 36 % 0x100), + m_floor(fraction * 2 ^ 44 % 0x100), + m_floor(fraction * 2 ^ 52 % 0x100) + ) +end + +if s_pack then + function encoder.float(num) + return s_pack(">Bd", 251, num); + end +end + + +-- Major type 2 - byte strings +function encoder.bytestring(s) + return integer(#s, 64) .. s; +end + +-- Major type 3 - UTF-8 strings +function encoder.utf8string(s) + return integer(#s, 96) .. s; +end + +-- Lua strings are byte strings +encoder.string = encoder.bytestring; + +function encoder.boolean(bool) + return bool and "\245" or "\244"; +end + +encoder["nil"] = function() return "\246"; end + +function encoder.userdata(ud, opts) + local mt = dbg_getmetatable(ud); + if mt then + local encode_ud = opts and opts[mt] or mt.__tocbor; + if encode_ud then + return encode_ud(ud, opts); + end + end + error "can't encode userdata"; +end + +function encoder.table(t, opts) + local mt = getmetatable(t); + if mt then + local encode_t = opts and opts[mt] or mt.__tocbor; + if encode_t then + return encode_t(t, opts); + end + end + -- the table is encoded as an array iff when we iterate over it, + -- we see successive integer keys starting from 1. The lua + -- language doesn't actually guarantee that this will be the case + -- when we iterate over a table with successive integer keys, but + -- due an implementation detail in PUC Rio Lua, this is what we + -- usually observe. See the Lua manual regarding the # (length) + -- operator. In the case that this does not happen, we will fall + -- back to a map with integer keys, which becomes a bit larger. + local array, map, i, p = { integer(#t, 128) }, { "\191" }, 1, 2; + local is_array = true; + for k, v in pairs(t) do + is_array = is_array and i == k; + i = i + 1; + + local encoded_v = encode(v, opts); + array[i] = encoded_v; + + map[p], p = encode(k, opts), p + 1; + map[p], p = encoded_v, p + 1; + end + -- map[p] = "\255"; + map[1] = integer(i - 1, 160); + return t_concat(is_array and array or map); +end + +-- Array or dict-only encoders, which can be set as __tocbor metamethod +function encoder.array(t, opts) + local array = { }; + for i, v in ipairs(t) do + array[i] = encode(v, opts); + end + return integer(#array, 128) .. t_concat(array); +end + +function encoder.map(t, opts) + local map, p, len = { "\191" }, 2, 0; + for k, v in pairs(t) do + map[p], p = encode(k, opts), p + 1; + map[p], p = encode(v, opts), p + 1; + len = len + 1; + end + -- map[p] = "\255"; + map[1] = integer(len, 160); + return t_concat(map); +end +encoder.dict = encoder.map; -- COMPAT + +function encoder.ordered_map(t, opts) + local map = {}; + if not t[1] then -- no predefined order + local i = 0; + for k in pairs(t) do + i = i + 1; + map[i] = k; + end + t_sort(map); + end + for i, k in ipairs(t[1] and t or map) do + map[i] = encode(k, opts) .. encode(t[k], opts); + end + return integer(#map, 160) .. t_concat(map); +end + +encoder["function"] = function () + error "can't encode function"; +end + +-- Decoder +-- Reads from a file-handle like object +local function read_bytes(fh, len) + return fh:read(len); +end + +local function read_byte(fh) + return fh:read(1):byte(); +end + +local function read_length(fh, mintyp) + if mintyp < 24 then + return mintyp; + elseif mintyp < 28 then + local out = 0; + for _ = 1, 2 ^ (mintyp - 24) do + out = out * 256 + read_byte(fh); + end + return out; + else + error "invalid length"; + end +end + +local decoder = {}; + +local function read_type(fh) + local byte = read_byte(fh); + return b_rshift(byte, 5), byte % 32; +end + +local function read_object(fh, opts) + local typ, mintyp = read_type(fh); + return decoder[typ](fh, mintyp, opts); +end + +local function read_integer(fh, mintyp) + return read_length(fh, mintyp); +end + +local function read_negative_integer(fh, mintyp) + return -1 - read_length(fh, mintyp); +end + +local function read_string(fh, mintyp) + if mintyp ~= 31 then + return read_bytes(fh, read_length(fh, mintyp)); + end + local out = {}; + local i = 1; + local v = read_object(fh); + while v ~= BREAK do + out[i], i = v, i + 1; + v = read_object(fh); + end + return t_concat(out); +end + +local function read_unicode_string(fh, mintyp) + return read_string(fh, mintyp); + -- local str = read_string(fh, mintyp); + -- if have_utf8 and not utf8.len(str) then + -- TODO How to handle this? + -- end + -- return str; +end + +local function read_array(fh, mintyp, opts) + local out = {}; + if mintyp == 31 then + local i = 1; + local v = read_object(fh, opts); + while v ~= BREAK do + out[i], i = v, i + 1; + v = read_object(fh, opts); + end + else + local len = read_length(fh, mintyp); + for i = 1, len do + out[i] = read_object(fh, opts); + end + end + return out; +end + +local function read_map(fh, mintyp, opts) + local out = {}; + local k; + if mintyp == 31 then + local i = 1; + k = read_object(fh, opts); + while k ~= BREAK do + out[k], i = read_object(fh, opts), i + 1; + k = read_object(fh, opts); + end + else + local len = read_length(fh, mintyp); + for _ = 1, len do + k = read_object(fh, opts); + out[k] = read_object(fh, opts); + end + end + return out; +end + +local tagged_decoders = {}; + +local function read_semantic(fh, mintyp, opts) + local tag = read_length(fh, mintyp); + local value = read_object(fh, opts); + local postproc = opts and opts[tag] or tagged_decoders[tag]; + if postproc then + return postproc(value); + end + return tagged(tag, value); +end + +local function read_half_float(fh) + local exponent = read_byte(fh); + local fraction = read_byte(fh); + local sign = exponent < 128 and 1 or -1; -- sign is highest bit + + fraction = fraction + (exponent * 256) % 1024; -- copy two(?) bits from exponent to fraction + exponent = b_rshift(exponent, 2) % 32; -- remove sign bit and two low bits from fraction; + + if exponent == 0 then + return sign * m_ldexp(fraction, -24); + elseif exponent ~= 31 then + return sign * m_ldexp(fraction + 1024, exponent - 25); + elseif fraction == 0 then + return sign * m_huge; + else + return NaN; + end +end + +local function read_float(fh) + local exponent = read_byte(fh); + local fraction = read_byte(fh); + local sign = exponent < 128 and 1 or -1; -- sign is highest bit + exponent = exponent * 2 % 256 + b_rshift(fraction, 7); + fraction = fraction % 128; + fraction = fraction * 256 + read_byte(fh); + fraction = fraction * 256 + read_byte(fh); + + if exponent == 0 then + return sign * m_ldexp(exponent, -149); + elseif exponent ~= 0xff then + return sign * m_ldexp(fraction + 2 ^ 23, exponent - 150); + elseif fraction == 0 then + return sign * m_huge; + else + return NaN; + end +end + +local function read_double(fh) + local exponent = read_byte(fh); + local fraction = read_byte(fh); + local sign = exponent < 128 and 1 or -1; -- sign is highest bit + + exponent = exponent % 128 * 16 + b_rshift(fraction, 4); + fraction = fraction % 16; + fraction = fraction * 256 + read_byte(fh); + fraction = fraction * 256 + read_byte(fh); + fraction = fraction * 256 + read_byte(fh); + fraction = fraction * 256 + read_byte(fh); + fraction = fraction * 256 + read_byte(fh); + fraction = fraction * 256 + read_byte(fh); + + if exponent == 0 then + return sign * m_ldexp(exponent, -149); + elseif exponent ~= 0xff then + return sign * m_ldexp(fraction + 2 ^ 52, exponent - 1075); + elseif fraction == 0 then + return sign * m_huge; + else + return NaN; + end +end + + +if s_unpack then + function read_float(fh) return s_unpack(">f", read_bytes(fh, 4)) end + function read_double(fh) return s_unpack(">d", read_bytes(fh, 8)) end +end + +local function read_simple(fh, value, opts) + if value == 24 then + value = read_byte(fh); + end + if value == 20 then + return false; + elseif value == 21 then + return true; + elseif value == 22 then + return null; + elseif value == 23 then + return undefined; + elseif value == 25 then + return read_half_float(fh); + elseif value == 26 then + return read_float(fh); + elseif value == 27 then + return read_double(fh); + elseif value == 31 then + return BREAK; + end + if opts and opts.simple then + return opts.simple(value); + end + return simple(value); +end + +decoder[0] = read_integer; +decoder[1] = read_negative_integer; +decoder[2] = read_string; +decoder[3] = read_unicode_string; +decoder[4] = read_array; +decoder[5] = read_map; +decoder[6] = read_semantic; +decoder[7] = read_simple; + +-- opts.more(n) -> want more data +-- opts.simple -> decode simple value +-- opts[int] -> tagged decoder +local function decode(s, opts) + local fh = {}; + local pos = 1; + + local more; + if type(opts) == "function" then + more = opts; + elseif type(opts) == "table" then + more = opts.more; + elseif opts ~= nil then + error(("bad argument #2 to 'decode' (function or table expected, got %s)"):format(type(opts))); + end + if type(more) ~= "function" then + function more() + error "input too short"; + end + end + + function fh:read(bytes) + local ret = s:sub(pos, pos + bytes - 1); + if #ret < bytes then + ret = more(bytes - #ret, fh, opts); + if ret then self:write(ret); end + return self:read(bytes); + end + pos = pos + bytes; + return ret; + end + + function fh:write(bytes) -- luacheck: no self + s = s .. bytes; + if pos > 256 then + s = s:sub(pos + 1); + pos = 1; + end + return #bytes; + end + + return read_object(fh, opts); +end + +return { + -- en-/decoder functions + encode = encode; + decode = decode; + decode_file = read_object; + + -- tables of per-type en-/decoders + type_encoders = encoder; + type_decoders = decoder; + + -- special treatment for tagged values + tagged_decoders = tagged_decoders; + + -- constructors for annotated types + simple = simple; + tagged = tagged; + + -- pre-defined simple values + null = null; + undefined = undefined; +} diff --git a/sys/apis/class.lua b/sys/modules/opus/class.lua similarity index 94% rename from sys/apis/class.lua rename to sys/modules/opus/class.lua index f01a9e0..bb94b63 100644 --- a/sys/apis/class.lua +++ b/sys/modules/opus/class.lua @@ -7,6 +7,9 @@ return function(base) local c = { } -- a new class instance if type(base) == 'table' then -- our new class is a shallow copy of the base class! + if base._preload then + base = base._preload(base) + end for i,v in pairs(base) do c[i] = v end diff --git a/sys/apis/config.lua b/sys/modules/opus/config.lua similarity index 94% rename from sys/apis/config.lua rename to sys/modules/opus/config.lua index 13ab441..3518ba2 100644 --- a/sys/apis/config.lua +++ b/sys/modules/opus/config.lua @@ -1,4 +1,4 @@ -local Util = require('util') +local Util = require('opus.util') local fs = _G.fs local shell = _ENV.shell @@ -7,6 +7,7 @@ local Config = { } function Config.load(fname, data) local filename = 'usr/config/' .. fname + data = data or { } if not fs.exists('usr/config') then fs.makeDir('usr/config') diff --git a/sys/modules/opus/crypto/chacha20.lua b/sys/modules/opus/crypto/chacha20.lua new file mode 100644 index 0000000..7c3b819 --- /dev/null +++ b/sys/modules/opus/crypto/chacha20.lua @@ -0,0 +1,201 @@ +-- Chacha20 cipher in ComputerCraft +-- By Anavrins + +local cbor = require('opus.cbor') +local sha2 = require('opus.crypto.sha2') +local Util = require('opus.util') + +local ROUNDS = 8 -- Adjust this for speed tradeoff + +local bxor = bit32.bxor +local band = bit32.band +local blshift = bit32.lshift +local brshift = bit32.arshift +local textutils = _G.textutils + +local mod = 2^32 +local tau = {("expand 16-byte k"):byte(1,-1)} +local sigma = {("expand 32-byte k"):byte(1,-1)} +local null32 = {("A"):rep(32):byte(1,-1)} +local null12 = {("A"):rep(12):byte(1,-1)} + +local function rotl(n, b) + local s = n/(2^(32-b)) + local f = s%1 + return (s-f) + f*mod +end + +local function quarterRound(s, a, b, c, d) + s[a] = (s[a]+s[b])%mod; s[d] = rotl(bxor(s[d], s[a]), 16) + s[c] = (s[c]+s[d])%mod; s[b] = rotl(bxor(s[b], s[c]), 12) + s[a] = (s[a]+s[b])%mod; s[d] = rotl(bxor(s[d], s[a]), 8) + s[c] = (s[c]+s[d])%mod; s[b] = rotl(bxor(s[b], s[c]), 7) + return s +end + +local function hashBlock(state, rnd) + local s = {table.unpack(state)} + for i = 1, rnd do + local r = i%2==1 + s = r and quarterRound(s, 1, 5, 9, 13) or quarterRound(s, 1, 6, 11, 16) + s = r and quarterRound(s, 2, 6, 10, 14) or quarterRound(s, 2, 7, 12, 13) + s = r and quarterRound(s, 3, 7, 11, 15) or quarterRound(s, 3, 8, 9, 14) + s = r and quarterRound(s, 4, 8, 12, 16) or quarterRound(s, 4, 5, 10, 15) + end + for i = 1, 16 do s[i] = (s[i]+state[i])%mod end + return s +end + +local function LE_toInt(bs, i) + return (bs[i+1] or 0)+ + blshift((bs[i+2] or 0), 8)+ + blshift((bs[i+3] or 0), 16)+ + blshift((bs[i+4] or 0), 24) +end + +local function initState(key, nonce, counter) + local isKey256 = #key == 32 + local const = isKey256 and sigma or tau + local state = {} + + state[ 1] = LE_toInt(const, 0) + state[ 2] = LE_toInt(const, 4) + state[ 3] = LE_toInt(const, 8) + state[ 4] = LE_toInt(const, 12) + + state[ 5] = LE_toInt(key, 0) + state[ 6] = LE_toInt(key, 4) + state[ 7] = LE_toInt(key, 8) + state[ 8] = LE_toInt(key, 12) + state[ 9] = LE_toInt(key, isKey256 and 16 or 0) + state[10] = LE_toInt(key, isKey256 and 20 or 4) + state[11] = LE_toInt(key, isKey256 and 24 or 8) + state[12] = LE_toInt(key, isKey256 and 28 or 12) + + state[13] = counter + state[14] = LE_toInt(nonce, 0) + state[15] = LE_toInt(nonce, 4) + state[16] = LE_toInt(nonce, 8) + + return state +end + +local function serialize(state) + local r = {} + for i = 1, 16 do + r[#r+1] = band(state[i], 0xFF) + r[#r+1] = band(brshift(state[i], 8), 0xFF) + r[#r+1] = band(brshift(state[i], 16), 0xFF) + r[#r+1] = band(brshift(state[i], 24), 0xFF) + end + return r +end + +local mt = { + __tostring = function(a) return string.char(table.unpack(a)) end, + __index = { + toHex = function(self) return ("%02x"):rep(#self):format(table.unpack(self)) end, + isEqual = function(self, t) + if type(t) ~= "table" then return false end + if #self ~= #t then return false end + local ret = 0 + for i = 1, #self do + ret = bit32.bor(ret, bxor(self[i], t[i])) + end + return ret == 0 + end + } +} + +local function crypt(data, key, nonce, cntr, round) + assert(type(key) == "table", "ChaCha20: Invalid key format ("..type(key).."), must be table") + assert(type(nonce) == "table", "ChaCha20: Invalid nonce format ("..type(nonce).."), must be table") + assert(#key == 16 or #key == 32, "ChaCha20: Invalid key length ("..#key.."), must be 16 or 32") + assert(#nonce == 12, "ChaCha20: Invalid nonce length ("..#nonce.."), must be 12") + + data = type(data) == "table" and {table.unpack(data)} or {tostring(data):byte(1,-1)} + cntr = tonumber(cntr) or 1 + round = tonumber(round) or 20 + + local throttle = Util.throttle(function() _syslog('throttle') end) + local out = {} + local state = initState(key, nonce, cntr) + local blockAmt = math.floor(#data/64) + for i = 0, blockAmt do + local ks = serialize(hashBlock(state, round)) + state[13] = (state[13]+1) % mod + + local block = {} + for j = 1, 64 do + block[j] = data[((i)*64)+j] + end + for j = 1, #block do + out[#out+1] = bxor(block[j], ks[j]) + end + + --if i % 1000 == 0 then + throttle() + --os.queueEvent("") + --os.pullEvent("") + --end + end + return setmetatable(out, mt) +end + +local function genNonce(len) + local nonce = {} + for i = 1, len do + nonce[i] = math.random(0, 0xFF) + end + return setmetatable(nonce, mt) +end + +local function encrypt(data, key) + local nonce = genNonce(12) + data = cbor.encode(data) + key = sha2.digest(key) + local ctx = crypt(data, key, nonce, 1, ROUNDS) + + return { nonce:toHex(), ctx:toHex() } +end + +local function decrypt(data, key) + local nonce = Util.hexToByteArray(data[1]) + data = Util.hexToByteArray(data[2]) + key = sha2.digest(key) + local ptx = crypt(data, key, nonce, 1, ROUNDS) + return cbor.decode(tostring(ptx)) +end + +local obj = {} +local rng_mt = {['__index'] = obj} + +function obj:nextInt(byte) + if not byte or byte < 1 or byte > 6 then error("Can only return 1-6 bytes", 2) end + local output = 0 + for i = 0, byte-1 do + if #self.block == 0 then + self.cnt = self.cnt + 1 + self.block = crypt(null32, self.seed, null12, self.cnt) + end + + local newByte = table.remove(self.block) + output = output + (newByte * (2^(8*i))) + end + return output +end + +local function newRNG(seed) + local o = {} + o.seed = seed + o.cnt = 0 + o.block = {} + + return setmetatable(o, rng_mt) +end + +return { + encrypt = encrypt, + decrypt = decrypt, + newRNG = newRNG, +} diff --git a/sys/modules/opus/crypto/ecc/elliptic.lua b/sys/modules/opus/crypto/ecc/elliptic.lua new file mode 100644 index 0000000..9af2683 --- /dev/null +++ b/sys/modules/opus/crypto/ecc/elliptic.lua @@ -0,0 +1,306 @@ +---- Elliptic Curve Arithmetic + +---- About the Curve Itself +-- Field Size: 192 bits +-- Field Modulus (p): 65533 * 2^176 + 3 +-- Equation: x^2 + y^2 = 1 + 108 * x^2 * y^2 +-- Parameters: Edwards Curve with c = 1, and d = 108 +-- Curve Order (n): 4 * 1569203598118192102418711808268118358122924911136798015831 +-- Cofactor (h): 4 +-- Generator Order (q): 1569203598118192102418711808268118358122924911136798015831 +---- About the Curve's Security +-- Current best attack security: 94.822 bits (Pollard's Rho) +-- Rho Security: log2(0.884 * sqrt(q)) = 94.822 +-- Transfer Security? Yes: p ~= q; k > 20 +-- Field Discriminant Security? Yes: t = 67602300638727286331433024168; s = 2^2; |D| = 5134296629560551493299993292204775496868940529592107064435 > 2^100 +-- Rigidity? A little, the parameters are somewhat small. +-- XZ/YZ Ladder Security? No: Single coordinate ladders are insecure, so they can't be used. +-- Small Subgroup Security? Yes: Secret keys are calculated modulo 4q. +-- Invalid Curve Security? Yes: Any point to be multiplied is checked beforehand. +-- Invalid Curve Twist Security? No: The curve is not protected against single coordinate ladder attacks, so don't use them. +-- Completeness? Yes: The curve is an Edwards Curve with non-square d and square a, so the curve is complete. +-- Indistinguishability? No: The curve does not support indistinguishability maps. + +local fp = require('opus.crypto.ecc.fp') +local Util = require('opus.util') + +local eq = fp.eq +local mul = fp.mul +local sqr = fp.sqr +local add = fp.add +local sub = fp.sub +local shr = fp.shr +local mont = fp.mont +local invMont = fp.invMont +local sub192 = fp.sub192 +local unpack = table.unpack + +local bits = 192 +local pMinusTwoBinary = {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} +local pMinusThreeOverFourBinary = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0} +local ZERO = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +local ONE = mont({1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) + +local p = mont({3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65533}) +local G = { + mont({30457, 58187, 5603, 63215, 8936, 58151, 26571, 7272, 26680, 23486, 32353, 59456}), + mont({3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}), + mont({1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) +} +local GTable = {G} + +local d = mont({108, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) + +local function generator() + return G +end + +local function expMod(a, t) + local a = {unpack(a)} + local result = {unpack(ONE)} + + for i = 1, bits do + if t[i] == 1 then + result = mul(result, a) + end + a = mul(a, a) + end + + return result +end + +-- We're using Projective Coordinates +-- For Edwards curves +-- The identity element is represented by (0:1:1) +local function pointDouble(P1) + local X1, Y1, Z1 = unpack(P1) + + local b = add(X1, Y1) + local B = sqr(b) + local C = sqr(X1) + local D = sqr(Y1) + local E = add(C, D) + local H = sqr(Z1) + local J = sub(E, add(H, H)) + local X3 = mul(sub(B, E), J) + local Y3 = mul(E, sub(C, D)) + local Z3 = mul(E, J) + + local P3 = {X3, Y3, Z3} + + return P3 +end + +local function pointAdd(P1, P2) + local X1, Y1, Z1 = unpack(P1) + local X2, Y2, Z2 = unpack(P2) + + local A = mul(Z1, Z2) + local B = sqr(A) + local C = mul(X1, X2) + local D = mul(Y1, Y2) + local E = mul(d, mul(C, D)) + local F = sub(B, E) + local G = add(B, E) + local X3 = mul(A, mul(F, sub(mul(add(X1, Y1), add(X2, Y2)), add(C, D)))) + local Y3 = mul(A, mul(G, sub(D, C))) + local Z3 = mul(F, G) + + local P3 = {X3, Y3, Z3} + + return P3 +end + +local function pointNeg(P1) + local X1, Y1, Z1 = unpack(P1) + + local X3 = sub(p, X1) + local Y3 = {unpack(Y1)} + local Z3 = {unpack(Z1)} + + local P3 = {X3, Y3, Z3} + + return P3 +end + +local function pointSub(P1, P2) + return pointAdd(P1, pointNeg(P2)) +end + +local function pointScale(P1) + local X1, Y1, Z1 = unpack(P1) + + local A = expMod(Z1, pMinusTwoBinary) + local X3 = mul(X1, A) + local Y3 = mul(Y1, A) + local Z3 = {unpack(ONE)} + + local P3 = {X3, Y3, Z3} + + return P3 +end + +local function pointEq(P1, P2) + local X1, Y1, Z1 = unpack(P1) + local X2, Y2, Z2 = unpack(P2) + + local A1 = mul(X1, Z2) + local B1 = mul(Y1, Z2) + local A2 = mul(X2, Z1) + local B2 = mul(Y2, Z1) + + return eq(A1, A2) and eq(B1, B2) +end + +local function isOnCurve(P1) + local X1, Y1, Z1 = unpack(P1) + + local X12 = sqr(X1) + local Y12 = sqr(Y1) + local Z12 = sqr(Z1) + local Z14 = sqr(Z12) + local a = add(X12, Y12) + a = mul(a, Z12) + local b = mul(d, mul(X12, Y12)) + b = add(Z14, b) + + return eq(a, b) +end + +local function mods(d) + -- w = 5 + local result = d[1] % 32 + + if result >= 16 then + result = result - 32 + end + + return result +end + +local function NAF(d) + local t = {} + local d = {unpack(d)} + + while d[12] >= 0 and not eq(d, ZERO) do + if d[1] % 2 == 1 then + t[#t + 1] = mods(d) + d = sub192(d, {t[#t], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) + else + t[#t + 1] = 0 + end + + d = shr(d) + end + + return t +end + +local function scalarMul(s, P1) + local naf = NAF(s) + local PTable = {P1} + local P2 = pointDouble(P1) + + for i = 3, 31, 2 do + PTable[i] = pointAdd(PTable[i - 2], P2) + end + + local Q = {{unpack(ZERO)}, {unpack(ONE)}, {unpack(ONE)}} + for i = #naf, 1, -1 do -- can this loop be optimized ? + local n = naf[i] + Q = pointDouble(Q) + if n > 0 then + Q = pointAdd(Q, PTable[n]) + elseif n < 0 then + Q = pointSub(Q, PTable[-n]) + end + end + + return Q +end + +local throttle = Util.throttle() +for i = 2, 196 do + GTable[i] = pointDouble(GTable[i - 1]) + throttle() +end + +local function scalarMulG(s) + local result = {{unpack(ZERO)}, {unpack(ONE)}, {unpack(ONE)}} + local k = 1 + + for i = 1, 12 do + local w = s[i] + + for j = 1, 16 do + if w % 2 == 1 then + result = pointAdd(result, GTable[k]) + end + + k = k + 1 + + w = w / 2 + w = w - w % 1 + end + end + + return result +end + +local function pointEncode(P1) + P1 = pointScale(P1) + + local result = {} + local x, y = unpack(P1) + + result[1] = x[1] % 2 + + for i = 1, 12 do + local m = y[i] % 256 + result[2 * i] = m + result[2 * i + 1] = (y[i] - m) / 256 + end + + return result +end + +local function pointDecode(enc) + local y = {} + for i = 1, 12 do + y[i] = enc[2 * i] + y[i] = y[i] + enc[2 * i + 1] * 256 + end + + local y2 = sqr(y) + local u = sub(y2, ONE) + local v = sub(mul(d, y2), ONE) + local u2 = sqr(u) + local u3 = mul(u, u2) + local u5 = mul(u3, u2) + local v3 = mul(v, sqr(v)) + local w = mul(u5, v3) + local x = mul(u3, mul(v, expMod(w, pMinusThreeOverFourBinary))) + + if x[1] % 2 ~= enc[1] then + x = sub(p, x) + end + + local P3 = {x, y, {unpack(ONE)}} + + return P3 +end + +return { + generator = generator, + pointDouble = pointDouble, + pointAdd = pointAdd, + pointNeg = pointNeg, + pointSub = pointSub, + pointScale = pointScale, + pointEq = pointEq, + isOnCurve = isOnCurve, + scalarMul = scalarMul, + scalarMulG = scalarMulG, + pointEncode = pointEncode, + pointDecode = pointDecode, +} diff --git a/sys/modules/opus/crypto/ecc/fp.lua b/sys/modules/opus/crypto/ecc/fp.lua new file mode 100644 index 0000000..9e791bd --- /dev/null +++ b/sys/modules/opus/crypto/ecc/fp.lua @@ -0,0 +1,930 @@ +-- Fp Integer Arithmetic + +local unpack = table.unpack + +local n = 0xffff +local m = 0x10000 + +local p = {3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65533} +local p2 = {21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 43690} +local r2 = {44014, 58358, 19452, 6484, 45852, 58974, 63348, 64806, 65292, 65454, 65508, 21512} + +local function eq(a, b) + for i = 1, 12 do + if a[i] ~= b[i] then + return false + end + end + + return true +end + +local function reduce(a) + local r1 = a[1] + local r2 = a[2] + local r3 = a[3] + local r4 = a[4] + local r5 = a[5] + local r6 = a[6] + local r7 = a[7] + local r8 = a[8] + local r9 = a[9] + local r10 = a[10] + local r11 = a[11] + local r12 = a[12] + + if r12 < 65533 or r12 == 65533 and r1 < 3 then + return {unpack(a)} + end + + r1 = r1 - 3 + r12 = r12 - 65533 + + if r1 < 0 then + r2 = r2 - 1 + r1 = r1 + m + end + if r2 < 0 then + r3 = r3 - 1 + r2 = r2 + m + end + if r3 < 0 then + r4 = r4 - 1 + r3 = r3 + m + end + if r4 < 0 then + r5 = r5 - 1 + r4 = r4 + m + end + if r5 < 0 then + r6 = r6 - 1 + r5 = r5 + m + end + if r6 < 0 then + r7 = r7 - 1 + r6 = r6 + m + end + if r7 < 0 then + r8 = r8 - 1 + r7 = r7 + m + end + if r8 < 0 then + r9 = r9 - 1 + r8 = r8 + m + end + if r9 < 0 then + r10 = r10 - 1 + r9 = r9 + m + end + if r10 < 0 then + r11 = r11 - 1 + r10 = r10 + m + end + if r11 < 0 then + r12 = r12 - 1 + r11 = r11 + m + end + + return {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12} +end + +local function add(a, b) + local r1 = a[1] + b[1] + local r2 = a[2] + b[2] + local r3 = a[3] + b[3] + local r4 = a[4] + b[4] + local r5 = a[5] + b[5] + local r6 = a[6] + b[6] + local r7 = a[7] + b[7] + local r8 = a[8] + b[8] + local r9 = a[9] + b[9] + local r10 = a[10] + b[10] + local r11 = a[11] + b[11] + local r12 = a[12] + b[12] + + if r1 > n then + r2 = r2 + 1 + r1 = r1 - m + end + if r2 > n then + r3 = r3 + 1 + r2 = r2 - m + end + if r3 > n then + r4 = r4 + 1 + r3 = r3 - m + end + if r4 > n then + r5 = r5 + 1 + r4 = r4 - m + end + if r5 > n then + r6 = r6 + 1 + r5 = r5 - m + end + if r6 > n then + r7 = r7 + 1 + r6 = r6 - m + end + if r7 > n then + r8 = r8 + 1 + r7 = r7 - m + end + if r8 > n then + r9 = r9 + 1 + r8 = r8 - m + end + if r9 > n then + r10 = r10 + 1 + r9 = r9 - m + end + if r10 > n then + r11 = r11 + 1 + r10 = r10 - m + end + if r11 > n then + r12 = r12 + 1 + r11 = r11 - m + end + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12} + + return reduce(result) +end + +local function shr(a) + local r1 = a[1] + local r2 = a[2] + local r3 = a[3] + local r4 = a[4] + local r5 = a[5] + local r6 = a[6] + local r7 = a[7] + local r8 = a[8] + local r9 = a[9] + local r10 = a[10] + local r11 = a[11] + local r12 = a[12] + + r1 = r1 / 2 + r1 = r1 - r1 % 1 + r1 = r1 + (r2 % 2) * 0x8000 + r2 = r2 / 2 + r2 = r2 - r2 % 1 + r2 = r2 + (r3 % 2) * 0x8000 + r3 = r3 / 2 + r3 = r3 - r3 % 1 + r3 = r3 + (r4 % 2) * 0x8000 + r4 = r4 / 2 + r4 = r4 - r4 % 1 + r4 = r4 + (r5 % 2) * 0x8000 + r5 = r5 / 2 + r5 = r5 - r5 % 1 + r5 = r5 + (r6 % 2) * 0x8000 + r6 = r6 / 2 + r6 = r6 - r6 % 1 + r6 = r6 + (r7 % 2) * 0x8000 + r7 = r7 / 2 + r7 = r7 - r7 % 1 + r7 = r7 + (r8 % 2) * 0x8000 + r8 = r8 / 2 + r8 = r8 - r8 % 1 + r8 = r8 + (r9 % 2) * 0x8000 + r9 = r9 / 2 + r9 = r9 - r9 % 1 + r9 = r9 + (r10 % 2) * 0x8000 + r10 = r10 / 2 + r10 = r10 - r10 % 1 + r10 = r10 + (r11 % 2) * 0x8000 + r11 = r11 / 2 + r11 = r11 - r11 % 1 + r11 = r11 + (r12 % 2) * 0x8000 + r12 = r12 / 2 + r12 = r12 - r12 % 1 + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12} + + return result +end + +local function sub192(a, b) + local r1 = a[1] - b[1] + local r2 = a[2] - b[2] + local r3 = a[3] - b[3] + local r4 = a[4] - b[4] + local r5 = a[5] - b[5] + local r6 = a[6] - b[6] + local r7 = a[7] - b[7] + local r8 = a[8] - b[8] + local r9 = a[9] - b[9] + local r10 = a[10] - b[10] + local r11 = a[11] - b[11] + local r12 = a[12] - b[12] + + if r1 < 0 then + r2 = r2 - 1 + r1 = r1 + m + end + if r2 < 0 then + r3 = r3 - 1 + r2 = r2 + m + end + if r3 < 0 then + r4 = r4 - 1 + r3 = r3 + m + end + if r4 < 0 then + r5 = r5 - 1 + r4 = r4 + m + end + if r5 < 0 then + r6 = r6 - 1 + r5 = r5 + m + end + if r6 < 0 then + r7 = r7 - 1 + r6 = r6 + m + end + if r7 < 0 then + r8 = r8 - 1 + r7 = r7 + m + end + if r8 < 0 then + r9 = r9 - 1 + r8 = r8 + m + end + if r9 < 0 then + r10 = r10 - 1 + r9 = r9 + m + end + if r10 < 0 then + r11 = r11 - 1 + r10 = r10 + m + end + if r11 < 0 then + r12 = r12 - 1 + r11 = r11 + m + end + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12} + + return result +end + +local function sub(a, b) + local r1 = a[1] - b[1] + local r2 = a[2] - b[2] + local r3 = a[3] - b[3] + local r4 = a[4] - b[4] + local r5 = a[5] - b[5] + local r6 = a[6] - b[6] + local r7 = a[7] - b[7] + local r8 = a[8] - b[8] + local r9 = a[9] - b[9] + local r10 = a[10] - b[10] + local r11 = a[11] - b[11] + local r12 = a[12] - b[12] + + if r1 < 0 then + r2 = r2 - 1 + r1 = r1 + m + end + if r2 < 0 then + r3 = r3 - 1 + r2 = r2 + m + end + if r3 < 0 then + r4 = r4 - 1 + r3 = r3 + m + end + if r4 < 0 then + r5 = r5 - 1 + r4 = r4 + m + end + if r5 < 0 then + r6 = r6 - 1 + r5 = r5 + m + end + if r6 < 0 then + r7 = r7 - 1 + r6 = r6 + m + end + if r7 < 0 then + r8 = r8 - 1 + r7 = r7 + m + end + if r8 < 0 then + r9 = r9 - 1 + r8 = r8 + m + end + if r9 < 0 then + r10 = r10 - 1 + r9 = r9 + m + end + if r10 < 0 then + r11 = r11 - 1 + r10 = r10 + m + end + if r11 < 0 then + r12 = r12 - 1 + r11 = r11 + m + end + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12} + + if r12 < 0 then + result = add(result, p) + end + + return result +end + +local function add384(a, b) + local r1 = a[1] + b[1] + local r2 = a[2] + b[2] + local r3 = a[3] + b[3] + local r4 = a[4] + b[4] + local r5 = a[5] + b[5] + local r6 = a[6] + b[6] + local r7 = a[7] + b[7] + local r8 = a[8] + b[8] + local r9 = a[9] + b[9] + local r10 = a[10] + b[10] + local r11 = a[11] + b[11] + local r12 = a[12] + b[12] + local r13 = a[13] + b[13] + local r14 = a[14] + b[14] + local r15 = a[15] + b[15] + local r16 = a[16] + b[16] + local r17 = a[17] + b[17] + local r18 = a[18] + b[18] + local r19 = a[19] + b[19] + local r20 = a[20] + b[20] + local r21 = a[21] + b[21] + local r22 = a[22] + b[22] + local r23 = a[23] + b[23] + local r24 = a[24] + b[24] + + if r1 > n then + r2 = r2 + 1 + r1 = r1 - m + end + if r2 > n then + r3 = r3 + 1 + r2 = r2 - m + end + if r3 > n then + r4 = r4 + 1 + r3 = r3 - m + end + if r4 > n then + r5 = r5 + 1 + r4 = r4 - m + end + if r5 > n then + r6 = r6 + 1 + r5 = r5 - m + end + if r6 > n then + r7 = r7 + 1 + r6 = r6 - m + end + if r7 > n then + r8 = r8 + 1 + r7 = r7 - m + end + if r8 > n then + r9 = r9 + 1 + r8 = r8 - m + end + if r9 > n then + r10 = r10 + 1 + r9 = r9 - m + end + if r10 > n then + r11 = r11 + 1 + r10 = r10 - m + end + if r11 > n then + r12 = r12 + 1 + r11 = r11 - m + end + if r12 > n then + r13 = r13 + 1 + r12 = r12 - m + end + if r13 > n then + r14 = r14 + 1 + r13 = r13 - m + end + if r14 > n then + r15 = r15 + 1 + r14 = r14 - m + end + if r15 > n then + r16 = r16 + 1 + r15 = r15 - m + end + if r16 > n then + r17 = r17 + 1 + r16 = r16 - m + end + if r17 > n then + r18 = r18 + 1 + r17 = r17 - m + end + if r18 > n then + r19 = r19 + 1 + r18 = r18 - m + end + if r19 > n then + r20 = r20 + 1 + r19 = r19 - m + end + if r20 > n then + r21 = r21 + 1 + r20 = r20 - m + end + if r21 > n then + r22 = r22 + 1 + r21 = r21 - m + end + if r22 > n then + r23 = r23 + 1 + r22 = r22 - m + end + if r23 > n then + r24 = r24 + 1 + r23 = r23 - m + end + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24} + + return result +end + +local function mul384(a, b) + local a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 = unpack(a) + local b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12 = unpack(b) + + local r1 = a1 * b1 + + local r2 = a1 * b2 + r2 = r2 + a2 * b1 + + local r3 = a1 * b3 + r3 = r3 + a2 * b2 + r3 = r3 + a3 * b1 + + local r4 = a1 * b4 + r4 = r4 + a2 * b3 + r4 = r4 + a3 * b2 + r4 = r4 + a4 * b1 + + local r5 = a1 * b5 + r5 = r5 + a2 * b4 + r5 = r5 + a3 * b3 + r5 = r5 + a4 * b2 + r5 = r5 + a5 * b1 + + local r6 = a1 * b6 + r6 = r6 + a2 * b5 + r6 = r6 + a3 * b4 + r6 = r6 + a4 * b3 + r6 = r6 + a5 * b2 + r6 = r6 + a6 * b1 + + local r7 = a1 * b7 + r7 = r7 + a2 * b6 + r7 = r7 + a3 * b5 + r7 = r7 + a4 * b4 + r7 = r7 + a5 * b3 + r7 = r7 + a6 * b2 + r7 = r7 + a7 * b1 + + local r8 = a1 * b8 + r8 = r8 + a2 * b7 + r8 = r8 + a3 * b6 + r8 = r8 + a4 * b5 + r8 = r8 + a5 * b4 + r8 = r8 + a6 * b3 + r8 = r8 + a7 * b2 + r8 = r8 + a8 * b1 + + local r9 = a1 * b9 + r9 = r9 + a2 * b8 + r9 = r9 + a3 * b7 + r9 = r9 + a4 * b6 + r9 = r9 + a5 * b5 + r9 = r9 + a6 * b4 + r9 = r9 + a7 * b3 + r9 = r9 + a8 * b2 + r9 = r9 + a9 * b1 + + local r10 = a1 * b10 + r10 = r10 + a2 * b9 + r10 = r10 + a3 * b8 + r10 = r10 + a4 * b7 + r10 = r10 + a5 * b6 + r10 = r10 + a6 * b5 + r10 = r10 + a7 * b4 + r10 = r10 + a8 * b3 + r10 = r10 + a9 * b2 + r10 = r10 + a10 * b1 + + local r11 = a1 * b11 + r11 = r11 + a2 * b10 + r11 = r11 + a3 * b9 + r11 = r11 + a4 * b8 + r11 = r11 + a5 * b7 + r11 = r11 + a6 * b6 + r11 = r11 + a7 * b5 + r11 = r11 + a8 * b4 + r11 = r11 + a9 * b3 + r11 = r11 + a10 * b2 + r11 = r11 + a11 * b1 + + local r12 = a1 * b12 + r12 = r12 + a2 * b11 + r12 = r12 + a3 * b10 + r12 = r12 + a4 * b9 + r12 = r12 + a5 * b8 + r12 = r12 + a6 * b7 + r12 = r12 + a7 * b6 + r12 = r12 + a8 * b5 + r12 = r12 + a9 * b4 + r12 = r12 + a10 * b3 + r12 = r12 + a11 * b2 + r12 = r12 + a12 * b1 + + local r13 = a2 * b12 + r13 = r13 + a3 * b11 + r13 = r13 + a4 * b10 + r13 = r13 + a5 * b9 + r13 = r13 + a6 * b8 + r13 = r13 + a7 * b7 + r13 = r13 + a8 * b6 + r13 = r13 + a9 * b5 + r13 = r13 + a10 * b4 + r13 = r13 + a11 * b3 + r13 = r13 + a12 * b2 + + local r14 = a3 * b12 + r14 = r14 + a4 * b11 + r14 = r14 + a5 * b10 + r14 = r14 + a6 * b9 + r14 = r14 + a7 * b8 + r14 = r14 + a8 * b7 + r14 = r14 + a9 * b6 + r14 = r14 + a10 * b5 + r14 = r14 + a11 * b4 + r14 = r14 + a12 * b3 + + local r15 = a4 * b12 + r15 = r15 + a5 * b11 + r15 = r15 + a6 * b10 + r15 = r15 + a7 * b9 + r15 = r15 + a8 * b8 + r15 = r15 + a9 * b7 + r15 = r15 + a10 * b6 + r15 = r15 + a11 * b5 + r15 = r15 + a12 * b4 + + local r16 = a5 * b12 + r16 = r16 + a6 * b11 + r16 = r16 + a7 * b10 + r16 = r16 + a8 * b9 + r16 = r16 + a9 * b8 + r16 = r16 + a10 * b7 + r16 = r16 + a11 * b6 + r16 = r16 + a12 * b5 + + local r17 = a6 * b12 + r17 = r17 + a7 * b11 + r17 = r17 + a8 * b10 + r17 = r17 + a9 * b9 + r17 = r17 + a10 * b8 + r17 = r17 + a11 * b7 + r17 = r17 + a12 * b6 + + local r18 = a7 * b12 + r18 = r18 + a8 * b11 + r18 = r18 + a9 * b10 + r18 = r18 + a10 * b9 + r18 = r18 + a11 * b8 + r18 = r18 + a12 * b7 + + local r19 = a8 * b12 + r19 = r19 + a9 * b11 + r19 = r19 + a10 * b10 + r19 = r19 + a11 * b9 + r19 = r19 + a12 * b8 + + local r20 = a9 * b12 + r20 = r20 + a10 * b11 + r20 = r20 + a11 * b10 + r20 = r20 + a12 * b9 + + local r21 = a10 * b12 + r21 = r21 + a11 * b11 + r21 = r21 + a12 * b10 + + local r22 = a11 * b12 + r22 = r22 + a12 * b11 + + local r23 = a12 * b12 + + local r24 = 0 + + r2 = r2 + (r1 / m) + r2 = r2 - r2 % 1 + r1 = r1 % m + r3 = r3 + (r2 / m) + r3 = r3 - r3 % 1 + r2 = r2 % m + r4 = r4 + (r3 / m) + r4 = r4 - r4 % 1 + r3 = r3 % m + r5 = r5 + (r4 / m) + r5 = r5 - r5 % 1 + r4 = r4 % m + r6 = r6 + (r5 / m) + r6 = r6 - r6 % 1 + r5 = r5 % m + r7 = r7 + (r6 / m) + r7 = r7 - r7 % 1 + r6 = r6 % m + r8 = r8 + (r7 / m) + r8 = r8 - r8 % 1 + r7 = r7 % m + r9 = r9 + (r8 / m) + r9 = r9 - r9 % 1 + r8 = r8 % m + r10 = r10 + (r9 / m) + r10 = r10 - r10 % 1 + r9 = r9 % m + r11 = r11 + (r10 / m) + r11 = r11 - r11 % 1 + r10 = r10 % m + r12 = r12 + (r11 / m) + r12 = r12 - r12 % 1 + r11 = r11 % m + r13 = r13 + (r12 / m) + r13 = r13 - r13 % 1 + r12 = r12 % m + r14 = r14 + (r13 / m) + r14 = r14 - r14 % 1 + r13 = r13 % m + r15 = r15 + (r14 / m) + r15 = r15 - r15 % 1 + r14 = r14 % m + r16 = r16 + (r15 / m) + r16 = r16 - r16 % 1 + r15 = r15 % m + r17 = r17 + (r16 / m) + r17 = r17 - r17 % 1 + r16 = r16 % m + r18 = r18 + (r17 / m) + r18 = r18 - r18 % 1 + r17 = r17 % m + r19 = r19 + (r18 / m) + r19 = r19 - r19 % 1 + r18 = r18 % m + r20 = r20 + (r19 / m) + r20 = r20 - r20 % 1 + r19 = r19 % m + r21 = r21 + (r20 / m) + r21 = r21 - r21 % 1 + r20 = r20 % m + r22 = r22 + (r21 / m) + r22 = r22 - r22 % 1 + r21 = r21 % m + r23 = r23 + (r22 / m) + r23 = r23 - r23 % 1 + r22 = r22 % m + r24 = r24 + (r23 / m) + r24 = r24 - r24 % 1 + r23 = r23 % m + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24} + + return result +end + +local function REDC(T) + local m = {unpack(mul384({unpack(T, 1, 12)}, p2), 1, 12)} + local t = {unpack(add384(T, mul384(m, p)), 13, 24)} + + return reduce(t) +end + +local function mul(a, b) + return REDC(mul384(a, b)) +end + +local function sqr(a) + local a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 = unpack(a) + + local r1 = a1 * a1 + + local r2 = a1 * a2 * 2 + + local r3 = a1 * a3 * 2 + r3 = r3 + a2 * a2 + + local r4 = a1 * a4 * 2 + r4 = r4 + a2 * a3 * 2 + + local r5 = a1 * a5 * 2 + r5 = r5 + a2 * a4 * 2 + r5 = r5 + a3 * a3 + + local r6 = a1 * a6 * 2 + r6 = r6 + a2 * a5 * 2 + r6 = r6 + a3 * a4 * 2 + + local r7 = a1 * a7 * 2 + r7 = r7 + a2 * a6 * 2 + r7 = r7 + a3 * a5 * 2 + r7 = r7 + a4 * a4 + + local r8 = a1 * a8 * 2 + r8 = r8 + a2 * a7 * 2 + r8 = r8 + a3 * a6 * 2 + r8 = r8 + a4 * a5 * 2 + + local r9 = a1 * a9 * 2 + r9 = r9 + a2 * a8 * 2 + r9 = r9 + a3 * a7 * 2 + r9 = r9 + a4 * a6 * 2 + r9 = r9 + a5 * a5 + + local r10 = a1 * a10 * 2 + r10 = r10 + a2 * a9 * 2 + r10 = r10 + a3 * a8 * 2 + r10 = r10 + a4 * a7 * 2 + r10 = r10 + a5 * a6 * 2 + + local r11 = a1 * a11 * 2 + r11 = r11 + a2 * a10 * 2 + r11 = r11 + a3 * a9 * 2 + r11 = r11 + a4 * a8 * 2 + r11 = r11 + a5 * a7 * 2 + r11 = r11 + a6 * a6 + + local r12 = a1 * a12 * 2 + r12 = r12 + a2 * a11 * 2 + r12 = r12 + a3 * a10 * 2 + r12 = r12 + a4 * a9 * 2 + r12 = r12 + a5 * a8 * 2 + r12 = r12 + a6 * a7 * 2 + + local r13 = a2 * a12 * 2 + r13 = r13 + a3 * a11 * 2 + r13 = r13 + a4 * a10 * 2 + r13 = r13 + a5 * a9 * 2 + r13 = r13 + a6 * a8 * 2 + r13 = r13 + a7 * a7 + + local r14 = a3 * a12 * 2 + r14 = r14 + a4 * a11 * 2 + r14 = r14 + a5 * a10 * 2 + r14 = r14 + a6 * a9 * 2 + r14 = r14 + a7 * a8 * 2 + + local r15 = a4 * a12 * 2 + r15 = r15 + a5 * a11 * 2 + r15 = r15 + a6 * a10 * 2 + r15 = r15 + a7 * a9 * 2 + r15 = r15 + a8 * a8 + + local r16 = a5 * a12 * 2 + r16 = r16 + a6 * a11 * 2 + r16 = r16 + a7 * a10 * 2 + r16 = r16 + a8 * a9 * 2 + + local r17 = a6 * a12 * 2 + r17 = r17 + a7 * a11 * 2 + r17 = r17 + a8 * a10 * 2 + r17 = r17 + a9 * a9 + + local r18 = a7 * a12 * 2 + r18 = r18 + a8 * a11 * 2 + r18 = r18 + a9 * a10 * 2 + + local r19 = a8 * a12 * 2 + r19 = r19 + a9 * a11 * 2 + r19 = r19 + a10 * a10 + + local r20 = a9 * a12 * 2 + r20 = r20 + a10 * a11 * 2 + + local r21 = a10 * a12 * 2 + r21 = r21 + a11 * a11 + + local r22 = a11 * a12 * 2 + + local r23 = a12 * a12 + + local r24 = 0 + + r2 = r2 + (r1 / m) + r2 = r2 - r2 % 1 + r1 = r1 % m + r3 = r3 + (r2 / m) + r3 = r3 - r3 % 1 + r2 = r2 % m + r4 = r4 + (r3 / m) + r4 = r4 - r4 % 1 + r3 = r3 % m + r5 = r5 + (r4 / m) + r5 = r5 - r5 % 1 + r4 = r4 % m + r6 = r6 + (r5 / m) + r6 = r6 - r6 % 1 + r5 = r5 % m + r7 = r7 + (r6 / m) + r7 = r7 - r7 % 1 + r6 = r6 % m + r8 = r8 + (r7 / m) + r8 = r8 - r8 % 1 + r7 = r7 % m + r9 = r9 + (r8 / m) + r9 = r9 - r9 % 1 + r8 = r8 % m + r10 = r10 + (r9 / m) + r10 = r10 - r10 % 1 + r9 = r9 % m + r11 = r11 + (r10 / m) + r11 = r11 - r11 % 1 + r10 = r10 % m + r12 = r12 + (r11 / m) + r12 = r12 - r12 % 1 + r11 = r11 % m + r13 = r13 + (r12 / m) + r13 = r13 - r13 % 1 + r12 = r12 % m + r14 = r14 + (r13 / m) + r14 = r14 - r14 % 1 + r13 = r13 % m + r15 = r15 + (r14 / m) + r15 = r15 - r15 % 1 + r14 = r14 % m + r16 = r16 + (r15 / m) + r16 = r16 - r16 % 1 + r15 = r15 % m + r17 = r17 + (r16 / m) + r17 = r17 - r17 % 1 + r16 = r16 % m + r18 = r18 + (r17 / m) + r18 = r18 - r18 % 1 + r17 = r17 % m + r19 = r19 + (r18 / m) + r19 = r19 - r19 % 1 + r18 = r18 % m + r20 = r20 + (r19 / m) + r20 = r20 - r20 % 1 + r19 = r19 % m + r21 = r21 + (r20 / m) + r21 = r21 - r21 % 1 + r20 = r20 % m + r22 = r22 + (r21 / m) + r22 = r22 - r22 % 1 + r21 = r21 % m + r23 = r23 + (r22 / m) + r23 = r23 - r23 % 1 + r22 = r22 % m + r24 = r24 + (r23 / m) + r24 = r24 - r24 % 1 + r23 = r23 % m + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24} + + return REDC(result) +end + +local function mont(a) + return mul(a, r2) +end + +local function invMont(a) + local a = {unpack(a)} + + for i = 13, 24 do + a[i] = 0 + end + + return REDC(a) +end + +return { + eq = eq, + add = add, + shr = shr, + sub192 = sub192, + sub = sub, + mul = mul, + sqr = sqr, + mont = mont, + invMont = invMont, +} diff --git a/sys/modules/opus/crypto/ecc/fq.lua b/sys/modules/opus/crypto/ecc/fq.lua new file mode 100644 index 0000000..c552688 --- /dev/null +++ b/sys/modules/opus/crypto/ecc/fq.lua @@ -0,0 +1,743 @@ +-- Fq Integer Arithmetic + +local unpack = table.unpack + +local n = 0xffff +local m = 0x10000 + +local q = {1372, 62520, 47765, 8105, 45059, 9616, 65535, 65535, 65535, 65535, 65535, 65532} +local qn = {1372, 62520, 47765, 8105, 45059, 9616, 65535, 65535, 65535, 65535, 65535, 65532, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + +local function eq(a, b) + for i = 1, 12 do + if a[i] ~= b[i] then + return false + end + end + + return true +end + +local function cmp(a, b) + for i = 12, 1, -1 do + if a[i] > b[i] then + return 1 + elseif a[i] < b[i] then + return -1 + end + end + + return 0 +end + +local function cmp384(a, b) + for i = 24, 1, -1 do + if a[i] > b[i] then + return 1 + elseif a[i] < b[i] then + return -1 + end + end + + return 0 +end + +local function bytes(x) + local result = {} + + for i = 0, 11 do + local m = x[i + 1] % 256 + result[2 * i + 1] = m + result[2 * i + 2] = (x[i + 1] - m) / 256 + end + + return result +end + +local function fromBytes(enc) + local result = {} + + for i = 0, 11 do + result[i + 1] = enc[2 * i + 1] % 256 + result[i + 1] = result[i + 1] + enc[2 * i + 2] * 256 + end + + return result +end + +local function sub192(a, b) + local r1 = a[1] - b[1] + local r2 = a[2] - b[2] + local r3 = a[3] - b[3] + local r4 = a[4] - b[4] + local r5 = a[5] - b[5] + local r6 = a[6] - b[6] + local r7 = a[7] - b[7] + local r8 = a[8] - b[8] + local r9 = a[9] - b[9] + local r10 = a[10] - b[10] + local r11 = a[11] - b[11] + local r12 = a[12] - b[12] + + if r1 < 0 then + r2 = r2 - 1 + r1 = r1 + m + end + if r2 < 0 then + r3 = r3 - 1 + r2 = r2 + m + end + if r3 < 0 then + r4 = r4 - 1 + r3 = r3 + m + end + if r4 < 0 then + r5 = r5 - 1 + r4 = r4 + m + end + if r5 < 0 then + r6 = r6 - 1 + r5 = r5 + m + end + if r6 < 0 then + r7 = r7 - 1 + r6 = r6 + m + end + if r7 < 0 then + r8 = r8 - 1 + r7 = r7 + m + end + if r8 < 0 then + r9 = r9 - 1 + r8 = r8 + m + end + if r9 < 0 then + r10 = r10 - 1 + r9 = r9 + m + end + if r10 < 0 then + r11 = r11 - 1 + r10 = r10 + m + end + if r11 < 0 then + r12 = r12 - 1 + r11 = r11 + m + end + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12} + + return result +end + +local function reduce(a) + local result = {unpack(a)} + + if cmp(result, q) >= 0 then + result = sub192(result, q) + end + + return result +end + +local function add(a, b) + local r1 = a[1] + b[1] + local r2 = a[2] + b[2] + local r3 = a[3] + b[3] + local r4 = a[4] + b[4] + local r5 = a[5] + b[5] + local r6 = a[6] + b[6] + local r7 = a[7] + b[7] + local r8 = a[8] + b[8] + local r9 = a[9] + b[9] + local r10 = a[10] + b[10] + local r11 = a[11] + b[11] + local r12 = a[12] + b[12] + + if r1 > n then + r2 = r2 + 1 + r1 = r1 - m + end + if r2 > n then + r3 = r3 + 1 + r2 = r2 - m + end + if r3 > n then + r4 = r4 + 1 + r3 = r3 - m + end + if r4 > n then + r5 = r5 + 1 + r4 = r4 - m + end + if r5 > n then + r6 = r6 + 1 + r5 = r5 - m + end + if r6 > n then + r7 = r7 + 1 + r6 = r6 - m + end + if r7 > n then + r8 = r8 + 1 + r7 = r7 - m + end + if r8 > n then + r9 = r9 + 1 + r8 = r8 - m + end + if r9 > n then + r10 = r10 + 1 + r9 = r9 - m + end + if r10 > n then + r11 = r11 + 1 + r10 = r10 - m + end + if r11 > n then + r12 = r12 + 1 + r11 = r11 - m + end + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12} + + return reduce(result) +end + +local function sub(a, b) + local result = sub192(a, b) + + if result[12] < 0 then + result = add(result, q) + end + + return result +end + +local function add384(a, b) + local r1 = a[1] + b[1] + local r2 = a[2] + b[2] + local r3 = a[3] + b[3] + local r4 = a[4] + b[4] + local r5 = a[5] + b[5] + local r6 = a[6] + b[6] + local r7 = a[7] + b[7] + local r8 = a[8] + b[8] + local r9 = a[9] + b[9] + local r10 = a[10] + b[10] + local r11 = a[11] + b[11] + local r12 = a[12] + b[12] + local r13 = a[13] + b[13] + local r14 = a[14] + b[14] + local r15 = a[15] + b[15] + local r16 = a[16] + b[16] + local r17 = a[17] + b[17] + local r18 = a[18] + b[18] + local r19 = a[19] + b[19] + local r20 = a[20] + b[20] + local r21 = a[21] + b[21] + local r22 = a[22] + b[22] + local r23 = a[23] + b[23] + local r24 = a[24] + b[24] + + if r1 > n then + r2 = r2 + 1 + r1 = r1 - m + end + if r2 > n then + r3 = r3 + 1 + r2 = r2 - m + end + if r3 > n then + r4 = r4 + 1 + r3 = r3 - m + end + if r4 > n then + r5 = r5 + 1 + r4 = r4 - m + end + if r5 > n then + r6 = r6 + 1 + r5 = r5 - m + end + if r6 > n then + r7 = r7 + 1 + r6 = r6 - m + end + if r7 > n then + r8 = r8 + 1 + r7 = r7 - m + end + if r8 > n then + r9 = r9 + 1 + r8 = r8 - m + end + if r9 > n then + r10 = r10 + 1 + r9 = r9 - m + end + if r10 > n then + r11 = r11 + 1 + r10 = r10 - m + end + if r11 > n then + r12 = r12 + 1 + r11 = r11 - m + end + if r12 > n then + r13 = r13 + 1 + r12 = r12 - m + end + if r13 > n then + r14 = r14 + 1 + r13 = r13 - m + end + if r14 > n then + r15 = r15 + 1 + r14 = r14 - m + end + if r15 > n then + r16 = r16 + 1 + r15 = r15 - m + end + if r16 > n then + r17 = r17 + 1 + r16 = r16 - m + end + if r17 > n then + r18 = r18 + 1 + r17 = r17 - m + end + if r18 > n then + r19 = r19 + 1 + r18 = r18 - m + end + if r19 > n then + r20 = r20 + 1 + r19 = r19 - m + end + if r20 > n then + r21 = r21 + 1 + r20 = r20 - m + end + if r21 > n then + r22 = r22 + 1 + r21 = r21 - m + end + if r22 > n then + r23 = r23 + 1 + r22 = r22 - m + end + if r23 > n then + r24 = r24 + 1 + r23 = r23 - m + end + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24} + + return result +end + +local function sub384(a, b) + local r1 = a[1] - b[1] + local r2 = a[2] - b[2] + local r3 = a[3] - b[3] + local r4 = a[4] - b[4] + local r5 = a[5] - b[5] + local r6 = a[6] - b[6] + local r7 = a[7] - b[7] + local r8 = a[8] - b[8] + local r9 = a[9] - b[9] + local r10 = a[10] - b[10] + local r11 = a[11] - b[11] + local r12 = a[12] - b[12] + local r13 = a[13] - b[13] + local r14 = a[14] - b[14] + local r15 = a[15] - b[15] + local r16 = a[16] - b[16] + local r17 = a[17] - b[17] + local r18 = a[18] - b[18] + local r19 = a[19] - b[19] + local r20 = a[20] - b[20] + local r21 = a[21] - b[21] + local r22 = a[22] - b[22] + local r23 = a[23] - b[23] + local r24 = a[24] - b[24] + + if r1 < 0 then + r2 = r2 - 1 + r1 = r1 + m + end + if r2 < 0 then + r3 = r3 - 1 + r2 = r2 + m + end + if r3 < 0 then + r4 = r4 - 1 + r3 = r3 + m + end + if r4 < 0 then + r5 = r5 - 1 + r4 = r4 + m + end + if r5 < 0 then + r6 = r6 - 1 + r5 = r5 + m + end + if r6 < 0 then + r7 = r7 - 1 + r6 = r6 + m + end + if r7 < 0 then + r8 = r8 - 1 + r7 = r7 + m + end + if r8 < 0 then + r9 = r9 - 1 + r8 = r8 + m + end + if r9 < 0 then + r10 = r10 - 1 + r9 = r9 + m + end + if r10 < 0 then + r11 = r11 - 1 + r10 = r10 + m + end + if r11 < 0 then + r12 = r12 - 1 + r11 = r11 + m + end + if r12 < 0 then + r13 = r13 - 1 + r12 = r12 + m + end + if r13 < 0 then + r14 = r14 - 1 + r13 = r13 + m + end + if r14 < 0 then + r15 = r15 - 1 + r14 = r14 + m + end + if r15 < 0 then + r16 = r16 - 1 + r15 = r15 + m + end + if r16 < 0 then + r17 = r17 - 1 + r16 = r16 + m + end + if r17 < 0 then + r18 = r18 - 1 + r17 = r17 + m + end + if r18 < 0 then + r19 = r19 - 1 + r18 = r18 + m + end + if r19 < 0 then + r20 = r20 - 1 + r19 = r19 + m + end + if r20 < 0 then + r21 = r21 - 1 + r20 = r20 + m + end + if r21 < 0 then + r22 = r22 - 1 + r21 = r21 + m + end + if r22 < 0 then + r23 = r23 - 1 + r22 = r22 + m + end + if r23 < 0 then + r24 = r24 - 1 + r23 = r23 + m + end + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24} + + return result +end + +local function mul384(a, b) + local a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 = unpack(a) + local b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12 = unpack(b) + + local r1 = a1 * b1 + + local r2 = a1 * b2 + r2 = r2 + a2 * b1 + + local r3 = a1 * b3 + r3 = r3 + a2 * b2 + r3 = r3 + a3 * b1 + + local r4 = a1 * b4 + r4 = r4 + a2 * b3 + r4 = r4 + a3 * b2 + r4 = r4 + a4 * b1 + + local r5 = a1 * b5 + r5 = r5 + a2 * b4 + r5 = r5 + a3 * b3 + r5 = r5 + a4 * b2 + r5 = r5 + a5 * b1 + + local r6 = a1 * b6 + r6 = r6 + a2 * b5 + r6 = r6 + a3 * b4 + r6 = r6 + a4 * b3 + r6 = r6 + a5 * b2 + r6 = r6 + a6 * b1 + + local r7 = a1 * b7 + r7 = r7 + a2 * b6 + r7 = r7 + a3 * b5 + r7 = r7 + a4 * b4 + r7 = r7 + a5 * b3 + r7 = r7 + a6 * b2 + r7 = r7 + a7 * b1 + + local r8 = a1 * b8 + r8 = r8 + a2 * b7 + r8 = r8 + a3 * b6 + r8 = r8 + a4 * b5 + r8 = r8 + a5 * b4 + r8 = r8 + a6 * b3 + r8 = r8 + a7 * b2 + r8 = r8 + a8 * b1 + + local r9 = a1 * b9 + r9 = r9 + a2 * b8 + r9 = r9 + a3 * b7 + r9 = r9 + a4 * b6 + r9 = r9 + a5 * b5 + r9 = r9 + a6 * b4 + r9 = r9 + a7 * b3 + r9 = r9 + a8 * b2 + r9 = r9 + a9 * b1 + + local r10 = a1 * b10 + r10 = r10 + a2 * b9 + r10 = r10 + a3 * b8 + r10 = r10 + a4 * b7 + r10 = r10 + a5 * b6 + r10 = r10 + a6 * b5 + r10 = r10 + a7 * b4 + r10 = r10 + a8 * b3 + r10 = r10 + a9 * b2 + r10 = r10 + a10 * b1 + + local r11 = a1 * b11 + r11 = r11 + a2 * b10 + r11 = r11 + a3 * b9 + r11 = r11 + a4 * b8 + r11 = r11 + a5 * b7 + r11 = r11 + a6 * b6 + r11 = r11 + a7 * b5 + r11 = r11 + a8 * b4 + r11 = r11 + a9 * b3 + r11 = r11 + a10 * b2 + r11 = r11 + a11 * b1 + + local r12 = a1 * b12 + r12 = r12 + a2 * b11 + r12 = r12 + a3 * b10 + r12 = r12 + a4 * b9 + r12 = r12 + a5 * b8 + r12 = r12 + a6 * b7 + r12 = r12 + a7 * b6 + r12 = r12 + a8 * b5 + r12 = r12 + a9 * b4 + r12 = r12 + a10 * b3 + r12 = r12 + a11 * b2 + r12 = r12 + a12 * b1 + + local r13 = a2 * b12 + r13 = r13 + a3 * b11 + r13 = r13 + a4 * b10 + r13 = r13 + a5 * b9 + r13 = r13 + a6 * b8 + r13 = r13 + a7 * b7 + r13 = r13 + a8 * b6 + r13 = r13 + a9 * b5 + r13 = r13 + a10 * b4 + r13 = r13 + a11 * b3 + r13 = r13 + a12 * b2 + + local r14 = a3 * b12 + r14 = r14 + a4 * b11 + r14 = r14 + a5 * b10 + r14 = r14 + a6 * b9 + r14 = r14 + a7 * b8 + r14 = r14 + a8 * b7 + r14 = r14 + a9 * b6 + r14 = r14 + a10 * b5 + r14 = r14 + a11 * b4 + r14 = r14 + a12 * b3 + + local r15 = a4 * b12 + r15 = r15 + a5 * b11 + r15 = r15 + a6 * b10 + r15 = r15 + a7 * b9 + r15 = r15 + a8 * b8 + r15 = r15 + a9 * b7 + r15 = r15 + a10 * b6 + r15 = r15 + a11 * b5 + r15 = r15 + a12 * b4 + + local r16 = a5 * b12 + r16 = r16 + a6 * b11 + r16 = r16 + a7 * b10 + r16 = r16 + a8 * b9 + r16 = r16 + a9 * b8 + r16 = r16 + a10 * b7 + r16 = r16 + a11 * b6 + r16 = r16 + a12 * b5 + + local r17 = a6 * b12 + r17 = r17 + a7 * b11 + r17 = r17 + a8 * b10 + r17 = r17 + a9 * b9 + r17 = r17 + a10 * b8 + r17 = r17 + a11 * b7 + r17 = r17 + a12 * b6 + + local r18 = a7 * b12 + r18 = r18 + a8 * b11 + r18 = r18 + a9 * b10 + r18 = r18 + a10 * b9 + r18 = r18 + a11 * b8 + r18 = r18 + a12 * b7 + + local r19 = a8 * b12 + r19 = r19 + a9 * b11 + r19 = r19 + a10 * b10 + r19 = r19 + a11 * b9 + r19 = r19 + a12 * b8 + + local r20 = a9 * b12 + r20 = r20 + a10 * b11 + r20 = r20 + a11 * b10 + r20 = r20 + a12 * b9 + + local r21 = a10 * b12 + r21 = r21 + a11 * b11 + r21 = r21 + a12 * b10 + + local r22 = a11 * b12 + r22 = r22 + a12 * b11 + + local r23 = a12 * b12 + + local r24 = 0 + + r2 = r2 + (r1 / m) + r2 = r2 - r2 % 1 + r1 = r1 % m + r3 = r3 + (r2 / m) + r3 = r3 - r3 % 1 + r2 = r2 % m + r4 = r4 + (r3 / m) + r4 = r4 - r4 % 1 + r3 = r3 % m + r5 = r5 + (r4 / m) + r5 = r5 - r5 % 1 + r4 = r4 % m + r6 = r6 + (r5 / m) + r6 = r6 - r6 % 1 + r5 = r5 % m + r7 = r7 + (r6 / m) + r7 = r7 - r7 % 1 + r6 = r6 % m + r8 = r8 + (r7 / m) + r8 = r8 - r8 % 1 + r7 = r7 % m + r9 = r9 + (r8 / m) + r9 = r9 - r9 % 1 + r8 = r8 % m + r10 = r10 + (r9 / m) + r10 = r10 - r10 % 1 + r9 = r9 % m + r11 = r11 + (r10 / m) + r11 = r11 - r11 % 1 + r10 = r10 % m + r12 = r12 + (r11 / m) + r12 = r12 - r12 % 1 + r11 = r11 % m + r13 = r13 + (r12 / m) + r13 = r13 - r13 % 1 + r12 = r12 % m + r14 = r14 + (r13 / m) + r14 = r14 - r14 % 1 + r13 = r13 % m + r15 = r15 + (r14 / m) + r15 = r15 - r15 % 1 + r14 = r14 % m + r16 = r16 + (r15 / m) + r16 = r16 - r16 % 1 + r15 = r15 % m + r17 = r17 + (r16 / m) + r17 = r17 - r17 % 1 + r16 = r16 % m + r18 = r18 + (r17 / m) + r18 = r18 - r18 % 1 + r17 = r17 % m + r19 = r19 + (r18 / m) + r19 = r19 - r19 % 1 + r18 = r18 % m + r20 = r20 + (r19 / m) + r20 = r20 - r20 % 1 + r19 = r19 % m + r21 = r21 + (r20 / m) + r21 = r21 - r21 % 1 + r20 = r20 % m + r22 = r22 + (r21 / m) + r22 = r22 - r22 % 1 + r21 = r21 % m + r23 = r23 + (r22 / m) + r23 = r23 - r23 % 1 + r22 = r22 % m + r24 = r24 + (r23 / m) + r24 = r24 - r24 % 1 + r23 = r23 % m + + local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24} + + return result +end + +local function reduce384(a) + local result = {unpack(a)} + + while cmp384(result, qn) >= 0 do + local qn = {unpack(qn)} + local qn2 = add384(qn, qn) + while cmp384(result, qn2) > 0 do + qn = qn2 + qn2 = add384(qn2, qn2) + end + result = sub384(result, qn) + end + + result = {unpack(result, 1, 12)} + + return result +end + +local function mul(a, b) + return reduce384(mul384(a, b)) +end + +return { + eq = eq, + cmp = cmp, + bytes = bytes, + fromBytes = fromBytes, + reduce = reduce, + add = add, + sub = sub, + mul = mul, +} diff --git a/sys/modules/opus/crypto/ecc/init.lua b/sys/modules/opus/crypto/ecc/init.lua new file mode 100644 index 0000000..980f699 --- /dev/null +++ b/sys/modules/opus/crypto/ecc/init.lua @@ -0,0 +1,88 @@ +local fq = require('opus.crypto.ecc.fq') +local elliptic = require('opus.crypto.ecc.elliptic') +local sha256 = require('opus.crypto.sha2') + +local os = _G.os +local unpack = table.unpack + +local q = {1372, 62520, 47765, 8105, 45059, 9616, 65535, 65535, 65535, 65535, 65535, 65532} + +local sLen = 24 +local eLen = 24 + +local function hashModQ(sk) + local hash = sha256.hmac({0x00}, sk) + local x + repeat + hash = sha256.digest(hash) + x = fq.fromBytes(hash) + until fq.cmp(x, q) <= 0 + + return x +end + +local function publicKey(sk) + local x = hashModQ(sk) + + local Y = elliptic.scalarMulG(x) + local pk = elliptic.pointEncode(Y) + + return pk +end + +local function exchange(sk, pk) + local Y = elliptic.pointDecode(pk) + local x = hashModQ(sk) + + local Z = elliptic.scalarMul(x, Y) + Z = elliptic.pointScale(Z) + + local ss = fq.bytes(Z[2]) + return sha256.digest(ss) +end + +local function sign(sk, message) + message = type(message) == "table" and string.char(unpack(message)) or message + sk = type(sk) == "table" and string.char(unpack(sk)) or sk + local epoch = tostring(os.epoch("utc")) + local x = hashModQ(sk) + local k = hashModQ(message .. epoch .. sk) + + local R = elliptic.scalarMulG(k) + R = string.char(unpack(elliptic.pointEncode(R))) + local e = hashModQ(R .. message) + local s = fq.sub(k, fq.mul(x, e)) + + e = fq.bytes(e) + s = fq.bytes(s) + + local sig = {unpack(e)} + + for i = 1, #s do + sig[#sig + 1] = s[i] + end + + return sig +end + +local function verify(pk, message, sig) + local Y = elliptic.pointDecode(pk) + local e = {unpack(sig, 1, eLen)} + local s = {unpack(sig, eLen + 1, eLen + sLen)} + + e = fq.fromBytes(e) + s = fq.fromBytes(s) + + local R = elliptic.pointAdd(elliptic.scalarMulG(s), elliptic.scalarMul(e, Y)) + R = string.char(unpack(elliptic.pointEncode(R))) + local e2 = hashModQ(R .. message) + + return fq.eq(e2, e) +end + +return { + publicKey = publicKey, + exchange = exchange, + sign = sign, + verify = verify, +} diff --git a/sys/modules/opus/crypto/sha2.lua b/sys/modules/opus/crypto/sha2.lua new file mode 100644 index 0000000..4dcb98b --- /dev/null +++ b/sys/modules/opus/crypto/sha2.lua @@ -0,0 +1,208 @@ +-- SHA-256, HMAC and PBKDF2 functions in ComputerCraft +-- By Anavrins +local Util = require('opus.util') + +local bit = _G.bit +local mod32 = 2^32 +local band = bit32 and bit32.band or bit.band +local bnot = bit32 and bit32.bnot or bit.bnot +local bxor = bit32 and bit32.bxor or bit.bxor +local blshift = bit32 and bit32.lshift or bit.blshift +local upack = unpack or table.unpack + +local function rrotate(n, b) + local s = n/(2^b) + local f = s%1 + return (s-f) + f*mod32 +end +local function brshift(int, by) + local s = int / (2^by) + return s - s%1 +end + +local H = { + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, +} + +local K = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, +} + +local function counter(incr) + local t1, t2 = 0, 0 + if 0xFFFFFFFF - t1 < incr then + t2 = t2 + 1 + t1 = incr - (0xFFFFFFFF - t1) - 1 + else t1 = t1 + incr + end + return t2, t1 +end + +local function BE_toInt(bs, i) + return blshift((bs[i] or 0), 24) + blshift((bs[i+1] or 0), 16) + blshift((bs[i+2] or 0), 8) + (bs[i+3] or 0) +end + +local function preprocess(data) + local len = #data + local proc = {} + data[#data+1] = 0x80 + while #data%64~=56 do data[#data+1] = 0 end + local blocks = math.ceil(#data/64) + for i = 1, blocks do + proc[i] = {} + for j = 1, 16 do + proc[i][j] = BE_toInt(data, 1+((i-1)*64)+((j-1)*4)) + end + end + proc[blocks][15], proc[blocks][16] = counter(len*8) + return proc +end + +local function digestblock(w, C) + for j = 17, 64 do + -- local v = w[j-15] + local s0 = bxor(bxor(rrotate(w[j-15], 7), rrotate(w[j-15], 18)), brshift(w[j-15], 3)) + local s1 = bxor(bxor(rrotate(w[j-2], 17), rrotate(w[j-2], 19)), brshift(w[j-2], 10)) + w[j] = (w[j-16] + s0 + w[j-7] + s1)%mod32 + end + local a, b, c, d, e, f, g, h = upack(C) + for j = 1, 64 do + local S1 = bxor(bxor(rrotate(e, 6), rrotate(e, 11)), rrotate(e, 25)) + local ch = bxor(band(e, f), band(bnot(e), g)) + local temp1 = (h + S1 + ch + K[j] + w[j])%mod32 + local S0 = bxor(bxor(rrotate(a, 2), rrotate(a, 13)), rrotate(a, 22)) + local maj = bxor(bxor(band(a, b), band(a, c)), band(b, c)) + local temp2 = (S0 + maj)%mod32 + h, g, f, e, d, c, b, a = g, f, e, (d+temp1)%mod32, c, b, a, (temp1+temp2)%mod32 + end + C[1] = (C[1] + a)%mod32 + C[2] = (C[2] + b)%mod32 + C[3] = (C[3] + c)%mod32 + C[4] = (C[4] + d)%mod32 + C[5] = (C[5] + e)%mod32 + C[6] = (C[6] + f)%mod32 + C[7] = (C[7] + g)%mod32 + C[8] = (C[8] + h)%mod32 + return C +end + +local mt = { + __tostring = function(a) return string.char(upack(a)) end, + __index = { + toHex = function(self) return ("%02x"):rep(#self):format(upack(self)) end, + isEqual = function(self, t) + if type(t) ~= "table" then return false end + if #self ~= #t then return false end + local ret = 0 + for i = 1, #self do + ret = bit32.bor(ret, bxor(self[i], t[i])) + end + return ret == 0 + end + } +} + +local function toBytes(t, n) + local b = {} + for i = 1, n do + b[(i-1)*4+1] = band(brshift(t[i], 24), 0xFF) + b[(i-1)*4+2] = band(brshift(t[i], 16), 0xFF) + b[(i-1)*4+3] = band(brshift(t[i], 8), 0xFF) + b[(i-1)*4+4] = band(t[i], 0xFF) + end + return setmetatable(b, mt) +end + +local function digest(data) + data = data or "" + data = type(data) == "table" and {upack(data)} or {tostring(data):byte(1,-1)} + + data = preprocess(data) + local C = {upack(H)} + for i = 1, #data do C = digestblock(data[i], C) end + return toBytes(C, 8) +end + +local function hmac(data, key) + data = type(data) == "table" and {upack(data)} or {tostring(data):byte(1,-1)} + key = type(key) == "table" and {upack(key)} or {tostring(key):byte(1,-1)} + + local blocksize = 64 + + key = #key > blocksize and digest(key) or key + + local ipad = {} + local opad = {} + local padded_key = {} + + for i = 1, blocksize do + ipad[i] = bxor(0x36, key[i] or 0) + opad[i] = bxor(0x5C, key[i] or 0) + end + + for i = 1, #data do + ipad[blocksize+i] = data[i] + end + + ipad = digest(ipad) + + for i = 1, blocksize do + padded_key[i] = opad[i] + padded_key[blocksize+i] = ipad[i] + end + + return digest(padded_key) +end + +local function pbkdf2(pass, salt, iter, dklen) + salt = type(salt) == "table" and salt or {tostring(salt):byte(1,-1)} + local hashlen = 32 + dklen = dklen or 32 + local block = 1 + local out = {} + local throttle = Util.throttle() + + while dklen > 0 do + local ikey = {} + local isalt = {upack(salt)} + local clen = dklen > hashlen and hashlen or dklen + + isalt[#isalt+1] = band(brshift(block, 24), 0xFF) + isalt[#isalt+1] = band(brshift(block, 16), 0xFF) + isalt[#isalt+1] = band(brshift(block, 8), 0xFF) + isalt[#isalt+1] = band(block, 0xFF) + + for j = 1, iter do + isalt = hmac(isalt, pass) + for k = 1, clen do ikey[k] = bxor(isalt[k], ikey[k] or 0) end + if j % 200 == 0 then + throttle() + --os.queueEvent("PBKDF2", j) coroutine.yield("PBKDF2") + end + end + dklen = dklen - clen + block = block+1 + for k = 1, clen do out[#out+1] = ikey[k] end + end + + return setmetatable(out, mt) +end + +local function compute(data) + return digest(data):toHex() +end + +return { + digest = digest, + compute = compute, + hmac = hmac, + pbkdf2 = pbkdf2, +} diff --git a/sys/modules/opus/entry.lua b/sys/modules/opus/entry.lua new file mode 100644 index 0000000..693fe87 --- /dev/null +++ b/sys/modules/opus/entry.lua @@ -0,0 +1,388 @@ +local class = require('opus.class') + +local os = _G.os + +local Entry = class() + +function Entry:init(args) + self.pos = 0 + self.scroll = 0 + self.value = '' + self.width = args.width or 256 + self.limit = args.limit or 1024 + self.mark = { } + self.offset = args.offset or 1 +end + +function Entry:reset() + self.pos = 0 + self.scroll = 0 + self.value = '' + self.mark = { } +end + +function Entry:nextWord() + return select(2, self.value:find("[%s%p]?%w[%s%p]", self.pos + 1)) or #self.value +end + +function Entry:prevWord() + local x = #self.value - (self.pos - 1) + local _, n = self.value:reverse():find("[%s%p]?%w[%s%p]", x) + return n and #self.value - n + 1 or 0 +end + +function Entry:updateScroll() + local ps = self.scroll + if self.pos > #self.value then + self.pos = #self.value + self.scroll = 0 -- ?? + end + if self.pos - self.scroll > self.width then + self.scroll = self.pos - self.width + elseif self.pos < self.scroll then + self.scroll = self.pos + end + if ps ~= self.scroll then + self.textChanged = true + end +end + +function Entry:copyText(cx, ex) + return self.value:sub(cx + 1, ex) +end + +function Entry:insertText(x, text) + if #self.value + #text > self.limit then + text = text:sub(1, self.limit-#self.value) + end + self.value = self.value:sub(1, x) .. text .. self.value:sub(x + 1) + self.pos = self.pos + #text +end + +function Entry:deleteText(sx, ex) + local front = self.value:sub(1, sx) + local back = self.value:sub(ex + 1, #self.value) + self.value = front .. back + self.pos = sx +end + +function Entry:moveLeft() + if self.pos > 0 then + self.pos = self.pos - 1 + return true + end +end + +function Entry:moveRight() + if self.pos < #self.value then + self.pos = self.pos + 1 + return true + end +end + +function Entry:moveHome() + if self.pos ~= 0 then + self.pos = 0 + return true + end +end + +function Entry:moveEnd() + if self.pos ~= #self.value then + self.pos = #self.value + return true + end +end + +function Entry:moveTo(ie) + self.pos = math.max(0, math.min(ie.x + self.scroll - self.offset, #self.value)) +end + +function Entry:backspace() + if self.mark.active then + self:delete() + elseif self:moveLeft() then + self:delete() + end +end + +function Entry:moveWordRight() + if self.pos < #self.value then + self.pos = self:nextWord(self.value, self.pos + 1) + return true + end +end + +function Entry:moveWordLeft() + if self.pos > 0 then + self.pos = self:prevWord(self.value, self.pos - 1) or 0 + return true + end +end + +function Entry:delete() + if self.mark.active then + self:deleteText(self.mark.x, self.mark.ex) + elseif self.pos < #self.value then + self:deleteText(self.pos, self.pos + 1) + end +end + +function Entry:cutFromStart() + if self.pos > 0 then + local text = self:copyText(1, self.pos) + self:deleteText(1, self.pos) + os.queueEvent('clipboard_copy', text) + end +end + +function Entry:cutToEnd() + if self.pos < #self.value then + local text = self:copyText(self.pos, #self.value) + self:deleteText(self.pos, #self.value) + os.queueEvent('clipboard_copy', text) + end +end + +function Entry:cutNextWord() + if self.pos < #self.value then + local ex = self:nextWord(self.value, self.pos) + local text = self:copyText(self.pos, ex) + self:deleteText(self.pos, ex) + os.queueEvent('clipboard_copy', text) + end +end + +function Entry:cutPrevWord() + if self.pos > 0 then + local sx = self:prevWord(self.value, self.pos) + local text = self:copyText(sx, self.pos) + self:deleteText(sx, self.pos) + os.queueEvent('clipboard_copy', text) + end +end + +function Entry:insertChar(ie) + if self.mark.active then + self:delete() + end + self:insertText(self.pos, ie.ch) +end + +function Entry:copy() + if #self.value > 0 then + self.mark.continue = true + if self.mark.active then + self:copyMarked() + else + os.queueEvent('clipboard_copy', self.value) + end + end +end + +function Entry:cut() + if self.mark.active then + self:copyMarked() + self:delete() + end +end + +function Entry:copyMarked() + local text = self:copyText(self.mark.x, self.mark.ex) + os.queueEvent('clipboard_copy', text) +end + +function Entry:paste(ie) + if #ie.text > 0 then + if self.mark.active then + self:delete() + end + self:insertText(self.pos, ie.text) + end +end + +function Entry:clearLine() + if #self.value > 0 then + self:reset() + end +end + +function Entry:markBegin() + if not self.mark.active then + self.mark.active = true + self.mark.anchor = { x = self.pos } + end +end + +function Entry:markFinish() + if self.pos == self.mark.anchor.x then + self.mark.active = false + else + self.mark.x = math.min(self.mark.anchor.x, self.pos) + self.mark.ex = math.max(self.mark.anchor.x, self.pos) + end + self.textChanged = true + self.mark.continue = self.mark.active +end + +function Entry:unmark() + if self.mark.active then + self.textChanged = true + self.mark.active = false + end +end + +function Entry:markAnchor(ie) + self:unmark() + self:moveTo(ie) + self:markBegin() + self:markFinish() +end + +function Entry:markLeft() + self:markBegin() + if self:moveLeft() then + self:markFinish() + end +end + +function Entry:markRight() + self:markBegin() + if self:moveRight() then + self:markFinish() + end +end + +function Entry:markWord(ie) + local index = 1 + self:moveTo(ie) + while true do + local s, e = self.value:find('%w+', index) + if not s or s - 1 > self.pos then + break + end + if self.pos >= s - 1 and self.pos < e then + self.pos = s - 1 + self:markBegin() + self.pos = e + self:markFinish() + self:moveTo(ie) + break + end + index = e + 1 + end +end + +function Entry:markNextWord() + self:markBegin() + if self:moveWordRight() then + self:markFinish() + end +end + +function Entry:markPrevWord() + self:markBegin() + if self:moveWordLeft() then + self:markFinish() + end +end + +function Entry:markAll() + if #self.value > 0 then + self.mark.anchor = { x = 1 } + self.mark.active = true + self.mark.continue = true + self.mark.x = 0 + self.mark.ex = #self.value + self.textChanged = true + end +end + +function Entry:markHome() + self:markBegin() + if self:moveHome() then + self:markFinish() + end +end + +function Entry:markEnd() + self:markBegin() + if self:moveEnd() then + self:markFinish() + end +end + +function Entry:markTo(ie) + self:markBegin() + self:moveTo(ie) + self:markFinish() +end + +local mappings = { + [ 'left' ] = Entry.moveLeft, + [ 'control-b' ] = Entry.moveLeft, + [ 'right' ] = Entry.moveRight, + [ 'control-f' ] = Entry.moveRight, + [ 'home' ] = Entry.moveHome, + [ 'end' ] = Entry.moveEnd, + [ 'control-e' ] = Entry.moveEnd, + [ 'mouse_click' ] = Entry.moveTo, + [ 'control-right' ] = Entry.moveWordRight, + [ 'alt-f' ] = Entry.moveWordRight, + [ 'control-left' ] = Entry.moveWordLeft, + [ 'alt-b' ] = Entry.moveWordLeft, + + [ 'backspace' ] = Entry.backspace, + [ 'delete' ] = Entry.delete, + [ 'char' ] = Entry.insertChar, + [ 'mouse_rightclick' ] = Entry.clearLine, + + [ 'control-c' ] = Entry.copy, + [ 'control-u' ] = Entry.cutFromStart, + [ 'control-k' ] = Entry.cutToEnd, + [ 'control-w' ] = Entry.cutPrevWord, + --[ 'control-d' ] = Entry.cutNextWord, + [ 'control-x' ] = Entry.cut, + [ 'paste' ] = Entry.paste, +-- [ 'control-y' ] = Entry.paste, -- well this won't work... + + [ 'mouse_doubleclick' ] = Entry.markWord, + [ 'shift-left' ] = Entry.markLeft, + [ 'shift-right' ] = Entry.markRight, + [ 'mouse_down' ] = Entry.markAnchor, + [ 'mouse_drag' ] = Entry.markTo, + [ 'shift-mouse_click' ] = Entry.markTo, + [ 'control-a' ] = Entry.markAll, + [ 'control-shift-right' ] = Entry.markNextWord, + [ 'control-shift-left' ] = Entry.markPrevWord, + [ 'shift-end' ] = Entry.markEnd, + [ 'shift-home' ] = Entry.markHome, +} + +function Entry:process(ie) + local action = mappings[ie.code] + + self.textChanged = false + + if action then + local pos = self.pos + local line = self.value + + local wasMarking = self.mark.continue + self.mark.continue = false + + action(self, ie) + + self.textChanged = self.textChanged or self.value ~= line + self.posChanged = pos ~= self.pos + self:updateScroll() + + if not self.mark.continue and wasMarking then + self:unmark() + end + + return true + end +end + +return Entry diff --git a/sys/apis/event.lua b/sys/modules/opus/event.lua similarity index 84% rename from sys/apis/event.lua rename to sys/modules/opus/event.lua index 2a66542..924461e 100644 --- a/sys/apis/event.lua +++ b/sys/modules/opus/event.lua @@ -5,9 +5,8 @@ local Event = { uid = 1, -- unique id for handlers routines = { }, -- coroutines types = { }, -- event handlers - timers = { }, -- named timers terminate = false, - free = { }, + free = { }, -- allocated unused coroutines } -- Use a pool of coroutines for event handlers @@ -68,7 +67,7 @@ function Routine:resume(event, ...) end if not s and event ~= 'terminate' then - error('\n' .. (m or 'Error processing event')) + error(m or 'Error processing event', -1) end return s, m @@ -118,57 +117,31 @@ function Event.off(h) end end -local function addTimer(interval, recurring, fn) - local timerId = os.startTimer(interval) - local handler - - handler = Event.on('timer', function(t, id) - if timerId == id then - fn(t, id) - if recurring then - timerId = os.startTimer(interval) - else - Event.off(handler) - end - end - end) - - return handler -end - function Event.onInterval(interval, fn) - return Event.addRoutine(function() + local h = Event.addRoutine(function() while true do os.sleep(interval) fn() end end) + function h.updateInterval(i) + interval = i + end + return h end function Event.onTimeout(timeout, fn) - return addTimer(timeout, false, fn) -end - -function Event.addNamedTimer(name, interval, recurring, fn) - Event.cancelNamedTimer(name) - Event.timers[name] = addTimer(interval, recurring, fn) -end - -function Event.cancelNamedTimer(name) - local timer = Event.timers[name] - if timer then - Event.off(timer) - end -end - -function Event.waitForEvent(event, timeout) local timerId = os.startTimer(timeout) - repeat - local e = { os.pullEvent() } - if e[1] == event then - return table.unpack(e) + local handler + + handler = Event.on('timer', function(t, id) + if timerId == id then + fn(t, id) + Event.off(handler) end - until e[1] == 'timer' and e[2] == timerId + end) + + return handler end -- Set a handler for the terminate event. Within the function, return @@ -243,6 +216,21 @@ local function processRoutines(...) end end +-- invoke the handlers registered for this event +function Event.trigger(event, ...) + local handlers = Event.types[event] + if handlers then + for _,h in pairs(handlers) do + if not h.co then + -- callbacks are single threaded (only 1 co per handler) + h.co = createCoroutine(h) + Event.routines[h.uid] = h + h:resume(event, ...) + end + end + end +end + function Event.processEvent(e) processHandlers(e[1]) processRoutines(table.unpack(e)) diff --git a/sys/apis/fs/gitfs.lua b/sys/modules/opus/fs/gitfs.lua similarity index 91% rename from sys/apis/fs/gitfs.lua rename to sys/modules/opus/fs/gitfs.lua index 026d9e8..21fe1d9 100644 --- a/sys/apis/fs/gitfs.lua +++ b/sys/modules/opus/fs/gitfs.lua @@ -1,4 +1,4 @@ -local git = require('git') +local git = require('opus.git') local fs = _G.fs diff --git a/sys/apis/fs/linkfs.lua b/sys/modules/opus/fs/linkfs.lua similarity index 92% rename from sys/apis/fs/linkfs.lua rename to sys/modules/opus/fs/linkfs.lua index e46342c..79ec13d 100644 --- a/sys/apis/fs/linkfs.lua +++ b/sys/modules/opus/fs/linkfs.lua @@ -2,6 +2,8 @@ local fs = _G.fs local linkfs = { } +-- TODO: implement broken links + local methods = { 'exists', 'getFreeSpace', 'getSize', 'isDir', 'isReadOnly', 'list', 'listEx', 'makeDir', 'open', 'getDrive' } @@ -17,6 +19,9 @@ function linkfs.mount(_, source) error('Source is required') end source = fs.combine(source, '') + if not fs.exists(source) then + error('Source is missing') + end if fs.isDir(source) then return { source = source, diff --git a/sys/apis/fs/netfs.lua b/sys/modules/opus/fs/netfs.lua similarity index 95% rename from sys/apis/fs/netfs.lua rename to sys/modules/opus/fs/netfs.lua index d7eb2eb..e36a1cc 100644 --- a/sys/apis/fs/netfs.lua +++ b/sys/modules/opus/fs/netfs.lua @@ -1,5 +1,5 @@ -local Socket = require('socket') -local synchronized = require('sync').sync +local Socket = require('opus.socket') +local synchronized = require('opus.sync').sync local fs = _G.fs @@ -36,6 +36,7 @@ end local methods = { 'delete', 'exists', 'getFreeSpace', 'makeDir', 'list', 'listEx' } local function resolveDir(dir, node) + -- TODO: Wrong ! (does not support names with dashes) dir = dir:gsub(node.mountPoint, '', 1) return fs.combine(node.directory, dir) end diff --git a/sys/apis/fs/ramfs.lua b/sys/modules/opus/fs/ramfs.lua similarity index 96% rename from sys/apis/fs/ramfs.lua rename to sys/modules/opus/fs/ramfs.lua index 5dc7470..1ed2f7a 100644 --- a/sys/apis/fs/ramfs.lua +++ b/sys/modules/opus/fs/ramfs.lua @@ -1,4 +1,4 @@ -local Util = require('util') +local Util = require('opus.util') local fs = _G.fs @@ -48,6 +48,10 @@ function ramfs.getDrive() return 'ram' end +function ramfs.getFreeSpace() + return math.huge +end + function ramfs.list(node, dir) if node.nodes and node.mountPoint == dir then local files = { } diff --git a/sys/apis/fs/urlfs.lua b/sys/modules/opus/fs/urlfs.lua similarity index 92% rename from sys/apis/fs/urlfs.lua rename to sys/modules/opus/fs/urlfs.lua index 6feb4f0..960806e 100644 --- a/sys/apis/fs/urlfs.lua +++ b/sys/modules/opus/fs/urlfs.lua @@ -1,5 +1,5 @@ -local rttp = require('rttp') -local Util = require('util') +--local rttp = require('rttp') +local Util = require('opus.util') local fs = _G.fs @@ -39,7 +39,6 @@ function urlfs.getDrive() end function urlfs.open(node, fn, fl) - if fl == 'w' or fl == 'wb' then fs.delete(fn) return fs.open(fn, fl) @@ -51,12 +50,15 @@ function urlfs.open(node, fn, fl) local c = node.cache if not c then + --[[ if node.url:match("^(rttps?:)") then local s, response = rttp.get(node.url) c = s and response.statusCode == 200 and response.data else c = Util.httpGet(node.url) end + ]]-- + c = Util.httpGet(node.url) if c then node.cache = c node.size = #c diff --git a/sys/apis/git.lua b/sys/modules/opus/git.lua similarity index 72% rename from sys/apis/git.lua rename to sys/modules/opus/git.lua index 02fa103..da18ecf 100644 --- a/sys/apis/git.lua +++ b/sys/modules/opus/git.lua @@ -1,8 +1,5 @@ -local json = require('json') -local Util = require('util') - --- Limit queries to once per minecraft day --- TODO: will not work if time is stopped +local json = require('opus.json') +local Util = require('opus.util') local TREE_URL = 'https://api.github.com/repos/%s/%s/git/trees/%s?recursive=1' local FILE_URL = 'https://raw.githubusercontent.com/%s/%s/%s/%s' @@ -15,10 +12,6 @@ end local fs = _G.fs local os = _G.os -if not _G.GIT then - _G.GIT = { } -end - function git.list(repository) local t = Util.split(repository, '(.-)/') @@ -31,17 +24,7 @@ function git.list(repository) path = table.concat(t, '/') .. '/' end - local cacheKey = table.concat({ user, repo, branch }, '-') - local fname = fs.combine('.git', cacheKey) - local function getContents() - if fs.exists(fname) then - local contents = Util.readTable(fname) - if contents and contents.data == os.day() then - return contents.data - end - fs.delete(fname) - end local dataUrl = string.format(TREE_URL, user, repo, branch) local contents = Util.download(dataUrl) if contents then @@ -59,10 +42,6 @@ function git.list(repository) error("Invalid repository") end - if not fs.exists(fname) then - Util.writeTable('.git/' .. cacheKey, { day = os.day(), data = data }) - end - local list = { } for _,v in pairs(data.tree) do if v.type == "blob" then diff --git a/sys/apis/gps.lua b/sys/modules/opus/gps.lua similarity index 65% rename from sys/apis/gps.lua rename to sys/modules/opus/gps.lua index cf53d61..0ca7a66 100644 --- a/sys/apis/gps.lua +++ b/sys/modules/opus/gps.lua @@ -1,8 +1,9 @@ +local Util = require('opus.util') + local GPS = { } local device = _G.device local gps = _G.gps -local turtle = _G.turtle function GPS.locate(timeout, debug) local pt = { } @@ -34,53 +35,6 @@ function GPS.getPoint(timeout, debug) return pt end -function GPS.getHeading(timeout) - - if not turtle then - return - end - - local apt = GPS.locate(timeout) - if not apt then - return - end - - local heading = turtle.point.heading - - while not turtle.forward() do - turtle.turnRight() - if turtle.getHeading() == heading then - _G.printError('GPS.getPoint: Unable to move forward') - return - end - end - - local bpt = GPS.locate() - if not bpt then - return - end - - if apt.x < bpt.x then - return 0 - elseif apt.z < bpt.z then - return 1 - elseif apt.x > bpt.x then - return 2 - end - return 3 -end - -function GPS.getPointAndHeading(timeout) - local heading = GPS.getHeading(timeout) - if heading then - local pt = GPS.getPoint() - if pt then - pt.heading = heading - end - return pt - end -end - -- from stock gps API local function trilaterate(A, B, C) local a2b = B.position - A.position @@ -137,19 +91,17 @@ end -- end stock gps api function GPS.trilaterate(tFixes) - local pos1, pos2 = trilaterate(tFixes[1], tFixes[2], tFixes[3]) - - if pos2 then - pos1, pos2 = narrow(pos1, pos2, tFixes[4]) + local attemps = 0 + for tFixes in Util.permutation(tFixes) do + attemps = attemps + 1 + local pos1, pos2 = trilaterate(tFixes[4], tFixes[3], tFixes[2]) + if pos2 then + pos1, pos2 = narrow(pos1, pos2, tFixes[1]) + end + if not pos2 then + return pos1, attemps + end end - - if pos1 and pos2 then - print("Ambiguous position") - print("Could be "..pos1.x..","..pos1.y..","..pos1.z.." or "..pos2.x..","..pos2.y..","..pos2.z ) - return - end - - return pos1 end return GPS \ No newline at end of file diff --git a/sys/apis/history.lua b/sys/modules/opus/history.lua similarity index 96% rename from sys/apis/history.lua rename to sys/modules/opus/history.lua index 7162a48..99cc060 100644 --- a/sys/apis/history.lua +++ b/sys/modules/opus/history.lua @@ -1,4 +1,4 @@ -local Util = require('util') +local Util = require('opus.util') local History = { } local History_mt = { __index = History } diff --git a/sys/modules/opus/http/pastebin.lua b/sys/modules/opus/http/pastebin.lua new file mode 100644 index 0000000..a6cf60b --- /dev/null +++ b/sys/modules/opus/http/pastebin.lua @@ -0,0 +1,128 @@ +--- Parse the pastebin code from the given code or URL +local function parseCode(paste) + local patterns = { + "^([%a%d]+)$", + "^https?://pastebin.com/([%a%d]+)$", + "^pastebin.com/([%a%d]+)$", + "^https?://pastebin.com/raw/([%a%d]+)$", + "^pastebin.com/raw/([%a%d]+)$", + } + + for i = 1, #patterns do + local code = paste:match(patterns[i]) + if code then + return code + end + end + + return nil +end + +-- Download the contents of a paste +local function download(code) + if type(code) ~= "string" then + error("bad argument #1 (expected string, got " .. type(code) .. ")", 2) + end + + if not http then + return false, "Pastebin requires http API" + end + + -- Add a cache buster so that spam protection is re-checked + local cacheBuster = ("%x"):format(math.random(0, 2 ^ 30)) + local response, err = http.get( + "https://pastebin.com/raw/" .. textutils.urlEncode(code) .. "?cb=" .. cacheBuster + ) + + if not response then + return response, err + end + + -- If spam protection is activated, we get redirected to /paste with Content-Type: text/html + local headers = response.getResponseHeaders() + if not headers["Content-Type"] or not headers["Content-Type"]:find("^text/plain") then + return false, "Pastebin blocked due to spam protection" + end + + local contents = response.readAll() + response.close() + return contents +end + +-- Upload text to pastebin +local function upload(name, text) + if not http then + return false, "Pastebin requires http API" + end + + -- POST the contents to pastebin + local key = "0ec2eb25b6166c0c27a394ae118ad829" + local response = http.post( + "https://pastebin.com/api/api_post.php", + "api_option=paste&" .. + "api_dev_key=" .. key .. "&" .. + "api_paste_format=lua&" .. + "api_paste_name=" .. textutils.urlEncode(name) .. "&" .. + "api_paste_code=" .. textutils.urlEncode(text) + ) + + if not response then + return false, "Failed." + end + + local contents = response.readAll() + response.close() + + return string.match(contents, "[^/]+$") +end + +-- Download the contents to a file from pastebin +local function get(code, path) + if type(code) ~= "string" then + error( "bad argument #1 (expected string, got " .. type(code) .. ")", 2) + end + + if type(path) ~= "string" then + error("bad argument #2 (expected string, got " .. type(path) .. ")", 2) + end + + local res, msg = download(code) + if not res then + return res, msg + end + + local file = fs.open(path, "w") + file.write(res) + file.close() + + return true +end + +-- Upload a file to pastebin.com +local function put(path) + if type(path) ~= "string" then + error("bad argument #1 (expected string, got " .. type(path) .. ")", 2) + end + + -- Determine file to upload + if not fs.exists(path) or fs.isDir(path) then + return false, "No such file" + end + + -- Read in the file + local name = fs.getName(path) + local file = fs.open(path, "r") + local text = file.readAll() + file.close() + + return upload(name, text) +end + +return { + download = download, + upload = upload, + get = get, + put = put, + parseCode = parseCode, +} + diff --git a/sys/modules/opus/injector.lua b/sys/modules/opus/injector.lua new file mode 100644 index 0000000..e93e5d1 --- /dev/null +++ b/sys/modules/opus/injector.lua @@ -0,0 +1,119 @@ +local function split(str, pattern) + local t = { } + local function helper(line) table.insert(t, line) return "" end + helper((str:gsub(pattern, helper))) + return t +end + +local hasMain +local luaPaths = package and package.path and split(package.path, '(.-);') or { } +for i = 1, #luaPaths do + if luaPaths[i] == '?' or luaPaths[i] == '?.lua' or luaPaths[i] == '?/init.lua' then + luaPaths[i] = nil + elseif string.find(luaPaths[i], '/rom/modules/main') then + hasMain = true + end +end + +table.insert(luaPaths, 1, '?.lua') +table.insert(luaPaths, 2, '?/init.lua') +table.insert(luaPaths, 3, '/usr/modules/?.lua') +table.insert(luaPaths, 4, '/usr/modules/?/init.lua') +if not hasMain then + table.insert(luaPaths, 5, '/rom/modules/main/?') + table.insert(luaPaths, 6, '/rom/modules/main/?.lua') + table.insert(luaPaths, 7, '/rom/modules/main/?/init.lua') +end +table.insert(luaPaths, '/sys/modules/?.lua') +table.insert(luaPaths, '/sys/modules/?/init.lua') + +local DEFAULT_PATH = table.concat(luaPaths, ';') + +local fs = _G.fs +local os = _G.os +local string = _G.string + +-- Add require and package to the environment +return function(env) + local function preloadSearcher(modname) + if env.package.preload[modname] then + return function() + return env.package.preload[modname](modname, env) + end + end + end + + local function loadedSearcher(modname) + if env.package.loaded[modname] then + return function() + return env.package.loaded[modname] + end + end + end + + local sentinel = { } + + local function pathSearcher(modname) + if env.package.loaded[modname] == sentinel then + error("loop or previous error loading module '" .. modname .. "'", 0) + end + env.package.loaded[modname] = sentinel + + local fname = modname:gsub('%.', '/') + + for pattern in string.gmatch(env.package.path, "[^;]+") do + local sPath = string.gsub(pattern, "%?", fname) + -- TODO: if there's no shell, we should not be checking relative paths below + -- as they will resolve to root directory + if env.shell and + type(env.shell.getRunningProgram) == 'function' and + sPath:sub(1, 1) ~= "/" then + + sPath = fs.combine(fs.getDir(env.shell.getRunningProgram() or ''), sPath) + end + if fs.exists(sPath) and not fs.isDir(sPath) then + return loadfile(sPath, env) + end + end + end + + -- place package and require function into env + env.package = { + path = env.LUA_PATH or _G.LUA_PATH or DEFAULT_PATH, + config = '/\n:\n?\n!\n-', + preload = { }, + loaded = { + coroutine = coroutine, + io = io, + math = math, + os = os, + string = string, + table = table, + }, + loaders = { + preloadSearcher, + loadedSearcher, + pathSearcher, + } + } + + function env.require(modname) + for _,searcher in ipairs(env.package.loaders) do + local fn, msg = searcher(modname) + if fn then + local module, msg2 = fn(modname, env) + if not module then + error(msg2 or (modname .. ' module returned nil'), 2) + end + env.package.loaded[modname] = module + return module + end + if msg then + error(msg, 2) + end + end + error('Unable to find module ' .. modname, 2) + end + + return env.require -- backwards compatible +end diff --git a/sys/apis/input.lua b/sys/modules/opus/input.lua similarity index 72% rename from sys/apis/input.lua rename to sys/modules/opus/input.lua index d03036d..46b58fe 100644 --- a/sys/apis/input.lua +++ b/sys/modules/opus/input.lua @@ -1,4 +1,4 @@ -local Util = require('util') +local Util = require('opus.util') local keyboard = _G.device and _G.device.keyboard local keys = _G.keys @@ -10,14 +10,19 @@ local modifiers = Util.transpose { keys.leftAlt, keys.rightAlt, } -local input = { - state = { }, -} +if not keyboard then -- not running under Opus OS + keyboard = { state = { } } -if not keyboard then - keyboard = { state = input.state } + local Event = require('opus.event') + Event.on({ 'key', 'key_up' }, function(event, code) + if modifiers[code] then + keyboard.state[code] = event == 'key' + end + end) end +local input = { } + function input:modifierPressed() return keyboard.state[keys.leftCtrl] or keyboard.state[keys.rightCtrl] or @@ -39,11 +44,11 @@ function input:toCode(ch, code) -- the key-up event for alt keys is not generated if the minecraft -- window loses focus - -- - -- if keyboard.state[keys.leftAlt] or keyboard.state[keys.rightAlt] or - -- code == keys.leftAlt or code == keys.rightAlt then - -- table.insert(result, 'alt') - -- end + + if keyboard.state[keys.leftAlt] or keyboard.state[keys.rightAlt] or + code == keys.leftAlt or code == keys.rightAlt then + table.insert(result, 'alt') + end if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] or code == keys.leftShift or code == keys.rightShift then @@ -64,49 +69,45 @@ end function input:reset() self.state = { } - self.fired = nil self.timer = nil self.mch = nil self.mfired = nil end +local function isCombo() + -- allow control-alt combinations for certain keyboards + return (keyboard.state[keys.leftAlt] or keyboard.state[keys.rightAlt]) and + (keyboard.state[keys.leftCtrl] or keyboard.state[keys.rightCtrl]) +end + function input:translate(event, code, p1, p2) if event == 'key' then if p1 then -- key is held down if not modifiers[code] then - self.fired = true - return { code = input:toCode(keys.getName(code), code) } + local ch = input:toCode(keys.getName(code), code) + if #ch == 1 then + return { + code = 'char', + ch = ch, + } + end + return { code = ch } end - else - self.state[code] = true - if self:modifierPressed() and not modifiers[code] or code == 57 then - self.fired = true - return { code = input:toCode(keys.getName(code), code) } - else - self.fired = false + elseif code then + local ch = input:toCode(keys.getName(code), code) + if #ch ~= 1 then + return { code = ch } end end elseif event == 'char' then - if not self:modifierPressed() then - self.fired = true - return { code = event, ch = input:toCode(code) } + local combo = isCombo() + if combo or not (keyboard.state[keys.leftCtrl] or keyboard.state[keys.rightCtrl]) then + return { code = event, ch = code } end - elseif event == 'key_up' then - if not self.fired then - if self.state[code] then - self.fired = true - local ch = input:toCode(keys.getName(code), code) - self.state[code] = nil - return { code = ch } - end - end - self.state[code] = nil - elseif event == 'paste' then - self.fired = true if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] then return { code = 'shift-paste', text = code } else @@ -117,10 +118,15 @@ function input:translate(event, code, p1, p2) local buttons = { 'mouse_click', 'mouse_rightclick' } self.mch = buttons[code] self.mfired = nil + return { + code = input:toCode('mouse_down', 255), + button = code, + x = p1, + y = p2, + } elseif event == 'mouse_drag' then self.mfired = true - self.fired = true return { code = input:toCode('mouse_drag', 255), button = code, @@ -147,7 +153,6 @@ function input:translate(event, code, p1, p2) self.mch = 'mouse_up' self.mfired = input:toCode(self.mch, 255) end - self.fired = true return { code = self.mfired, button = code, @@ -160,7 +165,6 @@ function input:translate(event, code, p1, p2) [ -1 ] = 'scroll_up', [ 1 ] = 'scroll_down' } - self.fired = true return { code = input:toCode(directions[code], 255), x = p1, @@ -181,4 +185,4 @@ function input:test() end end -return input \ No newline at end of file +return input diff --git a/sys/modules/opus/json.lua b/sys/modules/opus/json.lua new file mode 100644 index 0000000..04b1cd1 --- /dev/null +++ b/sys/modules/opus/json.lua @@ -0,0 +1,589 @@ +-- Module options: +local register_global_module_table = false +local global_module_name = 'json' + +--[==[ +NOTE: Modified to reduce file size. +See https://github.com/LuaDist/dkjson/blob/master/dkjson.lua +for full version. + +David Kolf's JSON module for Lua 5.1/5.2 +Version 2.5 + +For the documentation see the corresponding readme.txt or visit +. + +You can contact the author by sending an e-mail to 'david' at the +domain 'dkolf.de'. + +Copyright (C) 2010-2014 David Heiko Kolf + +Refer to license located at https://github.com/LuaDist/dkjson/blob/master/dkjson.lua + +--]==] + +-- global dependencies: +local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset = + pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset +local error, require, pcall, select = error, require, pcall, select +local floor, huge = math.floor, math.huge +local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat = + string.rep, string.gsub, string.sub, string.byte, string.char, + string.find, string.len, string.format +local strmatch = string.match +local concat = table.concat + +local json = { version = "dkjson 2.5" } + +if register_global_module_table then + _G[global_module_name] = json +end + +local _ENV = nil -- blocking globals in Lua 5.2 + +pcall (function() + -- Enable access to blocked metatables. + -- Don't worry, this module doesn't change anything in them. + local debmeta = require "debug".getmetatable + if debmeta then getmetatable = debmeta end +end) + +json.null = setmetatable ({}, { + __tojson = function () return "null" end +}) + +local function isarray (tbl) + local max, n, arraylen = 0, 0, 0 + for k,v in pairs (tbl) do + if k == 'n' and type(v) == 'number' then + arraylen = v + if v > max then + max = v + end + else + if type(k) ~= 'number' or k < 1 or floor(k) ~= k then + return false + end + if k > max then + max = k + end + n = n + 1 + end + end + if max > 10 and max > arraylen and max > n * 2 then + return false -- don't create an array with too many holes + end + return true, max +end + +local escapecodes = { + ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f", + ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t" +} + +local function escapeutf8 (uchar) + local value = escapecodes[uchar] + if value then + return value + end + local a, b, c, d = strbyte (uchar, 1, 4) + a, b, c, d = a or 0, b or 0, c or 0, d or 0 + if a <= 0x7f then + value = a + elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then + value = (a - 0xc0) * 0x40 + b - 0x80 + elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then + value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80 + elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then + value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80 + else + return "" + end + if value <= 0xffff then + return strformat ("\\u%.4x", value) + elseif value <= 0x10ffff then + -- encode as UTF-16 surrogate pair + value = value - 0x10000 + local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400) + return strformat ("\\u%.4x\\u%.4x", highsur, lowsur) + else + return "" + end +end + +local function fsub (str, pattern, repl) + -- gsub always builds a new string in a buffer, even when no match + -- exists. First using find should be more efficient when most strings + -- don't contain the pattern. + if strfind (str, pattern) then + return gsub (str, pattern, repl) + else + return str + end +end + +local function quotestring (value) + -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js + value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8) + if strfind (value, "[\194\216\220\225\226\239]") then + value = fsub (value, "\194[\128-\159\173]", escapeutf8) + value = fsub (value, "\216[\128-\132]", escapeutf8) + value = fsub (value, "\220\143", escapeutf8) + value = fsub (value, "\225\158[\180\181]", escapeutf8) + value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8) + value = fsub (value, "\226\129[\160-\175]", escapeutf8) + value = fsub (value, "\239\187\191", escapeutf8) + value = fsub (value, "\239\191[\176-\191]", escapeutf8) + end + return "\"" .. value .. "\"" +end +json.quotestring = quotestring + +local function replace(str, o, n) + local i, j = strfind (str, o, 1, true) + if i then + return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1) + else + return str + end +end + +-- locale independent num2str and str2num functions +local decpoint, numfilter + +local function updatedecpoint () + decpoint = strmatch(tostring(0.5), "([^05+])") + -- build a filter that can be used to remove group separators + numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+" +end + +updatedecpoint() + +local function num2str (num) + return replace(fsub(tostring(num), numfilter, ""), decpoint, ".") +end + +local function str2num (str) + local num = tonumber(replace(str, ".", decpoint)) + if not num then + updatedecpoint() + num = tonumber(replace(str, ".", decpoint)) + end + return num +end + +local function addnewline2 (level, buffer, buflen) + buffer[buflen+1] = "\n" + buffer[buflen+2] = strrep (" ", level) + buflen = buflen + 2 + return buflen +end + +function json.addnewline (state) + if state.indent then + state.bufferlen = addnewline2 (state.level or 0, + state.buffer, state.bufferlen or #(state.buffer)) + end +end + +local encode2 -- forward declaration + +local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state) + local kt = type (key) + if kt ~= 'string' and kt ~= 'number' then + return nil, "type '" .. kt .. "' is not supported as a key by JSON." + end + if prev then + buflen = buflen + 1 + buffer[buflen] = "," + end + if indent then + buflen = addnewline2 (level, buffer, buflen) + end + buffer[buflen+1] = quotestring (key) + buffer[buflen+2] = ":" + return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state) +end + +local function appendcustom(res, buffer, state) + local buflen = state.bufferlen + if type (res) == 'string' then + buflen = buflen + 1 + buffer[buflen] = res + end + return buflen +end + +local function exception(reason, value, state, buffer, buflen, defaultmessage) + defaultmessage = defaultmessage or reason + local handler = state.exception + if not handler then + return nil, defaultmessage + else + state.bufferlen = buflen + local ret, msg = handler (reason, value, state, defaultmessage) + if not ret then return nil, msg or defaultmessage end + return appendcustom(ret, buffer, state) + end +end + +function json.encodeexception(reason, value, state, defaultmessage) + return quotestring("<" .. defaultmessage .. ">") +end + +encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state) + local valtype = type (value) + local valmeta = getmetatable (value) + valmeta = type (valmeta) == 'table' and valmeta -- only tables + local valtojson = valmeta and valmeta.__tojson + if valtojson then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + state.bufferlen = buflen + local ret, msg = valtojson (value, state) + if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end + tables[value] = nil + buflen = appendcustom(ret, buffer, state) + elseif value == nil then + buflen = buflen + 1 + buffer[buflen] = "null" + elseif valtype == 'number' then + local s + if value ~= value or value >= huge or -value >= huge then + -- This is the behaviour of the original JSON implementation. + s = "null" + else + s = num2str (value) + end + buflen = buflen + 1 + buffer[buflen] = s + elseif valtype == 'boolean' then + buflen = buflen + 1 + buffer[buflen] = value and "true" or "false" + elseif valtype == 'string' then + buflen = buflen + 1 + buffer[buflen] = quotestring (value) + elseif valtype == 'table' then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + level = level + 1 + local isa, n = isarray (value) + if n == 0 and valmeta and valmeta.__jsontype == 'object' then + isa = false + end + local msg + if isa then -- JSON array + buflen = buflen + 1 + buffer[buflen] = "[" + for i = 1, n do + buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + if i < n then + buflen = buflen + 1 + buffer[buflen] = "," + end + end + buflen = buflen + 1 + buffer[buflen] = "]" + else -- JSON object + local prev = false + buflen = buflen + 1 + buffer[buflen] = "{" + local order = valmeta and valmeta.__jsonorder or globalorder + if order then + local used = {} + n = #order + for i = 1, n do + local k = order[i] + local v = value[k] + if v then + used[k] = true + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + prev = true -- add a seperator before the next element + end + end + for k,v in pairs (value) do + if not used[k] then + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + else -- unordered + for k,v in pairs (value) do + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + if indent then + buflen = addnewline2 (level - 1, buffer, buflen) + end + buflen = buflen + 1 + buffer[buflen] = "}" + end + tables[value] = nil + else + return exception ('unsupported type', value, state, buffer, buflen, + "type '" .. valtype .. "' is not supported by JSON.") + end + return buflen +end + +function json.encode (value, state) + state = state or {} + local oldbuffer = state.buffer + local buffer = oldbuffer or {} + state.buffer = buffer + updatedecpoint() + local ret, msg = encode2 (value, state.indent, state.level or 0, + buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state) + if not ret then + error (msg, 2) + elseif oldbuffer == buffer then + state.bufferlen = ret + return true + else + state.bufferlen = nil + state.buffer = nil + return concat (buffer) + end +end + +local function loc (str, where) + local line, pos, linepos = 1, 1, 0 + while true do + pos = strfind (str, "\n", pos, true) + if pos and pos < where then + line = line + 1 + linepos = pos + pos = pos + 1 + else + break + end + end + return "line " .. line .. ", column " .. (where - linepos) +end + +local function unterminated (str, what, where) + return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where) +end + +local function scanwhite (str, pos) + while true do + pos = strfind (str, "%S", pos) + if not pos then return nil end + local sub2 = strsub (str, pos, pos + 1) + if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then + -- UTF-8 Byte Order Mark + pos = pos + 3 + elseif sub2 == "//" then + pos = strfind (str, "[\n\r]", pos + 2) + if not pos then return nil end + elseif sub2 == "/*" then + pos = strfind (str, "*/", pos + 2) + if not pos then return nil end + pos = pos + 2 + else + return pos + end + end +end + +local escapechars = { + ["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f", + ["n"] = "\n", ["r"] = "\r", ["t"] = "\t" +} + +local function unichar (value) + if value < 0 then + return nil + elseif value <= 0x007f then + return strchar (value) + elseif value <= 0x07ff then + return strchar (0xc0 + floor(value/0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0xffff then + return strchar (0xe0 + floor(value/0x1000), + 0x80 + (floor(value/0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0x10ffff then + return strchar (0xf0 + floor(value/0x40000), + 0x80 + (floor(value/0x1000) % 0x40), + 0x80 + (floor(value/0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + else + return nil + end +end + +local function scanstring (str, pos) + local lastpos = pos + 1 + local buffer, n = {}, 0 + while true do + local nextpos = strfind (str, "[\"\\]", lastpos) + if not nextpos then + return unterminated (str, "string", pos) + end + if nextpos > lastpos then + n = n + 1 + buffer[n] = strsub (str, lastpos, nextpos - 1) + end + if strsub (str, nextpos, nextpos) == "\"" then + lastpos = nextpos + 1 + break + else + local escchar = strsub (str, nextpos + 1, nextpos + 1) + local value + if escchar == "u" then + value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16) + if value then + local value2 + if 0xD800 <= value and value <= 0xDBff then + -- we have the high surrogate of UTF-16. Check if there is a + -- low surrogate escaped nearby to combine them. + if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then + value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16) + if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then + value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000 + else + value2 = nil -- in case it was out of range for a low surrogate + end + end + end + value = value and unichar (value) + if value then + if value2 then + lastpos = nextpos + 12 + else + lastpos = nextpos + 6 + end + end + end + end + if not value then + value = escapechars[escchar] or escchar + lastpos = nextpos + 2 + end + n = n + 1 + buffer[n] = value + end + end + if n == 1 then + return buffer[1], lastpos + elseif n > 1 then + return concat (buffer), lastpos + else + return "", lastpos + end +end + +local scanvalue -- forward declaration + +local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta) + local len = strlen (str) + local tbl, n = {}, 0 + local pos = startpos + 1 + if what == 'object' then + setmetatable (tbl, objectmeta) + else + setmetatable (tbl, arraymeta) + end + while true do + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + local char = strsub (str, pos, pos) + if char == closechar then + return tbl, pos + 1 + end + local val1, err + val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + char = strsub (str, pos, pos) + if char == ":" then + if val1 == nil then + return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")" + end + pos = scanwhite (str, pos + 1) + if not pos then return unterminated (str, what, startpos) end + local val2 + val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + tbl[val1] = val2 + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + char = strsub (str, pos, pos) + else + n = n + 1 + tbl[n] = val1 + end + if char == "," then + pos = pos + 1 + end + end +end + +scanvalue = function (str, pos, nullval, objectmeta, arraymeta) + pos = pos or 1 + pos = scanwhite (str, pos) + if not pos then + return nil, strlen (str) + 1, "no valid JSON value (reached the end)" + end + local char = strsub (str, pos, pos) + if char == "{" then + return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta) + elseif char == "[" then + return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta) + elseif char == "\"" then + return scanstring (str, pos) + else + local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos) + if pstart then + local number = str2num (strsub (str, pstart, pend)) + if number then + return number, pend + 1 + end + end + pstart, pend = strfind (str, "^%a%w*", pos) + if pstart then + local name = strsub (str, pstart, pend) + if name == "true" then + return true, pend + 1 + elseif name == "false" then + return false, pend + 1 + elseif name == "null" then + return nullval, pend + 1 + end + end + return nil, pos, "no valid JSON value at " .. loc (str, pos) + end +end + +local function optionalmetatables(...) + if select("#", ...) > 0 then + return ... + else + return {__jsontype = 'object'}, {__jsontype = 'array'} + end +end + +function json.decode (str, pos, nullval, ...) + local objectmeta, arraymeta = optionalmetatables(...) + return scanvalue (str, pos, nullval, objectmeta, arraymeta) +end + +-- NOTE: added method - not in original source +function json.decodeFromFile(path) + local file = assert(fs.open(path, "r")) + local decoded = json.decode(file.readAll()) + file.close() + return decoded +end + +return json diff --git a/sys/modules/opus/map.lua b/sys/modules/opus/map.lua new file mode 100644 index 0000000..bd576d3 --- /dev/null +++ b/sys/modules/opus/map.lua @@ -0,0 +1,51 @@ +-- convience functions for tables with key/value pairs +local Util = require('opus.util') + +local Map = { } + +-- TODO: refactor +Map.merge = Util.merge +Map.shallowCopy = Util.shallowCopy +Map.find = Util.find +Map.filter = Util.filter + +function Map.removeMatches(t, values) + local function matchAll(entry) + for k, v in pairs(values) do + if entry[k] ~= v then + return + end + end + return true + end + + for _, key in pairs(Util.keys(t)) do + if matchAll(t[key]) then + t[key] = nil + end + end +end + +-- remove table entries if passed function returns false +function Map.prune(t, fn) + for _,k in pairs(Util.keys(t)) do + local v = t[k] + if type(v) == 'table' then + t[k] = Map.prune(v, fn) + end + if not fn(t[k]) then + t[k] = nil + end + end + return t +end + +function Map.size(list) + local length = 0 + for _ in pairs(list) do + length = length + 1 + end + return length +end + +return Map diff --git a/sys/apis/nft.lua b/sys/modules/opus/nft.lua similarity index 64% rename from sys/apis/nft.lua rename to sys/modules/opus/nft.lua index 056961b..4fd8d02 100644 --- a/sys/apis/nft.lua +++ b/sys/modules/opus/nft.lua @@ -1,4 +1,4 @@ -local Util = require('util') +local Util = require('opus.util') local NFT = { } @@ -34,29 +34,23 @@ function NFT.parse(imageText) --As we're no longer 1-1, we keep track of what index to write to local writeIndex = 1 --Tells us if we've hit a 30 or 31 (BG and FG respectively)- next char specifies the curr colour - local bgNext, fgNext = false, false - --The current background and foreground colours - local currBG, currFG = nil,nil - for i = 1, #sLine do - local nextChar = string.sub(sLine, i, i) - if nextChar:byte() == 30 then - bgNext = true - elseif nextChar:byte() == 31 then - fgNext = true - elseif bgNext then - currBG = getColourOf(nextChar) - bgNext = false - elseif fgNext then - currFG = getColourOf(nextChar) - fgNext = false + + local tcol, bcol = colors.white,colors.black + local cx, sx = 1, 0 + while sx < #sLine do + sx = sx + 1 + if sLine:sub(sx,sx) == "\30" then + bcol = getColourOf(sLine:sub(sx+1,sx+1)) + sx = sx + 1 + elseif sLine:sub(sx,sx) == "\31" then + tcol = getColourOf(sLine:sub(sx+1,sx+1)) + sx = sx + 1 else - if nextChar ~= " " and currFG == nil then - currFG = _G.colors.white - end - image.bg[num][writeIndex] = currBG - image.fg[num][writeIndex] = currFG - image.text[num][writeIndex] = nextChar + image.bg[num][writeIndex] = bcol + image.fg[num][writeIndex] = tcol + image.text[num][writeIndex] = sLine:sub(sx,sx) writeIndex = writeIndex + 1 + cx = cx + 1 end end image.height = num diff --git a/sys/modules/opus/packages.lua b/sys/modules/opus/packages.lua new file mode 100644 index 0000000..8b0f5e1 --- /dev/null +++ b/sys/modules/opus/packages.lua @@ -0,0 +1,96 @@ +local Util = require('opus.util') + +local fs = _G.fs +local textutils = _G.textutils + +local PACKAGE_DIR = 'packages' + +local Packages = { } + +function Packages:installed() + local list = { } + + if fs.exists(PACKAGE_DIR) then + for _, dir in pairs(fs.list(PACKAGE_DIR)) do + local path = fs.combine(fs.combine(PACKAGE_DIR, dir), '.package') + list[dir] = Util.readTable(path) + end + end + + return list +end + +function Packages:installedSorted() + local list = { } + + for k, v in pairs(self.installed()) do + v.name = k + v.deps = { } + table.insert(list, v) + for _, v2 in pairs(v.required or { }) do + v.deps[v2] = true + end + end + + table.sort(list, function(a, b) + return not not (b.deps and b.deps[a.name]) + end) + + table.sort(list, function(a, b) + return not (a.deps and a.deps[b.name]) + end) + + return list +end + +function Packages:list() + if not fs.exists('usr/config/packages') then + self:downloadList() + end + return Util.readTable('usr/config/packages') or { } +end + +function Packages:isInstalled(package) + return self:installed()[package] +end + +function Packages:downloadList() + local packages = { + [ 'develop-1.8' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/packages.list', + [ 'master-1.8' ] = 'https://pastebin.com/raw/pexZpAxt', + } + + if packages[_G.OPUS_BRANCH] then + Util.download(packages[_G.OPUS_BRANCH], 'usr/config/packages') + end +end + +function Packages:downloadManifest(package) + local list = self:list() + local url = list and list[package] + + if url then + local c = Util.httpGet(url) + if c then + c = textutils.unserialize(c) + if c then + c.repository = c.repository:gsub('{{OPUS_BRANCH}}', _G.OPUS_BRANCH) + return c + end + end + end +end + +function Packages:getManifest(package) + local fname = 'packages/' .. package .. '/.package' + if fs.exists(fname) then + local c = Util.readTable(fname) + if c and c.repository then + c.repository = c.repository:gsub('{{OPUS_BRANCH}}', _G.OPUS_BRANCH) + return c + end + end + return self:downloadManifest(package) +end + +return Packages diff --git a/sys/modules/opus/peripheral.lua b/sys/modules/opus/peripheral.lua new file mode 100644 index 0000000..86d63fc --- /dev/null +++ b/sys/modules/opus/peripheral.lua @@ -0,0 +1,126 @@ +local Util = require('opus.util') + +local Peripheral = Util.shallowCopy(_G.peripheral) + +function Peripheral.getList() + if _G.device then + return _G.device + end + + local deviceList = { } + for _,side in pairs(Peripheral.getNames()) do + Peripheral.addDevice(deviceList, side) + end + + return deviceList +end + +function Peripheral.addDevice(deviceList, side) + local name = side + pcall(function() + local ptype = Peripheral.getType(side) + local dev = Peripheral.wrap(side) + + if not ptype or not dev then + return + end + + if ptype == 'modem' then + if not Peripheral.call(name, 'isWireless') then + -- ptype = 'wireless_modem' + -- else + ptype = 'wired_modem' + if dev.isAccessPoint then + -- avoid open computer relays being registered + -- as 'wired_modem' + ptype = dev.getMetadata().name or 'wired_modem' + end + end + end + + local sides = { + front = true, + back = true, + top = true, + bottom = true, + left = true, + right = true + } + + if sides[name] then + local i = 1 + local uniqueName = ptype + while deviceList[uniqueName] do + uniqueName = ptype .. '_' .. i + i = i + 1 + end + name = uniqueName + end + + -- this can randomly fail + if not deviceList[name] then + deviceList[name] = dev + + if deviceList[name] then + Util.merge(deviceList[name], { + name = name, + type = ptype, + side = side, + }) + end + end + end) + + return deviceList[name] +end + +function Peripheral.getBySide(side) + return Util.find(Peripheral.getList(), 'side', side) +end + +function Peripheral.getByType(typeName) + return Util.find(Peripheral.getList(), 'type', typeName) +end + +function Peripheral.getByMethod(method) + for _,p in pairs(Peripheral.getList()) do + if p[method] then + return p + end + end +end + +-- match any of the passed arguments +function Peripheral.get(args) + + if type(args) == 'string' then + args = { type = args } + end + + if args.name then + return _G.device[args.name] + end + + if args.type then + local p = Peripheral.getByType(args.type) + if p then + return p + end + end + + if args.method then + local p = Peripheral.getByMethod(args.method) + if p then + return p + end + end + + if args.side then + local p = Peripheral.getBySide(args.side) + if p then + return p + end + end +end + +return Peripheral diff --git a/sys/apis/point.lua b/sys/modules/opus/point.lua similarity index 90% rename from sys/apis/point.lua rename to sys/modules/opus/point.lua index 1d83eab..a118df3 100644 --- a/sys/apis/point.lua +++ b/sys/modules/opus/point.lua @@ -1,4 +1,4 @@ -local Util = require('util') +local Util = require('opus.util') local Point = { } @@ -131,8 +131,11 @@ end function Point.calculateMoves(pta, ptb, distance) local heading = pta.heading local moves = distance or Point.turtleDistance(pta, ptb) + local weighted = moves + if (pta.heading % 2) == 0 and pta.z ~= ptb.z then moves = moves + 1 + weighted = weighted + .9 if ptb.heading and (ptb.heading % 2 == 1) then heading = ptb.heading elseif ptb.z > pta.z then @@ -142,6 +145,7 @@ function Point.calculateMoves(pta, ptb, distance) end elseif (pta.heading % 2) == 1 and pta.x ~= ptb.x then moves = moves + 1 + weighted = weighted + .9 if ptb.heading and (ptb.heading % 2 == 0) then heading = ptb.heading elseif ptb.x > pta.x then @@ -152,15 +156,18 @@ function Point.calculateMoves(pta, ptb, distance) end if not ptb.heading then - return moves, heading, moves + return moves, heading, weighted end + -- need to know if we are in digging mode + -- if so, we need to face blocks -- need a no-backwards flag + -- calc turns as slightly less than moves - local weighted = moves + -- local weighted = moves if heading ~= ptb.heading then local turns = Point.calculateTurns(heading, ptb.heading) moves = moves + turns - local wturns = { [0] = 0, [1] = .9, [2] = 1.9 } + local wturns = { [0] = 0, [1] = .9, [2] = 1.8 } weighted = weighted + wturns[turns] heading = ptb.heading end @@ -177,7 +184,12 @@ function Point.closest(reference, pts) local lm, lpt = math.huge for _,pt in pairs(pts) do local distance = Point.turtleDistance(reference, pt) - if distance < lm then + if not reference.heading then + if distance < lm then + lpt = pt + lm = distance + end + elseif distance < lm then local _, _, m = Point.calculateMoves(reference, pt, distance) if m < lm then lpt = pt @@ -189,6 +201,8 @@ function Point.closest(reference, pts) end function Point.eachClosest(spt, ipts, fn) + if not ipts then error('Point.eachClosest: invalid points', 2) end + local pts = Util.shallowCopy(ipts) while #pts > 0 do local pt = Point.closest(spt, pts) @@ -200,6 +214,17 @@ function Point.eachClosest(spt, ipts, fn) end end +function Point.iterateClosest(spt, ipts) + local pts = Util.shallowCopy(ipts) + return function() + local pt = Point.closest(spt, pts) + if pt then + Util.removeByValue(pts, pt) + return pt + end + end +end + function Point.adjacentPoints(pt) local pts = { } @@ -215,7 +240,7 @@ end function Point.nearestTo(pta, ptb) local heading - if pta.x < ptb.x then + if pta.x < ptb.x then heading = 0 elseif pta.z < ptb.z then heading = 1 diff --git a/sys/modules/opus/security.lua b/sys/modules/opus/security.lua new file mode 100644 index 0000000..4fb6f8d --- /dev/null +++ b/sys/modules/opus/security.lua @@ -0,0 +1,40 @@ +local Config = require('opus.config') + +local Security = { } + +function Security.verifyPassword(password) + local current = Security.getPassword() + return current and password == current +end + +function Security.hasPassword() + return not not Security.getPassword() +end + +function Security.getIdentifier() + local config = Config.load('os') + + if not config.identifier then + local key = { } + for _ = 1, 32 do + table.insert(key, ("%02x"):format(math.random(0, 0xFF))) + end + config.identifier = table.concat(key) + + Config.update('os', config) + end + + return config.identifier +end + +function Security.updatePassword(password) + local config = Config.load('os') + config.password = password + Config.update('os', config) +end + +function Security.getPassword() + return Config.load('os').password +end + +return Security diff --git a/sys/modules/opus/socket.lua b/sys/modules/opus/socket.lua new file mode 100644 index 0000000..8151855 --- /dev/null +++ b/sys/modules/opus/socket.lua @@ -0,0 +1,255 @@ +local Crypto = require('opus.crypto.chacha20') +local ECC = require('opus.crypto.ecc') +local Security = require('opus.security') +local SHA = require('opus.crypto.sha2') +local Util = require('opus.util') + +local device = _G.device +local os = _G.os +local network = _G.network + +local socketClass = { } + +function socketClass:read(timeout) + local data, distance = network.getTransport().read(self) + if data then + return data, distance + end + + if not self.connected then + return + end + + local timerId = os.startTimer(timeout or 5) + + while true do + local e, id = os.pullEvent() + + if e == 'transport_' .. self.uid then + data, distance = network.getTransport().read(self) + if data then + os.cancelTimer(timerId) + return data, distance + end + if not self.connected then + break + end + + elseif e == 'timer' and id == timerId then + if timeout or not self.connected then + break + end + timerId = os.startTimer(5) + self:ping() + end + end +end + +function socketClass:write(data) + if self.connected then + network.getTransport().write(self, { + type = 'DATA', + seq = self.wseq, + data = data, + }) + return true + end +end + +function socketClass:ping() + if self.connected then + network.getTransport().ping(self) + return true + end +end + +function socketClass:close() + if self.connected then + self.transmit(self.dport, self.dhost, { + type = 'DISC', + seq = self.wseq, + }) + self.connected = false + end + device.wireless_modem.close(self.sport) + network.getTransport().close(self) +end + +local Socket = { } + +local function loopback(port, sport, msg) + os.queueEvent('modem_message', 'loopback', port, sport, msg, 0) +end + +local function newSocket(isLoopback) + for _ = 16384, 32767 do + local i = math.random(16384, 32767) + if not device.wireless_modem.isOpen(i) then + local socket = { + shost = os.getComputerID(), + sport = i, + transmit = device.wireless_modem.transmit, + timers = { }, + messages = { }, + } + setmetatable(socket, { __index = socketClass }) + + device.wireless_modem.open(socket.sport) + + if isLoopback then + socket.transmit = loopback + end + return socket + end + end + error('No ports available') +end + +local function setupCrypto(socket, isClient) + socket.sharedKey = ECC.exchange(socket.privKey, socket.remotePubKey) + socket.enckey = SHA.pbkdf2(socket.sharedKey, "1enc", 1) + --self.hmackey = SHA.pbkdf2(self.sharedKey, "2hmac", 1) + + socket.rrng = Crypto.newRNG( + SHA.pbkdf2(socket.sharedKey, isClient and "3rseed" or "4sseed", 1)) + socket.wrng = Crypto.newRNG( + SHA.pbkdf2(socket.sharedKey, isClient and "4sseed" or "3rseed", 1)) + + socket.rseq = socket.rrng:nextInt(5) + socket.wseq = socket.wrng:nextInt(5) +end + +function Socket.connect(host, port, options) + if not device.wireless_modem then + return false, 'Wireless modem not found', 'NOMODEM' + end + + local socket = newSocket(host == os.getComputerID()) + socket.dhost = tonumber(host) + socket.privKey, socket.pubKey = network.getKeyPair() + local identifier = options and options.identifier or Security.getIdentifier() + + socket.transmit(port, socket.sport, { + type = 'OPEN', + shost = socket.shost, + dhost = socket.dhost, + t = Crypto.encrypt({ -- this is not that much data... + ts = os.epoch('utc'), + pk = Util.byteArrayToHex(socket.pubKey), + }, Util.hexToByteArray(identifier)), + }) + + local timerId = os.startTimer(3) + repeat + local e, id, sport, dport, msg = os.pullEvent() + if e == 'modem_message' and + sport == socket.sport and + type(msg) == 'table' and + msg.dhost == socket.shost then + + os.cancelTimer(timerId) + + if msg.type == 'CONN' and type(msg.pk) == 'string' then + socket.dport = dport + socket.connected = true + socket.remotePubKey = Util.hexToByteArray(msg.pk) + socket.options = msg.options or { } + setupCrypto(socket, true) + network.getTransport().open(socket) + return socket + + elseif msg.type == 'NOPASS' then + socket:close() + return false, 'Password not set on target', 'NOPASS' + + elseif msg.type == 'REJE' then + socket:close() + return false, 'Trust not established', 'NOTRUST' + end + end + until e == 'timer' and id == timerId + + socket:close() + return false, 'Connection timed out', 'TIMEOUT' +end + +local function trusted(socket, msg, options) + local function getIdentifier() + local trustList = Util.readTable('usr/.known_hosts') or { } + return trustList[msg.shost] + end + + local identifier = options and options.identifier or getIdentifier() + + local s, m = pcall(function() + if identifier and type(msg.t) == 'table' then + local data = Crypto.decrypt(msg.t, Util.hexToByteArray(identifier)) + + if data and data.ts and tonumber(data.ts) then + if math.abs(os.epoch('utc') - data.ts) < 4096 then + socket.remotePubKey = Util.hexToByteArray(data.pk) + socket.privKey, socket.pubKey = network.getKeyPair() + setupCrypto(socket) + return true + end + _G._syslog('time diff ' .. math.abs(os.epoch('utc') - data.ts)) + end + end + end) + if not s and m then + _G._syslog('trust failure') + _G._syslog(m) + end + return s and m +end + +function Socket.server(port, options) + device.wireless_modem.open(port) + + while true do + local _, _, sport, dport, msg = os.pullEvent('modem_message') + + if sport == port and + type(msg) == 'table' and + msg.dhost == os.getComputerID() and + msg.type == 'OPEN' then + + local socket = newSocket(msg.shost == os.getComputerID()) + socket.dport = dport + socket.dhost = msg.shost + socket.options = options or { } + + if not Security.hasPassword() then + socket.transmit(socket.dport, socket.sport, { + type = 'NOPASS', + dhost = socket.dhost, + shost = socket.shost, + }) + socket:close() + + elseif trusted(socket, msg, options) then + socket.connected = true + socket.transmit(socket.dport, socket.sport, { + type = 'CONN', + dhost = socket.dhost, + shost = socket.shost, + pk = Util.byteArrayToHex(socket.pubKey), + options = socket.options.ENCRYPT and { ENCRYPT = true }, + }) + + network.getTransport().open(socket) + return socket + + else + socket.transmit(socket.dport, socket.sport, { + type = 'REJE', + dhost = socket.dhost, + shost = socket.shost, + }) + socket:close() + end + end + end +end + +return Socket diff --git a/sys/apis/sound.lua b/sys/modules/opus/sound.lua similarity index 61% rename from sys/apis/sound.lua rename to sys/modules/opus/sound.lua index b4dab46..18dad1c 100644 --- a/sys/apis/sound.lua +++ b/sys/modules/opus/sound.lua @@ -5,10 +5,9 @@ local Sound = { } function Sound.play(sound, vol) - local speaker = peripheral.find('speaker') - if speaker then - speaker.playSound('minecraft:' .. sound, vol or Sound._volume) - end + peripheral.find('speaker', function(_, s) + s.playSound('minecraft:' .. sound, vol or Sound._volume) + end) end function Sound.setVolume(volume) diff --git a/sys/modules/opus/sync.lua b/sys/modules/opus/sync.lua new file mode 100644 index 0000000..fd3402c --- /dev/null +++ b/sys/modules/opus/sync.lua @@ -0,0 +1,61 @@ +local Sync = { + syncLocks = { } +} + +local os = _G.os + +function Sync.sync(obj, fn) + local key = tostring(obj) + if Sync.syncLocks[key] then + local cos = tostring(coroutine.running()) + table.insert(Sync.syncLocks[key], cos) + repeat + local _, co = os.pullEvent('sync_lock') + until co == cos + else + Sync.syncLocks[key] = { } + end + local s, m = pcall(fn) + local co = table.remove(Sync.syncLocks[key], 1) + if co then + os.queueEvent('sync_lock', co) + else + Sync.syncLocks[key] = nil + end + if not s then + error(m) + end +end + +function Sync.lock(obj) + local key = tostring(obj) + if Sync.syncLocks[key] then + local cos = tostring(coroutine.running()) + table.insert(Sync.syncLocks[key], cos) + repeat + local _, co = os.pullEvent('sync_lock') + until co == cos + else + Sync.syncLocks[key] = { } + end +end + +function Sync.release(obj) + local key = tostring(obj) + if not Sync.syncLocks[key] then + error('Sync.release: Lock was not obtained', 2) + end + local co = table.remove(Sync.syncLocks[key], 1) + if co then + os.queueEvent('sync_lock', co) + else + Sync.syncLocks[key] = nil + end +end + +function Sync.isLocked(obj) + local key = tostring(obj) + return not not Sync.syncLocks[key] +end + +return Sync diff --git a/sys/modules/opus/terminal.lua b/sys/modules/opus/terminal.lua new file mode 100644 index 0000000..cb7df22 --- /dev/null +++ b/sys/modules/opus/terminal.lua @@ -0,0 +1,362 @@ +local Canvas = require('opus.ui.canvas') + +local colors = _G.colors +local term = _G.term +local _gsub = string.gsub + +local Terminal = { } + +local mapColorToGray = { + [ colors.white ] = colors.white, + [ colors.orange ] = colors.lightGray, + [ colors.magenta ] = colors.lightGray, + [ colors.lightBlue ] = colors.lightGray, + [ colors.yellow ] = colors.lightGray, + [ colors.lime ] = colors.lightGray, + [ colors.pink ] = colors.lightGray, + [ colors.gray ] = colors.gray, + [ colors.lightGray ] = colors.lightGray, + [ colors.cyan ] = colors.lightGray, + [ colors.purple ] = colors.gray, + [ colors.blue ] = colors.gray, + [ colors.brown ] = colors.gray, + [ colors.green ] = colors.lightGray, + [ colors.red ] = colors.gray, + [ colors.black ] = colors.black, +} + +-- Replacement for window api with scrolling and buffering +function Terminal.window(parent, sx, sy, w, h, isVisible) + isVisible = isVisible ~= false + if not w or not h then + w, h = parent.getSize() + end + + local win = { } + local maxScroll = 100 + local cx, cy = 1, 1 + local blink = false + local bg, fg = parent.getBackgroundColor(), parent.getTextColor() + + local canvas = Canvas({ + x = sx, + y = sy, + width = w, + height = h, + isColor = parent.isColor(), + offy = 0, + }) + + win.canvas = canvas + + local function update() + if isVisible then + canvas:render(parent) + win.setCursorPos(cx, cy) + end + end + + local function scrollTo(y) + y = math.max(0, y) + y = math.min(#canvas.lines - canvas.height, y) + + if y ~= canvas.offy then + canvas.offy = y + canvas:dirty() + update() + end + end + + function win.write(str) + str = tostring(str) or '' + canvas:write(cx, cy + canvas.offy, str, bg, fg) + win.setCursorPos(cx + #str, cy) + update() + end + + function win.blit(str, fg, bg) + canvas:blit(cx, cy + canvas.offy, str, bg, fg) + win.setCursorPos(cx + #str, cy) + update() + end + + function win.clear() + canvas.offy = 0 + for i = #canvas.lines, canvas.height + 1, -1 do + canvas.lines[i] = nil + end + canvas:clear(bg, fg) + update() + end + + function win.clearLine() + canvas:clearLine(cy + canvas.offy, bg, fg) + win.setCursorPos(cx, cy) + update() + end + + function win.getCursorPos() + return cx, cy + end + + function win.setCursorPos(x, y) + cx, cy = math.floor(x), math.floor(y) + if isVisible then + parent.setCursorPos(cx + canvas.x - 1, cy + canvas.y - 1) + end + end + + function win.setCursorBlink(b) + blink = b + if isVisible then + parent.setCursorBlink(b) + end + end + + function win.isColor() + return canvas.isColor + end + win.isColour = win.isColor + + function win.setTextColor(c) + fg = c + end + win.setTextColour = win.setTextColor + + function win.getPaletteColor(n) + if parent.getPaletteColor then + return parent.getPaletteColor(n) + end + return 0, 0, 0 + end + win.getPaletteColour = win.getPaletteColor + + function win.setPaletteColor(n, r, g, b) + if parent.setPaletteColor then + return parent.setPaletteColor(n, r, g, b) + end + end + win.setPaletteColour = win.setPaletteColor + + function win.setBackgroundColor(c) + bg = c + end + win.setBackgroundColour = win.setBackgroundColor + + function win.getSize() + return canvas.width, canvas.height + end + + function win.scroll(n) + n = n or 1 + if n > 0 then + local lines = #canvas.lines + for i = 1, n do + canvas.lines[lines + i] = { } + canvas:clearLine(lines + i, bg, fg) + end + while #canvas.lines > maxScroll do + table.remove(canvas.lines, 1) + end + scrollTo(#canvas.lines) + canvas:dirty() + update() + end + end + + function win.getTextColor() + return fg + end + win.getTextColour = win.getTextColor + + function win.getBackgroundColor() + return bg + end + win.getBackgroundColour = win.getBackgroundColor + + function win.setVisible(visible) + if visible ~= isVisible then + isVisible = visible + if isVisible then + canvas:dirty() + update() + end + end + end + + function win.redraw() + if isVisible then + canvas:dirty() + update() + end + end + + function win.restoreCursor() + if isVisible then + win.setCursorPos(cx, cy) + win.setTextColor(fg) + win.setCursorBlink(blink) + end + end + + function win.getPosition() + return canvas.x, canvas.y + end + + function win.reposition(x, y, width, height) + canvas.x, canvas.y = x, y + canvas:resize(width or canvas.width, height or canvas.height) + end + + --[[ Additional methods ]]-- + function win.scrollDown() + scrollTo(canvas.offy + 1) + end + + function win.scrollUp() + scrollTo(canvas.offy - 1) + end + + function win.scrollTop() + scrollTo(0) + end + + function win.scrollBottom() + scrollTo(#canvas.lines) + end + + function win.setMaxScroll(ms) + maxScroll = ms + end + + function win.getCanvas() + return canvas + end + + function win.getParent() + return parent + end + + canvas:clear() + + return win +end + +-- get windows contents +function Terminal.getContents(win, parent) + local oblit, oscp = parent.blit, parent.setCursorPos + local lines = { } + + parent.blit = function(text, fg, bg) + lines[#lines + 1] = { + text = text, + fg = fg, + bg = bg, + } + end + parent.setCursorPos = function() end + + win.setVisible(true) + win.redraw() + + parent.blit = oblit + parent.setCursorPos = oscp + + return lines +end + +function Terminal.colorToGrayscale(c) + return mapColorToGray[c] +end + +function Terminal.toGrayscale(ct) + local methods = { 'setBackgroundColor', 'setBackgroundColour', + 'setTextColor', 'setTextColour' } + for _,v in pairs(methods) do + local fn = ct[v] + ct[v] = function(c) + fn(mapColorToGray[c]) + end + end + + local bcolors = { + [ '1' ] = '8', + [ '2' ] = '8', + [ '3' ] = '8', + [ '4' ] = '8', + [ '5' ] = '8', + [ '6' ] = '8', + [ '9' ] = '8', + [ 'a' ] = '7', + [ 'b' ] = '7', + [ 'c' ] = '7', + [ 'd' ] = '8', + [ 'e' ] = '7', + } + + local function translate(s) + if s then + s = _gsub(s, "%w", bcolors) + end + return s + end + + local fn = ct.blit + ct.blit = function(text, fg, bg) + fn(text, translate(fg), translate(bg)) + end +end + +function Terminal.getNullTerm(ct) + local nt = Terminal.copy(ct) + + local methods = { 'blit', 'clear', 'clearLine', 'scroll', + 'setCursorBlink', 'setCursorPos', 'write' } + for _,v in pairs(methods) do + nt[v] = function() end + end + + return nt +end + +function Terminal.copy(it, ot) + ot = ot or { } + for k,v in pairs(it) do + if type(v) == 'function' then + ot[k] = v + end + end + return ot +end + +function Terminal.mirror(ct, dt) + local t = { } + for k,f in pairs(ct) do + t[k] = function(...) + local ret = { f(...) } + if dt[k] then + dt[k](...) + end + return table.unpack(ret) + end + end + return t +end + +function Terminal.readPassword(prompt) + if prompt then + term.write(prompt) + end + local fn = term.current().write + term.current().write = function() end + local s + pcall(function() s = _G.read(prompt) end) + term.current().write = fn + + if s == '' then + return + end + return s +end + +return Terminal diff --git a/sys/modules/opus/trace.lua b/sys/modules/opus/trace.lua new file mode 100644 index 0000000..5673b6d --- /dev/null +++ b/sys/modules/opus/trace.lua @@ -0,0 +1,110 @@ +-- stack trace by SquidDev (MIT License) +-- https://raw.githubusercontent.com/SquidDev-CC/mbs/master/lib/stack_trace.lua + +local type = type +local debug_traceback = type(debug) == "table" and type(debug.traceback) == "function" and debug.traceback + +local function traceback(x) + -- Attempt to detect error() and error("xyz", 0). + -- This probably means they're erroring the program intentionally and so we + -- shouldn't display anything. + if x == nil or (type(x) == "string" and not x:find(":%d+:")) then + return x + end + + if debug_traceback then + -- The parens are important, as they prevent a tail call occuring, meaning + -- the stack level is preserved. This ensures the code behaves identically + -- on LuaJ and PUC Lua. + return (debug_traceback(tostring(x), 2)) + else + local level = 3 + local out = { tostring(x), "stack traceback:" } + while true do + local _, msg = pcall(error, "", level) + if msg == "" then break end + + out[#out + 1] = " " .. msg + level = level + 1 + end + + return table.concat(out, "\n") + end +end + +local function trim_traceback(target, marker) + local ttarget, tmarker = {}, {} + for line in target:gmatch("([^\n]*)\n?") do ttarget[#ttarget + 1] = line end + for line in marker:gmatch("([^\n]*)\n?") do tmarker[#tmarker + 1] = line end + + -- Trim identical suffixes + local t_len, m_len = #ttarget, #tmarker + while t_len >= 3 and ttarget[t_len] == tmarker[m_len] do + table.remove(ttarget, t_len) + t_len, m_len = t_len - 1, m_len - 1 + end + + -- Trim elements from this file and xpcall invocations + while t_len >= 1 and ttarget[t_len]:find("^\tstack_trace%.lua:%d+:") or + ttarget[t_len] == "\t[C]: in function 'xpcall'" or ttarget[t_len] == " xpcall: " do + table.remove(ttarget, t_len) + t_len = t_len - 1 + end + + ttarget[#ttarget] = nil -- remove 2 calls added by the added xpcall + ttarget[#ttarget] = nil + + return ttarget +end + +--- Run a function with +return function (fn, ...) + -- So this is rather grim: we need to get the full traceback and current one and remove + -- the common prefix + local trace + local args = { ... } + + -- xpcall in Lua 5.1 does not accept parameters + -- which is not ideal + local res = table.pack(xpcall(function() + return fn(table.unpack(args)) + end, traceback)) + + if not res[1] then + trace = traceback("trace.lua:1:") + end + local ok, err = res[1], res[2] + + if not ok and err ~= nil then + trace = trim_traceback(err, trace) + + -- Find the position where the stack traceback actually starts + local trace_starts + for i = #trace, 1, -1 do + if trace[i] == "stack traceback:" then trace_starts = i; break end + end + + for _, line in pairs(trace) do + _G._syslog(line) + end + + -- If this traceback is more than 15 elements long, keep the first 9, last 5 + -- and put an ellipsis between the rest + local max = 10 + if trace_starts and #trace - trace_starts > max then + local keep_starts = trace_starts + 7 + for i = #trace - trace_starts - max, 0, -1 do + table.remove(trace, keep_starts + i) + end + table.insert(trace, keep_starts, " ...") + end + + for k, line in pairs(trace) do + trace[k] = line:gsub("in function", " in") + end + + return false, table.remove(trace, 1), table.concat(trace, "\n") + end + + return table.unpack(res, 1, res.n) +end diff --git a/sys/modules/opus/ui.lua b/sys/modules/opus/ui.lua new file mode 100644 index 0000000..9e9a4c5 --- /dev/null +++ b/sys/modules/opus/ui.lua @@ -0,0 +1,1250 @@ +local Canvas = require('opus.ui.canvas') +local class = require('opus.class') +local Event = require('opus.event') +local Input = require('opus.input') +local Transition = require('opus.ui.transition') +local Util = require('opus.util') + +local _rep = string.rep +local _sub = string.sub +local colors = _G.colors +local device = _G.device +local fs = _G.fs +local os = _G.os +local term = _G.term +local textutils = _G.textutils + +--[[ + Using the shorthand window definition, elements are created from + the bottom up. Once reaching the top, setParent is called top down. + + On :init(), elements do not know the parent or can calculate sizing. +]] + +-- need to add offsets to this test +local function getPosition(element) + local x, y = 1, 1 + repeat + x = element.x + x - 1 + y = element.y + y - 1 + element = element.parent + until not element + return x, y +end + +--[[-- Top Level Manager --]]-- +local Manager = class() +function Manager:init() + self.devices = { } + self.theme = { } + self.extChars = Util.getVersion() >= 1.76 + + local function keyFunction(event, code, held) + local ie = Input:translate(event, code, held) + + local currentPage = self:getActivePage() + if ie and currentPage then + local target = currentPage.focused or currentPage + self:inputEvent(target, + { type = 'key', key = ie.code == 'char' and ie.ch or ie.code, element = target, ie = ie }) + currentPage:sync() + end + end + + local function resize(_, side) + local dev = self.devices[side or 'terminal'] + if dev and dev.currentPage then + -- the parent doesn't have any children set... + -- that's why we have to resize both the parent and the current page + -- kinda makes sense + dev.currentPage.parent:resize() + + dev.currentPage:resize() + dev.currentPage:draw() + dev.currentPage:sync() + end + end + + local handlers = { + char = keyFunction, + key_up = keyFunction, + key = keyFunction, + term_resize = resize, + monitor_resize = resize, + + mouse_scroll = function(_, direction, x, y) + local currentPage = self:getActivePage() + if currentPage then + local event = currentPage:pointToChild(x, y) + local directions = { + [ -1 ] = 'up', + [ 1 ] = 'down' + } + -- revisit - should send out scroll_up and scroll_down events + -- let the element convert them to up / down + self:inputEvent(event.element, + { type = 'key', key = directions[direction] }) + currentPage:sync() + end + end, + + monitor_touch = function(_, side, x, y) + local dev = self.devices[side] + if dev and dev.currentPage then + Input:translate('mouse_click', 1, x, y) + local ie = Input:translate('mouse_up', 1, x, y) + self:click(dev.currentPage, ie.code, 1, x, y) + end + end, + + mouse_click = function(_, button, x, y) + local ie = Input:translate('mouse_click', button, x, y) + + local currentPage = self:getActivePage() + if currentPage then + if not currentPage.parent.device.side then + local event = currentPage:pointToChild(x, y) + if event.element.focus and not event.element.inactive then + currentPage:setFocus(event.element) + currentPage:sync() + end + self:click(currentPage, ie.code, button, x, y) + end + end + end, + + mouse_up = function(_, button, x, y) + local ie = Input:translate('mouse_up', button, x, y) + local currentPage = self:getActivePage() + + if ie.code == 'control-shift-mouse_click' then -- hack + local event = currentPage:pointToChild(x, y) + _ENV.multishell.openTab({ + path = 'sys/apps/Lua.lua', + args = { event.element }, + focused = true }) + + elseif ie and currentPage then + if not currentPage.parent.device.side then + self:click(currentPage, ie.code, button, x, y) + end + end + end, + + mouse_drag = function(_, button, x, y) + local ie = Input:translate('mouse_drag', button, x, y) + local currentPage = self:getActivePage() + + if ie and currentPage then + self:click(currentPage, ie.code, button, x, y) + end + end, + + paste = function(_, text) + local ie = Input:translate('paste', text) + self:emitEvent({ type = 'paste', text = text, ie = ie }) + self:getActivePage():sync() + end, + } + + -- use 1 handler to single thread all events + Event.on({ + 'char', 'key_up', 'key', 'term_resize', 'monitor_resize', + 'mouse_scroll', 'monitor_touch', 'mouse_click', + 'mouse_up', 'mouse_drag', 'paste' }, + function(event, ...) + handlers[event](event, ...) + end) +end + +function Manager:configure(appName, ...) + local defaults = Util.loadTable('usr/config/' .. appName) or { } + if not defaults.device then + defaults.device = { } + end + + -- starting a program: gpsServer --display=monitor_3148 --scale=.5 gps + local _, options = Util.parse(...) + local optionValues = { + name = options.display, + textScale = tonumber(options.scale), + } + + Util.merge(defaults.device, optionValues) + + if defaults.device.name then + local dev + + if defaults.device.name == 'terminal' then + dev = term.current() + else + dev = device[defaults.device.name] + end + + if not dev then + error('Invalid display device') + end + self:setDefaultDevice(self.Device({ + device = dev, + textScale = defaults.device.textScale, + })) + end + + if defaults.theme then + for k,v in pairs(defaults.theme) do + if self[k] and self[k].defaults then + Util.merge(self[k].defaults, v) + end + end + end +end + +function Manager:disableEffects() + self.defaultDevice.effectsEnabled = false +end + +function Manager:loadTheme(filename) + if fs.exists(filename) then + local theme, err = Util.loadTable(filename) + if not theme then + error(err) + end + Util.deepMerge(self.theme, theme) + end +end + +function Manager:generateTheme(filename) + local t = { } + for k,v in pairs(self) do + if type(v) == 'table' then + if v._preload then + v._preload() + v = self[k] + end + if v.defaults and v.defaults.UIElement ~= 'Device' then + for p,d in pairs(v.defaults) do + if p:find('olor') then + if not t[k] then + t[k] = { } + end + for c, n in pairs(colors) do + if n == d then + t[k][p] = 'colors.' .. c + break + end + end + end + end + end + end + end + Util.writeFile(filename, textutils.serialize(t):gsub('(")', '')) +end + +function Manager:emitEvent(event) + local currentPage = self:getActivePage() + if currentPage and currentPage.focused then + return currentPage.focused:emit(event) + end +end + +function Manager:inputEvent(parent, event) + while parent do + if parent.accelerators then + local acc = parent.accelerators[event.key] + if acc then + if parent:emit({ type = acc, element = parent }) then + return true + end + end + end + if parent.eventHandler then + if parent:eventHandler(event) then + return true + end + end + parent = parent.parent + end +end + +function Manager:click(target, code, button, x, y) + local clickEvent = target:pointToChild(x, y) + + if code == 'mouse_doubleclick' then + if self.doubleClickElement ~= clickEvent.element then + return + end + else + self.doubleClickElement = clickEvent.element + end + + clickEvent.button = button + clickEvent.type = code + clickEvent.key = code + clickEvent.ie = { code = code, x = clickEvent.x, y = clickEvent.y } + + if clickEvent.element.focus then + target:setFocus(clickEvent.element) + end + self:inputEvent(clickEvent.element, clickEvent) + + target:sync() +end + +function Manager:setDefaultDevice(dev) + self.defaultDevice = dev + self.term = dev +end + +function Manager:addPage(name, page) + if not self.pages then + self.pages = { } + end + self.pages[name] = page +end + +function Manager:setPages(pages) + self.pages = pages +end + +function Manager:getPage(pageName) + local page = self.pages[pageName] + + if not page then + error('UI:getPage: Invalid page: ' .. tostring(pageName), 2) + end + + return page +end + +function Manager:getActivePage(page) + if page then + return page.parent.currentPage + end + return self.defaultDevice.currentPage +end + +function Manager:setActivePage(page) + page.parent.currentPage = page + page.parent.canvas = page.canvas +end + +function Manager:setPage(pageOrName, ...) + local page = pageOrName + + if type(pageOrName) == 'string' then + page = self.pages[pageOrName] or error('Invalid page: ' .. pageOrName) + end + + local currentPage = self:getActivePage(page) + if page == currentPage then + page:draw() + else + if currentPage then + if currentPage.focused then + currentPage.focused.focused = false + currentPage.focused:focus() + end + currentPage:disable() + page.previousPage = currentPage + end + self:setActivePage(page) + --page:clear(page.backgroundColor) + page:enable(...) + page:draw() + if page.focused then + page.focused.focused = true + page.focused:focus() + end + page:sync() + end +end + +function Manager:getCurrentPage() + return self.defaultDevice.currentPage +end + +function Manager:setPreviousPage() + if self.defaultDevice.currentPage.previousPage then + local previousPage = self.defaultDevice.currentPage.previousPage.previousPage + self:setPage(self.defaultDevice.currentPage.previousPage) + self.defaultDevice.currentPage.previousPage = previousPage + end +end + +function Manager:getDefaults(element, args) + local defaults = Util.deepCopy(element.defaults) + if args then + Manager:mergeProperties(defaults, args) + end + return defaults +end + +function Manager:mergeProperties(obj, args) + if args then + for k,v in pairs(args) do + if k == 'accelerators' then + if obj.accelerators then + Util.merge(obj.accelerators, args.accelerators) + else + obj[k] = v + end + else + obj[k] = v + end + end + end +end + +function Manager:pullEvents(...) + local s, m = pcall(Event.pullEvents, ...) + self.term:reset() + if not s and m then + error(m, -1) + end +end + +function Manager:exitPullEvents() + Event.exitPullEvents() +end + +local UI = Manager() + +--[[-- Basic drawable area --]]-- +UI.Window = class() +UI.Window.uid = 1 +UI.Window.defaults = { + UIElement = 'Window', + x = 1, + y = 1, + -- z = 0, -- eventually... + offx = 0, + offy = 0, + cursorX = 1, + cursorY = 1, +} +function UI.Window:init(args) + -- merge defaults for all subclasses + local defaults = args + local m = getmetatable(self) -- get the class for this instance + repeat + defaults = UI:getDefaults(m, defaults) + m = m._base + until not m + UI:mergeProperties(self, defaults) + + -- each element has a unique ID + self.uid = UI.Window.uid + UI.Window.uid = UI.Window.uid + 1 + + -- at this time, the object has all the properties set + + -- postInit is a special constructor. the element does not need to implement + -- the method. But we need to guarantee that each subclass which has this + -- method is called. + m = self + local lpi + repeat + if m.postInit and m.postInit ~= lpi then + m.postInit(self) + lpi = m.postInit + end + m = m._base + until not m +end + +function UI.Window:postInit() + if self.parent then + -- this will cascade down the whole tree of elements starting at the + -- top level window (which has a device as a parent) + self:setParent() + end +end + +function UI.Window:initChildren() + local children = self.children + + -- insert any UI elements created using the shorthand + -- window definition into the children array + for k,child in pairs(self) do + if k ~= 'parent' then -- reserved + if type(child) == 'table' and child.UIElement and not child.parent then + if not children then + children = { } + end + table.insert(children, child) + end + end + end + if children then + for _,child in pairs(children) do + if not child.parent then + child.parent = self + child:setParent() + if self.enabled then + child:enable() + end + end + end + self.children = children + end +end + +function UI.Window:layout() + local function calc(p, max) + p = tonumber(p:match('(%d+)%%')) + return p and math.floor(max * p / 100) or 1 + end + + if type(self.x) == 'string' then + self.x = calc(self.x, self.parent.width) + end + if type(self.ex) == 'string' then + self.ex = calc(self.ex, self.parent.width) + end + if type(self.y) == 'string' then + self.y = calc(self.y, self.parent.height) + end + if type(self.ey) == 'string' then + self.ey = calc(self.ey, self.parent.height) + end + + if self.x < 0 then + self.x = self.parent.width + self.x + 1 + end + if self.y < 0 then + self.y = self.parent.height + self.y + 1 + end + + if self.ex then + local ex = self.ex + if self.ex <= 1 then + ex = self.parent.width + self.ex + 1 + end + if self.width then + self.x = ex - self.width + 1 + else + self.width = ex - self.x + 1 + end + end + if self.ey then + local ey = self.ey + if self.ey <= 1 then + ey = self.parent.height + self.ey + 1 + end + if self.height then + self.y = ey - self.height + 1 + else + self.height = ey - self.y + 1 + end + end + + if not self.width then + self.width = self.parent.width - self.x + 1 + end + if not self.height then + self.height = self.parent.height - self.y + 1 + end +end + +-- Called when the window's parent has be assigned +function UI.Window:setParent() + self.oh, self.ow = self.height, self.width + self.ox, self.oy = self.x, self.y + + self:layout() + + self:initChildren() +end + +function UI.Window:resize() + self.height, self.width = self.oh, self.ow + self.x, self.y = self.ox, self.oy + + self:layout() + + if self.children then + for _,child in ipairs(self.children) do + child:resize() + end + end +end + +function UI.Window:add(children) + UI:mergeProperties(self, children) + self:initChildren() +end + +function UI.Window:getCursorPos() + return self.cursorX, self.cursorY +end + +function UI.Window:setCursorPos(x, y) + self.cursorX = x + self.cursorY = y + self.parent:setCursorPos(self.x + x - 1, self.y + y - 1) +end + +function UI.Window:setCursorBlink(blink) + self.parent:setCursorBlink(blink) +end + +function UI.Window:draw() + self:clear(self.backgroundColor) + if self.children then + for _,child in pairs(self.children) do + if child.enabled then + child:draw() + end + end + end +end + +function UI.Window:sync() + if self.parent then + self.parent:sync() + end +end + +function UI.Window:enable(...) + self.enabled = true + if self.children then + for _,child in pairs(self.children) do + child:enable(...) + end + end +end + +function UI.Window:disable() + self.enabled = false + if self.children then + for _,child in pairs(self.children) do + child:disable() + end + end +end + +function UI.Window:setTextScale(textScale) + self.textScale = textScale + self.parent:setTextScale(textScale) +end + +function UI.Window:clear(bg, fg) + if self.canvas then + self.canvas:clear(bg or self.backgroundColor, fg or self.textColor) + else + self:clearArea(1 + self.offx, 1 + self.offy, self.width, self.height, bg) + end +end + +function UI.Window:clearLine(y, bg) + self:write(1, y, _rep(' ', self.width), bg) +end + +function UI.Window:clearArea(x, y, width, height, bg) + if width > 0 then + local filler = _rep(' ', width) + for i = 0, height - 1 do + self:write(x, y + i, filler, bg) + end + end +end + +function UI.Window:write(x, y, text, bg, tc) + bg = bg or self.backgroundColor + tc = tc or self.textColor + + if self.canvas then + self.canvas:write(x, y, text, bg, tc) + else + x = x - self.offx + y = y - self.offy + if y <= self.height and y > 0 then + self.parent:write( + self.x + x - 1, self.y + y - 1, tostring(text), bg, tc) + end + end +end + +function UI.Window:centeredWrite(y, text, bg, fg) + if #text >= self.width then + self:write(1, y, text, bg, fg) + else + local space = math.floor((self.width-#text) / 2) + local filler = _rep(' ', space + 1) + local str = _sub(filler, 1, space) .. text + str = str .. _sub(filler, self.width - #str + 1) + self:write(1, y, str, bg, fg) + end +end + +function UI.Window:print(text, bg, fg) + local marginLeft = self.marginLeft or 0 + local marginRight = self.marginRight or 0 + local width = self.width - marginLeft - marginRight + + local function nextWord(line, cx) + local result = { line:find("(%w+)", cx) } + if #result > 1 and result[2] > cx then + return _sub(line, cx, result[2] + 1) + elseif #result > 0 and result[1] == cx then + result = { line:find("(%w+)", result[2]) } + if #result > 0 then + return _sub(line, cx, result[1] + 1) + end + end + if cx <= #line then + return _sub(line, cx, #line) + end + end + + local function pieces(f, bg, fg) + local pos = 1 + local t = { } + while true do + local s = string.find(f, '\027', pos, true) + if not s then + break + end + if pos < s then + table.insert(t, _sub(f, pos, s - 1)) + end + local seq = _sub(f, s) + seq = seq:match("\027%[([%d;]+)m") + local e = { } + for color in string.gmatch(seq, "%d+") do + color = tonumber(color) + if color == 0 then + e.fg = fg + e.bg = bg + elseif color > 20 then + e.bg = 2 ^ (color - 21) + else + e.fg = 2 ^ (color - 1) + end + end + table.insert(t, e) + pos = s + #seq + 3 + end + if pos <= #f then + table.insert(t, _sub(f, pos)) + end + return t + end + + local lines = Util.split(text) + for k,line in pairs(lines) do + local fragments = pieces(line, bg, fg) + for _, fragment in ipairs(fragments) do + local lx = 1 + if type(fragment) == 'table' then -- ansi sequence + fg = fragment.fg + bg = fragment.bg + else + while true do + local word = nextWord(fragment, lx) + if not word then + break + end + local w = word + if self.cursorX + #word > width then + self.cursorX = marginLeft + 1 + self.cursorY = self.cursorY + 1 + w = word:gsub('^ ', '') + end + self:write(self.cursorX, self.cursorY, w, bg, fg) + self.cursorX = self.cursorX + #w + lx = lx + #word + end + end + end + if lines[k + 1] then + self.cursorX = marginLeft + 1 + self.cursorY = self.cursorY + 1 + end + end + + return self.cursorX, self.cursorY +end + +function UI.Window:setFocus(focus) + if self.parent then + self.parent:setFocus(focus) + end +end + +function UI.Window:capture(child) + if self.parent then + self.parent:capture(child) + end +end + +function UI.Window:release(child) + if self.parent then + self.parent:release(child) + end +end + +function UI.Window:pointToChild(x, y) + -- TODO: get rid of this offx/y mess and scroll canvas instead + x = x + self.offx - self.x + 1 + y = y + self.offy - self.y + 1 + if self.children then + for _,child in pairs(self.children) do + if child.enabled and not child.inactive and + x >= child.x and x < child.x + child.width and + y >= child.y and y < child.y + child.height then + local c = child:pointToChild(x, y) + if c then + return c + end + end + end + end + return { + element = self, + x = x, + y = y + } +end + +function UI.Window:getFocusables() + local focusable = { } + + local function focusSort(a, b) + if a.y == b.y then + return a.x < b.x + end + return a.y < b.y + end + + local function getFocusable(parent) + for _,child in Util.spairs(parent.children, focusSort) do + if child.enabled and child.focus and not child.inactive then + table.insert(focusable, child) + end + if child.children then + getFocusable(child) + end + end + end + + if self.children then + getFocusable(self, self.x, self.y) + end + + return focusable +end + +function UI.Window:focusFirst() + local focusables = self:getFocusables() + local focused = focusables[1] + if focused then + self:setFocus(focused) + end +end + +function UI.Window:refocus() + local el = self + while el do + local focusables = el:getFocusables() + if focusables[1] then + self:setFocus(focusables[1]) + break + end + el = el.parent + end +end + +function UI.Window:scrollIntoView() + local parent = self.parent + + if self.x <= parent.offx then + parent.offx = math.max(0, self.x - 1) + parent:draw() + elseif self.x + self.width > parent.width + parent.offx then + parent.offx = self.x + self.width - parent.width - 1 + parent:draw() + end + + -- TODO: fix + local function setOffset(y) + parent.offy = y + if parent.canvas then + parent.canvas.offy = parent.offy + end + parent:draw() + end + + if self.y <= parent.offy then + setOffset(math.max(0, self.y - 1)) + elseif self.y + self.height > parent.height + parent.offy then + setOffset(self.y + self.height - parent.height - 1) + end +end + +function UI.Window:getCanvas() + local el = self + repeat + if el.canvas then + return el.canvas + end + el = el.parent + until not el +end + +function UI.Window:addLayer(bg, fg) + local canvas = self:getCanvas() + local x, y = self.x, self.y + local parent = self.parent + while parent and not parent.canvas do + x = x + parent.x - 1 + y = y + parent.y - 1 + parent = parent.parent + end + canvas = canvas:addLayer({ + x = x, y = y, height = self.height, width = self.width + }, bg, fg) + + canvas:clear(bg or self.backgroundColor, fg or self.textColor) + return canvas +end + +function UI.Window:addTransition(effect, args) + if self.parent then + args = args or { } + if not args.x then -- not good + args.x, args.y = getPosition(self) + args.width = self.width + args.height = self.height + end + + args.canvas = args.canvas or self.canvas + self.parent:addTransition(effect, args) + end +end + +function UI.Window:emit(event) + local parent = self + while parent do + if parent.eventHandler then + if parent:eventHandler(event) then + return true + end + end + parent = parent.parent + end +end + +function UI.Window:getProperty(property) + return self[property] or self.parent and self.parent:getProperty(property) +end + +function UI.Window:find(uid) + return self.children and Util.find(self.children, 'uid', uid) +end + +function UI.Window:eventHandler() + return false +end + +--[[-- Terminal for computer / advanced computer / monitor --]]-- +UI.Device = class(UI.Window) +UI.Device.defaults = { + UIElement = 'Device', + backgroundColor = colors.black, + textColor = colors.white, + textScale = 1, + effectsEnabled = true, +} +function UI.Device:postInit() + self.device = self.device or term.current() + + --if self.deviceType then + -- self.device = device[self.deviceType] + --end + + if not self.device.setTextScale then + self.device.setTextScale = function() end + end + + self.device.setTextScale(self.textScale) + self.width, self.height = self.device.getSize() + + self.isColor = self.device.isColor() + + UI.devices[self.device.side or 'terminal'] = self +end + +function UI.Device:resize() + self.device.setTextScale(self.textScale) + self.width, self.height = self.device.getSize() + self.lines = { } + -- TODO: resize all pages added to this device + self.canvas:resize(self.width, self.height) + self.canvas:clear(self.backgroundColor, self.textColor) +end + +function UI.Device:setCursorPos(x, y) + self.cursorX = x + self.cursorY = y +end + +function UI.Device:getCursorBlink() + return self.cursorBlink +end + +function UI.Device:setCursorBlink(blink) + self.cursorBlink = blink + self.device.setCursorBlink(blink) +end + +function UI.Device:setTextScale(textScale) + self.textScale = textScale + self.device.setTextScale(self.textScale) +end + +function UI.Device:reset() + self.device.setBackgroundColor(colors.black) + self.device.setTextColor(colors.white) + self.device.clear() + self.device.setCursorPos(1, 1) +end + +function UI.Device:addTransition(effect, args) + if not self.transitions then + self.transitions = { } + end + + args = args or { } + args.ex = args.x + args.width - 1 + args.ey = args.y + args.height - 1 + args.canvas = args.canvas or self.canvas + + if type(effect) == 'string' then + effect = Transition[effect] + if not effect then + error('Invalid transition') + end + end + + table.insert(self.transitions, { update = effect(args), args = args }) +end + +function UI.Device:runTransitions(transitions, canvas) + while true do + for _,k in ipairs(Util.keys(transitions)) do + local transition = transitions[k] + if not transition.update() then + transitions[k] = nil + end + end + canvas:render(self.device) + if Util.empty(transitions) then + break + end + os.sleep(0) + end +end + +function UI.Device:sync() + local transitions = self.effectsEnabled and self.transitions + self.transitions = nil + + if self:getCursorBlink() then + self.device.setCursorBlink(false) + end + + self.canvas:render(self.device) + if transitions then + self:runTransitions(transitions, self.canvas) + end + + if self:getCursorBlink() then + self.device.setCursorPos(self.cursorX, self.cursorY) + self.device.setCursorBlink(true) + end +end + +--[[-- Page (focus manager) --]]-- +UI.Page = class(UI.Window) +UI.Page.defaults = { + UIElement = 'Page', + accelerators = { + down = 'focus_next', + enter = 'focus_next', + tab = 'focus_next', + ['shift-tab' ] = 'focus_prev', + up = 'focus_prev', + }, + backgroundColor = colors.cyan, + textColor = colors.white, +} +function UI.Page:postInit() + self.parent = self.parent or UI.defaultDevice + self.__target = self + self.canvas = Canvas({ + x = 1, y = 1, width = self.parent.width, height = self.parent.height, + isColor = self.parent.isColor, + }) + self.canvas:clear(self.backgroundColor, self.textColor) +end + +function UI.Page:enable() + self.canvas.visible = true + UI.Window.enable(self) + + if not self.focused or not self.focused.enabled then + self:focusFirst() + end +end + +function UI.Page:disable() + self.canvas.visible = false + UI.Window.disable(self) +end + +function UI.Page:sync() + if self.enabled then + self.parent:sync() + end +end + +function UI.Page:capture(child) + self.__target = child +end + +function UI.Page:release(child) + if self.__target == child then + self.__target = self + end +end + +function UI.Page:pointToChild(x, y) + if self.__target == self then + return UI.Window.pointToChild(self, x, y) + end + x = x + self.offx - self.x + 1 + y = y + self.offy - self.y + 1 + return self.__target:pointToChild(x, y) +end + +function UI.Page:getFocusables() + if self.__target == self or self.__target.pageType ~= 'modal' then + return UI.Window.getFocusables(self) + end + return self.__target:getFocusables() +end + +function UI.Page:getFocused() + return self.focused +end + +function UI.Page:focusPrevious() + local function getPreviousFocus(focused) + local focusables = self:getFocusables() + local k = Util.contains(focusables, focused) + if k then + if k > 1 then + return focusables[k - 1] + end + return focusables[#focusables] + end + end + + local focused = getPreviousFocus(self.focused) + if focused then + self:setFocus(focused) + end +end + +function UI.Page:focusNext() + local function getNextFocus(focused) + local focusables = self:getFocusables() + local k = Util.contains(focusables, focused) + if k then + if k < #focusables then + return focusables[k + 1] + end + return focusables[1] + end + end + + local focused = getNextFocus(self.focused) + if focused then + self:setFocus(focused) + end +end + +function UI.Page:setFocus(child) + if not child or not child.focus then + return + end + + if self.focused and self.focused ~= child then + self.focused.focused = false + self.focused:focus() + self.focused:emit({ type = 'focus_lost', focused = child }) + end + + self.focused = child + if not child.focused then + child.focused = true + child:emit({ type = 'focus_change', focused = child }) + --self:emit({ type = 'focus_change', focused = child }) + end + + child:focus() +end + +function UI.Page:eventHandler(event) + if self.focused then + if event.type == 'focus_next' then + self:focusNext() + return true + elseif event.type == 'focus_prev' then + self:focusPrevious() + return true + end + end +end + +local function loadComponents() + local function load(name) + local s, m = Util.run(_ENV, 'sys/modules/opus/ui/components/' .. name .. '.lua') + if not s then + error(m) + end + if UI[name]._preload then + error('Error loading UI.' .. name) + end + if UI.theme[name] and UI[name].defaults then + Util.merge(UI[name].defaults, UI.theme[name]) + end + return UI[name] + end + + local components = fs.list('sys/modules/opus/ui/components') + for _, f in pairs(components) do + local name = f:match('(.+)%.') + + UI[name] = setmetatable({ }, { + __call = function(self, ...) + load(name) + setmetatable(self, getmetatable(UI[name])) + return self(...) + end + }) + UI[name]._preload = function(self) + return load(name) + end + end +end + +loadComponents() +UI:loadTheme('usr/config/ui.theme') +Util.merge(UI.Window.defaults, UI.theme.Window) +Util.merge(UI.Page.defaults, UI.theme.Page) +UI:setDefaultDevice(UI.Device({ device = term.current() })) + +return UI diff --git a/sys/modules/opus/ui/canvas.lua b/sys/modules/opus/ui/canvas.lua new file mode 100644 index 0000000..75e1792 --- /dev/null +++ b/sys/modules/opus/ui/canvas.lua @@ -0,0 +1,414 @@ +local class = require('opus.class') +local Region = require('opus.ui.region') +local Util = require('opus.util') + +local _rep = string.rep +local _sub = string.sub +local _gsub = string.gsub +local colors = _G.colors + +local Canvas = class() + +Canvas.colorPalette = { } +Canvas.darkPalette = { } +Canvas.grayscalePalette = { } + +for n = 1, 16 do + Canvas.colorPalette[2 ^ (n - 1)] = _sub("0123456789abcdef", n, n) + Canvas.grayscalePalette[2 ^ (n - 1)] = _sub("088888878877787f", n, n) + Canvas.darkPalette[2 ^ (n - 1)] = _sub("8777777f77fff77f", n, n) +end + +--[[ + A canvas can have more lines than canvas.height in order to scroll +]] + +function Canvas:init(args) + self.x = 1 + self.y = 1 + self.layers = { } + + Util.merge(self, args) + + self.ex = self.x + self.width - 1 + self.ey = self.y + self.height - 1 + + if not self.palette then + if self.isColor then + self.palette = Canvas.colorPalette + else + self.palette = Canvas.grayscalePalette + end + end + + self.lines = { } + for i = 1, self.height do + self.lines[i] = { } + end +end + +function Canvas:move(x, y) + self.x, self.y = x, y + self.ex = self.x + self.width - 1 + self.ey = self.y + self.height - 1 +end + +function Canvas:resize(w, h) + for i = #self.lines, h do + self.lines[i] = { } + self:clearLine(i) + end + + while #self.lines > h do + table.remove(self.lines, #self.lines) + end + + if w < self.width then + for i = 1, h do + self.lines[i].text = _sub(self.lines[i].text, 1, w) + self.lines[i].fg = _sub(self.lines[i].fg, 1, w) + self.lines[i].bg = _sub(self.lines[i].bg, 1, w) + end + elseif w > self.width then + local d = w - self.width + local text = _rep(' ', d) + local fg = _rep(self.palette[self.fg or colors.white], d) + local bg = _rep(self.palette[self.bg or colors.black], d) + for i = 1, h do + self.lines[i].text = self.lines[i].text .. text + self.lines[i].fg = self.lines[i].fg .. fg + self.lines[i].bg = self.lines[i].bg .. bg + end + end + + self.ex = self.x + w - 1 + self.ey = self.y + h - 1 + self.width = w + self.height = h +end + +function Canvas:copy() + local b = Canvas({ + x = self.x, + y = self.y, + width = self.width, + height = self.height, + isColor = self.isColor, + }) + for i = 1, #self.lines do + b.lines[i].text = self.lines[i].text + b.lines[i].fg = self.lines[i].fg + b.lines[i].bg = self.lines[i].bg + end + return b +end + +function Canvas:addLayer(layer) + local canvas = Canvas({ + x = layer.x, + y = layer.y, + width = layer.width, + height = layer.height, + isColor = self.isColor, + }) + canvas.parent = self + table.insert(self.layers, canvas) + return canvas +end + +function Canvas:removeLayer() + for k, layer in pairs(self.parent.layers) do + if layer == self then + self:setVisible(false) + table.remove(self.parent.layers, k) + break + end + end +end + +function Canvas:setVisible(visible) + self.visible = visible + if not visible and self.parent then + self.parent:dirty() + -- TODO: set parent's lines to dirty for each line in self + end +end + +-- Push a layer to the top +function Canvas:raise() + if self.parent then + local layers = self.parent.layers or { } + for k, v in pairs(layers) do + if v == self then + table.insert(layers, table.remove(layers, k)) + break + end + end + end +end + +function Canvas:write(x, y, text, bg, fg) + if bg then + bg = _rep(self.palette[bg], #text) + end + if fg then + fg = _rep(self.palette[fg] or self.palette[1], #text) + end + self:blit(x, y, text, bg, fg) +end + +function Canvas:blit(x, y, text, bg, fg) + if y > 0 and y <= #self.lines and x <= self.width then + local width = #text + + -- fix ffs + if x < 1 then + text = _sub(text, 2 - x) + if bg then + bg = _sub(bg, 2 - x) + end + if fg then + fg = _sub(fg, 2 - x) + end + width = width + x - 1 + x = 1 + end + + if x + width - 1 > self.width then + text = _sub(text, 1, self.width - x + 1) + if bg then + bg = _sub(bg, 1, self.width - x + 1) + end + if fg then + fg = _sub(fg, 1, self.width - x + 1) + end + width = #text + end + + if width > 0 then + + local function replace(sstr, pos, rstr) + if pos == 1 and width == self.width then + return rstr + elseif pos == 1 then + return rstr .. _sub(sstr, pos+width) + elseif pos + width > self.width then + return _sub(sstr, 1, pos-1) .. rstr + end + return _sub(sstr, 1, pos-1) .. rstr .. _sub(sstr, pos+width) + end + + local line = self.lines[y] + if line then + line.dirty = true + line.text = replace(line.text, x, text, width) + if fg then + line.fg = replace(line.fg, x, fg, width) + end + if bg then + line.bg = replace(line.bg, x, bg, width) + end + end + end + end +end + +function Canvas:writeLine(y, text, fg, bg) + if y > 0 and y <= #self.lines then + self.lines[y].dirty = true + self.lines[y].text = text + self.lines[y].fg = fg + self.lines[y].bg = bg + end +end + +function Canvas:clearLine(y, bg, fg) + fg = _rep(self.palette[fg or colors.white], self.width) + bg = _rep(self.palette[bg or colors.black], self.width) + self:writeLine(y, _rep(' ', self.width), fg, bg) +end + +function Canvas:clear(bg, fg) + local text = _rep(' ', self.width) + fg = _rep(self.palette[fg or colors.white], self.width) + bg = _rep(self.palette[bg or colors.black], self.width) + for i = 1, #self.lines do + self:writeLine(i, text, fg, bg) + end +end + +function Canvas:isDirty() + for i = 1, #self.lines do + if self.lines[i].dirty then + return true + end + end +end + +function Canvas:dirty() + for i = 1, #self.lines do + self.lines[i].dirty = true + end + if self.layers then + for _, canvas in pairs(self.layers) do + canvas:dirty() + end + end +end + +function Canvas:clean() + for i = 1, #self.lines do + self.lines[i].dirty = nil + end +end + +function Canvas:applyPalette(palette) + local lookup = { } + for n = 1, 16 do + lookup[self.palette[2 ^ (n - 1)]] = palette[2 ^ (n - 1)] + end + + for _, l in pairs(self.lines) do + l.fg = _gsub(l.fg, '%w', lookup) + l.bg = _gsub(l.bg, '%w', lookup) + l.dirty = true + end + + self.palette = palette +end + +function Canvas:render(device) + local offset = { x = 0, y = 0 } + local parent = self.parent + while parent do + offset.x = offset.x + parent.x - 1 + offset.y = offset.y + parent.y - 1 + parent = parent.parent + end + if #self.layers > 0 then + self:__renderLayers(device, offset) + else + self:__blitRect(device, nil, { + x = self.x + offset.x, + y = self.y + offset.y + }) + self:clean() + end +end + +-- regions are comprised of absolute values that coorespond to the output device. +-- canvases have coordinates relative to their parent. +-- canvas layer's stacking order is determined by the position within the array. +-- layers in the beginning of the array are overlayed by layers further down in +-- the array. +function Canvas:__renderLayers(device, offset) + if #self.layers > 0 then + self.regions = self.regions or Region.new(self.x, self.y, self.ex, self.ey) + + for i = 1, #self.layers do + local canvas = self.layers[i] + if canvas.visible then + + -- punch out this area from the parent's canvas + self:__punch(canvas, offset) + + -- get the area to render for this layer + canvas.regions = Region.new( + canvas.x + offset.x, + canvas.y + offset.y, + canvas.ex + offset.x, + canvas.ey + offset.y) + + -- punch out any layers that overlap this one + for j = i + 1, #self.layers do + if self.layers[j].visible then + canvas:__punch(self.layers[j], offset) + end + end + if #canvas.regions.region > 0 then + canvas:__renderLayers(device, { + x = canvas.x + offset.x - 1, + y = canvas.y + offset.y - 1, + }) + end + canvas.regions = nil + end + end + + self:__blitClipped(device, offset) + self.regions = nil + + elseif self.regions and #self.regions.region > 0 then + self:__blitClipped(device, offset) + self.regions = nil + + else + self:__blitRect(device, nil, { + x = self.x + offset.x, + y = self.y + offset.y + }) + self.regions = nil + end + self:clean() +end + +function Canvas:__blitClipped(device, offset) + for _,region in ipairs(self.regions.region) do + self:__blitRect(device, + { x = region[1] - offset.x, + y = region[2] - offset.y, + ex = region[3] - offset.x, + ey = region[4] - offset.y}, + { x = region[1], y = region[2] }) + end +end + +function Canvas:__punch(rect, offset) + self.regions:subRect( + rect.x + offset.x, + rect.y + offset.y, + rect.ex + offset.x, + rect.ey + offset.y) +end + +function Canvas:__blitRect(device, src, tgt) + src = src or { x = 1, y = 1, ex = self.ex - self.x + 1, ey = self.ey - self.y + 1 } + tgt = tgt or self + + --[[ + -- for visualizing updates on the screen + local drew + for i = 0, src.ey - src.y do + local line = self.lines[src.y + i + (self.offy or 0)] + if line and line.dirty then + drew = true + local t, fg, bg = line.text, line.fg, line.bg + if src.x > 1 or src.ex < self.ex then + t = _sub(t, src.x, src.ex) + fg = _rep(1, src.ex-src.x + 1) + bg = _rep(2, src.ex-src.x + 1) + end + device.setCursorPos(tgt.x, tgt.y + i) + device.blit(t, fg, bg) + end + end + if drew then + os.sleep(.3) + end + ]] + for i = 0, src.ey - src.y do + local line = self.lines[src.y + i + (self.offy or 0)] + if line and line.dirty then + local t, fg, bg = line.text, line.fg, line.bg + if src.x > 1 or src.ex < self.ex then + t = _sub(t, src.x, src.ex) + fg = _sub(fg, src.x, src.ex) + bg = _sub(bg, src.x, src.ex) + end + device.setCursorPos(tgt.x, tgt.y + i) + device.blit(t, fg, bg) + end + end +end + +return Canvas diff --git a/sys/modules/opus/ui/components/ActiveLayer.lua b/sys/modules/opus/ui/components/ActiveLayer.lua new file mode 100644 index 0000000..dcbb499 --- /dev/null +++ b/sys/modules/opus/ui/components/ActiveLayer.lua @@ -0,0 +1,32 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +UI.ActiveLayer = class(UI.Window) +UI.ActiveLayer.defaults = { + UIElement = 'ActiveLayer', +} +function UI.ActiveLayer:layout() + UI.Window.layout(self) + if not self.canvas then + self.canvas = self:addLayer() + else + self.canvas:resize(self.width, self.height) + end +end + +function UI.ActiveLayer:enable(...) + self.canvas:raise() + self.canvas:setVisible(true) + UI.Window.enable(self, ...) + if self.parent.transitionHint then + self:addTransition(self.parent.transitionHint) + end + self:focusFirst() +end + +function UI.ActiveLayer:disable() + if self.canvas then + self.canvas:setVisible(false) + end + UI.Window.disable(self) +end diff --git a/sys/modules/opus/ui/components/Button.lua b/sys/modules/opus/ui/components/Button.lua new file mode 100644 index 0000000..c6ea3bf --- /dev/null +++ b/sys/modules/opus/ui/components/Button.lua @@ -0,0 +1,66 @@ +local class = require('opus.class') +local UI = require('opus.ui') +local Util = require('opus.util') + +local colors = _G.colors + +UI.Button = class(UI.Window) +UI.Button.defaults = { + UIElement = 'Button', + text = 'button', + backgroundColor = colors.lightGray, + backgroundFocusColor = colors.gray, + textFocusColor = colors.white, + textInactiveColor = colors.gray, + textColor = colors.black, + centered = true, + height = 1, + focusIndicator = ' ', + event = 'button_press', + accelerators = { + space = 'button_activate', + enter = 'button_activate', + mouse_click = 'button_activate', + } +} +function UI.Button:setParent() + if not self.width and not self.ex then + self.width = #self.text + 2 + end + UI.Window.setParent(self) +end + +function UI.Button:draw() + local fg = self.textColor + local bg = self.backgroundColor + local ind = ' ' + if self.focused then + bg = self.backgroundFocusColor + fg = self.textFocusColor + ind = self.focusIndicator + elseif self.inactive then + fg = self.textInactiveColor + end + local text = ind .. self.text .. ' ' + if self.centered then + self:clear(bg) + self:centeredWrite(1 + math.floor(self.height / 2), text, bg, fg) + else + self:write(1, 1, Util.widthify(text, self.width), bg, fg) + end +end + +function UI.Button:focus() + if self.focused then + self:scrollIntoView() + end + self:draw() +end + +function UI.Button:eventHandler(event) + if event.type == 'button_activate' then + self:emit({ type = self.event, button = self }) + return true + end + return false +end diff --git a/sys/modules/opus/ui/components/Checkbox.lua b/sys/modules/opus/ui/components/Checkbox.lua new file mode 100644 index 0000000..a90e911 --- /dev/null +++ b/sys/modules/opus/ui/components/Checkbox.lua @@ -0,0 +1,63 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +local colors = _G.colors + +UI.Checkbox = class(UI.Window) +UI.Checkbox.defaults = { + UIElement = 'Checkbox', + nochoice = 'Select', + checkedIndicator = UI.extChars and '\4' or 'X', + leftMarker = UI.extChars and '\124' or '[', + rightMarker = UI.extChars and '\124' or ']', + value = false, + textColor = colors.white, + backgroundColor = colors.black, + backgroundFocusColor = colors.lightGray, + height = 1, + width = 3, + accelerators = { + space = 'checkbox_toggle', + mouse_click = 'checkbox_toggle', + } +} +function UI.Checkbox:draw() + local bg = self.backgroundColor + if self.focused then + bg = self.backgroundFocusColor + end + if type(self.value) == 'string' then + self.value = nil -- TODO: fix form + end + local text = string.format('[%s]', not self.value and ' ' or self.checkedIndicator) + local x = 1 + if self.label then + self:write(1, 1, self.label) + x = #self.label + 2 + end + self:write(x, 1, text, bg) + self:write(x, 1, self.leftMarker, self.backgroundColor, self.textColor) + self:write(x + 1, 1, not self.value and ' ' or self.checkedIndicator, bg) + self:write(x + 2, 1, self.rightMarker, self.backgroundColor, self.textColor) +end + +function UI.Checkbox:focus() + self:draw() +end + +function UI.Checkbox:setValue(v) + self.value = v +end + +function UI.Checkbox:reset() + self.value = false +end + +function UI.Checkbox:eventHandler(event) + if event.type == 'checkbox_toggle' then + self.value = not self.value + self:emit({ type = 'checkbox_change', checked = self.value, element = self }) + self:draw() + return true + end +end diff --git a/sys/modules/opus/ui/components/Chooser.lua b/sys/modules/opus/ui/components/Chooser.lua new file mode 100644 index 0000000..e4d41fa --- /dev/null +++ b/sys/modules/opus/ui/components/Chooser.lua @@ -0,0 +1,88 @@ +local class = require('opus.class') +local UI = require('opus.ui') +local Util = require('opus.util') + +local colors = _G.colors + +UI.Chooser = class(UI.Window) +UI.Chooser.defaults = { + UIElement = 'Chooser', + choices = { }, + nochoice = 'Select', + backgroundFocusColor = colors.lightGray, + textInactiveColor = colors.gray, + leftIndicator = UI.extChars and '\17' or '<', + rightIndicator = UI.extChars and '\16' or '>', + height = 1, +} +function UI.Chooser:setParent() + if not self.width and not self.ex then + self.width = 1 + for _,v in pairs(self.choices) do + if #v.name > self.width then + self.width = #v.name + end + end + self.width = self.width + 4 + end + UI.Window.setParent(self) +end + +function UI.Chooser:draw() + local bg = self.backgroundColor + if self.focused then + bg = self.backgroundFocusColor + end + local fg = self.inactive and self.textInactiveColor or self.textColor + local choice = Util.find(self.choices, 'value', self.value) + local value = self.nochoice + if choice then + value = choice.name + end + self:write(1, 1, self.leftIndicator, self.backgroundColor, colors.black) + self:write(2, 1, ' ' .. Util.widthify(tostring(value), self.width-4) .. ' ', bg, fg) + self:write(self.width, 1, self.rightIndicator, self.backgroundColor, colors.black) +end + +function UI.Chooser:focus() + self:draw() +end + +function UI.Chooser:eventHandler(event) + if event.type == 'key' then + if event.key == 'right' or event.key == 'space' then + local _,k = Util.find(self.choices, 'value', self.value) + local choice + if not k then k = 1 end + if k and k < #self.choices then + choice = self.choices[k+1] + else + choice = self.choices[1] + end + self.value = choice.value + self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice }) + self:draw() + return true + elseif event.key == 'left' then + local _,k = Util.find(self.choices, 'value', self.value) + local choice + if k and k > 1 then + choice = self.choices[k-1] + else + choice = self.choices[#self.choices] + end + self.value = choice.value + self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice }) + self:draw() + return true + end + elseif event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then + if event.x == 1 then + self:emit({ type = 'key', key = 'left' }) + return true + elseif event.x == self.width then + self:emit({ type = 'key', key = 'right' }) + return true + end + end +end diff --git a/sys/modules/opus/ui/components/Dialog.lua b/sys/modules/opus/ui/components/Dialog.lua new file mode 100644 index 0000000..dd9de1a --- /dev/null +++ b/sys/modules/opus/ui/components/Dialog.lua @@ -0,0 +1,39 @@ +local Canvas = require('opus.ui.canvas') +local class = require('opus.class') +local UI = require('opus.ui') + +local colors = _G.colors + +UI.Dialog = class(UI.SlideOut) +UI.Dialog.defaults = { + UIElement = 'Dialog', + height = 7, + textColor = colors.black, + backgroundColor = colors.white, + okEvent ='dialog_ok', + cancelEvent = 'dialog_cancel', +} +function UI.Dialog:postInit() + self.y = -self.height + self.titleBar = UI.TitleBar({ event = self.cancelEvent, title = self.title }) +end + +function UI.Dialog:show(...) + local canvas = self.parent:getCanvas() + self.oldPalette = canvas.palette + canvas:applyPalette(Canvas.darkPalette) + UI.SlideOut.show(self, ...) +end + +function UI.Dialog:hide(...) + self.parent:getCanvas().palette = self.oldPalette + UI.SlideOut.hide(self, ...) + self.parent:draw() +end + +function UI.Dialog:eventHandler(event) + if event.type == 'dialog_cancel' then + self:hide() + end + return UI.SlideOut.eventHandler(self, event) +end diff --git a/sys/modules/opus/ui/components/DropMenu.lua b/sys/modules/opus/ui/components/DropMenu.lua new file mode 100644 index 0000000..5cce43a --- /dev/null +++ b/sys/modules/opus/ui/components/DropMenu.lua @@ -0,0 +1,75 @@ +local class = require('opus.class') +local UI = require('opus.ui') +local Util = require('opus.util') + +local colors = _G.colors + +UI.DropMenu = class(UI.MenuBar) +UI.DropMenu.defaults = { + UIElement = 'DropMenu', + backgroundColor = colors.white, + buttonClass = 'DropMenuItem', +} +function UI.DropMenu:layout() + UI.MenuBar.layout(self) + + local maxWidth = 1 + for y,child in ipairs(self.children) do + child.x = 1 + child.y = y + if #(child.text or '') > maxWidth then + maxWidth = #child.text + end + end + for _,child in ipairs(self.children) do + child.width = maxWidth + 2 + if child.spacer then + child.inactive = true + child.text = string.rep('-', child.width - 2) + end + end + + self.height = #self.children + 1 + self.width = maxWidth + 2 + + if not self.canvas then + self.canvas = self:addLayer() + else + self.canvas:resize(self.width, self.height) + end +end + +function UI.DropMenu:enable() +end + +function UI.DropMenu:show(x, y) + self.x, self.y = x, y + self.canvas:move(x, y) + self.canvas:setVisible(true) + + UI.Window.enable(self) + + self:draw() + self:capture(self) + self:focusFirst() +end + +function UI.DropMenu:hide() + self:disable() + self.canvas:setVisible(false) + self:release(self) +end + +function UI.DropMenu:eventHandler(event) + if event.type == 'focus_lost' and self.enabled then + if not Util.contains(self.children, event.focused) then + self:hide() + end + elseif event.type == 'mouse_out' and self.enabled then + self:hide() + self:refocus() + else + return UI.MenuBar.eventHandler(self, event) + end + return true +end diff --git a/sys/modules/opus/ui/components/DropMenuItem.lua b/sys/modules/opus/ui/components/DropMenuItem.lua new file mode 100644 index 0000000..09263da --- /dev/null +++ b/sys/modules/opus/ui/components/DropMenuItem.lua @@ -0,0 +1,21 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +local colors = _G.colors + +--[[-- DropMenuItem --]]-- +UI.DropMenuItem = class(UI.Button) +UI.DropMenuItem.defaults = { + UIElement = 'DropMenuItem', + textColor = colors.black, + backgroundColor = colors.white, + textFocusColor = colors.white, + textInactiveColor = colors.lightGray, + backgroundFocusColor = colors.lightGray, +} +function UI.DropMenuItem:eventHandler(event) + if event.type == 'button_activate' then + self.parent:hide() + end + return UI.Button.eventHandler(self, event) +end diff --git a/sys/modules/opus/ui/components/Embedded.lua b/sys/modules/opus/ui/components/Embedded.lua new file mode 100644 index 0000000..fcdee82 --- /dev/null +++ b/sys/modules/opus/ui/components/Embedded.lua @@ -0,0 +1,76 @@ +local class = require('opus.class') +local Terminal = require('opus.terminal') +local UI = require('opus.ui') + +local colors = _G.colors + +UI.Embedded = class(UI.Window) +UI.Embedded.defaults = { + UIElement = 'Embedded', + backgroundColor = colors.black, + textColor = colors.white, + maxScroll = 100, + accelerators = { + up = 'scroll_up', + down = 'scroll_down', + } +} +function UI.Embedded:setParent() + UI.Window.setParent(self) + + self.win = Terminal.window(UI.term.device, self.x, self.y, self.width, self.height, false) + self.win.setMaxScroll(self.maxScroll) + + local canvas = self:getCanvas() + self.win.getCanvas().parent = canvas + table.insert(canvas.layers, self.win.getCanvas()) + self.canvas = self.win.getCanvas() + + self.win.setCursorPos(1, 1) + self.win.setBackgroundColor(self.backgroundColor) + self.win.setTextColor(self.textColor) + self.win.clear() +end + +function UI.Embedded:layout() + UI.Window.layout(self) + if self.win then + self.win.reposition(self.x, self.y, self.width, self.height) + end +end + +function UI.Embedded:draw() + self.canvas:dirty() +end + +function UI.Embedded:enable() + self.canvas:setVisible(true) + self.canvas:raise() + if self.visible then + -- the window will automatically update on changes + -- the canvas does not need to be rendereed + self.win.setVisible(true) + end + UI.Window.enable(self) + self.canvas:dirty() +end + +function UI.Embedded:disable() + self.canvas:setVisible(false) + self.win.setVisible(false) + UI.Window.disable(self) +end + +function UI.Embedded:eventHandler(event) + if event.type == 'scroll_up' then + self.win.scrollUp() + return true + elseif event.type == 'scroll_down' then + self.win.scrollDown() + return true + end +end + +function UI.Embedded:focus() + -- allow scrolling +end diff --git a/sys/modules/opus/ui/components/Form.lua b/sys/modules/opus/ui/components/Form.lua new file mode 100644 index 0000000..ed9ae0d --- /dev/null +++ b/sys/modules/opus/ui/components/Form.lua @@ -0,0 +1,148 @@ +local class = require('opus.class') +local Sound = require('opus.sound') +local UI = require('opus.ui') + +local colors = _G.colors + +UI.Form = class(UI.Window) +UI.Form.defaults = { + UIElement = 'Form', + values = { }, + margin = 2, + event = 'form_complete', + cancelEvent = 'form_cancel', +} +function UI.Form:postInit() + self:createForm() +end + +function UI.Form:reset() + for _,child in pairs(self.children) do + if child.reset then + child:reset() + end + end +end + +function UI.Form:setValues(values) + self:reset() + self.values = values + for _,child in pairs(self.children) do + if child.formKey then + if child.setValue then + child:setValue(self.values[child.formKey]) + else + child.value = self.values[child.formKey] or '' + end + end + end +end + +function UI.Form:createForm() + self.children = self.children or { } + + if not self.labelWidth then + self.labelWidth = 1 + for _, child in pairs(self) do + if type(child) == 'table' and child.UIElement then + if child.formLabel then + self.labelWidth = math.max(self.labelWidth, #child.formLabel + 2) + end + end + end + end + + local y = self.margin + for _, child in pairs(self) do + if type(child) == 'table' and child.UIElement then + if child.formKey then + child.value = self.values[child.formKey] or '' + end + if child.formLabel then + child.x = self.labelWidth + self.margin - 1 + child.y = child.formIndex and (child.formIndex + self.margin - 1) or y + if not child.width and not child.ex then + child.ex = -self.margin + end + + table.insert(self.children, UI.Text { + x = self.margin, + y = child.y, + textColor = colors.black, + width = #child.formLabel, + value = child.formLabel, + }) + end + if child.formLabel then + y = y + 1 + end + end + end + + if not self.manualControls then + table.insert(self.children, UI.Button { + y = -self.margin, x = -12 - self.margin, + text = 'Ok', + event = 'form_ok', + }) + table.insert(self.children, UI.Button { + y = -self.margin, x = -7 - self.margin, + text = 'Cancel', + event = self.cancelEvent, + }) + end +end + +function UI.Form:validateField(field) + if field.required then + if not field.value or #tostring(field.value) == 0 then + return false, 'Field is required' + end + end + if field.validate == 'numeric' then + field.value = field.value or '' + if #tostring(field.value) > 0 then + if not tonumber(field.value) then + return false, 'Invalid number' + end + end + end + return true +end + +function UI.Form:save() + for _,child in pairs(self.children) do + if child.formKey then + local s, m = self:validateField(child) + if not s then + self:setFocus(child) + Sound.play('entity.villager.no', .5) + self:emit({ type = 'form_invalid', message = m, field = child }) + return false + end + end + end + for _,child in pairs(self.children) do + if child.formKey then + if child.validate == 'numeric' then + self.values[child.formKey] = tonumber(child.value) + else + self.values[child.formKey] = child.value + end + end + end + + return true +end + +function UI.Form:eventHandler(event) + if event.type == 'form_ok' then + if not self:save() then + return false + end + self:emit({ type = self.event, UIElement = self, values = self.values }) + else + return UI.Window.eventHandler(self, event) + end + return true +end diff --git a/sys/modules/opus/ui/components/Grid.lua b/sys/modules/opus/ui/components/Grid.lua new file mode 100644 index 0000000..bcdab81 --- /dev/null +++ b/sys/modules/opus/ui/components/Grid.lua @@ -0,0 +1,495 @@ +local class = require('opus.class') +local UI = require('opus.ui') +local Util = require('opus.util') + +local colors = _G.colors +local os = _G.os +local _rep = string.rep +local _sub = string.sub + +local function safeValue(v) + local t = type(v) + if t == 'string' or t == 'number' then + return v + end + return tostring(v) +end + +local Writer = class() +function Writer:init(element, y) + self.element = element + self.y = y + self.x = 1 +end + +function Writer:write(s, width, align, bg, fg) + local len = #tostring(s or '') + if len > width then + s = _sub(s, 1, width) + end + local padding = len < width and _rep(' ', width - len) + if padding then + if align == 'right' then + s = padding .. s + else + s = s .. padding + end + end + self.element:write(self.x, self.y, s, bg, fg) + self.x = self.x + width +end + +function Writer:finish(bg) + if self.x <= self.element.width then + self.element:write(self.x, self.y, _rep(' ', self.element.width - self.x + 1), bg) + end + self.x = 1 + self.y = self.y + 1 +end + +--[[-- Grid --]]-- +UI.Grid = class(UI.Window) +UI.Grid.defaults = { + UIElement = 'Grid', + index = 1, + inverseSort = false, + disableHeader = false, + headerHeight = 1, + marginRight = 0, + textColor = colors.white, + textSelectedColor = colors.white, + backgroundColor = colors.black, + backgroundSelectedColor = colors.gray, + headerBackgroundColor = colors.cyan, + headerTextColor = colors.white, + headerSortColor = colors.yellow, + unfocusedTextSelectedColor = colors.white, + unfocusedBackgroundSelectedColor = colors.gray, + focusIndicator = UI.extChars and '\183' or '>', + sortIndicator = ' ', + inverseSortIndicator = UI.extChars and '\24' or '^', + values = { }, + columns = { }, + accelerators = { + enter = 'key_enter', + [ 'control-c' ] = 'copy', + down = 'scroll_down', + up = 'scroll_up', + home = 'scroll_top', + [ 'end' ] = 'scroll_bottom', + pageUp = 'scroll_pageUp', + [ 'control-b' ] = 'scroll_pageUp', + pageDown = 'scroll_pageDown', + [ 'control-f' ] = 'scroll_pageDown', + }, +} +function UI.Grid:setParent() + UI.Window.setParent(self) + + for _,c in pairs(self.columns) do + c.cw = c.width + if not c.heading then + c.heading = '' + end + end + + self:update() + + if not self.pageSize then + if self.disableHeader then + self.pageSize = self.height + else + self.pageSize = self.height - self.headerHeight + end + end +end + +function UI.Grid:resize() + UI.Window.resize(self) + + if self.disableHeader then + self.pageSize = self.height + else + self.pageSize = self.height - self.headerHeight + end + self:adjustWidth() +end + +function UI.Grid:adjustWidth() + local t = { } -- cols without width + local w = self.width - #self.columns - 1 - self.marginRight -- width remaining + + for _,c in pairs(self.columns) do + if c.width then + c.cw = c.width + w = w - c.cw + else + table.insert(t, c) + end + end + + if #t == 0 then + return + end + + if #t == 1 then + t[1].cw = #(t[1].heading or '') + t[1].cw = math.max(t[1].cw, w) + return + end + + if not self.autospace then + for k,c in ipairs(t) do + c.cw = math.floor(w / (#t - k + 1)) + w = w - c.cw + end + + else + for _,c in ipairs(t) do + c.cw = #(c.heading or '') + w = w - c.cw + end + -- adjust the size to the length of the value + for key,row in pairs(self.values) do + if w <= 0 then + break + end + row = self:getDisplayValues(row, key) + for _,col in pairs(t) do + local value = row[col.key] + if value then + value = tostring(value) + if #value > col.cw then + w = w + col.cw + col.cw = math.min(#value, w) + w = w - col.cw + if w <= 0 then + break + end + end + end + end + end + + -- last column does not get padding (right alignment) + if not self.columns[#self.columns].width then + Util.removeByValue(t, self.columns[#self.columns]) + end + + -- got some extra room - add some padding + if w > 0 then + for k,c in ipairs(t) do + local padding = math.floor(w / (#t - k + 1)) + c.cw = c.cw + padding + w = w - padding + end + end + end +end + +function UI.Grid:setPageSize(pageSize) + self.pageSize = pageSize +end + +function UI.Grid:getValues() + return self.values +end + +function UI.Grid:setValues(t) + self.values = t + self:update() +end + +function UI.Grid:setInverseSort(inverseSort) + self.inverseSort = inverseSort + self:update() + self:setIndex(self.index) +end + +function UI.Grid:setSortColumn(column) + self.sortColumn = column +end + +function UI.Grid:getDisplayValues(row, key) + return row +end + +function UI.Grid:getSelected() + if self.sorted then + return self.values[self.sorted[self.index]], self.sorted[self.index] + end +end + +function UI.Grid:setSelected(name, value) + if self.sorted then + for k,v in pairs(self.sorted) do + if self.values[v][name] == value then + self:setIndex(k) + return + end + end + end + self:setIndex(1) +end + +function UI.Grid:focus() + self:drawRows() +end + +function UI.Grid:draw() + if not self.disableHeader then + self:drawHeadings() + end + + if self.index <= 0 then + self:setIndex(1) + elseif self.index > #self.sorted then + self:setIndex(#self.sorted) + end + self:drawRows() +end + +-- Something about the displayed table has changed +-- resort the table +function UI.Grid:update() + local function sort(a, b) + if not a[self.sortColumn] then + return false + elseif not b[self.sortColumn] then + return true + end + return self:sortCompare(a, b) + end + + local function inverseSort(a, b) + return not sort(a, b) + end + + local order + if self.sortColumn then + order = sort + if self.inverseSort then + order = inverseSort + end + end + + self.sorted = Util.keys(self.values) + if order then + table.sort(self.sorted, function(a,b) + return order(self.values[a], self.values[b]) + end) + end + + self:adjustWidth() +end + +function UI.Grid:drawHeadings() + if self.headerHeight > 1 then + self:clear(self.headerBackgroundColor) + end + local sb = Writer(self, math.ceil(self.headerHeight / 2)) + for _,col in ipairs(self.columns) do + local ind = ' ' + local color = self.headerTextColor + if col.key == self.sortColumn then + if self.inverseSort then + ind = self.inverseSortIndicator + else + ind = self.sortIndicator + end + color = self.headerSortColor + end + sb:write(ind .. col.heading, + col.cw + 1, + col.align, + self.headerBackgroundColor, + color) + end + sb:finish(self.headerBackgroundColor) +end + +function UI.Grid:sortCompare(a, b) + a = safeValue(a[self.sortColumn]) + b = safeValue(b[self.sortColumn]) + if type(a) == type(b) then + return a < b + end + return tostring(a) < tostring(b) +end + +function UI.Grid:drawRows() + local startRow = math.max(1, self:getStartRow()) + + local sb = Writer(self, self.disableHeader and 1 or self.headerHeight + 1) + + local lastRow = math.min(startRow + self.pageSize - 1, #self.sorted) + for index = startRow, lastRow do + + local key = self.sorted[index] + local rawRow = self.values[key] + local row = self:getDisplayValues(rawRow, key) + + local selected = index == self.index and not self.inactive + local bg = self:getRowBackgroundColor(rawRow, selected) + local fg = self:getRowTextColor(rawRow, selected) + local focused = self.focused and selected + + self:drawRow(sb, row, focused, bg, fg) + + sb:finish(bg) + end + + if sb.y <= self.height then + self:clearArea(1, sb.y, self.width, self.height - sb.y + 1) + end +end + +function UI.Grid:drawRow(sb, row, focused, bg, fg) + local ind = focused and self.focusIndicator or ' ' + + for _,col in pairs(self.columns) do + sb:write(ind .. safeValue(row[col.key] or ''), + col.cw + 1, + col.align, + col.backgroundColor or bg, + col.textColor or fg) + ind = ' ' + end +end + +function UI.Grid:getRowTextColor(row, selected) + if selected then + if self.focused then + return self.textSelectedColor + end + return self.unfocusedTextSelectedColor + end + return self.textColor +end + +function UI.Grid:getRowBackgroundColor(row, selected) + if selected then + if self.focused then + return self.backgroundSelectedColor + end + return self.unfocusedBackgroundSelectedColor + end + return self.backgroundColor +end + +function UI.Grid:getIndex() + return self.index +end + +function UI.Grid:setIndex(index) + index = math.max(1, index) + self.index = math.min(index, #self.sorted) + + local selected = self:getSelected() + if selected ~= self.selected then + self:drawRows() + self.selected = selected + if selected then + self:emit({ type = 'grid_focus_row', selected = selected, element = self }) + end + end +end + +function UI.Grid:getStartRow() + return math.floor((self.index - 1) / self.pageSize) * self.pageSize + 1 +end + +function UI.Grid:getPage() + return math.floor(self.index / self.pageSize) + 1 +end + +function UI.Grid:getPageCount() + local tableSize = Util.size(self.values) + local pc = math.floor(tableSize / self.pageSize) + if tableSize % self.pageSize > 0 then + pc = pc + 1 + end + return pc +end + +function UI.Grid:nextPage() + self:setPage(self:getPage() + 1) +end + +function UI.Grid:previousPage() + self:setPage(self:getPage() - 1) +end + +function UI.Grid:setPage(pageNo) + -- 1 based paging + self:setIndex((pageNo-1) * self.pageSize + 1) +end + +function UI.Grid:eventHandler(event) + if event.type == 'mouse_click' or + event.type == 'mouse_rightclick' or + event.type == 'mouse_doubleclick' then + if not self.disableHeader then + if event.y <= self.headerHeight then + local col = 2 + for _,c in ipairs(self.columns) do + if event.x < col + c.cw then + self:emit({ + type = 'grid_sort', + sortColumn = c.key, + inverseSort = self.sortColumn == c.key and not self.inverseSort, + element = self, + }) + break + end + col = col + c.cw + 1 + end + return true + end + end + local row = self:getStartRow() + event.y - 1 + if not self.disableHeader then + row = row - self.headerHeight + end + if row > 0 and row <= Util.size(self.values) then + self:setIndex(row) + if event.type == 'mouse_doubleclick' then + self:emit({ type = 'key_enter' }) + elseif event.type == 'mouse_rightclick' then + self:emit({ type = 'grid_select_right', selected = self.selected, element = self }) + end + return true + end + return false + + elseif event.type == 'grid_sort' then + self.sortColumn = event.sortColumn + self:setInverseSort(event.inverseSort) + self:draw() + elseif event.type == 'scroll_down' then + self:setIndex(self.index + 1) + elseif event.type == 'scroll_up' then + self:setIndex(self.index - 1) + elseif event.type == 'scroll_top' then + self:setIndex(1) + elseif event.type == 'scroll_bottom' then + self:setIndex(Util.size(self.values)) + elseif event.type == 'scroll_pageUp' then + self:setIndex(self.index - self.pageSize) + elseif event.type == 'scroll_pageDown' then + self:setIndex(self.index + self.pageSize) + elseif event.type == 'scroll_to' then + self:setIndex(event.offset) + elseif event.type == 'key_enter' then + if self.selected then + self:emit({ type = 'grid_select', selected = self.selected, element = self }) + end + elseif event.type == 'copy' then + if self.selected then + os.queueEvent('clipboard_copy', self.selected) + end + else + return false + end + return true +end diff --git a/sys/modules/opus/ui/components/Image.lua b/sys/modules/opus/ui/components/Image.lua new file mode 100644 index 0000000..787d813 --- /dev/null +++ b/sys/modules/opus/ui/components/Image.lua @@ -0,0 +1,40 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +UI.Image = class(UI.Window) +UI.Image.defaults = { + UIElement = 'Image', + event = 'button_press', +} +function UI.Image:setParent() + if self.image then + self.height = #self.image + end + if self.image and not self.width then + self.width = #self.image[1] + end + UI.Window.setParent(self) +end + +function UI.Image:draw() + self:clear() + if self.image then + for y = 1, #self.image do + local line = self.image[y] + for x = 1, #line do + local ch = line[x] + if type(ch) == 'number' then + if ch > 0 then + self:write(x, y, ' ', ch) + end + else + self:write(x, y, ch) + end + end + end + end +end + +function UI.Image:setImage(image) + self.image = image +end diff --git a/sys/modules/opus/ui/components/Menu.lua b/sys/modules/opus/ui/components/Menu.lua new file mode 100644 index 0000000..e31e8df --- /dev/null +++ b/sys/modules/opus/ui/components/Menu.lua @@ -0,0 +1,61 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +--[[-- Menu --]]-- +UI.Menu = class(UI.Grid) +UI.Menu.defaults = { + UIElement = 'Menu', + disableHeader = true, + columns = { { heading = 'Prompt', key = 'prompt', width = 20 } }, + menuItems = { }, +} +function UI.Menu:postInit() + self.values = self.menuItems + self.pageSize = #self.menuItems +end + +function UI.Menu:setParent() + UI.Grid.setParent(self) + self.itemWidth = 1 + for _,v in pairs(self.values) do + if #v.prompt > self.itemWidth then + self.itemWidth = #v.prompt + end + end + self.columns[1].width = self.itemWidth + + if self.centered then + self:center() + else + self.width = self.itemWidth + 2 + end +end + +function UI.Menu:center() + self.x = (self.width - self.itemWidth + 2) / 2 + self.width = self.itemWidth + 2 +end + +function UI.Menu:eventHandler(event) + if event.type == 'key' then + if event.key == 'enter' then + local selected = self.menuItems[self.index] + self:emit({ + type = selected.event or 'menu_select', + selected = selected + }) + return true + end + elseif event.type == 'mouse_click' then + if event.y <= #self.menuItems then + UI.Grid.setIndex(self, event.y) + local selected = self.menuItems[self.index] + self:emit({ + type = selected.event or 'menu_select', + selected = selected + }) + return true + end + end + return UI.Grid.eventHandler(self, event) +end diff --git a/sys/modules/opus/ui/components/MenuBar.lua b/sys/modules/opus/ui/components/MenuBar.lua new file mode 100644 index 0000000..bccb9be --- /dev/null +++ b/sys/modules/opus/ui/components/MenuBar.lua @@ -0,0 +1,90 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +local colors = _G.colors + +local function getPosition(element) + local x, y = 1, 1 + repeat + x = element.x + x - 1 + y = element.y + y - 1 + element = element.parent + until not element + return x, y +end + +UI.MenuBar = class(UI.Window) +UI.MenuBar.defaults = { + UIElement = 'MenuBar', + buttons = { }, + height = 1, + backgroundColor = colors.lightGray, + textColor = colors.black, + spacing = 2, + lastx = 1, + showBackButton = false, + buttonClass = 'MenuItem', +} +function UI.MenuBar:postInit() + self:addButtons(self.buttons) +end + +function UI.MenuBar:addButtons(buttons) + if not self.children then + self.children = { } + end + + for _,button in pairs(buttons) do + if button.UIElement then + table.insert(self.children, button) + else + local buttonProperties = { + x = self.lastx, + width = #(button.text or 'button') + self.spacing, + centered = false, + } + self.lastx = self.lastx + buttonProperties.width + UI:mergeProperties(buttonProperties, button) + + button = UI[self.buttonClass](buttonProperties) + if button.name then + self[button.name] = button + else + table.insert(self.children, button) + end + + if button.dropdown then + button.dropmenu = UI.DropMenu { buttons = button.dropdown } + end + end + end + if self.parent then + self:initChildren() + end +end + +function UI.MenuBar:getActive(menuItem) + return not menuItem.inactive +end + +function UI.MenuBar:eventHandler(event) + if event.type == 'button_press' and event.button.dropmenu then + if event.button.dropmenu.enabled then + event.button.dropmenu:hide() + self:refocus() + return true + else + local x, y = getPosition(event.button) + if x + event.button.dropmenu.width > self.width then + x = self.width - event.button.dropmenu.width + 1 + end + for _,c in pairs(event.button.dropmenu.children) do + if not c.spacer then + c.inactive = not self:getActive(c) + end + end + event.button.dropmenu:show(x, y + 1) + end + return true + end +end diff --git a/sys/modules/opus/ui/components/MenuItem.lua b/sys/modules/opus/ui/components/MenuItem.lua new file mode 100644 index 0000000..2f0efe8 --- /dev/null +++ b/sys/modules/opus/ui/components/MenuItem.lua @@ -0,0 +1,14 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +local colors = _G.colors + +--[[-- MenuItem --]]-- +UI.MenuItem = class(UI.Button) +UI.MenuItem.defaults = { + UIElement = 'MenuItem', + textColor = colors.black, + backgroundColor = colors.lightGray, + textFocusColor = colors.white, + backgroundFocusColor = colors.lightGray, +} diff --git a/sys/modules/opus/ui/components/NftImage.lua b/sys/modules/opus/ui/components/NftImage.lua new file mode 100644 index 0000000..4d295d7 --- /dev/null +++ b/sys/modules/opus/ui/components/NftImage.lua @@ -0,0 +1,35 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +UI.NftImage = class(UI.Window) +UI.NftImage.defaults = { + UIElement = 'NftImage', +} +function UI.NftImage:setParent() + if self.image then + self.height = self.image.height + end + if self.image and not self.width then + self.width = self.image.width + end + UI.Window.setParent(self) +end + +function UI.NftImage:draw() + if self.image then + -- due to blittle, the background and foreground transparent + -- color is the same as the background color + local bg = self:getProperty('backgroundColor') + for y = 1, self.image.height do + for x = 1, #self.image.text[y] do + self:write(x, y, self.image.text[y][x], self.image.bg[y][x], self.image.fg[y][x] or bg) + end + end + else + self:clear() + end +end + +function UI.NftImage:setImage(image) + self.image = image +end diff --git a/sys/modules/opus/ui/components/Notification.lua b/sys/modules/opus/ui/components/Notification.lua new file mode 100644 index 0000000..f1b38f6 --- /dev/null +++ b/sys/modules/opus/ui/components/Notification.lua @@ -0,0 +1,92 @@ +local class = require('opus.class') +local Event = require('opus.event') +local Sound = require('opus.sound') +local UI = require('opus.ui') +local Util = require('opus.util') + +local colors = _G.colors + +UI.Notification = class(UI.Window) +UI.Notification.defaults = { + UIElement = 'Notification', + backgroundColor = colors.gray, + closeInd = UI.extChars and '\215' or '*', + height = 3, + timeout = 3, + anchor = 'bottom', +} +function UI.Notification:draw() +end + +function UI.Notification:enable() +end + +function UI.Notification:error(value, timeout) + self.backgroundColor = colors.red + Sound.play('entity.villager.no', .5) + self:display(value, timeout) +end + +function UI.Notification:info(value, timeout) + self.backgroundColor = colors.lightGray + self:display(value, timeout) +end + +function UI.Notification:success(value, timeout) + self.backgroundColor = colors.green + self:display(value, timeout) +end + +function UI.Notification:cancel() + if self.timer then + Event.off(self.timer) + self.timer = nil + end + + if self.canvas then + self.enabled = false + self.canvas:removeLayer() + self.canvas = nil + end +end + +function UI.Notification:display(value, timeout) + self:cancel() + self.enabled = true + local lines = Util.wordWrap(value, self.width - 3) + self.height = #lines + + if self.anchor == 'bottom' then + self.y = self.parent.height - self.height + 1 + self.canvas = self:addLayer(self.backgroundColor, self.textColor) + self:addTransition('expandUp', { ticks = self.height }) + else + self.canvas = self:addLayer(self.backgroundColor, self.textColor) + self.y = 1 + end + self.canvas:setVisible(true) + self:clear() + for k,v in pairs(lines) do + self:write(2, k, v) + end + + timeout = timeout or self.timeout + if timeout > 0 then + self.timer = Event.onTimeout(timeout, function() + self:cancel() + self:sync() + end) + else + self:write(self.width, 1, self.closeInd) + self:sync() + end +end + +function UI.Notification:eventHandler(event) + if event.type == 'mouse_click' then + if event.x == self.width then + self:cancel() + return true + end + end +end diff --git a/sys/modules/opus/ui/components/ProgressBar.lua b/sys/modules/opus/ui/components/ProgressBar.lua new file mode 100644 index 0000000..af12708 --- /dev/null +++ b/sys/modules/opus/ui/components/ProgressBar.lua @@ -0,0 +1,28 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +local colors = _G.colors + +UI.ProgressBar = class(UI.Window) +UI.ProgressBar.defaults = { + UIElement = 'ProgressBar', + backgroundColor = colors.gray, + height = 1, + progressColor = colors.lime, + progressChar = UI.extChars and '\153' or ' ', + fillChar = ' ', + fillColor = colors.gray, + textColor = colors.green, + value = 0, +} +function UI.ProgressBar:draw() + local width = math.ceil(self.value / 100 * self.width) + + local filler = string.rep(self.fillChar, self.width) + local progress = string.rep(self.progressChar, width) + + for i = 1, self.height do + self:write(1, i, filler, nil, self.fillColor) + self:write(1, i, progress, self.progressColor) + end +end diff --git a/sys/modules/opus/ui/components/ScrollBar.lua b/sys/modules/opus/ui/components/ScrollBar.lua new file mode 100644 index 0000000..d4bef52 --- /dev/null +++ b/sys/modules/opus/ui/components/ScrollBar.lua @@ -0,0 +1,74 @@ +local class = require('opus.class') +local UI = require('opus.ui') +local Util = require('opus.util') + +local colors = _G.colors + +UI.ScrollBar = class(UI.Window) +UI.ScrollBar.defaults = { + UIElement = 'ScrollBar', + lineChar = '|', + sliderChar = UI.extChars and '\127' or '#', + upArrowChar = UI.extChars and '\30' or '^', + downArrowChar = UI.extChars and '\31' or 'v', + scrollbarColor = colors.lightGray, + width = 1, + x = -1, + ey = -1, +} +function UI.ScrollBar:draw() + local view = self.parent:getViewArea() + + if view.totalHeight > view.height then + local maxScroll = view.totalHeight - view.height + local percent = view.offsetY / maxScroll + local sliderSize = math.max(1, Util.round(view.height / view.totalHeight * (view.height - 2))) + local x = self.width + + local row = view.y + if not view.static then -- does the container scroll ? + self.height = view.totalHeight + end + + for i = 1, view.height - 2 do + self:write(x, row + i, self.lineChar, nil, self.scrollbarColor) + end + + local y = Util.round((view.height - 2 - sliderSize) * percent) + for i = 1, sliderSize do + self:write(x, row + y + i, self.sliderChar, nil, self.scrollbarColor) + end + + local color = self.scrollbarColor + if view.offsetY > 0 then + color = colors.white + end + self:write(x, row, self.upArrowChar, nil, color) + + color = self.scrollbarColor + if view.offsetY + view.height < view.totalHeight then + color = colors.white + end + self:write(x, row + view.height - 1, self.downArrowChar, nil, color) + end +end + +function UI.ScrollBar:eventHandler(event) + if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then + if event.x == 1 then + local view = self.parent:getViewArea() + if view.totalHeight > view.height then + if event.y == view.y then + self:emit({ type = 'scroll_up'}) + elseif event.y == view.y + view.height - 1 then + self:emit({ type = 'scroll_down'}) + else + local percent = (event.y - view.y) / (view.height - 2) + local y = math.floor((view.totalHeight - view.height) * percent) + self:emit({ type = 'scroll_to', offset = y }) + end + end + return true + end + end +end diff --git a/sys/modules/opus/ui/components/ScrollingGrid.lua b/sys/modules/opus/ui/components/ScrollingGrid.lua new file mode 100644 index 0000000..93a35e6 --- /dev/null +++ b/sys/modules/opus/ui/components/ScrollingGrid.lua @@ -0,0 +1,60 @@ +local class = require('opus.class') +local UI = require('opus.ui') +local Util = require('opus.util') + +--[[-- ScrollingGrid --]]-- +UI.ScrollingGrid = class(UI.Grid) +UI.ScrollingGrid.defaults = { + UIElement = 'ScrollingGrid', + scrollOffset = 0, + marginRight = 1, +} +function UI.ScrollingGrid:postInit() + self.scrollBar = UI.ScrollBar() +end + +function UI.ScrollingGrid:drawRows() + UI.Grid.drawRows(self) + self.scrollBar:draw() +end + +function UI.ScrollingGrid:getViewArea() + local y = 1 + if not self.disableHeader then + y = y + self.headerHeight + end + + return { + static = true, -- the container doesn't scroll + y = y, -- scrollbar Y + height = self.pageSize, -- viewable height + totalHeight = Util.size(self.values), -- total height + offsetY = self.scrollOffset, -- scroll offset + } +end + +function UI.ScrollingGrid:getStartRow() + local ts = Util.size(self.values) + if ts < self.pageSize then + self.scrollOffset = 0 + end + return self.scrollOffset + 1 +end + +function UI.ScrollingGrid:setIndex(index) + if index < self.scrollOffset + 1 then + self.scrollOffset = index - 1 + elseif index - self.scrollOffset > self.pageSize then + self.scrollOffset = index - self.pageSize + end + + if self.scrollOffset < 0 then + self.scrollOffset = 0 + else + local ts = Util.size(self.values) + if self.pageSize + self.scrollOffset + 1 > ts then + self.scrollOffset = math.max(0, ts - self.pageSize) + end + end + UI.Grid.setIndex(self, index) +end diff --git a/sys/modules/opus/ui/components/SlideOut.lua b/sys/modules/opus/ui/components/SlideOut.lua new file mode 100644 index 0000000..37f5e37 --- /dev/null +++ b/sys/modules/opus/ui/components/SlideOut.lua @@ -0,0 +1,52 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +--[[-- SlideOut --]]-- +UI.SlideOut = class(UI.Window) +UI.SlideOut.defaults = { + UIElement = 'SlideOut', + pageType = 'modal', +} +function UI.SlideOut:layout() + UI.Window.layout(self) + if not self.canvas then + self.canvas = self:addLayer() + else + self.canvas:resize(self.width, self.height) + end +end + +function UI.SlideOut:enable() +end + +function UI.SlideOut:show(...) + self:addTransition('expandUp') + self.canvas:raise() + self.canvas:setVisible(true) + UI.Window.enable(self, ...) + self:draw() + self:capture(self) + self:focusFirst() +end + +function UI.SlideOut:disable() + self.canvas:setVisible(false) + UI.Window.disable(self) +end + +function UI.SlideOut:hide() + self:disable() + self:release(self) + self:refocus() +end + +function UI.SlideOut:eventHandler(event) + if event.type == 'slide_show' then + self:show() + return true + + elseif event.type == 'slide_hide' then + self:hide() + return true + end +end diff --git a/sys/modules/opus/ui/components/Slider.lua b/sys/modules/opus/ui/components/Slider.lua new file mode 100644 index 0000000..9dee765 --- /dev/null +++ b/sys/modules/opus/ui/components/Slider.lua @@ -0,0 +1,77 @@ +local class = require('opus.class') +local UI = require('opus.ui') +local Util = require('opus.util') + +local colors = _G.colors + +UI.Slider = class(UI.Window) +UI.Slider.defaults = { + UIElement = 'Slider', + height = 1, + barChar = UI.extChars and '\140' or '-', + barColor = colors.gray, + sliderChar = UI.extChars and '\143' or '\124', + sliderColor = colors.blue, + sliderFocusColor = colors.lightBlue, + leftBorder = UI.extChars and '\141' or '\124', + rightBorder = UI.extChars and '\142' or '\124', + value = 0, + min = 0, + max = 100, + event = 'slider_update', + accelerators = { + right = 'slide_right', + left = 'slide_left', + } +} +function UI.Slider:setValue(value) + self.value = tonumber(value) or self.min +end + +function UI.Slider:reset() -- form support + self.value = self.min + self:draw() +end + +function UI.Slider:focus() + self:draw() +end + +function UI.Slider:draw() + local range = self.max - self.min + local perc = (self.value - self.min) / range + local progress = Util.clamp(1 + self.width * perc, 1, self.width) + + local bar = { } + for i = 1, self.width do + local filler = + i == 1 and self.leftBorder or + i == self.width and self.rightBorder or + self.barChar + + table.insert(bar, filler) + end + self:write(1, 1, table.concat(bar), nil, self.barColor) + self:write(progress, 1, self.sliderChar, nil, self.focused and self.sliderFocusColor or self.sliderColor) +end + +function UI.Slider:eventHandler(event) + if event.type == "mouse_down" or event.type == "mouse_drag" then + local range = self.max - self.min + local i = (event.x - 1) / (self.width - 1) + self.value = self.min + (i * range) + self:emit({ type = self.event, value = self.value, element = self }) + self:draw() + + elseif event.type == 'slide_left' or event.type == 'slide_right' then + local range = self.max - self.min + local step = range / self.width + if event.type == 'slide_left' then + self.value = Util.clamp(self.value - step, self.min, self.max) + else + self.value = Util.clamp(self.value + step, self.min, self.max) + end + self:emit({ type = self.event, value = self.value, element = self }) + self:draw() + end +end diff --git a/sys/modules/opus/ui/components/StatusBar.lua b/sys/modules/opus/ui/components/StatusBar.lua new file mode 100644 index 0000000..a92c0c2 --- /dev/null +++ b/sys/modules/opus/ui/components/StatusBar.lua @@ -0,0 +1,98 @@ +local class = require('opus.class') +local Event = require('opus.event') +local UI = require('opus.ui') +local Util = require('opus.util') + +local colors = _G.colors + +UI.StatusBar = class(UI.Window) +UI.StatusBar.defaults = { + UIElement = 'StatusBar', + backgroundColor = colors.lightGray, + textColor = colors.gray, + height = 1, + ey = -1, +} +function UI.StatusBar:adjustWidth() + -- Can only have 1 adjustable width + if self.columns then + local w = self.width - #self.columns - 1 + for _,c in pairs(self.columns) do + if c.width then + c.cw = c.width -- computed width + w = w - c.width + end + end + for _,c in pairs(self.columns) do + if not c.width then + c.cw = w + end + end + end +end + +function UI.StatusBar:resize() + UI.Window.resize(self) + self:adjustWidth() +end + +function UI.StatusBar:setParent() + UI.Window.setParent(self) + self:adjustWidth() +end + +function UI.StatusBar:setStatus(status) + if self.values ~= status then + self.values = status + self:draw() + end +end + +function UI.StatusBar:setValue(name, value) + if not self.values then + self.values = { } + end + self.values[name] = value +end + +function UI.StatusBar:getValue(name) + if self.values then + return self.values[name] + end +end + +function UI.StatusBar:timedStatus(status, timeout) + self:write(2, 1, Util.widthify(status, self.width-2), self.backgroundColor) + Event.on(timeout or 3, function() + if self.enabled then + self:draw() + self:sync() + end + end) +end + +function UI.StatusBar:getColumnWidth(name) + local c = Util.find(self.columns, 'key', name) + return c and c.cw +end + +function UI.StatusBar:setColumnWidth(name, width) + local c = Util.find(self.columns, 'key', name) + if c then + c.cw = width + end +end + +function UI.StatusBar:draw() + if not self.values then + self:clear() + elseif type(self.values) == 'string' then + self:write(1, 1, Util.widthify(' ' .. self.values, self.width)) + else + local s = '' + for _,c in ipairs(self.columns) do + s = s .. ' ' .. Util.widthify(tostring(self.values[c.key] or ''), c.cw) + end + self:write(1, 1, Util.widthify(s, self.width)) + end +end diff --git a/sys/modules/opus/ui/components/Tab.lua b/sys/modules/opus/ui/components/Tab.lua new file mode 100644 index 0000000..2f86b2f --- /dev/null +++ b/sys/modules/opus/ui/components/Tab.lua @@ -0,0 +1,12 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +local colors = _G.colors + +UI.Tab = class(UI.ActiveLayer) +UI.Tab.defaults = { + UIElement = 'Tab', + tabTitle = 'tab', + backgroundColor = colors.cyan, + y = 2, +} diff --git a/sys/modules/opus/ui/components/TabBar.lua b/sys/modules/opus/ui/components/TabBar.lua new file mode 100644 index 0000000..72e07f6 --- /dev/null +++ b/sys/modules/opus/ui/components/TabBar.lua @@ -0,0 +1,45 @@ +local class = require('opus.class') +local UI = require('opus.ui') +local Util = require('opus.util') + +local colors = _G.colors + +UI.TabBar = class(UI.MenuBar) +UI.TabBar.defaults = { + UIElement = 'TabBar', + buttonClass = 'TabBarMenuItem', + selectedBackgroundColor = colors.cyan, +} +function UI.TabBar:enable() + UI.MenuBar.enable(self) + if not Util.find(self.children, 'selected', true) then + local menuItem = self:getFocusables()[1] + if menuItem then + menuItem.selected = true + end + end +end + +function UI.TabBar:eventHandler(event) + if event.type == 'tab_select' then + local selected, si = Util.find(self.children, 'uid', event.button.uid) + local previous, pi = Util.find(self.children, 'selected', true) + + if si ~= pi then + selected.selected = true + if previous then + previous.selected = false + self:emit({ type = 'tab_change', current = si, last = pi, tab = selected }) + end + end + UI.MenuBar.draw(self) + end + return UI.MenuBar.eventHandler(self, event) +end + +function UI.TabBar:selectTab(text) + local menuItem = Util.find(self.children, 'text', text) + if menuItem then + menuItem.selected = true + end +end diff --git a/sys/modules/opus/ui/components/TabBarMenuItem.lua b/sys/modules/opus/ui/components/TabBarMenuItem.lua new file mode 100644 index 0000000..0a7799c --- /dev/null +++ b/sys/modules/opus/ui/components/TabBarMenuItem.lua @@ -0,0 +1,25 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +local colors = _G.colors + +--[[-- TabBarMenuItem --]]-- +UI.TabBarMenuItem = class(UI.Button) +UI.TabBarMenuItem.defaults = { + UIElement = 'TabBarMenuItem', + event = 'tab_select', + textColor = colors.black, + selectedBackgroundColor = colors.cyan, + unselectedBackgroundColor = colors.lightGray, + backgroundColor = colors.lightGray, +} +function UI.TabBarMenuItem:draw() + if self.selected then + self.backgroundColor = self.selectedBackgroundColor + self.backgroundFocusColor = self.selectedBackgroundColor + else + self.backgroundColor = self.unselectedBackgroundColor + self.backgroundFocusColor = self.unselectedBackgroundColor + end + UI.Button.draw(self) +end diff --git a/sys/modules/opus/ui/components/Tabs.lua b/sys/modules/opus/ui/components/Tabs.lua new file mode 100644 index 0000000..46f87d1 --- /dev/null +++ b/sys/modules/opus/ui/components/Tabs.lua @@ -0,0 +1,89 @@ +local class = require('opus.class') +local UI = require('opus.ui') +local Util = require('opus.util') + +UI.Tabs = class(UI.Window) +UI.Tabs.defaults = { + UIElement = 'Tabs', +} +function UI.Tabs:postInit() + self:add(self) +end + +function UI.Tabs:add(children) + local buttons = { } + for _,child in pairs(children) do + if type(child) == 'table' and child.UIElement and child.tabTitle then + child.y = 2 + table.insert(buttons, { + text = child.tabTitle, + event = 'tab_select', + tabUid = child.uid, + }) + end + end + + if not self.tabBar then + self.tabBar = UI.TabBar({ + buttons = buttons, + }) + else + self.tabBar:addButtons(buttons) + end + + if self.parent then + return UI.Window.add(self, children) + end +end + +function UI.Tabs:selectTab(tab) + local menuItem = Util.find(self.tabBar.children, 'tabUid', tab.uid) + if menuItem then + self.tabBar:emit({ type = 'tab_select', button = { uid = menuItem.uid } }) + end +end + +function UI.Tabs:setActive(tab, active) + local menuItem = Util.find(self.tabBar.children, 'tabUid', tab.uid) + if menuItem then + menuItem.inactive = not active + end +end + +function UI.Tabs:enable() + self.enabled = true + self.transitionHint = nil + self.tabBar:enable() + + local menuItem = Util.find(self.tabBar.children, 'selected', true) + + for _,child in pairs(self.children) do + if child.uid == menuItem.tabUid then + child:enable() + self:emit({ type = 'tab_activate', activated = child }) + elseif child.tabTitle then + child:disable() + end + end +end + +function UI.Tabs:eventHandler(event) + if event.type == 'tab_change' then + local tab = self:find(event.tab.tabUid) + if event.current > event.last then + self.transitionHint = 'slideLeft' + else + self.transitionHint = 'slideRight' + end + + for _,child in pairs(self.children) do + if child.uid == event.tab.tabUid then + child:enable() + elseif child.tabTitle then + child:disable() + end + end + self:emit({ type = 'tab_activate', activated = tab }) + tab:draw() + end +end diff --git a/sys/modules/opus/ui/components/Text.lua b/sys/modules/opus/ui/components/Text.lua new file mode 100644 index 0000000..0250a48 --- /dev/null +++ b/sys/modules/opus/ui/components/Text.lua @@ -0,0 +1,20 @@ +local class = require('opus.class') +local UI = require('opus.ui') +local Util = require('opus.util') + +UI.Text = class(UI.Window) +UI.Text.defaults = { + UIElement = 'Text', + value = '', + height = 1, +} +function UI.Text:setParent() + if not self.width and not self.ex then + self.width = #tostring(self.value) + end + UI.Window.setParent(self) +end + +function UI.Text:draw() + self:write(1, 1, Util.widthify(self.value, self.width, self.align)) +end diff --git a/sys/modules/opus/ui/components/TextArea.lua b/sys/modules/opus/ui/components/TextArea.lua new file mode 100644 index 0000000..51b8972 --- /dev/null +++ b/sys/modules/opus/ui/components/TextArea.lua @@ -0,0 +1,36 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +--[[-- TextArea --]]-- +UI.TextArea = class(UI.Viewport) +UI.TextArea.defaults = { + UIElement = 'TextArea', + marginRight = 2, + value = '', +} +function UI.TextArea:postInit() + self.scrollBar = UI.ScrollBar() +end + +function UI.TextArea:setText(text) + self:reset() + self.value = text + self:draw() +end + +function UI.TextArea:focus() + -- allow keyboard scrolling +end + +function UI.TextArea:draw() + self:clear() +-- self:setCursorPos(1, 1) + self.cursorX, self.cursorY = 1, 1 + self:print(self.value) + + for _,child in pairs(self.children) do + if child.enabled then + child:draw() + end + end +end diff --git a/sys/modules/opus/ui/components/TextEntry.lua b/sys/modules/opus/ui/components/TextEntry.lua new file mode 100644 index 0000000..dda3766 --- /dev/null +++ b/sys/modules/opus/ui/components/TextEntry.lua @@ -0,0 +1,136 @@ +local class = require('opus.class') +local entry = require('opus.entry') +local UI = require('opus.ui') +local Util = require('opus.util') + +local colors = _G.colors +local _rep = string.rep +local _lower = string.lower +local _upper = string.upper + +UI.TextEntry = class(UI.Window) +UI.TextEntry.defaults = { + UIElement = 'TextEntry', + --value = '', + shadowText = '', + focused = false, + textColor = colors.white, + shadowTextColor = colors.gray, + backgroundColor = colors.black, -- colors.lightGray, + backgroundFocusColor = colors.black, --lightGray, + height = 1, + limit = 6, + accelerators = { + [ 'control-c' ] = 'copy', + } +} +function UI.TextEntry:postInit() + self.entry = entry({ limit = self.limit, offset = 2 }) +end + +function UI.TextEntry:layout() + UI.Window.layout(self) + self.entry.width = self.width - 2 +end + +function UI.TextEntry:setValue(value) + self.value = value --or '' + self.entry:unmark() + self.entry.value = tostring(value) + self.entry:updateScroll() +end + +function UI.TextEntry:setPosition(pos) + self.entry.pos = pos + self.entry.value = tostring(self.value or '') + self.entry:updateScroll() +end + +function UI.TextEntry:draw() + local bg = self.backgroundColor + local tc = self.textColor + if self.focused then + bg = self.backgroundFocusColor + end + + local text = tostring(self.value or '') + if #text > 0 then + if self.entry.scroll > 0 then + text = text:sub(1 + self.entry.scroll) + end + if self.mask then + text = _rep('*', #text) + end + else + tc = self.shadowTextColor + text = self.shadowText + end + + self:write(1, 1, ' ' .. Util.widthify(text, self.width - 2) .. ' ', bg, tc) + + if self.entry.mark.active then + local tx = math.max(self.entry.mark.x - self.entry.scroll, 0) + local tex = self.entry.mark.ex - self.entry.scroll + + if tex > self.width - 2 then -- unsure about this + tex = self.width - 2 - tx + end + + if tx ~= tex then + self:write(tx + 2, 1, text:sub(tx + 1, tex), colors.gray, tc) + end + end + if self.focused then + self:setCursorPos(self.entry.pos - self.entry.scroll + 2, 1) + end +end + +function UI.TextEntry:reset() + self.entry:reset() + self.value = nil--'' + self:draw() + self:updateCursor() +end + +function UI.TextEntry:updateCursor() + self:setCursorPos(self.entry.pos - self.entry.scroll + 2, 1) +end + +function UI.TextEntry:focus() + self:draw() + if self.focused then + self:setCursorBlink(true) + else + self:setCursorBlink(false) + end +end + +function UI.TextEntry:_transform(text) + if self.transform == 'lowercase' then + return _lower(text) + elseif self.transform == 'uppercase' then + return _upper(text) + elseif self.transform == 'number' then + return tonumber(text) --or 0 + end + return text +end + +function UI.TextEntry:eventHandler(event) + local text = self.value --or '' + self.entry.value = tostring(text or '') + if event.ie and self.entry:process(event.ie) then + if self.entry.textChanged then + self.value = self:_transform(self.entry.value) + self:draw() + if text ~= self.value then + self:emit({ type = 'text_change', text = self.value, element = self }) + end + elseif self.entry.posChanged then + self:updateCursor() + end + return true + end + + return false +end diff --git a/sys/modules/opus/ui/components/Throttle.lua b/sys/modules/opus/ui/components/Throttle.lua new file mode 100644 index 0000000..0466486 --- /dev/null +++ b/sys/modules/opus/ui/components/Throttle.lua @@ -0,0 +1,65 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +local colors = _G.colors +local os = _G.os + +UI.Throttle = class(UI.Window) +UI.Throttle.defaults = { + UIElement = 'Throttle', + backgroundColor = colors.gray, + bordercolor = colors.cyan, + height = 4, + width = 10, + timeout = .075, + ctr = 0, + image = { + ' //) (O )~@ &~&-( ?Q ', + ' //) (O )- @ \\-( ?) && ', + ' //) (O ), @ \\-(?) && ', + ' //) (O ). @ \\-d ) (@ ' + } +} +function UI.Throttle:setParent() + self.x = math.ceil((self.parent.width - self.width) / 2) + self.y = math.ceil((self.parent.height - self.height) / 2) + UI.Window.setParent(self) +end + +function UI.Throttle:enable() + self.c = os.clock() + self.enabled = false +end + +function UI.Throttle:disable() + if self.canvas then + self.enabled = false + self.canvas:removeLayer() + self.canvas = nil + self.ctr = 0 + end +end + +function UI.Throttle:update() + local cc = os.clock() + if cc > self.c + self.timeout then + os.sleep(0) + self.c = os.clock() + self.enabled = true + if not self.canvas then + self.canvas = self:addLayer(self.backgroundColor, self.borderColor) + self.canvas:setVisible(true) + self:clear(self.borderColor) + end + local image = self.image[self.ctr + 1] + local width = self.width - 2 + for i = 0, #self.image do + self:write(2, i + 1, image:sub(width * i + 1, width * i + width), + self.backgroundColor, self.textColor) + end + + self.ctr = (self.ctr + 1) % #self.image + + self:sync() + end +end diff --git a/sys/modules/opus/ui/components/TitleBar.lua b/sys/modules/opus/ui/components/TitleBar.lua new file mode 100644 index 0000000..ce7fdd4 --- /dev/null +++ b/sys/modules/opus/ui/components/TitleBar.lua @@ -0,0 +1,73 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +local colors = _G.colors +local _rep = string.rep +local _sub = string.sub + +-- For manipulating text in a fixed width string +local SB = class() +function SB:init(width) + self.width = width + self.buf = _rep(' ', width) +end +function SB:insert(x, str, width) + if x < 1 then + x = self.width + x + 1 + end + width = width or #str + if x + width - 1 > self.width then + width = self.width - x + end + if width > 0 then + self.buf = _sub(self.buf, 1, x - 1) .. _sub(str, 1, width) .. _sub(self.buf, x + width) + end +end +function SB:fill(x, ch, width) + width = width or self.width - x + 1 + self:insert(x, _rep(ch, width)) +end +function SB:center(str) + self:insert(math.max(1, math.ceil((self.width - #str + 1) / 2)), str) +end +function SB:get() + return self.buf +end + +UI.TitleBar = class(UI.Window) +UI.TitleBar.defaults = { + UIElement = 'TitleBar', + height = 1, + textColor = colors.white, + backgroundColor = colors.cyan, + title = '', + frameChar = UI.extChars and '\140' or '-', + closeInd = UI.extChars and '\215' or '*', +} +function UI.TitleBar:draw() + local sb = SB(self.width) + sb:fill(2, self.frameChar, sb.width - 3) + sb:center(string.format(' %s ', self.title)) + if self.previousPage or self.event then + sb:insert(-1, self.closeInd) + else + sb:insert(-2, self.frameChar) + end + self:write(1, 1, sb:get()) +end + +function UI.TitleBar:eventHandler(event) + if event.type == 'mouse_click' then + if (self.previousPage or self.event) and event.x == self.width then + if self.event then + self:emit({ type = self.event, element = self }) + elseif type(self.previousPage) == 'string' or + type(self.previousPage) == 'table' then + UI:setPage(self.previousPage) + else + UI:setPreviousPage() + end + return true + end + end +end diff --git a/sys/modules/opus/ui/components/VerticalMeter.lua b/sys/modules/opus/ui/components/VerticalMeter.lua new file mode 100644 index 0000000..012d0e5 --- /dev/null +++ b/sys/modules/opus/ui/components/VerticalMeter.lua @@ -0,0 +1,18 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +local colors = _G.colors + +UI.VerticalMeter = class(UI.Window) +UI.VerticalMeter.defaults = { + UIElement = 'VerticalMeter', + backgroundColor = colors.gray, + meterColor = colors.lime, + width = 1, + value = 0, +} +function UI.VerticalMeter:draw() + local height = self.height - math.ceil(self.value / 100 * self.height) + self:clear() + self:clearArea(1, height + 1, self.width, self.height, self.meterColor) +end diff --git a/sys/modules/opus/ui/components/Viewport.lua b/sys/modules/opus/ui/components/Viewport.lua new file mode 100644 index 0000000..acb84cd --- /dev/null +++ b/sys/modules/opus/ui/components/Viewport.lua @@ -0,0 +1,100 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +local colors = _G.colors + +--[[-- Viewport --]]-- +UI.Viewport = class(UI.Window) +UI.Viewport.defaults = { + UIElement = 'Viewport', + backgroundColor = colors.cyan, + accelerators = { + down = 'scroll_down', + up = 'scroll_up', + home = 'scroll_top', + [ 'end' ] = 'scroll_bottom', + pageUp = 'scroll_pageUp', + [ 'control-b' ] = 'scroll_pageUp', + pageDown = 'scroll_pageDown', + [ 'control-f' ] = 'scroll_pageDown', + }, +} +function UI.Viewport:layout() + UI.Window.layout(self) + if not self.canvas then + self.canvas = self:addLayer() + else + self.canvas:resize(self.width, self.height) + end +end + +function UI.Viewport:enable() + UI.Window.enable(self) + self.canvas:setVisible(true) +end + +function UI.Viewport:disable() + UI.Window.disable(self) + self.canvas:setVisible(false) +end + +function UI.Viewport:setScrollPosition(offset) + local oldOffset = self.offy + self.offy = math.max(offset, 0) + self.offy = math.min(self.offy, math.max(#self.canvas.lines, self.height) - self.height) + if self.offy ~= oldOffset then + if self.scrollBar then + self.scrollBar:draw() + end + self.canvas.offy = offset + self.canvas:dirty() + end +end + +function UI.Viewport:write(x, y, text, bg, tc) + if y > #self.canvas.lines then + for i = #self.canvas.lines, y do + self.canvas.lines[i + 1] = { } + self.canvas:clearLine(i + 1, self.backgroundColor, self.textColor) + end + end + return UI.Window.write(self, x, y, text, bg, tc) +end + +function UI.Viewport:reset() + self.offy = 0 + self.canvas.offy = 0 + for i = self.height + 1, #self.canvas.lines do + self.canvas.lines[i] = nil + end +end + +function UI.Viewport:getViewArea() + return { + y = (self.offy or 0) + 1, + height = self.height, + totalHeight = #self.canvas.lines, + offsetY = self.offy or 0, + } +end + +function UI.Viewport:eventHandler(event) + if event.type == 'scroll_down' then + self:setScrollPosition(self.offy + 1) + elseif event.type == 'scroll_up' then + self:setScrollPosition(self.offy - 1) + elseif event.type == 'scroll_top' then + self:setScrollPosition(0) + elseif event.type == 'scroll_bottom' then + self:setScrollPosition(10000000) + elseif event.type == 'scroll_pageUp' then + self:setScrollPosition(self.offy - self.height) + elseif event.type == 'scroll_pageDown' then + self:setScrollPosition(self.offy + self.height) + elseif event.type == 'scroll_to' then + self:setScrollPosition(event.offset) + else + return false + end + return true +end diff --git a/sys/modules/opus/ui/components/Wizard.lua b/sys/modules/opus/ui/components/Wizard.lua new file mode 100644 index 0000000..e6bce7e --- /dev/null +++ b/sys/modules/opus/ui/components/Wizard.lua @@ -0,0 +1,124 @@ +local class = require('opus.class') +local UI = require('opus.ui') +local Util = require('opus.util') + +UI.Wizard = class(UI.Window) +UI.Wizard.defaults = { + UIElement = 'Wizard', + pages = { }, +} +function UI.Wizard:postInit() + self.cancelButton = UI.Button { + x = 2, y = -1, + text = 'Cancel', + event = 'cancel', + } + self.previousButton = UI.Button { + x = -18, y = -1, + text = '< Back', + event = 'previousView', + } + self.nextButton = UI.Button { + x = -9, y = -1, + text = 'Next >', + event = 'nextView', + } + + Util.merge(self, self.pages) + --for _, child in pairs(self.pages) do + -- child.ey = -2 + --end +end + +function UI.Wizard:add(pages) + Util.merge(self.pages, pages) + Util.merge(self, pages) + + for _, child in pairs(self.pages) do + child.ey = child.ey or -2 + end + + if self.parent then + self:initChildren() + end +end + +function UI.Wizard:getPage(index) + return Util.find(self.pages, 'index', index) +end + +function UI.Wizard:enable(...) + self.enabled = true + self.index = 1 + self.transitionHint = nil + local initial = self:getPage(1) + for _,child in pairs(self.children) do + if child == initial or not child.index then + child:enable(...) + else + child:disable() + end + end + self:emit({ type = 'enable_view', next = initial }) +end + +function UI.Wizard:isViewValid() + local currentView = self:getPage(self.index) + return not currentView.validate and true or currentView:validate() +end + +function UI.Wizard:eventHandler(event) + if event.type == 'nextView' then + local currentView = self:getPage(self.index) + if self:isViewValid() then + self.index = self.index + 1 + local nextView = self:getPage(self.index) + currentView:emit({ type = 'enable_view', next = nextView, current = currentView }) + end + + elseif event.type == 'previousView' then + local currentView = self:getPage(self.index) + local nextView = self:getPage(self.index - 1) + if nextView then + self.index = self.index - 1 + currentView:emit({ type = 'enable_view', prev = nextView, current = currentView }) + end + return true + + elseif event.type == 'wizard_complete' then + if self:isViewValid() then + self:emit({ type = 'accept' }) + end + + elseif event.type == 'enable_view' then + local current = event.next or event.prev + if not current then error('property "index" is required on wizard pages') end + + if event.current then + if event.next then + self.transitionHint = 'slideLeft' + elseif event.prev then + self.transitionHint = 'slideRight' + end + event.current:disable() + end + + if self:getPage(self.index - 1) then + self.previousButton:enable() + else + self.previousButton:disable() + end + + if self:getPage(self.index + 1) then + self.nextButton.text = 'Next >' + self.nextButton.event = 'nextView' + else + self.nextButton.text = 'Accept' + self.nextButton.event = 'wizard_complete' + end + -- a new current view + current:enable() + current:emit({ type = 'view_enabled', view = current }) + self:draw() + end +end diff --git a/sys/modules/opus/ui/components/WizardPage.lua b/sys/modules/opus/ui/components/WizardPage.lua new file mode 100644 index 0000000..cb2c2de --- /dev/null +++ b/sys/modules/opus/ui/components/WizardPage.lua @@ -0,0 +1,11 @@ +local class = require('opus.class') +local UI = require('opus.ui') + +local colors = _G.colors + +UI.WizardPage = class(UI.ActiveLayer) +UI.WizardPage.defaults = { + UIElement = 'WizardPage', + backgroundColor = colors.cyan, + ey = -2, +} diff --git a/sys/apis/ui/region.lua b/sys/modules/opus/ui/region.lua similarity index 100% rename from sys/apis/ui/region.lua rename to sys/modules/opus/ui/region.lua diff --git a/sys/modules/opus/ui/transition.lua b/sys/modules/opus/ui/transition.lua new file mode 100644 index 0000000..4448760 --- /dev/null +++ b/sys/modules/opus/ui/transition.lua @@ -0,0 +1,53 @@ +local Tween = require('opus.ui.tween') + +local Transition = { } + +function Transition.slideLeft(args) + local ticks = args.ticks or 10 + local easing = args.easing or 'outQuint' + local pos = { x = args.ex } + local tween = Tween.new(ticks, pos, { x = args.x }, easing) + + args.canvas:move(pos.x, args.canvas.y) + + return function() + local finished = tween:update(1) + args.canvas:move(math.floor(pos.x), args.canvas.y) + args.canvas:dirty() + return not finished + end +end + +function Transition.slideRight(args) + local ticks = args.ticks or 10 + local easing = args.easing or'outQuint' + local pos = { x = -args.canvas.width } + local tween = Tween.new(ticks, pos, { x = 1 }, easing) + + args.canvas:move(pos.x, args.canvas.y) + + return function() + local finished = tween:update(1) + args.canvas:move(math.floor(pos.x), args.canvas.y) + args.canvas:dirty() + return not finished + end +end + +function Transition.expandUp(args) + local ticks = args.ticks or 3 + local easing = args.easing or 'linear' + local pos = { y = args.ey + 1 } + local tween = Tween.new(ticks, pos, { y = args.y }, easing) + + args.canvas:move(args.x, pos.y) + + return function() + local finished = tween:update(1) + args.canvas:move(args.x, math.floor(pos.y)) + args.canvas:dirty() + return not finished + end +end + +return Transition diff --git a/sys/apis/ui/tween.lua b/sys/modules/opus/ui/tween.lua similarity index 99% rename from sys/apis/ui/tween.lua rename to sys/modules/opus/ui/tween.lua index 7d0a13c..22601de 100644 --- a/sys/apis/ui/tween.lua +++ b/sys/modules/opus/ui/tween.lua @@ -7,7 +7,7 @@ local tween = { Copyright (c) 2014 Enrique GarcĂ­a Cota, Yuichi Tateno, Emmanuel Oga - Licence details: https://opensource.org/licenses/MIT + license details: https://opensource.org/licenses/MIT ]] } diff --git a/sys/apis/util.lua b/sys/modules/opus/util.lua similarity index 71% rename from sys/apis/util.lua rename to sys/modules/opus/util.lua index 40f3cb2..564b9f8 100644 --- a/sys/apis/util.lua +++ b/sys/modules/opus/util.lua @@ -6,6 +6,24 @@ local os = _G.os local term = _G.term local textutils = _G.textutils +local _sformat = string.format +local _srep = string.rep +local _ssub = string.sub + +function Util.hexToByteArray(str) + local r = {} + str = tostring(str) + for b in str:gmatch("%x%x?") do + r[#r+1] = tonumber(b, 16) + end + return r +end + +function Util.byteArrayToHex(tbl) + if not tbl then error('byteArrayToHex: invalid table', 2) end + return ("%02x"):rep(#tbl):format(table.unpack(tbl)) +end + function Util.tryTimed(timeout, f, ...) local c = os.clock() repeat @@ -21,15 +39,24 @@ function Util.tryTimes(attempts, f, ...) for _ = 1, attempts do result = { f(...) } if result[1] then - return unpack(result) + return table.unpack(result) end end - return unpack(result) + return table.unpack(result) end +function Util.timer() + local ct = os.clock() + return function() + return os.clock() - ct + end +end + +Util.Timer = Util.timer -- deprecate + function Util.throttle(fn) local ts = os.clock() - local timeout = .095 + local timeout = .295 return function(...) local nts = os.clock() if nts > ts + timeout then @@ -49,11 +76,11 @@ function Util.tostring(pattern, ...) for k, v in pairs(tbl) do local value if type(v) == 'table' then - value = string.format('table: %d', Util.size(v)) + value = _sformat('table: %d', Util.size(v)) else value = tostring(v) end - str = str .. string.format(' %s: %s\n', k, value) + str = str .. _sformat(' %s: %s\n', k, value) end --if #str < width then --str = str:gsub('\n', '') .. ' }' @@ -64,7 +91,10 @@ function Util.tostring(pattern, ...) end if type(pattern) == 'string' then - return string.format(pattern, ...) + if select('#', ...) == 0 then + return pattern + end + return _sformat(pattern, ...) elseif type(pattern) == 'table' then return serialize(pattern, term.current().getSize()) end @@ -109,15 +139,30 @@ function Util.checkMinecraftVersion(minVersion) return convert(version) >= convert(tostring(minVersion)) end +function Util.signum(num) + if num > 0 then + return 1 + elseif num < 0 then + return -1 + else + return 0 + end +end + +function Util.clamp(num, low, high) + return num < low and low or num > high and high or num +end + -- http://lua-users.org/wiki/SimpleRound function Util.round(num, idp) local mult = 10^(idp or 0) - return math.floor(num * mult + 0.5) / mult + return Util.signum(num) * math.floor(math.abs(num) * mult + 0.5) / mult end -function Util.random(max, min) +function Util.randomFloat(max, min) min = min or 0 - return math.random(0, max-min) + min + max = max or 1 + return (max-min) * math.random() + min end --[[ Table functions ]] -- @@ -172,20 +217,6 @@ function Util.deepMerge(obj, args) end end --- remove table entries if passed function returns false -function Util.prune(t, fn) - for _,k in pairs(Util.keys(t)) do - local v = t[k] - if type(v) == 'table' then - t[k] = Util.prune(v, fn) - end - if not fn(t[k]) then - t[k] = nil - end - end - return t -end - function Util.transpose(t) local tt = { } for k,v in pairs(t) do @@ -275,6 +306,14 @@ function Util.filter(it, f) return ot end +function Util.reduce(t, fn, acc) + acc = acc or 0 + for _, v in pairs(t) do + acc = fn(acc, v) + end + return acc +end + function Util.size(list) if type(list) == 'table' then local length = 0 @@ -286,15 +325,41 @@ function Util.size(list) return 0 end +local function isArray(value) + -- dubious + return type(value) == "table" and (value[1] or next(value) == nil) +end + function Util.removeByValue(t, e) for k,v in pairs(t) do if v == e then - table.remove(t, k) + if isArray(t) then + table.remove(t, k) + else + t[k] = nil + end break end end end +function Util.any(t, fn) + for _,v in pairs(t) do + if fn(v) then + return true + end + end +end + +function Util.every(t, fn) + for _,v in pairs(t) do + if not fn(v) then + return false + end + end + return true +end + function Util.each(list, func) for index, value in pairs(list) do func(value, index, list) @@ -347,8 +412,8 @@ function Util.first(t, order) end --[[ File functions ]]-- -function Util.readFile(fname) - local f = fs.open(fname, "r") +function Util.readFile(fname, flags) + local f = fs.open(fname, flags or "r") if f then local t = f.readAll() f.close() @@ -356,8 +421,26 @@ function Util.readFile(fname) end end +function Util.backup(fname) + local backup = fname .. '.bak' + if backup then + fs.delete(backup) + end + fs.copy(fname, backup) +end + function Util.writeFile(fname, data) if not fname or not data then error('Util.writeFile: invalid parameters', 2) end + + if fs.exists(fname) then + local diff = #data - fs.getSize(fname) + if diff > 0 then + if fs.getFreeSpace(fs.getDir(fname)) < diff then + error('Insufficient disk space for ' .. fname) + end + end + end + local file = io.open(fname, "w") if not file then error('Unable to open ' .. fname, 2) @@ -418,8 +501,8 @@ function Util.loadTable(fname) end --[[ loading and running functions ]] -- -function Util.httpGet(url, headers) - local h, msg = http.get(url, headers) +function Util.httpGet(url, headers, isBinary) + local h, msg = http.get(url, headers, isBinary) if h then local contents = h.readAll() h.close() @@ -431,7 +514,7 @@ end function Util.download(url, filename) local contents, msg = Util.httpGet(url) if not contents then - error(string.format('Failed to download %s\n%s', url, msg), 2) + error(_sformat('Failed to download %s\n%s', url, msg), 2) end if filename then @@ -477,11 +560,11 @@ end function Util.toBytes(n) if not tonumber(n) then error('Util.toBytes: n must be a number', 2) end if n >= 1000000 or n <= -1000000 then - return string.format('%sM', math.floor(n/1000000 * 10) / 10) + return _sformat('%sM', math.floor(n/1000000 * 10) / 10) elseif n >= 10000 or n <= -10000 then - return string.format('%sK', math.floor(n/1000)) + return _sformat('%sK', math.floor(n/1000)) elseif n >= 1000 or n <= -1000 then - return string.format('%sK', math.floor(n/1000 * 10) / 10) + return _sformat('%sK', math.floor(n/1000 * 10) / 10) end return tostring(n) end @@ -491,7 +574,7 @@ function Util.insertString(str, istr, pos) end function Util.split(str, pattern) - if not str then error('Util.split: Invalid parameters', 2) end + if not str or type(str) ~= 'string' then error('Util.split: Invalid parameters', 2) end pattern = pattern or "(.-)\n" local t = {} local function helper(line) table.insert(t, line) return "" end @@ -509,33 +592,46 @@ function Util.matches(str, pattern) end function Util.startsWith(s, match) - return string.sub(s, 1, #match) == match + return _ssub(s, 1, #match) == match end -function Util.widthify(s, len) +-- return a fixed length string using specified alignment +function Util.widthify(s, len, align) s = s or '' local slen = #s - if slen < len then - s = s .. string.rep(' ', len - #s) - elseif slen > len then - s = s:sub(1, len) + + if slen > len then + return _ssub(s, 1, len) + + elseif slen == len then + return s + + elseif align == 'center' then + local space = math.floor((len - slen) / 2) + s = _srep(' ', space) .. s + return s .. _srep(' ', len - #s) + + elseif align == 'right' then + return _srep(' ', len - slen) .. s + end - return s + + return s .. _srep(' ', len - slen) end -- http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76 function Util.trim(s) - return s:find'^%s*$' and '' or s:match'^%s*(.*%S)' + return s:find('^%s*$') and '' or s:match('^%s*(.*%S)') end -- trim whitespace from left end of string function Util.triml(s) - return s:match'^%s*(.*)' + return s:match('^%s*(.*)') end -- trim whitespace from right end of string function Util.trimr(s) - return s:find'^%s*$' and '' or s:match'^(.*%S)' + return s:find('^%s*$') and '' or s:match('^(.*%S)') end -- end http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76 @@ -580,14 +676,45 @@ function Util.wordWrap(str, limit) return lines end +-- https://github.com/MightyPirates/OpenComputers +function Util.parse(...) + local params = table.pack(...) + local args = {} + local options = {} + local doneWithOptions = false + for i = 1, params.n do + local param = params[i] + if not doneWithOptions and type(param) == "string" then + if param == "--" then + doneWithOptions = true -- stop processing options at `--` + elseif param:sub(1, 2) == "--" then + local key, value = param:match("%-%-(.-)=(.*)") + if not key then + key, value = param:sub(3), true + end + options[key] = value + elseif param:sub(1, 1) == "-" and param ~= "-" then + for j = 2, string.len(param) do + options[string.sub(param, j, j)] = true + end + else + table.insert(args, param) + end + else + table.insert(args, param) + end + end + return args, options +end + function Util.args(arg) local options, args = { }, { } local k = 1 while k <= #arg do local v = arg[k] - if string.sub(v, 1, 1) == '-' then - local opt = string.sub(v, 2) + if _ssub(v, 1, 1) == '-' then + local opt = _ssub(v, 2) options[opt] = arg[k + 1] k = k + 1 else @@ -603,20 +730,20 @@ local function getopt( arg, options ) local tab = {} for k, v in ipairs(arg) do if type(v) == 'string' then - if string.sub( v, 1, 2) == "--" then + if _ssub( v, 1, 2) == "--" then local x = string.find( v, "=", 1, true ) - if x then tab[ string.sub( v, 3, x-1 ) ] = string.sub( v, x+1 ) - else tab[ string.sub( v, 3 ) ] = true + if x then tab[ _ssub( v, 3, x-1 ) ] = _ssub( v, x+1 ) + else tab[ _ssub( v, 3 ) ] = true end - elseif string.sub( v, 1, 1 ) == "-" then + elseif _ssub( v, 1, 1 ) == "-" then local y = 2 local l = string.len(v) local jopt while ( y <= l ) do - jopt = string.sub( v, y, y ) + jopt = _ssub( v, y, y ) if string.find( options, jopt, 1, true ) then if y < l then - tab[ jopt ] = string.sub( v, y+1 ) + tab[ jopt ] = _ssub( v, y+1 ) y = l else tab[ jopt ] = arg[ k + 1 ] @@ -635,7 +762,7 @@ end function Util.showOptions(options) print('Arguments: ') for _, v in pairs(options) do - print(string.format('-%s %s', v.arg, v.desc)) + print(_sformat('-%s %s', v.arg, v.desc)) end end @@ -672,4 +799,25 @@ function Util.getOptions(options, args, ignoreInvalid) return true, Util.size(rawOptions) end +-- https://www.lua.org/pil/9.3.html +function Util.permutation(tbl) + local function permgen(a, n) + if n == 0 then + coroutine.yield(a) + else + for i=1,n do + a[n], a[i] = a[i], a[n] + permgen(a, n - 1) + a[n], a[i] = a[i], a[n] + end + end + end + + local co = coroutine.create(function() permgen(tbl, #tbl) end) + return function() + local _, res = coroutine.resume(co) + return res + end +end + return Util diff --git a/sys/network/peripheral.lua b/sys/network/peripheral.lua deleted file mode 100644 index 89e2b86..0000000 --- a/sys/network/peripheral.lua +++ /dev/null @@ -1,86 +0,0 @@ ---[[ - Allow sharing of local peripherals. -]]-- - -local Event = require('event') -local Peripheral = require('peripheral') -local Socket = require('socket') - -Event.addRoutine(function() - print('peripheral: listening on port 189') - while true do - local socket = Socket.server(189) - - print('peripheral: connection from ' .. socket.dhost) - - Event.addRoutine(function() - local uri = socket:read(2) - if uri then - local peripheral = Peripheral.lookup(uri) - --- only 1 proxy of this device can happen at one time --- need to prevent multiple shares - if not peripheral then - print('peripheral: invalid peripheral ' .. uri) - socket:write('Invalid peripheral: ' .. uri) - else - print('peripheral: proxing ' .. uri) - local proxy = { - methods = { } - } - - if peripheral.blit then - --peripheral = Util.shallowCopy(peripheral) - peripheral.fastBlit = function(data) - for _,v in ipairs(data) do - peripheral[v.fn](unpack(v.args)) - end - end - end - - for k,v in pairs(peripheral) do - if type(v) == 'function' then - table.insert(proxy.methods, k) - else - proxy[k] = v - end - end - - socket:write(proxy) - - if proxy.type == 'monitor' then - peripheral.eventChannel = function(...) - socket:write({ - fn = 'event', - data = { ... } - }) - end - end - - while true do - local data = socket:read() - if not data then - print('peripheral: lost connection from ' .. socket.dhost) - break - end - if not _G.device[peripheral.name] then - print('periperal: detached') - socket:close() - break - end - if peripheral[data.fn] then - -- need to trigger an error on the other end - -- local s, m = pcall() - socket:write({ peripheral[data.fn](table.unpack(data.args)) }) - else - socket:write({ false, "Invalid function: " .. data.fn }) - end - end - - peripheral.eventChannel = nil - peripheral.fastBlit = nil - end - end - end) - end -end) diff --git a/sys/network/proxy.lua b/sys/network/proxy.lua deleted file mode 100644 index 46ec14b..0000000 --- a/sys/network/proxy.lua +++ /dev/null @@ -1,40 +0,0 @@ -local Event = require('event') -local Socket = require('socket') - -Event.addRoutine(function() - while true do - print('proxy: listening on port 188') - local socket = Socket.server(188) - - print('proxy: connection from ' .. socket.dhost) - - Event.addRoutine(function() - local api = socket:read(2) - if api then - local proxy = _G[api] - - if not proxy then - print('proxy: invalid API') - return - end - - local methods = { } - for k,v in pairs(proxy) do - if type(v) == 'function' then - table.insert(methods, k) - end - end - socket:write(methods) - - while true do - local data = socket:read() - if not data then - print('proxy: lost connection from ' .. socket.dhost) - break - end - socket:write({ proxy[data.fn](table.unpack(data.args)) }) - end - end - end) - end -end) diff --git a/sys/network/redserver.lua b/sys/network/redserver.lua deleted file mode 100644 index fda019c..0000000 --- a/sys/network/redserver.lua +++ /dev/null @@ -1,113 +0,0 @@ -local Event = require('event') -local Util = require('util') - -local fs = _G.fs -local modem = _G.device.wireless_modem -local os = _G.os - -local computerId = os.getComputerID() - -modem.open(80) - --- https://github.com/golgote/neturl/blob/master/lib/net/url.lua -local function parseQuery(str) - local sep = '&' - - local values = {} - for key,val in str:gmatch(string.format('([^%q=]+)(=*[^%q=]*)', sep, sep)) do - --local key = decode(key) - local keys = {} - key = key:gsub('%[([^%]]*)%]', function(v) - -- extract keys between balanced brackets - if string.find(v, "^-?%d+$") then - v = tonumber(v) - --else - --v = decode(v) - end - table.insert(keys, v) - return "=" - end) - key = key:gsub('=+.*$', "") - key = key:gsub('%s', "_") -- remove spaces in parameter name - val = val:gsub('^=+', "") - - if not values[key] then - values[key] = {} - end - if #keys > 0 and type(values[key]) ~= 'table' then - values[key] = {} - elseif #keys == 0 and type(values[key]) == 'table' then - values[key] = val --decode(val) - end - - local t = values[key] - for i,k in ipairs(keys) do - if type(t) ~= 'table' then - t = {} - end - if k == "" then - k = #t+1 - end - if not t[k] then - t[k] = {} - end - if i == #keys then - t[k] = val --decode(val) - end - t = t[k] - end - end - return values -end - -local function getListing(path, recursive) - local list = { } - local function listing(p) - for _, f in pairs(fs.listEx(p)) do - local abs = fs.combine(p, f.name) - table.insert(list, { - isDir = f.isDir, - path = string.sub(abs, #path + 1), - size = f.size, - }) - if recursive and f.isDir then - listing(abs) - end - end - end - listing(path) - return list -end - -Event.on('modem_message', function(_, _, dport, dhost, request) - if dport == 80 and dhost == computerId and type(request) == 'table' then - if request.method == 'GET' then - local query - if not request.path or type(request.path) ~= 'string' then - return - end - local path = request.path:gsub('%?(.*)', function(v) - query = parseQuery(v) - return '' - end) - if fs.isDir(path) then - -- TODO: more validation - modem.transmit(request.replyPort, request.replyAddress, { - statusCode = 200, - contentType = 'table/directory', - data = getListing(path, query and query.recursive == 'true'), - }) - elseif fs.exists(path) then - modem.transmit(request.replyPort, request.replyAddress, { - statusCode = 200, - contentType = 'table/file', - data = Util.readFile(path), - }) - else - modem.transmit(request.replyPort, request.replyAddress, { - statusCode = 404, - }) - end - end - end -end) diff --git a/sys/network/trust.lua b/sys/network/trust.lua deleted file mode 100644 index 03ed4e8..0000000 --- a/sys/network/trust.lua +++ /dev/null @@ -1,35 +0,0 @@ -local Crypto = require('crypto') -local Event = require('event') -local Security = require('security') -local Socket = require('socket') -local Util = require('util') - -Event.addRoutine(function() - - print('trust: listening on port 19') - while true do - local socket = Socket.server(19) - - print('trust: connection from ' .. socket.dhost) - - local data = socket:read(2) - if data then - local password = Security.getPassword() - if not password then - socket:write({ msg = 'No password has been set' }) - else - data = Crypto.decrypt(data, password) - if data and data.pk and data.dh == socket.dhost then - local trustList = Util.readTable('usr/.known_hosts') or { } - trustList[data.dh] = data.pk - Util.writeTable('usr/.known_hosts', trustList) - - socket:write({ success = true, msg = 'Trust accepted' }) - else - socket:write({ msg = 'Invalid password' }) - end - end - end - socket:close() - end -end)