1
0
mirror of https://github.com/20kdc/OC-KittenOS.git synced 2024-11-08 11:38:07 +11:00
OC-KittenOS/code/apps/sys-everest.lua

665 lines
19 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 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. 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", "registering npw")
if not everestProvider then return end
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.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, 160, 50}
-- {monitor, x, y, w, h, callback}
-- callback events are:
-- key ka kc down
-- line y
local surfaces = {}
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.
-- Also note, should savingThrow fail, neo.dead is now a thing.
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
-- 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)
mg.set(bdx, bdy, unicode.undoSafeTextFormat(str))
mg.fill(bdx + strl, bdy, bdl - strl, 1, " ")
else
monitorGPUColours(m, mg, 0x000020, 0)
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 = 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
-- 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, ◣-Λ-C: Session hardkill"
if surfaces[1] then
if #monitors > 1 then
statusLine = "Λ-: move, Λ-Z: switch, Λ-X: swMonitor"
else
statusLine = "Λ-: move, Λ-Z: switch"
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.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
updateStatus()
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
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
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 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)
return w, (h - 1)
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,
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)
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