1
0
mirror of https://github.com/20kdc/OC-KittenOS.git synced 2024-11-23 10:58:06 +11:00

With SYSTEM HEROES making continuing feasible, FIRST COMMIT

This commit is contained in:
20kdc 2018-03-18 23:10:54 +00:00
commit 6474b9356b
29 changed files with 4595 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# leaving in preSH.tar.gz for anyone who's interested
# in how NOT to do compression
code.tar
inst.lua
com2/code.tar.bd

79
README.md Normal file
View File

@ -0,0 +1,79 @@
# KittenOS NEO
### efficient. multi-tasking. clean. security-oriented.
# WARNING! STATUS: UNSTABLE!
The first commit is after I got the installer working again after the new compression system (BDIVIDE).
The older compression systems (which are not compatible with `heroes.lua`) are kept in preSH.tar.gz in case you want to see how NOT to do things.
## Description
KittenOS NEO is an OpenComputers operating system designed for Tier 1 hardware.
This means, among other things, it has an operating overhead limit of 192KiB real-world (on 32-bit).
Unlike the original KittenOS (now in the "legacy" branch), it is also designed with some attempt at cleanliness.
## User Guide
It is recommended that you take out your OpenComputers CPU, and shift-right-click until it says "Architecture: Lua 5.3", if possible.
Then simply download the installer from inst.lua here, rename it to "init.lua" and put it on a blank disk.
Finally, remove all other disks and reboot.
KittenOS NEO will install itself.
(This does not account for custom EEPROMs.)
## Policy regarding updates
KittenOS NEO's installer, including the full KittenOS NEO base operating system, is 65536 bytes or below.
As the installer must be loaded in full into RAM, this is not negotiable.
If it can't be kept this way with the current compressor, then a better compressor will have to be made.
All kernel or security-critical `sys-` process bugs will cause an installer update.
Other bugs will merely result in an updated copy in the repository.
This copy will be copied to installer code if and only if another condition requires the installer code be updated.
The code in the `code/` folder is the code meant for the installer.
Non-installer code is in the `repository/`, (WORKING ON THIS) and thus accessible via CLAW.
(NOTE: HTTPS is not used for this due to OC/OCEmu issues.)
Requests for additional features in system APIs will NOT cause an installer update.
## Building
The tools are meant for internal use, so are thus designed to run on some generic Unix.
Firstly, you can create a "raw installer" (uncompressed) with `mkucinst.lua`.
This executes `tar -cf code.tar code`, which you will need to do in any case - the installer contains a compressed TAR.
Secondly, for a compressed installer, after creating the TAR, `symsear-st1.sh`, `symsear-st2.sh`, and `symsear-st4.sh` (st3 is executed by st2) are used.
## Kernel Architecture
KittenOS NEO is an idea of what a Lua-based efficient microkernel would look like.
Scheduling is based entirely around uptime and timers,
which cause something to be executed at a given uptime.
## Installer Architecture
The installer is split into a generic TAR extractor frontend `insthead.lua` and a replacable compression backend (written in by relevant tools - in current versions, `heroes.lua` is where it starts).
There was more details on this but the details changed.
## License
This is released into the public domain.
No warranty is provided, implied or otherwise.

338
code/apps/app-claw.lua Normal file
View File

