Merge branch 'develop-1.8' into master-1.8
This commit is contained in:
48
sys/modules/opus/alternate.lua
Normal file
48
sys/modules/opus/alternate.lua
Normal file
@@ -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
|
||||
55
sys/modules/opus/ansi.lua
Normal file
55
sys/modules/opus/ansi.lua
Normal file
@@ -0,0 +1,55 @@
|
||||
local Ansi = setmetatable({ }, {
|
||||
__call = function(_, ...)
|
||||
local str = '\027['
|
||||
for k,v in ipairs({ ...}) do
|
||||
if k == 1 then
|
||||
str = str .. v
|
||||
else
|
||||
str = str .. ';' .. v
|
||||
end
|
||||
end
|
||||
return str .. 'm'
|
||||
end
|
||||
})
|
||||
|
||||
Ansi.codes = {
|
||||
reset = 0,
|
||||
white = 1,
|
||||
orange = 2,
|
||||
magenta = 3,
|
||||
lightBlue = 4,
|
||||
yellow = 5,
|
||||
lime = 6,
|
||||
pink = 7,
|
||||
gray = 8,
|
||||
lightGray = 9,
|
||||
cyan = 10,
|
||||
purple = 11,
|
||||
blue = 12,
|
||||
brown = 13,
|
||||
green = 14,
|
||||
red = 15,
|
||||
black = 16,
|
||||
onwhite = 21,
|
||||
onorange = 22,
|
||||
onmagenta = 23,
|
||||
onlightBlue = 24,
|
||||
onyellow = 25,
|
||||
onlime = 26,
|
||||
onpink = 27,
|
||||
ongray = 28,
|
||||
onlightGray = 29,
|
||||
oncyan = 30,
|
||||
onpurple = 31,
|
||||
onblue = 32,
|
||||
onbrown = 33,
|
||||
ongreen = 34,
|
||||
onred = 35,
|
||||
onblack = 36,
|
||||
}
|
||||
|
||||
for k,v in pairs(Ansi.codes) do
|
||||
Ansi[k] = Ansi(v)
|
||||
end
|
||||
|
||||
return Ansi
|
||||
22
sys/modules/opus/array.lua
Normal file
22
sys/modules/opus/array.lua
Normal file
@@ -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
|
||||
33
sys/modules/opus/bulkget.lua
Normal file
33
sys/modules/opus/bulkget.lua
Normal file
@@ -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
|
||||
581
sys/modules/opus/cbor.lua
Normal file
581
sys/modules/opus/cbor.lua
Normal file
@@ -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;
|
||||
}
|
||||
49
sys/modules/opus/class.lua
Normal file
49
sys/modules/opus/class.lua
Normal file
@@ -0,0 +1,49 @@
|
||||
-- From http://lua-users.org/wiki/SimpleLuaClasses
|
||||
-- (with some modifications)
|
||||
|
||||
-- class.lua
|
||||
-- Compatible with Lua 5.1 (not 5.0).
|
||||
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
|
||||
c._base = base
|
||||
end
|
||||
-- the class will be the metatable for all its objects,
|
||||
-- and they will look up their methods in it.
|
||||
c.__index = c
|
||||
|
||||
-- expose a constructor which can be called by <classname>(<args>)
|
||||
setmetatable(c, {
|
||||
__call = function(class_tbl, ...)
|
||||
local obj = { }
|
||||
setmetatable(obj,c)
|
||||
if class_tbl.init then
|
||||
class_tbl.init(obj, ...)
|
||||
else
|
||||
-- make sure that any stuff from the base class is initialized!
|
||||
if base and base.init then
|
||||
base.init(obj, ...)
|
||||
end
|
||||
end
|
||||
return obj
|
||||
end
|
||||
})
|
||||
|
||||
c.is_a =
|
||||
function(self, klass)
|
||||
local m = getmetatable(self)
|
||||
while m do
|
||||
if m == klass then return true end
|
||||
m = m._base
|
||||
end
|
||||
return false
|
||||
end
|
||||
return c
|
||||
end
|
||||
50
sys/modules/opus/config.lua
Normal file
50
sys/modules/opus/config.lua
Normal file
@@ -0,0 +1,50 @@
|
||||
local Util = require('opus.util')
|
||||
|
||||
local fs = _G.fs
|
||||
local shell = _ENV.shell
|
||||
|
||||
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')
|
||||
end
|
||||
|
||||
if not fs.exists(filename) then
|
||||
Util.writeTable(filename, data)
|
||||
else
|
||||
local contents = Util.readTable(filename) or
|
||||
error('Configuration file is corrupt:' .. filename)
|
||||
|
||||
Util.merge(data, contents)
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
function Config.loadWithCheck(fname, data)
|
||||
local filename = 'usr/config/' .. fname
|
||||
|
||||
if not fs.exists(filename) then
|
||||
Config.load(fname, data)
|
||||
print()
|
||||
print('The configuration file has been created.')
|
||||
print('The file name is: ' .. filename)
|
||||
print()
|
||||
_G.printError('Press enter to configure')
|
||||
_G.read()
|
||||
shell.run('edit ' .. filename)
|
||||
end
|
||||
|
||||
return Config.load(fname, data)
|
||||
end
|
||||
|
||||
function Config.update(fname, data)
|
||||
local filename = 'usr/config/' .. fname
|
||||
Util.writeTable(filename, data)
|
||||
end
|
||||
|
||||
return Config
|
||||
201
sys/modules/opus/crypto/chacha20.lua
Normal file
201
sys/modules/opus/crypto/chacha20.lua
Normal file
@@ -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,
|
||||
}
|
||||
306
sys/modules/opus/crypto/ecc/elliptic.lua
Normal file
306
sys/modules/opus/crypto/ecc/elliptic.lua
Normal file
@@ -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,
|
||||
}
|
||||
930
sys/modules/opus/crypto/ecc/fp.lua
Normal file
930
sys/modules/opus/crypto/ecc/fp.lua
Normal file
@@ -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,
|
||||
}
|
||||
743
sys/modules/opus/crypto/ecc/fq.lua
Normal file
743
sys/modules/opus/crypto/ecc/fq.lua
Normal file
@@ -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,
|
||||
}
|
||||
88
sys/modules/opus/crypto/ecc/init.lua
Normal file
88
sys/modules/opus/crypto/ecc/init.lua
Normal file
@@ -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,
|
||||
}
|
||||
208
sys/modules/opus/crypto/sha2.lua
Normal file
208
sys/modules/opus/crypto/sha2.lua
Normal file
@@ -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,
|
||||
}
|
||||
388
sys/modules/opus/entry.lua
Normal file
388
sys/modules/opus/entry.lua
Normal file
@@ -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
|
||||
263
sys/modules/opus/event.lua
Normal file
263
sys/modules/opus/event.lua
Normal file
@@ -0,0 +1,263 @@
|
||||
local os = _G.os
|
||||
local table = _G.table
|
||||
|
||||
local Event = {
|
||||
uid = 1, -- unique id for handlers
|
||||
routines = { }, -- coroutines
|
||||
types = { }, -- event handlers
|
||||
terminate = false,
|
||||
free = { }, -- allocated unused coroutines
|
||||
}
|
||||
|
||||
-- Use a pool of coroutines for event handlers
|
||||
local function createCoroutine(h)
|
||||
local co = table.remove(Event.free)
|
||||
if not co then
|
||||
co = coroutine.create(function(_, ...)
|
||||
local args = { ... }
|
||||
while true do
|
||||
h.fn(table.unpack(args))
|
||||
h.co = nil
|
||||
table.insert(Event.free, co)
|
||||
args = { coroutine.yield() }
|
||||
h = table.remove(args, 1)
|
||||
h.co = co
|
||||
end
|
||||
end)
|
||||
end
|
||||
h.primeCo = true -- TODO: fix...
|
||||
return co
|
||||
end
|
||||
|
||||
local Routine = { }
|
||||
|
||||
function Routine:isDead()
|
||||
if not self.co then
|
||||
return true
|
||||
end
|
||||
return coroutine.status(self.co) == 'dead'
|
||||
end
|
||||
|
||||
function Routine:terminate()
|
||||
if self.co then
|
||||
self:resume('terminate')
|
||||
end
|
||||
end
|
||||
|
||||
function Routine:resume(event, ...)
|
||||
if not self.co then
|
||||
error('Cannot resume a dead routine')
|
||||
end
|
||||
|
||||
if not self.filter or self.filter == event or event == "terminate" then
|
||||
local s, m
|
||||
if self.primeCo then
|
||||
-- Only need self passed when using a coroutine from the pool
|
||||
s, m = coroutine.resume(self.co, self, event, ...)
|
||||
self.primeCo = nil
|
||||
else
|
||||
s, m = coroutine.resume(self.co, event, ...)
|
||||
end
|
||||
if self:isDead() then
|
||||
self.co = nil
|
||||
self.filter = nil
|
||||
Event.routines[self.uid] = nil
|
||||
else
|
||||
self.filter = m
|
||||
end
|
||||
|
||||
if not s and event ~= 'terminate' then
|
||||
error(m or 'Error processing event', -1)
|
||||
end
|
||||
|
||||
return s, m
|
||||
end
|
||||
|
||||
return true, self.filter
|
||||
end
|
||||
|
||||
local function nextUID()
|
||||
Event.uid = Event.uid + 1
|
||||
return Event.uid - 1
|
||||
end
|
||||
|
||||
function Event.on(events, fn)
|
||||
events = type(events) == 'table' and events or { events }
|
||||
|
||||
local handler = setmetatable({
|
||||
uid = nextUID(),
|
||||
event = events,
|
||||
fn = fn,
|
||||
}, { __index = Routine })
|
||||
|
||||
for _,event in pairs(events) do
|
||||
local handlers = Event.types[event]
|
||||
if not handlers then
|
||||
handlers = { }
|
||||
Event.types[event] = handlers
|
||||
end
|
||||
|
||||
handlers[handler.uid] = handler
|
||||
end
|
||||
|
||||
return handler
|
||||
end
|
||||
|
||||
function Event.off(h)
|
||||
if h and h.event then
|
||||
for _,event in pairs(h.event) do
|
||||
local handler = Event.types[event][h.uid]
|
||||
if handler then
|
||||
handler:terminate()
|
||||
end
|
||||
Event.types[event][h.uid] = nil
|
||||
end
|
||||
elseif h and h.co then
|
||||
h:terminate()
|
||||
end
|
||||
end
|
||||
|
||||
function Event.onInterval(interval, fn)
|
||||
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)
|
||||
local timerId = os.startTimer(timeout)
|
||||
local handler
|
||||
|
||||
handler = Event.on('timer', function(t, id)
|
||||
if timerId == id then
|
||||
fn(t, id)
|
||||
Event.off(handler)
|
||||
end
|
||||
end)
|
||||
|
||||
return handler
|
||||
end
|
||||
|
||||
-- Set a handler for the terminate event. Within the function, return
|
||||
-- true or false to indicate whether the event should be propagated to
|
||||
-- all sub-threads
|
||||
function Event.onTerminate(fn)
|
||||
Event.termFn = fn
|
||||
end
|
||||
|
||||
function Event.termFn()
|
||||
Event.terminate = true
|
||||
return true -- propagate
|
||||
end
|
||||
|
||||
function Event.addRoutine(fn)
|
||||
local r = setmetatable({
|
||||
co = coroutine.create(fn),
|
||||
uid = nextUID()
|
||||
}, { __index = Routine })
|
||||
|
||||
Event.routines[r.uid] = r
|
||||
r:resume()
|
||||
|
||||
return r
|
||||
end
|
||||
|
||||
function Event.pullEvents(...)
|
||||
for _, fn in ipairs({ ... }) do
|
||||
Event.addRoutine(fn)
|
||||
end
|
||||
|
||||
repeat
|
||||
Event.pullEvent()
|
||||
until Event.terminate
|
||||
|
||||
Event.terminate = false
|
||||
end
|
||||
|
||||
function Event.exitPullEvents()
|
||||
Event.terminate = true
|
||||
os.sleep(0)
|
||||
end
|
||||
|
||||
local function processHandlers(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
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function tokeys(t)
|
||||
local keys = { }
|
||||
for k in pairs(t) do
|
||||
keys[#keys+1] = k
|
||||
end
|
||||
return keys
|
||||
end
|
||||
|
||||
local function processRoutines(...)
|
||||
local keys = tokeys(Event.routines)
|
||||
for _,key in ipairs(keys) do
|
||||
local r = Event.routines[key]
|
||||
if r then
|
||||
r:resume(...)
|
||||
end
|
||||
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))
|
||||
end
|
||||
|
||||
function Event.pullEvent(eventType)
|
||||
while true do
|
||||
local e = { os.pullEventRaw() }
|
||||
local propagate = true -- don't like this...
|
||||
|
||||
if e[1] == 'terminate' then
|
||||
propagate = Event.termFn()
|
||||
end
|
||||
|
||||
if propagate then
|
||||
processHandlers(e[1])
|
||||
processRoutines(table.unpack(e))
|
||||
end
|
||||
|
||||
if Event.terminate then
|
||||
return { 'terminate' }
|
||||
end
|
||||
|
||||
if not eventType or e[1] == eventType then
|
||||
return e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return Event
|
||||
21
sys/modules/opus/fs/gitfs.lua
Normal file
21
sys/modules/opus/fs/gitfs.lua
Normal file
@@ -0,0 +1,21 @@
|
||||
local git = require('opus.git')
|
||||
|
||||
local fs = _G.fs
|
||||
|
||||
local gitfs = { }
|
||||
|
||||
function gitfs.mount(dir, repo)
|
||||
if not repo then
|
||||
error('gitfs syntax: repo')
|
||||
end
|
||||
|
||||
local list = git.list(repo)
|
||||
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 gitfs
|
||||
68
sys/modules/opus/fs/linkfs.lua
Normal file
68
sys/modules/opus/fs/linkfs.lua
Normal file
@@ -0,0 +1,68 @@
|
||||
local fs = _G.fs
|
||||
|
||||
local linkfs = { }
|
||||
|
||||
-- TODO: implement broken links
|
||||
|
||||
local methods = { 'exists', 'getFreeSpace', 'getSize',
|
||||
'isDir', 'isReadOnly', 'list', 'listEx', 'makeDir', 'open', 'getDrive' }
|
||||
|
||||
for _,m in pairs(methods) do
|
||||
linkfs[m] = function(node, dir, ...)
|
||||
dir = dir:gsub(node.mountPoint, node.source, 1)
|
||||
return fs[m](dir, ...)
|
||||
end
|
||||
end
|
||||
|
||||
function linkfs.mount(_, source)
|
||||
if not source then
|
||||
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,
|
||||
nodes = { },
|
||||
}
|
||||
end
|
||||
return {
|
||||
source = source
|
||||
}
|
||||
end
|
||||
|
||||
function linkfs.copy(node, s, t)
|
||||
s = s:gsub(node.mountPoint, node.source, 1)
|
||||
t = t:gsub(node.mountPoint, node.source, 1)
|
||||
return fs.copy(s, t)
|
||||
end
|
||||
|
||||
function linkfs.delete(node, dir)
|
||||
if dir == node.mountPoint then
|
||||
fs.unmount(node.mountPoint)
|
||||
else
|
||||
dir = dir:gsub(node.mountPoint, node.source, 1)
|
||||
return fs.delete(dir)
|
||||
end
|
||||
end
|
||||
|
||||
function linkfs.find(node, spec)
|
||||
spec = spec:gsub(node.mountPoint, node.source, 1)
|
||||
|
||||
local list = fs.find(spec)
|
||||
for k,f in ipairs(list) do
|
||||
list[k] = f:gsub(node.source, node.mountPoint, 1)
|
||||
end
|
||||
|
||||
return list
|
||||
end
|
||||
|
||||
function linkfs.move(node, s, t)
|
||||
s = s:gsub(node.mountPoint, node.source, 1)
|
||||
t = t:gsub(node.mountPoint, node.source, 1)
|
||||
return fs.move(s, t)
|
||||
end
|
||||
|
||||
return linkfs
|
||||
166
sys/modules/opus/fs/netfs.lua
Normal file
166
sys/modules/opus/fs/netfs.lua
Normal file
@@ -0,0 +1,166 @@
|
||||
local Socket = require('opus.socket')
|
||||
local synchronized = require('opus.sync').sync
|
||||
|
||||
local fs = _G.fs
|
||||
|
||||
local netfs = { }
|
||||
|
||||
local function remoteCommand(node, msg)
|
||||
|
||||
for _ = 1, 2 do
|
||||
if not node.socket then
|
||||
node.socket = Socket.connect(node.id, 139)
|
||||
end
|
||||
|
||||
if not node.socket then
|
||||
error('netfs: Unable to establish connection to ' .. node.id)
|
||||
fs.unmount(node.mountPoint)
|
||||
return
|
||||
end
|
||||
|
||||
local ret
|
||||
synchronized(node.socket, function()
|
||||
node.socket:write(msg)
|
||||
ret = node.socket:read(1)
|
||||
end)
|
||||
|
||||
if ret then
|
||||
return ret.response
|
||||
end
|
||||
node.socket:close()
|
||||
node.socket = nil
|
||||
end
|
||||
error('netfs: Connection failed', 2)
|
||||
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
|
||||
|
||||
for _,m in pairs(methods) do
|
||||
netfs[m] = function(node, dir)
|
||||
dir = resolveDir(dir, node)
|
||||
|
||||
return remoteCommand(node, {
|
||||
fn = m,
|
||||
args = { dir },
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function netfs.mount(_, id, directory)
|
||||
if not id or not tonumber(id) then
|
||||
error('ramfs syntax: computerId [directory]')
|
||||
end
|
||||
return {
|
||||
id = tonumber(id),
|
||||
nodes = { },
|
||||
directory = directory or '',
|
||||
}
|
||||
end
|
||||
|
||||
function netfs.getDrive()
|
||||
return 'net'
|
||||
end
|
||||
|
||||
function netfs.complete(node, partial, dir, includeFiles, includeSlash)
|
||||
dir = resolveDir(dir, node)
|
||||
|
||||
return remoteCommand(node, {
|
||||
fn = 'complete',
|
||||
args = { partial, dir, includeFiles, includeSlash },
|
||||
})
|
||||
end
|
||||
|
||||
function netfs.copy(node, s, t)
|
||||
s = resolveDir(s, node)
|
||||
t = resolveDir(t, node)
|
||||
|
||||
return remoteCommand(node, {
|
||||
fn = 'copy',
|
||||
args = { s, t },
|
||||
})
|
||||
end
|
||||
|
||||
function netfs.isDir(node, dir)
|
||||
if dir == node.mountPoint and node.directory == '' then
|
||||
return true
|
||||
end
|
||||
return remoteCommand(node, {
|
||||
fn = 'isDir',
|
||||
args = { resolveDir(dir, node) },
|
||||
})
|
||||
end
|
||||
|
||||
function netfs.isReadOnly(node, dir)
|
||||
if dir == node.mountPoint and node.directory == '' then
|
||||
return false
|
||||
end
|
||||
return remoteCommand(node, {
|
||||
fn = 'isReadOnly',
|
||||
args = { resolveDir(dir, node) },
|
||||
})
|
||||
end
|
||||
|
||||
function netfs.getSize(node, dir)
|
||||
if dir == node.mountPoint and node.directory == '' then
|
||||
return 0
|
||||
end
|
||||
return remoteCommand(node, {
|
||||
fn = 'getSize',
|
||||
args = { resolveDir(dir, node) },
|
||||
})
|
||||
end
|
||||
|
||||
function netfs.find(node, spec)
|
||||
spec = resolveDir(spec, node)
|
||||
local list = remoteCommand(node, {
|
||||
fn = 'find',
|
||||
args = { spec },
|
||||
})
|
||||
|
||||
for k,f in ipairs(list) do
|
||||
list[k] = fs.combine(node.mountPoint, f)
|
||||
end
|
||||
|
||||
return list
|
||||
end
|
||||
|
||||
function netfs.move(node, s, t)
|
||||
s = resolveDir(s, node)
|
||||
t = resolveDir(t, node)
|
||||
|
||||
return remoteCommand(node, {
|
||||
fn = 'move',
|
||||
args = { s, t },
|
||||
})
|
||||
end
|
||||
|
||||
function netfs.open(node, fn, fl)
|
||||
fn = resolveDir(fn, node)
|
||||
|
||||
local vfh = remoteCommand(node, {
|
||||
fn = 'open',
|
||||
args = { fn, fl },
|
||||
})
|
||||
|
||||
if vfh then
|
||||
vfh.node = node
|
||||
for _,m in ipairs(vfh.methods) do
|
||||
vfh[m] = function(...)
|
||||
return remoteCommand(node, {
|
||||
fn = 'fileOp',
|
||||
args = { vfh.fileUid, m, ... },
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return vfh
|
||||
end
|
||||
|
||||
return netfs
|
||||
151
sys/modules/opus/fs/ramfs.lua
Normal file
151
sys/modules/opus/fs/ramfs.lua
Normal file
@@ -0,0 +1,151 @@
|
||||
local Util = require('opus.util')
|
||||
|
||||
local fs = _G.fs
|
||||
|
||||
local ramfs = { }
|
||||
|
||||
function ramfs.mount(_, nodeType)
|
||||
if nodeType == 'directory' then
|
||||
return {
|
||||
nodes = { },
|
||||
size = 0,
|
||||
}
|
||||
elseif nodeType == 'file' then
|
||||
return {
|
||||
size = 0,
|
||||
}
|
||||
end
|
||||
error('ramfs syntax: [directory, file]')
|
||||
end
|
||||
|
||||
function ramfs.delete(node, dir)
|
||||
if node.mountPoint == dir then
|
||||
fs.unmount(node.mountPoint)
|
||||
end
|
||||
end
|
||||
|
||||
function ramfs.exists(node, fn)
|
||||
return node.mountPoint == fn
|
||||
end
|
||||
|
||||
function ramfs.getSize(node)
|
||||
return node.size
|
||||
end
|
||||
|
||||
function ramfs.isReadOnly()
|
||||
return false
|
||||
end
|
||||
|
||||
function ramfs.makeDir(_, dir)
|
||||
fs.mount(dir, 'ramfs', 'directory')
|
||||
end
|
||||
|
||||
function ramfs.isDir(node)
|
||||
return not not node.nodes
|
||||
end
|
||||
|
||||
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 = { }
|
||||
for k in pairs(node.nodes) do
|
||||
table.insert(files, k)
|
||||
end
|
||||
return files
|
||||
end
|
||||
error('Not a directory')
|
||||
end
|
||||
|
||||
function ramfs.open(node, fn, fl)
|
||||
|
||||
if fl ~= 'r' and fl ~= 'w' and fl ~= 'rb' and fl ~= 'wb' then
|
||||
error('Unsupported mode')
|
||||
end
|
||||
|
||||
if fl == 'r' then
|
||||
if node.mountPoint ~= fn then
|
||||
return
|
||||
end
|
||||
|
||||
local ctr = 0
|
||||
local lines
|
||||
return {
|
||||
readLine = function()
|
||||
if not lines then
|
||||
lines = Util.split(node.contents)
|
||||
end
|
||||
ctr = ctr + 1
|
||||
return lines[ctr]
|
||||
end,
|
||||
readAll = function()
|
||||
return node.contents
|
||||
end,
|
||||
close = function()
|
||||
lines = nil
|
||||
end,
|
||||
}
|
||||
elseif fl == 'w' then
|
||||
node = fs.mount(fn, 'ramfs', 'file')
|
||||
|
||||
local c = ''
|
||||
return {
|
||||
write = function(str)
|
||||
c = c .. str
|
||||
end,
|
||||
writeLine = function(str)
|
||||
c = c .. str .. '\n'
|
||||
end,
|
||||
flush = function()
|
||||
node.contents = c
|
||||
node.size = #c
|
||||
end,
|
||||
close = function()
|
||||
node.contents = c
|
||||
node.size = #c
|
||||
c = nil
|
||||
end,
|
||||
}
|
||||
elseif fl == 'rb' then
|
||||
if node.mountPoint ~= fn or not node.contents then
|
||||
return
|
||||
end
|
||||
|
||||
local ctr = 0
|
||||
return {
|
||||
read = function()
|
||||
ctr = ctr + 1
|
||||
return node.contents[ctr]
|
||||
end,
|
||||
close = function()
|
||||
end,
|
||||
}
|
||||
|
||||
elseif fl == 'wb' then
|
||||
node = fs.mount(fn, 'ramfs', 'file')
|
||||
|
||||
local c = { }
|
||||
return {
|
||||
write = function(b)
|
||||
table.insert(c, b)
|
||||
end,
|
||||
flush = function()
|
||||
node.contents = c
|
||||
node.size = #c
|
||||
end,
|
||||
close = function()
|
||||
node.contents = c
|
||||
node.size = #c
|
||||
c = nil
|
||||
end,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return ramfs
|
||||
103
sys/modules/opus/fs/urlfs.lua
Normal file
103
sys/modules/opus/fs/urlfs.lua
Normal file
@@ -0,0 +1,103 @@
|
||||
--local rttp = require('rttp')
|
||||
local Util = require('opus.util')
|
||||
|
||||
local fs = _G.fs
|
||||
|
||||
local urlfs = { }
|
||||
|
||||
function urlfs.mount(_, url)
|
||||
if not url then
|
||||
error('URL is required')
|
||||
end
|
||||
return {
|
||||
url = url,
|
||||
}
|
||||
end
|
||||
|
||||
function urlfs.delete(_, dir)
|
||||
fs.unmount(dir)
|
||||
end
|
||||
|
||||
function urlfs.exists()
|
||||
return true
|
||||
end
|
||||
|
||||
function urlfs.getSize(node)
|
||||
return node.size or 0
|
||||
end
|
||||
|
||||
function urlfs.isReadOnly()
|
||||
return true
|
||||
end
|
||||
|
||||
function urlfs.isDir()
|
||||
return false
|
||||
end
|
||||
|
||||
function urlfs.getDrive()
|
||||
return 'url'
|
||||
end
|
||||
|
||||
function urlfs.open(node, fn, fl)
|
||||
if fl == 'w' or fl == 'wb' then
|
||||
fs.delete(fn)
|
||||
return fs.open(fn, fl)
|
||||
end
|
||||
|
||||
if fl ~= 'r' and fl ~= 'rb' then
|
||||
error('Unsupported mode')
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
if not c then
|
||||
return
|
||||
end
|
||||
|
||||
local ctr = 0
|
||||
local lines
|
||||
|
||||
if fl == 'r' then
|
||||
return {
|
||||
readLine = function()
|
||||
if not lines then
|
||||
lines = Util.split(c)
|
||||
end
|
||||
ctr = ctr + 1
|
||||
return lines[ctr]
|
||||
end,
|
||||
readAll = function()
|
||||
return c
|
||||
end,
|
||||
close = function()
|
||||
lines = nil
|
||||
end,
|
||||
}
|
||||
end
|
||||
return {
|
||||
read = function()
|
||||
ctr = ctr + 1
|
||||
return c:sub(ctr, ctr):byte()
|
||||
end,
|
||||
close = function()
|
||||
ctr = 0
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
return urlfs
|
||||
67
sys/modules/opus/git.lua
Normal file
67
sys/modules/opus/git.lua
Normal file
@@ -0,0 +1,67 @@
|
||||
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'
|
||||
local git = { }
|
||||
|
||||
if _G._GIT_API_KEY then
|
||||
TREE_URL = TREE_URL .. '&access_token=' .. _G._GIT_API_KEY
|
||||
end
|
||||
|
||||
local fs = _G.fs
|
||||
local os = _G.os
|
||||
|
||||
function git.list(repository)
|
||||
local t = Util.split(repository, '(.-)/')
|
||||
|
||||
local user = table.remove(t, 1)
|
||||
local repo = table.remove(t, 1)
|
||||
local branch = table.remove(t, 1) or 'master'
|
||||
local path
|
||||
|
||||
if not Util.empty(t) then
|
||||
path = table.concat(t, '/') .. '/'
|
||||
end
|
||||
|
||||
local function getContents()
|
||||
local dataUrl = string.format(TREE_URL, user, repo, branch)
|
||||
local contents = Util.download(dataUrl)
|
||||
if contents then
|
||||
return json.decode(contents)
|
||||
end
|
||||
end
|
||||
|
||||
local data = getContents() or error('Invalid repository')
|
||||
|
||||
if data.message and data.message:find("API rate limit exceeded") then
|
||||
error("Out of API calls, try again later")
|
||||
end
|
||||
|
||||
if data.message and data.message == "Not found" then
|
||||
error("Invalid repository")
|
||||
end
|
||||
|
||||
local list = { }
|
||||
for _,v in pairs(data.tree) do
|
||||
if v.type == "blob" then
|
||||
v.path = v.path:gsub("%s","%%20")
|
||||
if not path then
|
||||
list[v.path] = {
|
||||
url = string.format(FILE_URL, user, repo, branch, v.path),
|
||||
size = v.size,
|
||||
}
|
||||
elseif Util.startsWith(v.path, path) then
|
||||
local p = string.sub(v.path, #path)
|
||||
list[p] = {
|
||||
url = string.format(FILE_URL, user, repo, branch, path .. p),
|
||||
size = v.size,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return list
|
||||
end
|
||||
|
||||
return git
|
||||
107
sys/modules/opus/gps.lua
Normal file
107
sys/modules/opus/gps.lua
Normal file
@@ -0,0 +1,107 @@
|
||||
local Util = require('opus.util')
|
||||
|
||||
local GPS = { }
|
||||
|
||||
local device = _G.device
|
||||
local gps = _G.gps
|
||||
|
||||
function GPS.locate(timeout, debug)
|
||||
local pt = { }
|
||||
timeout = timeout or 10
|
||||
pt.x, pt.y, pt.z = gps.locate(timeout, debug)
|
||||
if pt.x then
|
||||
return pt
|
||||
end
|
||||
end
|
||||
|
||||
function GPS.isAvailable()
|
||||
return device.wireless_modem and GPS.locate()
|
||||
end
|
||||
|
||||
function GPS.getPoint(timeout, debug)
|
||||
local pt = GPS.locate(timeout, debug)
|
||||
if not pt then
|
||||
return
|
||||
end
|
||||
|
||||
pt.x = math.floor(pt.x)
|
||||
pt.y = math.floor(pt.y)
|
||||
pt.z = math.floor(pt.z)
|
||||
|
||||
if _G.pocket then
|
||||
pt.y = pt.y - 1
|
||||
end
|
||||
|
||||
return pt
|
||||
end
|
||||
|
||||
-- from stock gps API
|
||||
local function trilaterate(A, B, C)
|
||||
local a2b = B.position - A.position
|
||||
local a2c = C.position - A.position
|
||||
|
||||
if math.abs( a2b:normalize():dot( a2c:normalize() ) ) > 0.999 then
|
||||
return
|
||||
end
|
||||
|
||||
local d = a2b:length()
|
||||
local ex = a2b:normalize( )
|
||||
local i = ex:dot( a2c )
|
||||
local ey = (a2c - (ex * i)):normalize()
|
||||
local j = ey:dot( a2c )
|
||||
local ez = ex:cross( ey )
|
||||
|
||||
local r1 = A.distance
|
||||
local r2 = B.distance
|
||||
local r3 = C.distance
|
||||
|
||||
local x = (r1*r1 - r2*r2 + d*d) / (2*d)
|
||||
local y = (r1*r1 - r3*r3 - x*x + (x-i)*(x-i) + j*j) / (2*j)
|
||||
|
||||
local result = A.position + (ex * x) + (ey * y)
|
||||
|
||||
local zSquared = r1*r1 - x*x - y*y
|
||||
if zSquared > 0 then
|
||||
local z = math.sqrt( zSquared )
|
||||
local result1 = result + (ez * z)
|
||||
local result2 = result - (ez * z)
|
||||
|
||||
local rounded1, rounded2 = result1:round(), result2:round()
|
||||
if rounded1.x ~= rounded2.x or rounded1.y ~= rounded2.y or rounded1.z ~= rounded2.z then
|
||||
return rounded1, rounded2
|
||||
else
|
||||
return rounded1
|
||||
end
|
||||
end
|
||||
return result:round()
|
||||
end
|
||||
|
||||
local function narrow( p1, p2, fix )
|
||||
local dist1 = math.abs( (p1 - fix.position):length() - fix.distance )
|
||||
local dist2 = math.abs( (p2 - fix.position):length() - fix.distance )
|
||||
|
||||
if math.abs(dist1 - dist2) < 0.05 then
|
||||
return p1, p2
|
||||
elseif dist1 < dist2 then
|
||||
return p1:round()
|
||||
else
|
||||
return p2:round()
|
||||
end
|
||||
end
|
||||
-- end stock gps api
|
||||
|
||||
function GPS.trilaterate(tFixes)
|
||||
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
|
||||
end
|
||||
|
||||
return GPS
|
||||
50
sys/modules/opus/history.lua
Normal file
50
sys/modules/opus/history.lua
Normal file
@@ -0,0 +1,50 @@
|
||||
local Util = require('opus.util')
|
||||
|
||||
local History = { }
|
||||
local History_mt = { __index = History }
|
||||
|
||||
function History.load(filename, limit)
|
||||
|
||||
local self = setmetatable({
|
||||
limit = limit,
|
||||
filename = filename,
|
||||
}, History_mt)
|
||||
|
||||
self.entries = Util.readLines(filename) or { }
|
||||
self.pos = #self.entries + 1
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function History:add(line)
|
||||
if line ~= self.entries[#self.entries] then
|
||||
table.insert(self.entries, line)
|
||||
if self.limit then
|
||||
while #self.entries > self.limit do
|
||||
table.remove(self.entries, 1)
|
||||
end
|
||||
end
|
||||
Util.writeLines(self.filename, self.entries)
|
||||
self.pos = #self.entries + 1
|
||||
end
|
||||
end
|
||||
|
||||
function History:reset()
|
||||
self.pos = #self.entries + 1
|
||||
end
|
||||
|
||||
function History:back()
|
||||
if self.pos > 1 then
|
||||
self.pos = self.pos - 1
|
||||
return self.entries[self.pos]
|
||||
end
|
||||
end
|
||||
|
||||
function History:forward()
|
||||
if self.pos <= #self.entries then
|
||||
self.pos = self.pos + 1
|
||||
return self.entries[self.pos]
|
||||
end
|
||||
end
|
||||
|
||||
return History
|
||||
128
sys/modules/opus/http/pastebin.lua
Normal file
128
sys/modules/opus/http/pastebin.lua
Normal file
@@ -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,
|
||||
}
|
||||
|
||||
119
sys/modules/opus/injector.lua
Normal file
119
sys/modules/opus/injector.lua
Normal file
@@ -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
|
||||
188
sys/modules/opus/input.lua
Normal file
188
sys/modules/opus/input.lua
Normal file
@@ -0,0 +1,188 @@
|
||||
local Util = require('opus.util')
|
||||
|
||||
local keyboard = _G.device and _G.device.keyboard
|
||||
local keys = _G.keys
|
||||
local os = _G.os
|
||||
|
||||
local modifiers = Util.transpose {
|
||||
keys.leftCtrl, keys.rightCtrl,
|
||||
keys.leftShift, keys.rightShift,
|
||||
keys.leftAlt, keys.rightAlt,
|
||||
}
|
||||
|
||||
if not keyboard then -- not running under Opus OS
|
||||
keyboard = { 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
|
||||
keyboard.state[keys.leftAlt] or
|
||||
keyboard.state[keys.rightAlt]
|
||||
end
|
||||
|
||||
function input:toCode(ch, code)
|
||||
local result = { }
|
||||
|
||||
if not ch and code == 1 then
|
||||
ch = 'escape'
|
||||
end
|
||||
|
||||
if keyboard.state[keys.leftCtrl] or keyboard.state[keys.rightCtrl] or
|
||||
code == keys.leftCtrl or code == keys.rightCtrl then
|
||||
table.insert(result, 'control')
|
||||
end
|
||||
|
||||
-- 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.leftShift] or keyboard.state[keys.rightShift] or
|
||||
code == keys.leftShift or code == keys.rightShift then
|
||||
if code and modifiers[code] then
|
||||
table.insert(result, 'shift')
|
||||
elseif #ch == 1 then
|
||||
table.insert(result, ch:upper())
|
||||
else
|
||||
table.insert(result, 'shift')
|
||||
table.insert(result, ch)
|
||||
end
|
||||
elseif not code or not modifiers[code] then
|
||||
table.insert(result, ch)
|
||||
end
|
||||
|
||||
return table.concat(result, '-')
|
||||
end
|
||||
|
||||
function input:reset()
|
||||
self.state = { }
|
||||
|
||||
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
|
||||
local ch = input:toCode(keys.getName(code), code)
|
||||
if #ch == 1 then
|
||||
return {
|
||||
code = 'char',
|
||||
ch = ch,
|
||||
}
|
||||
end
|
||||
return { code = ch }
|
||||
end
|
||||
elseif code then
|
||||
local ch = input:toCode(keys.getName(code), code)
|
||||
if #ch ~= 1 then
|
||||
return { code = ch }
|
||||
end
|
||||
end
|
||||
|
||||
elseif event == 'char' then
|
||||
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 == 'paste' then
|
||||
if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] then
|
||||
return { code = 'shift-paste', text = code }
|
||||
else
|
||||
return { code = 'paste', text = code }
|
||||
end
|
||||
|
||||
elseif event == 'mouse_click' then
|
||||
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
|
||||
return {
|
||||
code = input:toCode('mouse_drag', 255),
|
||||
button = code,
|
||||
x = p1,
|
||||
y = p2,
|
||||
}
|
||||
|
||||
elseif event == 'mouse_up' then
|
||||
if not self.mfired then
|
||||
local clock = os.clock()
|
||||
if self.timer and
|
||||
p1 == self.x and p2 == self.y and
|
||||
(clock - self.timer < .5) then
|
||||
|
||||
self.mch = 'mouse_doubleclick'
|
||||
self.timer = nil
|
||||
else
|
||||
self.timer = os.clock()
|
||||
self.x = p1
|
||||
self.y = p2
|
||||
end
|
||||
self.mfired = input:toCode(self.mch, 255)
|
||||
else
|
||||
self.mch = 'mouse_up'
|
||||
self.mfired = input:toCode(self.mch, 255)
|
||||
end
|
||||
return {
|
||||
code = self.mfired,
|
||||
button = code,
|
||||
x = p1,
|
||||
y = p2,
|
||||
}
|
||||
|
||||
elseif event == "mouse_scroll" then
|
||||
local directions = {
|
||||
[ -1 ] = 'scroll_up',
|
||||
[ 1 ] = 'scroll_down'
|
||||
}
|
||||
return {
|
||||
code = input:toCode(directions[code], 255),
|
||||
x = p1,
|
||||
y = p2,
|
||||
}
|
||||
|
||||
elseif event == 'terminate' then
|
||||
return { code = 'terminate' }
|
||||
end
|
||||
end
|
||||
|
||||
function input:test()
|
||||
while true do
|
||||
local ch = self:translate(os.pullEvent())
|
||||
if ch then
|
||||
Util.print(ch)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return input
|
||||
589
sys/modules/opus/json.lua
Normal file
589
sys/modules/opus/json.lua
Normal file
@@ -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
|
||||
<http://dkolf.de/src/dkjson-lua.fsl/>.
|
||||
|
||||
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
|
||||
51
sys/modules/opus/map.lua
Normal file
51
sys/modules/opus/map.lua
Normal file
@@ -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
|
||||
74
sys/modules/opus/nft.lua
Normal file
74
sys/modules/opus/nft.lua
Normal file
@@ -0,0 +1,74 @@
|
||||
local Util = require('opus.util')
|
||||
|
||||
local NFT = { }
|
||||
|
||||
-- largely copied from http://www.computercraft.info/forums2/index.php?/topic/5029-145-npaintpro/
|
||||
|
||||
local tColourLookup = { }
|
||||
for n = 1, 16 do
|
||||
tColourLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1)
|
||||
end
|
||||
|
||||
local function getColourOf(hex)
|
||||
return tColourLookup[hex:byte()]
|
||||
end
|
||||
|
||||
function NFT.parse(imageText)
|
||||
local image = {
|
||||
fg = { },
|
||||
bg = { },
|
||||
text = { },
|
||||
}
|
||||
|
||||
local num = 1
|
||||
local lines = Util.split(imageText)
|
||||
while #lines[#lines] == 0 do
|
||||
table.remove(lines, #lines)
|
||||
end
|
||||
|
||||
for _,sLine in ipairs(lines) do
|
||||
table.insert(image.fg, { })
|
||||
table.insert(image.bg, { })
|
||||
table.insert(image.text, { })
|
||||
|
||||
--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 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
|
||||
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
|
||||
if not image.width or writeIndex - 1 > image.width then
|
||||
image.width = writeIndex - 1
|
||||
end
|
||||
num = num+1
|
||||
end
|
||||
return image
|
||||
end
|
||||
|
||||
function NFT.load(path)
|
||||
|
||||
local imageText = Util.readFile(path)
|
||||
if not imageText then
|
||||
error('Unable to read image file')
|
||||
end
|
||||
return NFT.parse(imageText)
|
||||
end
|
||||
|
||||
return NFT
|
||||
96
sys/modules/opus/packages.lua
Normal file
96
sys/modules/opus/packages.lua
Normal file
@@ -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
|
||||
126
sys/modules/opus/peripheral.lua
Normal file
126
sys/modules/opus/peripheral.lua
Normal file
@@ -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
|
||||
345
sys/modules/opus/point.lua
Normal file
345
sys/modules/opus/point.lua
Normal file
@@ -0,0 +1,345 @@
|
||||
local Util = require('opus.util')
|
||||
|
||||
local Point = { }
|
||||
|
||||
Point.directions = {
|
||||
[ 0 ] = { xd = 1, zd = 0, yd = 0, heading = 0, direction = 'east' },
|
||||
[ 1 ] = { xd = 0, zd = 1, yd = 0, heading = 1, direction = 'south' },
|
||||
[ 2 ] = { xd = -1, zd = 0, yd = 0, heading = 2, direction = 'west' },
|
||||
[ 3 ] = { xd = 0, zd = -1, yd = 0, heading = 3, direction = 'north' },
|
||||
[ 4 ] = { xd = 0, zd = 0, yd = 1, heading = 4, direction = 'up' },
|
||||
[ 5 ] = { xd = 0, zd = 0, yd = -1, heading = 5, direction = 'down' },
|
||||
}
|
||||
|
||||
Point.facings = {
|
||||
[ 0 ] = Point.directions[0],
|
||||
[ 1 ] = Point.directions[1],
|
||||
[ 2 ] = Point.directions[2],
|
||||
[ 3 ] = Point.directions[3],
|
||||
east = Point.directions[0],
|
||||
south = Point.directions[1],
|
||||
west = Point.directions[2],
|
||||
north = Point.directions[3],
|
||||
}
|
||||
|
||||
Point.headings = {
|
||||
[ 0 ] = Point.directions[0],
|
||||
[ 1 ] = Point.directions[1],
|
||||
[ 2 ] = Point.directions[2],
|
||||
[ 3 ] = Point.directions[3],
|
||||
[ 4 ] = Point.directions[4],
|
||||
[ 5 ] = Point.directions[5],
|
||||
east = Point.directions[0],
|
||||
south = Point.directions[1],
|
||||
west = Point.directions[2],
|
||||
north = Point.directions[3],
|
||||
up = Point.directions[4],
|
||||
down = Point.directions[5],
|
||||
}
|
||||
|
||||
Point.EAST = 0
|
||||
Point.SOUTH = 1
|
||||
Point.WEST = 2
|
||||
Point.NORTH = 3
|
||||
Point.UP = 4
|
||||
Point.DOWN = 5
|
||||
|
||||
function Point.copy(pt)
|
||||
return { x = pt.x, y = pt.y, z = pt.z }
|
||||
end
|
||||
|
||||
function Point.round(pt)
|
||||
pt.x = Util.round(pt.x)
|
||||
pt.y = Util.round(pt.y)
|
||||
pt.z = Util.round(pt.z)
|
||||
return pt
|
||||
end
|
||||
|
||||
function Point.same(pta, ptb)
|
||||
return pta.x == ptb.x and
|
||||
pta.y == ptb.y and
|
||||
pta.z == ptb.z
|
||||
end
|
||||
|
||||
function Point.above(pt)
|
||||
return { x = pt.x, y = pt.y + 1, z = pt.z, heading = pt.heading }
|
||||
end
|
||||
|
||||
function Point.below(pt)
|
||||
return { x = pt.x, y = pt.y - 1, z = pt.z, heading = pt.heading }
|
||||
end
|
||||
|
||||
function Point.subtract(a, b)
|
||||
a.x = a.x - b.x
|
||||
a.y = a.y - b.y
|
||||
a.z = a.z - b.z
|
||||
end
|
||||
|
||||
-- Euclidian distance
|
||||
function Point.distance(a, b)
|
||||
return math.sqrt(
|
||||
math.pow(a.x - b.x, 2) +
|
||||
math.pow(a.y - b.y, 2) +
|
||||
math.pow(a.z - b.z, 2))
|
||||
end
|
||||
|
||||
-- turtle distance (manhattan)
|
||||
function Point.turtleDistance(a, b)
|
||||
if a.y and b.y then
|
||||
return math.abs(a.x - b.x) +
|
||||
math.abs(a.y - b.y) +
|
||||
math.abs(a.z - b.z)
|
||||
else
|
||||
return math.abs(a.x - b.x) +
|
||||
math.abs(a.z - b.z)
|
||||
end
|
||||
end
|
||||
|
||||
function Point.calculateTurns(ih, oh)
|
||||
if ih == oh then
|
||||
return 0
|
||||
end
|
||||
if (ih % 2) == (oh % 2) then
|
||||
return 2
|
||||
end
|
||||
return 1
|
||||
end
|
||||
|
||||
function Point.calculateHeading(pta, ptb)
|
||||
local heading
|
||||
local xd, zd = pta.x - ptb.x, pta.z - ptb.z
|
||||
|
||||
if (pta.heading % 2) == 0 and zd ~= 0 then
|
||||
heading = zd < 0 and 1 or 3
|
||||
elseif (pta.heading % 2) == 1 and xd ~= 0 then
|
||||
heading = xd < 0 and 0 or 2
|
||||
elseif pta.heading == 0 and xd > 0 then
|
||||
heading = 2
|
||||
elseif pta.heading == 2 and xd < 0 then
|
||||
heading = 0
|
||||
elseif pta.heading == 1 and zd > 0 then
|
||||
heading = 3
|
||||
elseif pta.heading == 3 and zd < 0 then
|
||||
heading = 1
|
||||
end
|
||||
|
||||
return heading or pta.heading
|
||||
end
|
||||
|
||||
-- Calculate distance to location including turns
|
||||
-- also returns the resulting heading
|
||||
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
|
||||
heading = 1
|
||||
else
|
||||
heading = 3
|
||||
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
|
||||
heading = 0
|
||||
else
|
||||
heading = 2
|
||||
end
|
||||
end
|
||||
|
||||
if not ptb.heading then
|
||||
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
|
||||
if heading ~= ptb.heading then
|
||||
local turns = Point.calculateTurns(heading, ptb.heading)
|
||||
moves = moves + turns
|
||||
local wturns = { [0] = 0, [1] = .9, [2] = 1.8 }
|
||||
weighted = weighted + wturns[turns]
|
||||
heading = ptb.heading
|
||||
end
|
||||
|
||||
return moves, heading, weighted
|
||||
end
|
||||
|
||||
-- given a set of points, find the one taking the least moves
|
||||
function Point.closest(reference, pts)
|
||||
if #pts == 1 then
|
||||
return pts[1]
|
||||
end
|
||||
|
||||
local lm, lpt = math.huge
|
||||
for _,pt in pairs(pts) do
|
||||
local distance = Point.turtleDistance(reference, pt)
|
||||
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
|
||||
lm = m
|
||||
end
|
||||
end
|
||||
end
|
||||
return lpt
|
||||
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)
|
||||
local r = fn(pt)
|
||||
if r then
|
||||
return r
|
||||
end
|
||||
Util.removeByValue(pts, pt)
|
||||
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 = { }
|
||||
|
||||
for i = 0, 5 do
|
||||
local hi = Point.headings[i]
|
||||
table.insert(pts, { x = pt.x + hi.xd, y = pt.y + hi.yd, z = pt.z + hi.zd })
|
||||
end
|
||||
|
||||
return pts
|
||||
end
|
||||
|
||||
-- get the point nearest A that is in the direction of B
|
||||
function Point.nearestTo(pta, ptb)
|
||||
local heading
|
||||
|
||||
if pta.x < ptb.x then
|
||||
heading = 0
|
||||
elseif pta.z < ptb.z then
|
||||
heading = 1
|
||||
elseif pta.x > ptb.x then
|
||||
heading = 2
|
||||
elseif pta.z > ptb.z then
|
||||
heading = 3
|
||||
elseif pta.y < ptb.y then
|
||||
heading = 4
|
||||
elseif pta.y > ptb.y then
|
||||
heading = 5
|
||||
end
|
||||
|
||||
if heading then
|
||||
return {
|
||||
x = pta.x + Point.headings[heading].xd,
|
||||
y = pta.y + Point.headings[heading].yd,
|
||||
z = pta.z + Point.headings[heading].zd,
|
||||
}
|
||||
end
|
||||
|
||||
return pta -- error ?
|
||||
end
|
||||
|
||||
function Point.rotate(pt, facing)
|
||||
local x, z = pt.x, pt.z
|
||||
if facing == 1 then
|
||||
pt.x = z
|
||||
pt.z = -x
|
||||
elseif facing == 2 then
|
||||
pt.x = -x
|
||||
pt.z = -z
|
||||
elseif facing == 3 then
|
||||
pt.x = -z
|
||||
pt.z = x
|
||||
end
|
||||
end
|
||||
|
||||
function Point.makeBox(pt1, pt2)
|
||||
return {
|
||||
x = pt1.x,
|
||||
y = pt1.y,
|
||||
z = pt1.z,
|
||||
ex = pt2.x,
|
||||
ey = pt2.y,
|
||||
ez = pt2.z,
|
||||
}
|
||||
end
|
||||
|
||||
-- expand box to include point
|
||||
function Point.expandBox(box, pt)
|
||||
if pt.x < box.x then
|
||||
box.x = pt.x
|
||||
elseif pt.x > box.ex then
|
||||
box.ex = pt.x
|
||||
end
|
||||
if pt.y < box.y then
|
||||
box.y = pt.y
|
||||
elseif pt.y > box.ey then
|
||||
box.ey = pt.y
|
||||
end
|
||||
if pt.z < box.z then
|
||||
box.z = pt.z
|
||||
elseif pt.z > box.ez then
|
||||
box.ez = pt.z
|
||||
end
|
||||
end
|
||||
|
||||
function Point.normalizeBox(box)
|
||||
return {
|
||||
x = math.min(box.x, box.ex),
|
||||
y = math.min(box.y, box.ey),
|
||||
z = math.min(box.z, box.ez),
|
||||
ex = math.max(box.x, box.ex),
|
||||
ey = math.max(box.y, box.ey),
|
||||
ez = math.max(box.z, box.ez),
|
||||
}
|
||||
end
|
||||
|
||||
function Point.inBox(pt, box)
|
||||
return pt.x >= box.x and
|
||||
pt.y >= box.y and
|
||||
pt.z >= box.z and
|
||||
pt.x <= box.ex and
|
||||
pt.y <= box.ey and
|
||||
pt.z <= box.ez
|
||||
end
|
||||
|
||||
function Point.closestPointInBox(pt, box)
|
||||
local cpt = {
|
||||
x = math.abs(pt.x - box.x) < math.abs(pt.x - box.ex) and box.x or box.ex,
|
||||
y = math.abs(pt.y - box.y) < math.abs(pt.y - box.ey) and box.y or box.ey,
|
||||
z = math.abs(pt.z - box.z) < math.abs(pt.z - box.ez) and box.z or box.ez,
|
||||
}
|
||||
cpt.x = pt.x > box.x and pt.x < box.ex and pt.x or cpt.x
|
||||
cpt.y = pt.y > box.y and pt.y < box.ey and pt.y or cpt.y
|
||||
cpt.z = pt.z > box.z and pt.z < box.ez and pt.z or cpt.z
|
||||
|
||||
return cpt
|
||||
end
|
||||
|
||||
return Point
|
||||
40
sys/modules/opus/security.lua
Normal file
40
sys/modules/opus/security.lua
Normal file
@@ -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
|
||||
255
sys/modules/opus/socket.lua
Normal file
255
sys/modules/opus/socket.lua
Normal file
@@ -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
|
||||
17
sys/modules/opus/sound.lua
Normal file
17
sys/modules/opus/sound.lua
Normal file
@@ -0,0 +1,17 @@
|
||||
local peripheral = _G.peripheral
|
||||
|
||||
local Sound = {
|
||||
_volume = 1,
|
||||
}
|
||||
|
||||
function Sound.play(sound, vol)
|
||||
peripheral.find('speaker', function(_, s)
|
||||
s.playSound('minecraft:' .. sound, vol or Sound._volume)
|
||||
end)
|
||||
end
|
||||
|
||||
function Sound.setVolume(volume)
|
||||
Sound._volume = math.max(0, math.min(volume, 1))
|
||||
end
|
||||
|
||||
return Sound
|
||||
61
sys/modules/opus/sync.lua
Normal file
61
sys/modules/opus/sync.lua
Normal file
@@ -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
|
||||
362
sys/modules/opus/terminal.lua
Normal file
362
sys/modules/opus/terminal.lua
Normal file
@@ -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
|
||||
110
sys/modules/opus/trace.lua
Normal file
110
sys/modules/opus/trace.lua
Normal file
@@ -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
|
||||
1250
sys/modules/opus/ui.lua
Normal file
1250
sys/modules/opus/ui.lua
Normal file
File diff suppressed because it is too large
Load Diff
414
sys/modules/opus/ui/canvas.lua
Normal file
414
sys/modules/opus/ui/canvas.lua
Normal file
@@ -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
|
||||
32
sys/modules/opus/ui/components/ActiveLayer.lua
Normal file
32
sys/modules/opus/ui/components/ActiveLayer.lua
Normal file
@@ -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
|
||||
66
sys/modules/opus/ui/components/Button.lua
Normal file
66
sys/modules/opus/ui/components/Button.lua
Normal file
@@ -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
|
||||
63
sys/modules/opus/ui/components/Checkbox.lua
Normal file
63
sys/modules/opus/ui/components/Checkbox.lua
Normal file
@@ -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
|
||||
88
sys/modules/opus/ui/components/Chooser.lua
Normal file
88
sys/modules/opus/ui/components/Chooser.lua
Normal file
@@ -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
|
||||
39
sys/modules/opus/ui/components/Dialog.lua
Normal file
39
sys/modules/opus/ui/components/Dialog.lua
Normal file
@@ -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
|
||||
75
sys/modules/opus/ui/components/DropMenu.lua
Normal file
75
sys/modules/opus/ui/components/DropMenu.lua
Normal file
@@ -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
|
||||
21
sys/modules/opus/ui/components/DropMenuItem.lua
Normal file
21
sys/modules/opus/ui/components/DropMenuItem.lua
Normal file
@@ -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
|
||||
76
sys/modules/opus/ui/components/Embedded.lua
Normal file
76
sys/modules/opus/ui/components/Embedded.lua
Normal file
@@ -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
|
||||
148
sys/modules/opus/ui/components/Form.lua
Normal file
148
sys/modules/opus/ui/components/Form.lua
Normal file
@@ -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
|
||||
495
sys/modules/opus/ui/components/Grid.lua
Normal file
495
sys/modules/opus/ui/components/Grid.lua
Normal file
@@ -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
|
||||
40
sys/modules/opus/ui/components/Image.lua
Normal file
40
sys/modules/opus/ui/components/Image.lua
Normal file
@@ -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
|
||||
61
sys/modules/opus/ui/components/Menu.lua
Normal file
61
sys/modules/opus/ui/components/Menu.lua
Normal file
@@ -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
|
||||
90
sys/modules/opus/ui/components/MenuBar.lua
Normal file
90
sys/modules/opus/ui/components/MenuBar.lua
Normal file
@@ -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
|
||||
14
sys/modules/opus/ui/components/MenuItem.lua
Normal file
14
sys/modules/opus/ui/components/MenuItem.lua
Normal file
@@ -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,
|
||||
}
|
||||
35
sys/modules/opus/ui/components/NftImage.lua
Normal file
35
sys/modules/opus/ui/components/NftImage.lua
Normal file
@@ -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
|
||||
92
sys/modules/opus/ui/components/Notification.lua
Normal file
92
sys/modules/opus/ui/components/Notification.lua
Normal file
@@ -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
|
||||
28
sys/modules/opus/ui/components/ProgressBar.lua
Normal file
28
sys/modules/opus/ui/components/ProgressBar.lua
Normal file
@@ -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
|
||||
74
sys/modules/opus/ui/components/ScrollBar.lua
Normal file
74
sys/modules/opus/ui/components/ScrollBar.lua
Normal file
@@ -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
|
||||
60
sys/modules/opus/ui/components/ScrollingGrid.lua
Normal file
60
sys/modules/opus/ui/components/ScrollingGrid.lua
Normal file
@@ -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
|
||||
52
sys/modules/opus/ui/components/SlideOut.lua
Normal file
52
sys/modules/opus/ui/components/SlideOut.lua
Normal file
@@ -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
|
||||
77
sys/modules/opus/ui/components/Slider.lua
Normal file
77
sys/modules/opus/ui/components/Slider.lua
Normal file
@@ -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
|
||||
98
sys/modules/opus/ui/components/StatusBar.lua
Normal file
98
sys/modules/opus/ui/components/StatusBar.lua
Normal file
@@ -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
|
||||
12
sys/modules/opus/ui/components/Tab.lua
Normal file
12
sys/modules/opus/ui/components/Tab.lua
Normal file
@@ -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,
|
||||
}
|
||||
45
sys/modules/opus/ui/components/TabBar.lua
Normal file
45
sys/modules/opus/ui/components/TabBar.lua
Normal file
@@ -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
|
||||
25
sys/modules/opus/ui/components/TabBarMenuItem.lua
Normal file
25
sys/modules/opus/ui/components/TabBarMenuItem.lua
Normal file
@@ -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
|
||||
89
sys/modules/opus/ui/components/Tabs.lua
Normal file
89
sys/modules/opus/ui/components/Tabs.lua
Normal file
@@ -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
|
||||
20
sys/modules/opus/ui/components/Text.lua
Normal file
20
sys/modules/opus/ui/components/Text.lua
Normal file
@@ -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
|
||||
36
sys/modules/opus/ui/components/TextArea.lua
Normal file
36
sys/modules/opus/ui/components/TextArea.lua
Normal file
@@ -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
|
||||
136
sys/modules/opus/ui/components/TextEntry.lua
Normal file
136
sys/modules/opus/ui/components/TextEntry.lua
Normal file
@@ -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
|
||||
65
sys/modules/opus/ui/components/Throttle.lua
Normal file
65
sys/modules/opus/ui/components/Throttle.lua
Normal file
@@ -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
|
||||
73
sys/modules/opus/ui/components/TitleBar.lua
Normal file
73
sys/modules/opus/ui/components/TitleBar.lua
Normal file
@@ -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
|
||||
18
sys/modules/opus/ui/components/VerticalMeter.lua
Normal file
18
sys/modules/opus/ui/components/VerticalMeter.lua
Normal file
@@ -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
|
||||
100
sys/modules/opus/ui/components/Viewport.lua
Normal file
100
sys/modules/opus/ui/components/Viewport.lua
Normal file
@@ -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
|
||||
124
sys/modules/opus/ui/components/Wizard.lua
Normal file
124
sys/modules/opus/ui/components/Wizard.lua
Normal file
@@ -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
|
||||
11
sys/modules/opus/ui/components/WizardPage.lua
Normal file
11
sys/modules/opus/ui/components/WizardPage.lua
Normal file
@@ -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,
|
||||
}
|
||||
262
sys/modules/opus/ui/region.lua
Normal file
262
sys/modules/opus/ui/region.lua
Normal file
@@ -0,0 +1,262 @@
|
||||
--
|
||||
-- tek.lib.region
|
||||
-- Written by Timm S. Mueller <tmueller at schulze-mueller.de>
|
||||
--
|
||||
-- Copyright 2008 - 2016 by the authors and contributors:
|
||||
--
|
||||
-- * Timm S. Muller <tmueller at schulze-mueller.de>
|
||||
-- * Franciska Schulze <fschulze at schulze-mueller.de>
|
||||
-- * Tobias Schwinger <tschwinger at isonews2.com>
|
||||
--
|
||||
-- https://opensource.org/licenses/MIT
|
||||
--
|
||||
-- Some comments have been removed to reduce file size, see:
|
||||
-- https://github.com/technosaurus/tekui/blob/master/etc/region.lua
|
||||
-- for the full source
|
||||
|
||||
local insert = table.insert
|
||||
local ipairs = ipairs
|
||||
local max = math.max
|
||||
local min = math.min
|
||||
local setmetatable = setmetatable
|
||||
local unpack = unpack or table.unpack
|
||||
|
||||
local Region = { }
|
||||
Region._VERSION = "Region 11.3"
|
||||
|
||||
Region.__index = Region
|
||||
|
||||
-- x0, y0, x1, y1 = Region.intersect(d1, d2, d3, d4, s1, s2, s3, s4):
|
||||
-- Returns the coordinates of a rectangle where a rectangle specified by
|
||||
-- the coordinates s1, s2, s3, s4 overlaps with the rectangle specified
|
||||
-- by the coordinates d1, d2, d3, d4. The return value is '''nil''' if
|
||||
-- the rectangles do not overlap.
|
||||
function Region.intersect(d1, d2, d3, d4, s1, s2, s3, s4)
|
||||
if s3 >= d1 and s1 <= d3 and s4 >= d2 and s2 <= d4 then
|
||||
return max(s1, d1), max(s2, d2), min(s3, d3), min(s4, d4)
|
||||
end
|
||||
end
|
||||
|
||||
-- insertrect: insert rect to table, merging with an existing one if possible
|
||||
local function insertrect(d, s1, s2, s3, s4)
|
||||
for i = 1, min(4, #d) do
|
||||
local a = d[i]
|
||||
local a1, a2, a3, a4 = a[1], a[2], a[3], a[4]
|
||||
if a2 == s2 and a4 == s4 then
|
||||
if a3 + 1 == s1 then
|
||||
a[3] = s3
|
||||
return
|
||||
elseif a1 == s3 + 1 then
|
||||
a[1] = s1
|
||||
return
|
||||
end
|
||||
elseif a1 == s1 and a3 == s3 then
|
||||
if a4 + 1 == s2 then
|
||||
a[4] = s4
|
||||
return
|
||||
elseif a2 == s4 + 1 then
|
||||
a[2] = s2
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
insert(d, 1, { s1, s2, s3, s4 })
|
||||
end
|
||||
|
||||
-- cutrect: cut rect d into table of new rects, using rect s as a punch
|
||||
local function cutrect(d1, d2, d3, d4, s1, s2, s3, s4)
|
||||
if not Region.intersect(d1, d2, d3, d4, s1, s2, s3, s4) then
|
||||
return { { d1, d2, d3, d4 } }
|
||||
end
|
||||
local r = { }
|
||||
if d1 < s1 then
|
||||
insertrect(r, d1, d2, s1 - 1, d4)
|
||||
d1 = s1
|
||||
end
|
||||
if d2 < s2 then
|
||||
insertrect(r, d1, d2, d3, s2 - 1)
|
||||
d2 = s2
|
||||
end
|
||||
if d3 > s3 then
|
||||
insertrect(r, s3 + 1, d2, d3, d4)
|
||||
d3 = s3
|
||||
end
|
||||
if d4 > s4 then
|
||||
insertrect(r, d1, s4 + 1, d3, d4)
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
-- cutregion: cut region d, using s as a punch
|
||||
local function cutregion(d, s1, s2, s3, s4)
|
||||
local r = { }
|
||||
for _, dr in ipairs(d) do
|
||||
local d1, d2, d3, d4 = dr[1], dr[2], dr[3], dr[4]
|
||||
for _, t in ipairs(cutrect(d1, d2, d3, d4, s1, s2, s3, s4)) do
|
||||
insertrect(r, t[1], t[2], t[3], t[4])
|
||||
end
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
-- region = Region.new(r1, r2, r3, r4): Creates a new region from the given
|
||||
-- coordinates.
|
||||
function Region.new(r1, r2, r3, r4)
|
||||
if r1 then
|
||||
return setmetatable({ region = { { r1, r2, r3, r4 } } }, Region)
|
||||
end
|
||||
return setmetatable({ region = { } }, Region)
|
||||
end
|
||||
|
||||
-- self = region:setRect(r1, r2, r3, r4): Resets an existing region
|
||||
-- to the specified rectangle.
|
||||
function Region:setRect(r1, r2, r3, r4)
|
||||
self.region = { { r1, r2, r3, r4 } }
|
||||
return self
|
||||
end
|
||||
|
||||
-- region:orRect(r1, r2, r3, r4): Logical ''or''s a rectangle to a region
|
||||
function Region:orRect(s1, s2, s3, s4)
|
||||
self.region = cutregion(self.region, s1, s2, s3, s4)
|
||||
insertrect(self.region, s1, s2, s3, s4)
|
||||
end
|
||||
|
||||
-- region:orRegion(region): Logical ''or''s another region to a region
|
||||
function Region:orRegion(s)
|
||||
for _, r in ipairs(s) do
|
||||
self:orRect(r[1], r[2], r[3], r[4])
|
||||
end
|
||||
end
|
||||
|
||||
-- region:andRect(r1, r2, r3, r4): Logical ''and''s a rectange to a region
|
||||
function Region:andRect(s1, s2, s3, s4)
|
||||
local r = { }
|
||||
for _, d in ipairs(self.region) do
|
||||
local t1, t2, t3, t4 =
|
||||
Region.intersect(d[1], d[2], d[3], d[4], s1, s2, s3, s4)
|
||||
if t1 then
|
||||
insertrect(r, t1, t2, t3, t4)
|
||||
end
|
||||
end
|
||||
self.region = r
|
||||
end
|
||||
|
||||
-- region:xorRect(r1, r2, r3, r4): Logical ''xor''s a rectange to a region
|
||||
function Region:xorRect(s1, s2, s3, s4)
|
||||
local r1 = { }
|
||||
local r2 = { { s1, s2, s3, s4 } }
|
||||
for _, d in ipairs(self.region) do
|
||||
local d1, d2, d3, d4 = d[1], d[2], d[3], d[4]
|
||||
for _, t in ipairs(cutrect(d1, d2, d3, d4, s1, s2, s3, s4)) do
|
||||
insertrect(r1, t[1], t[2], t[3], t[4])
|
||||
end
|
||||
r2 = cutregion(r2, d1, d2, d3, d4)
|
||||
end
|
||||
self.region = r1
|
||||
self:orRegion(r2)
|
||||
end
|
||||
|
||||
-- self = region:subRect(r1, r2, r3, r4): Subtracts a rectangle from a region
|
||||
function Region:subRect(s1, s2, s3, s4)
|
||||
local r1 = { }
|
||||
for _, d in ipairs(self.region) do
|
||||
local d1, d2, d3, d4 = d[1], d[2], d[3], d[4]
|
||||
for _, t in ipairs(cutrect(d1, d2, d3, d4, s1, s2, s3, s4)) do
|
||||
insertrect(r1, t[1], t[2], t[3], t[4])
|
||||
end
|
||||
end
|
||||
self.region = r1
|
||||
return self
|
||||
end
|
||||
|
||||
-- region:getRect - gets an iterator on the rectangles in a region [internal]
|
||||
function Region:getRects()
|
||||
local index = 0
|
||||
return function(object)
|
||||
index = index + 1
|
||||
if object[index] then
|
||||
return unpack(object[index])
|
||||
end
|
||||
end, self.region
|
||||
end
|
||||
|
||||
-- success = region:checkIntersect(x0, y0, x1, y1): Returns a boolean
|
||||
-- indicating whether a rectangle specified by its coordinates overlaps
|
||||
-- with a region.
|
||||
function Region:checkIntersect(s1, s2, s3, s4)
|
||||
for _, d in ipairs(self.region) do
|
||||
if Region.intersect(d[1], d[2], d[3], d[4], s1, s2, s3, s4) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- region:subRegion(region2): Subtracts {{region2}} from {{region}}.
|
||||
function Region:subRegion(region)
|
||||
if region then
|
||||
for r1, r2, r3, r4 in region:getRects() do
|
||||
self:subRect(r1, r2, r3, r4)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- region:andRegion(r): Logically ''and''s a region to a region
|
||||
function Region:andRegion(s)
|
||||
local r = { }
|
||||
for _, s in ipairs(s.region) do
|
||||
for _, d in ipairs(self.region) do
|
||||
local t1, t2, t3, t4 =
|
||||
Region.intersect(d[1], d[2], d[3], d[4],
|
||||
s[1], s[2], s[3], s[4])
|
||||
if t1 then
|
||||
insertrect(r, t1, t2, t3, t4)
|
||||
end
|
||||
end
|
||||
end
|
||||
self.region = r
|
||||
end
|
||||
|
||||
-- region:forEach(func, obj, ...): For each rectangle in a region, calls the
|
||||
-- specified function according the following scheme:
|
||||
-- func(obj, x0, y0, x1, y1, ...)
|
||||
-- Extra arguments are passed through to the function.
|
||||
function Region:forEach(func, obj, ...)
|
||||
for x0, y0, x1, y1 in self:getRects() do
|
||||
func(obj, x0, y0, x1, y1, ...)
|
||||
end
|
||||
end
|
||||
|
||||
-- region:shift(dx, dy): Shifts a region by delta x and y.
|
||||
function Region:shift(dx, dy)
|
||||
for _, r in ipairs(self.region) do
|
||||
r[1] = r[1] + dx
|
||||
r[2] = r[2] + dy
|
||||
r[3] = r[3] + dx
|
||||
r[4] = r[4] + dy
|
||||
end
|
||||
end
|
||||
|
||||
-- region:isEmpty(): Returns '''true''' if a region is empty.
|
||||
function Region:isEmpty()
|
||||
return #self.region == 0
|
||||
end
|
||||
|
||||
-- minx, miny, maxx, maxy = region:get(): Get region's min/max extents
|
||||
function Region:get()
|
||||
if #self.region > 0 then
|
||||
local minx = 1000000 -- ui.HUGE
|
||||
local miny = 1000000
|
||||
local maxx = 0
|
||||
local maxy = 0
|
||||
for _, r in ipairs(self.region) do
|
||||
minx = min(minx, r[1])
|
||||
miny = min(miny, r[2])
|
||||
maxx = max(maxx, r[3])
|
||||
maxy = max(maxy, r[4])
|
||||
end
|
||||
return minx, miny, maxx, maxy
|
||||
end
|
||||
end
|
||||
|
||||
return Region
|
||||
53
sys/modules/opus/ui/transition.lua
Normal file
53
sys/modules/opus/ui/transition.lua
Normal file
@@ -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
|
||||
350
sys/modules/opus/ui/tween.lua
Normal file
350
sys/modules/opus/ui/tween.lua
Normal file
@@ -0,0 +1,350 @@
|
||||
local tween = {
|
||||
_VERSION = 'tween 2.1.1',
|
||||
_DESCRIPTION = 'tweening for lua',
|
||||
_URL = 'https://github.com/kikito/tween.lua',
|
||||
_LICENSE = [[
|
||||
MIT LICENSE
|
||||
|
||||
Copyright (c) 2014 Enrique García Cota, Yuichi Tateno, Emmanuel Oga
|
||||
|
||||
license details: https://opensource.org/licenses/MIT
|
||||
]]
|
||||
}
|
||||
|
||||
-- easing
|
||||
|
||||
-- Adapted from https://github.com/EmmanuelOga/easing. See LICENSE.txt for credits.
|
||||
-- For all easing functions:
|
||||
-- t = time == how much time has to pass for the tweening to complete
|
||||
-- b = begin == starting property value
|
||||
-- c = change == ending - beginning
|
||||
-- d = duration == running time. How much time has passed *right now*
|
||||
|
||||
local pow, sin, cos, pi, sqrt, abs, asin = math.pow, math.sin, math.cos, math.pi, math.sqrt, math.abs, math.asin
|
||||
|
||||
-- linear
|
||||
local function linear(t, b, c, d) return c * t / d + b end
|
||||
|
||||
-- quad
|
||||
local function inQuad(t, b, c, d) return c * pow(t / d, 2) + b end
|
||||
local function outQuad(t, b, c, d)
|
||||
t = t / d
|
||||
return -c * t * (t - 2) + b
|
||||
end
|
||||
local function inOutQuad(t, b, c, d)
|
||||
t = t / d * 2
|
||||
if t < 1 then return c / 2 * pow(t, 2) + b end
|
||||
return -c / 2 * ((t - 1) * (t - 3) - 1) + b
|
||||
end
|
||||
local function outInQuad(t, b, c, d)
|
||||
if t < d / 2 then return outQuad(t * 2, b, c / 2, d) end
|
||||
return inQuad((t * 2) - d, b + c / 2, c / 2, d)
|
||||
end
|
||||
|
||||
-- cubic
|
||||
local function inCubic (t, b, c, d) return c * pow(t / d, 3) + b end
|
||||
local function outCubic(t, b, c, d) return c * (pow(t / d - 1, 3) + 1) + b end
|
||||
local function inOutCubic(t, b, c, d)
|
||||
t = t / d * 2
|
||||
if t < 1 then return c / 2 * t * t * t + b end
|
||||
t = t - 2
|
||||
return c / 2 * (t * t * t + 2) + b
|
||||
end
|
||||
local function outInCubic(t, b, c, d)
|
||||
if t < d / 2 then return outCubic(t * 2, b, c / 2, d) end
|
||||
return inCubic((t * 2) - d, b + c / 2, c / 2, d)
|
||||
end
|
||||
|
||||
-- quart
|
||||
local function inQuart(t, b, c, d) return c * pow(t / d, 4) + b end
|
||||
local function outQuart(t, b, c, d) return -c * (pow(t / d - 1, 4) - 1) + b end
|
||||
local function inOutQuart(t, b, c, d)
|
||||
t = t / d * 2
|
||||
if t < 1 then return c / 2 * pow(t, 4) + b end
|
||||
return -c / 2 * (pow(t - 2, 4) - 2) + b
|
||||
end
|
||||
local function outInQuart(t, b, c, d)
|
||||
if t < d / 2 then return outQuart(t * 2, b, c / 2, d) end
|
||||
return inQuart((t * 2) - d, b + c / 2, c / 2, d)
|
||||
end
|
||||
|
||||
-- quint
|
||||
local function inQuint(t, b, c, d) return c * pow(t / d, 5) + b end
|
||||
local function outQuint(t, b, c, d) return c * (pow(t / d - 1, 5) + 1) + b end
|
||||
local function inOutQuint(t, b, c, d)
|
||||
t = t / d * 2
|
||||
if t < 1 then return c / 2 * pow(t, 5) + b end
|
||||
return c / 2 * (pow(t - 2, 5) + 2) + b
|
||||
end
|
||||
local function outInQuint(t, b, c, d)
|
||||
if t < d / 2 then return outQuint(t * 2, b, c / 2, d) end
|
||||
return inQuint((t * 2) - d, b + c / 2, c / 2, d)
|
||||
end
|
||||
|
||||
-- sine
|
||||
local function inSine(t, b, c, d) return -c * cos(t / d * (pi / 2)) + c + b end
|
||||
local function outSine(t, b, c, d) return c * sin(t / d * (pi / 2)) + b end
|
||||
local function inOutSine(t, b, c, d) return -c / 2 * (cos(pi * t / d) - 1) + b end
|
||||
local function outInSine(t, b, c, d)
|
||||
if t < d / 2 then return outSine(t * 2, b, c / 2, d) end
|
||||
return inSine((t * 2) -d, b + c / 2, c / 2, d)
|
||||
end
|
||||
|
||||
-- expo
|
||||
local function inExpo(t, b, c, d)
|
||||
if t == 0 then return b end
|
||||
return c * pow(2, 10 * (t / d - 1)) + b - c * 0.001
|
||||
end
|
||||
local function outExpo(t, b, c, d)
|
||||
if t == d then return b + c end
|
||||
return c * 1.001 * (-pow(2, -10 * t / d) + 1) + b
|
||||
end
|
||||
local function inOutExpo(t, b, c, d)
|
||||
if t == 0 then return b end
|
||||
if t == d then return b + c end
|
||||
t = t / d * 2
|
||||
if t < 1 then return c / 2 * pow(2, 10 * (t - 1)) + b - c * 0.0005 end
|
||||
return c / 2 * 1.0005 * (-pow(2, -10 * (t - 1)) + 2) + b
|
||||
end
|
||||
local function outInExpo(t, b, c, d)
|
||||
if t < d / 2 then return outExpo(t * 2, b, c / 2, d) end
|
||||
return inExpo((t * 2) - d, b + c / 2, c / 2, d)
|
||||
end
|
||||
|
||||
-- circ
|
||||
local function inCirc(t, b, c, d) return(-c * (sqrt(1 - pow(t / d, 2)) - 1) + b) end
|
||||
local function outCirc(t, b, c, d) return(c * sqrt(1 - pow(t / d - 1, 2)) + b) end
|
||||
local function inOutCirc(t, b, c, d)
|
||||
t = t / d * 2
|
||||
if t < 1 then return -c / 2 * (sqrt(1 - t * t) - 1) + b end
|
||||
t = t - 2
|
||||
return c / 2 * (sqrt(1 - t * t) + 1) + b
|
||||
end
|
||||
local function outInCirc(t, b, c, d)
|
||||
if t < d / 2 then return outCirc(t * 2, b, c / 2, d) end
|
||||
return inCirc((t * 2) - d, b + c / 2, c / 2, d)
|
||||
end
|
||||
|
||||
-- elastic
|
||||
local function calculatePAS(p,a,c,d)
|
||||
p, a = p or d * 0.3, a or 0
|
||||
if a < abs(c) then return p, c, p / 4 end -- p, a, s
|
||||
return p, a, p / (2 * pi) * asin(c/a) -- p,a,s
|
||||
end
|
||||
local function inElastic(t, b, c, d, a, p)
|
||||
local s
|
||||
if t == 0 then return b end
|
||||
t = t / d
|
||||
if t == 1 then return b + c end
|
||||
p,a,s = calculatePAS(p,a,c,d)
|
||||
t = t - 1
|
||||
return -(a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b
|
||||
end
|
||||
local function outElastic(t, b, c, d, a, p)
|
||||
local s
|
||||
if t == 0 then return b end
|
||||
t = t / d
|
||||
if t == 1 then return b + c end
|
||||
p,a,s = calculatePAS(p,a,c,d)
|
||||
return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) + c + b
|
||||
end
|
||||
local function inOutElastic(t, b, c, d, a, p)
|
||||
local s
|
||||
if t == 0 then return b end
|
||||
t = t / d * 2
|
||||
if t == 2 then return b + c end
|
||||
p,a,s = calculatePAS(p,a,c,d)
|
||||
t = t - 1
|
||||
if t < 0 then return -0.5 * (a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b end
|
||||
return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p ) * 0.5 + c + b
|
||||
end
|
||||
local function outInElastic(t, b, c, d, a, p)
|
||||
if t < d / 2 then return outElastic(t * 2, b, c / 2, d, a, p) end
|
||||
return inElastic((t * 2) - d, b + c / 2, c / 2, d, a, p)
|
||||
end
|
||||
|
||||
-- back
|
||||
local function inBack(t, b, c, d, s)
|
||||
s = s or 1.70158
|
||||
t = t / d
|
||||
return c * t * t * ((s + 1) * t - s) + b
|
||||
end
|
||||
local function outBack(t, b, c, d, s)
|
||||
s = s or 1.70158
|
||||
t = t / d - 1
|
||||
return c * (t * t * ((s + 1) * t + s) + 1) + b
|
||||
end
|
||||
local function inOutBack(t, b, c, d, s)
|
||||
s = (s or 1.70158) * 1.525
|
||||
t = t / d * 2
|
||||
if t < 1 then return c / 2 * (t * t * ((s + 1) * t - s)) + b end
|
||||
t = t - 2
|
||||
return c / 2 * (t * t * ((s + 1) * t + s) + 2) + b
|
||||
end
|
||||
local function outInBack(t, b, c, d, s)
|
||||
if t < d / 2 then return outBack(t * 2, b, c / 2, d, s) end
|
||||
return inBack((t * 2) - d, b + c / 2, c / 2, d, s)
|
||||
end
|
||||
|
||||
-- bounce
|
||||
local function outBounce(t, b, c, d)
|
||||
t = t / d
|
||||
if t < 1 / 2.75 then return c * (7.5625 * t * t) + b end
|
||||
if t < 2 / 2.75 then
|
||||
t = t - (1.5 / 2.75)
|
||||
return c * (7.5625 * t * t + 0.75) + b
|
||||
elseif t < 2.5 / 2.75 then
|
||||
t = t - (2.25 / 2.75)
|
||||
return c * (7.5625 * t * t + 0.9375) + b
|
||||
end
|
||||
t = t - (2.625 / 2.75)
|
||||
return c * (7.5625 * t * t + 0.984375) + b
|
||||
end
|
||||
local function inBounce(t, b, c, d) return c - outBounce(d - t, 0, c, d) + b end
|
||||
local function inOutBounce(t, b, c, d)
|
||||
if t < d / 2 then return inBounce(t * 2, 0, c, d) * 0.5 + b end
|
||||
return outBounce(t * 2 - d, 0, c, d) * 0.5 + c * .5 + b
|
||||
end
|
||||
local function outInBounce(t, b, c, d)
|
||||
if t < d / 2 then return outBounce(t * 2, b, c / 2, d) end
|
||||
return inBounce((t * 2) - d, b + c / 2, c / 2, d)
|
||||
end
|
||||
|
||||
tween.easing = {
|
||||
linear = linear,
|
||||
inQuad = inQuad, outQuad = outQuad, inOutQuad = inOutQuad, outInQuad = outInQuad,
|
||||
inCubic = inCubic, outCubic = outCubic, inOutCubic = inOutCubic, outInCubic = outInCubic,
|
||||
inQuart = inQuart, outQuart = outQuart, inOutQuart = inOutQuart, outInQuart = outInQuart,
|
||||
inQuint = inQuint, outQuint = outQuint, inOutQuint = inOutQuint, outInQuint = outInQuint,
|
||||
inSine = inSine, outSine = outSine, inOutSine = inOutSine, outInSine = outInSine,
|
||||
inExpo = inExpo, outExpo = outExpo, inOutExpo = inOutExpo, outInExpo = outInExpo,
|
||||
inCirc = inCirc, outCirc = outCirc, inOutCirc = inOutCirc, outInCirc = outInCirc,
|
||||
inElastic = inElastic, outElastic = outElastic, inOutElastic = inOutElastic, outInElastic = outInElastic,
|
||||
inBack = inBack, outBack = outBack, inOutBack = inOutBack, outInBack = outInBack,
|
||||
inBounce = inBounce, outBounce = outBounce, inOutBounce = inOutBounce, outInBounce = outInBounce
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- private stuff
|
||||
|
||||
local function copyTables(destination, keysTable, valuesTable)
|
||||
valuesTable = valuesTable or keysTable
|
||||
local mt = getmetatable(keysTable)
|
||||
if mt and getmetatable(destination) == nil then
|
||||
setmetatable(destination, mt)
|
||||
end
|
||||
for k,v in pairs(keysTable) do
|
||||
if type(v) == 'table' then
|
||||
destination[k] = copyTables({}, v, valuesTable[k])
|
||||
else
|
||||
destination[k] = valuesTable[k]
|
||||
end
|
||||
end
|
||||
return destination
|
||||
end
|
||||
|
||||
local function checkSubjectAndTargetRecursively(subject, target, path)
|
||||
path = path or {}
|
||||
local targetType, newPath
|
||||
for k,targetValue in pairs(target) do
|
||||
targetType, newPath = type(targetValue), copyTables({}, path)
|
||||
table.insert(newPath, tostring(k))
|
||||
if targetType == 'number' then
|
||||
assert(type(subject[k]) == 'number', "Parameter '" .. table.concat(newPath,'/') .. "' is missing from subject or isn't a number")
|
||||
elseif targetType == 'table' then
|
||||
checkSubjectAndTargetRecursively(subject[k], targetValue, newPath)
|
||||
else
|
||||
assert(targetType == 'number', "Parameter '" .. table.concat(newPath,'/') .. "' must be a number or table of numbers")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function checkNewParams(duration, subject, target, easing)
|
||||
assert(type(duration) == 'number' and duration > 0, "duration must be a positive number. Was " .. tostring(duration))
|
||||
local tsubject = type(subject)
|
||||
assert(tsubject == 'table' or tsubject == 'userdata', "subject must be a table or userdata. Was " .. tostring(subject))
|
||||
assert(type(target)== 'table', "target must be a table. Was " .. tostring(target))
|
||||
assert(type(easing)=='function', "easing must be a function. Was " .. tostring(easing))
|
||||
checkSubjectAndTargetRecursively(subject, target)
|
||||
end
|
||||
|
||||
local function getEasingFunction(easing)
|
||||
easing = easing or "linear"
|
||||
if type(easing) == 'string' then
|
||||
local name = easing
|
||||
easing = tween.easing[name]
|
||||
if type(easing) ~= 'function' then
|
||||
error("The easing function name '" .. name .. "' is invalid")
|
||||
end
|
||||
end
|
||||
return easing
|
||||
end
|
||||
|
||||
local function performEasingOnSubject(subject, target, initial, clock, duration, easing)
|
||||
local t,b,c,d
|
||||
for k,v in pairs(target) do
|
||||
if type(v) == 'table' then
|
||||
performEasingOnSubject(subject[k], v, initial[k], clock, duration, easing)
|
||||
else
|
||||
t,b,c,d = clock, initial[k], v - initial[k], duration
|
||||
subject[k] = easing(t,b,c,d)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Tween methods
|
||||
|
||||
local Tween = {}
|
||||
local Tween_mt = {__index = Tween}
|
||||
|
||||
function Tween:set(clock)
|
||||
assert(type(clock) == 'number', "clock must be a positive number or 0")
|
||||
|
||||
self.initial = self.initial or copyTables({}, self.target, self.subject)
|
||||
self.clock = clock
|
||||
|
||||
if self.clock <= 0 then
|
||||
|
||||
self.clock = 0
|
||||
copyTables(self.subject, self.initial)
|
||||
|
||||
elseif self.clock >= self.duration then -- the tween has expired
|
||||
|
||||
self.clock = self.duration
|
||||
copyTables(self.subject, self.target)
|
||||
|
||||
else
|
||||
|
||||
performEasingOnSubject(self.subject, self.target, self.initial, self.clock, self.duration, self.easing)
|
||||
|
||||
end
|
||||
|
||||
return self.clock >= self.duration
|
||||
end
|
||||
|
||||
function Tween:reset()
|
||||
return self:set(0)
|
||||
end
|
||||
|
||||
function Tween:update(dt)
|
||||
assert(type(dt) == 'number', "dt must be a number")
|
||||
return self:set(self.clock + dt)
|
||||
end
|
||||
|
||||
|
||||
-- Public interface
|
||||
|
||||
function tween.new(duration, subject, target, easing)
|
||||
easing = getEasingFunction(easing)
|
||||
checkNewParams(duration, subject, target, easing)
|
||||
return setmetatable({
|
||||
duration = duration,
|
||||
subject = subject,
|
||||
target = target,
|
||||
easing = easing,
|
||||
clock = 0
|
||||
}, Tween_mt)
|
||||
end
|
||||
|
||||
return tween
|
||||
823
sys/modules/opus/util.lua
Normal file
823
sys/modules/opus/util.lua
Normal file
@@ -0,0 +1,823 @@
|
||||
local Util = { }
|
||||
|
||||
local fs = _G.fs
|
||||
local http = _G.http
|
||||
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
|
||||
local ret = f(...)
|
||||
if ret then
|
||||
return ret
|
||||
end
|
||||
until os.clock()-c >= timeout
|
||||
end
|
||||
|
||||
function Util.tryTimes(attempts, f, ...)
|
||||
local result
|
||||
for _ = 1, attempts do
|
||||
result = { f(...) }
|
||||
if result[1] then
|
||||
return table.unpack(result)
|
||||
end
|
||||
end
|
||||
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 = .295
|
||||
return function(...)
|
||||
local nts = os.clock()
|
||||
if nts > ts + timeout then
|
||||
os.sleep(0)
|
||||
ts = os.clock()
|
||||
if fn then
|
||||
fn(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Util.tostring(pattern, ...)
|
||||
|
||||
local function serialize(tbl, width)
|
||||
local str = '{\n'
|
||||
for k, v in pairs(tbl) do
|
||||
local value
|
||||
if type(v) == 'table' then
|
||||
value = _sformat('table: %d', Util.size(v))
|
||||
else
|
||||
value = tostring(v)
|
||||
end
|
||||
str = str .. _sformat(' %s: %s\n', k, value)
|
||||
end
|
||||
--if #str < width then
|
||||
--str = str:gsub('\n', '') .. ' }'
|
||||
--else
|
||||
str = str .. '}'
|
||||
--end
|
||||
return str
|
||||
end
|
||||
|
||||
if type(pattern) == 'string' then
|
||||
if select('#', ...) == 0 then
|
||||
return pattern
|
||||
end
|
||||
return _sformat(pattern, ...)
|
||||
elseif type(pattern) == 'table' then
|
||||
return serialize(pattern, term.current().getSize())
|
||||
end
|
||||
return tostring(pattern)
|
||||
end
|
||||
|
||||
function Util.print(pattern, ...)
|
||||
print(Util.tostring(pattern, ...))
|
||||
end
|
||||
|
||||
function Util.getVersion()
|
||||
local version
|
||||
|
||||
if _G._CC_VERSION then
|
||||
version = tonumber(_G._CC_VERSION:match('[%d]+%.?[%d][%d]'))
|
||||
end
|
||||
if not version and _G._HOST then
|
||||
version = tonumber(_G._HOST:match('[%d]+%.?[%d][%d]'))
|
||||
end
|
||||
|
||||
return version or 1.7
|
||||
end
|
||||
|
||||
function Util.getMinecraftVersion()
|
||||
local mcVersion = _G._MC_VERSION or 'unknown'
|
||||
if _G._HOST then
|
||||
local version = _G._HOST:match('%S+ %S+ %((%S.+)%)')
|
||||
if version then
|
||||
mcVersion = version:match('Minecraft (%S+)') or version
|
||||
end
|
||||
end
|
||||
return mcVersion
|
||||
end
|
||||
|
||||
function Util.checkMinecraftVersion(minVersion)
|
||||
local version = Util.getMinecraftVersion()
|
||||
local function convert(v)
|
||||
local m1, m2, m3 = v:match('(%d)%.(%d)%.?(%d?)')
|
||||
return tonumber(m1) * 10000 + tonumber(m2) * 100 + (tonumber(m3) or 0)
|
||||
end
|
||||
|
||||
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 Util.signum(num) * math.floor(math.abs(num) * mult + 0.5) / mult
|
||||
end
|
||||
|
||||
function Util.randomFloat(max, min)
|
||||
min = min or 0
|
||||
max = max or 1
|
||||
return (max-min) * math.random() + min
|
||||
end
|
||||
|
||||
--[[ Table functions ]] --
|
||||
function Util.clear(t)
|
||||
local keys = Util.keys(t)
|
||||
for _,k in pairs(keys) do
|
||||
t[k] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function Util.empty(t)
|
||||
return not next(t)
|
||||
end
|
||||
|
||||
function Util.key(t, value)
|
||||
for k,v in pairs(t) do
|
||||
if v == value then
|
||||
return k
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Util.keys(t)
|
||||
local keys = { }
|
||||
for k in pairs(t) do
|
||||
keys[#keys+1] = k
|
||||
end
|
||||
return keys
|
||||
end
|
||||
|
||||
function Util.merge(obj, args)
|
||||
if args then
|
||||
for k,v in pairs(args) do
|
||||
obj[k] = v
|
||||
end
|
||||
end
|
||||
return obj
|
||||
end
|
||||
|
||||
function Util.deepMerge(obj, args)
|
||||
if args then
|
||||
for k,v in pairs(args) do
|
||||
if type(v) == 'table' then
|
||||
if not obj[k] then
|
||||
obj[k] = { }
|
||||
end
|
||||
Util.deepMerge(obj[k], v)
|
||||
else
|
||||
obj[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Util.transpose(t)
|
||||
local tt = { }
|
||||
for k,v in pairs(t) do
|
||||
tt[v] = k
|
||||
end
|
||||
return tt
|
||||
end
|
||||
|
||||
function Util.contains(t, value)
|
||||
for k,v in pairs(t) do
|
||||
if v == value then
|
||||
return k
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Util.find(t, name, value)
|
||||
for k,v in pairs(t) do
|
||||
if v[name] == value then
|
||||
return v, k
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Util.findAll(t, name, value)
|
||||
local rt = { }
|
||||
for _,v in pairs(t) do
|
||||
if v[name] == value then
|
||||
table.insert(rt, v)
|
||||
end
|
||||
end
|
||||
return rt
|
||||
end
|
||||
|
||||
function Util.shallowCopy(t)
|
||||
if not t then error('Util.shallowCopy: invalid table', 2) end
|
||||
local t2 = { }
|
||||
for k,v in pairs(t) do
|
||||
t2[k] = v
|
||||
end
|
||||
return t2
|
||||
end
|
||||
|
||||
function Util.deepCopy(t)
|
||||
if type(t) ~= 'table' then
|
||||
return t
|
||||
end
|
||||
--local mt = getmetatable(t)
|
||||
local res = {}
|
||||
for k,v in pairs(t) do
|
||||
if type(v) == 'table' then
|
||||
v = Util.deepCopy(v)
|
||||
end
|
||||
res[k] = v
|
||||
end
|
||||
--setmetatable(res,mt)
|
||||
return res
|
||||
end
|
||||
|
||||
-- http://snippets.luacode.org/?p=snippets/Filter_a_table_in-place_119
|
||||
function Util.filterInplace(t, predicate)
|
||||
local j = 1
|
||||
|
||||
for i = 1,#t do
|
||||
local v = t[i]
|
||||
if predicate(v) then
|
||||
t[j] = v
|
||||
j = j + 1
|
||||
end
|
||||
end
|
||||
|
||||
while t[j] ~= nil do
|
||||
t[j] = nil
|
||||
j = j + 1
|
||||
end
|
||||
|
||||
return t
|
||||
end
|
||||
|
||||
function Util.filter(it, f)
|
||||
local ot = { }
|
||||
for k,v in pairs(it) do
|
||||
if f(v) then
|
||||
ot[k] = v
|
||||
end
|
||||
end
|
||||
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
|
||||
for _ in pairs(list) do
|
||||
length = length + 1
|
||||
end
|
||||
return length
|
||||
end
|
||||
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
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
function Util.rpairs(t)
|
||||
local tkeys = Util.keys(t)
|
||||
local i = #tkeys
|
||||
return function()
|
||||
local key = tkeys[i]
|
||||
local k,v = key, t[key]
|
||||
i = i - 1
|
||||
if v then
|
||||
return k, v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- http://stackoverflow.com/questions/15706270/sort-a-table-in-lua
|
||||
function Util.spairs(t, order)
|
||||
local keys = Util.keys(t)
|
||||
|
||||
-- if order function given, sort by it by passing the table and keys a, b,
|
||||
-- otherwise just sort the keys
|
||||
if order then
|
||||
table.sort(keys, function(a,b) return order(t[a], t[b]) end)
|
||||
else
|
||||
table.sort(keys)
|
||||
end
|
||||
|
||||
-- return the iterator function
|
||||
local i = 0
|
||||
return function()
|
||||
i = i + 1
|
||||
if keys[i] then
|
||||
return keys[i], t[keys[i]]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Util.first(t, order)
|
||||
local keys = Util.keys(t)
|
||||
if order then
|
||||
table.sort(keys, function(a,b) return order(t[a], t[b]) end)
|
||||
else
|
||||
table.sort(keys)
|
||||
end
|
||||
return keys[1], t[keys[1]]
|
||||
end
|
||||
|
||||
--[[ File functions ]]--
|
||||
function Util.readFile(fname, flags)
|
||||
local f = fs.open(fname, flags or "r")
|
||||
if f then
|
||||
local t = f.readAll()
|
||||
f.close()
|
||||
return t
|
||||
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)
|
||||
end
|
||||
file:write(data)
|
||||
file:close()
|
||||
end
|
||||
|
||||
function Util.readLines(fname)
|
||||
local file = fs.open(fname, "r")
|
||||
if file then
|
||||
local t = {}
|
||||
local line = file.readLine()
|
||||
while line do
|
||||
table.insert(t, line)
|
||||
line = file.readLine()
|
||||
end
|
||||
file.close()
|
||||
return t
|
||||
end
|
||||
end
|
||||
|
||||
function Util.writeLines(fname, lines)
|
||||
local file = fs.open(fname, 'w')
|
||||
if file then
|
||||
for _,line in ipairs(lines) do
|
||||
file.writeLine(line)
|
||||
end
|
||||
file.close()
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function Util.readTable(fname)
|
||||
local t = Util.readFile(fname)
|
||||
if t then
|
||||
return textutils.unserialize(t)
|
||||
end
|
||||
end
|
||||
|
||||
function Util.writeTable(fname, data)
|
||||
Util.writeFile(fname, textutils.serialize(data))
|
||||
end
|
||||
|
||||
function Util.loadTable(fname)
|
||||
local fc = Util.readFile(fname)
|
||||
if not fc then
|
||||
return false, 'Unable to read file'
|
||||
end
|
||||
local s, m = loadstring('return ' .. fc, fname)
|
||||
if s then
|
||||
s, m = pcall(s)
|
||||
if s then
|
||||
return m
|
||||
end
|
||||
end
|
||||
return s, m
|
||||
end
|
||||
|
||||
--[[ loading and running functions ]] --
|
||||
function Util.httpGet(url, headers, isBinary)
|
||||
local h, msg = http.get(url, headers, isBinary)
|
||||
if h then
|
||||
local contents = h.readAll()
|
||||
h.close()
|
||||
return contents
|
||||
end
|
||||
return h, msg
|
||||
end
|
||||
|
||||
function Util.download(url, filename)
|
||||
local contents, msg = Util.httpGet(url)
|
||||
if not contents then
|
||||
error(_sformat('Failed to download %s\n%s', url, msg), 2)
|
||||
end
|
||||
|
||||
if filename then
|
||||
Util.writeFile(filename, contents)
|
||||
end
|
||||
return contents
|
||||
end
|
||||
|
||||
function Util.loadUrl(url, env) -- loadfile equivalent
|
||||
local c, msg = Util.httpGet(url)
|
||||
if not c then
|
||||
return c, msg
|
||||
end
|
||||
return load(c, url, nil, env)
|
||||
end
|
||||
|
||||
function Util.runUrl(env, url, ...) -- os.run equivalent
|
||||
setmetatable(env, { __index = _G })
|
||||
local fn, m = Util.loadUrl(url, env)
|
||||
if fn then
|
||||
return pcall(fn, ...)
|
||||
end
|
||||
return fn, m
|
||||
end
|
||||
|
||||
function Util.run(env, path, ...)
|
||||
if type(env) ~= 'table' then error('Util.run: env must be a table', 2) end
|
||||
setmetatable(env, { __index = _G })
|
||||
local fn, m = loadfile(path, env)
|
||||
if fn then
|
||||
return pcall(fn, ...)
|
||||
end
|
||||
return fn, m
|
||||
end
|
||||
|
||||
function Util.runFunction(env, fn, ...)
|
||||
setfenv(fn, env)
|
||||
setmetatable(env, { __index = _G })
|
||||
return pcall(fn, ...)
|
||||
end
|
||||
|
||||
--[[ String functions ]] --
|
||||
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 _sformat('%sM', math.floor(n/1000000 * 10) / 10)
|
||||
elseif n >= 10000 or n <= -10000 then
|
||||
return _sformat('%sK', math.floor(n/1000))
|
||||
elseif n >= 1000 or n <= -1000 then
|
||||
return _sformat('%sK', math.floor(n/1000 * 10) / 10)
|
||||
end
|
||||
return tostring(n)
|
||||
end
|
||||
|
||||
function Util.insertString(str, istr, pos)
|
||||
return str:sub(1, pos - 1) .. istr .. str:sub(pos)
|
||||
end
|
||||
|
||||
function Util.split(str, pattern)
|
||||
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
|
||||
helper((str:gsub(pattern, helper)))
|
||||
return t
|
||||
end
|
||||
|
||||
function Util.matches(str, pattern)
|
||||
pattern = pattern or '%S+'
|
||||
local t = { }
|
||||
for s in str:gmatch(pattern) do
|
||||
table.insert(t, s)
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
function Util.startsWith(s, match)
|
||||
return _ssub(s, 1, #match) == match
|
||||
end
|
||||
|
||||
-- return a fixed length string using specified alignment
|
||||
function Util.widthify(s, len, align)
|
||||
s = s or ''
|
||||
local slen = #s
|
||||
|
||||
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 .. _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)')
|
||||
end
|
||||
|
||||
-- trim whitespace from left end of string
|
||||
function Util.triml(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)')
|
||||
end
|
||||
-- end http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76
|
||||
|
||||
-- word wrapping based on:
|
||||
-- https://www.rosettacode.org/wiki/Word_wrap#Lua and
|
||||
-- http://lua-users.org/wiki/StringRecipes
|
||||
local function paragraphwrap(text, linewidth, res)
|
||||
linewidth = linewidth or 75
|
||||
local spaceleft = linewidth
|
||||
local line = { }
|
||||
|
||||
for word in text:gmatch("%S+") do
|
||||
local len = #word + 1
|
||||
|
||||
--if colorMode then
|
||||
-- word:gsub('()@([@%d])', function(pos, c) len = len - 2 end)
|
||||
--end
|
||||
|
||||
if len > spaceleft then
|
||||
table.insert(res, table.concat(line, ' '))
|
||||
line = { word }
|
||||
spaceleft = linewidth - len - 1
|
||||
else
|
||||
table.insert(line, word)
|
||||
spaceleft = spaceleft - len
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(res, table.concat(line, ' '))
|
||||
return table.concat(res, '\n')
|
||||
end
|
||||
-- end word wrapping
|
||||
|
||||
function Util.wordWrap(str, limit)
|
||||
local longLines = Util.split(str)
|
||||
local lines = { }
|
||||
|
||||
for _,line in ipairs(longLines) do
|
||||
paragraphwrap(line, limit, lines)
|
||||
end
|
||||
|
||||
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 _ssub(v, 1, 1) == '-' then
|
||||
local opt = _ssub(v, 2)
|
||||
options[opt] = arg[k + 1]
|
||||
k = k + 1
|
||||
else
|
||||
table.insert(args, v)
|
||||
end
|
||||
k = k + 1
|
||||
end
|
||||
return options, args
|
||||
end
|
||||
|
||||
-- http://lua-users.org/wiki/AlternativeGetOpt
|
||||
local function getopt( arg, options )
|
||||
local tab = {}
|
||||
for k, v in ipairs(arg) do
|
||||
if type(v) == 'string' then
|
||||
if _ssub( v, 1, 2) == "--" then
|
||||
local x = string.find( v, "=", 1, true )
|
||||
if x then tab[ _ssub( v, 3, x-1 ) ] = _ssub( v, x+1 )
|
||||
else tab[ _ssub( v, 3 ) ] = true
|
||||
end
|
||||
elseif _ssub( v, 1, 1 ) == "-" then
|
||||
local y = 2
|
||||
local l = string.len(v)
|
||||
local jopt
|
||||
while ( y <= l ) do
|
||||
jopt = _ssub( v, y, y )
|
||||
if string.find( options, jopt, 1, true ) then
|
||||
if y < l then
|
||||
tab[ jopt ] = _ssub( v, y+1 )
|
||||
y = l
|
||||
else
|
||||
tab[ jopt ] = arg[ k + 1 ]
|
||||
end
|
||||
else
|
||||
tab[ jopt ] = true
|
||||
end
|
||||
y = y + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return tab
|
||||
end
|
||||
|
||||
function Util.showOptions(options)
|
||||
print('Arguments: ')
|
||||
for _, v in pairs(options) do
|
||||
print(_sformat('-%s %s', v.arg, v.desc))
|
||||
end
|
||||
end
|
||||
|
||||
function Util.getOptions(options, args, ignoreInvalid)
|
||||
local argLetters = ''
|
||||
for _,o in pairs(options) do
|
||||
if o.type ~= 'flag' then
|
||||
argLetters = argLetters .. o.arg
|
||||
end
|
||||
end
|
||||
local rawOptions = getopt(args, argLetters)
|
||||
|
||||
for k,ro in pairs(rawOptions) do
|
||||
local found = false
|
||||
for _,o in pairs(options) do
|
||||
if o.arg == k then
|
||||
found = true
|
||||
if o.type == 'number' then
|
||||
o.value = tonumber(ro)
|
||||
elseif o.type == 'help' then
|
||||
Util.showOptions(options)
|
||||
return false
|
||||
else
|
||||
o.value = ro
|
||||
end
|
||||
end
|
||||
end
|
||||
if not found and not ignoreInvalid then
|
||||
print('Invalid argument')
|
||||
Util.showOptions(options)
|
||||
return false
|
||||
end
|
||||
end
|
||||
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
|
||||
Reference in New Issue
Block a user