mirror of
https://github.com/CCLeonOS/LeonOS.git
synced 2026-03-03 15:17:01 +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:
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
|
||||
Reference in New Issue
Block a user