@ -0,0 +1,338 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- app-claw: Package manager.
-- HTTP-only because OCEmu SUCKS
local source = "http://20kdc.duckdns.org/neo/"
local primaryINet = neo.requestAccess("c.internet")
if primaryINet then primaryINet = primaryINet.list()() end
local function pkgExists(pkg, isApp)
local b = {}
if isApp then
b = neo.listApps()
else
b = neo.listLibs()
end
for _, v in ipairs(b) do
if v == pkg then return true end
end
end
-- global app variables
-- elements:
-- {
-- description,
-- dlURL/nil,
-- localPath,
-- isApp,
-- }
local packages = {}
local packageList = {}
local libList = {} -- removed after scan
local windows = 1
local searchTx = ""
local primaryWindowRegen
--
local event = require("event")(neo)
local neoux, err = require("neoux")
if not neoux then error(err) end
neoux = neoux(event, neo)
local function download(url)
if not primaryINet then return nil, "no internet" end
local req, err = primaryINet.request(url)
if not req then
return nil, tostring(err)
end
local ok, err = req.finishConnect()
if not req.finishConnect() then
req.close()
return nil, tostring(err)
end
local dt = ""
while true do
local n, n2 = req.read()
if not n then
req.close()
if n2 then
return nil, n2
else
break
end
else
if n == "" then
-- slightly dangerous, but what can we do?
event.sleepTo(os.uptime() + 0.05)
end
dt = dt .. n
end
end
req.close()
return dt
end
local function readLocal()
-- Read apps & libs
local function isLua(v)
if v:sub(#v - 3) == ".lua" then
return v:sub(1, #v - 4)
end
end
for _, pkg in ipairs(neo.listApps()) do
table.insert(packageList, pkg)
packages[pkg] = {
"A pre-installed or self-written process.",
nil,
"apps/" .. pkg .. ".lua",
true
}
end
for _, pkg in ipairs(neo.listLibs()) do
table.insert(libList, pkg)
packages[pkg] = {
"A pre-installed or self-written library.",
nil,
"libs/" .. pkg .. ".lua",
false
}
end
end
local function getEntry(name, isApp)
if packages[name] then
if packages[name][4] ~= isApp then
return
end
else
local path = "libs/" .. name .. ".lua"
if isApp then
path = "apps/" .. name .. ".lua"
end
packages[name] = {"An unknown entry, lost to time.", nil, path, isApp}
if isApp then
table.insert(packageList, name)
else
table.insert(libList, name)
end
end
return packages[name]
end
local function readWeb()
local listData, err = download(source .. "list")
if not listData then
neoux.startDialog("Couldn't get web index: " .. err, "web", true)
return
end
--neoux.startDialog(listData, "web", false)
listData = (listData .. "\n"):gmatch(".-\n")
local function listDataStrip()
local l = listData()
if not l then
return
end
l = l:sub(1, #l - 1)
return l
end
while true do
local l = listDataStrip()
if not l then return end
if l == "end" then return end
local ent = getEntry(l:sub(5), l:sub(1, 4) == "app ")
if ent then
ent[1] = listDataStrip() or "PARSE ERROR"
ent[2] = source .. l:sub(5) .. ".lua"
end
end
end
readLocal()
if source then
readWeb()
end
table.sort(packageList)
table.sort(libList)
for _, v in ipairs(libList) do
table.insert(packageList, v)
end
libList = nil
local function startPackageWindow(pkg)
windows = windows + 1
local downloading = false
local desc = packages[pkg][1]
local isApp = packages[pkg][4]
local isSysSvc = isApp and (pkg:sub(1, 4) == "sys-")
local isSvc = isSysSvc or (isApp and (pkg:sub(1, 4) == "svc-"))
local settings
if isSvc then
settings = neo.requestAccess("x.neo.sys.manage")
end
local function update(w)
if downloading then return end
downloading = true
local fd = download(packages[pkg][2])
local msg = "Success!"
if fd then
local primaryDisk = neo.requestAccess("c.filesystem").primary
local f = primaryDisk.open(packages[pkg][3], "wb")
primaryDisk.write(f, fd)
primaryDisk.close(f)
else
msg = "Couldn't download."
end
w.close()
windows = windows - 1
primaryWindowRegen()
-- Another event loop so the program won't exit too early.
neoux.startDialog(msg, pkg, true)
downloading = false
end
local elems = {
neoux.tcrawview(1, 1, neoux.fmtText(unicode.safeTextFormat(desc), 30)),
}
-- {txt, run}
local buttonbar = {}
if pkgExists(pkg, isApp) then
if isApp then
if not isSvc then
table.insert(buttonbar, {"Start", function (w)
neo.executeAsync(pkg)
w.close()
windows = windows - 1
end})
else
if not isSysSvc then
table.insert(buttonbar, {"Start", function (w)
neo.executeAsync(pkg)
w.close()
windows = windows - 1
end})
end
if settings.getSetting("run." .. pkg) == "yes" then
table.insert(buttonbar, {"Disable", function (w)
settings.setSetting("run." .. pkg, "no")
w.close()
windows = windows - 1
end})
else
table.insert(buttonbar, {"Enable", function (w)
settings.setSetting("run." .. pkg, "yes")
w.close()
windows = windows - 1
end})
end
end
end
if packages[pkg][2] then
table.insert(buttonbar, {"Update", update})
end
table.insert(buttonbar, {"Delete", function (w)
local primaryDisk = neo.requestAccess("c.filesystem").primary
primaryDisk.remove(packages[pkg][3])
w.close()
windows = windows - 1
primaryWindowRegen()
end})
else
if packages[pkg][2] then
table.insert(buttonbar, {"Install", update})
end
end
local x = 1
for _, v in ipairs(buttonbar) do
local b = neoux.tcbutton(x, 10, v[1], v[2])
x = x + (#v[1]) + 2
table.insert(elems, b)
end
neoux.create(30, 10, pkg, neoux.tcwindow(30, 10, elems, function (w)
w.close()
windows = windows - 1
end, 0xFFFFFF, 0))
end
local genwin, primaryWindow
local primaryWindowPage = 1
local primaryWindowList = packageList
function primaryWindowRegen()
primaryWindow.reset(20, 8, genwin(primaryWindowPage, primaryWindowList))
end
genwin = function (page, efList)
local pages = math.ceil(#efList / 6)
local elems = {
neoux.tcbutton(18, 1, "+", function (w)
if page < pages then
primaryWindowPage = page + 1
primaryWindowRegen()
end
end),
neoux.tcrawview(4, 1, {neoux.pad(page .. " / " .. pages, 14, true, true)}),
neoux.tcbutton(1, 1, "-", function (w)
if page > 1 then
primaryWindowPage = page - 1
primaryWindowRegen()
end
end)
}
local base = (page - 1) * 6
for i = 1, 6 do
local ent = efList[base + i]
if ent then
local enttx = ent
if packages[ent][4] then
enttx = "A " .. enttx
else
enttx = "L " .. enttx
end
if pkgExists(ent, packages[ent][4]) then
if packages[ent][2] then
enttx = "I" .. enttx
else
enttx = "i" .. enttx
end
else
enttx = " " .. enttx
end
table.insert(elems, neoux.tcbutton(1, i + 1, unicode.safeTextFormat(enttx), function (w)
-- Start a dialog
startPackageWindow(ent)
end))
end
end
table.insert(elems, neoux.tcfield(1, 8, 11, function (s)
if s then searchTx = s end
return searchTx
end))
table.insert(elems, neoux.tcbutton(12, 8, "Search!", function (w)
local n = {}
for _, v in ipairs(packageList) do
for i = 1, #v do
if v:sub(i, i + #searchTx - 1) == searchTx then
table.insert(n, v)
break
end
end
end
primaryWindowPage = 1
primaryWindowList = n
primaryWindowRegen()
end))
return neoux.tcwindow(20, 8, elems, function (w)
w.close()
windows = windows - 1
end, 0xFFFFFF, 0)
end
primaryWindow = neoux.create(20, 8, "claw", genwin(1, packageList))
while windows > 0 do
event.pull()
end

13
code/apps/app-fm.lua Normal file
View File

@ -0,0 +1,13 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- app-fm: dummy app to start FM
neo.requestAccess("x.neo.pub.base").showFileDialogAsync(nil)
while true do
local x = {coroutine.yield()}
if x[1] == "x.neo.pub.base" then
if x[2] == "filedialog" then
return
end
end
end

View File

@ -0,0 +1,41 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- app-launcher: The launcher
local event = require("event")(neo)
local neoux, err = require("neoux")
if not neoux then error(err) end -- This app is basically neoux's testcase
neoux = neoux(event, neo)
local running = true
local buttons = {}
local xlen = 0
local appNames = neo.listApps()
for k, v in ipairs(appNames) do
if v:sub(1, 4) == "app-" then
local vl = unicode.len(v)
if xlen < vl then
xlen = vl
end
table.insert(buttons, neoux.tcbutton(1, #buttons + 1, v, function (w)
-- Button pressed.
local pid, err = neo.executeAsync(v)
if not pid then
neoux.startDialog(tostring(err), "launchErr")
else
w.close()
running = false
end
end))
end
end
neoux.create(xlen + 2, #buttons, nil, neoux.tcwindow(xlen + 2, #buttons, buttons, function (w)
w.close()
running = false
end, 0xFFFFFF, 0))
while running do
event.pull()
end

47
code/apps/app-pass.lua Normal file
View File

@ -0,0 +1,47 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- app-pass: The password setter
local settings = neo.requestAccess("x.neo.sys.manage")
if not settings then error("no management") return end
local event = require("event")(neo)
local neoux, err = require("neoux")
if not neoux then error(err) end
neoux = neoux(event, neo)
local running = true
local pw = settings.getSetting("password")
neoux.create(20, 2, nil, neoux.tcwindow(20, 2, {
neoux.tcfield(1, 1, 12, function (set)
if not set then
return pw
end
pw = set
end),
neoux.tcbutton(13, 1, "set PW", function (w)
settings.setSetting("password", pw)
w.close()
running = false
end),
neoux.tcbutton(1, 2, "log out", function (w)
w.close()
running = false
local session = neo.requestAccess("x.neo.sys.session")
if not session then return end
session.endSession(true)
end),
neoux.tcbutton(11, 2, "shutdown", function (w)
w.close()
running = false
settings.shutdown(false)
end)
}, function (w)
w.close()
running = false
end, 0xFFFFFF, 0))
while running do
event.pull()
end

134
code/apps/app-taskmgr.lua Normal file
View File

@ -0,0 +1,134 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- app-taskmgr: Task manager
-- a-hello : simple test program for Everest.
local everest = neo.requestAccess("x.neo.pub.window")
if not everest then error("no everest") return end
local kill = neo.requestAccess("k.kill")
local sW, sH = 20, 8
local headlines = 2
local window = everest(sW, sH)
if not window then error("no window") end
local lastIdleTimeTime = os.uptime()
local lastIdleTime = neo.totalIdleTime()
local cpuPercent = 100
local lastCPUTimeRecord = {}
local cpuCause = "(none)"
local camY = 1
-- elements have {pid, text}
local consistentProcList = {}
local function drawLine(n)
local red = false
local idx = (camY + n) - (headlines + 1)
local stat = ("~"):rep(sW)
if n == 1 then
-- x.everest redraw. Since this window only has one line...
local usage = math.floor((os.totalMemory() - os.freeMemory()) / 1024)
stat = usage .. "/" .. math.floor(os.totalMemory() / 1024) .. "K, CPU " .. cpuPercent .. "%"
red = true
elseif n == 2 then
stat = "MAX:" .. cpuCause
red = true
elseif consistentProcList[idx] then
if idx == camY then
stat = ">" .. consistentProcList[idx][2]
else
stat = " " .. consistentProcList[idx][2]
end
end
stat = unicode.safeTextFormat(stat)
while unicode.len(stat) < sW do stat = stat .. " " end
if red then
window.span(1, n, unicode.sub(stat, 1, sW), 0xFFFFFF, 0)
else
window.span(1, n, unicode.sub(stat, 1, sW), 0x000000, 0xFFFFFF)
end
end
local function updateConsistentProcList(pt, lp)
local tbl = {}
local tbl2 = {}
local tbl3 = {}
for _, v in ipairs(pt) do
table.insert(tbl, v[1])
tbl2[v[1]] = v[2] .. "/" .. v[1] .. " " .. tostring(lp[v[1]]) .. "%"
end
table.sort(tbl)
for k, v in ipairs(tbl) do
tbl3[k] = {v, tbl2[v]}
end
consistentProcList = tbl3
end
local p = os.uptime()
neo.scheduleTimer(p)
while true do
local n = {coroutine.yield()}
if n[1] == "x.neo.pub.window" then
if n[3] == "line" then
drawLine(n[4])
end
if n[3] == "close" then
return
end
if n[3] == "key" then
if n[6] then
if n[4] == 8 then
if consistentProcList[camY] then
kill(consistentProcList[camY][1])
end
end
if n[5] == 200 then
camY = camY - 1
if camY < 1 then camY = 1 end
for i = (headlines + 1), sH do drawLine(i) end
end
if n[5] == 208 then
camY = camY + 1
for i = (headlines + 1), sH do drawLine(i) end
end
end
end
end
if n[1] == "k.timer" then
local now = os.uptime()
local nowIT = neo.totalIdleTime()
local tD = now - lastIdleTimeTime
local iD = nowIT - lastIdleTime
cpuPercent = math.ceil(((tD - iD) / tD) * 100)
lastIdleTimeTime = now
lastIdleTime = nowIT
local newRecord = {}
cpuCause = "(none)"
local causeUsage = 0
local pt = neo.listProcs()
local localPercent = {}
for _, v in ipairs(pt) do
-- pkg, pid, cpuTime
local baseline = 0
if lastCPUTimeRecord[v[1]] then
baseline = lastCPUTimeRecord[v[1]]
end
local val = v[3] - baseline
localPercent[v[1]] = math.ceil(100 * (val / tD))
if causeUsage < val then
cpuCause = v[2] .. "/" .. v[1]
causeUsage = val
end
newRecord[v[1]] = v[3]
end
lastCPUTimeRecord = newRecord
updateConsistentProcList(pt, localPercent)
for i = 1, sH do drawLine(i) end
p = p + 1
neo.scheduleTimer(p)
end
end

423
code/apps/app-textedit.lua Normal file
View File

@ -0,0 +1,423 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- 'neolithic': Text Editor
-- This was textedit (femto) from KittenOS brought kicking and screaming into the NEO world
-- It also has fixes for bugs involving wide text, and runs faster due to the chars -> lines change.
-- needs translation??? figure out how to do translation in the new system NICELY
local lines = {
"Neolithic: Text Editor",
"^A, ^S, ^Q: Load, Save, New",
"^C, ^V, ^D: Copy, Paste, Delete Line",
-- These two are meant to replace similar functionality in GNU Nano
-- (which I consider the best console text editor out there - Neolithic is an *imitation* and a poor one at that),
-- except fixing a UI flaw by instead making J responsible for resetting the append flag,
-- so the user can more or less arbitrarily mash together lines
"^J: Reset 'append' flag for Cut Lines",
"^K: Cut Line(s)",
"^<arrows>: Resize Win",
"'^' is Control.",
"Wide text can be pasted in,",
" using your 'actual' clipboard.",
" ",
}
-- If replicating Nano's clipboard :
-- Nano starts off in a "replace" mode,
-- and then after an action occurs switches to "append" until *any cursor action is performed*.
-- The way I have things setup is that you perform J then K(repeat) *instead*, which means you have to explicitly say "destroy current clipboard".
local event = require("event")(neo)
local clipboard = neo.requestAccess("x.neo.pub.clip.text")
if not clipboard then
local clipboardData = ""
clipboard = {
copy = function (text) clipboardData = text end,
paste = function () return clipboardData end
}
end
local cursorX = 1
local cursorY = math.ceil(#lines / 2)
local cFlash = true
local ctrlFlag = false
local dialogLock = false
local appendFlag = false
local sW, sH = 37, #lines + 2
local window = neo.requestAccess("x.neo.pub.window")(sW, sH)
local flush
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
local function fileDialog(writing, callback)
local tag = neo.requestAccess("x.neo.pub.base").showFileDialogAsync(writing)
local f
function f(_, evt, tag2, res)
if evt == "filedialog" then
if tag == tag2 then
callback(res)
event.ignore(f)
end
end
end
event.listen("x.neo.pub.base", f)
end
-- Save/Load
local function startSave()
dialogLock = true
fileDialog(true, function (res)
dialogLock = false
if res then
for k, v in ipairs(lines) do
if k ~= 1 then
res.write("\n" .. v)
else
res.write(v)
end
end
res.close()
end
end)
end
local function startLoad()
dialogLock = true
fileDialog(false, function (res)
dialogLock = false
if res then
lines = {}
local lb = ""
while true do
local l = res.read(64)
if not l then
table.insert(lines, lb)
cursorX = 1
cursorY = 1
res.close()
flush()
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)
end
local function getline(y)
-- do rY first since unw
-- only requires that
-- horizontal stuff be
-- messed with...
-- ...thankfully
local rY = (y + cursorY) - math.ceil(sH / 2)
-- rX is difficult!
local rX = 1
local Xthold = math.floor(sW / 2)
local _, cursorXP = unicode.safeTextFormat(lines[cursorY], cursorX)
if cursorXP > Xthold then
rX = rX + (cursorXP - Xthold)
end
local line = lines[rY]
if not line then
return ("¬"):rep(sW)
end
line = unicode.safeTextFormat(line)
-- <alter RX here by 1 if needed>
local tl = unicode.sub(line, rX, rX + sW - 1)
cursorXP = (cursorXP - rX) + 1
if cFlash then
if rY == cursorY then
if cursorXP >= 1 then
if cursorXP <= sW then
local start = unicode.sub(tl, 1, cursorXP - 1)
local endx = unicode.sub(tl, cursorXP + 1)
tl = start .. "_" .. endx
end
end
end
end
while unicode.len(tl) < sW do
tl = tl .. " "
end
return tl
end
local function delLine()
local contents = lines[cursorY]
if cursorY == #lines then
if cursorY == 1 then
lines[1] = ""
else
cursorY = cursorY - 1
lines[#lines] = nil
end
else
table.remove(lines, cursorY)
end
return contents
end
-- add a single character
local 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
local function ev_key(ka, kc, down)
if dialogLock then
return false
end
if kc == 29 then
ctrlFlag = down
return false
end
if ctrlFlag then
if not down then return false end
if kc == 200 then -- Up
sH = sH - 1
if sH == 0 then
sH = 1
end
sW, sH = window.setSize(sW, sH)
end
if kc == 208 then -- Down
sH = sH + 1
sW, sH = window.setSize(sW, sH)
end
if kc == 203 then -- Left
sW = sW - 1
if sW == 0 then
sW = 1
end
sW, sH = window.setSize(sW, sH)
end
if kc == 205 then -- Right
sW = sW + 1
sW, sH = window.setSize(sW, sH)
end
if kc == 31 then -- S
startSave()
end
if kc == 30 then -- A
startLoad()
end
if kc == 16 then -- Q
lines = {""}
cursorX = 1
cursorY = 1
return true
end
if kc == 32 then -- D
delLine()
return true
end
if kc == 46 then -- C
clipboard.copy(lines[cursorY])
end
if kc == 36 then -- J
appendFlag = false
end
if kc == 37 then -- K
if appendFlag then
local base = clipboard.paste()
clipboard.copy(base .. "\n" .. delLine())
else
clipboard.copy(delLine())
end
appendFlag = true
return true
end
if kc == 47 then -- V
local tx = clipboard.paste()
local txi = tx:find("\n")
local nt = {}
while txi do
table.insert(nt, 1, tx:sub(1, txi - 1))
tx = tx:sub(txi + 1)
txi = tx:find("\n")
end
table.insert(lines, cursorY, tx)
for _, v in ipairs(nt) do
table.insert(lines, cursorY, v)
end
return true
end
return false
end
-- action keys
if not down then
return false
end
if kc == 200 or kc == 201 then -- Go up one - go up page
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 -- Go down one - go down page
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
local function ev_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
end
flush = function ()
for i = 1, sH do
window.span(1, i, getline(i), 0xFFFFFF, 0)
end
end
local flash
flash = function ()
cFlash = not cFlash
-- reverse:
--local rY = (y + cursorY) - math.ceil(sH / 2)
local csY = math.ceil(sH / 2)
window.span(1, csY, getline(csY), 0xFFFFFF, 0)
event.runAt(os.uptime() + 0.5, flash)
end
event.runAt(os.uptime() + 0.5, flash)
while true do
local e = {event.pull()}
if e[1] == "x.neo.pub.window" then
if e[2] == window.id then
if e[3] == "touch" then
-- reverse:
--local rY = (y + cursorY) - math.ceil(sH / 2)
local csY = math.ceil(sH / 2)
local nY = math.max(1, math.min(#lines, (math.floor(e[5]) - csY) + cursorY))
cursorY = nY
clampCursorX()
flush()
end
if e[3] == "key" then
if ev_key(e[4], e[5], e[6]) then
flush()
end
end
if e[3] == "line" then
window.span(1, e[4], getline(e[4]), 0xFFFFFF, 0)
end
if e[3] == "focus" then
ctrlFlag = false
end
if e[3] == "close" then
return
end
if e[3] == "clipboard" then
ev_clipboard(e[4])
flush()
end
end
end
end

295
code/apps/sys-donkonit.lua Normal file
View File

@ -0,0 +1,295 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- s-donkonit : config, shutdown, screens
-- Doesn't matter what calls this service, because there's a mutex here.
local donkonitSPProvider = neo.requestAccess("r.neo.sys.manage") -- Restrict to s-
if not donkonitSPProvider then return end
local donkonitRDProvider = neo.requestAccess("r.neo.sys.screens")
if not donkonitRDProvider then return end
local computer = neo.requestAccess("k.computer")
local fs = neo.requestAccess("c.filesystem")
local gpus = neo.requestAccess("c.gpu")
local screens = neo.requestAccess("c.screen")
neo.requestAccess("s.h.component_added")
neo.requestAccess("s.h.component_removed")
local function shutdownFin(reboot)
-- any final actions donkonit needs to take here
computer.shutdown(reboot)
end
-- keys are pids
local targs = {} -- settings notify targs
local targsSD = {} -- shutdown notify targs
local targsST = {} -- saving throws
local targsRD = {} -- pid->{sendSig,dead}
-- screen address -> {gpu, monitor}
local monitorMap = {}
local shuttingDown = false
local shutdownMode = false
-- needs improvements
local settings = {
-- The list of settings is here:
-- password
password = "",
["run.sys-everest"] = "yes",
["run.sys-icecap"] = "yes",
-- scr.w/h/d.<uuid>
}
local function loadSettings()
pcall(function ()
local fw = require("sys-filewrap")
local se = require("serial")
local st = fw(fs.primary, "data/sys-donkonit/sysconf.lua", false)
local cfg = st.read("*a")
st.close()
st = nil
fw = nil
cfg = se.deserialize(cfg)
for k, v in pairs(cfg) do
if type(k) == "string" then
if type(v) == "string" then
settings[k] = v
end
end
end
end)
end
local function saveSettings()
local fw = require("sys-filewrap")
local se = require("serial")
fs.primary.makeDirectory("data/sys-donkonit")
local st = fw(fs.primary, "data/sys-donkonit/sysconf.lua", true)
st.write(se.serialize(settings))
st.close()
end
-- Monitor management stuff
local monitorPool = {}
-- {gpu, claimedLoseCallback}
local monitorClaims = {}
local function announceFreeMonitor(address, except)
for k, v in pairs(targsRD) do
if k ~= except then
v[1]("available", address)
end
end
end
local function getGPU(monitor)
local bestG
local bestD = 0
for v in gpus.list() do
v.bind(monitor.address)
local d = v.maxDepth()
if d > bestD then
bestG = v
bestD = d
end
end
return bestG
end
local function setupMonitor(gpu, monitor)
local maxW, maxH = gpu.maxResolution()
local maxD = gpu.maxDepth()
local w = tonumber(settings["scr.w." .. monitor.address]) or 80
local h = tonumber(settings["scr.h." .. monitor.address]) or 25
local d = tonumber(settings["scr.d." .. monitor.address]) or 8
w, h, d = math.floor(w), math.floor(h), math.floor(d)
w, h, d = math.min(w, maxW), math.min(h, maxH), math.min(d, maxD)
settings["scr.w." .. monitor.address] = tostring(w)
settings["scr.h." .. monitor.address] = tostring(h)
settings["scr.d." .. monitor.address] = tostring(d)
gpu.setResolution(w, h)
gpu.setDepth(d)
pcall(saveSettings)
end
donkonitSPProvider(function (pkg, pid, sendSig)
targs[pid] = sendSig
return {
listSettings = function ()
local s = {}
for k, v in pairs(settings) do
table.insert(s, k)
end
return s
end,
getSetting = function (name)
if type(name) ~= "string" then error("Setting name must be string") end
return settings[name]
end,
delSetting = function (name)
if type(name) ~= "string" then error("Setting name must be string") end
local val = nil
if name == "password" then val = "" end
settings[name] = val
for _, v in pairs(targs) do
v("set_setting", name, val)
end
pcall(saveSettings)
end,
setSetting = function (name, val)
if type(name) ~= "string" then error("Setting name must be string") end
if type(val) ~= "string" then error("Setting value must be string") end
settings[name] = val
for _, v in pairs(targs) do
v("set_setting", name, val)
end
pcall(saveSettings)
--saveSettings()
end,
registerForShutdownEvent = function ()
targsSD[pid] = sendSig
end,
registerSavingThrow = function (st)
if type(st) ~= "function" then error("Saving throw function must be a function") end
targsST[pid] = st
end,
shutdown = function (reboot)
if type(reboot) ~= "boolean" then error("Shutdown parameter must be a boolean (reboot)") end
if shuttingDown then return end
shuttingDown = true
shutdownMode = reboot
local counter = 0
neo.scheduleTimer(os.uptime() + 5) -- in case the upcoming code fails in some way
for f, v in pairs(targsSD) do
counter = counter + 1
v("shutdown", reboot, function ()
counter = counter - 1
if counter == 0 then
shutdownFin(reboot)
end
end)
end
if counter == 0 then
shutdownFin(reboot)
end
-- donkonit will shutdown when the timer is hit.
end
}
end)
donkonitRDProvider(function (pkg, pid, sendSig)
local claimed = {}
targsRD[pid] = {sendSig, function ()
for k, v in pairs(claimed) do
-- Nothing to really do here
monitorClaims[k] = nil
announceFreeMonitor(k, pid)
end
end}
return {
getClaimable = function ()
local c = {}
-- do we have gpu?
if not gpus.list()() then return c end
for _, v in ipairs(monitorPool) do
table.insert(c, v.address)
end
return c
end,
claim = function (address)
if type(address) ~= "string" then error("Address must be string.") end
for k, v in ipairs(monitorPool) do
if v.address == address then
local gpu = getGPU(v)
if gpu then
setupMonitor(gpu, v)
gpu = gpu.address
local disclaimer = function (wasDevLoss)
-- we lost it
monitorClaims[address] = nil
claimed[address] = nil
if not wasDevLoss then
table.insert(monitorPool, v)
announceFreeMonitor(address, pid)
else
sendSig("lost", address)
end
end
claimed[address] = disclaimer
monitorClaims[address] = {gpu, disclaimer}
table.remove(monitorPool, k)
return function ()
for v in gpus.list() do
if v.address == gpu then
local _, v2 = v.bind(address)
if not v2 then
return v
else
return
end
end
end
end
end
end
end
end,
disclaim = function (address)
if not address then error("Cannot disclaim nothing.") end
if claimed[address] then
claimed[address](false)
end
end
}
end)
-- -- The actual initialization
loadSettings()
local function rescanDevs()
monitorPool = {}
local hasGPU = gpus.list()()
for m in screens.list() do
if monitorClaims[m.address] then
monitorClaims[m.address][2](true)
end
table.insert(monitorPool, m)
if hasGPU then
announceFreeMonitor(m.address)
end
end
end
rescanDevs()
-- Save any settings made during the above (or just the language)
saveSettings()
-- --
while true do
local s = {coroutine.yield()}
if s[1] == "k.timer" then
-- always shutdown
shutdownFin(shutdownMode)
end
if s[1] == "h.component_added" then
rescanDevs()
end
if s[1] == "h.component_removed" then
rescanDevs()
end
if s[1] == "k.procdie" then
targs[s[3]] = nil
targsSD[s[3]] = nil
if targsST[s[3]] then
if s[4] then
pcall(targsST[s[3]])
end
end
targsST[s[3]] = nil
if targsRD[s[3]] then
targsRD[s[3]][2]()
end
end
end

639
code/apps/sys-everest.lua Normal file
View File

@ -0,0 +1,639 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- s-everest
-- Terminology:
-- "monitor": Either the Null Virtual Monitor[0] (a safetynet),
-- or an actual GPU/Screen pair managed by donkonit.
-- "surface": Everest system-level drawing primitive
-- "window" : Everest user-level wrapper around a surface providing a reliable window frame, movement, etc.
-- "line" : A Wx1 area across a surface.
-- "span" : A ?x1 area across a surface with text and a single fg/bg colour.
-- This has less user calls as opposed to the old KittenOS system, which had a high CPU usage.
-- Another thing to note is that Everest still uses callbacks, both for efficiency and library convenience,
-- though with automatically closing windows on process death.
-- How Bristol talks to this is:
-- 1. Bristol starts up Everest. Everest does not claim new monitors by default.
-- 2. Bristol claims all available monitors to blank out the display
-- 3. The user logs in
-- 4. Bristol runs "startSession", enabling claiming of free monitors, and then promptly dies.
-- 5. Everest claims the new monitors, and the desktop session begins
-- 6. Everest dies/respawns, or endSession is called - in both cases,
-- Everest is now essentially back at the state in 1.
-- 7. Either this is Bristol, so go to 2,
-- or this is a screensaver host, and has a saving-throw to start Bristol if it dies unexpectedly.
-- In any case, this eventually returns to 2 or 4.
local everestProvider = neo.requestAccess("r.neo.pub.window")
if not everestProvider then return end
local everestSessionProvider = neo.requestAccess("r.neo.sys.session")
if not everestSessionProvider then return end
-- Got mutexes. Now setup saving throw and shutdown callback
-- Something to note is that Donkonit is the safety net on this,
-- as it auto-releases the monitors.
local screens = neo.requestAccess("x.neo.sys.screens")
if not screens then
error("Donkonit is required to run Everest")
end
neo.requestAccess("s.h.clipboard")
neo.requestAccess("s.h.touch")
neo.requestAccess("s.h.drag")
neo.requestAccess("s.h.key_up")
neo.requestAccess("s.h.key_down")
-- {gpu, screenAddr, w, h, bg, fg}
local monitors = {}
-- NULL VIRTUAL MONITOR!
-- This is where we stuff processes while Bristol isn't online
monitors[0] = {nil, nil, 80, 25}
-- {monitor, x, y, w, h, callback}
-- callback events are:
-- key ka kc down
-- line y
local surfaces = {}
-- Dead process's switch to clean up resources by crashing processes which "just don't get it"
local dead = false
local savingThrow = neo.requestAccess("x.neo.sys.manage")
if savingThrow then
savingThrow.registerForShutdownEvent()
savingThrow.registerSavingThrow(function ()
if #monitors > 0 then
neo.executeAsync("sys-init", monitors[1][2])
end
neo.executeAsync("sys-everest")
-- In this case the surfaces are leaked and hold references here. They have to be removed manually.
-- Do this via a "primary event" (k.deregistration) and "deathtrap events"
-- If a process evades the deathtrap then it clearly has reason to stay alive regardless of Everest status.
dead = true
monitors = {}
for _, v in ipairs(surfaces) do
pcall(v[6], "line", 1)
pcall(v[6], "line", 2)
end
end)
end
-- Grab all available monitors when they become available
local inSession = false
local function renderingAllowed()
-- This is a safety feature to prevent implosion due to missing monitors.
return #monitors > 0
end
local function surfaceAt(monitor, x, y)
for k, v in ipairs(surfaces) do
if v[1] == monitor then
if x >= v[2] then
if y >= v[3] then
if x < (v[2] + v[4]) then
if y < (v[3] + v[5]) then
return k, (x - v[2]) + 1, (y - v[3]) + 1
end
end
end
end
end
end
end
local function monitorGPUColours(m, cb, bg, fg)
local nbg = m[5]
local nfg = m[6]
if nbg ~= bg then
cb.setBackground(bg)
m[5] = bg
end
if nfg ~= fg then
cb.setForeground(fg)
m[6] = fg
end
end
local function updateRegion(monitorId, x, y, w, h, surfaceSpanCache)
if not renderingAllowed() then return end
local m = monitors[monitorId]
local mg = m[1]()
if not mg then return end
-- The input region is the one that makes SENSE.
-- Considering WC handling, that's not an option.
-- WCHAX: start
if x > 1 then
x = x - 1
w = w + 1
end
-- this, in combination with 'forcefully blank out last column of window during render',
-- cleans up littering
w = w + 1
-- end
for span = 1, h do
local backgroundMarkStart = nil
for sx = 1, w do
local t, tx, ty = surfaceAt(monitorId, sx + x - 1, span + y - 1)
if t then
-- It has to be in this order to get rid of wide char weirdness
if backgroundMarkStart then
monitorGPUColours(m, mg, 0x000020, 0)
mg.fill(backgroundMarkStart + x - 1, span + y - 1, sx - backgroundMarkStart, 1, " ")
backgroundMarkStart = nil
end
if not surfaceSpanCache[t .. "_" .. ty] then
surfaceSpanCache[t .. "_" .. ty] = true
surfaces[t][6]("line", ty)
end
elseif not backgroundMarkStart then
backgroundMarkStart = sx
end
end
if backgroundMarkStart then
local m = monitors[monitorId]
monitorGPUColours(m, mg, 0x000020, 0)
mg.fill(backgroundMarkStart + x - 1, span + y - 1, (w - backgroundMarkStart) + 1, 1, " ")
end
end
end
local function ensureOnscreen(monitor, x, y, w, h)
if monitor <= 0 then monitor = #monitors end
if monitor >= (#monitors + 1) then monitor = 1 end
-- Failing anything else, revert to monitor 0
if #monitors == 0 then monitor = 0 end
x = math.min(math.max(1, x), monitors[monitor][3] - (w - 1))
y = math.min(math.max(1, y), monitors[monitor][4] - (h - 1))
return monitor, x, y
end
-- This is the "a state change occurred" function, only for use when needed
local function reconcileAll()
for k, v in ipairs(surfaces) do
-- About to update whole screen anyway so avoid the wait.
v[1], v[2], v[3] = ensureOnscreen(v[1], v[2], v[3], v[4], v[5])
end
for k, v in ipairs(monitors) do
local mon = v[1]()
if mon then
v[3], v[4] = mon.getResolution()
end
v[5] = -1
v[6] = -1
updateRegion(k, 1, 1, v[3], v[4], {})
end
end
local function moveSurface(surface, m, x, y, w, h)
local om, ox, oy, ow, oh = table.unpack(surface, 1, 5)
m = m or om
x = x or ox
y = y or oy
w = w or ow
h = h or oh
surface[1], surface[2], surface[3], surface[4], surface[5] = m, x, y, w, h
local cache = {}
if om == m then
if ow == w then
if oh == h then
-- Cheat - perform a GPU copy
-- this increases "apparent" performance while we're inevitably waiting for the app to catch up,
-- CANNOT glitch since we're going to draw over this later,
-- and will usually work since the user can only move focused surfaces
if renderingAllowed() then
local cb = monitors[m][1]()
if cb then
cb.copy(ox, oy, w, h, x - ox, y - oy)
end
end
end
end
end
updateRegion(om, ox, oy, ow, oh, cache)
updateRegion(m, x, y, w, h, cache)
end
-- Returns offset from where we expected to be to where we are.
local function ofsSurface(focus, dx, dy)
local exX, exY = focus[2] + dx, focus[3] + dy
local m, x, y = ensureOnscreen(focus[1], exX, exY, focus[4], focus[5])
moveSurface(focus, nil, x, y)
return focus[2] - exX, focus[3] - exY
end
local function ofsMSurface(focus, dm)
local m, x, y = ensureOnscreen(focus[1] + dm, focus[2], focus[3], focus[4], focus[5])
moveSurface(focus, m, x, y)
end
local function handleSpan(target, x, y, text, bg, fg)
if not renderingAllowed() then return end
local m = monitors[target[1]]
local cb = m[1]()
if not cb then return end
-- It is assumed basic type checks were handled earlier.
if y < 1 then return end
if y > target[5] then return end
if x < 1 then return end
-- Note the use of unicode.len here.
-- It's assumed that if the app is using Unicode text, then it used safeTextFormat earlier.
-- This works for a consistent safety check.
local w = unicode.len(text)
if ((x + w) - 1) > target[4] then return end
-- Checks complete, now commence screen cropping...
local worldY = ((y + target[3]) - 1)
if worldY < 1 then return end
if worldY > monitors[target[1]][4] then return end
-- The actual draw loop
local buildingSegmentWX = nil
local buildingSegmentWY = nil
local buildingSegment = nil
local buildingSegmentE = nil
local function submitSegment()
if buildingSegment then
base = unicode.sub(text, buildingSegment, buildingSegmentE - 1)
local ext = unicode.sub(text, buildingSegmentE, buildingSegmentE)
if unicode.charWidth(ext) == 1 then
base = base .. ext
else
-- While the GPU may or may not be able to display "half a character",
-- getting it to do so reliably is another matter.
-- In my experience it always leads to drawing errors much worse than if the code was left alone.
-- If your language uses wide chars and you are affected by a window's positioning...
-- ... may I ask how, exactly, you intend me to fix it?
-- My current theory is that for cases where the segment is >= 2 chars (so we have scratchpad),
-- the GPU might be tricked via a copy.
-- Then the rest of the draw can proceed as normal,
-- with the offending char removed.
base = base .. " "
end
monitorGPUColours(m, cb, bg, fg)
cb.set(buildingSegmentWX, buildingSegmentWY, unicode.undoSafeTextFormat(base))
buildingSegment = nil
end
end
for i = 1, w do
local rWX = (i - 1) + (x - 1) + target[2]
local rWY = (y - 1) + target[3]
local s = surfaceAt(target[1], rWX, rWY)
local ok = false
if s then
ok = surfaces[s] == target
end
if ok then
if not buildingSegment then
buildingSegmentWX = rWX
buildingSegmentWY = rWY
buildingSegment = i
end
buildingSegmentE = i
else
submitSegment()
end
end
submitSegment()
end
local function changeFocus(oldSurface, optcache)
local ns1 = surfaces[1]
optcache = optcache or {}
if ns1 ~= oldSurface then
if oldSurface then
oldSurface[6]("focus", false)
end
if ns1 then
ns1[6]("focus", true)
end
if oldSurface then
updateRegion(oldSurface[1], oldSurface[2], oldSurface[3], oldSurface[4], oldSurface[5], optcache)
end
if ns1 then
updateRegion(ns1[1], ns1[2], ns1[3], ns1[4], ns1[5], optcache)
end
end
end
-- THE EVEREST USER API BEGINS
local surfaceOwners = {}
-- Not relevant here really, but has to be up here because it closes the window
local waitingShutdownCallback = nil
local function checkWSC()
if waitingShutdownCallback then
if #surfaces == 0 then
waitingShutdownCallback()
waitingShutdownCallback = nil
end
end
end
everestProvider(function (pkg, pid, sendSig)
local base = pkg .. "/" .. pid
local lid = 0
return function (w, h, title)
if dead then error("everest died") end
w = math.floor(math.max(w, 8))
h = math.floor(math.max(h, 1)) + 1
if type(title) ~= "string" then
title = base
else
title = base .. ":" .. title
end
local m = 0
if renderingAllowed() then m = 1 end
local surf = {m, 1, 1, w, h}
local focusState = false
local llid = lid
lid = lid + 1
local specialDragHandler
surf[6] = function (ev, a, b, c)
-- Must forward surface events
if ev == "focus" then
focusState = a
end
if ev == "touch" then
specialDragHandler = nil
if math.floor(b) == 1 then
specialDragHandler = function (x, y)
local ofsX, ofsY = math.floor(x) - math.floor(a), math.floor(y) - math.floor(b)
if (ofsX == 0) and (ofsY == 0) then return end
local pX, pY = ofsSurface(surf, ofsX, ofsY)
--a = a + pX
--b = b + pY
end
return
end
b = b - 1
end
if ev == "drag" then
if specialDragHandler then
specialDragHandler(a, b)
return
end
b = b - 1
end
if ev == "line" then
if a == 1 then
local lw = surf[4]
local bg = 0x0080FF
local fg = 0x000000
local tx = "-"
if focusState then
bg = 0x000000
fg = 0x0080FF
tx = "+"
end
local vtitle = title
local vto = unicode.len(vtitle)
if vto < lw then
vtitle = vtitle .. (tx):rep(lw - vto)
else
vtitle = unicode.sub(vtitle, 1, lw)
end
handleSpan(surf, 1, 1, vtitle, bg, fg)
return
end
-- WCHAX : Wide-char-cleanup has to be done left-to-right, so this handles the important part of that.
handleSpan(surf, surf[4], a, " ", 0, 0)
a = a - 1
end
sendSig(llid, ev, a, b, c)
end
local osrf = surfaces[1]
table.insert(surfaces, 1, surf)
surfaceOwners[surf] = pid
changeFocus(osrf)
return {
id = llid,
setSize = function (w, h)
if dead then return end
w = math.floor(math.max(w, 8))
h = math.floor(math.max(h, 1)) + 1
local _, x, y = ensureOnscreen(surf[1], surf[2], surf[3], w, h)
moveSurface(surf, nil, x, y, w, h)
return w, (h - 1)
end,
span = function (x, y, text, bg, fg)
if dead then error("everest died") end
if type(x) ~= "number" then error("X must be number.") end
if type(y) ~= "number" then error("Y must be number.") end
if type(bg) ~= "number" then error("Background must be number.") end
if type(fg) ~= "number" then error("Foreground must be number.") end
if type(text) ~= "string" then error("Text must be string.") end
x, y, bg, fg = math.floor(x), math.floor(y), math.floor(bg), math.floor(fg)
if y == 0 then return end
handleSpan(surf, x, y + 1, text, bg, fg)
end,
close = function ()
if dead then return end
local os1 = surfaces[1]
surfaceOwners[surf] = nil
for k, v in ipairs(surfaces) do
if v == surf then
table.remove(surfaces, k)
local cache = {}
checkWSC()
changeFocus(os1, cache)
-- focus up to date, deal with any remains
updateRegion(surf[1], surf[2], surf[3], surf[4], surf[5], cache)
return
end
end
end
}
end
end)
-- THE EVEREST USER API ENDS (now for the session API, which just does boring stuff)
everestSessionProvider(function (pkg, pid, sendSig)
return {
startSession = function ()
inSession = true
end,
endSession = function (startBristol)
if not inSession then return end
local m = nil
if monitors[1] then
m = monitors[1][2]
end
inSession = false
for k = 1, #monitors do
screens.disclaim(monitors[k][2])
monitors[k] = nil
end
if startBristol then
neo.executeAsync("sys-init", m)
end
reconcileAll()
if not startBristol then
return m
end
end
}
end)
-- THE EVEREST SESSION API ENDS
-- WM shortcuts are:
-- Alt-Z: Switch surface
-- Alt-Enter: Launcher
-- Alt-Up/Down/Left/Right: Move surface
local isAltDown = false
local isCtrDown = false
local function key(ka, kc, down)
local focus = surfaces[1]
if kc == 29 then isCtrDown = down end
if kc == 56 then isAltDown = down end
if isAltDown then
if ka == 120 then
if focus and down then ofsMSurface(focus, 1) end return
end
if kc == 200 then
if focus and down then ofsSurface(focus, 0, -1) end return
end
if kc == 208 then
if focus and down then ofsSurface(focus, 0, 1) end return
end
if kc == 203 then
if focus and down then ofsSurface(focus, -1, 0) end return
end
if kc == 205 then
if focus and down then ofsSurface(focus, 1, 0) end return
end
if ka == 122 then
if focus and down then
local n = table.remove(surfaces, 1)
table.insert(surfaces, n)
changeFocus(n)
end return
end
if ka == 3 then
-- Ctrl-Alt-C (!?!?!!)
if isCtrDown then
error("User-authorized Everest crash.")
end
end
if ka == 99 then
if down then
if isCtrDown then
error("User-authorized Everest crash.")
else
if focus then
focus[6]("close")
end
end
end
return
end
if ka == 13 then
if down and (not waitingShutdownCallback) then neo.executeAsync("app-launcher") end return
end
end
if focus then
focus[6]("key", ka, kc, down)
end
end
while true do
local s = {coroutine.yield()}
if renderingAllowed() then
if s[1] == "h.key_down" then
key(s[3], s[4], true)
end
if s[1] == "h.key_up" then
key(s[3], s[4], false)
end
if s[1] == "h.clipboard" then
if surfaces[1] then
surfaces[1][6]("clipboard", s[3])
end
end
-- next on my list: high-res coordinates
if s[1] == "h.touch" then
for k, v in ipairs(monitors) do
if v[2] == s[2] then
local x, y = math.floor(s[3]), math.floor(s[4])
local sid, lx, ly = surfaceAt(k, x, y)
if sid then
local os = surfaces[1]
local ns = table.remove(surfaces, sid)
table.insert(surfaces, 1, ns)
changeFocus(os)
ns[6]("touch", lx, ly)
end
break
end
end
end
if s[1] == "h.drag" then
-- Pass to focus surface, even if out of bounds
local focus = surfaces[1]
if focus then
for k, v in ipairs(monitors) do
if v[2] == s[2] then
if k == focus[1] then
local x, y = (math.floor(s[3]) - focus[2]) + 1, (math.floor(s[4]) - focus[3]) + 1
focus[6]("drag", x, y)
end
break
end
end
end
end
else
isCtrDown = false
isAltDown = false
end
if s[1] == "k.procdie" then
local os1 = surfaces[1]
-- Note this is in order (that's important)
local tags = {}
for k, v in ipairs(surfaces) do
if surfaceOwners[v] == s[3] then
table.insert(tags, k)
surfaceOwners[v] = nil
end
end
for k, v in ipairs(tags) do
local surf = table.remove(surfaces, v - (k - 1))
updateRegion(surf[1], surf[2], surf[3], surf[4], surf[5], {})
end
checkWSC()
changeFocus(os1)
end
if s[1] == "x.neo.sys.screens" then
if s[2] == "available" then
if inSession then
local gpu = screens.claim(s[3])
local gpucb = gpu and (gpu())
if gpucb then
local w, h = gpucb.getResolution()
table.insert(monitors, {gpu, s[3], w, h, -1, -1})
-- This is required to ensure windows are moved off of the null monitor.
-- Luckily, there's an obvious sign if they aren't - everest will promptly crash.
reconcileAll()
end
end
end
if s[2] == "lost" then
for k, v in ipairs(monitors) do
if v[2] == s[3] then
table.remove(monitors, k)
reconcileAll()
break
end
end
end
end
if s[1] == "x.neo.sys.manage" then
if s[2] == "shutdown" then
waitingShutdownCallback = s[4]
for k, v in ipairs(surfaces) do
v[6]("close")
end
checkWSC()
end
end
end

137
code/apps/sys-icecap.lua Normal file
View File

@ -0,0 +1,137 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- s-icecap : Responsible for x.neo.pub API, crash dialogs, and security policy that isn't "sys- has ALL access, anything else has none"
-- In general, this is what userspace will be interacting with in some way or another to get stuff done
local event = require("event")(neo)
local neoux, err = require("neoux")
if not neoux then error(err) end -- This app is basically neoux's testcase
neoux = neoux(event, neo)
local donkonit = neo.requestAccess("x.neo.sys.manage")
if not donkonit then error("needs donkonit for sysconf access") end
local scr = neo.requestAccess("s.k.securityrequest")
if not scr then error("no security request access") end
local fs = neo.requestAccess("c.filesystem")
local donkonitDFProvider = neo.requestAccess("r.neo.pub.base")
if not donkonitDFProvider then return end
local targsDH = {} -- data disposal
donkonitDFProvider(function (pkg, pid, sendSig)
local prefixNS = "data/" .. pkg
local prefixWS = "data/" .. pkg .. "/"
fs.primary.makeDirectory(prefixNS)
local openHandles = {}
targsDH[pid] = function ()
for k, v in pairs(openHandles) do
v()
end
end
return {
showFileDialogAsync = function (forWrite)
-- Not hooked into the event API, so can't safely interfere
-- Thus, this is async and uses a return event.
local tag = {}
event.runAt(0, function ()
-- sys-filedialog is yet another "library to control memory usage".
local closer = require("sys-filedialog")(event, neoux, function (res) openHandles[tag] = nil sendSig("filedialog", tag, res) end, fs, pkg, forWrite)
openHandles[tag] = closer
end)
return tag
end,
-- Paths must begin with / implicitly
list = function (path)
if type(path) ~= "string" then error("Expected path to be string") end
path = prefixNS .. path
neo.ensurePath(path, prefixWS)
if path:sub(#path, #path) ~= "/" then error("Expected / at end") end
return fs.primary.list(path:sub(1, #path - 1))
end,
makeDirectory = function (path)
if type(path) ~= "string" then error("Expected path to be string") end
path = prefixNS .. path
neo.ensurePath(path, prefixWS)
if path:sub(#path, #path) == "/" then error("Expected no / at end") end
return fs.primary.makeDirectory(path)
end,
rename = function (path1, path2)
if type(path1) ~= "string" then error("Expected path to be string") end
if type(path2) ~= "string" then error("Expected path to be string") end
path1 = prefixNS .. path1
path2 = prefixNS .. path2
neo.ensurePath(path1, prefixWS)
neo.ensurePath(path2, prefixWS)
if path:sub(#path1, #path1) == "/" then error("Expected no / at end") end
if path:sub(#path2, #path2) == "/" then error("Expected no / at end") end
return fs.primary.rename(path1, path2)
end,
open = function (path, mode)
if type(path) ~= "string" then error("Expected path to be string") end
if type(mode) ~= "boolean" then error("Expected mode to be boolean (writing)") end
path = prefixNS .. path
neo.ensurePath(path, prefixWS)
if path:sub(#path, #path) == "/" then error("Expected no / at end") end
local fw, closer = require("sys-filewrap")(fs.primary, path, mode)
local oc = fw.close
fw.close = function ()
oc()
openHandles[fw] = nil
end
openHandles[fw] = closer
return fw
end,
remove = function (path)
if type(path) ~= "string" then error("Expected path to be string") end
path = prefixNS .. path
neo.ensurePath(path, prefixWS)
if path:sub(#path, #path) == "/" then error("Expected no / at end") end
return fs.primary.remove(path)
end,
stat = function (path)
if type(path) ~= "string" then error("Expected path to be string") end
path = prefixNS .. path
neo.ensurePath(path, prefixWS)
if path:sub(#path, #path) == "/" then error("Expected no / at end") end
if not fs.primary.exists(path) then return nil end
return {
fs.primary.isDirectory(path),
fs.primary.size(path),
fs.primary.lastModified(path)
}
end,
-- getLabel/setLabel have nothing to do with this
spaceUsed = fs.primary.spaceUsed,
spaceTotal = fs.primary.spaceTotal,
isReadOnly = fs.primary.isReadOnly
}
end)
event.listen("k.securityrequest", function (evt, pkg, pid, perm, rsp)
local secpol, err = require("sys-secpolicy")
if not secpol then
rsp(false)
error("Bad security policy: " .. err)
end
secpol(neoux, donkonit, pkg, pid, perm, rsp)
end)
event.listen("k.procdie", function (evt, pkg, pid, reason)
if targsDH[pid] then
targsDH[pid]()
end
targsDH[pid] = nil
if reason then
-- Process death logging in console (for lifecycle dbg)
-- neo.emergency(n[2])
-- neo.emergency(n[4])
neoux.startDialog(string.format("%s/%i died:\n%s", pkg, pid, reason), "error")
end
end)
while true do
event.pull()
end

417
code/apps/sys-init.lua Normal file
View File

@ -0,0 +1,417 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- s-bristol splash screen, login agent
-- Named to allude to Plymouth (splash screen used in some linux distros)
local callerPkg, callerPid, callerScr = ...
local gpu, screen = nil, nil
local shutdownEmergency = neo.requestAccess("k.computer").shutdown
neo.requestAccess("s.h.key_down")
-- donkonit hasn't started yet - this will be dealt with later
local function _(tx)
return tx
end
local scrW, scrH
local warnings = {
"",
"",
""
}
-- Attempts to call upon nsm for a safe shutdown
local function shutdown(reboot)
local donkonit = neo.requestAccess("x.neo.sys.manage")
if donkonit then
donkonit.shutdown(reboot)
while true do
coroutine.yield()
end
else
shutdownEmergency(reboot)
end
end
local function basicDraw()
scrW, scrH = gpu.getResolution()
gpu.fill(1, 1, scrW, scrH, " ")
gpu.set(2, 2, _("KittenOS NEO"))
end
local function advDraw()
basicDraw()
local usage = math.floor((os.totalMemory() - os.freeMemory()) / 1024)
gpu.set(2, 3, _("RAM Usage: ") .. usage .. "K / " .. math.floor(os.totalMemory() / 1024) .. "K")
for i = 1, #warnings do
gpu.set(2, 6 + i, warnings[i])
end
end
-- Callback setup by finalPrompt to disclaim the main monitor first
local performDisclaim = nil
local function retrieveNssMonitor(nss)
gpu = nil
local subpool = {}
while not gpu do
if performDisclaim then
performDisclaim()
end
-- nss available - this means the monitor pool is now ready.
-- If no monitors are available, shut down now.
-- donkonit monitor pool output is smaller than, but similar to, Everest monitor data:
-- {gpu, screenAddr}
local pool = nss.getClaimable()
while not pool[1] do
coroutine.yield() -- wait for presumably a donkonit notification
pool = nss.getClaimable()
end
subpool = {}
-- Specifies which element to elevate to top priority
local optimalSwap = nil
if screen then
for k, v in ipairs(pool) do
if v == screen then
optimalSwap = k
break
end
end
end
if optimalSwap then
local swapA = pool[optimalSwap]
pool[optimalSwap] = pool[1]
pool[1] = swapA
end
for _, v in ipairs(pool) do
local gpu = nss.claim(v)
if gpu then
local gcb = gpu()
if gcb then
table.insert(subpool, {gpu, v})
gcb.setBackground(0x000020)
local w, h = gcb.getResolution()
gcb.fill(1, 1, w, h, " ")
end
end
end
if not subpool[1] then error(_("Unable to claim any monitor.")) end
gpu = subpool[1][1]() -- BAD
screen = subpool[1][2]
end
-- done with search
scrW, scrH = gpu.getResolution()
gpu.setBackground(0xFFFFFF)
gpu.setForeground(0x000000)
gpu.fill(1, 1, scrW, scrH, " ")
performDisclaim = function ()
for _, v in ipairs(subpool) do
nss.disclaim(v[2])
end
end
end
local function sleep(t)
neo.scheduleTimer(os.uptime() + t)
while true do
local ev = {coroutine.yield()}
if ev[1] == "k.timer" then
break
end
if ev[1] == "x.neo.sys.screens" then
-- This implies we have and can use nss, but check anyway
local nss = neo.requestAccess("x.neo.sys.screens")
if nss then
retrieveNssMonitor(nss)
gpu.setBackground(0xFFFFFF)
gpu.setForeground(0x000000)
basicDraw()
end
end
end
end
local function finalPrompt()
local nss = neo.requestAccess("x.neo.sys.screens")
if nss then
retrieveNssMonitor(nss)
end
-- This is nsm's final chance to make itself available and thus allow the password to be set
local nsm = neo.requestAccess("x.neo.sys.manage")
local waiting = true
local safeModeActive = false
local password = ""
if nsm then
password = nsm.getSetting("password")
end
warnings[1] = _("TAB to change option,")
warnings[2] = _("ENTER to select...")
-- The actual main prompt loop
while waiting do
advDraw()
local entry = ""
local entry2 = ""
local active = true
local shButton = _("<Shutdown>")
local rbButton = _("<Reboot>")
local smButton = _("<Safe Mode>")
local pw = {function ()
return _("Password: ") .. entry2
end, function (key)
if key >= 32 then
entry = entry .. unicode.char(key)
entry2 = entry2 .. "*"
end
if key == 13 then
if entry == password then
waiting = false
else
advDraw()
sleep(1)
end
active = false
end
end, 2, 5, scrW - 2}
if password == "" then
pw = {function ()
return "Log in..."
end, function (key)
if key == 13 then
waiting = false
active = false
end
end, 2, 5, scrW - 2}
end
local controls = {
{function ()
return shButton
end, function (key)
if key == 13 then
basicDraw()
gpu.set(2, 4, "Shutting down...")
shutdown(false)
end
end, 2, scrH - 1, unicode.len(shButton)},
{function ()
return rbButton
end, function (key)
if key == 13 then
basicDraw()
gpu.set(2, 4, "Rebooting...")
shutdown(true)
end
end, 3 + unicode.len(shButton), scrH - 1, unicode.len(rbButton)},
{function ()
return smButton
end, function (key)
if key == 13 then
basicDraw()
gpu.set(2, 4, "Login to activate Safe Mode.")
sleep(1)
safeModeActive = true
advDraw()
end
end, 4 + unicode.len(shButton) + unicode.len(rbButton), scrH - 1, unicode.len(smButton)},
pw,
}
local control = #controls
while active do
for k, v in ipairs(controls) do
if k == control then
gpu.setBackground(0x000000)
gpu.setForeground(0xFFFFFF)
else
gpu.setBackground(0xFFFFFF)
gpu.setForeground(0x000000)
end
gpu.fill(v[3], v[4], v[5], 1, " ")
gpu.set(v[3], v[4], v[1]())
end
-- Reset to normal
gpu.setBackground(0xFFFFFF)
gpu.setForeground(0x000000)
-- event handling...
local sig = {coroutine.yield()}
if sig[1] == "x.neo.sys.screens" then
-- We need to reinit screens no matter what.
retrieveNssMonitor(nss)
end
if sig[1] == "h.key_down" then
if sig[4] == 15 then
-- this makes sense in context
control = control % (#controls)
control = control + 1
else
controls[control][2](sig[3])
end
end
end
end
advDraw()
return safeModeActive
end
local function postPrompt()
-- Begin to finish login, or fail
local everests = neo.requestAccess("x.neo.sys.session")
if everests then
local s, e = pcall(everests.startSession)
if not s then
table.insert(warnings, "Everest failed to create a session")
table.insert(warnings, tostring(e))
else
warnings = {"Transferring to Everest..."}
advDraw()
if performDisclaim then
performDisclaim()
-- Give Everest time (this isn't perceptible, and is really just a safety measure)
sleep(1)
end
return
end
else
table.insert(warnings, "Couldn't communicate with Everest...")
end
advDraw()
sleep(1)
shutdown(true)
end
local function initializeSystem()
-- System has just booted, bristol is in charge
-- Firstly, initialize hardware to something sensible since we don't know scrcfg
gpu = neo.requestAccess("c.gpu").list()()
if gpu then
screen = neo.requestAccess("c.screen").list()()
if not screen then gpu = nil end
end
if gpu then
screen = screen.address
gpu.bind(screen, true)
gpu.setDepth(gpu.maxDepth())
local gW, gH = gpu.maxResolution()
gW, gH = math.min(80, gW), math.min(25, gH)
gpu.setResolution(gW, gH)
gpu.setForeground(0x000000)
end
local w = 1
local steps = {
"sys-donkonit", -- (Donkonit : Config, Screen, Power)
-- Let that start, and system GC
"WAIT",
"WAIT",
"WAIT",
"WAIT",
-- Start services
"INJECT",
-- extra GC time
"WAIT",
"WAIT",
"WAIT",
"WAIT",
"WAIT",
"WAIT",
"WAIT"
}
local stepCount = #steps
neo.scheduleTimer(os.uptime())
while true do
local ev = {coroutine.yield()}
if ev[1] == "k.procnew" then
table.insert(warnings, ev[2] .. "/" .. ev[3] .. " UP")
end
if ev[1] == "k.procdie" then
table.insert(warnings, ev[2] .. "/" .. ev[3] .. " DOWN")
table.insert(warnings, tostring(ev[4]))
end
if ev[1] == "k.timer" then
if gpu then
gpu.setForeground(0x000000)
if w < stepCount then
local n = math.floor((w / stepCount) * 255)
gpu.setBackground((n * 0x10000) + (n * 0x100) + n)
else
gpu.setBackground(0xFFFFFF)
end
basicDraw()
end
if steps[w] then
if steps[w] == "INJECT" then
local donkonit = neo.requestAccess("x.neo.sys.manage")
if not donkonit then
table.insert(warnings, "Settings not available for INJECT.")
else
local nextstepsA = {}
local nextstepsB = {}
for _, v in ipairs(neo.listApps()) do
if donkonit.getSetting("run." .. v) == "yes" then
if v:sub(1, 4) == "sys-" then
table.insert(nextstepsA, v)
else
table.insert(nextstepsB, v)
end
end
end
for _, v in ipairs(nextstepsB) do
table.insert(steps, w + 1, v)
end
for _, v in ipairs(nextstepsA) do
table.insert(steps, w + 1, v)
end
end
elseif steps[w] == "WAIT" then
else
local v, err = neo.executeAsync(steps[w])
if not v then
table.insert(warnings, steps[w] .. " STF")
table.insert(warnings, err)
end
end
else
break
end
w = w + 1
neo.scheduleTimer(os.uptime() + 0.049)
end
end
end
-- Actual sequence
if callerPkg ~= nil then
-- Everest can call into this to force a login screen
-- In this case it locks Everest, then starts Bristol.
--
if callerPkg ~= "sys-everest" then
return
end
screen = callerScr
-- Skip to "System initialized" (Everest either logged off, or died & used a Saving Throw to restart)
else
initializeSystem()
end
-- System initialized
if finalPrompt() then
-- Safe Mode
basicDraw()
local donkonit = neo.requestAccess("x.neo.sys.manage")
if donkonit then
gpu.set(2, 4, "Rebooting for Safe Mode...")
for _, v in ipairs(donkonit.listSettings()) do
if v ~= "password" then
donkonit.delSetting(v)
end
end
else
-- assume sysconf.lua did something very bad
gpu.set(2, 4, "No Donkonit. Wiping configuration completely.")
local fs = neo.requestAccess("c.filesystem")
fs.primary.remove("/data/sys-donkonit/sysconf.lua")
end
-- Do not give anything a chance to alter the new configuration
shutdownEmergency(true)
return
end
postPrompt()

667
code/init.lua Normal file
View File

@ -0,0 +1,667 @@
-- KittenOS N.E.O Kernel: "Tell Mettaton I said hi."
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- NOTE: If it's not "local", it's because k.root ought to access it.
-- List of things that apply:
-- primaryDisk, timers, libraries, processes, accesses, wrapMeta,
-- distEvent, baseProcEnv, loadLibraryInner, retrieveAccess,
-- start
-- Debugging option, turns process errors into actual errors (!)
local criticalFailure = false
-- In case of OpenComputers configuration abnormality
local readBufSize = 2048
-- A function used for logging, usable by programs.
-- Comment this out if you don't want programs to have
-- access to ocemu's logger.
local emergencyFunction
local ocemu = component.list("ocemu", true)()
if ocemu then
ocemu = component.proxy(ocemu)
emergencyFunction = ocemu.log
end
primaryDisk = component.proxy(computer.getBootAddress())
timers = {}
libraries = {}
setmetatable(libraries, {
__mode = "v"
})
-- proc.co = coroutine.create(appfunc)
-- proc.pkg = "pkg"
-- proc.access = {["perm"] = true, ...}
-- proc.denied = {["perm"] = true, ...}
-- proc.deathcbs = {function(), ...}
-- very slightly adjusted total CPU time
-- proc.cpuUsage
processes = {}
-- Maps registration-accesses to function(pkg, pid)
accesses = {}
local lastPID = 0
-- keys: <any>
-- sr.waiting: keys are PIDs, values are just "true"
-- sr.service: function()
-- sr.result: boolean
local outstandingSR = {}
-- Kernel global "idle time" counter, useful for accurate performance data
local idleTime = 0
-- This function is critical to wide text support.
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
-- The issue with the above function, of course, is that in practice the GPU is a weird mess.
-- So this undoes the above transformation for feeding to gpu.set.
-- (In practice if safeTextFormat supports RTL, and that's a big "if", then this will not undo that.
-- The point is that this converts it into gpu.set format.)
function unicode.undoSafeTextFormat(s)
local res = ""
local ignoreNext = false
for i = 1, unicode.len(s) do
if not ignoreNext then
local ch = unicode.sub(s, i, i)
ignoreNext = unicode.charWidth(ch) ~= 1
res = res .. ch
else
ignoreNext = false
end
end
return res
end
local function loadfile(s, e)
local h = primaryDisk.open(s)
if h then
local ch = ""
local c = primaryDisk.read(h, readBufSize)
while c do
ch = ch .. c
c = primaryDisk.read(h, readBufSize)
end
primaryDisk.close(h)
return load(ch, "=" .. s, "t", e)
end
return nil, "File Unreadable"
end
local wrapMeta = nil
local uniqueNEOProtectionObject = {}
function wrapMeta(t)
if type(t) == "table" then
local t2 = {}
setmetatable(t2, {
__index = function (a, k) return wrapMeta(t[k]) end,
__newindex = function (a, k, v) end,
__pairs = function (a)
return function (x, key)
local k, v = next(t, k)
if k then return k, wrapMeta(v) end
end, {}, nil
end,
__ipairs = function (a)
return function (x, key)
key = key + 1
if t[key] then
return key, wrapMeta(t[key])
end
end, {}, 0
end,
__metatable = uniqueNEOProtectionObject
-- Don't protect this table - it'll make things worse
})
return t2
else
return t
end
end
local function ensureType(a, t)
if type(a) ~= t then error("Invalid parameter, expected a " .. t) end
if t == "table" then
if getmetatable(a) then error("Invalid parameter, has metatable") end
end
end
local function ensurePathComponent(s)
if not s:match("^[a-zA-Z0-9_%-%+%,%#%~%@%'%;%[%]%(%)%&%%%$%! %=%{%}%^]+") then error("chars disallowed") end
if s == "." then error("single dot disallowed") end
if s == ".." then error("double dot disallowed") end
end
local function ensurePath(s, r)
-- Filter filename for anything "worrying". Note / is allowed, see further filters
if not s:match("^[a-zA-Z0-9_%-%+%,%#%~%@%'%;%[%]%(%)%&%%%$%! %=%{%}%^%/]+") then error("chars disallowed") end
if s:sub(1, r:len()) ~= r then error("base disallowed") end
if s:match("//") then error("// disallowed") end
if s:match("^%.%./") then error("../ disallowed") end
if s:match("/%.%./") then error("/../ disallowed") end
if s:match("/%.%.$") then error("/.. disallowed") end
if s:match("^%./") then error("./ disallowed") end
if s:match("/%./") then error("/./ disallowed") end
if s:match("/%.$") then error("/. disallowed") end
end
local wrapMath = wrapMeta(math)
local wrapTable = wrapMeta(table)
local wrapString = wrapMeta(string)
local wrapUnicode = wrapMeta(unicode)
local wrapCoroutine = wrapMeta(coroutine)
local wrapOs = wrapMeta({
totalMemory = computer.totalMemory, freeMemory = computer.freeMemory,
energy = computer.energy, maxEnergy = computer.maxEnergy,
clock = os.clock, date = os.date, difftime = os.difftime,
time = os.time, uptime = computer.uptime
})
local distEvent = nil
-- Use with extreme care.
-- (A process killing itself will actually survive until the next yield... before any of the death events have run.)
local function termProc(pid, reason)
if processes[pid] then
-- Immediately prepare for GC, it's possible this is out of memory.
-- If out of memory, then to reduce risk of memory leak by error, memory needs to be freed ASAP.
-- Start by getting rid of all process data.
local dcbs = processes[pid].deathcbs
local pkg = processes[pid].pkg
local usage = processes[pid].cpuUsage
processes[pid] = nil
-- This gets rid of a few more bits of data.
for _, v in ipairs(dcbs) do
v()
end
-- This finishes off that.
dcbs = nil
if reason and criticalFailure then
error(tostring(reason)) -- This is a debugging aid to give development work an easy-to-get-at outlet. Icecap is for most cases
end
if reason and emergencyFunction then
emergencyFunction("d1 " .. pkg .. "/" .. pid)
emergencyFunction("d2 " .. reason)
end
-- And this is why it's important, because this generates timers.
-- The important targets of these timers will delete even more data.
distEvent(nil, "k.procdie", pkg, pid, reason, usage)
end
end
local function execEvent(k, ...)
if processes[k] then
local v = processes[k]
local timerA = computer.uptime()
local r, reason = coroutine.resume(v.co, ...)
-- Mostly reliable accounting
v.cpuUsage = v.cpuUsage + (computer.uptime() - timerA)
local dead = not r
local hasReason = dead
if not dead then
if coroutine.status(v.co) == "dead" then
dead = true
end
end
if dead then
if hasReason then
reason = tostring(reason)
else
reason = nil
end
termProc(k, reason)
return hasReason
end
end
end
function distEvent(pid, s, ...)
local ev = {...}
if pid then
local v = processes[pid]
if not v then
return
end
if (not v.access["s." .. s]) or v.access["k.root"] then
return
end
-- Schedule a timer for "now"
table.insert(timers, {computer.uptime(), function ()
return execEvent(pid, s, table.unpack(ev))
end})
return
end
for k, v in pairs(processes) do
distEvent(k, s, ...)
end
end
local loadLibraryInner = nil
function baseProcEnv()
return {math = wrapMath,
table = wrapTable,
string = wrapString,
unicode = wrapUnicode,
coroutine = wrapCoroutine,
os = wrapOs,
-- Note raw-methods are gone - these can interfere with the metatable safeties.
require = loadLibraryInner,
assert = assert, ipairs = ipairs,
load = load, next = next,
pairs = pairs, pcall = pcall,
xpcall = xpcall, select = select,
type = type, error = error,
tonumber = tonumber, tostring = tostring,
setmetatable = setmetatable, getmetatable = function (n)
local mt = getmetatable(n)
if mt == uniqueNEOProtectionObject then return "NEO-Protected Object" end
return mt
end,
rawset = function (t, i, v)
local mt = getmetatable(n)
if mt == uniqueNEOProtectionObject then error("NEO-Protected Object") end
return rawset(t, i, v)
end, rawget = rawget, rawlen = rawlen, rawequal = rawequal,
neo = {
emergency = emergencyFunction,
readBufSize = readBufSize,
wrapMeta = wrapMeta,
listProcs = function ()
local n = {}
for k, v in pairs(processes) do
table.insert(n, {k, v.pkg, v.cpuUsage})
end
return n
end,
listApps = function ()
local n = primaryDisk.list("apps/")
local n2 = {}
for k, v in ipairs(n) do
if v:sub(#v - 3) == ".lua" then
table.insert(n2, v:sub(1, #v - 4))
end
end
return n2
end,
listLibs = function ()
local n = primaryDisk.list("libs/")
local n2 = {}
for k, v in ipairs(n) do
if v:sub(#v - 3) == ".lua" then
table.insert(n2, v:sub(1, #v - 4))
end
end
return n2
end,
totalIdleTime = function () return idleTime end,
ensurePath = ensurePath,
ensurePathComponent = ensurePathComponent,
}
}
end
function loadLibraryInner(library)
ensureType(library, "string")
library = "libs/" .. library .. ".lua"
ensurePath(library, "libs/")
if libraries[library] then return libraries[library] end
local l, r = loadfile(library, baseProcEnv())
if l then
local ok, al = pcall(l)
if ok then
libraries[library] = al
return al
else
return nil, al
end
end
return nil, r
end
function retrieveAccess(perm, pkg, pid)
-- Return the access lib and the death callback.
-- Access categories are sorted into:
-- "c.<hw>": Component
-- "s.<event>": Signal receiver (with responsibilities for Security Request watchers)
-- "s.k.<...>": Kernel stuff
-- "s.k.securityrequest": !!! HAS SIDE EFFECTS !!!
-- "s.h.<...>": Incoming HW messages
-- "s.x.<endpoint>": This access is actually useless on it's own - it is given by x.<endpoint>
-- "k.<x>": Kernel
-- "k.root": _ENV (holy grail)
-- "k.computer": computer
-- "r.<endpoint>": Registration Of Service...
-- "x.<endpoint>": Access Of Service (handled by r. & accesses table)
if accesses[perm] then
return accesses[perm](pkg, pid)
end
if perm == "k.root" then
return _ENV
end
if perm == "k.computer" then
return wrapMeta(computer)
end
if perm == "k.kill" then
return function(npid)
ensureType(npid, "number")
termProc(npid, "Killed by " .. pkg .. "/" .. pid)
end
end
if perm:sub(1, 2) == "s." then
-- This is more of a "return success". Signal access is determined by the access/denied maps.
return true
end
if perm:sub(1, 2) == "c." then
-- 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 primary = nil
local temporary = nil
local t = perm:sub(3)
if t == "filesystem" then
primary = primaryDisk
temporary = component.proxy(computer.tmpAddress())
end
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,
temporary = temporary
}
end
if perm:sub(1, 2) == "r." then
local uid = "x" .. perm:sub(2)
local sid = "s.x" .. perm:sub(2)
if accesses[uid] then return nil end
accesses[uid] = function (pkg, pid)
return nil
end
return function (f)
-- Registration function
ensureType(f, "function")
local accessObjectCache = {}
accesses[uid] = function(pkg, pid)
-- Basically, a per registration per process cache.
-- This is a consistent yet flexible behavior.
if accessObjectCache[pid] then
return accessObjectCache[pid]
end
processes[pid].access[sid] = true
local ok, a = pcall(f, pkg, pid, function (...)
distEvent(pid, uid, ...)
end)
if ok then
accessObjectCache[pid] = a
return a, function ()
accessObjectCache[pid] = nil
end
end
-- returns nil and fails
end
-- Announce registration
distEvent(nil, "k.registration", uid)
end, function ()
-- Registration becomes null (access is held but other processes cannot retrieve object)
if accesses[uid] then
distEvent(nil, "k.deregistration", uid)
end
accesses[uid] = nil
end
end
end
local start = nil
function start(pkg, ...)
ensureType(pkg, "string")
ensurePathComponent(pkg .. ".lua")
local args = {...}
local proc = {}
local pid = lastPID
lastPID = lastPID + 1
local function startFromUser(ipkg, ...)
-- VERY specific injunction here:
-- non "sys-" apps NEVER start "sys-" apps
-- This is part of the "default security policy" below:
-- sys- has all access
-- anything else has none
if ipkg:sub(1, 4) == "sys-" then
if pkg:sub(1, 4) ~= "sys-" then
return nil, "non-sys app trying to start sys app"
end
end
return start(ipkg, pkg, pid, ...)
end
local function osExecuteCore(handler, ...)
local pid, err = startFromUser(...)
while pid do
local sig = {coroutine.yield()}
if sig[1] == "k.procdie" then
if sig[3] == pid then
return 0, sig[4]
end
end
end
return -1, err
end
local requestAccessAsync = function (perm)
ensureType(perm, "string")
-- Safety-checked, prepare security event.
local req = {}
req.waiting = {}
req.service = function ()
if processes[pid] then
local n = nil
local n2 = nil
if req.result then
proc.access[perm] = true
n, n2 = retrieveAccess(perm, pkg, pid)
if n2 then
table.insert(processes[pid].deathcbs, n2)
end
else
proc.denied[perm] = true
end
distEvent(pid, "k.securityresponse", perm, n)
end
end
req.result = (not proc.denied[perm]) or proc.access["k.root"]
if proc.access["k.root"] or proc.access[perm] or proc.denied[perm] then
-- Use cached result to prevent possible unintentional security service spam
req.service()
return
end
-- Anything with s.k.securityrequest access has the response function and thus the vote,
-- but can't vote on itself for obvious reasons. Kernel judge is a fallback.
local shouldKernelJudge = true
for k, v in pairs(processes) do
if v.access["s.k.securityrequest"] then
shouldKernelJudge = false
if k ~= pid then
req.waiting[k] = true
distEvent(k, "k.securityrequest", pkg, pid, perm, function (r)
ensureType(r, "boolean")
if not r then
req.result = false
end
req.waiting[k] = nil
end)
end
end
end
if shouldKernelJudge then
-- Rather restrictive, but an important safety measure
req.result = pkg:sub(1, 4) == "sys-"
req.service()
else
table.insert(outstandingSR, req)
end
end
local env = baseProcEnv()
env.neo.pid = pid
env.neo.executeAsync = startFromUser
env.neo.execute = function (...)
return osExecuteCore(function () end, ...)
end
env.neo.executeExt = osExecuteCore
env.neo.requestAccessAsync = requestAccessAsync
env.neo.requestAccess = function (perm, handler)
requestAccessAsync(perm)
if not handler then handler = function() end end
while true do
local n = {coroutine.yield()}
if n[1] == "k.securityresponse" then
-- Security response - if it involves the permission, then take it
if n[2] == perm then return n[3] end
end
handler(table.unpack(n))
end
end
env.neo.scheduleTimer = function (time)
ensureType(time, "number")
local tag = {}
table.insert(timers, {time, function(ofs)
return execEvent(pid, "k.timer", tag, time, ofs)
end})
return tag
end
local appfunc, r = loadfile("apps/" .. pkg .. ".lua", env)
if not appfunc then
return nil, r
end
proc.co = coroutine.create(appfunc)
proc.pkg = pkg
proc.access = {
-- These permissions are the "critical set".
["s.k.securityresponse"] = true,
["s.k.timer"] = true,
["s.k.procnew"] = true,
["s.k.procdie"] = true,
-- Used when a registration is updated, in particular, as this signifies "readiness"
["s.k.registration"] = true,
}
proc.denied = {}
proc.deathcbs = {}
proc.cpuUsage = 0
-- Note the target process doesn't get the procnew (it's executed after it's creation)
pcall(distEvent, nil, "k.procnew", pkg, pid)
processes[pid] = proc
-- For processes waiting on others, this at least tries to guarantee some safety.
if criticalFailure then
execEvent(pid, ...)
else
if not pcall(execEvent, pid, ...) then
return nil, "neocore"
end
end
return pid
end
-- Main Scheduling Loop --
local function processSRs()
local didAnything = false
for k, v in pairs(outstandingSR) do
-- Outstanding security request handler.
local actualWaitingCount = 0
for k2, _ in pairs(v.waiting) do
if not processes[k2] then
v.waiting[k2] = nil
v.result = false
else
actualWaitingCount = actualWaitingCount + 1
end
end
if actualWaitingCount == 0 then
-- Service the SR
outstandingSR[k].service()
outstandingSR[k] = nil
didAnything = true
end
end
return didAnything
end
-- The actual loop & initialization
if not start("sys-init") then error("Could not start sys-init") end
while true do
local tmr = nil
for i = 1, 16 do
tmr = nil
local now = computer.uptime()
local breaking = false -- Used when a process dies - in this case it's assumed OC just did something drastic
local didAnything = false
local k = 1
while timers[k] do
local v = timers[k]
if v[1] <= now then
if v[2](now - v[1]) then
breaking = true
tmr = 0.05
break
end
didAnything = true
v = nil
else
if not tmr then
tmr = v[1]
else
tmr = math.min(tmr, v[1])
end
end
if v then
k = k + 1
else
table.remove(timers, k)
end
end
if breaking then break end
didAnything = didAnything or processSRs()
-- If the system didn't make any progress, then we're waiting for a signal (this includes timers)
if not didAnything then break end
end
now = computer.uptime() -- the above probably took a while
local dist = nil
if tmr then
dist = tmr - now
if dist < 0.05 then dist = 0.05 end
end
local signal = {computer.pullSignal(dist)}
idleTime = idleTime + (computer.uptime() - now)
if signal[1] then
distEvent(nil, "h." .. signal[1], select(2, table.unpack(signal)))
end
end

104
code/libs/event.lua Normal file
View File

@ -0,0 +1,104 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- event: Implements pull/listen semantics in a consistent way for a given process.
-- This is similar in API to OpenOS's event framework, but is per-process.
-- To make this work, a function is shared.
-- This function needs access to the caller's NEO in order to ensure that NEO system functions are covered.
-- This can do less checks than usual as it only affects the caller.
return function (neo)
local listeners = {}
local translations = {}
local timers = {}
local oldRA = neo.requestAccess
local function doPush(tp, tag, ...)
if tp == "k.timer" then
local f = timers[tag]
timers[tag] = nil
if f then
f(...)
end
end
local listeners2 = {}
for k, _ in pairs(listeners) do
table.insert(listeners2, k)
end
for _, v in ipairs(listeners2) do
v(tp, tag, ...)
end
end
neo.requestAccess = function (perm, handler)
return oldRA(perm, function (...)
doPush(...)
if handler then
handler(...)
end
end)
end
local function doPull()
local ev = {coroutine.yield()}
doPush(table.unpack(ev))
return ev
end
return {
listen = function (p1, p2)
if type(p2) == "function" then
local t = function (...)
local evn = ...
if evn == p1 then
p2(...)
end
end
translations[p2] = t
listeners[t] = true
else
listeners[p1] = true
end
end,
ignore = function (func)
if translations[func] then
listeners[translations[func]] = nil
translations[func] = nil
end
listeners[func] = nil
end,
-- Arguments are filtering.
-- For example, to filter for timers with a given tag, use pull("k.timer", tag)
-- Note the explicit discouragement of timeout-pulls. Use runAt or the complex form of sleep.
pull = function (...)
local filter = {...}
while true do
local ev = doPull()
local err = false
for i = 1, #filter do
err = err or (filter[i] ~= ev[i])
end
if not err then return table.unpack(ev) end
end
end,
-- Run a function at a specified uptime.
runAt = function (time, func)
timers[neo.scheduleTimer(time)] = func
end,
-- Sleeps until a time (unless time is nil, in which case sleeps forever), but can be woken up before then.
-- This allows using an async API as a synchronous one with optional time-to-failure.
sleepTo = function (time, wakeUpPoll)
local timerHit = false
local oWUP = wakeUpPoll
wakeUpPoll = function ()
if oWUP then
return timerHit or (oWUP())
end
return timerHit
end
if time then
timers[neo.scheduleTimer(time)] = function () timerHit = true end
end
while true do
local ev = doPull()
if wakeUpPoll() then return end
end
end
}
end

406
code/libs/neoux.lua Normal file
View File

@ -0,0 +1,406 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- neoux: Implements utilities on top of Everest & event:
-- Everest crash protection
return function (event, neo)
-- this is why neo access is 'needed'
local function retrieveIcecap()
return neo.requestAccess("x.neo.pub.base")
end
local function retrieveEverest()
return neo.requestAccess("x.neo.pub.window")
end
-- id -> {lclEv, w, h, title, callback}
local windows = {}
local lclEvToW = {}
retrieveEverest()
local function everestDied()
for _, v in pairs(windows) do
v[1] = nil
end
lclEvToW = {}
end
local function pushWindowToEverest(k)
local everest = retrieveEverest()
if not everest then
everestDied()
return
end
local v = windows[k]
local r, res = pcall(everest, v[2], v[3], v[4])
if not r then
everestDied()
return
else
-- res is the window!
lclEvToW[res.id] = k
windows[k][1] = res
end
end
event.listen("k.registration", function (_, xe)
if #windows > 0 then
if xe == "x.neo.pub.window" then
for k, v in pairs(windows) do
pushWindowToEverest(k)
end
end
end
end)
event.listen("k.deregistration", function (_, xe)
if xe == "x.neo.pub.window" then
everestDied()
end
end)
event.listen("x.neo.pub.window", function (_, window, tp, ...)
if lclEvToW[window] then
windows[lclEvToW[window]][5](tp, ...)
end
end)
local neoux = {}
neoux.fileDialog = function (forWrite, callback)
local sync = false
local rtt = nil
if not callback then
sync = true
callback = function (rt)
sync = false
rtt = rt
end
end
local tag = retrieveIcecap().showFileDialogAsync(forWrite)
local f
f = function (_, fd, tg, re)
if fd == "filedialog" then
if tg == tag then
callback(re)
event.ignore(f)
end
end
end
event.listen("x.neo.pub.base", f)
while sync do
event.pull()
end
return rtt
end
-- Creates a wrapper around a window.
neoux.create = function (w, h, title, callback)
local window = {}
local windowCore = {nil, w, h, title, function (...) callback(window, ...) end}
local k = #windows + 1
table.insert(windows, windowCore)
pushWindowToEverest(k)
window.reset = function (w, h, cb)
callback = cb
if mw or nh then
windowCore[2] = nw
windowCore[3] = nh
end
if windowCore[1] then
windowCore[1].setSize(windowCore[2], windowCore[3])
end
end
window.getSize = function ()
return windowCore[2], windowCore[3]
end
window.setSize = function (w, h)
windowCore[2] = w
windowCore[3] = h
if windowCore[1] then
windowCore[1].setSize(w, h)
end
end
window.span = function (x, y, text, bg, fg)
if windowCore[1] then
pcall(windowCore[1].span, x, y, text, bg, fg)
end
end
window.close = function ()
if windowCore[1] then
windowCore[1].close()
lclEvToW[windowCore[1].id] = nil
windowCore[1] = nil
end
windows[k] = nil
end
return window
end
-- Padding function
neoux.pad = function (t, len, centre, cut)
local l = unicode.len(t)
local add = len - l
if add > 0 then
if centre then
t = (" "):rep(math.floor(add / 2)) .. t .. (" "):rep(math.ceil(add / 2))
else
t = t .. (" "):rep(add)
end
end
if cut then
t = unicode.sub(t, 1, len)
end
return t
end
-- Text dialog formatting function.
-- Assumes you've run unicode.safeTextFormat if need be
neoux.fmtText = function (text, w)
local nl = text:find("\n")
if nl then
local base = text:sub(1, nl - 1)
local ext = text:sub(nl + 1)
local baseT = neoux.fmtText(base, w)
local extT = neoux.fmtText(ext, w)
for _, v in ipairs(extT) do
table.insert(baseT, v)
end
return baseT
end
if unicode.len(text) > w then
local lastSpace
for i = 1, w do
if unicode.sub(text, i, i) == " " then
-- Check this isn't an inserted space (unicode safe text format)
local ok = true
if i > 1 then
if unicode.charWidth(unicode.sub(text, i - 1, i - 1)) ~= 1 then
ok = false
end
end
if ok then
lastSpace = i
end
end
end
local baseText, extText
if not lastSpace then
-- Break at a 1-earlier boundary
local wEffect = w
if unicode.charWidth(unicode.sub(text, w, w)) ~= 1 then
-- Guaranteed to be safe, so
wEffect = wEffect - 1
end
baseText = unicode.sub(text, 1, wEffect)
extText = unicode.sub(text, wEffect + 1)
else
baseText = unicode.sub(text, 1, lastSpace - 1)
extText = unicode.sub(text, lastSpace + 1)
end
local full = neoux.fmtText(extText, w)
table.insert(full, 1, neoux.pad(baseText, w))
return full
end
return {neoux.pad(text, w)}
end
-- UI FRAMEWORK --
neoux.tcwindow = function (w, h, controls, closing, bg, fg)
local selIndex = #controls
if #controls == 0 then
selIndex = 1
end
local function rotateSelIndex()
local original = selIndex
while true do
selIndex = selIndex + 1
if not controls[selIndex] then
selIndex = 1
end
if controls[selIndex] then
if controls[selIndex].selectable then
return
end
end
if selIndex == original then
return
end
end
end
rotateSelIndex()
local function doLine(window, a)
window.span(1, a, (" "):rep(w), bg, fg)
for k, v in ipairs(controls) do
if a >= v.y then
if a < (v.y + v.h) then
v.line(window, v.x, a, (a - v.y) + 1, bg, fg, selIndex == k)
end
end
end
end
local function doZone(window, control, cache)
for i = 1, control.h do
local l = i + control.y - 1
if (not cache) or (not cache[l]) then
doLine(window, l)
if cache then cache[l] = true end
end
end
end
return function (window, ev, a, b, c)
if ev == "touch" then
local found = nil
for k, v in ipairs(controls) do
if v.selectable then
if a >= v.x then
if a < (v.x + v.w) then
if b >= v.y then
if b < (v.y + v.h) then
found = k
break
end
end
end
end
end
end
if found then
local c1 = controls[selIndex]
selIndex = found
local c2 = controls[selIndex]
local cache = {}
if c1 then doZone(window, c1, cache) end
if c2 then
doZone(window, c2, cache)
if c2.touch then
c2.touch(window, function () doZone(window, c2) end, (a - c2.x) + 1, (b - c2.y) + 1)
end
end
end
end
if ev == "drag" then
if controls[selIndex] then
if controls[selIndex].drag then
controls[selIndex].drag(window, function () doZone(window, controls[selIndex]) end, (a - controls[selIndex].x) + 1, (b - controls[selIndex].y) + 1)
end
end
end
if ev == "key" then
if a == 9 then
if c then
local c1 = controls[selIndex]
rotateSelIndex()
local c2 = controls[selIndex]
local cache = {}
if c1 then doZone(window, c1, cache) end
if c2 then doZone(window, c2, cache) end
end
else
if controls[selIndex] then
if controls[selIndex].key then
controls[selIndex].key(window, function () doZone(window, controls[selIndex]) end, a, b, c)
end
end
end
end
if ev == "line" then
doLine(window, a)
end
if ev == "close" then
closing(window)
end
end, doZone
end
neoux.tcrawview = function (x, y, lines)
return {
x = x,
y = y,
w = unicode.len(lines[1]),
h = #lines,
selectable = false,
line = function (window, x, y, lined, bg, fg, selected)
-- Can't be selected normally so ignore that flag
window.span(x, y, lines[lined], bg, fg)
end
}
end
neoux.tchdivider = function (x, y, w)
return neoux.tcrawview(x, y, {("-"):rep(w)})
end
neoux.tcvdivider = function (x, y, h)
local n = {}
for i = 1, h do
n[i] = "|"
end
return neoux.tcrawview(x, y, n)
end
neoux.tcbutton = function (x, y, text, callback)
text = "<" .. text .. ">"
return {
x = x,
y = y,
w = unicode.len(text),
h = 1,
selectable = true,
key = function (window, update, a, c, d)
if d then
if a == 13 or a == 32 then
callback(window)
end
end
end,
touch = function (window, update, x, y)
callback(window)
end,
line = function (window, x, y, lind, bg, fg, selected)
local fg1 = fg
if selected then
fg = bg
bg = fg1
end
window.span(x, y, text, bg, fg)
end
}
end
-- Note: w should be at least 2 - this is similar to buttons.
neoux.tcfield = function (x, y, w, textprop)
return {
x = x,
y = y,
w = w,
h = 1,
selectable = true,
key = function (window, update, a, c, d)
if d then
if a == 13 then
elseif a == 8 then
local str = textprop()
textprop(unicode.sub(str, 1, unicode.len(str) - 1))
update()
elseif a ~= 0 then
textprop(textprop() .. unicode.char(a))
update()
end
end
end,
line = function (window, x, y, lind, bg, fg, selected)
local fg1 = fg
if selected then
fg = bg
bg = fg1
end
local text = unicode.safeTextFormat(textprop())
local txl = unicode.len(text)
local start = math.max(1, (txl - (w - 2)) + 1)
text = "[" .. neoux.pad(unicode.sub(text, start, start + (w - 3)), w - 2, false, true) .. "]"
window.span(x, y, text, bg, fg)
end
}
end
neoux.startDialog = function (fmt, title, wait)
fmt = neoux.fmtText(unicode.safeTextFormat(fmt), 20)
neoux.create(20, #fmt, title, function (window, ev, a, b, c)
if ev == "line" then
window.span(1, a, fmt[a], 0xFFFFFF, 0)
end
if ev == "close" then
window.close()
wait = nil
end
end)
while wait do
event.pull()
end
end
return neoux
end

28
code/libs/serial.lua Normal file
View File

@ -0,0 +1,28 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
local doSerialize = nil
function doSerialize(s)
if type(s) == "table" then
local str = "{\n"
for k, v in pairs(s) do
str = str .. "[" .. doSerialize(k) .. "]=" .. doSerialize(v) .. ",\n"
end
return str .. "}"
end
if type(s) == "string" then
return string.format("%q", s)
end
if type(s) == "number" then
return tostring(s)
end
error("Cannot serialize " .. type(s))
end
return neo.wrapMeta({
serialize = function (x) return "return " .. doSerialize(x) end,
deserialize = function (s)
local r1, r2 = pcall(function() return load(s, "=serial", "t", {})() end)
if r1 then
return r2
end
end
})

View File

@ -0,0 +1,159 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- just don't bother with proper indent here
return function (event, neoux, retFunc, fs, pkg, mode)
local class = "manage"
if mode ~= nil then
if mode then
class = "save"
else
class = "load"
end
end
local prepareNode
local ccb = nil
local function cb(...)
local res, e = pcall(ccb, ...)
if not res then
prepareNode({
name = "F.M. Error",
list = function ()
local l = {}
for k, v in ipairs(neoux.fmtText(unicode.safeTextFormat(e), 25)) do
l[k] = {v, function () return true end}
end
return l
end,
unknownAvailable = false,
selectUnknown = function (text) end
})
end
end
local function prepareNodeI(node)
local l = node.list()
local w, h = 30, 8
-- Local State
-- Selection. Having this equal to #l + 1 means typing area ('unknown')
local selection = 1
local unknownTx = ""
--
local function format(a)
if a <= 1 then
return true, true, node.name
end
local camY = math.max(1, selection - 4)
local idx = a + camY - 2
if node.unknownAvailable then
if idx == #l + 1 then
return selection == #l + 1, false, ":" .. unknownTx
end
end
if l[idx] then
return selection == idx, false, l[idx][1]
end
return true, true, "~~~"
end
local function updateLine(wnd, a)
local colA, colB = 0xFFFFFF, 0
local sel, cen, text = format(a)
if sel then
colB, colA = 0xFFFFFF, 0
end
wnd.span(1, a, neoux.pad(unicode.safeTextFormat(text), w, cen, true), colA, colB)
end
local function flush(wnd)
for i = 1, h do
updateLine(wnd, i)
end
end
local function key(wnd, ka, kc, down)
if not down then return end
if (ka == 9) or (kc == 208) then
local lo = selection
selection = selection + 1
local max = #l
if node.unknownAvailable then
max = max + 1
end
if selection > max then
selection = 1
end
flush(wnd)
return
end
if kc == 200 then
local lo = selection
selection = selection - 1
local max = #l
if node.unknownAvailable then
max = max + 1
end
if selection == 0 then
selection = max
end
flush(wnd)
return
end
if ka == 13 then
local aResult, res
if selection ~= #l + 1 then
aResult, res = l[selection][2]()
else
aResult, res = node.selectUnknown(unknownTx)
end
if aResult then
retFunc(res)
wnd.close()
else
prepareNode(res)
end
return
end
if selection == #l + 1 then
if ka == 8 then
unknownTx = unicode.sub(unknownTx, 1, unicode.len(unknownTx) - 1)
flush(wnd)
return
end
if ka ~= 0 then
unknownTx = unknownTx .. unicode.char(ka)
flush(wnd)
end
end
end
return w, h, function (wnd, evt, a, b, c)
if evt == "key" then
key(wnd, a, b, c)
end
if evt == "line" then
updateLine(wnd, a)
end
if evt == "close" then
retFunc(nil)
wnd.close()
end
end
end
local text = class .. " " .. pkg
local window = neoux.create(25, 10, text, cb)
function prepareNode(node)
local w, h, c = prepareNodeI(node)
ccb = c
window.setSize(w, h)
end
prepareNode(require("sys-filevfs")(fs, mode))
return function ()
retFunc()
window.close()
end
-- end bad indent
end

181
code/libs/sys-filevfs.lua Normal file
View File

@ -0,0 +1,181 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- Used by filedialog to provide a sane relative environment.
-- Essentially, the filedialog is just a 'thin' UI wrapper over this.
-- Returns the root node.
local function dialog(name, parent)
return {
name = name,
list = function () return {{"Back", function () return nil, parent end}} end,
unknownAvailable = false,
selectUnknown = function (text) end
}
end
local getFsNode, getRoot
local setupCopyNode
function setupCopyNode(parent, myRoot, op, complete)
local function handleResult(aRes, res)
if aRes then
return complete(res)
else
return nil, setupCopyNode(parent, res, op, complete)
end
end
return {
name = "(" .. op .. ") " .. myRoot.name,
list = function ()
local l = {}
table.insert(l, {"Cancel Operation: " .. op, function ()
return false, parent
end})
for _, v in ipairs(myRoot.list()) do
table.insert(l, {v[1], function ()
return handleResult(v[2]())
end})
end
return l
end,
unknownAvailable = myRoot.unknownAvailable,
selectUnknown = function (tx)
return handleResult(myRoot.selectUnknown(tx))
end
}
end
local function setupCopyVirtualEnvironment(fs, parent, fwrap)
if not fwrap then
return false, dialog("Could not open source", parent)
end
local myRoot = getRoot(fs, true)
-- Setup wrapping node
return setupCopyNode(parent, myRoot, "Copy", function (fwrap2)
if not fwrap2 then
return false, dialog("Could not open dest.", parent)
end
local data = fwrap.read(neo.readBufSize)
while data do
fwrap2.write(data)
data = fwrap.read(neo.readBufSize)
end
fwrap.close()
fwrap2.close()
return false, dialog("Completed copy.", parent)
end)
end
getFsNode = function (fs, parent, fsc, path, mode)
local va = fsc.address:sub(1, 4)
if path:sub(#path, #path) == "/" then
local t
local confirmedDel = false
t = {
name = "DIR : " .. va .. path,
list = function ()
local n = {}
n[1] = {"..", function ()
return nil, parent
end}
for k, v in ipairs(fsc.list(path)) do
local nm = "[F] " .. v
local fp = path .. v
if fsc.isDirectory(fp) then
nm = "[D] " .. v
end
n[k + 1] = {nm, function () return nil, getFsNode(fs, t, fsc, fp, mode) end}
end
local delText = "Delete"
if confirmedDel then
delText = "Delete <ARMED>"
end
n[#n + 1] = {delText, function ()
if not confirmedDel then
confirmedDel = true
return nil, t
end
fsc.remove(path)
return nil, dialog("Done.", parent)
end}
n[#n + 1] = {"Mk. Directory", function ()
return nil, {
name = "MKDIR...",
list = function () return {} end,
unknownAvailable = true,
selectUnknown = function (text)
fsc.makeDirectory(path .. text)
return nil, dialog("Done!", t)
end
}
end}
return n
end,
unknownAvailable = mode ~= nil,
selectUnknown = function (text)
return true, require("sys-filewrap")(fsc, path .. text, mode)
end
}
return t
end
return {
name = "FILE: " .. va .. path,
list = function ()
local n = {}
table.insert(n, {"Back", function ()
return nil, parent
end})
if mode ~= nil then
table.insert(n, {"Open", function ()
return true, require("sys-filewrap")(fsc, path, mode)
end})
end
table.insert(n, {"Copy", function ()
return nil, setupCopyVirtualEnvironment(fs, parent, require("sys-filewrap")(fsc, path, false))
end})
table.insert(n, {"Delete", function ()
fsc.remove(path)
return nil, dialog("Done.", parent)
end})
return n
end,
unknownAvailable = false,
selectUnknown = function (text) end
}
end
function getRoot(fs, mode)
local t
t = {
name = "DRVS:",
list = function ()
local l = {}
for fsi in fs.list() do
local id = fsi.getLabel()
if not id then
id = " Disk"
else
id = ":" .. id
end
if fsi == fs.primary then
id = "NEO" .. id
elseif fsi == fs.temporary then
id = "RAM" .. id
end
local used, total = fsi.spaceUsed(), fsi.spaceTotal()
local amount = string.format("%02i", math.ceil((used / total) * 100))
local mb = math.floor(total / (1024 * 1024))
if fsi.isReadOnly() then
id = amount .. "% RO " .. mb .. "M " .. id
else
id = amount .. "% RW " .. mb .. "M " .. id
end
table.insert(l, {fsi.address:sub(1, 4) .. ": " .. id, function ()
return nil, getFsNode(fs, t, fsi, "/", mode)
end})
end
return l
end,
unknownAvailable = false,
selectUnknown = function (text) end
}
return t
end
return getRoot

View File

@ -0,0 +1,42 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
return function(dev, file, mode)
local n = "rb"
if mode then n = "wb" end
local handle = dev.open(file, n)
local open = true
local function closer()
if not open then return end
open = false
pcall(function()
dev.close(handle)
end)
end
if not mode then
return {
close = closer,
read = function (len)
if len == "*a" then
local ch = ""
local c = dev.read(handle, neo.readBufSize)
while c do
ch = ch .. c
c = dev.read(handle, neo.readBufSize)
end
return ch
end
if type(len) ~= "number" then error("Length of read must be number or '*a'") end
return dev.read(handle, len)
end
}, closer
else
return {
close = closer,
write = function (txt)
if type(txt) ~= "string" then error("Write data must be string-bytearray") end
return dev.write(handle, txt)
end
}, closer
end
end

View File

@ -0,0 +1,90 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- CRITICAL FILE!
-- This file defines how your KittenOS NEO system responds to access requests.
-- Modification, renaming or deletion can disable security features.
-- Usually, a change that breaks the ability for the file to do it's job will cause the "failsafe" to activate,
-- and for the system to become unable to run user applications.
-- However - I would not like to test this in a situation where said user applications were in any way untrusted,
-- for example, if you downloaded them from the Internet, or in particular if someone forwarded them over Discord.
-- IRC is usually pretty safe, but no guarantees.
-- Returns "allow", "deny", or "ask".
local actualPolicy = function (pkg, pid, perm)
-- System stuff is allowed.
if pkg:sub(1, 4) == "sys-" then
return "allow"
end
-- <The following is for apps & services>
-- x.neo.pub (aka Icecap) is open to all
if perm:sub(1, 10) == "x.neo.pub." then
return "allow"
end
-- This is to ensure the prefix naming scheme is FOLLOWED!
-- sys- : System, part of KittenOS NEO and thus tries to present a "unified fragmented interface" in 'neo'
-- app- : Application - these can have ad-hoc relationships. It is EXPECTED these have a GUI
-- svc- : Service - Same as Application but with no expectation of desktop usability
-- Libraries "have no rights" as they are essentially loadable blobs of Lua code.
-- They have access via the calling program, and have a subset of the NEO Kernel API
local pfx = nil
if pkg:sub(1, 4) == "app-" then pfx = "app" end
if pkg:sub(1, 4) == "svc-" then pfx = "svc" end
if pfx then
-- Apps can register with their own name
if perm == "r." .. pfx .. "." .. pkg:sub(5) then
return "allow"
end
end
-- Userlevel has no other registration rights
if perm:sub(1, 2) == "r." then
return "deny"
end
-- app/svc stuff is world-accessible
if perm:sub(1, 6) == "x.app." then
return "allow"
end
if perm:sub(1, 6) == "x.svc." then
return "allow"
end
-- For hardware access, ASK!
return "ask"
end
return function (neoux, settings, pkg, pid, perm, rsp)
local res = actualPolicy(pkg, pid, perm)
if res == "ask" then
res = settings.getSetting("perm|" .. pkg .. "|" .. perm) or "ask"
end
if res == "ask" then
local fmt = neoux.fmtText(unicode.safeTextFormat(string.format("%s/%i wants:\n%s\nAllow this?", pkg, pid, perm)), 20)
local always = "Always"
local yes = "Yes"
local no = "No"
local totalW = (#yes) + (#always) + (#no) + 8
neoux.create(20, #fmt + 2, "security", neoux.tcwindow(20, #fmt + 3, {
neoux.tcbutton(1, #fmt + 2, no, function (w)
rsp(false)
w.close()
end),
neoux.tcbutton(totalW - ((#yes) + 1), #fmt + 2, yes, function (w)
rsp(true)
w.close()
end),
neoux.tcbutton((#yes) + 3, #fmt + 2, always, function (w)
settings.setSetting("perm|" .. pkg .. "|" .. perm, "allow")
rsp(true)
w.close()
end),
neoux.tchdivider(1, #fmt + 1, 21),
neoux.tcrawview(1, 1, fmt),
}, function (w)
rsp(false)
w.close()
end, 0xFFFFFF, 0))
else
rsp(res == "allow")
end
end

58
com2/bdivide.lua Normal file
View File

@ -0,0 +1,58 @@
-- BDIVIDE
-- format:
-- 0-127 for constants
-- <block + 128>, <(len - 3) * 2, + lowest bit is upper bit of position>, <position - 1>
io.write("\x00") -- initiation character
local blockCache = {}
local window = 0
local blockUse = 128
for i = 128, 128 + blockUse - 1 do
blockCache[i] = ("\x00"):rep(512)
end
local function runBlock(blk)
-- firstly, get current block index
local blockIndex = window + 128
window = (window + 1) % blockUse
blockCache[blockIndex] = ""
-- ok, now work on the problem
local i = 1
while i <= #blk do
local bestData = blk:sub(i, i)
local bestRes = bestData
local bestScore = 1
for bid = 128, 128 + blockUse - 1 do
for lm = 0, 127 do
local al = lm + 3
local pfx = blk:sub(i, i + al - 1)
if #pfx ~= al then
break
end
local p = blockCache[bid]:find(pfx, 1, true)
if not p then
break
end
local score = al / 3
if score > bestScore then
bestData = string.char(bid) .. string.char((lm * 2) + math.floor((p - 1) / 256)) .. string.char((p - 1) % 256)
bestRes = pfx
bestScore = score
end
end
end
-- ok, encode!
io.write(bestData)
blockCache[blockIndex] = blockCache[blockIndex] .. bestRes
i = i + #bestRes
end
end
while true do
local blkd = io.read(512)
runBlock(blkd)
if #blkd < 512 then
return
end
end

64
com2/bundiv.lua Normal file
View File

@ -0,0 +1,64 @@
local sector = io.write -- XX
-- BUNDIVIDE reference implementation for integration XX
local Cs,Cbu,Cb,Cw,Cp,Ci,CP,CB,CD={},128,"",128,""
CP=function(d,b,i)
i=1
while i<=#d do
b=d:byte(i)
i=i+1
if b==127 then
b=d:byte(i)
i=i+1
if b==127 then
b=d:byte(i)+254
i=i+1
else
b=b+127
end
end
Cp=Cp..string.char(b)
if #Cp==512 then
sector(Cp)
Cp=""
end
end
end
for i=128,127+Cbu do Cs[i]=("\x00"):rep(512) end
Cs[Cw]=""
CB=function(d,i,d2,x,y)
i=1
while i<=#d-2 do
b=d:byte(i)
d2=d:sub(i,i)
i=i+1
if not Ci then
if b==0then
Ci=1
end
else
if b>=128then
x=d:byte(i)i=i+1
y=(d:byte(i)+((x%2)*256))i=i+1
d2=Cs[b]:sub(y+1,y+3+math.floor(x/2))
end
Cs[Cw]=Cs[Cw]..d2
if #Cs[Cw]>=512then
CP(Cs[Cw])
Cw=((Cw-127)%Cbu)+128
Cs[Cw]=""
end
end
end
return i
end
CD=function(d)
Cb=Cb..d
Cb=Cb:sub(CB(Cb))
end
CD(io.read("*a")) -- XX
--D.remove("init-bdivide.lua")--
--D.rename("init.lua","init-bdivide.lua")--
--local Ch=D.open("init-bdivide.lua","rb")--
--dieCB=function()D.close(Ch)D.remove("init-bdivide.lua")end--
--while true do local t=D.read(Ch, 64)if not t then break end CD(t)end--
CD("\x00\x00")CP(Cs[Cw])

17
com2/preproc.lua Normal file
View File

@ -0,0 +1,17 @@
-- PREPROC: preprocess input
while true do
local c = io.read(1)
if not c then return end
local bc = c:byte()
if bc < 127 then
io.write(c)
else
if bc <= 253 then
-- 127(0) through 253(126)
io.write("\x7F" .. string.char(bc - 127))
else
-- 254(0) through 255 (1)
io.write("\x7F\x7F" .. string.char(bc - 254))
end
end
end

32
heroes.lua Normal file
View File

@ -0,0 +1,32 @@
-- SYSTEM HEROES.
-- Nabbed Sonic Heroes lyrics follow:
-- "What comes up, must come down... "
-- "Yet, my feet don't touch the ground..."
-- arg is the size of the code.tar file
local arg = ...
os.execute("lua com2/preproc.lua < code.tar | lua com2/bdivide.lua > com2/code.tar.bd")
os.execute("cat insthead.lua")
print("sC=" .. math.ceil(tonumber(arg) / 512))
local src = io.open("com2/bundiv.lua", "r")
while true do
local line = src:read()
if not line then
src:close()
io.write("--[[")
io.flush()
os.execute("cat com2/code.tar.bd")
io.write("]]")
return
end
local endix = line:sub(#line-1,#line)
if endix ~= "XX" then
if endix == "--" then
-- included
print(line:sub(3,#line-2))
else
print(line)
end
end
-- XX means ignored.
end

103
insthead.lua Normal file
View File

@ -0,0 +1,103 @@
-- KOSNEO inst.
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
local C, O, G, D = component, computer
local sa = C.list("screen", true)()
if sa then
G = C.list("gpu", true)()
if G then
G = C.proxy(G)
G.bind(sa)
G.setForeground(0xFFFFFF)
G.setBackground(0x000000)
G.setResolution(50, 5)
G.setDepth(1)
G.fill(1, 1, 50, 5, " ")
G.setBackground(0xFFFFFF)
G.setForeground(0x000000)
G.fill(1, 2, 50, 1, " ")
G.set(2, 2, "KittenOS NEO Installer")
end
end
D = C.proxy(O.getBootAddress())
local tF = nil
local tFN = "Starting..."
local tFSR = 0
local tW = 0
local convoct
convoct = function (oct)
local v = oct:byte(#oct) - 0x30
if #oct > 1 then
return (convoct(oct:sub(1, #oct - 1)) * 8) + v
end
return v
end
local function tA(s)
if tW > 0 then
tW = tW - 1
return
end
if tF then
local ta = math.min(512, tFSR)
D.write(tF, s:sub(1, ta))
tFSR = tFSR - ta
if tFSR == 0 then
D.close(tF)
tF = nil
end
else
tFN = s:sub(1, 100):gsub("\x00", "")
local sz = convoct(s:sub(125, 135))
if tFN:sub(1, 5) ~= "code/" then
tW = math.ceil(sz / 512)
else
tFN = tFN:sub(6)
if tFN == "" then
return
end
if tFN:sub(#tFN) == "/" then
tW = math.ceil(sz / 512)
D.makeDirectory(tFN)
else
tF = D.open(tFN, "wb")
tFSR = sz
if tFSR == 0 then
D.close(tF)
tF = nil
end
end
end
end
end
local dieCB = function () end
local sN = 0
local sC = 0
local function sector(n)
tA(n)
sN = sN + 1
if G then
local a = sN / sC
G.fill(1, 2, 50, 1, " ")
G.set(2, 2, "KittenOS NEO Installer : " .. tFN)
G.setForeground(0xFFFFFF)
G.setBackground(0x000000)
G.fill(2, 4, 48, 1, " ")
G.setBackground(0xFFFFFF)
G.setForeground(0x000000)
G.fill(2, 4, math.ceil(48 * a), 1, " ")
end
if sN % 8 == 0 then
O.pullSignal(0.05)
end
if sN == sC then
dieCB()
O.shutdown(true)
end
end

24
mkucinst.lua Normal file
View File

@ -0,0 +1,24 @@
os.execute("tar -cf code.tar code")
os.execute("cat insthead.lua > inst.lua")
local f = io.open("inst.lua", "ab")
local df = io.open("code.tar", "rb")
local sc = 0
while true do
local d = df:read(512)
if not d then break end
sc = sc + 1
end
df:close()
local df = io.open("code.tar", "rb")
f:write("sC = " .. sc .. "\n")
while true do
local d = df:read(512)
if d then
f:write(string.format("sector(%q)", d))
else
break
end
end
df:close()
f:close()

BIN
preSH.tar.gz Normal file

Binary file not shown.

48
repository/app-eeprog.lua Normal file
View File

@ -0,0 +1,48 @@
-- This is released into the public domain.
-- No warranty is provided, implied or otherwise.
-- app-eeprom: Tiny EEPROM flasher
-- Example of a tiny app a user could write relatively quickly if they have NEO system knowledge
-- Note the high amount of synchronous routines used here.
-- For a tiny app like this, it's fine, and KittenOS NEO makes sure it won't interfere.
-- (Plus, this isn't a library, so that's not a concern)
-- Really, in KittenOS NEO, the only program you break is your own
local event = require("event")(neo)
local neoux = require("neoux")(event, neo)
local eeprom = neo.requestAccess("c.eeprom")
if eeprom then
eeprom = eeprom.list()()
end
if not eeprom then
error("No EEPROM access")
end
neoux.startDialog("NOTE: If this program is used improperly, it can require EEPROM replacement.\nOnly use trusted EEPROMs.", "eeprom-flash", true)
local fd = neoux.fileDialog(false)
if not fd then return end
local eepromCode = fd.read("*a")
fd.close()
eeprom.set(eepromCode)
neoux.startDialog("The flash was successful - the next dialog can change the label.", "eeprom-flash", true)
-- text dialog
local done = false
neoux.create(20, 1, "label", neoux.tcwindow(20, 1, {
neoux.tcfield(1, 1, 20, function (nv)
if not nv then
return eeprom.getLabel()
end
eeprom.setLabel(nv)
end)
}, function (w)
w.close()
done = true
end, 0xFFFFFF, 0))
while not done do
event.pull()
end

3
repository/list Normal file
View File

@ -0,0 +1,3 @@
app app-eeprog
This flashes EEPROMs, an example of a package that does not come by default with KittenOS NEO.
end