OC-KittenOS/init.lua

691 lines
19 KiB
Lua

-- KittenOS
-- ISO 639 language code.
local systemLanguage = "en"
function component.get(tp)
local a = component.list(tp, true)()
if not a then return nil end
return component.proxy(a)
end
local primaryDisk = component.proxy(computer.getBootAddress())
local langFile = primaryDisk.open("language", "rb")
if langFile then
systemLanguage = primaryDisk.read(langFile, 64)
primaryDisk.close(langFile)
end
local function loadfile(s, e)
local h = primaryDisk.open(s)
if h then
local ch = ""
local c = primaryDisk.read(h, 256)
while c do
ch = ch .. c
c = primaryDisk.read(h, 256)
end
primaryDisk.close(h)
return load(ch, "=" .. s, "t", e)
end
return nil, "File Unreadable"
end
-- Must be a sane source of time in seconds.
local function saneTime()
return computer.uptime()
end
local primaryScreen = component.get("screen")
local primaryGPU = component.get("gpu")
primaryGPU.bind(primaryScreen.address)
local scrW, scrH = 50, 16
local frameH = 1
local redrawWorldSoon = true
primaryGPU.setResolution(scrW, scrH)
primaryGPU.setBackground(0x000000)
primaryGPU.setForeground(0xFFFFFF)
primaryGPU.fill(1, 1, scrW, scrH, "#")
-- apps maps aid (Running application ID, like Process ID) to app data
-- appZ handles Z-order
local apps = {}
local appZ = {}
local function sanech(ch, bch)
if not bch then bch = " " end
if not ch then return bch end
if unicode.len(ch) ~= 1 then return bch end
return ch
end
-- aid
local launchApp = nil
-- text, die
local dialogApp = nil
-- aid, pkg, txt
local openDialog = nil
-- Component of the system that handles file access,
-- does cleanup after dead apps, etc.
local fileWrapper = nil
local drawing = false
local function handleEv(aid, evt, ...)
local f = apps[aid].i[evt]
if not f then return end
local r2 = {pcall(f, ...)}
if r2[1] then
return select(2, table.unpack(r2))
else
-- possible error during death
if apps[aid] then
-- error, override instance immediately.
local i, w, h = dialogApp(r2[2], apps[aid].A.die)
-- REALLY BAD STUFF (not much choice)
local od = drawing
drawing = false
apps[aid].i = i
--apps[aid].i = {}
apps[aid].A.resize(w, h)
drawing = od
else
openDialog("error-" .. aid, "*error-during-death", r2[2])
end
end
end
local needRedraw = {}
local function handleEvNRD(aid, ...)
local doRedraw = handleEv(aid, ...)
if doRedraw then needRedraw[aid] = true end
end
-- This function is critical to wide text support.
-- The measures taken below mean that the *system* can deal with wide text,
-- but applications need to have the same spacing rules in place.
-- Since these spacing rules,
-- and the ability to get a wide-char point from a "normal" point,
-- are probably beneficial to everybody anyway,
-- they're exposed here as a unicode function.
function unicode.safeTextFormat(s, ptr)
local res = ""
if not ptr then ptr = 1 end
local aptr = 1
for i = 1, unicode.len(s) do
local ch = unicode.sub(s, i, i)
local ex = unicode.charWidth(ch)
if i < ptr then
aptr = aptr + ex
end
for j = 2, ex do
ch = ch .. " "
end
res = res .. ch
end
return res, aptr
end
-- Do not let wide characters cause weirdness outside home window!!!
local function wideCharSpillFilter(ch, x, doNotTouch)
if (x + 1) == doNotTouch then
if unicode.isWide(ch) then
return "%"
end
end
return ch
end
local function getChar(x, y)
-- Note: The colours used here tend to
-- game the autoselect so this works
-- without any depth nastiness.
for i = 1, #appZ do
local k = appZ[(#appZ + 1) - i]
local title = unicode.safeTextFormat(k)
local v = apps[k]
if x >= v.x and x < (v.x + v.w) then
if y == v.y then
local bgc = 0x80FFFF
local bch = "-"
if i == 1 then bgc = 0x808080 bch = "+" end
local ch = sanech(unicode.sub(title, (x - v.x) + 1, (x - v.x) + 1), bch)
return 0x000000, bgc, wideCharSpillFilter(ch, x, v.x + v.w)
else
if y > v.y and y < (v.y + v.h + frameH) then
-- get char from app
local ch = sanech(handleEv(k, "get_ch", (x - v.x) + 1, (y - (v.y + frameH)) + 1))
return 0xFFFFFF, 0x000000, wideCharSpillFilter(ch, x, v.x + v.w)
end
end
end
end
return 0xFFFFFF, 0x000000, " "
end
local function failDrawing()
if drawing then error("Cannot call when drawing.") end
end
local function redrawSection(x, y, w, h)
drawing = true
primaryGPU.setBackground(0x000000)
primaryGPU.setForeground(0xFFFFFF)
local cfg, cbg = 0xFFFFFF, 0x000000
--primaryGPU.fill(x, y, w, h, " ")
for ly = 1, h do
local buf = ""
local bufX = x
-- Wide characters are annoying.
local wideCharacterAdvance = 0
for lx = 1, w do
if wideCharacterAdvance == 0 then
local fg, bg, tx = getChar(x + (lx - 1), y + (ly - 1))
local flush = false
if fg ~= cfg then flush = true end
if bg ~= cbg then flush = true end
if flush then
if buf:len() > 0 then
primaryGPU.set(bufX, y + (ly - 1), buf)
end
buf = ""
bufX = x + (lx - 1)
end
if fg ~= cfg then primaryGPU.setForeground(fg) cfg = fg end
if bg ~= cbg then primaryGPU.setBackground(bg) cbg = bg end
buf = buf .. tx
wideCharacterAdvance = unicode.charWidth(tx) - 1
else
-- nothing to add to buffer, since the extra "letters" don't count
wideCharacterAdvance = wideCharacterAdvance - 1
end
end
primaryGPU.set(bufX, y + (ly - 1), buf)
end
drawing = false
end
local function redrawApp(aid, onlyBar)
local h = frameH
if not onlyBar then h = h + apps[aid].h end
redrawSection(apps[aid].x, apps[aid].y, apps[aid].w, h)
end
local function hideApp(aid, deferRedraw)
local function sk()
for ri, v in ipairs(appZ) do
if v == aid then table.remove(appZ, ri) return end
end
end
sk()
if not deferRedraw then
redrawApp(aid)
local newFocus = appZ[#appZ]
if newFocus then redrawApp(newFocus, true) end
end
return {apps[aid].x, apps[aid].y, apps[aid].w, apps[aid].h + frameH}
end
local function focusApp(aid, focusUndrawn)
hideApp(aid, true) -- just ensure it's not on stack, no need to redraw as it won't move
local lastFocus = appZ[#appZ]
table.insert(appZ, aid)
-- make the focus indicator disappear should one exist.
-- focusUndrawn indicates that the focus was transient and never got drawn
if lastFocus and (not focusUndrawn) then redrawApp(lastFocus, true) end
-- Finally, make absolutely sure the application is shown on the screen
redrawApp(aid)
end
local function moveApp(aid, x, y)
local section = hideApp(aid, true) -- remove from stack, do NOT redraw
apps[aid].x = x -- (prevents interim focus weirdness)
apps[aid].y = y
-- put back on stack, redrawing destination, but not the
-- interim focus target (since we made sure NOT to redraw that)
focusApp(aid, true)
redrawSection(table.unpack(section)) -- handle source cleanup
end
local function ofsApp(aid, x, y)
moveApp(aid, apps[aid].x + x, apps[aid].y + y)
end
local function killApp(aid)
hideApp(aid)
apps[aid] = nil
if fileWrapper then
fileWrapper.appDead(aid)
if fileWrapper.canFree() then
fileWrapper = nil
end
end
end
local getLCopy = nil
function getLCopy(t)
if type(t) == "table" then
local t2 = {}
setmetatable(t2, {__index = function(a, k) return getLCopy(t[k]) end})
return t2
else
return t
end
end
-- Used to ensure the "primary" device is safe
-- while allowing complete control otherwise.
local function omittingComponentL(o, t)
local i = component.list(t, true)
return function()
local ii = i()
if ii == o then ii = i() end
if not ii then return nil end
return component.proxy(ii)
end
end
-- Allows for simple "Control any of these connected to the system" APIs,
-- for things the OS shouldn't be poking it's nose in.
local function basicComponentSW(t, primary)
return {
list = function()
local i = component.list(t, true)
return function ()
local ii = i()
if not ii then return nil end
return component.proxy(ii)
end
end,
primary = primary
}
end
local function getAPI(s, cAid, cPkg, access)
if s == "math" then return getLCopy(math) end
if s == "table" then return getLCopy(table) end
if s == "string" then return getLCopy(string) end
if s == "unicode" then return getLCopy(unicode) end
if s == "root" then return _ENV end
if s == "stat" then return {
totalMemory = computer.totalMemory,
freeMemory = computer.freeMemory,
energy = computer.energy,
maxEnergy = computer.maxEnergy,
clock = os.clock,
date = os.date,
difftime = os.difftime,
time = os.time,
componentList = component.list
} end
if s == "proc" then return {
aid = cAid,
pkg = cPkg,
listApps = function ()
local t = {}
local t2 = {}
for k, v in pairs(apps) do
table.insert(t, k)
t2[k] = v.pkg
end
table.sort(t)
local t3 = {}
for k, v in ipairs(t) do
t3[k] = {v, t2[v]}
end
return t3
end,
sendRPC = function (aid, ...)
if type(aid) ~= "string" then error("aid must be string") end
if not apps[aid] then error("RPC target does not exist.") end
return handleEv(aid, "rpc", cPkg, cAid, ...)
end
} end
if s == "lang" then return {
getLanguage = function ()
return systemLanguage
end,
getTable = function ()
local ca, cb = loadfile("lang/" .. systemLanguage .. "/" .. cPkg .. ".lua", {})
if not ca then return nil, cb end
ca, cb = pcall(ca)
if not ca then return nil, cb end
return cb
end
} end
if s == "setlang" then return function (lang)
if type(lang) ~= "string" then error("Language must be string") end
systemLanguage = lang
pcall(function ()
local langFile = primaryDisk.open("language", "wb")
if langFile then
primaryDisk.write(langFile, systemLanguage)
primaryDisk.close(langFile)
end
end)
end end
if s == "kill" then return {
killApp = function (aid)
if type(aid) ~= "string" then error("aid must be string") end
if apps[aid] then killApp(aid) end
end
} end
if s == "randr" then return {
getResolution = primaryGPU.getResolution,
maxResolution = primaryGPU.maxResolution,
setResolution = function (w, h)
failDrawing()
if primaryGPU.setResolution(w, h) then
scrW = w
scrH = h
redrawWorldSoon = true
return true
end
return false
end,
iterateScreens = function()
-- List all screens, but do NOT give a primary screen.
return omittingComponentL(primaryScreen.address, "screen")
end,
iterateGPUs = function()
-- List all GPUs, but do NOT give a primary GPU.
return omittingComponentL(primaryGPU.address, "gpu")
end
} end
if s == "c.modem" then access["s.modem_message"] = true end
if s == "c.tunnel" then access["s.modem_message"] = true end
if s == "c.chat_box" then access["s.chat_message"] = true end
if s == "c.filesystem" then return basicComponentSW("filesystem", primaryDisk) end
if s == "c.screen" then return basicComponentSW("screen", primaryScreen) end
if s == "c.gpu" then return basicComponentSW("gpu", primaryGPU) end
if s:sub(1, 2) == "c." then return basicComponentSW(s:sub(3)) end
return nil
end
local function launchAppCore(aid, pkg, f)
if apps[aid] then return end
local function fD()
-- stops potentially nasty situations
if not apps[aid] then error("App already dead") end
end
local A = {
listApps = function (a) fD()
local appList = {}
for _, v in ipairs(primaryDisk.list("apps")) do
if v:sub(v:len() - 3) == ".lua" then
table.insert(appList, v:sub(1, v:len() - 4))
end
end
return appList
end,
launchApp = function (a) fD()
if type(a) ~= "string" then error("App IDs are strings") end
if a:gmatch("[a-zA-Z%-_\x80-\xFF]+")() ~= a then error("App '" .. a .. "' does not seem sane") end
failDrawing()
return launchApp(a)
end,
opencfg = function (openmode) fD()
if type(openmode) ~= "string" then
error("Openmode must be nil or string.")
end
local ok = false
if openmode == "r" then ok = true end
if openmode == "w" then ok = true end
if not ok then error("Bad openmode.") end
if not fileWrapper then
fileWrapper = loadfile("filewrap.lua", _ENV)()
end
return fileWrapper.open(aid, {primaryDisk.address, "cfgs/" .. pkg}, openmode)
end,
openfile = function (filetype, openmode) fD()
if openmode ~= nil then if type(openmode) ~= "string" then
error("Openmode must be nil or string.")
end end
if type(filetype) ~= "string" then error("Filetype must be string.") end
filetype = aid .. ": " .. filetype
failDrawing()
redrawWorldSoon = true
local rs, rt = pcall(function()
if fileWrapper then
if fileWrapper.canFree() then
fileWrapper = nil
end
end
local r = loadfile("tfilemgr.lua", _ENV)(filetype, openmode, primaryGPU)
if r and openmode then
if not fileWrapper then
fileWrapper = loadfile("filewrap.lua", _ENV)()
end
return fileWrapper.open(aid, r, openmode) -- 'r' is table {drive, dir}
end
end)
if not rs then
openDialog("*fmgr", "FMerr/" .. aid, rt)
else
return rt
end
end,
request = function (...) fD()
failDrawing()
local requests = {...}
-- If the same process requests a permission twice,
-- just let it.
-- "needed" is true if we still need permission.
-- first pass confirms we need permission,
-- second pass asks for it
local needed = false
for _, v in ipairs(requests) do
if type(v) == "string" then
if not apps[aid].hasAccess[v] then
needed = true
end
if apps[aid].denyAccess[v] then
return nil -- Don't even bother.
end
end
end
if needed then
local r, d = loadfile("policykit.lua", _ENV)(primaryGPU, pkg, requests)
if r then
needed = false
end
if not d then
redrawWorldSoon = true
end
end
local results = {}
for _, v in ipairs(requests) do
if type(v) == "string" then
if not needed then
table.insert(results, getAPI(v, aid, pkg, apps[aid].hasAccess))
apps[aid].hasAccess[v] = true
else
apps[aid].denyAccess[v] = true
end
end
end
return table.unpack(results)
end,
timer = function (ud) fD()
if type(ud) ~= "number" then error("Timer must take number.") end
failDrawing()
if ud > 0 then
apps[aid].nextUpdate = saneTime() + ud
else
apps[aid].nextUpdate = nil
end
end,
resize = function (w, h) fD()
if type(w) ~= "number" then error("Width must be number.") end
if type(h) ~= "number" then error("Height must be number.") end
w = math.floor(w)
h = math.floor(h)
if w < 1 then w = 1 end
if h < 1 then h = 1 end
failDrawing()
local dW = apps[aid].w
local dH = apps[aid].h
if dW < w then dW = w end
if dH < h then dH = h end
apps[aid].w = w
apps[aid].h = h
redrawSection(apps[aid].x, apps[aid].y, dW, dH + frameH)
end,
die = function () fD()
failDrawing()
killApp(aid)
end
}
apps[aid] = {}
apps[aid].pkg = pkg
local iDummy = {} -- Dummy object to keep app valid during init.
apps[aid].i = iDummy
apps[aid].A = A
apps[aid].nextUpdate = saneTime()
apps[aid].hasAccess = {}
apps[aid].denyAccess = {}
apps[aid].x = 1
apps[aid].y = 1
apps[aid].w = 1
apps[aid].h = 1
local i, w, h = f(A)
if apps[aid] then
-- If the app triggered an error handler,
-- then the instance could be replaced, make this act OK
if apps[aid].i == iDummy then
apps[aid].i = i
apps[aid].w = w
apps[aid].h = h
end
focusApp(aid)
return aid
end
-- App self-destructed
end
function launchApp(pkg)
local aid = 0
while apps[pkg .. "-" .. aid] do aid = aid + 1 end
aid = pkg .. "-" .. aid
return launchAppCore(aid, pkg, function (A)
local f, fe = loadfile("apps/" .. pkg .. ".lua", {
A = A,
assert = assert, ipairs = ipairs,
load = load, next = next,
pairs = pairs, pcall = pcall,
xpcall = xpcall, rawequal = rawequal,
rawget = rawget, rawlen = rawlen,
rawset = rawset, select = select,
type = type, error = error,
tonumber = tonumber, tostring = tostring
})
if not f then
return dialogApp(fe, A.die)
end
local ok, app, ww, wh = pcall(f)
if ok and ww and wh then
ww = math.floor(ww)
wh = math.floor(wh)
if ww < 1 then ww = 1 end
if wh < 1 then wh = 1 end
return app, ww, wh
end
if ok and not ww then app = "No Size" end
if ok and not wh then app = "No Size" end
return dialogApp(app, A.die)
end)
end
-- emergency dialog app
function dialogApp(fe, die)
fe = tostring(fe)
local ww, wh = 32, 1
wh = math.floor(unicode.len(fe) / ww) + 1
return {
key = function (ka, kc, down) if ka == 13 and down then die() end end,
update = function() end, get_ch = function (x, y)
local p = x + ((y - 1) * ww)
return unicode.sub(fe, p, p)
end
}, ww, wh
end
function openDialog(pkg, aid, txt)
launchAppCore(pkg, aid, function (A) return dialogApp(txt, A.die) end)
end
-- Perhaps outsource this to a file???
openDialog("Welcome to KittenOS", "~welcome",
--2345678901234567890123456789012
"Alt-(arrow key): Move window. " ..
"Alt-Enter: Start 'launcher'. " ..
"Tab: Switch window. " ..
"Shift-C will generally stop apps" ..
" which don't care about text. " ..
"On a Tier 1 RAM, use Lua 5.3 -- " ..
" you can do this by shift-using " ..
" your computer's CPU. ")
-- main WM
local isAltDown = false
local function key(ka, kc, down)
local focus = appZ[#appZ]
if kc == 56 then isAltDown = down end
if isAltDown then
if kc == 200 then
if focus and down then ofsApp(focus, 0, -1) end return
end
if kc == 208 then
if focus and down then ofsApp(focus, 0, 1) end return
end
if kc == 203 then
if focus and down then ofsApp(focus, -1, 0) end return
end
if kc == 205 then
if focus and down then ofsApp(focus, 1, 0) end return
end
if kc == 46 then
if focus and down then killApp(focus) end return
end
if ka == 13 then
if down then launchApp("launcher") end return
end
end
if kc == 15 then
if focus and down then focusApp(appZ[1]) end
return
end
if focus then
handleEvNRD(focus, "key", ka, kc, down)
end
end
while true do
local maxTime = 480
local now = saneTime()
for k, v in pairs(apps) do
if v.nextUpdate then
local timeIn = v.nextUpdate - now
if timeIn <= 0 then
v.nextUpdate = nil
handleEvNRD(k, "update")
if v.nextUpdate then
timeIn = v.nextUpdate - now
end
end
if timeIn > 0 then
maxTime = math.min(maxTime, timeIn)
end
end
end
for k, v in pairs(needRedraw) do if v then
if apps[k] and not redrawWorldSoon then
redrawApp(k)
end
needRedraw[k] = nil
end end
if redrawWorldSoon then
redrawWorldSoon = false
redrawSection(1, 1, scrW, scrH)
end
local signal = {computer.pullSignal(maxTime)}
local t, p1, p2, p3, p4 = table.unpack(signal)
if t then
for k, v in pairs(apps) do
if v.hasAccess["root"] or v.hasAccess["s." .. t] then
handleEvNRD(k, "event", table.unpack(signal))
end
end
if t == "key_down" then
key(p2, p3, true)
end
if t == "key_up" then
key(p2, p3, false)
end
if t == "clipboard" then
local focus = appZ[#appZ]
if focus then
handleEvNRD(focus, "clipboard", p2)
end
end
end
end