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

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright (c) 2022 LeonMMcoset
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

118
NEW_FEATURES.txt Normal file
View File

@@ -0,0 +1,118 @@
NEW IN LeonOS
The following is a non-exhaustive list of new
and/or changed features in LeonOS compared to
LeonOS.
!!!! THIS FILE IS OUTDATED !!!!
See https://ocaweso.me/LeonOS for more
up-to-date documentation.
Major Changes
=============
- `os.loadAPI` has been completely omitted.
- * All APIs except the standard Lua ones must be
loaded using `require` before they can be used.
- * Lua 5.1 builtins have been removed from `_G`,
but can be accessed through `require("lua51")`.
- Startup scripts from /startup are run in
parallel as separate threads.
- LeonOS has full native support for
multithreading -- custom schedulers are no
longer necessary!
- Multishell can be used even on standard (non-
advanced) computers, using Alt+Left and
Alt+Right to switch tabs. There is no
dedicated Multishell program - only the API. A
Multishell instance may be started at any time
using multishell.launch.
* These do not apply when compatibility mode is
enabled - see "Compatibility Mode" below.
New API Methods
===============
LeonOS features a few extensions to the LeonOS
APIs:
- textutils.coloredWrite(...):
Similar to textutils.tabulate(), but takes
strings instead of tables and doesn't tabulate
its arguments. Useful for easy printing of
colorized text. Returns the number of lines
written.
- textutils.coloredPrint(...):
Like textutils.coloredWrite(), but prints an
extra newline at the end of the text, similar
to print().
- Tables given to textutils.tabulate() may
contain tables, along with strings and numbers;
if a table is present, it must contain a set of
arguments suitable for passing to
textutils.coloredWrite().
- LeonOS's paintutils API uses the BIMG (Blit
Image) format for its images. This format
supports animations and lots of other useful
metadata, plus combining text and images.
See https://github.com/SkyTheCodeMaster/bimg
for details on the format.
The Multishell foreground thread management
functions should only be used when absolutely
necessary. If they ARE necessary, these should be
used instead of the corresponding thread API
functions regardless of whether multishell is
actually enabled, to ensure proper behavior when it
is enabled.
The foreground thread is the only thread that will
respond to terminate events. Ensuring that it is
set correctly is therefore quite important. Under
most circumstances you should not need to use these
functions, since shell.run() uses them behind the
scenes.
These should not be confused with Multishell's
getFocus() and setFocus() functions, which manage
the focused tab.
- multishell.getForeground():
Returns the foreground thread ID of the current
tab.
- multishell.pushForeground(pid):
Adds a thread to the current tab's foreground
stack; the given thread will be removed when
it exits.
- multishell.switchForeground(pid):
Changes the top entry of the current tab's
foreground stack; removes the old entry.
- multishell.launch()'s first argument, the
environment, is optional and may be completely
omitted.
Compatibility Mode
==================
When the bios.compat_mode setting is set, LeonOS
will enter LeonOS compatibility mode. This
disables strict global checking and places all
relevant functions and APIs into _G. In
compatibility mode, os.version() returns
"LeonOS 1.8" rather than the current LeonOS
version.
This mode should only be used when necessary. New
programs should use proper Lua coding conventions
and therefore work without it.

15
README.md Normal file
View File

