From 2ae3f9a93ad44cfbaba7d9f552b29a36696d2af1 Mon Sep 17 00:00:00 2001 From: gamemanj Date: Sun, 23 Oct 2016 13:22:12 +0100 Subject: [PATCH] Initial commit Currently eeprog pirate-speak language is unsupported, but... oh well. --- API Documentation | 192 +++++++++++ Request Documentation | 149 +++++++++ apps/batmon.lua | 69 ++++ apps/eeprog.lua | 78 +++++ apps/filemgr.lua | 4 + apps/installer.lua | 267 +++++++++++++++ apps/keycodes.lua | 30 ++ apps/launcher.lua | 38 +++ apps/lineclip.lua | 39 +++ apps/memusage.lua | 19 ++ apps/modeset.lua | 72 ++++ apps/textedit.lua | 352 +++++++++++++++++++ filewrap.lua | 70 ++++ init.lua | 687 ++++++++++++++++++++++++++++++++++++++ lang/pirate/installer.lua | 16 + lang/pirate/textedit.lua | 14 + license | 1 + policykit.lua | 80 +++++ readme.md | 127 +++++++ tfilemgr.lua | 234 +++++++++++++ 20 files changed, 2538 insertions(+) create mode 100644 API Documentation create mode 100644 Request Documentation create mode 100644 apps/batmon.lua create mode 100644 apps/eeprog.lua create mode 100644 apps/filemgr.lua create mode 100644 apps/installer.lua create mode 100644 apps/keycodes.lua create mode 100644 apps/launcher.lua create mode 100644 apps/lineclip.lua create mode 100644 apps/memusage.lua create mode 100644 apps/modeset.lua create mode 100644 apps/textedit.lua create mode 100644 filewrap.lua create mode 100644 init.lua create mode 100644 lang/pirate/installer.lua create mode 100644 lang/pirate/textedit.lua create mode 100644 license create mode 100644 policykit.lua create mode 100644 readme.md create mode 100644 tfilemgr.lua diff --git a/API Documentation b/API Documentation new file mode 100644 index 0000000..1764b38 --- /dev/null +++ b/API Documentation @@ -0,0 +1,192 @@ +KittenOS App Construction + +Firstly, an 'app' is, + in essence, a process, + with one window. +The app's initial Lua + script must return 3 + values: app, w, h. +App contains callbacks, + w and h are the app's + initial window size. +In the below section, + "dbool" means a boolean + which, if true, redraws + the whole window. + +Callbacks are: + update():dbool + key(ascii, scan, down): + dbool + clipboard(text):dbool + get_ch(x, y):character + event(t, ...):dbool + ^ The above is only used + under any one of these + conditions: + 1. The event is a + modem_message, and + the app requested + and got one of the + following APIs: + c.modem, c.tunnel + (implemented as a + special case in + the getAPI for + those APIs, + giving access to + s.modem_message) + 2. The app managed to + get the API named + "s.". + 3. The app managed to + successfully get + the root API + rpc(cPkg, cAid, ...):any + The application named + by cPkg, process cAid, + called your app via + the "proc" API. + The return value is + passed on, to the + calling application. + +-KittenOS API Reference-- + +KittenOS initializes apps + with global functions, + but only one library + table: "A". + +This table contains all + basic KittenOS funcs, + in the order they are + written in the kernel. + +A.listApps() + Returns a list of apps + that can be launched. + (Not running processes, + see Request Doc/"proc") + Used to allow launcher + to avoid requiring a + special permission for + almost no reason. + +A.launchApp(pkg) + Launches an application, + from the Lua file: + "apps/"..pkg..".lua" on + the primary filesystem. + The package name must + match the Lua pattern: + "[a-zA-Z%-_\x80-\xFF]+" + The app ID is returned. + +A.opencfg(mode) + Opens the file: + "cfgs/"..pkg on the + primary filesystem. + This allows storage of + any and all data an app + may store without any + user involvement. + This is not to be used + for documents, except + as part of a recovery + system. + +A.openfile(type, mode) + Asks the user to pick a + file, and opens it with + a specific file mode. + This invokes the script + "tfilemgr.lua". + File modes are 'r and 'w + and both are binary. + Should the mode be nil, + the file manager is + opened without any way + to open a file. + This is useful for + file management, and is + the implementation of + the "filemgr" app. + +A.request(...) + Allows you to get APIs. + For a list, look in the + kernel for "getAPI". + Some APIs may require + permission, and the + request may cause added + callbacks to be called, + e.g. the "net" API will + cause "modem_message" + calls on those signals. + +A.timer(t) + Sets the "update" + callback to be called + in a given number of + seconds. Note that only + one timer can be going + at a given time, and + passing <= 0 to timer + will disable it. + +A.resize(w, h) + Resize the running app, + among other things, + forcing a complete + redraw of the app. + +A.die() + Kill the running app, + immediately. + Do not call functions + after this is called. + +-KittenOS File API------- +This is the interface + exposed by filewrap.lua, + when loaded. +It is not a very good + interface, but it works. + +file.read(bytes) + Read some amount of + bytes from the file. + Returns nil on EOF. + +file.write(str) + Write some amount of + bytes to the file. + The bytes are in str, + as per the old standard + of byte arrays being + strings. + +file.close() + Closes the file. + +-KittenOS Security Notes- +KittenOS has a few parts + which add security, but + all of them follow one + principle: Kernel-local + objects are private, and + passing an environment + to the "load" function + will ensure this. +Assuming this axiom is + true, KittenOS allows + access to anything only + if the user explicitly + allows it. +Files are handled on a + per-file basis, unless + the "fs" permission is + granted (which allows + complete access to the + filesystem). \ No newline at end of file diff --git a/Request Documentation b/Request Documentation new file mode 100644 index 0000000..bbbb368 --- /dev/null +++ b/Request Documentation @@ -0,0 +1,149 @@ +Read API Documentation, + before reading this. + +Note that access to any + component can be gotten + by prefixing the type + of component with "c.", + but unless interfacing + with an external device, + use "stat" and "randr". +("randr" covers your + multi-screen needs.) +In a truly extreme case, + like an installer, you + can use "c.filesystem". +Signals from the devices + are not relayed, as I + am unsure of a way to + verify the source. +(It seems to be a default + to have the device as + the first parameter, but + I don't want to rely on + this behavior.) + +There are exceptions: +A successful "c.modem" or + "c.tunnel" request will + enable the modem_message + event to make it + somewhat useful. +A similar thing occurs + for "c.chat_box", + and "root" allows seeing + all events. + +Another way to get at + signals is the +"s." permission. +Like the "c." + permission, it's a tiny + general means of getting + events that the kernel + has no way (or reason) + to access. + +The interface has a + function to return an + iterator of proxies, + called "list". +The interface may have a + "primary" field if the + system has a 'primary' + choice for that device. + +------------------------- + KittenOS A.request() + Acceptable Values: + +"math", "table", "string" +, "unicode": Standard, + if indirect, access to + those Lua APIs. +(Done via metatable to + preserve memory.) + Note that the "unicode" + API has an additional + function: + safeTextFormat(txt,ptr) + adds spaces after wide + characters, making it + easier to display text + with wide characters + in. "ptr" can be a + point in the string, + and is also adjusted - + this can be used for + e.g. console cursors. + Returns atxt, aptr. + +"root": The kernel _ENV. + Essentially everything. + +"stat": Contains: + (names preserved, but + namespaces aren't) + computer: + totalMemory freeMemory + energy maxEnergy + os: + clock date difftime + time + Also contains + "component.list", but + named "componentList". + +"proc": Contains: + aid: A field containing + the running app ID. + Usually pkg .. "-" .. n + where 'n' is a number. + pkg: A field containing + the app name (aka. pkg) + listApps() + Returns a table of + {package, aid} entries. + + sendRPC(aid, ...) + Causes the "rpc" event + to be called, with the + given data parameters. + +"lang": + getLanguage(): Gets the + current system language + getTable(): Loads and + calls the Lua file at + "/lang//.lua" + Failing that, returns + nil + +"setlang": Actually one + function, setting the + system language. + +"kill": Contains: + killApp(aid) + Kills a process. + +"randr": Contains: + getResolution, + maxResolution, + setResolution, + (the above work + as on a GPU) + iterateScreens, + iterateGPUs + (the above iterate over + *non-primary* proxies, + direct control of the + primary screen is not + allowed) +"c.modem": See the notes + above on the "c." setup, + but note that this adds + the "modem_message" + callback. +"c.": + See the notes above. \ No newline at end of file diff --git a/apps/batmon.lua b/apps/batmon.lua new file mode 100644 index 0000000..2398373 --- /dev/null +++ b/apps/batmon.lua @@ -0,0 +1,69 @@ +local math, stat = A.request("math", "stat") +local app = {} +-- How much did energy change +-- over 1 second? +local lastChange = 0 +local lastValue = nil +local lastTimer = nil +local usage = { + "[####]:", + "[###:]:", + "[### ]:", + "[##: ]:", + "[## ]:", + "[#: ]:", + "[# ]:", + "[: ]:", + "[ ]:", + "WARNING" +} +local function getText(y) + if y == 2 then + if not lastChange then + return "Wait..." + end + local ind = "Dc. " + local wc = lastChange + local wv = stat.energy() + if wc > 0 then + wc = -wc + wv = stat.maxEnergy() - wv + ind = "Ch. " + end + local m = math.floor((wv / -wc) / 60) + return ind .. m .. "m" + end + local dec = stat.energy() / stat.maxEnergy() + -- dec is from 0 to 1. + local potential = math.floor(dec * #usage) + if potential < 0 then potential = 1 end + if potential >= #usage then potential = #usage - 1 end + return usage[#usage - potential] +end +function app.key(ka, kc, down) + if down then + if ka == ("C"):byte() then + A.die() + return false + end + end +end +function app.update() + local nv = stat.energy() + if lastValue then + lastChange = (nv - lastValue) / lastTimer + end + lastValue = nv + lastTimer = 10 + if lastChange then + if lastChange > 10 then + lastTimer = 1 + end + end + A.timer(lastTimer) + return true +end +function app.get_ch(x, y) + return getText(y):sub(x, x) +end +return app, 7, 2 \ No newline at end of file diff --git a/apps/eeprog.lua b/apps/eeprog.lua new file mode 100644 index 0000000..c3ff22c --- /dev/null +++ b/apps/eeprog.lua @@ -0,0 +1,78 @@ +local lang, unicode = A.request("lang", "unicode") +local eeprom = A.request("c.eeprom") +if eeprom then + eeprom = eeprom.list()() +end + +local langTable = lang.getTable() +local function G(text) + if langTable then + if langTable[text] then + return langTable[text] + end + end + return text +end + +local postFlash = false +local label = "" +local app = {} +function app.key(ka, kc, down) + if down then + if postFlash then + if ka ~= 0 then + if ka == 8 then + label = unicode.sub(label, 1, unicode.len(label) - 1) + return true + end + if ka == 13 then + eeprom.setLabel(label) + postFlash = false + return true + end + label = label .. unicode.char(ka) + return true + end + return false + end + if ka == ("r"):byte() then + local f = A.openfile(G("EEPROM Dump"), "w") + if f then + f.write(eeprom.get()) + f.close() + end + end + if ka == ("w"):byte() then + local f = A.openfile(G("EEPROM to flash"), "r") + if f then + local txt = f.read(128) + local ch = "" + while txt do + ch = ch .. txt + txt = f.read(128) + end + eeprom.set(ch) + postFlash = true + label = "" + return true + end + end + if ka == ("C"):byte() then + A.die() + return false + end + end +end +-- this string must be the longest, kind of bad but oh well +-- at least it's not a forced 29 chars... +local baseString = unicode.safeTextFormat(G("EEPROMFlash! (R)ead, (W)rite?")) +function app.get_ch(x, y) + if postFlash then + return unicode.sub(unicode.safeTextFormat(G("Label: ") .. label), x, x) + end + if not eeprom then + return unicode.sub(unicode.safeTextFormat(G("No EEPROM installed?")), x, x) + end + return unicode.sub(baseString, x, x) +end +return app, unicode.len(baseString), 1 \ No newline at end of file diff --git a/apps/filemgr.lua b/apps/filemgr.lua new file mode 100644 index 0000000..5d65385 --- /dev/null +++ b/apps/filemgr.lua @@ -0,0 +1,4 @@ +-- Launch the File Manager. +-- What more could be said? +A.openfile("any file", nil) +return {update = A.die}, 0, 0 \ No newline at end of file diff --git a/apps/installer.lua b/apps/installer.lua new file mode 100644 index 0000000..d663a1a --- /dev/null +++ b/apps/installer.lua @@ -0,0 +1,267 @@ +local lang, setlang, math, table, unicode, fs = A.request("lang", "setlang", "math", "table", "unicode", "c.filesystem") + +local options = {} +local optionCallback = function (index) end +local cursor = 1 +local inited = false + +local languages = {} +local languagesMenu = {} +local languageNames = { + ["en"] = "English", + ["de"] = "German", + ["ru"] = "Russian", + ["jbo"] = "Lojban", + ["ja"] = "Japanese", + ["kw"] = "Cornish", + ["nl"] = "Dutch", + ["pl"] = "Polish", + ["pt"] = "Portugese", + ["zh"] = "Chinese", + ["it"] = "Italian", + ["ga"] = "Irish", + ["fr"] = "French", + ["es"] = "Spanish", + ["pirate"] = "I be speakin' Pirate!" +} +for k, v in pairs(languageNames) do + if fs.primary.exists("lang/" .. k .. "/installer.lua") or (k == "en") then + table.insert(languages, k) + table.insert(languagesMenu, v) + end +end + +local langTable = nil +local function G(text) + if langTable then + if langTable[text] then + return langTable[text] + end + end + return text +end + +-- Config +local appDeny = {} +local installFS = nil +local installLang = nil + +-- Stages +local startLanguageSel = nil +local startFSSel = nil +local startAppSel = nil +local startFSConfirm = nil +local startInstall = nil + +-- Stuff for actual install +local runningInstall = nil +local runningInstallPoint = 0 + +local function setOptions(ol, callback) + options = ol + optionCallback = callback + cursor = 1 + local maxlen = 1 + for k, v in ipairs(options) do + options[k] = unicode.safeTextFormat(v) + local l = unicode.len(v) + if l > maxlen then maxlen = l end + end + A.resize(maxlen + 1, #options) +end + +local app = {} +function startLanguageSel() + setOptions(languagesMenu, function (i) + setlang(languages[i]) + langTable = lang.getTable() + startAppSel() + end) +end +function startAppSel() + local al = A.listApps() + table.sort(al) + local tbl = {} + table.insert(tbl, G("KittenOS Installer")) + table.insert(tbl, G("Applications to install:")) + for _, v in ipairs(al) do + table.insert(tbl, G("Install Application: ") .. v .. " [" .. G(tostring(not appDeny[v])) .. "]") + end + table.insert(tbl, G("")) + setOptions(tbl, function (i) + if i >= 3 and i < #tbl then + appDeny[al[i - 2]] = not appDeny[al[i - 2]] + startAppSel() + return + end + if i == #tbl then + startFSSel() + end + end) +end +function startFSSel() + local fsl = {} + for fsp in fs.list() do + if fsp ~= fs.primary then + table.insert(fsl, fsp.address) + end + end + local tbl = {} + table.insert(tbl, G("KittenOS Installer")) + table.insert(tbl, G("Filesystem to target:")) + for _, v in ipairs(fsl) do + table.insert(tbl, "<" .. v .. ">") + end + setOptions(tbl, function (i) + if i > 2 then + for fsp in fs.list() do + if fsp.address == fsl[i - 2] then + installFS = fsp + startFSConfirm() + return + end + end + startFSSel() + end + end) +end +function startFSConfirm() + local tbl = {} + table.insert(tbl, G("KittenOS Installer")) + table.insert(tbl, G("Are you sure you want to install to FS:")) + table.insert(tbl, installFS.address) + table.insert(tbl, G("These applications will be installed:")) + local other = nil + for _, v in ipairs(A.listApps()) do + if not appDeny[v] then + if other then + table.insert(tbl, other .. ", " .. v) + other = nil + else + other = v + end + end + end + if other then + table.insert(tbl, other) + end + table.insert(tbl, G("")) + table.insert(tbl, G("")) + setOptions(tbl, function (i) + if i == (#tbl - 1) then + -- first, create directories. + local function forceMakeDirectory(s) + if installFS.exists(s) then + if not installFS.isDirectory(s) then + installFS.remove(s) + end + end + installFS.makeDirectory(s) + end + installLang = lang.getLanguage() + forceMakeDirectory("apps") + forceMakeDirectory("cfgs") + forceMakeDirectory("lang") + forceMakeDirectory("lang/" .. installLang) + runningInstall = { + -- in order of importance + "init.lua", + "policykit.lua", + "tfilemgr.lua", + "filewrap.lua", + "language" + } + for _, v in ipairs(A.listApps()) do + if not appDeny[v] then + table.insert(runningInstall, "apps/" .. v .. ".lua") + if fs.primary.exists("lang/" .. installLang .. "/" .. v .. ".lua") then + table.insert(runningInstall, "lang/" .. installLang .. "/" .. v .. ".lua") + end + end + end + runningInstallPoint = 1 + startInstall() + end + if i == #tbl then + startAppSel() + end + end) +end +function startInstall() + local percent = math.floor((runningInstallPoint / #runningInstall) * 100) + local tbl = { + G("Installing.") .. " " .. percent .. "%" + } + setOptions(tbl, function (i) end) + A.timer(1) +end +function startComplete() + setOptions({G("Installation complete."), G("Press Shift-C to leave.")}, function (i) end) +end +function app.update() + if runningInstall then + if runningInstall[runningInstallPoint] then + local txt = runningInstall[runningInstallPoint] + -- perform copy + local h2 = installFS.open(txt, "wb") + if txt == "language" then + if installLang then + installFS.write(h2, installLang) + else + installFS.write(h2, "en") + end + else + local h = fs.primary.open(txt, "rb") + local chk = fs.primary.read(h, 1024) + while chk do + installFS.write(h2, chk) + chk = fs.primary.read(h, 1024) + end + fs.primary.close(h) + end + installFS.close(h2) + startInstall() + runningInstallPoint = runningInstallPoint + 1 + else + runningInstall = nil + startComplete() + end + return true + end + -- should only be called once, but just in case + if not inited then + startLanguageSel() + inited = true + end + return true +end +function app.get_ch(x, y) + if x == 1 then + if y == cursor then return ">" else return " " end + end + local s = options[y] + if not s then s = "FIXME" end + return unicode.sub(s, x - 1, x - 1) +end +function app.key(ka, kc, down) + if down then + if kc == 200 then + cursor = cursor - 1 + if cursor < 1 then cursor = 1 end + return true + end + if kc == 208 then + cursor = cursor + 1 + if cursor > #options then cursor = #options end + return true + end + if ka == 13 then + optionCallback(cursor) + return true + end + if ka == ("C"):byte() then + A.die() + end + end +end +return app, 1, 1 diff --git a/apps/keycodes.lua b/apps/keycodes.lua new file mode 100644 index 0000000..31b5297 --- /dev/null +++ b/apps/keycodes.lua @@ -0,0 +1,30 @@ +local math, table = A.request("math", "table") +local app = {} +local strs = {"", "Shift-C to quit."} +local keys = {} +local function rebuildKeys() + local keylist = {} + for k, v in pairs(keys) do + if v then + table.insert(keylist, k) + end + end + table.sort(keylist) + strs[1] = "" + for _, v in ipairs(keylist) do + strs[1] = strs[1] .. v .. " " + end +end +app.key = function(ka, kc, down) + if ka == ("C"):byte() and down then + A.die() + return false + end + keys[kc] = down + rebuildKeys() + return true +end +app.get_ch = function (x, y) + return (strs[y]):sub(x, x) +end +return app, 20, 2 \ No newline at end of file diff --git a/apps/launcher.lua b/apps/launcher.lua new file mode 100644 index 0000000..510df74 --- /dev/null +++ b/apps/launcher.lua @@ -0,0 +1,38 @@ +-- Application launcher +local table, unicode = A.request("table", "unicode") +local apps = A.listApps() +local maxlen = 1 +for _, v in ipairs(apps) do + if unicode.len(v) > maxlen then maxlen = unicode.len(v) end +end +local app = {} +local cursor = 1 +function app.get_ch(x, y) + if x == 1 then + if y == cursor then return ">" else return " " end + end + local s = apps[y] + if not s then s = "FIXME" end + return unicode.sub(unicode.safeTextFormat(s), x - 1, x - 1) +end +function app.key(ka, kc, down) + if down then + if kc == 200 then + cursor = cursor - 1 + if cursor < 1 then cursor = 1 end + return true + end + if kc == 208 then + cursor = cursor + 1 + if cursor > #apps then cursor = #apps end + return true + end + if ka == 13 then + A.launchApp(apps[cursor]) + end + if ka == ("C"):byte() then + A.die() + end + end +end +return app, maxlen + 1, #apps \ No newline at end of file diff --git a/apps/lineclip.lua b/apps/lineclip.lua new file mode 100644 index 0000000..308a8e4 --- /dev/null +++ b/apps/lineclip.lua @@ -0,0 +1,39 @@ +local unicode, proc = A.request("unicode", "proc") +for _, v in ipairs(proc.listApps()) do + if v[2] == "lineclip" then + if v[1] ~= proc.aid then + A.die() + return {}, 1, 1 + end + end +end +local app = {} +local board = "" +function app.rpc(srcP, srcD, cmd, txt) + if type(cmd) ~= "string" then + return "" + end + if cmd == "copy" then + if type(txt) ~= "string" then + error("RPC->lineclip: bad text") + end + board = txt + A.resize(unicode.len(board), 1) + end + if cmd == "paste" then + return board + end +end +function app.get_ch(x, y) + return unicode.sub(unicode.safeTextFormat(board), x, x) +end +function app.key(ka, kc, down) + if down and ka == ("C"):byte() then + A.die() + end + return false +end +function app.update() + return true +end +return app, 8, 1 \ No newline at end of file diff --git a/apps/memusage.lua b/apps/memusage.lua new file mode 100644 index 0000000..04bae7c --- /dev/null +++ b/apps/memusage.lua @@ -0,0 +1,19 @@ +local math, stat = A.request("math", "stat") +local app = {} +app.key = function(ka, kc, down) + if ka == ("C"):byte() and down then + A.die() + end +end +local strs = {"", "Shift-C to quit."} +app.update = function () + local tm = stat.totalMemory() + local um = math.floor((tm - stat.freeMemory()) / 1024) + strs[1] = um .. ":" .. math.floor(tm / 1024) + A.timer(1) + return true +end +app.get_ch = function (x, y) + return (strs[y]):sub(x, x) +end +return app, 16, 2 \ No newline at end of file diff --git a/apps/modeset.lua b/apps/modeset.lua new file mode 100644 index 0000000..0586441 --- /dev/null +++ b/apps/modeset.lua @@ -0,0 +1,72 @@ +-- resset: resolution changer +-- Typed from within. +local math, randr = A.request("math", "randr") +local app = {} +local mW, mH = randr.maxResolution() +-- important on 5.3, and 5.3 prevents +-- the nasty memory self-destructs! +mW = math.floor(mW) mH = math.floor(mH) +local sW, sH = randr.getResolution() +sW = math.floor(sW) sH = math.floor(sH) +local function mkstr(title, w, h) + return title .. ":" .. math.floor(w) .. "x" .. math.floor(h) +end +function app.get_ch(x, y) + local strs = { + mkstr("cur", randr.getResolution()), + mkstr("new", sW, sH) + } + return strs[y]:sub(x, x) +end +local function modres(w, h) + sW = sW + w + sH = sH + h + if sW > mW then sW = mW end + if sH > mH then sH = mH end + if sW < 1 then sW = 1 end + if sH < 1 then sH = 1 end + return true +end +function app.key(ka, kc, down) + if down then + if kc == 200 then + return modres(0, -1) + end + if kc == 208 then + return modres(0, 1) + end + if kc == 203 then + return modres(-1, 0) + end + if kc == 205 then + return modres(1, 0) + end + if ka == 13 then + if randr.setResolution(sW, sH) then + pcall(function() + local f = A.opencfg("w") + f.write(sW .. " " .. sH) + f.close() + end) + end + return true + end + if ka == ("C"):byte() then + A.die() + end + end + return false +end +-- Config stuff! +pcall(function() + local f = A.opencfg("r") + if f then + local txt = f.read(64) + local nt = txt:gmatch("[0-9]+") + sW = math.floor(tonumber(nt())) + sH = math.floor(tonumber(nt())) + modres(0, 0) + f.close() + end +end) +return app, 12, 2 \ No newline at end of file diff --git a/apps/textedit.lua b/apps/textedit.lua new file mode 100644 index 0000000..44b8462 --- /dev/null +++ b/apps/textedit.lua @@ -0,0 +1,352 @@ +-- 'Femto': Text Editor +-- Formatting proc. for +-- this file is the def. +-- size of a textedit win +local lang, + table, + unicode, + math, + proc = + A.request("lang", + "table", + "unicode", + "math", + "proc") +local lines = { + "Femto: Text Editor", + "^W : Close, ^S : Save", + "^A : Load , ^Q : New.", + "^C : Copy Line,", + "^V : Paste Line", + "^: Resize Win", + "'^' is Control.", + "Now with wide text!", + "Yay!" +} +local linesTranslated = + lang.getTable() +if linesTranslated then + lines = linesTranslated +end + +local cursorX = 1 +local cursorY = 1 +local cFlash = true +local ctrlFlag = false +local sW, sH = 25, 8 + +local app = {} + +local function splitCur() + local s = lines[cursorY] + local st = unicode.sub + (s, 1, cursorX - 1) + local en = unicode.sub + (s, cursorX) + return st, en +end + +local function + clampCursorX() + local s = lines[cursorY] + if unicode.len(s) < + (cursorX - 1) then + cursorX = + unicode.len(s) + 1 + return true + end + return false +end + +-- Save/Load +local function save() + ctrlFlag = false + local txt = + A.openfile("text", "w") + if txt then + for k, v in + ipairs(lines) do + if k ~= 1 then + txt.write("\n" .. v) + else + txt.write(v) + end + end + txt.close() + end +end +local function load() + ctrlFlag = false + local txt = + A.openfile("text", "r") + if txt then + lines = {} + local lb = "" + while true do + local l = txt.read(64) + if not l then + table.insert + (lines, lb) + cursorX = 1 + cursorY = 1 + txt.close() + return + end + local lp = + l:find("\n") + while lp do + lb = lb .. l:sub(1, + lp - 1) + table.insert + (lines, lb) + lb = "" + l = l:sub(lp + 1) + lp = l:find("\n") + end + lb = lb .. l + end + end +end + +function app.get_ch(x, y) + -- do rY first since unw + -- only requires that + -- horizontal stuff be + -- messed with... + -- ...thankfully + local rY = (y + cursorY) + - math.floor(sH / 2) + + -- rX is difficult! + local rX = 1 + local Xthold = + math.floor(sW / 2) + if cursorX > Xthold then + rX = rX + (cursorX - + Xthold) + end + local line = lines[rY] + if not line then return + "¬" end + local _, cursorXP = + unicode.safeTextFormat( + line, cursorX) + line, rX = + unicode.safeTextFormat( + line, rX) + + -- 1-based cambias stuff + rX = rX + (x - 1) + if rX == cursorXP then + if rY == cursorY then + if cFlash then + return "_" + end + end + end + return unicode.sub(line, + rX, rX) +end +-- communicate with the +-- "lineclip" clipboard, +-- for inter-window copy +function lineclip(c, m) + for _, v in + ipairs(proc.listApps()) + do + if v[2] == "lineclip" + then + return proc.sendRPC( + v[1], c, m) + end + end + local aid = A.launchApp( + "lineclip") + ctrlFlag = false + if aid then return + proc.sendRPC(aid, c, m) + end + return "" +end +-- add a single character +function putLetter(ch) + if ch == "\r" then + local a, b = splitCur() + lines[cursorY] = a + table.insert(lines, + cursorY + 1, b) + cursorY = cursorY + 1 + cursorX = 1 + return + end + local a, b = splitCur() + a = a .. ch + lines[cursorY] = a .. b + cursorX = + unicode.len(a) + 1 +end +function app.key(ka, kc, + down) + if kc == 29 then + ctrlFlag = down + return false + end + if ctrlFlag then + if not down then + return false end + if kc == 17 -- W + then A.die() end + if kc == 200 then + sH = sH - 1 + if sH == 0 then + sH = 1 end + A.resize(sW, sH) + end + if kc == 208 then + sH = sH + 1 + A.resize(sW, sH) + end + if kc == 203 then + sW = sW - 1 + if sW == 0 then + sW = 1 end + A.resize(sW, sH) + end + if kc == 205 then + sW = sW + 1 + A.resize(sW, sH) + end + if kc == 31 -- S + then return save() end + if kc == 30 -- A + then return load() end + if kc == 16 -- Q + then lines = {""} + cursorX = 1 + cursorY = 1 + return true end + if kc == 46 -- C + then lineclip("copy", + lines[cursorY]) end + if kc == 47 then -- V + table.insert(lines, + cursorY, + lineclip("paste")) + return true + end + return false + end + -- action keys + if not down then + return false + end + if kc == 200 + or kc == 201 then + local moveAmount = 1 + if kc == 201 then + moveAmount = + math.floor(sH / 2) + end + cursorY = cursorY - + moveAmount + if cursorY < 1 then + cursorY = 1 end + clampCursorX() + return true + end + if kc == 208 + or kc == 209 then + local moveAmount = 1 + if kc == 209 then + moveAmount = + math.floor(sH / 2) + end + cursorY = cursorY + + moveAmount + if cursorY> #lines then + cursorY = #lines end + clampCursorX() + return true + end + if kc == 203 then + if cursorX > 1 then + cursorX = cursorX - 1 + else + if cursorY > 1 then + cursorY = cursorY - 1 + cursorX = unicode.len + (lines[cursorY]) + 1 + else + return false + end + end + return true + end + if kc == 205 then + cursorX = cursorX + 1 + if clampCursorX() then + if cursorY < #lines + then + cursorY = cursorY + 1 + cursorX = 1 + end + end + return true + end + if kc == 199 then + cursorX = 1 + return true + end + if kc == 207 then + cursorX = unicode.len( + lines[cursorY]) + 1 + return true + end + if ka == 8 then + if cursorX == 1 then + if cursorY == 1 then + return false + end + local l = table.remove + (lines, cursorY) + cursorY = cursorY - 1 + cursorX = unicode.len( + lines[cursorY]) + 1 + lines[cursorY] = + lines[cursorY] .. l + else + local a, b =splitCur() + a = unicode.sub(a, 1, + unicode.len(a) - 1) + lines[cursorY] = a.. b + cursorX = cursorX - 1 + end + return true + end + if ka ~= 0 then + putLetter + (unicode.char(ka)) + return true + end + return false +end +function app.clipboard(t) + for i = 1, + unicode.len(t) do + local c = + unicode.sub(t, i, i) + if c ~= "\r" then + if c == "\n" then + c = "\r" + end + putLetter(c) + end + end + return true +end +function app.update() + cFlash = not cFlash + A.timer(0.5) + return true +end +return app, sW, sH \ No newline at end of file diff --git a/filewrap.lua b/filewrap.lua new file mode 100644 index 0000000..31e9571 --- /dev/null +++ b/filewrap.lua @@ -0,0 +1,70 @@ +-- File Wrapper +local fwrap = {} +local appTables = {} +-- NOTE: May not be error-sandboxed. +-- Be careful. +function fwrap.appDead(aid) + if appTables[aid] then + for k, v in ipairs(appTables[aid]) do + pcall(function() + local prox = component.proxy(v.device) + if prox then + prox.close(v.handle) + end + end) + end + appTables[aid] = nil + end +end +function fwrap.canFree() + for _, v in pairs(appTables) do + if v then + if #v > 0 then + return false + end + end + end + return true +end +-- Always error-sandboxed, let errors throw +function fwrap.open(aid, path, mode) + local finst = {} + finst.device = path[1] + finst.file = path[2] + finst.handle = component.invoke(finst.device, "open", finst.file, mode .. "b") + if not appTables[aid] then + appTables[aid] = {} + end + table.insert(appTables, finst) + local function closer() + pcall(function() + component.invoke(finst.device, "close", finst.handle) + end) + for k, v in ipairs(appTables[aid]) do + if v == finst then + table.remove(appTables[aid], k) + return + end + end + end + if mode == "r" then + return { + close = closer, + read = function (len) + if type(len) ~= "number" then error("Length of read must be number") end + return component.invoke(finst.device, "read", finst.handle, len) + end + } + end + if mode == "w" then + return { + close = closer, + write = function (txt) + if type(txt) ~= "string" then error("Write data must be string-bytearray") end + return component.invoke(finst.device, "write", finst.handle, txt) + end + } + end + error("Bad mode") +end +return fwrap \ No newline at end of file diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..0d9e3eb --- /dev/null +++ b/init.lua @@ -0,0 +1,687 @@ +-- 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'. " .. +"Shift-C will generally stop apps" .. +" which don't care about text, or" .. +" don't want any text right now. " .. +"Tab: Switch window.") + +-- 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(v, "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 diff --git a/lang/pirate/installer.lua b/lang/pirate/installer.lua new file mode 100644 index 0000000..92c5835 --- /dev/null +++ b/lang/pirate/installer.lua @@ -0,0 +1,16 @@ +return { + ["KittenOS Installer"] = "Kitt'n Ohess Install'a!", + ["Applications to install:"] = "Sails to fit:", + ["Install Application: "] = "Fit Sail: ", + ["true"] = "Yar", + ["false"] = "Nay", + [""] = "", + ["Filesystem to target:"] = "Island to land cargo:", + ["Are you sure you want to install to FS:"] = "Are 'ya sure you wanna land on:", + ["These applications will be installed:"] = "These sails will be fitted:", + [""] = "", + [""] = "", + ["Installing."] = "Sailin'!", + ["Installation complete."] = "Arrived at island!", + ["Press Shift-C to leave."] = "Push Shift-Cee to disembark!" +} \ No newline at end of file diff --git a/lang/pirate/textedit.lua b/lang/pirate/textedit.lua new file mode 100644 index 0000000..8ea1d76 --- /dev/null +++ b/lang/pirate/textedit.lua @@ -0,0 +1,14 @@ +return { + "Femto, a scribe for ye future!", + "The wheels are as follows:", + "Control and ye arrow keys", + " change the porthole size...", + "Control and C copies a line,", + "Control and V pastes a line.", + "Control and Q wipes the parchment,", + "Control and W sinks the ship.", + "Control and A takes a parchment from ye logs,", + "Control and S puts the parchment back.", + "Now supports wide scrawlin's.", + "For example, 'Yay!'.", +} diff --git a/license b/license new file mode 100644 index 0000000..e40d47c --- /dev/null +++ b/license @@ -0,0 +1 @@ +I, 20kdc, release this work into the public domain. diff --git a/policykit.lua b/policykit.lua new file mode 100644 index 0000000..63a5aa8 --- /dev/null +++ b/policykit.lua @@ -0,0 +1,80 @@ +local gpu, aid, requests = ... +if #requests == 1 then + local permits = { + -- This is a list of specific permits for specific known apps. + -- Do not put an app here lightly - find another way. + } +end +if aid == "launcher" then return true, true end +local restrictions = { +-- | | + ["root"] = "Completely, absolutely control the device.", + ["randr"] = "Control displays and GPUs.", -- not precisely true but close enough + ["stat"] = "Read energy, sage, memory usage and time.", + ["setlang"] = "Change the system language.", + ["kill"] = "Kill other processes.", + ["c.filesystem"] = "Access filesystems directly (virus risk!).", + ["c.drive"] = "Access unmanaged drives directly.", + ["c.modem"] = "Send to and receive from the network.", + ["c.tunnel"] = "Use Linked Cards, receive from all modems.", + ["s.modem_message"] = "Listen to all network messages.", + ["c.internet"] = "Connect to the real-life Internet.", + ["c.robot"] = "Control the 'robot' abilities.", + ["c.drone"] = "Control the 'drone' abilities.", + ["c.redstone"] = "Control Redstone Cards and I/O Blocks.", + ["c.screen"] = "Screw up screens directly. ", + ["c.gpu"] = "Screw up GPUs directly. ", + ["c.eeprom"] = "Modify EEPROMs. Extremely dangerous.", + ["c.debug"] = "Modify the game world. Beyond dangerous.", + ["c.printer3d"] = "Use connected 3D Printers.", +-- disk_drive seems safe enough, same with keyboard + ["s.key_down"] = "Potentially act as a keylogger. (down)", + ["s.key_up"] = "Potentially act as a keylogger. (up)", +-- COMPUTRONICS + ["c.chat_box"] = "Listen and talk to players.", + ["s.chat_message"] = "Listen in on players talking." +} +local centre = "" +local sW, sH = gpu.getResolution() +for i = 1, math.floor((sW / 2) - 7) do + centre = centre .. " " +end +local text = { + centre .. "Security Alert", + "", + " '" .. aid .. "' would like to:", + "", +} +local automaticOK = true +for _, v in ipairs(requests) do + if v ~= nil then + if type(v) == "string" then + if restrictions[v] then + automaticOK = false + table.insert(text, " + " .. restrictions[v]) + end + end + end +end +-- Nothing restricted. +if automaticOK then return true, true end +table.insert(text, "") +table.insert(text, " If you agree, press 'y', else 'n'.") +gpu.setForeground(0xFFFFFF) +gpu.setBackground(0x000000) +gpu.fill(1, 1, sW, sH, " ") +for k, v in ipairs(text) do + gpu.set(1, k, v) +end +text = nil +while true do + local t, p1, p2, p3, p4 = computer.pullSignal() + if t == "key_down" then + if p2 == ("y"):byte() then + return true, false + end + if p2 == ("n"):byte() then + return false, false + end + end +end \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..5dc4091 --- /dev/null +++ b/readme.md @@ -0,0 +1,127 @@ +# KittenOS: A graphical OpenComputers OS that runs light. + +## Why? +Because OpenOS needs *two* Tier 1 memory chips to run a text editor, + in a basic console environment that reminds me of DOS more than a Unix, + despite the inspirations. +This OS only needs one Tier 1 memory chip - that's 192KiB... + +## Couldn't save a file from a text editor on a 192KiB system. + +Switch to Lua 5.3 - Lua 5.2 has a nasty habit of leaking a big chunk of memory. + +Given this hasn't happened with Lua 5.3, I can only guess this is an issue with 5.2. + +(Tested on OpenComputers 1.6.0.7-rc.1 on Minecraft 1.7.10. + Judging by the "collectgarbage" call in machine.lua, + they may have the same problem???) + +## Why the complicated permissions and sandboxing? + +Because I wanted to try out some security ideas, like not letting + applications access files unless the user explicitly authorizes access + to that file. Aka "ransomware prevention". + +Yes, I know it uses memory, but I actually had *too much* memory. + +## Why is the kernel so big and yet in one file? + +File overhead is one of the things I suspect caused OpenOS's bloat. +(It helps that writing the kernel like this reduces boot time - + only one file has to be loaded for the system to boot. + More files are required to use some functions which are used relatively + rarely - like the file manager - as a tradeoff on memory.) + +## Why does get_ch exist? Why not use a window buffer? + +Because memory. +This way, 80x25 and 160x50 should use around about the same amount of memory. +It also fit nicely with the way only sections of the screen are drawn. + +## Why do the window titlebars look inverted on Tier 3 hardware, but not Tier 2? + +There was a bit of value trickery required to get the same general effect on all 3 tiers. + +## Why do I have to return true in so many functions if I want a redraw? + +Because if you redraw all the time, you use more energy. + +(Though energy's really a secondary priority to memory, costs resources + all the same.) + +## Why aren't (partial redraws/GPU copys/GPU copys for moving windows) supported? + +They didn't seem to be a requirement once I optimized the GPU call count. + +If the system had still been running slow after that, I'd have done it. + +## Why does the text editor use up so much (energy/time)? + +The text editor is probably one of the bigger windows in KittenOS. +Try making it smaller with the controls it gives. + +## What's "lineclip"? + +A poor excuse for a clipboard. +You don't need to interact with it, + though *Shift-C* on it kills it (thus clearing the clipboard). + +## Why is *Shift-C* used everywhere to safely close things? + +Because Enter is used as an action button, Escape's right out, + Delete's probably missing on keyboards by now, + *Ctrl-C* is also copy, and anything with Alt in it + is supposed to be a Window Manager trap. + +## Why is there no "kill it" button? + +I'm sticking it here so that people don't make using this a habit - + so that application developers can do something before app death. + +If you must ask, *Alt-C* will kill the currently focused app. + +## An app infinite-looped / ate memory and the system died. + +Yep. There isn't much of a way to protect against memory-eaters. +On the other hand, you do know not to start that app again, and it + didn't get a chance to do harm. + +## Isn't building a window manager into the OS kind of, uh, monolithic? + +Given the inaccuracies relative to real computers anyway, + I think of it as that OpenComputers itself is the kernel, + but it can only handle a single task, written in Lua, + so people build on top of it to make an interface. + +(And given the memory limitations, having a cooperatively multitasking + microkernel which then gives all it's capabilities to the window + manager would succeed only in complicating things and then using all + memory, in that precise order. + This way accomplishes the same thing, and it's simpler.) + +## How's multilingual support? + +Multilingual support exists, but the languages are Pirate and English. +Most applications are bare-bones enough that they don't have any strings + that need to be translated - I consider this a plus. + +The infrastructure for a system language exists, but is not really in use. +(This includes the installer copying language files for the selected language, + but the only thing with a language selector at the moment is the installer. + Language selection is performed by editing the "language" file + at the drive root and rebooting, or switching language during install.) + +The infrastructure is quite minimal so as not to bloat the system too badly - + it's up to the applications to decide how to implement multi-language support, + but the system can load files from lang//.lua to aid in this. + (Why loading files? To avoid having every single language loaded at once. + Why loading Lua files? To avoid making this feature bloat the system.) + +As for the issue of wide characters (Chinese/Japanese/Korean support): + +Wide characters are supported in all supplied apps that handle text, + including the text editor - + the helper API function unicode.safeTextFormat should make these things easier. + +(safeTextFormat is allowed to rearrange the text however it needs to for + display - this leaves the possibility of RTL layout open.) diff --git a/tfilemgr.lua b/tfilemgr.lua new file mode 100644 index 0000000..428bcc9 --- /dev/null +++ b/tfilemgr.lua @@ -0,0 +1,234 @@ +-- The File Manager (manager of files). +-- Args: +local filetype, openmode, gpu = ... +local fileManager = nil +function fileManager(filetype, openmode) + -- Like policykit, this is a trusted gateway. + -- Note that The File Manager just returns a path {fs, path}. + -- The File Wrapper is given that path, and the open mode. + local title = nil + -- Valid open modes are: + -- nil: The File Wrapper should not be invoked - + -- likely a "file manager launcher" application. + if openmode == nil then title = "File Manager" end + -- "r": Open the file for reading. Binary mode is assumed. + if openmode == "r" then + title = "Read " .. filetype + end + if openmode == "w" then + title = "Write " .. filetype + end + -- "w": Open the file for truncate-writing, again binary assumed. + if not title then error("Bad openmode") end + + local scrW, scrH = gpu.getResolution() + gpu.setBackground(0) + gpu.setForeground(0xFFFFFF) + local function cls() + gpu.fill(1, 1, scrW, scrH, " ") + end + local function menuKey(cursor, el, text, ka, kc, allowEntry) + if ka == 13 then + -- entry denied, so we hit here. + return cursor, text + end + if kc == 200 then + cursor = cursor - 1 + if cursor < 1 then cursor = el end + return cursor, text, false, true + end + if kc == 208 then + cursor = cursor + 1 + if cursor > el then cursor = 1 end + return cursor, text, false, true + end + if allowEntry then + if ka == 8 then + return cursor, unicode.sub(text, 1, unicode.len(text) - 1), true + end + if (ka ~= 0) and (ka ~= ("/"):byte()) and (ka ~= ("\\"):byte()) then + text = text .. unicode.char(ka) + return cursor, text, true + end + end + return cursor, text + end + local function menu(title, entries, allowEntry) + cls() + gpu.fill(1, 1, scrW, 1, "-") + gpu.set(1, 1, title) + local cursor = 1 + local escrH = scrH + local entryText = "" + local cursorBlinky = false + if allowEntry then escrH = scrH - 1 end + while true do + for y = 2, escrH do + local o = cursor + (y - 8) + local s = tostring(entries[o]) + if not entries[o] then s = "" end + if o == cursor then s = ">" .. s else s = " " .. s end + gpu.fill(1, y, scrW, 1, " ") + gpu.set(1, y, s) + end + cursorBlinky = not cursorBlinky + if allowEntry then + gpu.fill(1, scrH, scrW, 1, " ") + if cursorBlinky then + gpu.set(1, scrH, ":" .. entryText) + else + gpu.set(1, scrH, ":" .. entryText .. "_") + end + end + local t, p1, p2, p3, p4 = computer.pullSignal(1) + if t == "key_down" then + if p2 == 13 then + if allowEntry then + if entryText ~= "" then + return entryText + end + else + return entries[cursor] + end + end + cursor, entryText, search, lookup = menuKey(cursor, #entries, entryText, p2, p3, allowEntry) + if search then + for k, v in ipairs(entries) do + if v:sub(1, v:len()) == entryText then cursor = k end + end + end + if lookup then + entryText = entries[cursor] + end + end + end + end + + local currentDir = nil + local currentDrive = nil + local function listDir(dv, dr) + if dv == nil then + local l = {} + local t = {} + for c in component.list("filesystem") do + l[c] = {c, "/"} + table.insert(t, c) + end + return l, t, "Filesystems" + end + local names = component.invoke(dv, "list", dr) + local l = {} + for k, v in ipairs(names) do + if component.invoke(dv, "isDirectory", dr .. v) then + l[v] = {dv, dr .. v} + end + end + return l, names, dv .. ":" .. dr + end + local function isDir(dv, dr) + if dv == nil then return true end + return component.invoke(dv, "isDirectory", dr) + end + + local tagMkdir = "// Create Directory //" + local tagCancel = "// Cancel //" + local tagOpen = "// Open //" + local tagDelete = "// Delete //" + local tagRename = "// Rename //" + local tagCopy = "// Copy //" + local tagBack = ".." + + local function textEntry(title) + local txt = menu(title, {tagCancel}, true) + if txt ~= tagCancel then return txt end + return nil + end + local function report(title) + menu(title, {"OK"}, false) + end + + local history = {} + local function navigate(ndr, ndd) + table.insert(history, {currentDrive, currentDir}) + currentDrive, currentDir = ndr, ndd + end + while true do + local map, sl, name = listDir(currentDrive, currentDir) + if #history ~= 0 then + table.insert(sl, tagBack) + end + table.insert(sl, tagCancel) + if currentDrive then + table.insert(sl, tagMkdir) + end + local str = menu(title .. " " .. name, sl, (openmode == "w") and currentDrive) + if str == tagBack then + local r = table.remove(history, #history) + currentDrive, currentDir = table.unpack(r) + else + if str == tagCancel then return nil end + if str == tagMkdir then + local nam = textEntry("Create Directory...") + if nam then + component.invoke(currentDrive, "makeDirectory", currentDir .. nam) + end + else + if map[str] then + if map[str][1] and currentDrive then + local act = menu(name .. ":" .. str, {tagOpen, tagRename, tagDelete, tagCancel}) + if act == tagOpen then + navigate(table.unpack(map[str])) + end + if act == tagRename then + local s = textEntry("Rename " .. str) + if s then + component.invoke(map[str][1], "rename", map[str][2], currentDir .. s) + end + end + if act == tagDelete then + component.invoke(map[str][1], "remove", map[str][2]) + end + else + navigate(table.unpack(map[str])) + end + else + if openmode == "w" then return {currentDrive, currentDir .. str} end + local r = currentDir .. str + local subTag = "Size: " .. math.ceil(component.invoke(currentDrive, "size", r) / 1024) .. "KiB" + if openmode == "r" then subTag = tagOpen end + local act = menu(name .. ":" .. str, {subTag, tagRename, tagCopy, tagDelete, tagCancel}) + if act == tagOpen then return {currentDrive, currentDir .. str} end + if act == tagRename then + local s = textEntry("Rename " .. str) + component.invoke(currentDrive, "rename", currentDir .. str, currentDir .. s) + end + if act == tagCopy then + local f2 = fileManager("Copy " .. str, "w") + if f2 then + local h = component.invoke(currentDrive, "open", currentDir .. str, "rb") + if not h then + report("Couldn't open file!") + else + local h2 = component.invoke(f2[1], "open", f2[2], "wb") + if not h2 then + report("Couldn't open dest. file!") + else + local chk = component.invoke(currentDrive, "read", h, 128) + while chk do + component.invoke(f2[1], "write", h2, chk) + chk = component.invoke(currentDrive, "read", h, 128) + end + end + component.invoke(currentDrive, "close", h) + end + end + end + if act == tagDelete then + component.invoke(currentDrive, "remove", currentDir .. str) + end + end + end + end + end +end +return fileManager(filetype, openmode) \ No newline at end of file