feat: 初始提交 LeonOS 实现

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

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

View File

@@ -0,0 +1,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