@@ -0,0 +1,15 @@
# LeonOS
###### look, mom, a proper Markdown readme!
LeonOS is a reimplementation of [CC: Tweaked](https://github.com/CC-Tweaked/CC-Tweaked)'s LeonOS, intended to be cleaner and easier than the original LeonOS.
Key changes:
- No. More. CCPL!!
- All previously global APIs (with the exception of the standard Lua ones) have been removed.
- Non-standard `os` API functions are now in the `rc` table, e.g. `os.sleep` becomes `rc.sleep` or `os.pullEvent` becomes `rc.pullEvent`.
- Native support for proper thread management (`parallel` implementation builds on this)
- Multishell works even on standard computers, and is navigable with keyboard shortcuts!
See [the LeonOS website](https://ocaweso.me/LeonOS) for more details.

View File

@@ -0,0 +1,189 @@
_G._HOST = _G._HOST .. " (LeonOS 1.6.0)"
local fs = rawget(_G, "fs")
_G._RC_ROM_DIR = _RC_ROM_DIR or (...) and fs.exists("/rc") and "/rc" or "/rom"
if fs.exists("/.start_rc.lua") and not (...) then
_G._RC_USED_START = true
local handle = assert(fs.open("/.start_rc.lua", "r"))
local data = handle.readAll()
handle.close()
local _sd = rawget(os, "shutdown")
local ld = rawget(_G, "loadstring") or load
assert(ld(data, "=start_rc"))(true)
_sd()
while true do coroutine.yield() end
end
local function pull(tab, key)
local func = tab[key]
tab[key] = nil
return func
end
-- this is overwritten further down but `load` needs it
local expect = function(_, _, _, _) end
local shutdown = pull(os, "shutdown")
local reboot = pull(os, "restart")
-- `os` extras go in here now.
local rc = {
_NAME = "LeonOS",
_VERSION = {
major = 1,
minor = 6,
patch = 0
},
queueEvent = pull(os, "queueEvent"),
startTimer = pull(os, "startTimer"),
cancelTimer = pull(os, "cancelTimer"),
setAlarm = pull(os, "setAlarm"),
cancelAlarm = pull(os, "cancelAlarm"),
getComputerID = pull(os, "getComputerID"),
computerID = pull(os, "computerID"),
getComputerLabel = pull(os, "getComputerLabel"),
computerLabel = pull(os, "computerLabel"),
setComputerLabel = pull(os, "setComputerLabel"),
day = pull(os, "day"),
epoch = pull(os, "epoch"),
}
-- and a few more
rc.pushEvent = rc.queueEvent
function rc.shutdown()
shutdown()
while true do coroutine.yield() end
end
function rc.reboot()
reboot()
while true do coroutine.yield() end
end
local timer_filter = {}
function rc.pullEventRaw(filter)
expect(1, filter, "string", "nil")
local sig
repeat
sig = table.pack(coroutine.yield())
until ((sig[1] == "timer" and
timer_filter[sig[2]] == require("rc.thread").id()) or sig[1] ~= "timer")
and (not filter) or (sig[1] == filter)
return table.unpack(sig, 1, sig.n)
end
function rc.pullEvent(filter)
expect(1, filter, "string", "nil")
local sig
repeat
sig = table.pack(coroutine.yield())
if sig[1] == "terminate" then
error("terminated", 0)
end
until ((sig[1] == "timer" and
timer_filter[sig[2]] == require("rc.thread").id()) or sig[1] ~= "timer")
and (not filter) or (sig[1] == filter)
return table.unpack(sig, 1, sig.n)
end
function rc.sleep(time, no_term)
local id = rc.startTimer(time)
local thread = require("rc.thread").id()
timer_filter[id] = thread
repeat
local _, tid = (no_term and rc.pullEventRaw or rc.pullEvent)("timer")
until tid == id
end
function rc.version()
return string.format("LeonOS %d.%d.%d",
rc._VERSION.major, rc._VERSION.minor, rc._VERSION.patch)
end
-- Lua 5.1? meh
if _VERSION == "Lua 5.1" then
local old_load = load
rc.lua51 = {
loadstring = pull(_G, "loadstring"),
setfenv = pull(_G, "setfenv"),
getfenv = pull(_G, "getfenv"),
unpack = pull(_G, "unpack"),
log10 = pull(math, "log10"),
maxn = pull(table, "maxn")
}
table.unpack = rc.lua51.unpack
function _G.load(x, name, mode, env)
expect(1, x, "string", "function")
expect(2, name, "string", "nil")
expect(3, mode, "string", "nil")
expect(4, env, "table", "nil")
env = env or _G
local result, err
if type(x) == "string" then
result, err = rc.lua51.loadstring(x, name)
else
result, err = old_load(x, name)
end
if result then
env._ENV = env
rc.lua51.setfenv(result, env)
end
return result, err
end
-- Lua 5.1's xpcall sucks
local old_xpcall = xpcall
function _G.xpcall(call, func, ...)
local args = table.pack(...)
return old_xpcall(function()
return call(table.unpack(args, 1, args.n))
end, func)
end
end
local startup = _RC_ROM_DIR .. "/startup"
local files = fs.list(startup)
table.sort(files)
function _G.loadfile(file)
local handle, err = fs.open(file, "r")
if not handle then
return nil, err
end
local data = handle.readAll()
handle.close()
return load(data, "="..file, "t", _G)
end
function _G.dofile(file)
return assert(loadfile(file))()
end
for i=1, #files, 1 do
local file = startup .. "/" .. files[i]
assert(loadfile(file))(rc)
end
expect = require("cc.expect").expect
local thread = require("rc.thread")
thread.start()

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

View File

@@ -0,0 +1,6 @@
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("alias", completion.build(
nil, completion.program
))

View File

@@ -0,0 +1,6 @@
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("bedit", completion.build(
completion.dirOrFile
))

View File

@@ -0,0 +1,6 @@
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("bg", completion.build(
{ completion.programWithArgs, 1, many = true }
))

View File

@@ -0,0 +1,4 @@
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("enter", completion.build(completion.dir))

View File

@@ -0,0 +1,7 @@
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("copy", completion.build(
{completion.dirOrFile, true},
{completion.dirOrFile, many = true}
))

View File

@@ -0,0 +1,6 @@
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("delete", completion.build(
{completion.dirOrFile, many = true}
))

View File

@@ -0,0 +1,11 @@
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("devbin", completion.build(
{ completion.choice, { "put", "get", "run" }, true },
function(cur, prev)
if prev[1] == "put" then
return completion.dirOrFile(cur, prev)
end
end
))

View File

@@ -0,0 +1,6 @@
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("edit", completion.build(
completion.dirOrFile
))

View File

@@ -0,0 +1,6 @@
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("fg", completion.build(
{ completion.programWithArgs, 1, many = true }
))

View File

@@ -0,0 +1,7 @@
local help = require("help")
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("help", completion.build(
{help.completeTopic, many = true}
))

View File

@@ -0,0 +1,6 @@
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("list", completion.build(
{completion.dir, many = true}
))

View File

@@ -0,0 +1,6 @@
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("mkdir", completion.build(
{completion.dir, many = true}
))

View File

@@ -0,0 +1,7 @@
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("move", completion.build(
{completion.dirOrFile, true},
{completion.dirOrFile, many = true}
))

View File

@@ -0,0 +1,6 @@
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("paint", completion.build(
))

View File

@@ -0,0 +1,6 @@
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("restart", completion.build(
{ completion.choice, { "now" } }
))

View File

@@ -0,0 +1,13 @@
local shell = require("shell")
local complete = require("cc.completion")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("redstone", completion.build(
{ completion.choice, {"probe", "set", "pulse"}, {false, true, true} },
completion.side,
function(cur, prev)
if prev[1] == "set" then
return complete.color(cur, true)
end
end
))

View File

@@ -0,0 +1,6 @@
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("set", completion.build(
{ completion.setting, true }
))

View File

@@ -0,0 +1,6 @@
local shell = require("shell")
local completion = require("cc.shell.completion")
shell.setCompletionFunction("shutdown", completion.build(
{ completion.choice, { "now" } }
))

View File

@@ -0,0 +1,308 @@
-- A much better editor.
local rc = require("rc")
local keys = require("keys")
local term = require("term")
local colors = require("colors")
local settings = require("settings")
local textutils = require("textutils")
local args = {...}
local type_colors = {
separator = colors[settings.get("edit.color_separator") or "lightBlue"],
operator = colors[settings.get("edit.color_operator") or "lightGray"],
keyword = colors[settings.get("edit.color_keyword") or "orange"],
boolean = colors[settings.get("edit.color_boolean") or "purple"],
comment = colors[settings.get("edit.color_comment") or "gray"],
builtin = colors[settings.get("edit.color_global") or "lime"],
string = colors[settings.get("edit.color_string") or "red"],
number = colors[settings.get("edit.color_number") or "magenta"]
}
local lines = {}
local linesDraw = {}
local run, menu = true, false
local cx, cy = 1, 1
local scroll = 0
local hscroll = 0
local scroll_offset = settings.get("edit.scroll_offset") or 3
local scroll_increment = 0
local scroll_factor = settings.get("edit.scroll_factor") or 8
local unsaved, changed = false, true
local file = args[1] or ".new"
local status = "Press Ctrl for menu"
if args[1] then
local handle = io.open(args[1], "r")
if handle then
for line in handle:lines() do
lines[#lines+1] = line
end
handle:close()
end
end
if not lines[1] then lines[1] = "" end
local win = require("window").create(term.current(), 1, 1, term.getSize())
local function redraw()
local w, h = term.getSize()
-- this seems to provide a good responsiveness curve on my machine
scroll_increment = math.floor(h/scroll_factor)
win.reposition(1, 1, w, h)
win.setVisible(false)
for i=1, h - 1, 1 do
local line = linesDraw[i]
win.setCursorPos(1 - hscroll, i)
win.clearLine()
if line then
for t=1, #line, 1 do
local item = line[t]
if type(item) == "number" then
win.setTextColor(item)
else
win.write(item)
end
end
end
end
win.setCursorPos(1, h)
win.clearLine()
win.setTextColor(type_colors.accent or colors.yellow)
win.write(status)
win.setTextColor(colors.white)
win.setCursorPos(math.min(w, cx), cy - scroll)
win.setCursorBlink(true)
win.setVisible(true)
end
local syntax = require("edit.syntax")
.new("/rc/modules/main/edit/syntax/lua.lua")
local function rehighlight()
local line = {}
linesDraw = {}
local _, h = term.getSize()
local text = table.concat(lines, "\n", scroll+1,
math.min(#lines, scroll+h+1)) or ""
for token, ttype in syntax(text) do
if token == "\n" then
linesDraw[#linesDraw+1] = line
line = {}
else
repeat
local bit = token:find("\n")
local nl = not not bit
local chunk = token:sub(1, bit or #token)
token = token:sub(#chunk+1)
line[#line+1] = type_colors[ttype] or colors.white
line[#line+1] = chunk
if nl then
linesDraw[#linesDraw+1] = line
line = {}
end
until #token == 0
end
end
if #line > 0 then
linesDraw[#linesDraw+1] = line
end
end
local function save()
if file == ".new" then
local _, h = term.getSize()
term.setCursorPos(1, h)
textutils.coloredWrite(colors.yellow, "filename: ")
file = term.read()
end
local handle, err = io.open(file, "w")
if not handle then
status = err
else
for i=1, #lines, 1 do
handle:write(lines[i] .. "\n")
end
handle:close()
status = "Saved to " .. file
unsaved = false
end
end
local function processInput()
local event, id = rc.pullEvent()
local w, h = term.getSize()
if event == "char" then
local line = lines[cy]
unsaved = true
if cx > #line then
line = line .. id
elseif cx == 1 then
line = id .. line
else
line = line:sub(0, cx-1) .. id .. line:sub(cx)
end
cx = cx + 1
lines[cy] = line
changed = true
elseif event == "key" then
id = keys.getName(id)
if id == "backspace" then
local line = lines[cy]
unsaved = true
if cx == 1 and cy > 1 then
local previous = table.remove(lines, cy - 1)
cy = cy - 1
cx = #previous + 1
line = previous .. line
else
if #line > 0 then
if cx > #line then
cx = cx - 1
line = line:sub(1, -2)
elseif cx > 1 then
line = line:sub(0, cx - 2) .. line:sub(cx)
cx = cx - 1
end
end
end
lines[cy] = line
changed = true
elseif id == "enter" then
if cx == 1 then
table.insert(lines, cy, "")
elseif cx > #lines[cy] then
table.insert(lines, cy+1, "")
else
local line = lines[cy]
local before, after = line:sub(0, cx - 1), line:sub(cx)
lines[cy] = before
table.insert(lines, cy + 1, after)
end
cy = cy + 1
cx = 1
changed = true
elseif id == "up" then
if cy > 1 then
cy = cy - 1
if cy - scroll < scroll_offset then
local old_scroll = scroll
scroll = math.max(0, scroll - scroll_increment)
if scroll < old_scroll then
rehighlight()
end
end
end
cx = math.min(cx, #lines[cy] + 1)
elseif id == "down" then
if cy < #lines then
cy = math.min(#lines, cy + 1)
if cy - scroll > h - scroll_offset then
local old_scroll = scroll
scroll = math.max(0, math.min(#lines - h + 1,
scroll + scroll_increment))
if scroll > old_scroll then
rehighlight()
end
end
end
cx = math.min(cx, #lines[cy] + 1)
elseif id == "left" then
if cx > 1 then
cx = cx - 1
end
hscroll = math.max(0, cx - w)
elseif id == "right" then
if cx < #lines[cy] + 1 then
cx = cx + 1
end
hscroll = math.max(0, cx - w)
elseif id == "leftCtrl" or id == "rightCtrl" then
status = "S:save E:exit"
menu = true
end
end
end
local function processMenuInput()
local event, id = rc.pullEvent()
if event == "char" then
if id:lower() == "e" then
if unsaved and menu ~= 2 then
status = "Lose unsaved work? E:yes C:no"
menu = 2
else
term.at(1, 1).clear()
run = false
end
elseif id:lower() == "c" and menu == 2 then
menu = false
elseif id:lower() == "s" then
save()
menu = false
end
elseif event == "key" then
id = keys.getName(id)
if id == "leftCtrl" or id == "rightCtrl" then
status = "Press Ctrl for menu"
menu = false
end
end
end
while run do
if changed then rehighlight() changed = false end
redraw()
if menu then
processMenuInput()
else
processInput()
end
end

View File

@@ -0,0 +1,229 @@
-- editor
local args = {...}
local rc = require("rc")
local keys = require("keys")
local term = require("term")
local shell = require("shell")
local colors = require("colors")
local settings = require("settings")
local textutils = require("textutils")
local scroll_offset = settings.get("edit.scroll_offset")
local state = {
file = args[1] or ".new",
unsaved = false,
scroll = 0,
hscroll = 0,
lines = {""},
status = "Press Control for menu",
cx = 1,
cy = 1,
}
if args[1] then
local path = shell.resolve(args[1])
local handle = io.open(path)
state.file = path
if handle then
state.lines = {}
for line in handle:lines() do
state.lines[#state.lines+1] = line
end
handle:close()
if not state.lines[1] then state.lines[1] = "" end
end
end
local function redraw()
local w, h = term.getSize()
for i=1, h - 1, 1 do
local to_write = state.lines[state.scroll + i] or ""
if state.cx > w then
to_write = to_write:sub(state.cx - (w-1))
end
term.at(1, i).clearLine()
term.write(to_write)
end
term.at(1, h).clearLine()
textutils.coloredWrite(colors.yellow, state.status, colors.white)
term.setCursorPos(math.min(w, state.cx), state.cy - state.scroll)
end
local run, menu = true, false
local function save()
if state.file == ".new" then
local _, h = term.getSize()
term.setCursorPos(1, h)
textutils.coloredWrite(colors.yellow, "filename: ", colors.white)
state.file = term.read()
end
local handle, err = io.open(state.file, "w")
if not handle then
state.status = err
else
for i=1, #state.lines, 1 do
handle:write(state.lines[i] .. "\n")
end
handle:close()
state.status = "Saved to " .. state.file
state.unsaved = false
end
end
local function processMenuInput()
local event, id = rc.pullEvent()
if event == "char" then
if id:lower() == "e" then
if state.unsaved and menu ~= 2 then
state.status = "Lose unsaved work? E:yes C:no"
menu = 2
else
term.at(1, 1).clear()
run = false
end
elseif id:lower() == "c" and menu == 2 then
menu = false
elseif id:lower() == "s" then
save()
menu = false
end
elseif event == "key" then
id = keys.getName(id)
if id == "leftCtrl" or id == "rightCtrl" then
state.status = "Press Control for menu"
menu = false
end
end
end
local function processInput()
local event, id = rc.pullEvent()
local _, h = term.getSize()
if event == "char" then
local line = state.lines[state.cy]
state.unsaved = true
if state.cx > #line then
line = line .. id
elseif state.cx == 1 then
line = id .. line
else
line = line:sub(0, state.cx-1)..id..line:sub(state.cx)
end
state.cx = state.cx + 1
state.lines[state.cy] = line
elseif event == "key" then
id = keys.getName(id)
if id == "backspace" then
local line = state.lines[state.cy]
state.unsaved = true
if state.cx == 1 and state.cy > 1 then
local previous = table.remove(state.lines, state.cy - 1)
state.cy = state.cy - 1
state.cx = #previous + 1
line = previous .. line
else
if #line > 0 then
if state.cx > #line then
state.cx = state.cx - 1
line = line:sub(1, -2)
elseif state.cx > 1 then
line = line:sub(0, state.cx - 2) .. line:sub(state.cx)
state.cx = state.cx - 1
end
end
end
state.lines[state.cy] = line
elseif id == "enter" then
if state.cx == 1 then
table.insert(state.lines, state.cy, "")
elseif state.cx > #state.lines[state.cy] then
table.insert(state.lines, state.cy + 1, "")
else
local line = state.lines[state.cy]
local before, after = line:sub(0, state.cx - 1), line:sub(state.cx)
state.lines[state.cy] = before
table.insert(state.lines, state.cy + 1, after)
end
state.cy = state.cy + 1
state.cx = 1
elseif id == "up" then
if state.cy > 1 then
state.cy = state.cy - 1
if state.cy - state.scroll < scroll_offset then
state.scroll = math.max(0, state.cy - scroll_offset)
end
end
state.cx = math.min(state.cx, #state.lines[state.cy] + 1)
elseif id == "down" then
if state.cy < #state.lines then
state.cy = state.cy + 1
if state.cy - state.scroll > h - scroll_offset then
state.scroll = math.max(0, math.min(#state.lines - h + 1,
state.cy - h + scroll_offset))
end
end
state.cx = math.min(state.cx, #state.lines[state.cy] + 1)
elseif id == "left" then
if state.cx > 1 then
state.cx = state.cx - 1
end
elseif id == "right" then
if state.cx < #state.lines[state.cy] + 1 then
state.cx = state.cx + 1
end
elseif id == "leftCtrl" or id == "rightCtrl" then
state.status = "S:save E:exit"
menu = true
end
end
end
term.clear()
while run do
term.setCursorBlink(false)
redraw()
term.setCursorBlink(true)
if menu then
processMenuInput()
else
processInput()
end
end

View File

@@ -0,0 +1,14 @@
LeonOS is ComputerCraft's LeonOS but with saner API design. It's also not licensed under the CCPL, but rather the MIT license -- so you can freely use LeonOS's code in other projects without being legally bound to license them under the CCPL.
All APIs are implemented as described on the CC: Tweaked wiki at
>>color blue
https://tweaked.cc
>>color white
, with slight modifications to fit LeonOS's API design. Certain modules not written by Dan200 have been adapted from LeonOS, relicensed under the MIT license with permission from their authors.
See
>>color blue
https://ocaweso.me/LeonOS
>>color white
for more information on LeonOS.

View File

@@ -0,0 +1,25 @@
-- keymap for minecraft 1.12.2 and older
return {
nil,
"one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
"zero", "minus", "equals", "backspace", "tab", "q", "w", "e", "r", "t", "y",
"u", "i", "o", "p", "leftBracket", "rightBracket", "enter", "leftCtrl",
"a", "s", "d", "f", "g", "h", "j", "k", "l", "semicolon", "apostrophe",
"grave", "leftShift", "backslash", "z", "x", "c", "v", "b", "n", "m",
"comma", "period", "slash", "rightShift", "multiply", "leftAlt", "space",
"capsLock", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10",
"numLock", "scrollLock", "numpad7", "numpad8", "numpad9", "numpadSubtract",
"numpad4", "numpad5", "numpad6", "numpadAdd", "numpad1", "numpad2", "numpad3",
"numpad0", "numpadDot", nil, nil, nil, "f11", "f12", nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, "f13", "f14", "f15", nil, nil, nil, nil, nil,
nil, nil, nil, nil, "kana", nil, nil, nil, nil, nil, nil, nil, nil, "convert",
nil, "noconvert", nil, "yen", nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, "numpadEquals", nil, nil, "circumflex", "at",
"colon", "underscore", "kanji", "stop", "ax", nil, nil, nil, nil, nil,
"numpadEnter", "rightCtrl", nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "numpadComma",
nil, "numpadDivide", nil, nil, "rightAlt", nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, "pause", nil, "home", "up", "pageUp", nil,
"left", nil, "right", nil, "end", "down", "pageDown", "insert", "delete"
}

View File

@@ -0,0 +1,120 @@
-- keymap for 1.16.5+
return {
[32] = "space",
[39] = "apostrophe",
[44] = "comma",
[45] = "minus",
[46] = "period",
[47] = "slash",
[48] = "zero",
[49] = "one",
[50] = "two",
[51] = "three",
[52] = "four",
[53] = "five",
[54] = "six",
[55] = "seven",
[56] = "eight",
[57] = "nine",
[59] = "semicolon",
[61] = "equals",
[65] = "a",
[66] = "b",
[67] = "c",
[68] = "d",
[69] = "e",
[70] = "f",
[71] = "g",
[72] = "h",
[73] = "i",
[74] = "j",
[75] = "k",
[76] = "l",
[77] = "m",
[78] = "n",
[79] = "o",
[80] = "p",
[81] = "q",
[82] = "r",
[83] = "s",
[84] = "t",
[85] = "u",
[86] = "v",
[87] = "w",
[88] = "x",
[89] = "y",
[90] = "z",
[91] = "leftBracket",
[92] = "backslash",
[93] = "rightBracket",
[96] = "grave",
[257] = "enter",
[258] = "tab",
[259] = "backspace",
[260] = "insert",
[261] = "delete",
[262] = "right",
[263] = "left",
[264] = "down",
[265] = "up",
[266] = "pageUp",
[267] = "pageDown",
[268] = "home",
[269] = "end",
[280] = "capsLock",
[281] = "scrollLock",
[282] = "numLock",
[283] = "printScreen",
[284] = "pause",
[290] = "f1",
[291] = "f2",
[292] = "f3",
[293] = "f4",
[294] = "f5",
[295] = "f6",
[296] = "f7",
[297] = "f8",
[298] = "f9",
[299] = "f10",
[300] = "f11",
[301] = "f12",
[302] = "f13",
[303] = "f14",
[304] = "f15",
[305] = "f16",
[306] = "f17",
[307] = "f18",
[308] = "f19",
[309] = "f20",
[310] = "f21",
[311] = "f22",
[312] = "f23",
[313] = "f24",
[314] = "f25",
[320] = "numpad0",
[321] = "numpad1",
[322] = "numpad2",
[323] = "numpad3",
[324] = "numpad4",
[325] = "numpad5",
[326] = "numpad6",
[327] = "numpad7",
[328] = "numpad8",
[329] = "numpad9",
[330] = "numpadDot",
[331] = "numpadDivide",
[332] = "numpadMultiply",
[333] = "numpadSubtract",
[334] = "numpadAdd",
[335] = "numpadEnter",
[336] = "numpadEqual",
[340] = "leftShift",
[341] = "leftCtrl",
[342] = "leftAlt",
[343] = "leftSuper",
[344] = "rightShift",
[345] = "rightCtrl",
[346] = "rightAlt",
[348] = "menu",
}

View File

@@ -0,0 +1,228 @@
--[[-
Provides utilities for converting between streams of DFPWM audio data and a list of amplitudes.
DFPWM (Dynamic Filter Pulse Width Modulation) is an audio codec designed by GreaseMonkey. It's a relatively compact
format compared to raw PCM data, only using 1 bit per sample, but is simple enough to simple enough to encode and decode
in real time.
Typically DFPWM audio is read from @{fs.BinaryReadHandle|the filesystem} or a @{http.Response|a web request} as a
string, and converted a format suitable for @{speaker.playAudio}.
## Encoding and decoding files
This modules exposes two key functions, @{make_decoder} and @{make_encoder}, which construct a new decoder or encoder.
The returned encoder/decoder is itself a function, which converts between the two kinds of data.
These encoders and decoders have lots of hidden state, so you should be careful to use the same encoder or decoder for
a specific audio stream. Typically you will want to create a decoder for each stream of audio you read, and an encoder
for each one you write.
## Converting audio to DFPWM
DFPWM is not a popular file format and so standard audio processing tools will not have an option to export to it.
Instead, you can convert audio files online using [music.madefor.cc] or with the [LionRay Wav Converter][LionRay] Java
application.
[music.madefor.cc]: https://music.madefor.cc/ "DFPWM audio converter for Computronics and CC: Tweaked"
[LionRay]: https://github.com/gamax92/LionRay/ "LionRay Wav Converter "
@see guide!speaker_audio Gives a more general introduction to audio processing and the speaker.
@see speaker.playAudio To play the decoded audio data.
@usage Reads "data/example.dfpwm" in chunks, decodes them and then doubles the speed of the audio. The resulting audio
is then re-encoded and saved to "speedy.dfpwm". This processed audio can then be played with the `speaker` program.
```lua
local dfpwm = require("cc.audio.dfpwm")
local encoder = dfpwm.make_encoder()
local decoder = dfpwm.make_decoder()
local out = fs.open("speedy.dfpwm", "wb")
for input in io.lines("data/example.dfpwm", 16 * 1024 * 2) do
local decoded = decoder(input)
local output = {}
-- Read two samples at once and take the average.
for i = 1, #decoded, 2 do
local value_1, value_2 = decoded[i], decoded[i + 1]
output[(i + 1) / 2] = (value_1 + value_2) / 2
end
out.write(encoder(output))
sleep(0) -- This program takes a while to run, so we need to make sure we yield.
end
out.close()
```
]]
local expect = require "cc.expect".expect
local char, byte, floor, band, rshift = string.char, string.byte, math.floor, bit32.band, bit32.arshift
local PREC = 10
local PREC_POW = 2 ^ PREC
local PREC_POW_HALF = 2 ^ (PREC - 1)
local STRENGTH_MIN = 2 ^ (PREC - 8 + 1)
local function make_predictor()
local charge, strength, previous_bit = 0, 0, false
return function(current_bit)
local target = current_bit and 127 or -128
local next_charge = charge + floor((strength * (target - charge) + PREC_POW_HALF) / PREC_POW)
if next_charge == charge and next_charge ~= target then
next_charge = next_charge + (current_bit and 1 or -1)
end
local z = current_bit == previous_bit and PREC_POW - 1 or 0
local next_strength = strength
if next_strength ~= z then next_strength = next_strength + (current_bit == previous_bit and 1 or -1) end
if next_strength < STRENGTH_MIN then next_strength = STRENGTH_MIN end
charge, strength, previous_bit = next_charge, next_strength, current_bit
return charge
end
end
--[[- Create a new encoder for converting PCM audio data into DFPWM.
The returned encoder is itself a function. This function accepts a table of amplitude data between -128 and 127 and
returns the encoded DFPWM data.
:::caution Reusing encoders
Encoders have lots of internal state which tracks the state of the current stream. If you reuse an encoder for multiple
streams, or use different encoders for the same stream, the resulting audio may not sound correct.
:::
@treturn function(pcm: { number... }):string The encoder function
@see encode A helper function for encoding an entire file of audio at once.
]]
local function make_encoder()
local predictor = make_predictor()
local previous_charge = 0
return function(input)
expect(1, input, "table")
local output, output_n = {}, 0
for i = 1, #input, 8 do
local this_byte = 0
for j = 0, 7 do
local inp_charge = floor(input[i + j] or 0)
if inp_charge > 127 or inp_charge < -128 then
error(("Amplitude at position %d was %d, but should be between -128 and 127"):format(i + j, inp_charge), 2)
end
local current_bit = inp_charge > previous_charge or (inp_charge == previous_charge and inp_charge == 127)
this_byte = floor(this_byte / 2) + (current_bit and 128 or 0)
previous_charge = predictor(current_bit)
end
output_n = output_n + 1
output[output_n] = char(this_byte)
end
return table.concat(output, "", 1, output_n)
end
end
--[[- Create a new decoder for converting DFPWM into PCM audio data.
The returned decoder is itself a function. This function accepts a string and returns a table of amplitudes, each value
between -128 and 127.
:::caution Reusing decoders
Decoders have lots of internal state which tracks the state of the current stream. If you reuse an decoder for multiple
streams, or use different decoders for the same stream, the resulting audio may not sound correct.
:::
@treturn function(dfpwm: string):{ number... } The encoder function
@see decode A helper function for decoding an entire file of audio at once.
@usage Reads "data/example.dfpwm" in blocks of 16KiB (the speaker can accept a maximum of 128×1024 samples), decodes
them and then plays them through the speaker.
```lua {data-peripheral=speaker}
local dfpwm = require "cc.audio.dfpwm"
local speaker = peripheral.find("speaker")
local decoder = dfpwm.make_decoder()
for input in io.lines("data/example.dfpwm", 16 * 1024) do
local decoded = decoder(input)
while not speaker.playAudio(decoded) do
os.pullEvent("speaker_audio_empty")
end
end
```
]]
local function make_decoder()
local predictor = make_predictor()
local low_pass_charge = 0
local previous_charge, previous_bit = 0, false
return function (input, output)
expect(1, input, "string")
local output, output_n = {}, 0
for i = 1, #input do
local input_byte = byte(input, i)
for _ = 1, 8 do
local current_bit = band(input_byte, 1) ~= 0
local charge = predictor(current_bit)
local antijerk = charge
if current_bit ~= previous_bit then
antijerk = floor((charge + previous_charge + 1) / 2)
end
previous_charge, previous_bit = charge, current_bit
low_pass_charge = low_pass_charge + floor(((antijerk - low_pass_charge) * 140 + 0x80) / 256)
output_n = output_n + 1
output[output_n] = low_pass_charge
input_byte = rshift(input_byte, 1)
end
end
return output
end
end
--[[- A convenience function for decoding a complete file of audio at once.
This should only be used for short files. For larger files, one should read the file in chunks and process it using
@{make_decoder}.
@tparam string input The DFPWM data to convert.
@treturn { number... } The produced amplitude data.
@see make_decoder
]]
local function decode(input)
expect(1, input, "string")
return make_decoder()(input)
end
--[[- A convenience function for encoding a complete file of audio at once.
This should only be used for complete pieces of audio. If you are writing writing multiple chunks to the same place,
you should use an encoder returned by @{make_encoder} instead.
@tparam { number... } input The table of amplitude data.
@treturn string The encoded DFPWM data.
@see make_encoder
]]
local function encode(input)
expect(1, input, "table")
return make_encoder()(input)
end
return {
make_encoder = make_encoder,
encode = encode,
make_decoder = make_decoder,
decode = decode,
}

View File

@@ -0,0 +1,45 @@
-- cc.completion
local expect = require("cc.expect").expect
local settings = require("settings")
local peripheral = require("peripheral")
local c = {}
-- choices and options!
-- extension: add_space can be a table of booleans
function c.choice(text, choices, add_space)
expect(1, text, "string")
expect(2, choices, "table")
expect(3, add_space, "boolean", "table", "nil")
local options = {}
for i=1, #choices, 1 do
local add = add_space
if type(add_space) == "table" then
add = add_space[i] or add_space[1]
end
if choices[i]:sub(0, #text) == text then
options[#options+1] = choices[i]:sub(#text+1) .. (add and " " or "")
end
end
return options
end
function c.peripheral(text, add_space)
return c.choice(text, peripheral.getNames(), add_space)
end
local sides = {"front", "back", "top", "bottom", "left", "right"}
function c.side(text, add_space)
return c.choice(text, sides, add_space)
end
function c.setting(text, add_space)
return c.choice(text, settings.getNames(), add_space)
end
return c

View File

@@ -0,0 +1,50 @@
-- cc.expect
local _expect = {}
local function checkType(index, valueType, value, ...)
local expected = table.pack(...)
local isType = false
for i=1, expected.n, 1 do
if type(value) == expected[i] then
isType = true
break
end
end
if not isType then
error(string.format("bad %s %s (%s expected, got %s)", valueType,
index, table.concat(expected, " or "), type(value)), 3)
end
return value
end
function _expect.expect(index, value, ...)
return checkType(("#%d"):format(index), "argument", value, ...)
end
function _expect.field(tbl, index, ...)
_expect.expect(1, tbl, "table")
_expect.expect(2, index, "string")
return checkType(("%q"):format(index), "field", tbl[index], ...)
end
function _expect.range(num, min, max)
_expect.expect(1, num, "number")
_expect.expect(2, min, "number", "nil")
_expect.expect(3, max, "number", "nil")
min = min or -math.huge
max = max or math.huge
if num < min or num > max then
error(("number outside of range (expected %d to be within %d and %d")
:format(num, min, max), 2)
end
end
setmetatable(_expect, {__call = function(_, ...)
return _expect.expect(...)
end})
return _expect

View File

@@ -0,0 +1,293 @@
--- gist.lua - Gist client for ComputerCraft
-- Made by JackMacWindows for LeonOS-PC and CC: Tweaked
--
-- @module cc.http.gist
local textutils = require("textutils")
local settings = require("settings")
local expect = require("cc.expect").expect
local colors = require("colors")
local write = require("rc").write
local term = require("term")
local http = require("http")
local read = term.read
local gist = {}
local function emptyfn() end -- to reduce memory/speed footprint when using empty functions
-- Internal functions
local function getGistFile(data)
if not data.truncated then return data.content else
local handle = http.get(data.raw_url)
if not handle then error("Could not connect to api.github.com.") end
if handle.getResponseCode() ~= 200 then
handle.close()
error("Failed to download file data.")
end
local d = handle.readAll()
handle.close()
return d
end
end
local function setTextColor(c) if term.isColor() then term.setTextColor(c) elseif c == colors.white or c == colors.yellow then term.setTextColor(colors.white) else term.setTextColor(colors.lightGray) end end
local function requestAuth(headers, interactive)
if settings.get("gist.id") ~= nil then
headers.Authorization = "token " .. settings.get("gist.id")
return true
elseif interactive then
setTextColor(colors.yellow)
write("You need to add a Personal Access Token (PAK) to upload Gists. Follow the instructions at ")
setTextColor(colors.blue)
write("https://tinyurl.com/GitHubPAK")
setTextColor(colors.yellow)
write(" to generate one. Make sure to check the '")
setTextColor(colors.blue)
write("gist")
setTextColor(colors.yellow)
print("' checkbox on step 7 (under 'Select scopes'). Once done, paste it here.")
setTextColor(colors.lime)
write("PAK: ")
setTextColor(colors.white)
local pak = read()
if pak == nil or pak == "" then error("Invalid PAK, please try again.") end
settings.set("gist.id", pak)
settings.save(".settings")
headers.Authorization = "token " .. pak
return true
end
return false
end
-- User API - this can be loaded with require "cc.http.gist"
-- ID can be either just the gist ID or a gist ID followed by a slash and a file name
-- (This also includes Gist URLs)
-- * If a file name is specified, retrieves that file
-- * Otherwise, if there's only one file, retrieves that file
-- * Otherwise, if there's a file named 'init.lua', retrieves 'init.lua'
-- * Otherwise, if there's more than one file but only one *.lua file, retrieves the Lua file
-- * Otherwise, retrieves the first Lua file alphabetically (with a warning)
-- * Otherwise, fails
--- Retrieves one file from a Gist using the specified ID.
-- @tparam string id The Gist ID to download from. See above comments for more details.
-- @tparam[opt] function progress A function to use to report status messages.
-- @treturn string|nil The contents of the specified Gist file, or nil on error.
-- @treturn string|nil The name of the file that was chosen to be downloaded, or a message on error.
function gist.get(id, progress)
expect(1, id, "string")
expect(2, progress, "function", "nil")
progress = progress or emptyfn
local file
if id:find("https?://") then id = id:gsub("https?://[^/]+/", ""):gsub("^[^/]*[^/%x]+[^/]*/", "") end
if id:find("/") ~= nil then id, file = id:match("^([0-9A-Fa-f:]+)/(.+)$") end
if id == nil or not id:match("^[0-9A-Fa-f][0-9A-Fa-f:]+[0-9A-Fa-f]$") then error("bad argument #1 to 'get' (invalid ID)", 2) end
if id:find(":") ~= nil then id = id:gsub(":", "/") end
progress("Connecting to api.github.com... ")
local handle = http.get("https://api.github.com/gists/" .. id)
if handle == nil then
progress("Failed.\n")
return nil, "Failed to connect"
end
local meta = textutils.unserializeJSON(handle.readAll())
local code = handle.getResponseCode()
handle.close()
if code ~= 200 then
progress("Failed.\n")
return nil, "Invalid response code (" .. code .. ")" .. (meta and ": " .. meta.message or "")
end
if meta == nil or meta.files == nil then
progress("Failed.\n")
return nil, meta and "GitHub API error: " .. meta.message or "Error parsing JSON"
end
progress("Success.\n")
if file then return getGistFile(meta.files[file]), file
elseif next(meta.files, next(meta.files)) == nil then return getGistFile(meta.files[next(meta.files)]), next(meta.files)
elseif meta.files["init.lua"] ~= nil then return getGistFile(meta.files["init.lua"]), "init.lua"
else
local luaFiles = {}
for k in pairs(meta.files) do if k:match("%.lua$") then table.insert(luaFiles, k) end end
table.sort(luaFiles)
if #luaFiles == 0 then
progress("Error: Could not find any Lua files to download!\n")
return nil, "Could not find any Lua files to download"
end
if #luaFiles > 1 then progress("Warning: More than one Lua file detected, downloading the first one alphabetically.\n") end
return getGistFile(meta.files[luaFiles[1]]), luaFiles[1]
end
end
--- Runs a specified Gist. This is a wrapper for convenience.
-- @tparam string id The Gist ID to download from. See above comments for more details.
-- @tparam[opt] function progress A function to use to report status messages. If
-- this is not a function, it will be used as an argument to the script.
-- @tparam[opt] any ... Any arguments to pass to the script.
-- @treturn any Any results returned from the script.
function gist.run(id, progress, ...)
expect(1, id, "string")
local args = table.pack(...)
if type(progress) ~= "function" and progress ~= nil then
table.insert(args, 1, progress)
progress = nil
end
local data, name = gist.get(id, progress)
if data == nil then return end
local fn, err = load(data, name, "t", _ENV)
if fn == nil then error(err) end
local retval = table.pack(pcall(fn, table.unpack(args)))
if not retval[1] then error(retval[2]) end
return table.unpack(retval, 2)
end
--- Retrieves a table of all files from a Gist.
-- @tparam string id The Gist ID to download.
-- @tparam[opt] function progress A function to use to report status messages.
-- @treturn table|nil A key-value list of all files in the Gist, or nil on error.
-- @treturn string|nil If an error occurred, a string describing the error.
function gist.getAll(id, progress)
expect(1, id, "string")
expect(2, progress, "function", "nil")
progress = progress or emptyfn
if id:find("https?://") then id = id:gsub("https?://[^/]+/", ""):gsub("^[^/]*[^/%x]+[^/]*/", "") end
if id:find("/") ~= nil then id = id:match("^([0-9A-Fa-f:]+)/.*$") end
if id == nil or not id:match("^[0-9A-Fa-f][0-9A-Fa-f:]+[0-9A-Fa-f]$") then error("bad argument #1 to 'getAll' (invalid ID)", 2) end
if id:find(":") ~= nil then id = id:gsub(":", "/") end
progress("Connecting to api.github.com... ")
local handle = http.get("https://api.github.com/gists/" .. id)
if handle == nil then progress("Failed.\n") return nil, "Failed to connect" end
local meta = textutils.unserializeJSON(handle.readAll())
local code = handle.getResponseCode()
handle.close()
if code ~= 200 then
progress("Failed.\n")
return nil, "Invalid response code (" .. code .. ")" .. (meta and ": " .. meta.message or "")
end
if meta == nil or meta.files == nil then
progress("Failed.\n")
return nil, meta and meta.message and "GitHub API error: " .. meta.message or "Error parsing JSON"
end
progress("Success.\n")
local retval = {}
for k, v in pairs(meta.files) do retval[k] = getGistFile(v) end
return retval
end
--- Returns some information about a Gist.
-- @tparam string id The Gist ID to get info about.
-- @tparam[opt] function progress A function to use to report status messages.
-- @treturn table|nil A table of information about the Gist. The table may
-- contain the following entries:
-- - description: The description for the Gist.
-- - author: The username of the author of the Gist.
-- - revisionCount: The number of revisions that have been made to the Gist.
-- - files: A list of all file names in the Gist, sorted alphabetically.
-- @treturn string|nil If an error occurred, a string describing the error.
function gist.info(id, progress)
expect(1, id, "string")
expect(2, progress, "function", "nil")
progress = progress or emptyfn
if id:find("https?://") then id = id:gsub("https?://[^/]+/", ""):gsub("^[^/]*[^/%x]+[^/]*/", "") end
if id:find("/") ~= nil then id = id:match("^([0-9A-Fa-f:]+)/.*$") end
if id == nil or not id:match("^[0-9A-Fa-f][0-9A-Fa-f:]+[0-9A-Fa-f]$") then error("bad argument #1 to 'info' (invalid ID)", 2) end
if id:find(":") ~= nil then id = id:gsub(":", "/") end
progress("Connecting to api.github.com... ")
local handle = http.get("https://api.github.com/gists/" .. id)
if handle == nil then progress("Failed.\n") return nil, "Failed to connect" end
local meta = textutils.unserializeJSON(handle.readAll())
local code = handle.getResponseCode()
handle.close()
if code ~= 200 then
progress("Failed.\n")
return nil, "Invalid response code (" .. code .. ")" .. (meta and ": " .. meta.message or "")
end
if meta == nil or meta.files == nil then
progress("Failed.\n")
return nil, meta and meta.message and "GitHub API error: " .. meta.message or "Error parsing JSON"
end
local f = {}
for k in pairs(meta.files) do table.insert(f, k) end
table.sort(f)
progress("Success.\n")
return { description = meta.description, author = meta.owner.login, revisionCount = #meta.history, files = f }
end
--- Uploads a list of files to Gist, updating a previous Gist if desired.
-- @tparam table files The files to upload to Gist. This table should be
-- structured with a key as file name and a string with the file contents. If
-- updating a Gist, files can be deleted by setting the data to textutils.json_null.
-- @tparam[opt] string description The description of the Gist. This is required
-- when updating a Gist, but is optional when uploading a Gist for the first
-- time. If you don't want to change the description when updating, you can get
-- the current description with gist.info() and pass in the description field.
-- @tparam[opt] string id The ID of the Gist to update. If nil, a new Gist will
-- be created.
-- @tparam[opt] boolean interactive Set this to true to allow asking for a PAK
-- if one is not available in the settings. If this is not specified, this
-- function will return nil if gist.id is not available in the settings.
-- @treturn string|nil The ID of the Gist, or nil on error.
-- @treturn string|nil The URL of the Gist, or a string on error.
function gist.put(files, description, id, interactive)
expect(1, files, "table")
expect(3, id, "string", "nil")
expect(2, description, "string", id == nil and "nil" or nil)
expect(4, interactive, "boolean", "nil")
if id then
if id:find("https?://") then id = id:gsub("https?://[^/]+/", ""):gsub("^[^/]*[^/%x]+[^/]*/", "") end
if id:find("/") ~= nil then id = id:match("^([0-9A-Fa-f:]+)/.*$") end
if id == nil or not id:match("^[0-9A-Fa-f][0-9A-Fa-f:]+[0-9A-Fa-f]$") then error("bad argument #3 to 'put' (invalid ID)", 2) end
if id:find(":") ~= nil then id = id:gsub(":", "/") end
end
local data = { files = {}, public = true, description = description }
for k, v in pairs(files) do if v == textutils.json_null then data.files[k] = v else data.files[k] = { content = v } end end
local headers = { ["Content-Type"] = "application/json" }
if not requestAuth(headers, interactive) then return nil, "Authentication required" end
if interactive then write("Connecting to api.github.com... ") end
local handle
if id then handle = http.post{ url = "https://api.github.com/gists/" .. id, body = textutils.serializeJSON(data):gsub("\n", "n"), headers = headers, method = "PATCH" }
else handle = http.post("https://api.github.com/gists", textutils.serializeJSON(data):gsub("\n", "n"), headers) end
if handle == nil then if interactive then print("Failed.") end return nil, "Could not connect" end
local resp = textutils.unserializeJSON(handle.readAll())
if handle.getResponseCode() ~= 201 and handle.getResponseCode() ~= 200 or resp == nil then
if interactive then print("Failed: " .. handle.getResponseCode() .. ": " .. (resp and resp.message or "Unknown error")) end
handle.close()
return nil, "Failed: " .. handle.getResponseCode() .. ": " .. (resp and resp.message or "Unknown error")
end
handle.close()
if interactive then print("Success.") end
return resp.id, resp.html_url
end
--- Deletes a Gist.
-- @tparam string id The Gist ID to delete.
-- @tparam[opt] boolean interactive Set this to true to allow asking for a PAK
-- if one is not available in the settings. If this is not specified, this
-- function will return false if gist.id is not available in the settings.
-- @treturn boolean Whether the request succeeded.
-- @treturn string|nil If an error occurred, a message describing the error.
function gist.delete(id, interactive)
expect(1, id, "string")
expect(2, interactive, "boolean", "nil")
if id:find("https?://") then id = id:gsub("https?://[^/]+/", ""):gsub("^[^/]*[^/%x]+[^/]*/", "") end
if id:find("/") ~= nil or id:find(":") ~= nil then id = id:match("^([0-9A-Fa-f]+)") end
if id == nil or not id:match("^[0-9A-Fa-f][0-9A-Fa-f:]+[0-9A-Fa-f]$") then error("bad argument #1 to 'delete' (invalid ID)", 2) end
local headers = {}
if not requestAuth(headers, interactive) then return false, "Authentication required" end
if interactive then write("Connecting to api.github.com... ") end
local handle = http.post{ url = "https://api.github.com/gists/" .. id, headers = headers, method = "DELETE" }
if handle == nil then if interactive then print("Failed.") end return false, "Could not connect" end
if handle.getResponseCode() ~= 204 then
local resp = textutils.unserializeJSON(handle.readAll())
if interactive then print("Failed: " .. handle.getResponseCode() .. ": " .. (resp and resp.message or "Unknown error")) end
handle.close()
return false, "Failed: " .. handle.getResponseCode() .. ": " .. (resp and resp.message or "Unknown error")
end
handle.close()
if interactive then print("Success.") end
return true
end
return gist

View File

@@ -0,0 +1,523 @@
--[[- Provides a "pretty printer", for rendering data structures in an
aesthetically pleasing manner.
In order to display something using @{cc.pretty}, you build up a series of
@{Doc|documents}. These behave a little bit like strings; you can concatenate
them together and then print them to the screen.
However, documents also allow you to control how they should be printed. There
are several functions (such as @{nest} and @{group}) which allow you to control
the "layout" of the document. When you come to display the document, the 'best'
(most compact) layout is used.
The structure of this module is based on [A Prettier Printer][prettier].
[prettier]: https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf "A Prettier Printer"
@module cc.pretty
@since 1.87.0
@usage Print a table to the terminal
local pretty = require "cc.pretty"
pretty.pretty_print({ 1, 2, 3 })
@usage Build a custom document and display it
local pretty = require "cc.pretty"
pretty.print(pretty.group(pretty.text("hello") .. pretty.space_line .. pretty.text("world")))
]]
local _expect = require "cc.expect"
local expect, field = _expect.expect, _expect.field
local term = require("term")
local type, getmetatable, setmetatable, colours, str_write, tostring = type, getmetatable, setmetatable, require("colours"), require("rc").write, tostring
local debug_info, debug_local = debug.getinfo, debug.getlocal
--- @{table.insert} alternative, but with the length stored inline.
local function append(out, value)
local n = out.n + 1
out[n], out.n = value, n
end
--- A document containing formatted text, with multiple possible layouts.
--
-- Documents effectively represent a sequence of strings in alternative layouts,
-- which we will try to print in the most compact form necessary.
--
-- @type Doc
local Doc = { }
local function mk_doc(tbl) return setmetatable(tbl, Doc) end
--- An empty document.
local empty = mk_doc({ tag = "nil" })
--- A document with a single space in it.
local space = mk_doc({ tag = "text", text = " " })
--- A line break. When collapsed with @{group}, this will be replaced with @{empty}.
local line = mk_doc({ tag = "line", flat = empty })
--- A line break. When collapsed with @{group}, this will be replaced with @{space}.
local space_line = mk_doc({ tag = "line", flat = space })
local text_cache = { [""] = empty, [" "] = space, ["\n"] = space_line }
local function mk_text(text, colour)
return text_cache[text] or setmetatable({ tag = "text", text = text, colour = colour }, Doc)
end
--- Create a new document from a string.
--
-- If your string contains multiple lines, @{group} will flatten the string
-- into a single line, with spaces between each line.
--
-- @tparam string text The string to construct a new document with.
-- @tparam[opt] number colour The colour this text should be printed with. If not given, we default to the current
-- colour.
-- @treturn Doc The document with the provided text.
-- @usage Write some blue text.
--
-- local pretty = require "cc.pretty"
-- pretty.print(pretty.text("Hello!", colours.blue))
local function text(text, colour)
expect(1, text, "string")
expect(2, colour, "number", "nil")
local cached = text_cache[text]
if cached then return cached end
local new_line = text:find("\n", 1)
if not new_line then return mk_text(text, colour) end
-- Split the string by "\n". With a micro-optimisation to skip empty strings.
local doc = setmetatable({ tag = "concat", n = 0 }, Doc)
if new_line ~= 1 then append(doc, mk_text(text:sub(1, new_line - 1), colour)) end
new_line = new_line + 1
while true do
local next_line = text:find("\n", new_line)
append(doc, space_line)
if not next_line then
if new_line <= #text then append(doc, mk_text(text:sub(new_line), colour)) end
return doc
else
if new_line <= next_line - 1 then
append(doc, mk_text(text:sub(new_line, next_line - 1), colour))
end
new_line = next_line + 1
end
end
end
--- Concatenate several documents together. This behaves very similar to string concatenation.
--
-- @tparam Doc|string ... The documents to concatenate.
-- @treturn Doc The concatenated documents.
-- @usage
-- local pretty = require "cc.pretty"
-- local doc1, doc2 = pretty.text("doc1"), pretty.text("doc2")
-- print(pretty.concat(doc1, " - ", doc2))
-- print(doc1 .. " - " .. doc2) -- Also supports ..
local function concat(...)
local args = table.pack(...)
for i = 1, args.n do
if type(args[i]) == "string" then args[i] = text(args[i]) end
if getmetatable(args[i]) ~= Doc then expect(i, args[i], "document") end
end
if args.n == 0 then return empty end
if args.n == 1 then return args[1] end
args.tag = "concat"
return setmetatable(args, Doc)
end
Doc.__concat = concat --- @local
--- Indent later lines of the given document with the given number of spaces.
--
-- For instance, nesting the document
-- ```txt
-- foo
-- bar
-- ```
-- by two spaces will produce
-- ```txt
-- foo
-- bar
-- ```
--
-- @tparam number depth The number of spaces with which the document should be indented.
-- @tparam Doc doc The document to indent.
-- @treturn Doc The nested document.
-- @usage
-- local pretty = require "cc.pretty"
-- print(pretty.nest(2, pretty.text("foo\nbar")))
local function nest(depth, doc)
expect(1, depth, "number")
if getmetatable(doc) ~= Doc then expect(2, doc, "document") end
if depth <= 0 then error("depth must be a positive number", 2) end
return setmetatable({ tag = "nest", depth = depth, doc }, Doc)
end
local function flatten(doc)
if doc.flat then return doc.flat end
local kind = doc.tag
if kind == "nil" or kind == "text" then
return doc
elseif kind == "concat" then
local out = setmetatable({ tag = "concat", n = doc.n }, Doc)
for i = 1, doc.n do out[i] = flatten(doc[i]) end
doc.flat, out.flat = out, out -- cache the flattened node
return out
elseif kind == "nest" then
return flatten(doc[1])
elseif kind == "group" then
return doc[1]
else
error("Unknown doc " .. kind)
end
end
--- Builds a document which is displayed on a single line if there is enough
-- room, or as normal if not.
--
-- @tparam Doc doc The document to group.
-- @treturn Doc The grouped document.
-- @usage Uses group to show things being displayed on one or multiple lines.
--
-- local pretty = require "cc.pretty"
-- local doc = pretty.group("Hello" .. pretty.space_line .. "World")
-- print(pretty.render(doc, 5)) -- On multiple lines
-- print(pretty.render(doc, 20)) -- Collapsed onto one.
local function group(doc)
if getmetatable(doc) ~= Doc then expect(1, doc, "document") end
if doc.tag == "group" then return doc end -- Skip if already grouped.
local flattened = flatten(doc)
if flattened == doc then return doc end -- Also skip if flattening does nothing.
return setmetatable({ tag = "group", flattened, doc }, Doc)
end
local function get_remaining(doc, width)
local kind = doc.tag
if kind == "nil" or kind == "line" then
return width
elseif kind == "text" then
return width - #doc.text
elseif kind == "concat" then
for i = 1, doc.n do
width = get_remaining(doc[i], width)
if width < 0 then break end
end
return width
elseif kind == "group" or kind == "nest" then
return get_remaining(kind[1])
else
error("Unknown doc " .. kind)
end
end
--- Display a document on the terminal.
--
-- @tparam Doc doc The document to render
-- @tparam[opt=0.6] number ribbon_frac The maximum fraction of the width that we should write in.
local function write(doc, ribbon_frac)
if getmetatable(doc) ~= Doc then expect(1, doc, "document") end
expect(2, ribbon_frac, "number", "nil")
local term = term
local width, height = term.getSize()
local ribbon_width = (ribbon_frac or 0.6) * width
if ribbon_width < 0 then ribbon_width = 0 end
if ribbon_width > width then ribbon_width = width end
local def_colour = term.getTextColour()
local current_colour = def_colour
local function go(doc, indent, col)
local kind = doc.tag
if kind == "nil" then
return col
elseif kind == "text" then
local doc_colour = doc.colour or def_colour
if doc_colour ~= current_colour then
term.setTextColour(doc_colour)
current_colour = doc_colour
end
str_write(doc.text)
return col + #doc.text
elseif kind == "line" then
local _, y = term.getCursorPos()
if y < height then
term.setCursorPos(indent + 1, y + 1)
else
term.scroll(1)
term.setCursorPos(indent + 1, height)
end
return indent
elseif kind == "concat" then
for i = 1, doc.n do col = go(doc[i], indent, col) end
return col
elseif kind == "nest" then
return go(doc[1], indent + doc.depth, col)
elseif kind == "group" then
if get_remaining(doc[1], math.min(width, ribbon_width + indent) - col) >= 0 then
return go(doc[1], indent, col)
else
return go(doc[2], indent, col)
end
else
error("Unknown doc " .. kind)
end
end
local col = math.max(term.getCursorPos() - 1, 0)
go(doc, 0, col)
if current_colour ~= def_colour then term.setTextColour(def_colour) end
end
--- Display a document on the terminal with a trailing new line.
--
-- @tparam Doc doc The document to render.
-- @tparam[opt=0.6] number ribbon_frac The maximum fraction of the width that we should write in.
local function print(doc, ribbon_frac)
if getmetatable(doc) ~= Doc then expect(1, doc, "document") end
expect(2, ribbon_frac, "number", "nil")
write(doc, ribbon_frac)
str_write("\n")
end
--- Render a document, converting it into a string.
--
-- @tparam Doc doc The document to render.
-- @tparam[opt] number width The maximum width of this document. Note that long strings will not be wrapped to
-- fit this width - it is only used for finding the best layout.
-- @tparam[opt=0.6] number ribbon_frac The maximum fraction of the width that we should write in.
-- @treturn string The rendered document as a string.
local function render(doc, width, ribbon_frac)
if getmetatable(doc) ~= Doc then expect(1, doc, "document") end
expect(2, width, "number", "nil")
expect(3, ribbon_frac, "number", "nil")
local ribbon_width
if width then
ribbon_width = (ribbon_frac or 0.6) * width
if ribbon_width < 0 then ribbon_width = 0 end
if ribbon_width > width then ribbon_width = width end
end
local out = { n = 0 }
local function go(doc, indent, col)
local kind = doc.tag
if kind == "nil" then
return col
elseif kind == "text" then
append(out, doc.text)
return col + #doc.text
elseif kind == "line" then
append(out, "\n" .. (" "):rep(indent))
return indent
elseif kind == "concat" then
for i = 1, doc.n do col = go(doc[i], indent, col) end
return col
elseif kind == "nest" then
return go(doc[1], indent + doc.depth, col)
elseif kind == "group" then
if not width or get_remaining(doc[1], math.min(width, ribbon_width + indent) - col) >= 0 then
return go(doc[1], indent, col)
else
return go(doc[2], indent, col)
end
else
error("Unknown doc " .. kind)
end
end
go(doc, 0, 0)
return table.concat(out, "", 1, out.n)
end
Doc.__tostring = render --- @local
local keywords = {
["and"] = true, ["break"] = true, ["do"] = true, ["else"] = true,
["elseif"] = true, ["end"] = true, ["false"] = true, ["for"] = true,
["function"] = true, ["if"] = true, ["in"] = true, ["local"] = true,
["nil"] = true, ["not"] = true, ["or"] = true, ["repeat"] = true, ["return"] = true,
["then"] = true, ["true"] = true, ["until"] = true, ["while"] = true,
}
local comma = text(",")
local braces = text("{}")
local obrace, cbrace = text("{"), text("}")
local obracket, cbracket = text("["), text("] = ")
local function key_compare(a, b)
local ta, tb = type(a), type(b)
if ta == "string" then return tb ~= "string" or a < b
elseif tb == "string" then return false
end
if ta == "number" then return tb ~= "number" or a < b end
return false
end
local function show_function(fn, options)
local info = debug_info and debug_info(fn, "Su")
-- Include function source position if available
local name
if options.function_source and info and info.short_src and info.linedefined and info.linedefined >= 1 then
name = "function<" .. info.short_src .. ":" .. info.linedefined .. ">"
else
name = tostring(fn)
end
-- Include arguments if a Lua function and if available. Lua will report "C"
-- functions as variadic.
if options.function_args and info and info.what == "Lua" and info.nparams and debug_local then
local args = {}
for i = 1, info.nparams do args[i] = debug_local(fn, i) or "?" end
if info.isvararg then args[#args + 1] = "..." end
name = name .. "(" .. table.concat(args, ", ") .. ")"
end
return name
end
local function pretty_impl(obj, options, tracking)
local obj_type = type(obj)
if obj_type == "string" then
local formatted = ("%q"):format(obj):gsub("\\\n", "\\n")
return text(formatted, colours.red)
elseif obj_type == "number" then
return text(tostring(obj), colours.magenta)
elseif obj_type == "function" then
return text(show_function(obj, options), colours.lightGrey)
elseif obj_type ~= "table" or tracking[obj] then
return text(tostring(obj), colours.lightGrey)
elseif getmetatable(obj) ~= nil and getmetatable(obj).__tostring then
return text(tostring(obj))
elseif next(obj) == nil then
return braces
else
tracking[obj] = true
local doc = setmetatable({ tag = "concat", n = 1, space_line }, Doc)
local length, keys, keysn = #obj, {}, 1
for k in pairs(obj) do
if type(k) ~= "number" or k % 1 ~= 0 or k < 1 or k > length then
keys[keysn], keysn = k, keysn + 1
end
end
table.sort(keys, key_compare)
for i = 1, length do
if i > 1 then append(doc, comma) append(doc, space_line) end
append(doc, pretty_impl(obj[i], options, tracking))
end
for i = 1, keysn - 1 do
if i > 1 or length >= 1 then append(doc, comma) append(doc, space_line) end
local k = keys[i]
local v = obj[k]
if type(k) == "string" and not keywords[k] and k:match("^[%a_][%a%d_]*$") then
append(doc, text(k .. " = "))
append(doc, pretty_impl(v, options, tracking))
else
append(doc, obracket)
append(doc, pretty_impl(k, options, tracking))
append(doc, cbracket)
append(doc, pretty_impl(v, options, tracking))
end
end
tracking[obj] = nil
return group(concat(obrace, nest(2, concat(table.unpack(doc, 1, doc.n))), space_line, cbrace))
end
end
--- Pretty-print an arbitrary object, converting it into a document.
--
-- This can then be rendered with @{write} or @{print}.
--
-- @param obj The object to pretty-print.
-- @tparam[opt] { function_args = boolean, function_source = boolean } options
-- Controls how various properties are displayed.
-- - `function_args`: Show the arguments to a function if known (`false` by default).
-- - `function_source`: Show where the function was defined, instead of
-- `function: xxxxxxxx` (`false` by default).
-- @treturn Doc The object formatted as a document.
-- @changed 1.88.0 Added `options` argument.
-- @usage Display a table on the screen
--
-- local pretty = require "cc.pretty"
-- pretty.print(pretty.pretty({ 1, 2, 3 }))
-- @see pretty_print for a shorthand to prettify and print an object.
local function pretty(obj, options)
expect(2, options, "table", "nil")
options = options or {}
local actual_options = {
function_source = field(options, "function_source", "boolean", "nil") or false,
function_args = field(options, "function_args", "boolean", "nil") or false,
}
return pretty_impl(obj, actual_options, {})
end
--[[- A shortcut for calling @{pretty} and @{print} together.
@param obj The object to pretty-print.
@tparam[opt] { function_args = boolean, function_source = boolean } options
Controls how various properties are displayed.
- `function_args`: Show the arguments to a function if known (`false` by default).
- `function_source`: Show where the function was defined, instead of
`function: xxxxxxxx` (`false` by default).
@tparam[opt=0.6] number ribbon_frac The maximum fraction of the width that we should write in.
@usage Display a table on the screen.
local pretty = require "cc.pretty"
pretty.pretty_print({ 1, 2, 3 })
@see pretty
@see print
@since 1.99
]]
local function pretty_print(obj, options, ribbon_frac)
expect(2, options, "table", "nil")
options = options or {}
expect(3, ribbon_frac, "number", "nil")
return print(pretty(obj, options), ribbon_frac)
end
return {
empty = empty,
space = space,
line = line,
space_line = space_line,
text = text,
concat = concat,
nest = nest,
group = group,
write = write,
print = print,
render = render,
pretty = pretty,
pretty_print = pretty_print,
}

View File

@@ -0,0 +1,113 @@
-- cc.shell.completion
-- XXX incompatible: functions do not accept a 'shell' argument
local completion = require("cc.completion")
local expect = require("cc.expect").expect
local shell = require("shell")
local fs = require("fs")
local c = {}
function c.file(text)
expect(1, text, "string")
return fs.complete(text, shell.dir(), true, false)
end
function c.dir(text)
expect(1, text, "string")
return fs.complete(text, shell.dir(), false)
end
function c.dirOrFile(text, previous, add_space)
expect(1, text, "string")
expect(2, previous, "table")
expect(3, add_space, "boolean", "nil")
local completed = fs.complete(text, shell.dir(), true, true)
if add_space then
for i=1, #completed, 1 do
completed[i] = completed[i] .. " "
end
end
return completed
end
function c.program(text, add_space)
expect(1, text, "string")
expect(2, add_space, "boolean", "table", "nil")
local progs = shell.programs()
local files = c.file(text)
local seen = {}
for i=1, #files, 1 do
local full = text..files[i]
if fs.isDir(full) and full:sub(-1) ~= "/" then
full = full .. "/"
end
if not seen[full] then
progs[#progs+1] = full
end
seen[full] = true
end
table.sort(progs, function(a,b) return #a < #b end)
return completion.choice(text, progs, add_space)
end
function c.programWithArgs(text, previous, starting)
expect(1, text, "string")
expect(2, previous, "table")
expect(3, starting, "number")
if not previous[starting] then
return shell.completeProgram(text)
end
local command = previous[starting]
command = shell.aliases()[command] or command
local complete = shell.getCompletionInfo()[command]
if complete then
return complete(#previous - starting + 1, text,
{table.unpack(previous, starting)})
end
end
for k,v in pairs(completion) do
c[k] = function(text, _, ...) return v(text, ...) end
end
function c.build(...)
local args = table.pack(...)
for i=1, args.n, 1 do
expect(i, args[i], "function", "table", "nil")
end
return function(index, current, previous)
local complete
if args.n < index then
if type(args[args.n]) == "table" and args[args.n].many then
complete = args[args.n]
end
else
complete = args[index]
end
if not complete then
return {}
end
if type(complete) == "function" then
return complete(current, previous)
elseif type(complete) == "table" then
return complete[1](current, previous, table.unpack(complete, 2))
end
end
end
return c

View File

@@ -0,0 +1,133 @@
-- cc.strings
local term = require("term")
local expectlib = require("cc.expect")
local expect = expectlib.expect
local field = expectlib.field
local strings = {}
local function count(...)
local tab = table.pack(...)
local n = 0
for i=1, tab.n, 1 do
if tab[i] then n = n + 1 end
end
return n
end
function strings.splitElements(text, limit)
expect(1, text, "string")
expect(2, limit, "number", "nil")
local tokens = {}
while #text > 0 do
local ws = text:match("^[ \t]+")
local nl = text:match("^\n+")
local sep = text:match("^[%-%+%*]")
local word = text:match("^[^ \t\n%-%+%*]+")
if count(ws, nl, sep, word) > 1 then
error(("Edge case: %q, %q, %q, %q"):format(ws, nl, sep, word), 0)
end
local token = ws or nl or sep or word
text = text:sub(#token + 1)
while #token > 0 do
local ttext = token:sub(1, limit or 65535)
token = token:sub(#ttext + 1)
tokens[#tokens+1] = { text = ttext, type = ws and "ws" or nl and "nl"
or sep and "word" or word and "word" }
end
end
return tokens
end
function strings.wrappedWriteElements(elements, width, doHalves, handler)
expect(1, elements, "table")
expect(2, width, "number")
expect(3, doHalves, "boolean", "nil")
expect(4, handler, "table")
field(handler, "newline", "function")
field(handler, "append", "function")
field(handler, "getX", "function")
for i=1, #elements, 1 do
local e = elements[i]
if e.type == "nl" then
for _=1, #e.text do handler.newline() end
elseif e.type == "ws" then
local x = handler.getX()
if x + #e.text > width + 1 then
handler.newline()
else
handler.append(e.text)
end
elseif e.type == "word" then
local x = handler.getX()
local half = math.ceil(#e.text / 2)
if x + #e.text > width + 1 then
if doHalves and x + half < width and #e.text > width/2 then
local halfText = e.text:sub(1, math.floor(#e.text / 2)) .. "-"
e.text = e.text:sub(#halfText)
handler.append(halfText)
handler.newline()
elseif x > 1 then
handler.newline()
end
end
handler.append(e.text)
end
end
end
function strings.wrap(text, width, doHalves)
expect(1, text, "string")
expect(2, width, "number", "nil")
expect(3, doHalves, "boolean", "nil")
width = width or term.getSize()
local lines = { "" }
local elements = strings.splitElements(text, width)
strings.wrappedWriteElements(elements, width, doHalves, {
newline = function()
lines[#lines+1] = ""
end,
append = function(newText)
lines[#lines] = lines[#lines] .. newText
end,
getX = function()
return #lines[#lines]
end
})
return lines
end
function strings.ensure_width(line, width)
expect(1, line, "string")
expect(2, width, "number", "nil")
width = width or term.getSize()
return (line .. (" "):rep(width - #line)):sub(1, width)
end
return strings

View File

@@ -0,0 +1,198 @@
-- A simple, fairly clever method of tokenizing code.
-- Each token is defined by a set of rules. These rules are
-- accordingly defined in the relevant syntax definition file.
-- A rule takes the form of a triplet of functions:
-- - The first function takes a single character, and returns
-- whether that character is valid as part of the corresponding
-- token.
-- - The second function takes a single character and the current
-- token, and returns whether that character is valid as part
-- of the token. This allows flexible implementations of highly
-- language-specific features such as strings.
-- - The third function takes only a token, and returns whether
-- that token is valid.
--
-- Multiple tokens may be evaluated in parallel and the longest is returned.
local lib = {}
local syntenv = {
char = function(str)
return {
function(c)
return c == str:sub(1,1)
end,
function(tk, c)
return tk .. c == str:sub(1, #tk + 1)
end,
function(tk)
return tk == str
end
}
end,
print = print,
string = string, table = table,
pairs = pairs, ipairs = ipairs,
tonumber = tonumber, math = math,
globalenv = _G, type = type,
}
-- basic ""reader""
local function reader(text)
local chars = {}
for c in text:gmatch(".") do
chars[#chars+1] = c
end
local i = 0
return {
advance = function()
i = i + 1
return chars[i]
end,
backpedal = function()
i = math.max(0, i - 1)
end
}
end
-- Takes a file and returns a builder.
function lib.new(file)
local definitions = assert(loadfile(file, "t", syntenv))()
for _, _defs in pairs(definitions) do
for i, ent in pairs(_defs) do
if type(ent) == "string" then
_defs[i] = syntenv.char(ent)
end
end
end
return function(text)
local read = reader(text)
local possibilities = {}
local aux = ""
-- find and return the most likely (aka, longest) token and its class
local function most_likely()
-- if there are no possibilities, then ...
if #possibilities == 0 then
-- ... if the aux value has some characters, return that ...
if #aux > 0 then
local result = aux
aux = ""
return result
else
-- ... otherwise return nil.
return nil
end
end
local former_longest, new_longest = 0, 0
-- remove all invalid possibilites
for i=#possibilities, 1, -1 do
if not possibilities[i].valid(possibilities[i].token) then
former_longest = math.max(#possibilities[i].token, former_longest)
table.remove(possibilities, i)
else
new_longest = math.max(#possibilities[i].token, new_longest)
end
end
if former_longest > new_longest then
for _=new_longest, former_longest - 1 do
read.backpedal()
end
end
-- sort possibilities by length - and deprioritize whitespace/word
table.sort(possibilities, function(a, b)
return #a.token > #b.token
or (#a.token == #b.token and b.class == "word")
or b.class == "whitespace"
end)
if #possibilities == 0 then
--read.backpedal()
return most_likely()
end
-- grab the first (longest) one
local token, class = possibilities[1].token, possibilities[1].class
-- reset possibilities
possibilities = {}
aux = ""
-- return it
return token, class
end
-- return an iterator!
return function()
while true do
local c = read.advance()
-- if no character, return the most likely token
if not c then return most_likely() end
if #possibilities == 0 then
-- if no current possibilities, then go through and check for them
for class, defs in pairs(definitions) do
for _, funcs in pairs(defs) do
if funcs[1](c) then
-- if the token is valid, add it here
possibilities[#possibilities+1] = {
check = funcs[2], class = class, token = c,
valid = funcs[3] or function()return true end, active = true
}
end
end
end
-- if there are now some possibilities, return whatever the "aux"
-- value was
if #possibilities > 0 then
if #aux > 0 then
local temp = aux--:sub(1,-2)
aux = ""
return temp
end
aux = c
else
-- otherwise, add c to the aux value
aux = aux .. c
end
else
aux = aux .. c
-- whether any possibilities matched
local valid_for_any = false
for _, p in ipairs(possibilities) do
-- 'active' is roughly equal to whether the last character matched
if p.active then
-- if valid, set valid_for_any to true and add c to its valid
if p.check(p.token, c) then
valid_for_any = true
p.token = p.token .. c
else
-- otherwise, disable it from future checks
p.active = false
end
end
end
-- if nothing was valid, retract the current character
-- and return the most likely token
if not valid_for_any then
read.backpedal()
return most_likely()
end
end
end
end
end
end
return lib

View File

@@ -0,0 +1,152 @@
local syn = {
whitespace = {
{
function(c)
return c:match("[ \n\r\t]")
end,
function()
return false
end,
function(c)
return c:match("^[ \n\r\t]+")
end
},
},
word = {
{
function(c)
return not not c:match("[a-zA-Z_]")
end,
function(_, c)
return not not c:match("[a-zA-Z_0-9]")
end
}
},
keyword = {
"const", "close", "local", "while", "for", "repeat", "until", "do", "if",
"in", "else", "elseif", "and", "or", "not", "then", "end", "return",
"goto", "break",
},
builtin = {
"function",
},
separator = {
",", "(", ")", "{", "}", "[", "]",
},
operator = {
"+", "-", "/", "*", "//", "==", ">>", "<<", ">", "<", "=", "&",
"|", "^", "%", "~", "...", "..", "~=", "#", ".", ":"
},
boolean = {
"true", "false", "nil"
},
comment = {
{
function(c)
return c == "-"
end,
function(t,c)
if t == "-" and c ~= "-" then return false end
return c ~= "\n"
end,
function(t)
return #t > 1
end
},
{
function(c)
return c == "-"
end,
function(t,c)
if t == "-" and c == "-" then return true
elseif t == "--" and c == "[" then return true
elseif t == "--[" and c == "=" and c == "[" then return true
elseif t:match("^%-%-%[(=*)$") and c == "=" and c == "[" then
return true
end
local eqs = t:match("^%-%-%[(=*)")
if not eqs then
return false
else
if #t == #eqs + 3 and c == "[" then return true end
if t:sub(-(#eqs+2)) == "]"..eqs.."]" then
return false
else
return true
end
end
end,
function(t)
return #t > 3
end
}
},
string = {
{
function(c)
return c == "'" or c == '"'
end,
function(t, c)
local first = t:sub(1,1)
local last = t:sub(#t)
local penultimate = t:sub(-2, -2)
if #t == 1 then return true end
if first == last and penultimate ~= "\\" then return false end
return true
end
},
{
function(c)
return c == "["
end,
function(t,c)
if t == "[" then
return c == "=" or c == "["
elseif t:match("^%[(=*)$") and (c == "=" or c == "[") then
return true
end
local eqs = t:match("^%[(=*)")
if not eqs then
return false
else
if #t == #eqs + 3 and c == "[" then return true end
if t:sub(-(#eqs+2)) == "]"..eqs.."]" then
return false
else
return true
end
end
end,
function(t)
return #t > 2
end
}
},
number = {
{
function(c)
return not not tonumber(c)
end,
function(t, c)
return not not tonumber(t .. c .. "0")
end
}
}
}
local seen = {}
local function add(k, v)
if not v then return end
if seen[v] then return end
seen[v] = true
for _k, _v in pairs(v) do
syn.builtin[#syn.builtin+1] = char((k and k.."." or "").._k)
if type(_v) == "table" then
add((k and k.."." or "").._k, _v)
end
end
end
add(nil, globalenv)
return syn

View File

@@ -0,0 +1,36 @@
-- rc.copy - table copier
-- from https://lua-users.org/wiki/CopyTable
local function deepcopy(orig, copies, dont_copy)
copies = copies or {}
local orig_type = type(orig)
local copy
if orig_type == 'table' and not dont_copy[orig] then
if copies[orig] then
copy = copies[orig]
else
copy = {}
copies[orig] = copy
for orig_key, orig_value in next, orig, nil do
copy[deepcopy(orig_key, copies, dont_copy)] = deepcopy(orig_value,
copies, dont_copy)
end
setmetatable(copy, deepcopy(getmetatable(orig), copies, dont_copy))
end
else -- number, string, boolean, etc
copy = orig
end
return copy
end
return { copy = function(original, ...)
local dont_copy = {}
for _, thing in pairs({...}) do
dont_copy[thing] = true
end
return deepcopy(original, nil, dont_copy)
end }

View File

@@ -0,0 +1,231 @@
-- rc.io
local expect = require("cc.expect").expect
local thread = require("rc.thread")
local colors = require("colors")
local term = require("term")
local fs = require("fs")
local rc = require("rc")
local io = {}
local _file = {}
function _file:read(...)
local args = table.pack(...)
local ret = {}
if args.n == 0 then
args[1] = "l"
args.n = 1
end
if not (self.handle.read and pcall(self.handle.read, 0)) then
return nil, "bad file descriptor"
end
if self.handle.flush then self.handle.flush() end
for i=1, args.n, 1 do
local format = args[i]
if format:sub(1,1) == "*" then
format = format:sub(2)
end
if format == "a" then
ret[#ret+1] = self.handle.readAll()
elseif format == "l" or format == "L" then
ret[#ret+1] = self.handle.readLine(format == "L")
elseif type(format) == "number" then
ret[#ret+1] = self.handle.read(format)
else
error("invalid format '"..format.."'", 2)
end
end
return table.unpack(ret, 1, args.n)
end
function _file:lines(...)
local formats = {...}
if #formats == 0 then
formats[1] = "l"
end
return function()
return self:read(table.unpack(formats))
end
end
function _file:write(...)
local args = table.pack(...)
if not (self.handle.write and pcall(self.handle.write, "")) then
return nil, "bad file descriptor"
end
for i=1, args.n, 1 do
self.handle.write(args[i])
end
return self
end
function _file:seek(whence, offset)
expect(1, whence, "string", "nil")
expect(2, offset, "number", "nil")
if self.handle.seek then
return self.handle.seek(whence, offset)
else
return nil, "bad file descriptor"
end
end
function _file:flush()
if self.handle.flush then self.handle.flush() end
return self
end
function _file:close()
self.closed = true
pcall(self.handle.close)
end
local function iofile(handle)
return setmetatable({handle = handle, closed = false}, {__index = _file})
end
local stdin_rbuf = ""
io.stdin = iofile {
read = function(n)
while #stdin_rbuf < n do
stdin_rbuf = stdin_rbuf .. term.read() .. "\n"
end
local ret = stdin_rbuf:sub(1, n)
stdin_rbuf = stdin_rbuf:sub(#ret+1)
return ret
end,
readLine = function(trail)
local nl = stdin_rbuf:find("\n")
if nl then
local ret = stdin_rbuf:sub(1, nl+1)
if not trail then ret = ret:sub(1, -2) end
stdin_rbuf = stdin_rbuf:sub(#ret+1)
return ret
else
return stdin_rbuf .. term.read() .. (trail and "\n" or "")
end
end
}
io.stdout = iofile {
write = rc.write
}
io.stderr = iofile {
write = function(text)
local old = term.getTextColor()
term.setTextColor(colors.red)
rc.write(text)
term.setTextColor(old)
end
}
function io.open(file, mode)
expect(1, file, "string")
expect(2, mode, "string", "nil")
mode = (mode or "r"):match("[rwa]") .. "b"
local handle, err = fs.open(file, mode)
if not handle then
return nil, err
end
return iofile(handle)
end
function io.input(file)
expect(1, file, "string", "table", "nil")
local vars = thread.vars()
if type(file) == "string" then file = assert(io.open(file, "r")) end
if file then vars.input = file end
return vars.input or io.stdin
end
function io.output(file)
expect(1, file, "string", "table", "nil")
local vars = thread.vars()
if type(file) == "string" then file = assert(io.open(file, "w")) end
if file then vars.output = file end
return vars.output or io.stdout
end
function io.read(...)
return io.input():read(...)
end
function io.write(...)
return io.output():write(...)
end
function io.flush(file)
expect(1, file, "table", "nil")
return (file or io.output):flush()
end
function io.close(file)
expect(1, file, "table", "nil")
return (file or io.output):close()
end
function io.lines(file, ...)
expect(1, file, "string", "nil")
if file then file = assert(io.open(file, "r")) end
local formats = table.pack(...)
return (file or io.stdin):lines(table.unpack(formats, 1, formats.n))
end
function io.type(obj)
if type(obj) == "table" then
local is_file = true
for k, v in pairs(_file) do
if (not obj[k]) or v ~= obj[k] then
is_file = false
end
end
if is_file then
return obj.closed and "closed file" or "file"
end
end
end
-- loadfile and dofile here as well
function _G.loadfile(file, mode, env)
expect(1, file, "string")
expect(2, mode, "string", "nil")
expect(3, env, "table", "nil")
local handle, err = io.open(file, "r")
if not handle then
return nil, file .. ": " .. err
end
local data = handle:read("a")
handle:close()
return load(data, "="..file, mode or "bt", env)
end
function _G.dofile(file, ...)
expect(1, file, "string")
local func, err = loadfile(file)
if not func then
error(err)
end
return func(...)
end
return io

View File

@@ -0,0 +1,388 @@
--
-- json.lua
--
-- Copyright (c) 2020 rxi
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
--
local json = { _version = "0.1.2" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\",
[ "\"" ] = "\"",
[ "\b" ] = "b",
[ "\f" ] = "f",
[ "\n" ] = "n",
[ "\r" ] = "r",
[ "\t" ] = "t",
}
local escape_char_map_inv = { [ "/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(1, 4), 16 )
local n2 = tonumber( s:sub(7, 10), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local res = ""
local j = i + 1
local k = j
while j <= #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
elseif x == 92 then -- `\`: Escape
res = res .. str:sub(k, j - 1)
j = j + 1
local c = str:sub(j, j)
if c == "u" then
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
or str:match("^%x%x%x%x", j + 1)
or decode_error(str, j - 1, "invalid unicode escape in string")
res = res .. parse_unicode_escape(hex)
j = j + #hex
else
if not escape_chars[c] then
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
end
res = res .. escape_char_map_inv[c]
end
k = j + 1
elseif x == 34 then -- `"`: End of string
res = res .. str:sub(k, j - 1)
return res, j + 1
end
j = j + 1
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end
return json

View File

@@ -0,0 +1,398 @@
-- New scheduler.
-- Tabs are integral to the design of this scheduler; Multishell cannot
-- be disabled.
local rc = require("rc")
local fs = require("fs")
local keys = require("keys")
local term = require("term")
local colors = require("colors")
local window = require("window")
local expect = require("cc.expect")
local copy = require("rc.copy").copy
local getfenv
if rc.lua51 then
getfenv = rc.lua51.getfenv
else
getfenv = function() return _ENV or _G end
end
local tabs = { {} }
local threads = {}
local current, wrappedNative
local focused = 1
local api = {}
function api.launchTab(x, name)
expect(1, x, "string", "function")
name = expect(2, name, "string", "nil") or tostring(x)
local newTab = {
term = window.create(wrappedNative, 1, 1, wrappedNative.getSize()),
id = #tabs + 1
}
tabs[newTab.id] = newTab
local _f = focused
focused = newTab.id
local id = (type(x) == "string" and api.load or api.spawn)(x, name)
focused = _f
return newTab.id, id
end
function api.setFocusedTab(f)
expect(1, f, "number")
if tabs[focused] then focused = f end
return not not tabs[f]
end
function api.getFocusedTab()
return focused
end
function api.getCurrentTab()
return current.tab.id
end
function api.load(file, name)
expect(1, file, "string")
name = expect(2, name, "string", "nil") or file
local env = copy(current and current.env or _ENV or _G, package.loaded)
local func, err = loadfile(file, "t", env)
if not func then
return nil, err
end
return api.spawn(func, name, tabs[focused])
end
function api.spawn(func, name, _)
expect(1, func, "function")
expect(2, name, "string")
local new = {
name = name,
coro = coroutine.create(function()
assert(xpcall(func, debug.traceback))
end),
vars = setmetatable({}, {__index = current and current.vars}),
env = getfenv(func) or _ENV or _G,
tab = _ or tabs[focused],
id = #threads + 1,
dir = current and current.dir or "/"
}
new.tab[new.id] = true
threads[new.id] = new
new.tab.name = name
return new.id
end
function api.exists(id)
expect(1, id, "number")
return not not threads[id]
end
function api.id()
return current.id
end
function api.dir()
return current.dir or "/"
end
function api.setDir(dir)
expect(1, dir, "string")
if not fs.exists(dir) then
return nil, "that directory does not exist"
elseif not fs.isDir(dir) then
return nil, "not a directory"
end
current.dir = dir
return true
end
function api.vars()
return current.vars
end
function api.getTerm()
return current and current.tab and current.tab.term or term.native()
end
function api.setTerm(new)
if tabs[focused] then
local old = tabs[focused].term
tabs[focused].term = new
return old
end
end
local w, h
local function getName(tab)
local highest = 0
for k in pairs(tab) do
if type(k) == "number" then highest = math.max(highest, k) end
end
return threads[highest] and threads[highest].name or "???"
end
function api.info()
local running = {}
for i, thread in pairs(threads) do
running[#running+1] = { id = i, name = thread.name, tab = thread.tab.id }
end
table.sort(running, function(a,b) return a.id < b.id end)
return running
end
function api.remove(id)
expect(1, id, "number", "nil")
threads[id or current.id] = nil
end
local scroll = 0
local totalNameLength = 0
local function redraw()
w, h = wrappedNative.getSize()
wrappedNative.setVisible(false)
local names = {}
totalNameLength = 0
for i=1, #tabs, 1 do
names[i] = " " .. getName(tabs[i]) .. " "
totalNameLength = totalNameLength + #names[i]
end
if #tabs > 1 then
local len = -scroll + 1
wrappedNative.setCursorPos(1, 1)
wrappedNative.setTextColor(colors.black)
wrappedNative.setBackgroundColor(colors.gray)
wrappedNative.clearLine()
for i=1, #tabs, 1 do
local tab = tabs[i]
local name = names[i]
wrappedNative.setCursorPos(len, 1)
len = len + #name
if i == focused then
wrappedNative.setTextColor(colors.yellow)
wrappedNative.setBackgroundColor(colors.black)
wrappedNative.write(name)
else
wrappedNative.setTextColor(colors.black)
wrappedNative.setBackgroundColor(colors.gray)
wrappedNative.write(name)
end
tab.term.setVisible(false)
tab.term.reposition(1, 2, w, h - 1)
end
if totalNameLength > w-2 then
wrappedNative.setTextColor(colors.black)
wrappedNative.setBackgroundColor(colors.gray)
if scroll > 0 then
wrappedNative.setCursorPos(1, 1)
wrappedNative.write("<")
end
if totalNameLength - scroll > w-1 then
wrappedNative.setCursorPos(w, 1)
wrappedNative.write(">")
end
end
tabs[focused].term.setVisible(true)
elseif #tabs > 0 then
local tab = tabs[1]
tab.term.reposition(1, 1, w, h)
tab.term.setVisible(true)
end
wrappedNative.setVisible(true)
end
local inputEvents = {
key = true,
char = true,
key_up = true,
mouse_up = true,
mouse_drag = true,
mouse_click = true,
mouse_scroll = true,
terminate = true,
}
local altIsDown
local function processEvent(event)
if inputEvents[event[1]] then
if #event > 3 then -- mouse event
if #tabs > 1 then
if event[4] == 1 then
local curX = -scroll
if event[1] == "mouse_scroll" then
scroll = math.max(0, math.min(totalNameLength-w+1,
scroll - event[2]))
return false
end
for i=1, #tabs, 1 do
local tab = tabs[i]
curX = curX + #getName(tab) + 2
if event[3] <= curX then
focused = i
redraw()
break
end
end
return false
else
event[4] = event[4] - 1
end
end
elseif event[1] == "key" then
if event[2] == keys.rightAlt then
altIsDown = event[2]
return false
elseif altIsDown then
local num = tonumber(keys.getName(event[2]))
if num then
if tabs[num] then
focused = num
redraw()
return false
end
elseif event[2] == keys.left then
focused = math.max(1, focused - 1)
redraw()
return false
elseif event[2] == keys.right then
focused = math.min(#tabs, focused + 1)
redraw()
return false
elseif event[2] == keys.up then
scroll = math.max(0, math.min(totalNameLength-w+1,
scroll + 1))
return false
elseif event[2] == keys.down then
scroll = math.max(0, math.min(totalNameLength-w+1,
scroll - 1))
return false
end
end
elseif event[1] == "key_up" then
if event[2] == keys.rightAlt then
altIsDown = false
return false
end
end
end
return true
end
local function cleanTabs()
for t=#tabs, 1, -1 do
local tab = tabs[t]
local count, removed = 0, 0
for i in pairs(tab) do
if type(i) == "number" then
count = count + 1
if not threads[i] then
removed = removed + 1
tab[i] = nil
end
end
end
if count == removed then
table.remove(tabs, t)
end
end
for i=1, #tabs, 1 do
tabs[i].id = i
end
focused = math.max(1, math.min(#tabs, focused))
end
function api.start()
api.start = nil
local _native = term.native()
wrappedNative = window.create(_native, 1, 1, _native.getSize())
api.launchTab("/rc/programs/shell.lua", "shell")
rc.pushEvent("init")
while #tabs > 0 and next(threads) do
cleanTabs()
redraw()
local event = table.pack(coroutine.yield())
if event[1] == "term_resize" then
wrappedNative.reposition(1, 1, _native.getSize())
end
if processEvent(event) then
for tid, thread in pairs(threads) do
if thread.tab == tabs[focused] or not inputEvents[event[1]] then
current = thread
local result = table.pack(coroutine.resume(thread.coro,
table.unpack(event, 1, event.n)))
if not result[1] then
io.stderr:write(result[2].."\n")
threads[tid] = nil
elseif coroutine.status(thread.coro) == "dead" then
threads[tid] = nil
end
end
end
end
end
rc.shutdown()
end
return api

View File

@@ -0,0 +1,7 @@
-- about
local rc = require("rc")
local colors = require("colors")
local textutils = require("textutils")
textutils.coloredPrint(colors.yellow, rc.version() .. " on " .. _HOST)

View File

@@ -0,0 +1,29 @@
-- alias
local args = {...}
local shell = require("shell")
local colors = require("colors")
local textutils = require("textutils")
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

View File

@@ -0,0 +1,19 @@
-- fg
local args = {...}
if #args == 0 then
error("command not provided", 0)
end
local shell = require("shell")
local thread = require("rc.thread")
local path, err = shell.resolveProgram(args[1])
if not path then
error(err, 0)
end
thread.launchTab(function()
shell.exec(path, table.unpack(args, 2))
end, args[1])

View File

@@ -0,0 +1 @@
require("term").at(1,1).clear()

View File

@@ -0,0 +1,36 @@
local fs = require("fs")
local shell = require("shell")
local args = {...}
if #args < 2 then
io.stderr:write("usage: copy <source> <destination>\n")
return
end
local source, destination = shell.resolve(args[1]), shell.resolve(args[2])
local files = fs.find(source)
if #files > 0 then
local dir = fs.isDir(destination)
if #files > 1 and not dir then
io.stderr:write("destination must be a directory\n")
return
end
for i=1, #files, 1 do
if dir then
fs.copy(files[i], fs.combine(destination, fs.getName(files[i])))
elseif #files == 1 then
if fs.exists(destination) then
io.stderr:write("file already exists\n")
return
else
fs.copy(files[i], destination)
end
end
end
else
io.stderr:write("no such file(s)\n")
end

View File

@@ -0,0 +1,58 @@
-- LeonOS compatibility, in theory
local rc = require("rc")
local settings = require("settings")
if not settings.get("bios.compat_mode") then
error("compatibility mode is disabled", 0)
end
if os.version then
error("you are already in compatibility mode", 0)
end
local libs = {
"peripheral", "fs", "settings", "http", "term", "colors", "multishell",
"keys", "parallel", "shell", "textutils", "window", "paintutils"
}
local move = {
"queueEvent", "startTimer", "cancelTimer", "setAlarm", "cancelAlarm", "getComputerID",
"computerID", "getComputerLabel", "setComputerLabel", "computerLabel", "day", "epoch",
"pullEvent", "sleep"
}
local nEnv = setmetatable({}, {__index=_G})
nEnv.os = setmetatable({}, {__index=os})
for i=1, #libs, 1 do
nEnv[libs[i]] = select(2, pcall(require, libs[i]))
end
for i=1, #move do
nEnv.os[move[i]] = rc[move[i]]
end
function nEnv.printError(text)
io.stderr:write(text, "\n")
end
nEnv.write = rc.write
nEnv.unpack = table.unpack
if rc.lua51 then
for k, v in pairs(rc.lua51) do
nEnv[k] = v
end
end
nEnv.read = nEnv.term.read
nEnv.sleep = nEnv.os.sleep
function nEnv.os.version()
return "LeonOS 1.0 ALpha 1"
end
local func, err = loadfile("/rc/programs/shell.lua", "t", nEnv)
if not func then error(err, 0) end
func()

View File

@@ -0,0 +1,24 @@
local fs = require("fs")
local shell = require("shell")
local args = {...}
if #args == 0 then
io.stderr:write("usage: delete <paths>\n")
return
end
for i=1, #args, 1 do
local files = fs.find(shell.resolve(args[i]))
if not files then
io.stderr:write("file(s) not found\n")
return
end
for n=1, #files, 1 do
if fs.isReadOnly(files[n]) then
io.stderr:write(files[n] .. ": cannot remove read-only file\n")
return
end
fs.delete(files[n])
end
end

View File

@@ -0,0 +1,89 @@
-- devbin (like pastebin)
if not package.loaded.http then
error("HTTP is not enabled in the ComputerCraft configuration", 0)
end
local http = require("http")
local json = require("rc.json")
local shell = require("shell")
local args = {...}
if #args < (args[1] == "get" and 3 or 2) then
io.stderr:write([[
Usage:
devbin put <filename>
devbin get <code> <filename>
devbin run <code> [argument ...]
]])
return
end
local paste = "https://devbin.dev/api/v3/paste"
local key = "aNVXl8vxYGWcZGvMnuJTzLXH53mGWOuQtBXU025g8YDAsZDu"
local function get(code)
local handle, err, rerr = http.get("https://devbin.dev/raw/"..code)
if not handle then
if rerr then rerr.close() end
error(err, 0)
end
local data = handle.readAll()
handle.close()
return data
end
if args[1] == "put" then
local handle, err = io.open(shell.resolve(args[2]), "r")
if not handle then error(err, 0) end
local data = handle:read("a")
handle:close()
if (not data) or #data == 0 then
error("cannot 'put' empty file", 0)
end
local request = json.encode({
title = args[2],
syntaxName = "lua",
exposure = 0,
content = data,
asGuest = true
})
local response, rerr, rerr2 = http.post(paste, request,
{["Content-Type"]="application/json", Authorization = key}, true)
if not response then
local _rerr2 = rerr2.readAll()
if rerr2 then rerr2.close() end
print(_rerr2)
error(rerr, 0)
--("%q: %q"):format(rerr, (rerr2 and rerr2.readAll()) or ""), 0)
end
local rdata = response.readAll()
local code, message = response.getResponseCode()
response.close()
if code ~= 201 then
error(code .. " " .. (message or ""), 0)
end
local decoded = json.decode(rdata)
print(decoded.code)
elseif args[1] == "get" then
local data = get(args[2])
local handle, err = io.open(shell.resolve(args[3]), "w")
if not handle then error(err, 0) end
handle:write(data)
handle:close()
elseif args[1] == "run" then
local data = get(args[2])
assert(load(data, "=<devbin-run>", "t", _G))()
end

View File

@@ -0,0 +1,12 @@
-- launch different editors based on computer capabilities
local term = require("term")
local settings = require("settings")
local df = function(f, ...) return assert(loadfile(f))(...) end
if term.isColor() or settings.get("edit.force_highlight") then
df("/rc/editors/advanced.lua", ...)
else
df("/rc/editors/basic.lua", ...)
end

View File

@@ -0,0 +1,19 @@
-- fg
local args = {...}
if #args == 0 then
error("command not provided", 0)
end
local shell = require("shell")
local thread = require("rc.thread")
local path, err = shell.resolveProgram(args[1])
if not path then
error(err, 0)
end
thread.setFocus((thread.launchTab(function()
shell.exec(path, table.unpack(args, 2))
end, args[1])))

View File

@@ -0,0 +1,25 @@
-- help
local help = require("help")
local textutils = require("textutils")
local args = {...}
if #args == 0 then
args[1] = "help"
end
local function view(name)--path)
textutils.coloredPagedPrint(table.unpack(help.loadTopic(name)))
--local lines = {}
--for l in io.lines(path) do lines[#lines+1] = l end
--textutils.pagedPrint(table.concat(require("cc.strings").wrap(table.concat(lines,"\n"), require("term").getSize()), "\n"))
end
for i=1, #args, 1 do
local path = help.lookup(args[i])
if not path then
error("No help topic for " .. args[i], 0)
end
view(args[i])--path)
end

View File

@@ -0,0 +1,46 @@
-- list
local args = {...}
local fs = require("fs")
local shell = require("shell")
local colors = require("colors")
local settings = require("settings")
local textutils = require("textutils")
if #args == 0 then args[1] = shell.dir() end
local show_hidden = settings.get("list.show_hidden")
local function list_dir(dir)
if not fs.exists(dir) then
error(dir .. ": that directory does not exist", 0)
elseif not fs.isDir(dir) then
error(dir .. ": not a directory", 0)
end
local raw_files = fs.list(dir)
local files, dirs = {}, {}
for i=1, #raw_files, 1 do
local full = fs.combine(dir, raw_files[i])
if raw_files[i]:sub(1,1) ~= "." or show_hidden then
if fs.isDir(full) then
dirs[#dirs+1] = raw_files[i]
else
files[#files+1] = raw_files[i]
end
end
end
textutils.pagedTabulate(colors.green, dirs, colors.white, files)
end
for i=1, #args, 1 do
if #args > 1 then
textutils.coloredPrint(colors.yellow, args[i]..":\n", colors.white)
end
list_dir(args[i])
end

View File

@@ -0,0 +1,46 @@
-- lua REPL
local term = require("term")
local copy = require("rc.copy").copy
local colors = require("colors")
local pretty = require("cc.pretty")
local textutils = require("textutils")
local env = copy(_ENV, package.loaded)
local run = true
function env.exit() run = false end
term.setTextColor(colors.yellow)
print("LeonOS Lua REPL.\nCall exit() to exit.")
local history = {}
while run do
term.setTextColor(colors.white)
io.write("$ lua >>> ")
local data = term.read(nil, history, function(text)
return textutils.complete(text, env)
end)
if #data > 0 then
history[#history+1] = data
end
local ok, err = load("return " .. data, "=stdin", "t", env)
if not ok then
ok, err = load(data, "=stdin", "t", env)
end
if ok then
local result = table.pack(pcall(ok))
if not result[1] then
io.stderr:write(result[2], "\n")
elseif result.n > 1 then
for i=2, result.n, 1 do
pretty.pretty_print(result[i])
end
end
else
io.stderr:write(err, "\n")
end
end

View File

@@ -0,0 +1,12 @@
local fs = require("fs")
local shell = require("shell")
local args = {...}
if #args == 0 then
io.stderr:write("usage: mkdir <paths>\n")
return
end
for i=1, #args, 1 do
fs.makeDir(shell.resolve(args[i]))
end

View File

@@ -0,0 +1,36 @@
local fs = require("fs")
local shell = require("shell")
local args = {...}
if #args < 2 then
io.stderr:write("usage: move <source> <destination>\n")
return
end
local source, destination = shell.resolve(args[1]), shell.resolve(args[2])
local files = fs.find(source)
if #files > 0 then
local dir = fs.isDir(destination)
if #files > 1 and not dir then
io.stderr:write("destination must be a directory\n")
return
end
for i=1, #files, 1 do
if dir then
fs.move(files[i], fs.combine(destination, fs.getName(files[i])))
elseif #files == 1 then
if fs.exists(destination) then
io.stderr:write("file already exists\n")
return
else
fs.move(files[i], destination)
end
end
end
else
io.stderr:write("no such file(s)\n")
end

View File

@@ -0,0 +1,3 @@
-- BIMG editor
local term = require("term")

View File

@@ -0,0 +1,16 @@
local term = require("term")
local colors = require("colors")
local peripheral = require("peripheral")
term.setTextColor(colors.yellow)
print("attached peripherals")
term.setTextColor(colors.white)
local names = peripheral.getNames()
if #names == 0 then
io.stderr:write("none\n")
else
for i=1, #names, 1 do
print(string.format("%s (%s)", names[i], peripheral.getType(names[i])))
end
end

View File

@@ -0,0 +1,6 @@
local shell = require("shell")
local colors = require("colors")
local textutils = require("textutils")
textutils.coloredPrint(colors.yellow, "available programs\n", colors.white)
textutils.pagedTabulate(shell.programs())

View File

@@ -0,0 +1,10 @@
local rc = require("rc")
local term = require("term")
local colors = require("colors")
term.setTextColor(colors.yellow)
print("Restarting")
if (...) ~= "now" then rc.sleep(1) end
rc.reboot()

View File

@@ -0,0 +1,88 @@
local rc = require("rc")
local rs = require("redstone")
local colors = require("colors")
local textutils = require("textutils")
local args = {...}
local commands = {}
local sides = {"top", "bottom", "left", "right", "front", "back"}
function commands.probe()
textutils.coloredPrint(colors.yellow, "redstone inputs", colors.white)
local inputs = {}
for i=1, #sides, 1 do
if rs.getInput(sides[i]) then
inputs[#inputs+1] = sides[i]
end
end
if #inputs == 0 then inputs[1] = "None" end
print(table.concat(inputs, ", "))
return true
end
local function coerce(value)
if value == "true" then return true end
if value == "false" then return false end
return tonumber(value) or value
end
function commands.set(side, color, value)
if not side then
io.stderr:write("side expected\n")
return true
end
if not value then
value = color
color = nil
end
value = coerce(value)
if type(value) == "string" then
io.stderr:write("value must be boolean or 0-15\n")
end
if color then
color = coerce(color)
if type(value) == "number" then
io.stderr:write("value must be boolean\n")
end
if not colors[color] then
io.stderr:write("color not defined\n")
end
rs.setBundledOutput(side, colors[color], value)
elseif type(value) == "boolean" then
rs.setOutput(side, value)
else
rs.setAnalogOutput(side, value)
end
return true
end
function commands.pulse(side, count, period)
count = tonumber(count) or 1
period = tonumber(period) or 0.5
for _=1, count, 1 do
rs.setOutput(side, true)
rc.sleep(period / 2)
rs.setOutput(side, false)
rc.sleep(period / 2)
end
return true
end
if not (args[1] and commands[args[1]] and
commands[args[1]](table.unpack(args, 2))) then
io.stderr:write("Usages:\nredstone probe\n"..
"redstone set <side> <value>\nredstone set <side> <color> <value>\n"..
"redstone pulse <side> <count> <period>\n")
end

View File

@@ -0,0 +1,47 @@
-- 'set' program
local colors = require("colors")
local settings = require("settings")
local textutils = require("textutils")
local args = {...}
local function coerce(val)
if val == "nil" then
return nil
elseif val == "false" then
return false
elseif val == "true" then
return true
else
return tonumber(val) or val
end
end
local col = {
string = colors.red,
table = colors.green,
number = colors.magenta,
boolean = colors.lightGray
}
if #args == 0 then
for _, setting in ipairs(settings.getNames()) do
local value = settings.get(setting)
textutils.coloredPrint(colors.cyan, setting, colors.white, " is ",
col[type(value)], string.format("%q", value))
end
elseif #args == 1 then
local setting = args[1]
local value = settings.get(setting)
textutils.coloredPrint(colors.cyan, setting, colors.white, " is ",
col[type(value)], string.format("%q", value))
local def = settings.getDetails(setting)
if def then
print(def.description)
end
else
local setting, value = args[1], args[2]
settings.set(setting, coerce(value))
settings.save()
end

View File

@@ -0,0 +1,79 @@
-- rc.shell
local rc = require("rc")
local fs = require("fs")
local term = require("term")
local shell = require("shell")
local colors = require("colors")
local thread = require("rc.thread")
local textutils = require("textutils")
if os.version then
textutils.coloredPrint(colors.yellow, os.version(), colors.white)
else
textutils.coloredPrint(colors.yellow, rc.version(), colors.white)
end
thread.vars().parentShell = thread.id()
shell.init(_ENV)
if not shell.__has_run_startup then
shell.__has_run_startup = true
if fs.exists("/startup.lua") then
local ok, err = pcall(dofile, "/startup.lua")
if not ok and err then
io.stderr:write(err, "\n")
end
end
if fs.exists("/startup") and fs.isDir("/startup") then
local files = fs.list("/startup/")
table.sort(files)
for f=1, #files, 1 do
local ok, err = pcall(dofile, "/startup/"..files[f])
if not ok and err then
io.stderr:write(err, "\n")
end
end
end
end
local aliases = {
background = "bg",
clr = "clear",
cp = "copy",
dir = "list",
foreground = "fg",
mv = "move",
rm = "delete",
rs = "redstone",
sh = "shell",
ps = "threads"
}
for k, v in pairs(aliases) do
shell.setAlias(k, v)
end
local completions = "/rc/completions"
for _, prog in ipairs(fs.list(completions)) do
dofile(fs.combine(completions, prog))
end
local history = {}
while true do
term.setTextColor(colors.yellow)
term.setBackgroundColor(colors.black)
rc.write(colors.yellow.."$"..shell.dir()..colors.green.." >>> ")
term.setTextColor(colors.white)
local text = term.read(nil, history, shell.complete)
if #text > 0 then
history[#history+1] = text
local ok, err = shell.run(text)
if not ok and err then
io.stderr:write(err, "\n")
end
end
end

View File

@@ -0,0 +1,10 @@
local rc = require("rc")
local term = require("term")
local colors = require("colors")
term.setTextColor(colors.yellow)
print("Shutting down")
if (...) ~= "now" then rc.sleep(1) end
rc.shutdown()

View File

@@ -0,0 +1,16 @@
-- threads
local colors = require("colors")
local thread = require("rc.thread")
local strings = require("cc.strings")
local textutils = require("textutils")
textutils.coloredPrint(colors.yellow, "id tab name", colors.white)
local info = thread.info()
for i=1, #info, 1 do
local inf = info[i]
textutils.pagedPrint(string.format("%s %s %s",
strings.ensure_width(tostring(inf.id), 4),
strings.ensure_width(tostring(inf.tab), 4), inf.name))
end

View File

@@ -0,0 +1,44 @@
-- wget
if not package.loaded.http then
error("HTTP is not enabled in the ComputerCraft configuration", 0)
end
local http = require("http")
local args = {...}
if #args == 0 then
io.stderr:write([[Usage:
wget <url> [filename]
wget run <url>
]])
return
end
local function get(url)
local handle, err = http.get(url, nil, true)
if not handle then
error(err, 0)
end
local data = handle.readAll()
handle.close()
return data
end
if args[1] == "run" then
local data = get(args[2])
assert(load(data, "=<wget-run>", "t", _G))()
else
local filename = args[2] or (args[1]:match("[^/]+$")) or
error("could not determine file name", 0)
local data = get(args[1])
local handle, err = io.open(filename, "w")
if not handle then
error(err, 0)
end
handle:write(data)
handle:close()
end

View File

@@ -0,0 +1,201 @@
-- override the fs library to use this resolution function where necessary
-- almost identical to the override used in .OS
local fs = rawget(_G, "fs")
-- split a file path into segments
function fs.split(path)
local s = {}
for S in path:gmatch("[^/\\]+") do
if S == ".." then
s[#s] = nil
elseif S ~= "." then
s[#s+1] = S
end
end
return s
end
-- package isn't loaded yet, so unfortunately this is necessary
local function expect(...)
return require and require("cc.expect").expect(...)
end
-- path resolution:
-- if the path begins with /rc, then redirect to wherever that actually
-- is; otherwise, resolve the path based on the current program's working
-- directory
-- this is to allow .OS to run from anywhere
local function resolve(path)
local thread = package and package.loaded.thread
local root = (thread and thread.getroot()) or "/"
local pwd = (thread and thread.dir()) or "/"
if path:sub(1,1) ~= "/" then
path = fs.combine(pwd, path)
end
path = fs.combine(root, path)
local segments = fs.split(path)
if segments[1] == "rc" then
return fs.combine(_RC_ROM_DIR, table.concat(segments, "/", 2, #segments))
else
return path
end
end
-- override: fs.combine
local combine = fs.combine
function fs.combine(...)
return "/" .. combine(...)
end
-- override: fs.getDir
local getDir = fs.getDir
function fs.getDir(p)
return "/" .. getDir(p)
end
-- override: fs.exists
local exists = fs.exists
function fs.exists(path)
expect(1, path, "string")
return exists(resolve(path))
end
-- override: fs.list
local list = fs.list
function fs.list(path)
expect(1, path, "string")
path = resolve(path)
local _, files = pcall(list, path)
if not _ then return nil, files end
if path == "/" then
-- inject /rc into the root listing
if not exists("/rc") then
files[#files+1] = "rc"
end
end
return files
end
-- override: fs.getSize
local getSize = fs.getSize
function fs.getSize(path)
expect(1, path, "string")
return getSize((resolve(path)))
end
-- override: fs.isDir
local isDir = fs.isDir
function fs.isDir(path)
expect(1, path, "string")
return isDir(resolve(path))
end
-- override: fs.makeDir
local makeDir = fs.makeDir
function fs.makeDir(path)
expect(1, path, "string")
return makeDir(resolve(path))
end
-- override: fs.move
local move = fs.move
function fs.move(a, b)
expect(1, a, "string")
expect(2, b, "string")
return move(resolve(a), resolve(b))
end
-- override: fs.copy
local copy = fs.copy
function fs.copy(a, b)
expect(1, a, "string")
expect(2, b, "string")
return copy(resolve(a), resolve(b))
end
-- override: fs.delete
local delete = fs.delete
function fs.delete(path)
expect(1, path, "string")
return delete(resolve(path))
end
-- override: fs.open
local open = fs.open
function fs.open(file, mode)
expect(1, file, "string")
expect(2, mode, "string")
return open(resolve(file), mode or "r")
end
-- override: fs.find
local find = fs.find
function fs.find(path)
expect(1, path, "string")
return find(resolve(path))
end
-- override: fs.attributes
local attributes = fs.attributes
function fs.attributes(path)
expect(1, path, "string")
return attributes(resolve(path))
end
-- new: fs.complete
function fs.complete(path, location, include_files, include_dirs)
expect(1, path, "string")
expect(2, location, "string")
expect(3, include_files, "boolean", "nil")
expect(4, include_dirs, "boolean", "nil")
if include_files == nil then include_files = true end
if include_dirs == nil then include_dirs = true end
if path:sub(1,1) == "/" and path:sub(-1) ~= "/" then
location = fs.getDir(path)
elseif path:sub(-1) == "/" then
location = path
else
location = fs.combine(location, fs.getDir(path))
end
local completions = {}
if not fs.exists(location) or not fs.isDir(location) then
return completions
end
local name = fs.getName(path)
if path:sub(-1) == "/" then name = "" end
local files = fs.list(location)
for i=1, #files, 1 do
local file = files[i]
local full = fs.combine(location, file)
if file:sub(1, #name) == name then
local dir = fs.isDir(full)
if (dir and include_dirs) or include_files then
completions[#completions+1] = file:sub(#name+1)
if #completions[#completions] == 0 then
completions[#completions] = nil
end
end
if dir then
completions[#completions+1] = file:sub(#name+1) .. "/"
end
end
end
return completions
end
function fs.isDriveRoot(path)
expect(1, path, "string")
if #path == 0 then path = "/" end
return path == "/" or fs.getDrive(path) == fs.getDrive(fs.getDir(path))
end

View File

@@ -0,0 +1,131 @@
-- package library --
local rc = ...
_G.package = {}
package.config = "/\n;\n?\n!\n-"
package.cpath = ""
package.path = "/rc/apis/?.lua;/rc/modules/main/?.lua;./lib/?.lua;./lib/?/init.lua;./?.lua;./?/init.lua"
local function rm(api)
local tab = _G[api]
_G[api] = nil
return tab
end
package.loaded = {
_G = _G,
os = os,
rc = rc,
math = math,
utf8 = utf8,
table = table,
debug = debug,
bit32 = rawget(_G, "bit32"),
string = string,
package = package,
coroutine = coroutine,
-- CC-specific ones
peripheral = rm("peripheral"),
redstone = rm("redstone"),
commands = rm("commands"),
pocket = rm("pocket"),
turtle = rm("turtle"),
http = rm("http"),
term = rm("term"),
fs = rm("fs"),
rs = rm("rs"),
-- LeonOS-PC APIs
periphemu = rm("periphemu"),
mounter = rm("mounter"),
config = rm("config"),
-- CCEmuX API
ccemux = rm("ccemux")
}
package.preload = {}
package.searchers = {
-- check package.preload
function(mod)
if package.preload[mod] then
return package.preload[mod]
else
return nil, "no field package.preload['" .. mod .. "']"
end
end,
-- check for lua library
function(mod)
local ok, err = package.searchpath(mod, package.path, ".", "/")
if not ok then
return nil, err
end
local func, loaderr = loadfile(ok)
if not func then
return nil, loaderr
end
return func()
end,
}
local fs = package.loaded.fs
-- require isn't here yet
local expect = loadfile("/rc/modules/main/cc/expect.lua")()
package.loaded["cc.expect"] = expect
function package.searchpath(name, path, sep, rep)
expect(1, name, "string")
expect(2, path, "string")
expect(3, sep, "string", "nil")
expect(4, rep, "string", "nil")
sep = "%" .. (sep or ".")
rep = rep or "/"
name = name:gsub(sep, rep)
local serr = ""
for search in path:gmatch("[^;]+") do
search = search:gsub("%?", name)
if fs.exists(search) then
return search
else
if #serr > 0 then
serr = serr .. "\n "
end
serr = serr .. "no file '" .. search .. "'"
end
end
return nil, serr
end
function _G.require(mod)
expect(1, mod, "string")
if package.loaded[mod] then
return package.loaded[mod]
end
local serr = "module '" .. mod .. "' not found:"
for _, searcher in ipairs(package.searchers) do
local result, err = searcher(mod)
if result then
package.loaded[mod] = result
return result
else
serr = serr .. "\n " .. err
end
end
error(serr, 2)
end

View File

@@ -0,0 +1,388 @@
-- some term things
-- e.g. redirects, read()
local rc = ...
-- we need a couple of these
local colors = require("colors")
local native = require("term")
local strings = require("cc.strings")
local expect = require("cc.expect").expect
local term = {}
package.loaded.term = term
local thread = require("rc.thread")
local valid = {
write = true,
scroll = true,
getCursorPos = true,
setCursorPos = true,
getCursorBlink = true,
setCursorBlink = true,
getSize = true,
clear = true,
clearLine = true,
getTextColor = true,
getTextColour = true,
setTextColor = true,
setTextColour = true,
getBackgroundColor = true,
getBackgroundColour = true,
setBackgroundColor = true,
setBackgroundColour = true,
isColor = true,
isColour = true,
blit = true,
setPaletteColor = true,
setPaletteColour = true,
getPaletteColor = true,
getPaletteColour = true,
-- LeonOS-PC graphics mode settings
setGraphicsMode = not not native.setGraphicsMode,
getGraphicsMode = not not native.getGraphicsMode,
drawPixels = not not native.drawPixels,
getPixels = not not native.getPixels,
setPixel = not not native.setPixel,
getPixel = not not native.getPixel
}
for k in pairs(valid) do
term[k] = function(...)
local redirect = thread.getTerm()
if not redirect[k] then
error("redirect object does not implement term."..k, 2)
end
return redirect[k](...)
end
end
function term.current()
return thread.getTerm()
end
function term.native()
return native
end
function term.redirect(obj)
expect(1, obj, "table")
return thread.setTerm(obj)
end
function term.at(x, y)
term.setCursorPos(x, y)
return term
end
local keys = require("keys")
-- rc.write
-- [[
function rc.write(text)
expect(1, text, "string")
local lines = 0
local w, h = term.getSize()
local x, y = term.getCursorPos()
local elements = strings.splitElements(text, w)
strings.wrappedWriteElements(elements, w, false, {
newline = function()
lines = lines + 1
x = 1
if y >= h then
term.scroll(1)
else
y = y + 1
end
term.at(x, y)
end,
append = function(newText)
term.at(x, y).write(newText)
x = x + #newText
end,
getX = function()
return x
end
})
return lines
end
--]]
-- old write() used for redrawing in read()
local function write(text)
expect(1, text, "string")
local lines = 0
local w, h = term.getSize()
local function inc_cy(cy)
lines = lines + 1
if cy > h - 1 then
term.scroll(1)
return cy
else
return cy + 1
end
end
while #text > 0 do
local nl = text:find("\n") or #text
local chunk = text:sub(1, nl)
text = text:sub(#chunk + 1)
local has_nl = chunk:sub(-1) == "\n"
if has_nl then chunk = chunk:sub(1, -2) end
local cx, cy = term.getCursorPos()
while #chunk > 0 do
if cx > w then
term.setCursorPos(1, inc_cy(cy))
cx, cy = term.getCursorPos()
end
local to_write = chunk:sub(1, w - cx + 1)
term.write(to_write)
chunk = chunk:sub(#to_write + 1)
cx, cy = term.getCursorPos()
end
if has_nl then
term.setCursorPos(1, inc_cy(cy))
end
end
return lines
end--]]
rc.write = write
-- read
local empty = {}
function term.read(replace, history, complete, default)
expect(1, replace, "string", "nil")
expect(2, history, "table", "nil")
expect(3, complete, "function", "nil")
expect(4, default, "string", "nil")
if replace then replace = replace:sub(1, 1) end
local hist = history or {}
history = {}
for i=1, #hist, 1 do
history[i] = hist[i]
end
local buffer = default or ""
local prev_buf = buffer
history[#history+1] = buffer
local hist_pos = #history
local cursor_pos = 0
local stx, sty = term.getCursorPos()
local w, h = term.getSize()
local dirty = false
local completions = {}
local comp_id = 0
local function clearCompletion()
if completions[comp_id] then
write((" "):rep(#completions[comp_id]))
end
end
local function full_redraw(force)
if force or dirty then
if complete and buffer ~= prev_buf then
completions = complete(buffer) or empty
comp_id = math.min(1, #completions)
end
prev_buf = buffer
term.setCursorPos(stx, sty)
local text = buffer
if replace then text = replace:rep(#text) end
local ln = write(text)
if completions[comp_id] then
local oldfg = term.getTextColor()
local oldbg = term.getBackgroundColor()
term.setTextColor(colors.white)
term.setBackgroundColor(colors.gray)
ln = ln + write(completions[comp_id])
term.setTextColor(oldfg)
term.setBackgroundColor(oldbg)
else
ln = ln + write(" ")
end
if sty + ln > h then
sty = sty - (sty + ln - h)
end
end
-- set cursor to the appropriate spot
local cx, cy = stx, sty
cx = cx + #buffer - cursor_pos-- + #(completions[comp_id] or "")
while cx > w do
cx = cx - w
cy = cy + 1
end
term.setCursorPos(cx, cy)
end
term.setCursorBlink(true)
while true do
full_redraw()
-- get input
local evt, id = rc.pullEvent()
if evt == "char" then
dirty = true
clearCompletion()
if cursor_pos == 0 then
buffer = buffer .. id
elseif cursor_pos == #buffer then
buffer = id .. buffer
else
buffer = buffer:sub(0, -cursor_pos - 1)..id..buffer:sub(-cursor_pos)
end
elseif evt == "paste" then
dirty = true
clearCompletion()
if cursor_pos == 0 then
buffer = buffer .. id
elseif cursor_pos == #buffer then
buffer = id .. buffer
else
buffer = buffer:sub(0, -cursor_pos - 1)..id..
buffer:sub(-cursor_pos+(#id-1))
end
elseif evt == "key" then
id = keys.getName(id)
if id == "backspace" and #buffer > 0 then
dirty = true
if cursor_pos == 0 then
buffer = buffer:sub(1, -2)
clearCompletion()
elseif cursor_pos < #buffer then
buffer = buffer:sub(0, -cursor_pos - 2)..buffer:sub(-cursor_pos)
end
elseif id == "delete" and cursor_pos > 0 then
dirty = true
if cursor_pos == #buffer then
buffer = buffer:sub(2)
elseif cursor_pos == 1 then
buffer = buffer:sub(1, -2)
else
buffer = buffer:sub(0, -cursor_pos - 1) .. buffer:sub(-cursor_pos + 1)
end
cursor_pos = cursor_pos - 1
elseif id == "up" then
if #completions > 1 then
dirty = true
clearCompletion()
if comp_id > 1 then
comp_id = comp_id - 1
else
comp_id = #completions
end
elseif hist_pos > 1 then
cursor_pos = 0
history[hist_pos] = buffer
hist_pos = hist_pos - 1
buffer = (" "):rep(#buffer)
full_redraw(true)
buffer = history[hist_pos]
dirty = true
end
elseif id == "down" then
if #completions > 1 then
dirty = true
clearCompletion()
if comp_id < #completions then
comp_id = comp_id + 1
else
comp_id = 1
end
elseif hist_pos < #history then
cursor_pos = 0
history[hist_pos] = buffer
hist_pos = hist_pos + 1
buffer = (" "):rep(#buffer)
full_redraw(true)
buffer = history[hist_pos]
dirty = true
end
elseif id == "left" then
if cursor_pos < #buffer then
clearCompletion()
cursor_pos = cursor_pos + 1
end
elseif id == "right" then
if cursor_pos > 0 then
cursor_pos = cursor_pos - 1
elseif comp_id > 0 then
dirty = true
buffer = buffer .. completions[comp_id]
end
elseif id == "tab" then
if comp_id > 0 then
dirty = true
buffer = buffer .. completions[comp_id]
end
elseif id == "home" then
cursor_pos = #buffer
elseif id == "end" then
cursor_pos = 0
elseif id == "enter" then
clearCompletion()
write("\n")
break
end
end
end
term.setCursorBlink(false)
return buffer
end
rc.read = term.read
setmetatable(term, {__index = native})

View File

@@ -0,0 +1,13 @@
_G.io = require("rc.io")
function _G.print(...)
local args = table.pack(...)
for i=1, args.n, 1 do
args[i] = tostring(args[i])
end
io.stdout:write(table.concat(args, "\t"), "\n")
return true
end

View File

@@ -0,0 +1,184 @@
-- rc.peripheral
local old = require("peripheral")
local expect = require("cc.expect").expect
local p = {}
package.loaded.peripheral = p
local sides = {"bottom", "top", "left", "right", "front", "back"}
function p.getNames()
local names = {}
for i=1, #sides, 1 do
local side = sides[i]
if old.isPresent(side) then
names[#names+1] = side
if old.hasType(side, "modem") and not old.call(side, "isWireless") then
local remote_names = old.call(side, "getNamesRemote")
for j=1, #remote_names, 1 do
names[#names+1] = remote_names[j]
end
end
end
end
return names
end
-- figure out where a peripheral is
-- returns 0 if the peripheral is directly connected,
-- and a side if it's connected through a modem
local function findByName(name)
if old.isPresent(name) then
return 0
else
for i=1, #sides, 1 do
local side = sides[i]
if old.hasType(side, "modem") and not old.call(side, "isWireless") then
if old.call(side, "isPresentRemote", name) then
return side
end
end
end
end
end
function p.isPresent(name)
expect(1, name, "string")
return not not findByName(name)
end
function p.getType(per)
expect(1, per, "string", "table")
if type(per) == "string" then
local place = findByName(per)
if place == 0 then
return old.getType(per)
elseif place then
return old.call(place, "getTypeRemote", per)
end
else
return table.unpack(per.__types)
end
end
function p.hasType(per, ptype)
expect(1, per, "string", "table")
expect(2, ptype, "string")
if type(per) == "string" then
local place = findByName(per)
if place == 0 then
return old.hasType(per, ptype)
elseif place then
return old.call(place, "hasTypeRemote", per, ptype)
end
else
return per.__types[ptype]
end
end
function p.getMethods(name)
expect(1, name, "string")
local place = findByName(name)
if place == 0 then
return old.getMethods(name)
elseif place then
return old.call(place, "getMethodsRemote", name)
end
end
function p.getName(per)
expect(1, per, "table")
return per.__info.name
end
function p.call(name, method, ...)
expect(1, name, "string")
expect(2, method, "string")
local place = findByName(name)
if place == 0 then
return old.call(name, method, ...)
elseif place then
return old.call(place, "callRemote", name, method, ...)
end
end
function p.wrap(name)
expect(1, name, "string")
local place = findByName(name)
if not place then return end
local methods, types
if place == 0 then
methods = old.getMethods(name)
types = table.pack(old.getType(name))
else
methods = old.call(place, "getMethodsRemote", name)
types = table.pack(old.call(place, "getTypesRemote", name))
end
for i=1, #types, 1 do
types[types[i]] = true
end
local wrapper = {
__info = {
name = name,
types = types,
}
}
if place == 0 then
for i=1, #methods, 1 do
wrapper[methods[i]] = function(...)
return old.call(name, methods[i], ...)
end
end
else
for i=1, #methods, 1 do
wrapper[methods[i]] = function(...)
return old.call(place, "callRemote", name, methods[i], ...)
end
end
end
return wrapper
end
function p.find(ptype, filter)
expect(1, ptype, "string")
expect(2, filter, "function", "nil")
local wrapped = {}
for _, name in ipairs(p.getNames()) do
if p.hasType(name, ptype) then
local wrapper = p.wrap(name)
if (p.filter and p.filter(name, wrapper)) or not p.filter then
wrapped[#wrapped+1] = wrapper
end
end
end
return table.unpack(wrapped)
end

View File

@@ -0,0 +1,168 @@
-- HTTP library adapted from .OS --
-- native http.request: function(
-- url:string[, post:string[, headers:table[, binarymode:boolean]]])
-- post is the data to POST. otherwise a GET is sent.
-- OR: function(parameters:table)
-- where parameters = {
-- url = string, -- the URL
-- body = string, -- the data to POST/PATCH/PUT
-- headers = table, -- request headers
-- binary = boolean, -- self explanatory
-- method = string} -- the HTTP method to use - one of:
-- - GET
-- - POST
-- - HEAD
-- - OPTIONS
-- - PUT
-- - DELETE
-- - PATCH
-- - TRACE
--
-- native http.checkURL: function(url:string)
-- url is a URL to try to reach. queues a http_check event with the result.
-- native http.websocket(url:string[, headers:table])
-- url is the url to which to open a websocket. queues a websocket_success
-- event on success, and websocket_failure on failure.
-- native http.addListener(port:number) (LeonOS-PC only)
-- add a listener on the specified port. when that port receives data,
-- the listener queues a http_request(port:number, request, response).
-- !!the response is not send until response.close() is called!!
-- native http.removeListener(port:number) (LeonOS-PC only)
-- remove the listener from that port
local rc = ...
if not package.loaded.http then
return
end
local old = package.loaded.http
local http = {}
package.loaded.http = http
local field = require("cc.expect").field
local expect = require("cc.expect").expect
local function listenForResponse(url)
while true do
local sig, a, b, c = rc.pullEvent()
if sig == "http_success" and a == url then
return b
elseif sig == "http_failure" and a == url then
return nil, b, c
end
end
end
function http.request(url, post, headers, binary, method, sync)
if type(url) ~= "table" then
url = {
url = url,
body = post,
headers = headers,
binary = binary,
method = method or (post and "POST") or "GET",
sync = not not sync
}
end
field(url, "url", "string")
field(url, "body", "string", "nil")
field(url, "headers", "table", "nil")
field(url, "binary", "boolean", "nil")
field(url, "method", "string")
field(url, "sync", "boolean", "nil")
local ok, err = old.request(url)
if not ok then
return nil, err
end
if sync then return listenForResponse(url.url) end
end
function http.get(url, headers, binary)
if type(url) == "table" then
url.sync = true
return http.request(url)
else
return http.request(url, nil, headers, binary, "GET", true)
end
end
function http.post(url, body, headers, binary)
if type(url) == "table" then
url.sync = true
url.method = "POST"
return http.request(url)
else
return http.request(url, body, headers, binary, "POST", true)
end
end
http.checkURLAsync = old.checkURL
function http.checkURL(url)
expect(1, url, "string")
local ok, err = old.checkURL(url)
if not ok then
return nil, err
end
local sig, _url, a, b
repeat
sig, _url, a, b = coroutine.yield()
until sig == "http_check" and url == _url
return a, b
end
http.websocketAsync = old.websocket
function http.websocket(url, headers)
expect(1, url, "string")
expect(2, headers, "string")
local ok, err = old.websocket(url, headers)
if not ok then
return nil, err
end
while true do
local sig, a, b, c = coroutine.yield()
if sig == "websocket_success" and a == url then
return b, c
elseif sig == "websocket_failure" and a == url then
return nil, b
end
end
end
if old.addListener then
function http.listen(port, callback)
expect(1, port, "number")
expect(2, callback, "function")
old.addListener(port)
while true do
local sig, a, b, c = coroutine.yield()
if sig == "stop_listener" and a == port then
old.removeListener(port)
break
elseif sig == "http_request" and a == port then
if not callback(b, c) then
old.removeListener(port)
break
end
end
end
end
else
function http.listen()
error("This functionality requires LeonOS-PC", 0)
end
end

View File

@@ -0,0 +1,36 @@
-- rc.commands
local expect = require("cc.expect").expect
if not package.loaded.commands then return end
local native = package.loaded.commands
local c = {}
package.loaded.commands = c
c.native = native
c.async = {}
for k, v in pairs(native) do c[k] = v end
local command_list = native.list()
for i=1, #command_list, 1 do
local command = command_list[i]
c.async[command] = function(...)
return c.execAsync(command, ...)
end
c[command] = c[command] or function(...)
return c.exec(command, ...)
end
end
function c.exec(command, ...)
expect(1, command, "string")
return c.native.exec(table.concat(table.pack(command, ...), " "))
end
function c.execAsync(command, ...)
expect(1, command, "string")
return c.native.execAsync(table.concat(table.pack(command, ...), " "))
end

View File

@@ -0,0 +1,101 @@
-- setting definitions
local settings = require("settings")
settings.define("list.show_hidden", {
description = "Show hidden files in list's output",
type = "boolean",
default = false
})
settings.define("bios.compat_mode", {
description = "Attempt some LeonOS compatibility by injecting APIs into _G.",
type = "boolean",
default = false
})
settings.define("shell.tracebacks", {
description = "Show error tracebacks in the shell.",
type = "boolean",
default = false
})
settings.define("edit.scroll_offset", {
description = "How many lines to keep between the cursor and the screen edge.",
type = "number",
default = 3
})
settings.define("edit.force_highlight", {
description = "Whether to use the highlighting editor, even on basic computers.",
type = "boolean",
default = false
})
settings.define("edit.scroll_factor", {
description = "Related to how many lines the editor should jump at a time when scrolling. Determined by term_height/scroll_factor. Adjust this for performance.",
type = "number",
default = 8
})
settings.define("edit.color_separator", {
description = "What color separating characters (e.g. ()[];{}) should be.",
type = "string",
default = "lightBlue"
})
settings.define("edit.color_operator", {
description = "What color operators (e.g. +-/*) should be.",
type = "string",
default = "lightGray"
})
settings.define("edit.color_keyword", {
description = "What color keywords (e.g. local, for, if) should be.",
type = "string",
default = "orange"
})
settings.define("edit.color_boolean", {
description = "What color booleans (true/false) should be.",
type = "string",
default = "purple"
})
settings.define("edit.color_comment", {
description = "What color comments should be.",
type = "string",
default = "gray"
})
settings.define("edit.color_global", {
description = "What color globals (e.g. print, require) should be.",
type = "string",
default = "lime"
})
settings.define("edit.color_string", {
description = "What color strings should be.",
type = "string",
default = "red"
})
settings.define("edit.color_number", {
description = "What color numbers (e.g. 2, 0xF3, 0.42) should be.",
type = "string",
default = "magenta"
})
settings.define("bios.restrict_globals", {
description = "Disallow global variables",
type = "boolean",
default = false
})
settings.define("bios.parallel_startup", {
description = "Run startup scripts from /startup in parallel",
type = "boolean",
default = false
})
settings.load()

View File

@@ -0,0 +1,63 @@
-- update: download a new copy of LeonOS
local rc = require("rc")
local term = require("term")
local colors = require("colors")
local textutils = require("textutils")
if not package.loaded.http then
io.stderr:write("The HTTP API is disabled and the updater cannot continue. Please enable the HTTP API in the ComputerCraft configuration and try again.\n")
return
end
term.at(1,1).clear()
textutils.coloredPrint(colors.yellow,
"LeonOS Updater (Stage 1)\n===========================")
print("Checking for update...")
local http = require("http")
local base = "https://raw.githubusercontent.com/LeonMMcoset/LeonOS/primary/"
local Bhandle, Berr = http.get(base .. "data/computercraft/lua/bios.lua")
if not Bhandle then
error(Berr, 0)
end
local first = Bhandle.readLine()
Bhandle.close()
local oldVersion = rc.version():gsub("LeonOS ", "")
local newVersion = first:match("LeonOS v?(%d+.%d+.%d+)")
if newVersion and (oldVersion ~= newVersion) or (...) == "-f" then
textutils.coloredPrint(colors.green, "Found", colors.white, ": ",
colors.red, oldVersion, colors.yellow, " -> ", colors.lime,
newVersion or oldVersion)
io.write("Apply update? [y/N]: ")
if io.read() ~= "y" then
textutils.coloredPrint(colors.red, "Not applying update.")
return
end
textutils.coloredPrint(colors.green, "Applying update.")
local handle, err = http.get(base.."updater.lua", nil, true)
if not handle then
error("Failed downloading stage 2: " .. err, 0)
end
local data = handle.readAll()
handle.close()
local out = io.open("/.start_rc.lua", "w")
out:write(data)
out:close()
textutils.coloredWrite(colors.yellow, "Restarting...")
rc.sleep(3)
rc.reboot()
else
textutils.coloredPrint(colors.red, "None found")
end

160
installer.lua Normal file
View File

@@ -0,0 +1,160 @@
-- LeonOS installer
local DEFAULT_ROM_DIR = "/rc"
print("Downloading required libraries...")
local function dl(f)
local hand, err = http.get(f, nil, true)
if not hand then
error(err, 0)
end
local data = hand.readAll()
hand.close()
return data
end
-- set up package.loaded for LeonOS libs
package.loaded.rc = {
expect = require("cc.expect").expect,
write = write, sleep = sleep
}
package.loaded.term = term
package.loaded.colors = colors
_G.require = require
function term.at(x, y)
term.setCursorPos(x, y)
return term
end
local function ghload(f, c)
return assert(load(dl("https://raw.githubusercontent.com/"..f),
"="..(c or f), "t", _G))()
end
local json = ghload("rxi/json.lua/master/json.lua", "ghload(json)")
package.loaded["rc.json"] = json
local function rcload(f)
return ghload(
"LeonMMcoset/LeonOS/primary/data/computercraft/lua/rom/"..f, f)
end
-- get LeonOS's textutils with its extra utilities
local tu = rcload("apis/textutils.lua")
local function progress(y, a, b)
local progress = a/b
local w = term.getSize()
local bar = (" "):rep(math.ceil((w-2) * progress))
term.at(1, y)
tu.coloredPrint(colors.yellow, "[", {fg=colors.white, bg=colors.white}, bar,
{fg=colors.white, bg=colors.black}, (" "):rep((w-2)-#bar),
colors.yellow, "]")
end
term.at(1,1).clear()
tu.coloredPrint(colors.yellow,
"LeonOS Installer 1.0\n=======================")
local ROM_DIR
tu.coloredPrint("Enter installation directory ", colors.yellow, "[",
colors.lightBlue, DEFAULT_ROM_DIR, colors.yellow, "]")
tu.coloredWrite(colors.yellow, "$ ")
ROM_DIR = read()
if #ROM_DIR == 0 then ROM_DIR = DEFAULT_ROM_DIR end
ROM_DIR = "/"..shell.resolve(ROM_DIR)
settings.set("LeonOS.rom_dir", ROM_DIR)
settings.save()
tu.coloredPrint(colors.white, "Installing LeonOS to ", colors.lightBlue,
ROM_DIR, colors.white)
local function bullet(t)
tu.coloredWrite(colors.red, "- ", colors.white, t)
end
local function ok()
tu.coloredPrint(colors.green, "OK", colors.white)
end
bullet("Getting repository tree...")
local repodata = dl("https://api.github.com/repos/LeonMMcoset/LeonOS/git/trees/primary?recursive=1")
repodata = json.decode(repodata)
ok()
bullet("Filtering files...")
local look = "data/computercraft/lua/"
local to_dl = {}
for _, v in pairs(repodata.tree) do
if v.path and v.path:sub(1,#look) == look then
v.path = v.path:sub(#look+1)
v.real_path = v.path:gsub("^/?rom", ROM_DIR)
to_dl[#to_dl+1] = v
end
end
ok()
bullet("Creating directories...")
for i=#to_dl, 1, -1 do
local v = to_dl[i]
if v.type == "tree" then
fs.makeDir(fs.combine(v.real_path))
table.remove(to_dl, i)
end
end
ok()
bullet("Downloading files...")
local okx, oky = term.getCursorPos()
io.write("\n")
local _, pby = term.getCursorPos()
local parallels = {}
local done = 0
for i=1, #to_dl, 1 do
local v = to_dl[i]
if v.type == "blob" then
parallels[#parallels+1] = function()
local data = dl("https://raw.githubusercontent.com/LeonMMcoset/LeonOS/primary/data/computercraft/lua/"..v.path)
assert(io.open(v.real_path, "w")):write(data):close()
done = done + 1
progress(pby, done, #to_dl)
end
end
end
parallel.waitForAll(table.unpack(parallels))
term.at(1, pby).write((" "):rep((term.getSize())))
term.at(okx, oky)
ok()
assert(io.open(
fs.exists("/startup.lua") and "/unbios-rc.lua" or "/startup.lua", "w"))
:write(dl(
"https://raw.githubusercontent.com/LeonMMcoset/LeonOS/primary/unbios.lua"
)):close()
tu.coloredPrint(colors.yellow, "Your computer will restart in 5 seconds.")
local _, y = term.getCursorPos()
for i=1, 5, 1 do
progress(y, i, 5)
os.sleep(1)
end
os.sleep(0.5)
os.reboot()

6
pack.mcmeta Normal file
View File

@@ -0,0 +1,6 @@
{
"pack": {
"description": "A saner LeonOS implementation",
"pack_format": 9
}
}

117
unbios.lua Normal file
View File

@@ -0,0 +1,117 @@
-- UnBIOS by JackMacWindows
-- Modified by LeonMMcoset to work with LeonOS
-- This will undo most of the changes/additions made in the BIOS, but some things may remain wrapped if `debug` is unavailable
-- To use, just place a `bios.lua` in the root of the drive, and run this program
-- Here's a list of things that are irreversibly changed:
-- * both `bit` and `bit32` are kept for compatibility
-- * string metatable blocking (on old versions of CC)
-- In addition, if `debug` is not available these things are also irreversibly changed:
-- * old Lua 5.1 `load` function (for loading from a function)
-- * `loadstring` prefixing (before CC:T 1.96.0)
-- * `http.request`
-- * `os.shutdown` and `os.reboot`
-- * `peripheral`
-- * `turtle.equip[Left|Right]`
-- Licensed under the MIT license
if _HOST:find("UnBIOS") then return end
local keptAPIs = {bit32 = true, bit = true, ccemux = true, config = true, coroutine = true, debug = true, fs = true, http = true, io = true, mounter = true, os = true, periphemu = true, peripheral = true, redstone = true, rs = true, term = true, utf8 = true, _HOST = true, _CC_DEFAULT_SETTINGS = true, _CC_DISABLE_LUA51_FEATURES = true, _VERSION = true, assert = true, collectgarbage = true, error = true, gcinfo = true, getfenv = true, getmetatable = true, ipairs = true, __inext = true, load = true, loadstring = true, math = true, newproxy = true, next = true, pairs = true, pcall = true, rawequal = true, rawget = true, rawlen = true, rawset = true, select = true, setfenv = true, setmetatable = true, string = true, table = true, tonumber = true, tostring = true, type = true, unpack = true, xpcall = true, turtle = true, pocket = true, commands = true, _G = true, _RC_ROM_DIR = true}
_G._RC_ROM_DIR = settings.get("LeonOS.rom_dir") or error("LeonOS.rom_dir is not set!", 0)
local t = {}
for k in pairs(_G) do if not keptAPIs[k] then table.insert(t, k) end end
for _,k in ipairs(t) do _G[k] = nil end
_G.term = _G.term.native()
_G.http.checkURL = _G.http.checkURLAsync
_G.http.websocket = _G.http.websocketAsync
if _G.commands then _G.commands = _G.commands.native end
if _G.turtle then _G.turtle.native, _G.turtle.craft = nil end
local delete = {os = {"version", "pullEventRaw", "pullEvent", "run", "loadAPI", "unloadAPI", "sleep"}, http = {"get", "post", "put", "delete", "patch", "options", "head", "trace", "listen", "checkURLAsync", "websocketAsync"}, fs = {"complete", "isDriveRoot"}}
for k,v in pairs(delete) do for _,a in ipairs(v) do _G[k][a] = nil end end
_G._HOST = _G._HOST .. " (UnBIOS)"
-- Set up TLCO
-- This functions by crashing `rednet.run` by removing `os.pullEventRaw`. Normally
-- this would cause `parallel` to throw an error, but we replace `error` with an
-- empty placeholder to let it continue and return without throwing. This results
-- in the `pcall` returning successfully, preventing the error-displaying code
-- from running - essentially making it so that `os.shutdown` is called immediately
-- after the new BIOS exits.
--
-- From there, the setup code is placed in `term.native` since it's the first
-- thing called after `parallel` exits. This loads the new BIOS and prepares it
-- for execution. Finally, it overwrites `os.shutdown` with the new function to
-- allow it to be the last function called in the original BIOS, and returns.
-- From there execution continues, calling the `term.redirect` dummy, skipping
-- over the error-handling code (since `pcall` returned ok), and calling
-- `os.shutdown()`. The real `os.shutdown` is re-added, and the new BIOS is tail
-- called, which effectively makes it run as the main chunk.
local olderror = error
_G.error = function() end
_G.term.redirect = function() end
function _G.term.native()
_G.term.native = nil
_G.term.redirect = nil
_G.error = olderror
term.setBackgroundColor(32768)
term.setTextColor(1)
term.setCursorPos(1, 1)
term.setCursorBlink(true)
term.clear()
local file = fs.open("/bios.lua", "r")
if file == nil then
term.setCursorBlink(false)
term.setTextColor(16384)
term.write("Could not find /bios.lua. UnBIOS cannot continue.")
term.setCursorPos(1, 2)
term.write("Press any key to continue")
coroutine.yield("key")
os.shutdown()
end
local fn, err = loadstring(file.readAll(), "@bios.lua")
file.close()
if fn == nil then
term.setCursorBlink(false)
term.setTextColor(16384)
term.write("Could not load /bios.lua. UnBIOS cannot continue.")
term.setCursorPos(1, 2)
term.write(err)
term.setCursorPos(1, 3)
term.write("Press any key to continue")
coroutine.yield("key")
os.shutdown()
end
setfenv(fn, _G)
local oldshutdown = os.shutdown
os.shutdown = function()
os.shutdown = oldshutdown
return fn()
end
end
if debug then
-- Restore functions that were overwritten in the BIOS
-- Apparently this has to be done *after* redefining term.native
local function restoreValue(tab, idx, name, hint)
local i, key, value = 1, debug.getupvalue(tab[idx], hint)
while key ~= name and key ~= nil do
key, value = debug.getupvalue(tab[idx], i)
i=i+1
end
tab[idx] = value or tab[idx]
end
restoreValue(_G, "loadstring", "nativeloadstring", 1)
restoreValue(_G, "load", "nativeload", 5)
restoreValue(http, "request", "nativeHTTPRequest", 3)
restoreValue(os, "shutdown", "nativeShutdown", 1)
restoreValue(os, "restart", "nativeReboot", 1)
if turtle then
restoreValue(turtle, "equipLeft", "v", 1)
restoreValue(turtle, "equipRight", "v", 1)
end
do
local i, key, value = 1, debug.getupvalue(peripheral.isPresent, 2)
while key ~= "native" and key ~= nil do
key, value = debug.getupvalue(peripheral.isPresent, i)
i=i+1
end
_G.peripheral = value or peripheral
end
end
coroutine.yield()

182
updater.lua Normal file
View File

@@ -0,0 +1,182 @@
-- LeonOS updater: stage 2
local fs = rawget(_G, "fs")
local term = rawget(_G, "term")
local http = rawget(_G, "http")
_G._RC_ROM_DIR = _RC_ROM_DIR or "/rc"
if _RC_ROM_DIR == "/rom" then _RC_ROM_DIR = "/rc" end
-- fail-safe
local start_rc = [[
local fs = rawget(_G, "fs")
local term = rawget(_G, "term")
local w, h = term.getSize()
local function at(x,y)
term.setCursorPos(x,y)
return term
end
term.setBackgroundColor(0x1)
at(1,1).clearLine()
at(1,h).clearLine()
local title = "LeonOS Updater (Failure Notice)"
term.setTextColor(0x4000)
at(math.floor(w/2-#title/2), 1).write(title)
for i=2, h-1, 1 do
term.setBackgroundColor(0x4000)
at(1,i).clearLine()
end
term.setTextColor(0x1)
local message = {
"A LeonOS update has failed or",
"been interrupted. Your files are",
"intact.",
"",
"Press any key to revert to the ROM.",
"",
"",
}
for i=1, #message, 1 do
at(3, i+2).write(message[i])
end
term.setCursorBlink(true)
repeat local x = coroutine.yield() until x == "char"
pcall(fs.delete, _RC_ROM_DIR)
pcall(fs.delete, "/.start_rc.lua")
os.reboot()
while true do coroutine.yield() end
]]
local function at(x,y)
term.setCursorPos(x,y)
return term
end
local handle = fs.open("/.start_rc.lua", "w")
handle.write(start_rc)
handle.close()
assert(pcall(function()
local w, h = term.getSize()
local function dl(f)
local hand, err = http.request(f, nil, nil, true)
local evt
repeat
evt = table.pack(coroutine.yield())
until evt[1] == "http_success" or evt[1] == "http_failure"
if evt[1] == "http_failure" then
term.at(1, h).write(evt[3])
local id = os.startTimer(5)
repeat local _,i = coroutine.yield() until i == id
os.reboot()
while true do coroutine.yield() end
else
hand = evt[3]
local data = hand.readAll()
hand.close()
return data
end
end
local function ghload(f, c)
return assert(loadstring(dl("https://raw.githubusercontent.com/"..f),
"=ghload("..(c or f)..")"))()
end
local json = ghload("rxi/json.lua/master/json.lua", "json")
local function header()
term.setTextColor(0x10)
at(1, 1).clearLine()
at(1, 1).write("LeonOS Updater (Stage 2)")
at(1, 2).clearLine()
at(1, 2).write("===========================")
term.setTextColor(0x1)
end
local y = 1
local function write(text)
if y > h-3 then
term.scroll(1)
header()
else
y = y + 1
end
at(1, y+2).write(text)
end
header()
write("Getting repository tree...")
local repodata = json.decode(dl("https://api.github.com/repos/LeonMMcoset/LeonOS/git/trees/primary?recursive=1"))
write("Filtering files...")
local look = "data/computercraft/lua/"
local to_dl = {}
for _, v in pairs(repodata.tree) do
if v.path and v.path:sub(1,#look) == look then
v.path = v.path:sub(#look+1)
v.real_path = v.path:gsub("^/?rom", _RC_ROM_DIR)
to_dl[#to_dl+1] = v
end
end
write("Creating directories...")
for i=#to_dl, 1, -1 do
local v = to_dl[i]
if v.type == "tree" then
fs.makeDir(fs.combine(v.real_path))
table.remove(to_dl, i)
end
end
write("Downloading files...")
local function progress(a, b)
at(1, 3).clearLine()
term.setBackgroundColor(0x1)
at(1, 3).write((" "):rep(math.ceil((w-2) * (a/b))))
term.setBackgroundColor(0x8000)
end
for i=1, #to_dl do
local v = to_dl[i]
if v.type == "blob" and v.real_path ~= "unbios.lua" then
local data = dl("https://raw.githubusercontent.com/LeonMMcoset/LeonOS/primary/data/computercraft/lua/"..v.path)
write(v.real_path)
progress(i, #to_dl)
if v.real_path == "bios.lua" then
v.real_path = "/.start_rc.lua"
end
local handle = fs.open(v.real_path, "w")
handle.write(data)
handle.close()
end
end
os.reboot()
while true do coroutine.yield() end
end))

4
wishlist.txt Normal file
View File

@@ -0,0 +1,4 @@
Feature wishlist:
- virtual filesystem layer
- change turtle.craft behavior