-- 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 ") local out = table.concat(a, " ") queueLines({ curUser .. " " .. cwd .. "> echo " .. out, out }) end, "Echo text", "") reg("clear", function(a) qClear() end, "Clear screen") reg("color", function(a) valid.exactArgs(a, 3, "color ") 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", " ") 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 ") 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", "") 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 ") 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", "") reg("man", function(a) valid.exactArgs(a, 1, "man ") 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", "") 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