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,7 @@
-- about
local rc = require("rc")
local colors = require("colors")
local textutils = require("textutils")
textutils.coloredPrint(colors.yellow, rc.version() .. " on " .. _HOST)

View File

@@ -0,0 +1,29 @@
-- alias
local args = {...}
local shell = require("shell")
local colors = require("colors")
local textutils = require("textutils")
if #args == 0 then
textutils.coloredPrint(colors.yellow, "shell aliases", colors.white)
local aliases = shell.aliases()
local _aliases = {}
for k, v in pairs(aliases) do
table.insert(_aliases, {colors.cyan, k, colors.white, ":", v})
end
textutils.pagedTabulate(_aliases)
elseif #args == 1 then
shell.clearAlias(args[1])
elseif #args == 2 then
shell.setAlias(args[1], args[2])
else
error("this program takes a maximum of two arguments", 0)
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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