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,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