375 lines
9.7 KiB
Lua
375 lines
9.7 KiB
Lua
-- CMC.lua — Command Controller (AtlasOS Shell/CPU v2.0)
|
|
-- in[1] = cmd_rx ← IOC.out[4] (IN|command)
|
|
-- in[2] = mem_rx ← MMC.out[1] (FS response)
|
|
-- out[1] = disp_tx → IOC.in[2] (OUT|text / CLR / COL|R,G,B)
|
|
-- out[4] = mem_tx → MMC.in[1] (FS request)
|
|
|
|
local ST_IDLE = 0
|
|
local ST_WAIT_FS = 1
|
|
local state = ST_IDLE
|
|
|
|
local uid, gid, curUser = 0, 0, "root"
|
|
local cwd = curUser == "root" and "/" or "/home/" .. curUser
|
|
local lastRespSeq = 0
|
|
|
|
local outQueue = {}
|
|
local pendingCmd = nil
|
|
|
|
local function parseMsg(msg)
|
|
local p = msg:find("|")
|
|
return p and msg:sub(1, p - 1) or msg, p and msg:sub(p + 1) or nil
|
|
end
|
|
|
|
local function normalizePath(path)
|
|
local parts = {}
|
|
for seg in path:gmatch("[^/]+") do
|
|
if seg == ".." then
|
|
if #parts > 0 then table.remove(parts) end
|
|
elseif seg ~= "." then
|
|
parts[#parts + 1] = seg
|
|
end
|
|
end
|
|
if #parts == 0 then return "/" end
|
|
return "/" .. table.concat(parts, "/")
|
|
end
|
|
|
|
local function expandHome(path)
|
|
local home = curUser == "root" and "/" or "/home/" .. curUser
|
|
local rest = path:sub(2)
|
|
if rest == "" or rest:sub(1, 1) == "/" then
|
|
return normalizePath(home .. rest)
|
|
end
|
|
return normalizePath(home .. "/" .. rest)
|
|
end
|
|
|
|
local function resolvePath(path)
|
|
if not path or path == "" then return cwd end
|
|
if path:sub(1, 1) == "~" then return expandHome(path) end
|
|
if path:sub(1, 1) == "/" then return normalizePath(path) end
|
|
local full = cwd .. (cwd:sub(-1) == "/" and "" or "/") .. path
|
|
return normalizePath(full)
|
|
end
|
|
|
|
local function fsReq(op, arg)
|
|
out[4] = op .. (arg and ("|" .. arg) or "")
|
|
end
|
|
|
|
-- queue items: op, value (op = "t" text, "c" color, "x" clear)
|
|
local function qText(v)
|
|
outQueue[#outQueue + 1] = { "t", tostring(v) }
|
|
end
|
|
|
|
local function qErr(v)
|
|
outQueue[#outQueue + 1] = { "c", "255,40,40" }
|
|
outQueue[#outQueue + 1] = { "t", tostring(v) }
|
|
outQueue[#outQueue + 1] = { "c", "0,255,0" }
|
|
end
|
|
|
|
local function qClear()
|
|
outQueue[#outQueue + 1] = { "x" }
|
|
end
|
|
|
|
local function qColor(r, g, b)
|
|
outQueue[#outQueue + 1] = { "c", r .. "," .. g .. "," .. b }
|
|
end
|
|
|
|
local function queueLines(lines)
|
|
for i = 1, #lines do
|
|
qText(lines[i])
|
|
end
|
|
end
|
|
|
|
-- centralized argument validation
|
|
local valid = {}
|
|
|
|
function valid.minArgs(args, n, msg)
|
|
if #args < n then error("usage: " .. msg) end
|
|
end
|
|
|
|
function valid.maxArgs(args, n, msg)
|
|
if #args > n then error("usage: " .. msg) end
|
|
end
|
|
|
|
function valid.exactArgs(args, n, msg)
|
|
if #args ~= n then error("usage: " .. msg) end
|
|
end
|
|
|
|
function valid.num(v, name)
|
|
local n = tonumber(v)
|
|
if not n then error(name .. ": expected number, got '" .. tostring(v) .. "'") end
|
|
return math.floor(math.max(0, math.min(255, n)))
|
|
end
|
|
|
|
function valid.flags(args, allowed)
|
|
local opts, positional = {}, nil
|
|
for _, arg in ipairs(args) do
|
|
if arg:sub(1, 1) == "-" then
|
|
for j = 2, #arg do
|
|
local c = arg:sub(j, j)
|
|
if not allowed[c] then error("unknown option: -" .. c) end
|
|
opts[c] = true
|
|
end
|
|
else
|
|
positional = arg
|
|
end
|
|
end
|
|
return opts, positional
|
|
end
|
|
|
|
local cmds = {}
|
|
|
|
local function reg(name, fn, desc, usage)
|
|
cmds[name] = { fn = fn, desc = desc, usage = usage }
|
|
end
|
|
|
|
reg("help", function(a)
|
|
local lines = { curUser .. " " .. cwd .. "> help" }
|
|
lines[#lines + 1] = "AtlasOS v2.0:"
|
|
for n, e in pairs(cmds) do
|
|
local u = e.usage and " " .. e.usage or ""
|
|
lines[#lines + 1] = " " .. n .. u .. " " .. e.desc
|
|
end
|
|
queueLines(lines)
|
|
end, "This help")
|
|
|
|
reg("echo", function(a)
|
|
valid.minArgs(a, 1, "echo <text>")
|
|
local out = table.concat(a, " ")
|
|
queueLines({ curUser .. " " .. cwd .. "> echo " .. out, out })
|
|
end, "Echo text", "<text>")
|
|
|
|
reg("clear", function(a)
|
|
qClear()
|
|
end, "Clear screen")
|
|
|
|
reg("color", function(a)
|
|
valid.exactArgs(a, 3, "color <R> <G> <B>")
|
|
local r = valid.num(a[1], "color")
|
|
local g = valid.num(a[2], "color")
|
|
local b = valid.num(a[3], "color")
|
|
qColor(r, g, b)
|
|
queueLines({ curUser .. " " .. cwd .. "> color",
|
|
"Color set to " .. r .. "," .. g .. "," .. b })
|
|
end, "Set color", "<R> <G> <B>")
|
|
|
|
reg("status", function(a)
|
|
pendingCmd = { name = "status",
|
|
prompt = curUser .. " " .. cwd .. "> status" }
|
|
state = ST_WAIT_FS
|
|
fsReq("STATFS", "")
|
|
end, "System status")
|
|
|
|
reg("ls", function(a)
|
|
local opts, path = valid.flags(a, { a = true, l = true })
|
|
path = resolvePath(path or cwd)
|
|
local cmdStr = "ls"
|
|
for i = 1, #a do cmdStr = cmdStr .. " " .. tostring(a[i]) end
|
|
pendingCmd = { name = "ls", opts = opts, path = path,
|
|
prompt = curUser .. " " .. cwd .. "> " .. cmdStr }
|
|
state = ST_WAIT_FS
|
|
fsReq("LS", path)
|
|
end, "List directory", "[-la] [path]")
|
|
|
|
reg("cat", function(a)
|
|
valid.minArgs(a, 1, "cat <path>")
|
|
local path = resolvePath(a[1])
|
|
pendingCmd = { name = "cat", path = path,
|
|
prompt = curUser .. " " .. cwd .. "> cat " .. a[1] }
|
|
state = ST_WAIT_FS
|
|
fsReq("READ", path .. "|0|8192")
|
|
end, "Show file content", "<path>")
|
|
|
|
reg("cd", function(a)
|
|
valid.maxArgs(a, 1, "cd [path]")
|
|
local path = a[1] and resolvePath(a[1]) or "/"
|
|
pendingCmd = { name = "cd", path = path,
|
|
prompt = curUser .. " " .. cwd .. "> cd " .. (a[1] or "") }
|
|
state = ST_WAIT_FS
|
|
fsReq("CHDIR", path)
|
|
end, "Change directory", "[path]")
|
|
|
|
reg("stat", function(a)
|
|
valid.minArgs(a, 1, "stat <path>")
|
|
local path = resolvePath(a[1])
|
|
pendingCmd = { name = "stat", path = path,
|
|
prompt = curUser .. " " .. cwd .. "> stat " .. a[1] }
|
|
state = ST_WAIT_FS
|
|
fsReq("STAT", path)
|
|
end, "Show file metadata", "<path>")
|
|
|
|
reg("man", function(a)
|
|
valid.exactArgs(a, 1, "man <command>")
|
|
local cmd = a[1]
|
|
pendingCmd = { name = "man", cmd = cmd,
|
|
prompt = curUser .. " " .. cwd .. "> man " .. cmd }
|
|
state = ST_WAIT_FS
|
|
fsReq("READ", "/etc/man/" .. cmd .. "|0|8192")
|
|
end, "Display manual page", "<command>")
|
|
|
|
local function execCmd(cmdStr)
|
|
local parts = {}
|
|
for t in cmdStr:gmatch("[^ ]+") do
|
|
parts[#parts + 1] = t
|
|
end
|
|
if #parts == 0 then return end
|
|
|
|
local name = parts[1]
|
|
local args = {}
|
|
for i = 2, #parts do
|
|
args[#args + 1] = parts[i]
|
|
end
|
|
|
|
local entry = cmds[name]
|
|
if not entry then
|
|
qText(curUser .. " " .. cwd .. "> " .. cmdStr)
|
|
qErr("Unknown: " .. name .. ". Try 'help'.")
|
|
return
|
|
end
|
|
|
|
local ok, msg = pcall(entry.fn, args)
|
|
if not ok then
|
|
qText(curUser .. " " .. cwd .. "> " .. cmdStr)
|
|
qErr(tostring(msg))
|
|
end
|
|
end
|
|
|
|
local function handleFSResponse(op, data)
|
|
if not pendingCmd then return end
|
|
local cmd = pendingCmd
|
|
|
|
if op == "ERR" then
|
|
local code = data or "EIO"
|
|
qText(cmd.prompt)
|
|
if cmd.name == "man" then
|
|
qErr("No manual entry for " .. cmd.cmd)
|
|
else
|
|
local m = cmd.path
|
|
if code == "ENOENT" then
|
|
m = m .. ": No such file or directory"
|
|
elseif code == "EACCES" then
|
|
m = m .. ": Permission denied"
|
|
elseif code == "ENOTDIR" then
|
|
m = m .. ": Not a directory"
|
|
elseif code == "EISDIR" then
|
|
m = m .. ": Is a directory"
|
|
else
|
|
m = m .. ": " .. code
|
|
end
|
|
qErr(m)
|
|
end
|
|
pendingCmd = nil
|
|
state = ST_IDLE
|
|
return
|
|
end
|
|
|
|
local lines = { cmd.prompt }
|
|
|
|
if cmd.name == "ls" then
|
|
local showAll = cmd.opts["a"] or false
|
|
local names = {}
|
|
for n in data:gmatch("([^|]+)") do
|
|
names[#names + 1] = n
|
|
end
|
|
for _, n in ipairs(names) do
|
|
if n ~= "." and n ~= ".." or showAll then
|
|
lines[#lines + 1] = n
|
|
end
|
|
end
|
|
if #lines == 1 then lines[#lines + 1] = "(empty)" end
|
|
|
|
elseif cmd.name == "cat" then
|
|
if data and #data > 0 then
|
|
for line in data:gmatch("[^\n]+") do
|
|
lines[#lines + 1] = line
|
|
end
|
|
else
|
|
lines[#lines + 1] = "(empty)"
|
|
end
|
|
|
|
elseif cmd.name == "cd" then
|
|
cwd = data
|
|
lines[#lines + 1] = cwd
|
|
|
|
elseif cmd.name == "status" then
|
|
local parts = {}
|
|
for s in data:gmatch("([^|]+)") do
|
|
parts[#parts + 1] = s
|
|
end
|
|
local total = tonumber(parts[1]) or 8192
|
|
local used = tonumber(parts[2]) or 0
|
|
lines[#lines + 1] = "AtlasOS v2.0 User: " .. curUser .. "(" .. uid .. ")"
|
|
lines[#lines + 1] = "CWD: " .. cwd
|
|
lines[#lines + 1] = "Mem: " .. (total - used) .. "/" .. total .. " free"
|
|
|
|
elseif cmd.name == "stat" then
|
|
local parts = {}
|
|
for s in data:gmatch("([^|]+)") do
|
|
parts[#parts + 1] = s
|
|
end
|
|
if #parts >= 6 then
|
|
local typ = parts[2] == "d" and "directory" or "file"
|
|
lines[#lines + 1] = "Inode: " .. parts[1] .. " Type: " .. typ
|
|
lines[#lines + 1] = "Mode: " .. parts[3] .. " UID: " .. parts[4] .. " GID: " .. parts[5]
|
|
lines[#lines + 1] = "Size: " .. parts[6] .. " Modified: " .. (parts[7] or "?")
|
|
else
|
|
lines[#lines + 1] = data
|
|
end
|
|
|
|
elseif cmd.name == "man" then
|
|
if data and #data > 0 then
|
|
for line in data:gmatch("[^\n]+") do
|
|
lines[#lines + 1] = line
|
|
end
|
|
else
|
|
lines[#lines + 1] = "(no content)"
|
|
end
|
|
end
|
|
|
|
queueLines(lines)
|
|
out[4] = ""
|
|
pendingCmd = nil
|
|
state = ST_IDLE
|
|
end
|
|
|
|
inp = {}
|
|
|
|
function upd()
|
|
if #outQueue > 0 then
|
|
local item = outQueue[1]
|
|
table.remove(outQueue, 1)
|
|
if item[1] == "t" then
|
|
out[1] = "OUT|" .. tostring(item[2])
|
|
elseif item[1] == "c" then
|
|
out[1] = "COL|" .. tostring(item[2])
|
|
elseif item[1] == "x" then
|
|
out[1] = "CLR"
|
|
end
|
|
end
|
|
|
|
if state == ST_IDLE then
|
|
if inp[1] ~= nil then
|
|
local op, cmd = parseMsg(tostring(inp[1]))
|
|
if op == "IN" and cmd and #cmd > 0 then
|
|
execCmd(cmd)
|
|
end
|
|
end
|
|
end
|
|
|
|
if state == ST_WAIT_FS and inp[2] ~= nil then
|
|
local resp = tostring(inp[2])
|
|
if #resp > 0 then
|
|
local op, rest = parseMsg(resp)
|
|
if op and rest then
|
|
local seqStr, data = parseMsg(rest)
|
|
local seq = tonumber(seqStr) or 0
|
|
if seq > lastRespSeq then
|
|
lastRespSeq = seq
|
|
handleFSResponse(op, data)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
table.clear(inp)
|
|
end
|