OC-KittenOS/code/apps/sys-everest.lua

821 lines
22 KiB
Lua

-- 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 Glacier.
-- "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. The user logs in
-- 2. Bristol starts up Everest, and frees the primary monitor
-- 3. The primary monitor is claimed by Everest and becomes monitor 1
-- 4. After a small time, Bristol dies, unclaiming all monitors
-- 5. Everest claims the new monitors, and the desktop session begins
-- 6. Everest shuts down for some reason,
-- sys-init gets started UNLESS endSession(false) was used
local everestProvider = neo.requireAccess("r.neo.pub.window", "registering npw")
local everestSessionProvider = neo.requireAccess("r.neo.sys.session", "registering nsse")
-- 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.requireAccess("x.neo.sys.screens", "access to screens")
neo.requestAccess("s.h.clipboard")
neo.requestAccess("s.h.touch")
neo.requestAccess("s.h.drag")
neo.requestAccess("s.h.drop")
neo.requestAccess("s.h.scroll")
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 until monitors show up
monitors[0] = {nil, nil, 160, 50}
-- {monitor, x, y, w, h, callback}
-- callback events are:
-- key ka kc down
-- line y
local surfaces = {}
-- Last Interact Monitor
local lIM = 1
-- Stops the main loop
local shuttingDown = false
-- Also used for settings.
local savingThrow = neo.requestAccess("x.neo.sys.manage")
local function suggestAppsStop()
for k, v in ipairs(surfaces) do
for i = 1, 4 do
v[6]("close")
end
end
end
local function dying()
local primary = (monitors[1] or {})[2] or ""
for _, v in ipairs(monitors) do
pcall(screens.disclaim, v[2])
end
monitors = {}
neo.executeAsync("sys-init", primary)
-- 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.
-- Also note, should savingThrow fail, neo.dead is now a thing.
for _, v in ipairs(surfaces) do
pcall(v[6], "line", 1)
pcall(v[6], "line", 2)
end
surfaces = {}
end
if savingThrow then
savingThrow.registerForShutdownEvent()
savingThrow.registerSavingThrow(dying)
end
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
-- Always use the first if the GPU has been rebound
local function monitorResetBF(m)
m[5] = -1
m[6] = -1
end
local function monitorGPUColours(m, cb, bg, fg)
local nbg = m[5]
local nfg = m[6]
if nbg ~= bg then
pcall(cb.setBackground, bg)
m[5] = bg
end
if nfg ~= fg then
pcall(cb.setForeground, fg)
m[6] = fg
end
end
-- Status line at top of screen
local statusLine = nil
local function doBackgroundLine(m, mg, bdx, bdy, bdl)
if statusLine and (bdy == 1) then
-- Status bar
monitorGPUColours(m, mg, 0x000000, 0xFFFFFF)
local str = unicode.sub(statusLine, bdx, bdx + bdl - 1)
local strl = unicode.len(str)
pcall(mg.set, bdx, bdy, unicode.undoSafeTextFormat(str))
pcall(mg.fill, bdx + strl, bdy, bdl - strl, 1, " ")
else
monitorGPUColours(m, mg, 0x000040, 0)
pcall(mg.fill, bdx, bdy, bdl, 1, " ")
end
end
local function updateRegion(monitorId, x, y, w, h, surfaceSpanCache)
if not renderingAllowed() then return end
local m = monitors[monitorId]
local mg, rb = m[1]()
if not mg then return end
if rb then
monitorResetBF(m)
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
-- WCHAX: 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
-- Background must occur first due to wide char weirdness
if backgroundMarkStart then
local bdx, bdy, bdl = backgroundMarkStart + x - 1, span + y - 1, sx - backgroundMarkStart
doBackgroundLine(m, mg, bdx, bdy, bdl)
backgroundMarkStart = nil
end
if not surfaceSpanCache[monitorId .. "_" .. t .. "_" .. ty] then
surfaceSpanCache[monitorId .. "_" .. t .. "_" .. ty] = true
surfaces[t][6]("line", ty)
end
elseif not backgroundMarkStart then
backgroundMarkStart = sx
end
end
if backgroundMarkStart then
doBackgroundLine(monitors[monitorId], mg, backgroundMarkStart + x - 1, span + y - 1, (w - backgroundMarkStart) + 1)
end
end
end
local function updateStatus()
statusLine = "Λ-¶: menu (launch 'control' to logout)"
if surfaces[1] then
if #monitors > 1 then
-- 123456789X123456789X123456789X123456789X123456789X
statusLine = "Λ-+: move, Λ-Z: switch, Λ-X: swMonitor, Λ-C: close"
else
statusLine = "Λ-+: move, Λ-Z: switch, Λ-C: close"
end
end
statusLine = unicode.safeTextFormat(statusLine)
for k, v in ipairs(monitors) do
updateRegion(k, 1, 1, v[3], 1, {})
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.max(1, math.min(monitors[monitor][4] - (h - 1), y))
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
local k = 1
while k <= #monitors do
local v = monitors[k]
local mon, rb = v[1]()
if rb then
monitorResetBF(v)
end
if mon then
-- This *can* return null if something went wonky. Let's detect that
v[3], v[4] = mon.getResolution()
if not v[3] then
neo.emergency("everest: monitor went AWOL and nobody told me u.u")
table.remove(monitors, k)
v = nil
end
end
if v then
updateRegion(k, 1, 1, v[3], v[4], {})
k = k + 1
end
end
updateStatus()
end
-- NOTE: If the M, X, Y, W and H are the same, this function ignores you, unless you put , true on the end.
local function moveSurface(surface, m, x, y, w, h, force)
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 and ow == w and oh == h then
if ox == x and oy == y and not force then
return
end
-- note: this doesn't always work due to WC support, and due to resize-to-repaint
if renderingAllowed() and not force then
local cb, b = monitors[m][1]()
if b then
monitorResetBF(monitors[m])
end
if cb then
cb.copy(ox, oy, w, h, x - ox, y - oy)
if surface == surfaces[1] then
local cacheControl = {}
for i = 1, h do
cacheControl[om .. "_1_" .. i] = true
end
updateRegion(om, ox, oy, ow, oh, cacheControl)
return
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, rb = m[1]()
if not cb then return end
if rb then
monitorResetBF(m)
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)
-- rely on undoSafeTextFormat for this transform now
monitorGPUColours(m, cb, bg, fg)
pcall(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 basePalT2 = {
-- on T2 we provide 'system colours' by default
0x000000, 0x0080FF, 0x000040, 0xFFFFFF,
-- stuff above cannot be altered by user applications, to prevent graphical glitches.
-- Provide some shades of grey to try and prevent accidental chroma.
0x182828, 0x404040, 0x686868, 0x909090,
0xB8B8B8, 0xE0E0E0, 0x800080, 0xFF0000,
0x808000, 0x00FF00, 0x008080, 0x0000FF
}
local basePalT3 = {
-- on T3 we provide the Tier 3 pal.
0x0F0F0F, 0x1E1E1E, 0x2D2D2D, 0x3C3C3C,
0x4B4B4B, 0x5A5A5A, 0x696969, 0x787878,
0x878787, 0x969696, 0xA5A5A5, 0xB4B4B4,
0xC3C3C3, 0xD2D2D2, 0xE1E1E1, 0xF0F0F0
}
local function setSurfacePalette(surf, pal)
if neo.dead then return 0 end
local m = monitors[surf[1]]
if not m then return 0 end
local cb, rb = m[1]()
if not cb then return 0 end
local dok, depth = pcall(cb.getDepth)
if not dok then depth = 4 end
if rb then
monitorResetBF(m)
end
local ko = -1
local unlocked = false
if not rawequal(pal, nil) then
neo.ensureType(pal, "table")
if depth < 8 then
ko = 3 -- start overriding at indexes 4+
end
elseif depth < 8 then
pal = basePalT2
unlocked = true
else
pal = basePalT3
unlocked = true
end
for k, v in ipairs(pal) do
-- prevent graphical glitches for
-- critical system colours on T3
local av = v % 0x1000000
if unlocked or (av ~= 0xFFFFFF and
av ~= 0x000000 and
av ~= 0x0080FF and
av ~= 0x000040) then
local ok = pcall(cb.setPaletteColor, k + ko, v)
if not ok then return k - 1 end
end
end
return #pal
end
local function changeFocus(oldSurface, optcache)
local ns1 = surfaces[1]
optcache = optcache or {}
if ns1 ~= oldSurface then
if oldSurface then
setSurfacePalette(oldSurface, nil)
oldSurface[6]("focus", false)
end
if ns1 then
setSurfacePalette(ns1, nil)
ns1[6]("focus", true)
end
updateStatus()
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 neo.dead then error("everest died") end
if shuttingDown or waitingShutdownCallback then error("system shutting down") 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 surf = {math.min(#monitors, math.max(1, lIM)), 1, 2, w, h}
if h >= monitors[surf[1]][4] then
surf[3] = 1
end
local focusState = false
local llid = lid
lid = lid + 1
local specialDragHandler
surf[6] = function (ev, a, b, c, d, e)
-- Must forward surface events
if ev == "focus" then
focusState = a
end
if ev == "touch" then
specialDragHandler = nil
if math.floor(b) == 1 then
if e == 1 then
sendSig(llid, "close")
return
end
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
ofsSurface(surf, ofsX, ofsY)
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 == "scroll" or ev == "drop" then
specialDragHandler = nil
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
a = a - 1
end
sendSig(llid, ev, a, b, c, d, e)
end
local osrf = surfaces[1]
table.insert(surfaces, 1, surf)
surfaceOwners[surf] = pid
changeFocus(osrf)
return {
id = llid,
setSize = function (w, h)
if neo.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, true)
return w, (h - 1)
end,
getDepth = function ()
if neo.dead then return 1 end
local m = monitors[surf[1]]
if not m then return 1 end
local cb, rb = m[1]()
if not cb then return 1 end
if rb then
monitorResetBF(m)
end
return cb.getDepth()
end,
span = function (x, y, text, bg, fg)
if neo.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,
recommendPalette = function (pal)
if not focusState then return 0 end
return setSurfacePalette(surf, pal)
end,
close = function ()
if neo.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)
-- used later on for lost monitor, too
local function disclaimMonitor(mon)
neo.ensureType(mon, "string")
screens.disclaim(mon)
for k, v in ipairs(monitors) do
if v[2] == mon then
table.remove(monitors, k)
reconcileAll()
return true
end
end
return false
end
everestSessionProvider(function (pkg, pid, sendSig)
return {
endSession = function (gotoBristol)
neo.ensureType(gotoBristol, "boolean")
shuttingDown = true
if gotoBristol then
suggestAppsStop()
dying()
end
end,
getMonitors = function ()
local details = {}
for k, v in ipairs(monitors) do
details[k] = v[2]
end
return details
end,
disclaimMonitor = disclaimMonitor
}
end)
-- THE EVEREST SESSION API ENDS
local function startLauncher()
if not waitingShutdownCallback then
local lApp = "app-launcher"
if savingThrow then
lApp = lApp or savingThrow.getSetting("sys-everest.launcher")
end
if lApp then
neo.executeAsync(lApp)
end
end
end
-- 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(ku, ka, kc, down)
local ku = screens.getMonitorByKeyboard(ku)
if not ku then return end
for k, v in ipairs(monitors) do
if v[2] == mu then
lIM = k
end
end
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 then
startLauncher()
end
return
end
end
if focus then
if kc ~= 56 then
lIM = focus[1]
end
focus[6]("key", ka, kc, down)
end
end
-- take all displays!
local function performClaim(s3)
local gpu, _ = screens.claim(s3)
local gpucb = gpu and (gpu())
if gpucb then
local w, h = gpucb.getResolution()
table.insert(monitors, {gpu, s3, w, h, -1, -1})
setSurfacePalette({#monitors}, nil)
-- 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
for _, v in ipairs(screens.getClaimable()) do
performClaim(v)
end
while not shuttingDown do
local s = {coroutine.yield()}
if renderingAllowed() then
if s[1] == "h.key_down" then
key(s[2], s[3], s[4], true)
end
if s[1] == "h.key_up" then
key(s[2], 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
lIM = k
local x, y = math.ceil(s[3]), math.ceil(s[4])
local ix, iy = s[3] - math.floor(x), s[4] - math.floor(y)
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, ix, iy, s[5])
else
if s[5] == 1 then startLauncher() end
end
break
end
end
end
if s[1] == "h.drag" or s[1] == "h.drop" or s[1] == "h.scroll" 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.ceil(s[3]) - focus[2]) + 1, (math.ceil(s[4]) - focus[3]) + 1
local ix, iy = s[3] - math.floor(s[3]), s[4] - math.floor(s[4])
-- Ok, so let's see...
focus[6](s[1]:sub(3), x, y, ix, iy, s[5])
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))
if os1 == surf then
os1 = nil
end
updateRegion(surf[1], surf[2], surf[3], surf[4], surf[5], {})
end
checkWSC()
if os1 then
changeFocus(os1)
else
changeFocus(surfaces[1])
end
end
if s[1] == "x.neo.sys.screens" then
if s[2] == "available" then
performClaim(s[3])
end
if s[2] == "lost" then
disclaimMonitor(s[3])
end
end
if s[1] == "x.neo.sys.manage" then
if s[2] == "shutdown" then
waitingShutdownCallback = s[4]
suggestAppsStop()
checkWSC()
end
end
end