feat: 初始提交 LeonOS 实现

添加 LeonOS 的基本实现,包括:
- 核心 API 模块(colors, disk, gps, keys, multishell, parallel, rednet, redstone, settings, vector)
- 命令行程序(about, alias, bg, clear, copy, delete, edit, fg, help, list, lua, mkdir, move, paint, peripherals, programs, reboot, set, shutdown, threads)
- 系统启动脚本和包管理
- 文档(README.md, LICENSE)
- 开发工具(devbin)和更新程序

实现功能:
- 完整的线程管理系统
- 兼容 ComputerCraft 的 API 设计
- 改进的 shell 和命令补全系统
- 多标签终端支持
- 设置管理系统
This commit is contained in:
2025-08-31 16:54:18 +08:00
commit 90a901f58e
94 changed files with 8372 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
-- rc.colors
local term = require("term")
local bit32 = require("bit32")
local expect = require("cc.expect")
local colors = {
white = 0x1,
orange = 0x2,
magenta = 0x4,
lightBlue = 0x8,
yellow = 0x10,
lime = 0x20,
pink = 0x40,
gray = 0x80,
grey = 0x80,
lightGray = 0x100,
lightGrey = 0x100,
cyan = 0x200,
purple = 0x400,
blue = 0x800,
brown = 0x1000,
green = 0x2000,
red = 0x4000,
black = 0x8000,
}
local defaults = {
0xf0f0f0, 0xf2b233, 0xe57fd8, 0x99b2f2,
0xdede6c, 0x7fcc19, 0xf2b2cc, 0x4c4c4c,
0x999999, 0x4c99b2, 0xb266e5, 0x3366cc,
0x7f664c, 0x57a64e, 0xcc4c4c, 0x111111
}
for i=1, #defaults, 1 do
term.setPaletteColor(2^(i-1), defaults[i])
end
function colors.combine(...)
local ret = 0
local cols = {...}
for i=1, #cols, 1 do
expect.expect(i, cols[i], "number")
ret = bit32.bor(ret, cols[i])
end
return ret
end
function colors.subtract(cols, ...)
expect.expect(1, cols, "number")
local subt = {...}
for i=1, #subt, 1 do
expect.expect(i+1, subt[i], "number")
cols = bit32.band(cols, bit32.bnot(subt[i]))
end
return cols
end
colors.test = bit32.btest
function colors.packRGB(r, g, b)
expect.expect(1, r, "number")
if r > 1 then return r end
expect.range(r, 0, 1)
expect.range(expect.expect(2, g, "number"), 0, 1)
expect.range(expect.expect(3, b, "number"), 0, 1)
return (r * 255 * 0x10000) + (g * 255 * 0x100) + (b * 255)
end
function colors.unpackRGB(rgb)
expect.range(expect.expect(1, rgb, "number"), 0, 0xFFFFFF)
return bit32.rshift(rgb, 16) / 255,
bit32.rshift(bit32.band(rgb, 0xFF00), 8) / 255,
bit32.band(rgb, 0xFF) / 255
end
function colors.toBlit(color)
expect.expect(1, color, "number")
return string.format("%x", math.floor(math.log(color, 2)))
end
return colors

View File

@@ -0,0 +1 @@
return require("colors")

View File

@@ -0,0 +1,35 @@
-- rc.disk
local p = require("peripheral")
local disk = {}
local function wrap(method)
return function(name, ...)
if not p.isPresent(name) then
return nil
end
return p.call(name, method, ...)
end
end
local methods = {
isPresent = "isDiskPresent",
getLabel = "getDiskLabel",
setLabel = "setDiskLabel",
hasData = false,
getMountPath = false,
hasAudio = false,
getAudioTitle = false,
playAudio = false,
stopAudio = false,
eject = "ejectDisk",
getID = "getDiskID"
}
for k, v in pairs(methods) do
disk[k] = wrap(v or k)
end
return disk

View File

@@ -0,0 +1,18 @@
-- rc.gps
error("gps is not fully implemented", 0)
local expect = require("cc.expect").expect
local rednet = require("rednet")
local gps = {}
gps.CHANNEL_GPS = 65534
function gps.locate(timeout, debug)
timeout = expect(1, timeout, "number", "nil") or 2
expect(2, debug, "boolean", "nil")
rednet.broadcast()
end
return gps

View File

