mirror of
https://github.com/CCLeonOS/LeonOS.git
synced 2026-03-03 06:47:00 +00:00
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:
7
LICENSE
Normal file
7
LICENSE
Normal 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
118
NEW_FEATURES.txt
Normal 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
15
README.md
Normal 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.
|
||||
189
data/computercraft/lua/bios.lua
Normal file
189
data/computercraft/lua/bios.lua
Normal 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()
|
||||
82
data/computercraft/lua/rom/apis/colors.lua
Normal file
82
data/computercraft/lua/rom/apis/colors.lua
Normal 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
|
||||
1
data/computercraft/lua/rom/apis/colours.lua
Normal file
1
data/computercraft/lua/rom/apis/colours.lua
Normal file
@@ -0,0 +1 @@
|
||||
return require("colors")
|
||||
35
data/computercraft/lua/rom/apis/disk.lua
Normal file
35
data/computercraft/lua/rom/apis/disk.lua
Normal 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
|
||||
18
data/computercraft/lua/rom/apis/gps.lua
Normal file
18
data/computercraft/lua/rom/apis/gps.lua
Normal 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
|
||||
102
data/computercraft/lua/rom/apis/help.lua
Normal file
102
data/computercraft/lua/rom/apis/help.lua
Normal 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
|
||||
29
data/computercraft/lua/rom/apis/keys.lua
Normal file
29
data/computercraft/lua/rom/apis/keys.lua
Normal 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
|
||||
31
data/computercraft/lua/rom/apis/multishell.lua
Normal file
31
data/computercraft/lua/rom/apis/multishell.lua
Normal 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
|
||||
158
data/computercraft/lua/rom/apis/paintutils.lua
Normal file
158
data/computercraft/lua/rom/apis/paintutils.lua
Normal 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
|
||||
54
data/computercraft/lua/rom/apis/parallel.lua
Normal file
54
data/computercraft/lua/rom/apis/parallel.lua
Normal 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
|
||||
113
data/computercraft/lua/rom/apis/rednet.lua
Normal file
113
data/computercraft/lua/rom/apis/rednet.lua
Normal 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
|
||||
114
data/computercraft/lua/rom/apis/settings.lua
Normal file
114
data/computercraft/lua/rom/apis/settings.lua
Normal 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
|
||||
334
data/computercraft/lua/rom/apis/shell.lua
Normal file
334
data/computercraft/lua/rom/apis/shell.lua
Normal 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
|
||||
391
data/computercraft/lua/rom/apis/textutils.lua
Normal file
391
data/computercraft/lua/rom/apis/textutils.lua
Normal 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
|
||||
76
data/computercraft/lua/rom/apis/vector.lua
Normal file
76
data/computercraft/lua/rom/apis/vector.lua
Normal 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
|
||||
368
data/computercraft/lua/rom/apis/window.lua
Normal file
368
data/computercraft/lua/rom/apis/window.lua
Normal 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
|
||||
6
data/computercraft/lua/rom/completions/alias.lua
Normal file
6
data/computercraft/lua/rom/completions/alias.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
local shell = require("shell")
|
||||
local completion = require("cc.shell.completion")
|
||||
|
||||
shell.setCompletionFunction("alias", completion.build(
|
||||
nil, completion.program
|
||||
))
|
||||
6
data/computercraft/lua/rom/completions/bedit.lua
Normal file
6
data/computercraft/lua/rom/completions/bedit.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
local shell = require("shell")
|
||||
local completion = require("cc.shell.completion")
|
||||
|
||||
shell.setCompletionFunction("bedit", completion.build(
|
||||
completion.dirOrFile
|
||||
))
|
||||
6
data/computercraft/lua/rom/completions/bg.lua
Normal file
6
data/computercraft/lua/rom/completions/bg.lua
Normal 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 }
|
||||
))
|
||||
4
data/computercraft/lua/rom/completions/cd.lua
Normal file
4
data/computercraft/lua/rom/completions/cd.lua
Normal file
@@ -0,0 +1,4 @@
|
||||
local shell = require("shell")
|
||||
local completion = require("cc.shell.completion")
|
||||
|
||||
shell.setCompletionFunction("enter", completion.build(completion.dir))
|
||||
7
data/computercraft/lua/rom/completions/copy.lua
Normal file
7
data/computercraft/lua/rom/completions/copy.lua
Normal 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}
|
||||
))
|
||||
6
data/computercraft/lua/rom/completions/delete.lua
Normal file
6
data/computercraft/lua/rom/completions/delete.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
local shell = require("shell")
|
||||
local completion = require("cc.shell.completion")
|
||||
|
||||
shell.setCompletionFunction("delete", completion.build(
|
||||
{completion.dirOrFile, many = true}
|
||||
))
|
||||
11
data/computercraft/lua/rom/completions/devbin.lua
Normal file
11
data/computercraft/lua/rom/completions/devbin.lua
Normal 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
|
||||
))
|
||||
6
data/computercraft/lua/rom/completions/edit.lua
Normal file
6
data/computercraft/lua/rom/completions/edit.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
local shell = require("shell")
|
||||
local completion = require("cc.shell.completion")
|
||||
|
||||
shell.setCompletionFunction("edit", completion.build(
|
||||
completion.dirOrFile
|
||||
))
|
||||
6
data/computercraft/lua/rom/completions/fg.lua
Normal file
6
data/computercraft/lua/rom/completions/fg.lua
Normal 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 }
|
||||
))
|
||||
7
data/computercraft/lua/rom/completions/help.lua
Normal file
7
data/computercraft/lua/rom/completions/help.lua
Normal 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}
|
||||
))
|
||||
6
data/computercraft/lua/rom/completions/list.lua
Normal file
6
data/computercraft/lua/rom/completions/list.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
local shell = require("shell")
|
||||
local completion = require("cc.shell.completion")
|
||||
|
||||
shell.setCompletionFunction("list", completion.build(
|
||||
{completion.dir, many = true}
|
||||
))
|
||||
6
data/computercraft/lua/rom/completions/mkdir.lua
Normal file
6
data/computercraft/lua/rom/completions/mkdir.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
local shell = require("shell")
|
||||
local completion = require("cc.shell.completion")
|
||||
|
||||
shell.setCompletionFunction("mkdir", completion.build(
|
||||
{completion.dir, many = true}
|
||||
))
|
||||
7
data/computercraft/lua/rom/completions/move.lua
Normal file
7
data/computercraft/lua/rom/completions/move.lua
Normal 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}
|
||||
))
|
||||
6
data/computercraft/lua/rom/completions/paint.lua
Normal file
6
data/computercraft/lua/rom/completions/paint.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
local shell = require("shell")
|
||||
local completion = require("cc.shell.completion")
|
||||
|
||||
shell.setCompletionFunction("paint", completion.build(
|
||||
|
||||
))
|
||||
6
data/computercraft/lua/rom/completions/reboot.lua
Normal file
6
data/computercraft/lua/rom/completions/reboot.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
local shell = require("shell")
|
||||
local completion = require("cc.shell.completion")
|
||||
|
||||
shell.setCompletionFunction("restart", completion.build(
|
||||
{ completion.choice, { "now" } }
|
||||
))
|
||||
13
data/computercraft/lua/rom/completions/redstone.lua
Normal file
13
data/computercraft/lua/rom/completions/redstone.lua
Normal 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
|
||||
))
|
||||
6
data/computercraft/lua/rom/completions/set.lua
Normal file
6
data/computercraft/lua/rom/completions/set.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
local shell = require("shell")
|
||||
local completion = require("cc.shell.completion")
|
||||
|
||||
shell.setCompletionFunction("set", completion.build(
|
||||
{ completion.setting, true }
|
||||
))
|
||||
6
data/computercraft/lua/rom/completions/shutdown.lua
Normal file
6
data/computercraft/lua/rom/completions/shutdown.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
local shell = require("shell")
|
||||
local completion = require("cc.shell.completion")
|
||||
|
||||
shell.setCompletionFunction("shutdown", completion.build(
|
||||
{ completion.choice, { "now" } }
|
||||
))
|
||||
308
data/computercraft/lua/rom/editors/advanced.lua
Normal file
308
data/computercraft/lua/rom/editors/advanced.lua
Normal 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
|
||||
229
data/computercraft/lua/rom/editors/basic.lua
Normal file
229
data/computercraft/lua/rom/editors/basic.lua
Normal 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
|
||||
14
data/computercraft/lua/rom/help/about.hlp
Normal file
14
data/computercraft/lua/rom/help/about.hlp
Normal 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.
|
||||
25
data/computercraft/lua/rom/keymaps/lwjgl2.lua
Normal file
25
data/computercraft/lua/rom/keymaps/lwjgl2.lua
Normal 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"
|
||||
}
|
||||
120
data/computercraft/lua/rom/keymaps/lwjgl3.lua
Normal file
120
data/computercraft/lua/rom/keymaps/lwjgl3.lua
Normal 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",
|
||||
}
|
||||
228
data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua
Normal file
228
data/computercraft/lua/rom/modules/main/cc/audio/dfpwm.lua
Normal 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,
|
||||
}
|
||||
45
data/computercraft/lua/rom/modules/main/cc/completion.lua
Normal file
45
data/computercraft/lua/rom/modules/main/cc/completion.lua
Normal 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
|
||||
50
data/computercraft/lua/rom/modules/main/cc/expect.lua
Normal file
50
data/computercraft/lua/rom/modules/main/cc/expect.lua
Normal 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
|
||||
293
data/computercraft/lua/rom/modules/main/cc/http/gist.lua
Normal file
293
data/computercraft/lua/rom/modules/main/cc/http/gist.lua
Normal 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
|
||||
523
data/computercraft/lua/rom/modules/main/cc/pretty.lua
Normal file
523
data/computercraft/lua/rom/modules/main/cc/pretty.lua
Normal 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,
|
||||
}
|
||||
113
data/computercraft/lua/rom/modules/main/cc/shell/completion.lua
Normal file
113
data/computercraft/lua/rom/modules/main/cc/shell/completion.lua
Normal 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
|
||||
133
data/computercraft/lua/rom/modules/main/cc/strings.lua
Normal file
133
data/computercraft/lua/rom/modules/main/cc/strings.lua
Normal 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
|
||||
198
data/computercraft/lua/rom/modules/main/edit/syntax.lua
Normal file
198
data/computercraft/lua/rom/modules/main/edit/syntax.lua
Normal 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
|
||||
152
data/computercraft/lua/rom/modules/main/edit/syntax/lua.lua
Normal file
152
data/computercraft/lua/rom/modules/main/edit/syntax/lua.lua
Normal 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
|
||||
36
data/computercraft/lua/rom/modules/main/rc/copy.lua
Normal file
36
data/computercraft/lua/rom/modules/main/rc/copy.lua
Normal 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 }
|
||||
231
data/computercraft/lua/rom/modules/main/rc/io.lua
Normal file
231
data/computercraft/lua/rom/modules/main/rc/io.lua
Normal 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
|
||||
388
data/computercraft/lua/rom/modules/main/rc/json.lua
Normal file
388
data/computercraft/lua/rom/modules/main/rc/json.lua
Normal 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
|
||||
398
data/computercraft/lua/rom/modules/main/rc/thread.lua
Normal file
398
data/computercraft/lua/rom/modules/main/rc/thread.lua
Normal 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
|
||||
7
data/computercraft/lua/rom/programs/about.lua
Normal file
7
data/computercraft/lua/rom/programs/about.lua
Normal 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)
|
||||
29
data/computercraft/lua/rom/programs/alias.lua
Normal file
29
data/computercraft/lua/rom/programs/alias.lua
Normal 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
|
||||
19
data/computercraft/lua/rom/programs/bg.lua
Normal file
19
data/computercraft/lua/rom/programs/bg.lua
Normal 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])
|
||||
1
data/computercraft/lua/rom/programs/clear.lua
Normal file
1
data/computercraft/lua/rom/programs/clear.lua
Normal file
@@ -0,0 +1 @@
|
||||
require("term").at(1,1).clear()
|
||||
36
data/computercraft/lua/rom/programs/copy.lua
Normal file
36
data/computercraft/lua/rom/programs/copy.lua
Normal 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
|
||||
58
data/computercraft/lua/rom/programs/craftos.lua
Normal file
58
data/computercraft/lua/rom/programs/craftos.lua
Normal 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()
|
||||
24
data/computercraft/lua/rom/programs/delete.lua
Normal file
24
data/computercraft/lua/rom/programs/delete.lua
Normal 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
|
||||
89
data/computercraft/lua/rom/programs/devbin.lua
Normal file
89
data/computercraft/lua/rom/programs/devbin.lua
Normal 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
|
||||
12
data/computercraft/lua/rom/programs/edit.lua
Normal file
12
data/computercraft/lua/rom/programs/edit.lua
Normal 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
|
||||
19
data/computercraft/lua/rom/programs/fg.lua
Normal file
19
data/computercraft/lua/rom/programs/fg.lua
Normal 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])))
|
||||
25
data/computercraft/lua/rom/programs/help.lua
Normal file
25
data/computercraft/lua/rom/programs/help.lua
Normal 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
|
||||
46
data/computercraft/lua/rom/programs/list.lua
Normal file
46
data/computercraft/lua/rom/programs/list.lua
Normal 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
|
||||
46
data/computercraft/lua/rom/programs/lua.lua
Normal file
46
data/computercraft/lua/rom/programs/lua.lua
Normal 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
|
||||
12
data/computercraft/lua/rom/programs/mkdir.lua
Normal file
12
data/computercraft/lua/rom/programs/mkdir.lua
Normal 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
|
||||
36
data/computercraft/lua/rom/programs/move.lua
Normal file
36
data/computercraft/lua/rom/programs/move.lua
Normal 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
|
||||
3
data/computercraft/lua/rom/programs/paint.lua
Normal file
3
data/computercraft/lua/rom/programs/paint.lua
Normal file
@@ -0,0 +1,3 @@
|
||||
-- BIMG editor
|
||||
|
||||
local term = require("term")
|
||||
16
data/computercraft/lua/rom/programs/peripherals.lua
Normal file
16
data/computercraft/lua/rom/programs/peripherals.lua
Normal 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
|
||||
6
data/computercraft/lua/rom/programs/programs.lua
Normal file
6
data/computercraft/lua/rom/programs/programs.lua
Normal 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())
|
||||
10
data/computercraft/lua/rom/programs/reboot.lua
Normal file
10
data/computercraft/lua/rom/programs/reboot.lua
Normal 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()
|
||||
88
data/computercraft/lua/rom/programs/redstone.lua
Normal file
88
data/computercraft/lua/rom/programs/redstone.lua
Normal 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
|
||||
47
data/computercraft/lua/rom/programs/set.lua
Normal file
47
data/computercraft/lua/rom/programs/set.lua
Normal 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
|
||||
79
data/computercraft/lua/rom/programs/shell.lua
Normal file
79
data/computercraft/lua/rom/programs/shell.lua
Normal 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
|
||||
10
data/computercraft/lua/rom/programs/shutdown.lua
Normal file
10
data/computercraft/lua/rom/programs/shutdown.lua
Normal 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()
|
||||
16
data/computercraft/lua/rom/programs/threads.lua
Normal file
16
data/computercraft/lua/rom/programs/threads.lua
Normal 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
|
||||
44
data/computercraft/lua/rom/programs/wget.lua
Normal file
44
data/computercraft/lua/rom/programs/wget.lua
Normal 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
|
||||
201
data/computercraft/lua/rom/startup/00_fs.lua
Normal file
201
data/computercraft/lua/rom/startup/00_fs.lua
Normal 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
|
||||
131
data/computercraft/lua/rom/startup/10_package.lua
Normal file
131
data/computercraft/lua/rom/startup/10_package.lua
Normal 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
|
||||
388
data/computercraft/lua/rom/startup/15_term.lua
Normal file
388
data/computercraft/lua/rom/startup/15_term.lua
Normal 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})
|
||||
13
data/computercraft/lua/rom/startup/20_io.lua
Normal file
13
data/computercraft/lua/rom/startup/20_io.lua
Normal 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
|
||||
184
data/computercraft/lua/rom/startup/30_peripheral.lua
Normal file
184
data/computercraft/lua/rom/startup/30_peripheral.lua
Normal 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
|
||||
168
data/computercraft/lua/rom/startup/35_http.lua
Normal file
168
data/computercraft/lua/rom/startup/35_http.lua
Normal 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
|
||||
36
data/computercraft/lua/rom/startup/40_commands.lua
Normal file
36
data/computercraft/lua/rom/startup/40_commands.lua
Normal 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
|
||||
101
data/computercraft/lua/rom/startup/90_settings.lua
Normal file
101
data/computercraft/lua/rom/startup/90_settings.lua
Normal 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()
|
||||
63
data/computercraft/lua/rom/update.lua
Normal file
63
data/computercraft/lua/rom/update.lua
Normal 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
160
installer.lua
Normal 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
6
pack.mcmeta
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"pack": {
|
||||
"description": "A saner LeonOS implementation",
|
||||
"pack_format": 9
|
||||
}
|
||||
}
|
||||
117
unbios.lua
Normal file
117
unbios.lua
Normal 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
182
updater.lua
Normal 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
4
wishlist.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
Feature wishlist:
|
||||
|
||||
- virtual filesystem layer
|
||||
- change turtle.craft behavior
|
||||
Reference in New Issue
Block a user