Merge branch 'develop-1.8' into master-1.8

This commit is contained in:
kepler155c@gmail.com
2019-11-10 18:51:51 -07:00
192 changed files with 14776 additions and 11766 deletions

View 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
View 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

View 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

View 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
View 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;
}

View 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

View 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

View 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,
}

View 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,
}

View 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,
}

View 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,
}

View 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,
}

View 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
View 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
View 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

View 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

View 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

View 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

View 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

View 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
View 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
View 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

View 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

View 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,
}

View 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
View 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
View 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
View 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
View 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

View 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

View 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
View 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

View 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
View 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

View 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
View 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

View 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
View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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,
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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,
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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,
}

View 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

View 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

View 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
View 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