@@ -0,0 +1,102 @@
-- rc.help
local fs = require("fs")
local thread = require("rc.thread")
local expect = require("cc.expect").expect
local completion = require("cc.completion")
local help = {}
help._DEFAULT_PATH = "/rc/help"
function help.init()
local vars = thread.vars()
vars.help = vars.help or help._DEFAULT_PATH
end
function help.path()
return thread.vars().help or help._DEFAULT_PATH
end
function help.setPath(new)
expect(1, new, "string")
thread.vars().help = new
end
function help.lookup(topic)
expect(1, topic, "string")
topic = topic
for directory in help.path():gmatch("[^:]+") do
local try = fs.combine(directory, topic .. ".hlp")
if fs.exists(try) then
return try
end
end
end
function help.topics()
local topics = {}
for directory in help.path():gmatch("[^:]+") do
local _topics = fs.list(directory)
for i=1, #_topics, 1 do
topics[#topics+1] = _topics[i]:gsub("%.hlp$", "")
end
end
return topics
end
function help.completeTopic(prefix)
local topics = help.topics()
table.sort(topics, function(a, b) return #a < #b end)
return completion.choice(prefix, topics)
end
local directives = {
color = function(c)
return require("colors")[c or "white"]
end,
["break"] = function()
return "\n"
end
}
function help.loadTopic(name)
local path = help.lookup(name)
if not path then return end
local handle = io.open(path, "r")
local data = {}
local lastWasText = false
for line in handle:lines() do
if line:sub(1,2) == ">>" then
lastWasText = false
local words = {}
for word in line:sub(3):gmatch("[^ ]+") do
words[#words+1] = word
end
if directives[words[1]] then
data[#data+1] = directives[words[1]](table.unpack(words, 2))
end
else
if lastWasText then
data[#data+1] = "\n"
end
lastWasText = true
data[#data+1] = line
end
end
handle:close()
return data
end
return help

View File

@@ -0,0 +1,29 @@
-- rc.keys
local expect = require("cc.expect").expect
-- automatic keymap detection :)
-- this uses a fair bit of magic
local kmap = "lwjgl3"
local mcver = tonumber(_HOST:match("%b()"):sub(2,-2):match("1%.(%d+)")) or 0
if _HOST:match("CCEmuX") then
-- use the 1.16.5 keymap
kmap = "lwjgl3"
elseif mcver <= 12 or _HOST:match("LeonOS%-PC") then
-- use the 1.12.2 keymap
kmap = "lwjgl2"
end
local base = dofile("/rc/keymaps/"..kmap..".lua")
local lib = {}
-- reverse-index it!
for k, v in pairs(base) do lib[k] = v; lib[v] = k end
lib["return"] = lib.enter
function lib.getName(code)
expect(1, code, "number")
return lib[code]
end
return lib

View File

@@ -0,0 +1,31 @@
-- multishell API
local thread = require("rc.thread")
local ms = {}
ms.getFocus = thread.getFocusedTab
ms.setFocus = thread.setFocusedTab
ms.getCurrent = thread.getCurrentTab
ms.setTitle = function() end
ms.getTitle = function() return "???" end
ms.getCount = function() return #thread.info() end
function ms.launch(...)
local env = _G
local args = table.pack(...)
if type(args[1]) == "table" then
env = table.remove(args, 1)
end
local function func()
return assert(loadfile(args[1], "bt", env))(table.unpack(args, 2, args.n))
end
return (thread.launchTab(func, args[1]:sub(-8)))
end
return ms

View File

@@ -0,0 +1,158 @@
-- rc.paintutils
local term = require("term")
local expect = require("cc.expect").expect
local textutils = require("textutils")
local p = {}
function p.parseImage(str)
expect(1, str, "string")
return textutils.unserialize(str)
end
function p.loadImage(path)
expect(1, path, "string")
local handle = io.open(path)
if handle then
local data = handle:read("a")
handle:close()
return p.parseImage(data)
end
end
function p.drawPixel(x, y, color)
expect(1, x, "number")
expect(2, y, "number")
expect(2, color, "number", "nil")
if color then term.setBackgroundColor(color) end
term.at(x, y).write(" ")
end
local function drawSteep(x0, y0, x1, y1, color)
local distX = x1 - x0
local distY = y1 - y0
local diff = 2*distX - distY
local x = x0
for y=y0, y1, 1 do
p.drawPixel(x, y, color)
if diff > 0 then
x = x + 1
diff = diff + 2 * (distX - distY)
else
diff = diff + 2*distX
end
end
end
local function drawShallow(x0, y0, x1, y1, color)
local distX, distY = x1 - x0, y1 - y0
local diff = 2*distY - distX
local y = y0
for x=x0, x1, 1 do
p.drawPixel(x, y, color)
if diff > 0 then
y = y + 1
diff = diff - 2*distX
end
diff = diff + 2*distY
end
end
function p.drawLine(_startX, _startY, _endX, _endY, color)
expect(1, _startX, "number")
expect(2, _startY, "number")
expect(3, _endX, "number")
expect(4, _endY, "number")
expect(5, color, "number")
local startX, startY, endX, endY =
math.min(_startX, _endX), math.min(_startY, _endY),
math.max(_startX, _endX), math.max(_startY, _endY)
if startX == endX and startY == endY then
return p.drawPixel(startX, startY, color)
elseif startX == endX then
if color then term.setBackgroundColor(color) end
for y=startY, endY, 1 do
term.at(startX, y).write(" ")
end
elseif startY == endY then
if color then term.setBackgroundColor(color) end
term.at(startX, startY).write((" "):rep(endX - startX))
end
if (endY - startY) < (endX - startX) then
drawShallow(startX, startY, endX, endY)
else
drawSteep(startX, startY, endX, endY)
end
end
function p.drawBox(startX, startY, endX, endY, color)
expect(1, startX, "number")
expect(2, startY, "number")
expect(3, endX, "number")
expect(4, endY, "number")
expect(5, color, "number")
local col = string.format("%x", math.floor(math.log(color, 2)))
local ht, hc = (" "):rep(endX-startX+1), col:rep(endX-startX+1)
term.at(startX, startY).blit(ht, hc, hc)
for i=startY, endY do
term.at(startX, i).blit(" ", col, col)
term.at(endX, i).blit(" ", col, col)
end
term.at(startX, endY).blit(ht, hc, hc)
end
function p.drawFilledBox(startX, startY, endX, endY, color)
expect(1, startX, "number")
expect(2, startY, "number")
expect(3, endX, "number")
expect(4, endY, "number")
expect(5, color, "number")
local col = string.format("%x", math.floor(math.log(color, 2)))
local ht, hc = (" "):rep(endX-startX+1), col:rep(endX-startX+1)
for y=startY, endY, 1 do
term.at(startX, y).blit(ht, hc, hc)
end
end
function p.drawImage(img, x, y, frame)
expect(1, img, "table")
expect(2, x, "number")
expect(3, y, "number")
expect(4, frame, "number", "nil")
frame = frame or 1
if not img[frame] then
return nil, "invalid frame index " .. frame
end
if img.palette then
for k, v in pairs(img.palette) do
term.setPaletteColor(k, table.unpack(v))
end
end
if img[frame].palette then
for k, v in pairs(img[frame].palette) do
term.setPaletteColor(k, table.unpack(v))
end
end
for i, line in ipairs(img[frame]) do
term.at(x+i-1, y).blit(table.unpack(line))
end
return true
end
return p

View File

@@ -0,0 +1,54 @@
-- 'parallel' implementation
-- uses LeonOS's native threading
local parallel = {}
local thread = require("rc.thread")
local expect = require("cc.expect").expect
local function rand_id()
local id = "parallel-"
for _=1, 8, 1 do
id = id .. string.char(math.random(33, 126))
end
return id
end
local function waitForN(num, ...)
local funcs = table.pack(...)
local threads = {}
for i=1, #funcs, 1 do
expect(i, funcs[i], "function")
end
for i=1, #funcs, 1 do
threads[i] = thread.spawn(funcs[i], rand_id())
end
local dead = 0
repeat
coroutine.yield()
for i=#threads, 1, -1 do
if not thread.exists(threads[i]) then
table.remove(threads, i)
dead = dead + 1
end
end
until dead >= num
-- clean up excess
for i=1, #threads, 1 do
thread.remove(threads[i])
end
end
function parallel.waitForAny(...)
return waitForN(1, ...)
end
function parallel.waitForAll(...)
return waitForN(select("#", ...), ...)
end
return parallel

View File

@@ -0,0 +1,113 @@
-- rc.rednet
local expect = require("cc.expect").expect
local peripheral = require("peripheral")
local rednet = {
CHANNEL_BROADCAST = 65535,
CHANNEL_REPEAT = 65533,
MAX_ID_CHANNELS = 65500,
}
local opened = {}
function rednet.open(modem)
expect(1, modem, "string")
peripheral.call(modem, "open", os.computerID())
peripheral.call(modem, "open", rednet.CHANNEL_BROADCAST)
opened[modem] = true
end
local function call(method, modem, erase, passids, ...)
local ret = false
if modem then
if erase then opened[modem] = false end
if passids then
ret = ret or peripheral.call(modem, method, os.computerID(), ...)
ret = ret or peripheral.call(modem, method, rednet.CHANNEL_BROADCAST, ...)
else
ret = peripheral.call(modem, method, ...)
end
else
for k in pairs(opened) do
ret = ret or call(k, method, erase, passids, ...)
end
end
return ret
end
function rednet.close(modem)
expect(1, modem, "string", "nil")
return call("close", modem, true, true)
end
function rednet.isOpen(modem)
expect(1, modem, "string", "nil")
return call("isOpen", modem, false, true)
end
function rednet.send(to, message, protocol)
expect(1, to, "number")
expect(2, message, "string", "table", "number", "boolean")
expect(3, protocol, "string", "nil")
if type(message) == "table" then
if protocol then table.insert(message, 1, protocol) end
table.insert(message, 1, "rednet_message")
else
message = {"rednet_message", to, message, protocol}
end
call("transmit", nil, false, false, rednet.CHANNEL_BROADCAST,
os.computerID(), message)
return rednet.isOpen()
end
function rednet.broadcast(message, protocol)
expect(1, message, "string", "table", "number", "boolean")
expect(2, protocol, "string", "nil")
call("transmit", nil, false, false, rednet.CHANNEL_BROADCAST,
rednet.CHANNEL_BROADCAST, message)
end
function rednet.receive(protocol, timeout)
expect(1, protocol, "string", "nil")
timeout = expect(2, timeout, "number", "nil") or math.huge
local timer
if timeout then
timer = os.startTimer(timer)
end
while true do
local event = table.pack(os.pullEvent())
if event[1] == "timer" and event[2] == timer then return end
if event[1] == "rednet_message" and (event[4] == protocol or
not protocol) then
return table.unpack(event, 2)
end
end
end
local running = false
function rednet.run()
if running then
error("rednet is already running")
end
running = true
while true do
local event = table.pack(os.pullEvent())
if event[1] == "modem_message" then
local message = event[5]
if type(message) == "table" then
if message[1] == "rednet_message" and (message[2] == os.computerID() or
message[2] == rednet.CHANNEL_BROADCAST) then
os.queueEvent("rednet_message", event[3], message[2], message[3])
end
end
end
end
end
return rednet

View File

@@ -0,0 +1,114 @@
-- rc.settings
local expect = require("cc.expect")
local textutils = require("textutils")
local settings = {}
local defs = {}
local set = {}
function settings.define(name, opts)
expect.expect(1, name, "string")
expect.expect(2, opts, "table", "nil")
opts = opts or {}
opts.description = expect.field(opts, "description", "string", "nil")
opts.type = expect.field(opts, "type", "string", "nil")
defs[name] = opts
end
function settings.undefine(name)
expect.expect(1, name, "string")
defs[name] = nil
end
function settings.set(name, value)
expect.expect(1, name, "string")
if defs[name] and defs[name].type then
expect.expect(2, value, defs[name].type)
else
expect.expect(2, value, "number", "string", "boolean")
end
set[name] = value
end
function settings.get(name, default)
expect.expect(1, name, "string")
if set[name] ~= nil then
return set[name]
elseif default ~= nil then
return default
else
return defs[name] and defs[name].default
end
end
function settings.getDetails(name)
expect.expect(1, name, "string")
local def = defs[name]
if not def then return end
return {
description = def.description,
default = def.default,
value = set[name],
type = def.type,
}
end
function settings.unset(name)
expect.expect(1, name, "string")
set[name] = nil
end
function settings.clear()
set = {}
end
function settings.getNames()
local names = {}
for k in pairs(defs) do
names[#names+1] = k
end
table.sort(names)
return names
end
function settings.load(path)
expect.expect(1, path, "string", "nil")
path = path or ".settings"
local handle = io.open(path, "r")
if not handle then
return false
end
local data = handle:read("a")
handle:close()
local new = textutils.unserialize(data)
if not new then return false end
for k, v in pairs(new) do
set[k] = v
end
return true
end
function settings.save(path)
expect.expect(1, path, "string", "nil")
path = path or ".settings"
local data = textutils.serialize(set)
local handle = io.open(path, "w")
if not handle then return false end
handle:write(data)
handle:close()
return true
end
return settings

View File

@@ -0,0 +1,334 @@
-- LeonOS shell api
local shell = {}
local rc = require("rc")
local fs = require("fs")
local colors = require("colors")
local thread = require("rc.thread")
local expect = require("cc.expect").expect
local settings = require("settings")
local textutils = require("textutils")
local function copyIfPresent(f, t)
if t[f] then
local old = t[f]
t[f] = {}
for k, v in pairs(old) do
t[f][k] = v
end
else
t[f] = {}
end
end
local completions = {[0]={}}
function shell.init(env)
local vars = thread.vars()
copyIfPresent("aliases", vars)
completions[vars.parentShell or 0] = completions[vars.parentShell or 0] or {}
vars.path = vars.path or ".:/rc/programs"
vars.env = env or _ENV or _G
end
local builtins = {
cd = function(dir)
if dir then
shell.setDir(dir)
else
print(shell.dir())
end
end,
exit = function()
shell.exit()
end,
alias = function(...)
local args = {...}
if #args == 0 then
textutils.coloredPrint(colors.yellow, "shell aliases", colors.white)
local aliases = shell.aliases()
local _aliases = {}
for k, v in pairs(aliases) do
table.insert(_aliases, {colors.cyan, k, colors.white, ":", v})
end
textutils.pagedTabulate(_aliases)
elseif #args == 1 then
shell.clearAlias(args[1])
elseif #args == 2 then
shell.setAlias(args[1], args[2])
else
error("this program takes a maximum of two arguments", 0)
end
end
}
local function callCommand(command, func, ...)
thread.vars().program = command
local success, prog_err
if settings.get("shell.tracebacks") then
success, prog_err = xpcall(func, debug.traceback, ...)
else
success, prog_err = pcall(func, ...)
end
thread.vars().program = "shell"
if not success then
return nil, prog_err
end
return true
end
local function execProgram(fork, command, ...)
local path, res_err = shell.resolveProgram(command)
if not path then
return nil, res_err
end
local ok, err = loadfile(path, "t", thread.vars().env)
if not ok then
return nil, err
end
if fork then
local args = table.pack(...)
local result
local id = thread.spawn(function()
shell.init()
result = table.pack(callCommand(path, ok,
table.unpack(args, 1, args.n)))
end, command)
repeat rc.sleep(0.05, true) until not thread.exists(id)
if result then
return table.unpack(result, 1, result.n)
end
return true
else
return callCommand(path, ok, ...)
end
end
-- execute a command, but do NOT fork
function shell.exec(command, ...)
expect(1, command, "string")
return execProgram(false, command, ...)
end
function shell.execute(command, ...)
expect(1, command, "string")
if builtins[command] then
local func = builtins[command]
return callCommand(command, func, ...)
else
return execProgram(true, command, ...)
end
return true
end
local function tokenize(str)
local words = {}
for word in str:gmatch("[^ ]+") do
words[#words+1] = word
end
return words
end
function shell.run(...)
return shell.execute(table.unpack(tokenize(table.concat({...}, " "))))
end
-- difference: this exits the current thread immediately
function shell.exit()
thread.remove()
end
function shell.dir()
return thread.dir()
end
function shell.setDir(dir)
expect(1, dir, "string")
return thread.setDir(shell.resolve(dir))
end
function shell.path()
return thread.vars().path
end
function shell.setPath(path)
expect(1, path, "string")
thread.vars().path = path
end
function shell.resolve(path)
expect(1, path, "string")
if path:sub(1,1) == "/" then
return path
end
return fs.combine(thread.dir(), path)
end
function shell.resolveProgram(path)
expect(1, path, "string")
local aliases = thread.vars().aliases
if aliases[path] then
path = aliases[path]
end
if fs.exists(path) and not fs.isDir(path) then
return path
end
for search in thread.vars().path:gmatch("[^:]+") do
if search == "." then search = shell.dir() end
local try1 = fs.combine(search, path)
local try2 = fs.combine(search, path .. ".lua")
if fs.exists(try1) and not fs.isDir(try1) then
return try1
end
if fs.exists(try2) and not fs.isDir(try2) then
return try2
end
end
return nil, "command not found"
end
function shell.programs(hidden)
expect(1, hidden, "boolean", "nil")
local programs = {}
local seen = {}
for search in thread.vars().path:gmatch("[^:]+") do
local files = fs.list(shell.resolve(search))
for i=1, #files, 1 do
programs[#programs+1] = files[i]:match("^(.+)%.lua$")
if programs[#programs] then
seen[programs[#programs]] = true
end
end
end
for alias in pairs(shell.aliases()) do
if not seen[alias] then programs[#programs+1] = alias end
end
for builtin in pairs(builtins) do
if not seen[builtin] then programs[#programs+1] = builtin end
end
return programs
end
function shell.complete(line)
expect(1, line, "string")
local words = tokenize(line)
local aliases = thread.vars().aliases or {}
if #words > (line:sub(-1) == " " and 0 or 1) then
words[1] = aliases[words[1]] or words[1]
end
if line:sub(-1) == " " and #words > 0 then
local complete = completions[thread.vars().parentShell or 0][words[1]]
if complete then
table.remove(words, 1)
return complete(#words + 1, "", words)
end
else
if #words == 1 then
local opt = shell.completeProgram(words[1])
for i=1, #opt, 1 do
if shell.resolveProgram(words[1] .. opt[i]) then
opt[i] = opt[i] .. " "
end
end
return opt
else
local complete = completions[thread.vars().parentShell or 0][words[1]]
if complete then
local arg = table.remove(words, #words)
table.remove(words, 1)
return complete(#words + 1, arg, words)
end
end
end
end
function shell.completeProgram(line)
expect(1, line, "string")
return require("cc.shell.completion").program(line)
end
function shell.setCompletionFunction(program, complete)
expect(1, program, "string")
expect(2, complete, "function")
completions[thread.vars().parentShell or 0][program] = complete
end
function shell.getCompletionInfo()
return completions[thread.vars().parentShell or 0]
end
function shell.getRunningProgram()
return thread.vars().program
end
function shell.setAlias(command, program)
expect(1, command, "string")
expect(2, program, "string")
thread.vars().aliases[command] = program
end
function shell.clearAlias(command)
expect(1, command, "string")
thread.vars().aliases[command] = nil
end
function shell.aliases()
return thread.vars().aliases
end
function shell.openTab(...)
return require("multishell").launch(...)
end
function shell.switchTab(id)
return require("multishell").setFocus(id)
end
return shell

View File

@@ -0,0 +1,391 @@
-- rc.textutils
local rc = require("rc")
local term = require("term")
local json = require("rc.json")
local colors = require("colors")
local expect = require("cc.expect").expect
local strings = require("cc.strings")
local tu = {}
function tu.slowWrite(text, rate)
expect(1, text, "string")
expect(2, rate, "number", "nil")
local delay = 1/(rate or 20)
for c in text:gmatch(".") do
rc.write(c)
rc.sleep(delay)
end
end
function tu.slowPrint(text, rate)
expect(1, text, "string")
expect(2, rate, "number", "nil")
tu.slowWrite(text.."\n", rate)
end
function tu.formatTime(time, _24h)
expect(1, time, "number")
expect(2, _24h, "boolean", "nil")
local fmt = _24h and "!%H:%M" or "!%I:%M %p"
return (os.date(fmt, time * 3600):gsub("^ ", ""))
end
local function pagedWrite(text, begin)
local w, h = term.getSize()
local x, y = term.getCursorPos()
local realTotal = 0
local lines = begin or 0
local elements = strings.splitElements(text, w)
strings.wrappedWriteElements(elements, w, false, {
newline = function()
rc.write("\n")
realTotal = realTotal + 1
lines = lines + 1
x, y = term.getCursorPos()
if lines >= h - 2 then
local old = term.getTextColor()
term.setTextColor(colors.white)
rc.write("Press any key to continue")
term.setTextColor(old)
rc.pullEvent("char")
local _, _y = term.getCursorPos()
term.at(1, _y).clearLine()
lines = 0
end
end,
append = function(newText)
term.at(x, y).write(newText)
x = x + #newText
end,
getX = function() return x end
})
return realTotal, lines
end
function tu.pagedPrint(text)
expect(1, text, "string")
return pagedWrite(text .. "\n")
end
local function coloredWrite(paged, ...)
local args = table.pack(...)
local lines = 0
local pageLines = 0
local write = paged and pagedWrite or rc.write
local old_fg, old_bg = term.getTextColor(), term.getBackgroundColor()
local _, h = term.getSize()
for i=1, args.n, 1 do
if type(args[i]) == "number" then
term.setTextColor(args[i])
elseif type(args[i]) == "table" then
if args[i].fg or args[i][1] then
term.setTextColor(args[i].fg or args[i][1])
end
if args[i].bg or args[i][2] then
term.setBackgroundColor(args[i].bg or args[i][2])
end
else
local _lines, _tot = write(args[i], pageLines)
lines = lines + _lines
pageLines = _tot or 0
while pageLines > h do pageLines = pageLines - h end
end
end
term.setTextColor(old_fg)
term.setBackgroundColor(old_bg)
return lines
end
local function tabulate(paged, ...)
local args = table.pack(...)
local w = term.getSize()
local max_len = 0
local linear = {}
for i=1, args.n, 1 do
local argi = args[i]
expect(i, argi, "table", "number")
if type(argi) == "table" then
for n=1, #argi, 1 do
if type(argi[n]) == "table" then
local total_len = 2
local argin = argi[n]
for j=1, #argin, 1 do
expect(j, argin[j], "string", "number")
if type(argin[j]) == "string" then
total_len = total_len + #argin[j]
end
end
argin.total_len = total_len
max_len = math.max(max_len, total_len + 2)
linear[#linear+1] = argi[n]
else
linear[#linear+1] = expect(n, argi[n], "string")
max_len = math.max(max_len, #argi[n] + 2)
end
end
else
linear[#linear+1] = args[i]
end
end
local written = 0
local prt = paged and function(_args)
if type(_args) == "string" then _args = {_args} end
return coloredWrite(true, table.unpack(_args))
end or function(_args)
if type(_args) == "string" then _args = {_args} end
return coloredWrite(false, table.unpack(_args))
end
for i=1, #linear, 1 do
local lini = linear[i]
if type(lini) == "number" then
if written > 0 then
prt("\n")
written = 0
end
term.setTextColor(lini)
else
local len = type(lini) == "table" and lini.total_len or #lini
if written + max_len > w then
if written + len > w then
prt("\n")
prt(lini)
rc.write((" "):rep(max_len - len))
written = max_len
else
prt(lini)
prt("\n")
written = 0
end
else
prt(lini)
rc.write((" "):rep(max_len - len))
written = written + max_len
end
end
end
if written > 0 then
prt("\n")
end
end
function tu.tabulate(...)
tabulate(false, ...)
end
function tu.pagedTabulate(...)
tabulate(true, ...)
end
local function mk_immut(str, field)
return setmetatable({}, {
__newindex = function()
error(string.format("attempt to modify textutils.%s", field), 2)
end,
__tostring = function()
return str
end})
end
tu.empty_json_array = mk_immut("[]", "empty_json_array")
tu.json_null = mk_immut("null", "json_null")
local function serialize(t, _seen)
local ret = ""
if type(t) == "table" then
local seen = setmetatable({}, {__index = _seen})
ret = "{"
for k, v in pairs(t) do
if seen[k] then
k = "<recursion>"
end
if seen[v] then
v = "<recursion>"
end
if type(k) == "table" then
seen[k] = true
end
if type(v) == "table" then
seen[v] = true
end
ret = ret .. string.format("[%s] = %s,", serialize(k, seen),
serialize(v, seen))
end
ret = ret .. "}"
elseif type(t) == "function" or type(t) == "thread" or
type(t) == "userdata" then
error("cannot serialize type " .. type(t), 2)
else
return string.format("%q", t)
end
return ret
end
function tu.serialize(t, opts)
expect(1, t, "table")
expect(2, opts, "table", "nil")
return serialize(t, {})
end
function tu.unserialize(s)
expect(1, s, "string")
local call = load("return " .. s, "=<unserialize>", "t", {})
if call then return call() end
end
tu.serialise = tu.serialize
tu.unserialise = tu.unserialize
function tu.serializeJSON(t, nbt)
expect(1, t, "table")
if nbt then
error("NBT mode is not yet supported")
end
return json.encode(t)
end
function tu.unserializeJSON(s)--s, options)
expect(1, s, "string")
return json.decode(s)
end
tu.serialiseJSON = tu.serializeJSON
tu.unserialiseJSON = tu.unserializeJSON
function tu.urlEncode(str)
expect(1, str, "string")
-- TODO: possibly UTF-8 support?
str = str:gsub("[^%w %-%_%.]", function(c)
return string.format("%%%02x", c:byte())
end):gsub(" ", "+"):gsub("\n", "\r\n")
return str
end
local function split(text)
local dots = {""}
for c in text:gmatch(".") do
if c == "." or c == ":" then
--dots[#dots+1] = c
dots[#dots+1] = ""
else
dots[#dots] = dots[#dots] .. c
end
end
return dots
end
local function getSuffix(thing, default)
if type(thing) == "table" then
return "."
elseif type(thing) == "function" then
return "("
end
return default
end
function tu.complete(text, env)
expect(1, text, "string")
env = expect(2, env, "table", "nil") or _G
local last_exp = text:match("[^%(%)%%%+%-%*/%[%]%{%}; =]*$")
local results = {}
if last_exp and #last_exp > 0 then
local search = {env}
local mt = getmetatable(env)
if mt and type(mt.__index) == "table" then
search[#search+1] = mt.__index
end
for s=1, #search, 1 do
local dots = split(last_exp)
local current = search[s]
local final = 0
for i=1, #dots, 1 do
if current[dots[i]] then
current = current[dots[i]]
final = i
else
break
end
end
for _=1, final, 1 do table.remove(dots, 1) end
if #dots == 0 then
results[#results+1] = getSuffix(current)
end
if #dots ~= 1 or type(current) ~= "table" then return results end
local find = dots[1]
for key, val in pairs(current) do
key = key .. getSuffix(val, "")
if key:sub(1, #find) == find then
results[#results+1] = key:sub(#find + 1)
end
end
end
end
return results
end
function tu.coloredWrite(...)
return coloredWrite(false, ...)
end
function tu.coloredPrint(...)
return coloredWrite(false, ...) + rc.write("\n")
end
function tu.coloredPagedPrint(...)
return coloredWrite(true, ...) + rc.write("\n")
end
return tu

View File

@@ -0,0 +1,76 @@
-- rc.vector
local vector = {}
local Vector = {}
function Vector:add(o)
return vector.new(self.x + o.x, self.y + o.y, self.z + o.z)
end
function Vector:sub(o)
return vector.new(self.x - o.x, self.y - o.y, self.z - o.z)
end
function Vector:mul(m)
return vector.new(self.x * m, self.y * m, self.z * m)
end
function Vector:div(m)
return vector.new(self.x / m, self.y / m, self.z / m)
end
function Vector:unm()
return vector.new(-self.x, -self.y, -self.z)
end
function Vector:dot(o)
return (self.x * o.x) + (self.y * o.y) + (self.z * o.z)
end
function Vector:cross(o)
return vector.new(
(self.y * o.z) - (self.z * o.y),
(self.z * o.x) - (self.x * o.z),
(self.x * o.y) - (self.y * o.x))
end
function Vector:length()
return math.sqrt((self.x * self.x) + (self.y * self.y) + (self.z * self.z))
end
function Vector:normalize()
return self:div(self:length())
end
function Vector:round(tolerance)
tolerance = tolerance or 1
local squared = tolerance * tolerance
return vector.new(
math.floor(self.x + (tolerance * 0.5)) / squared,
math.floor(self.y + (tolerance * 0.5)) / squared,
math.floor(self.z + (tolerance * 0.5)) / squared)
end
function Vector:tostring()
return string.format("%d,%d,%d", self.x, self.y, self.z)
end
function Vector:equals(o)
return self.x == o.x and self.y == o.y and self.z == o.z
end
Vector.eq = Vector.equals
local vect_mt = {
__index = Vector
}
for k, v in pairs(Vector) do
vect_mt["__"..k] = v
end
function vector.new(x, y, z)
return setmetatable({x = x or 0, y = y or 0, z = z or 0}, vect_mt)
end
return vector

View File

@@ -0,0 +1,368 @@
-- window api
local term = require("term")
local colors = require("colors")
local expect = require("cc.expect").expect
local range = require("cc.expect").range
local window = {}
local rep = string.rep
local sub = string.sub
local max = math.max
local min = math.min
local function into_buffer(buf, x, y, text)
if not text then return end
if not buf[y] then return end
text = sub(text, 1, #buf[y] - x + 1)
if x < 1 then
text = sub(text, -x + 2)
x = 1
end
local olen = #buf[y]
if x + #text > olen then
buf[y] = sub(buf[y], 0, max(0, x-1)) .. text
else
buf[y] = sub(buf[y], 0, max(0, x-1)) .. text .. buf[y]:sub(x + #text)
end
buf[y] = sub(buf[y], 1, olen)
end
function window.create(parent, x, y, width, height, visible)
if type(parent) ~= "table" then expect(1, parent, "table") end
if parent == term then
error("do not pass 'term' as a window parent", 0)
end
if type(x) ~= "number" then expect(2, x, "number") end
if type(y) ~= "number" then expect(3, y, "number") end
if type(width) ~= "number" then expect(4, width, "number") end
if type(height) ~= "number" then expect(5, height, "number") end
if type(visible) ~= "boolean" then expect(6, visible, "boolean", "nil") end
if visible == nil then visible = true end
local cursorX, cursorY, cursorBlink = 1, 1, false
local foreground, background = colors.toBlit(colors.white),
colors.toBlit(colors.black)
local textbuf, fgbuf, bgbuf = {}, {}, {}
local win = {}
local palette = {}
for i=0, 15, 1 do
palette[i] = colors.packRGB(parent.getPaletteColor(2^i))
end
local function drawLine(i)
parent.setCursorPos(x, y + i - 1)
if not textbuf[i] then return end
parent.blit(textbuf[i], fgbuf[i], bgbuf[i])
end
local function draw()
local blink = parent.getCursorBlink()
parent.setCursorBlink(false)
local parentW, parentH = parent.getSize()
local firstVisible = math.max(1, -y+2)
for i=1, math.min(height, parentH), 1 do
drawLine(firstVisible+i-1)
end
parent.setCursorBlink(blink)
end
local function restorePalette()
for i=0, 15, 1 do
parent.setPaletteColor(2^i, palette[i])
end
end
local function restoreCursorBlink()
parent.setCursorBlink(cursorBlink)
end
local function restoreCursorPos()
if cursorX > 0 and cursorY > 0 and
cursorX <= width and cursorY <= height then
parent.setCursorPos(x + cursorX - 1, y + cursorY - 1)
else
parent.setCursorPos(0, 0)
end
end
local function restoreCursorColor()
parent.setTextColor(2^tonumber(foreground, 16))
end
function win.write(text)
if type(text) ~= "string" then expect(1, text, "string") end
local fg, bg = rep(foreground, #text), background:rep(#text)
into_buffer(textbuf, cursorX, cursorY, text)
into_buffer(fgbuf, cursorX, cursorY, fg)
into_buffer(bgbuf, cursorX, cursorY, bg)
cursorX = max(-100, min(cursorX + #text, width + 1))
local firstVisible, _, maxHeight = math.max(1, -y+2), parent.getSize()
if visible and cursorY >= firstVisible and cursorY <= firstVisible+maxHeight then win.redraw() end
end
function win.blit(text, tcol, bcol)
if type(text) ~= "string" then expect(1, text, "string") end
if type(tcol) ~= "string" then expect(2, tcol, "string") end
if type(bcol) ~= "string" then expect(3, bcol, "string") end
assert(#text == #tcol and #text == #bcol, "mismatched argument lengths")
into_buffer(textbuf, cursorX, cursorY, text)
into_buffer(fgbuf, cursorX, cursorY, tcol)
into_buffer(bgbuf, cursorX, cursorY, bcol)
cursorX = max(0, min(cursorX + #text, width + 1))
local firstVisible, _, maxHeight = math.max(1, -y+2), parent.getSize()
if visible and cursorY >= firstVisible and cursorY <= firstVisible+maxHeight then
drawLine(cursorY)
restoreCursorColor()
restoreCursorPos()
end
end
function win.clear()
local fore = rep(foreground, width)
local back = rep(background, width)
local blank = rep(" ", width)
for i=1, height, 1 do
textbuf[i] = blank
fgbuf[i] = fore
bgbuf[i] = back
end
if visible then
win.redraw()
end
end
function win.clearLine()
local emptyText, emptyFg, emptyBg =
rep(" ", width),
rep(foreground, width),
rep(background, width)
textbuf[cursorY] = emptyText
fgbuf[cursorY] = emptyFg
bgbuf[cursorY] = emptyBg
local firstVisible, _, maxHeight = math.max(1, -y+2), parent.getSize()
if visible and cursorY >= firstVisible and cursorY <= firstVisible+maxHeight then
win.redraw()
end
end
function win.getCursorPos()
return cursorX, cursorY
end
function win.setCursorPos(_x, _y)
if type(_x) ~= "number" then expect(1, _x, "number") end
if type(_y) ~= "number" then expect(2, _y, "number") end
cursorX, cursorY = _x, _y
if visible then
restoreCursorPos()
end
end
function win.setCursorBlink(blink)
cursorBlink = not not blink
if visible then
restoreCursorBlink()
end
end
function win.getCursorBlink()
return cursorBlink
end
function win.isColor()
return parent.isColor()
end
win.isColour = win.isColor
function win.setTextColor(color)
if type(color) ~= "number" then expect(1, color, "number") end
foreground = colors.toBlit(color) or foreground
if visible then
restoreCursorColor()
end
end
win.setTextColour = win.setTextColor
function win.setPaletteColor(color, r, g, b)
if type(color) ~= "number" then expect(1, color, "number") end
if type(r) ~= "number" then expect(2, r, "number") end
if r < 1 then
if type(g) ~= "number" then expect(3, g, "number") end
if type(b) ~= "number" then expect(4, b, "number") end
palette[math.floor(math.log(color, 2))] = colors.packRGB(r, g, b)
else
palette[math.floor(math.log(color, 2))] = r
end
if visible then
restorePalette()
end
end
win.setPaletteColour = win.setPaletteColor
function win.getPaletteColor(color)
if type(color) ~= "number" then expect(1, color, "number") end
return palette[math.floor(math.log(color, 2))]
end
win.getPaletteColour = win.getPaletteColor
function win.setBackgroundColor(color)
if type(color) ~= "number" then expect(1, color, "number") end
background = colors.toBlit(color)
end
win.setBackgroundColour = win.setBackgroundColor
function win.getSize()
return width, height
end
function win.scroll(n)
if type(n) ~= "number" then expect(1, n, "number") end
if n == 0 then return end
local fg = rep(foreground, width)
local bg = rep(background, width)
local blank = rep(" ", width)
if n > 0 then
for _=1, n, 1 do
table.remove(textbuf, 1)
textbuf[#textbuf+1] = blank
table.remove(fgbuf, 1)
fgbuf[#fgbuf+1] = fg
table.remove(bgbuf, 1)
bgbuf[#bgbuf+1] = bg
end
else
for _=1, -n, 1 do
table.insert(textbuf, 1, blank)
textbuf[#textbuf] = nil
table.insert(fgbuf, 1, fg)
fgbuf[#fgbuf] = nil
table.insert(bgbuf, 1, bg)
bgbuf[#bgbuf] = nil
end
end
if visible then
win.redraw()
end
end
function win.getTextColor()
return 2^tonumber(foreground, 16)
end
win.getTextColour = win.getTextColor
function win.getBackgroundColor()
return 2^tonumber(background, 16)
end
win.getBackgroundColour = win.getBackgroundColor
function win.getLine(ly)
if type(ly) ~= "number" then expect(1, ly, "number") end
if ly < 1 or ly > height then range(ly, 1, height) end
return textbuf[ly], fgbuf[ly], bgbuf[ly]
end
function win.setVisible(vis)
if vis and not visible then
draw()
restorePalette()
restoreCursorBlink()
restoreCursorPos()
restoreCursorColor()
end
visible = not not vis
end
function win.redraw()
if visible then
draw()
restorePalette()
restoreCursorPos()
restoreCursorBlink()
restoreCursorColor()
end
end
function win.restoreCursor()
if visible then
restoreCursorBlink()
restoreCursorPos()
restoreCursorColor()
end
end
function win.getPosition()
return x, y
end
local function resize_buffer(buf, nw, nh, c)
if nw > width then
for i=1, #buf, 1 do
buf[i] = buf[i] .. sub(rep(buf[i], -1), nw - width)
end
end
if nh > #buf then
for _=1, nh - #buf, 1 do
buf[#buf+1] = rep(c, nw)
end
end
end
function win.reposition(nx, ny, nw, nh, npar)
if type(nx) ~= "number" then expect(1, nx, "number") end
if type(ny) ~= "number" then expect(2, ny, "number") end
if type(nw) ~= "number" then expect(3, nw, "number", "nil") end
if type(nh) ~= "number" then expect(4, nh, "number", "nil") end
if type(npar) ~= "table" then expect(5, npar, "table", "nil") end
x, y, width, height, parent =
nx or x, ny or y,
nw or width, nh or height,
npar or parent
resize_buffer(textbuf, width, height, " ")
resize_buffer(fgbuf, width, height, "0")
resize_buffer(bgbuf, width, height, "f")
if visible then
win.redraw()
end
end
function win.at(_x, _y)
win.setCursorPos(_x, _y)
return win
end
win.clear()
return win
end
return window