-- Copyright (C) 2018-2021 by KittenOS NEO contributors -- -- Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. -- -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -- THIS SOFTWARE. -- 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, title} -- 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 draggingWindowX, draggingWindowY local function suggestAppsStop() for k, v in ipairs(surfaces) do for i = 1, 4 do v[6](v[8], "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], v[8], "line", 1) pcall(v[6], v[8], "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.set, bdx + strl, bdy, (" "):rep(bdl - strl)) else monitorGPUColours(m, mg, 0x000040, 0) pcall(mg.set, bdx, bdy, (" "):rep(bdl)) end 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 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 if ty == 1 then local lw = surfaces[t][4] local bg = 0x0080FF local fg = 0x000000 local tx = "-" if t == 1 then bg = 0x000000 fg = 0x0080FF tx = "+" end local vtitle = surfaces[t][7] local vto = unicode.len(vtitle) if vto < lw then vtitle = vtitle .. (tx):rep(lw - vto) else vtitle = unicode.sub(vtitle, 1, lw) end handleSpan(surfaces[t], 1, 1, vtitle, bg, fg) else surfaces[t][6](surfaces[t][8], "line", ty - 1) end 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(x, 1), monitors[monitor][3] - (w - 1)) y = math.max(2 - h, math.min(monitors[monitor][4], 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 local monH = monitors[m][4] local cutTop = math.max(0, 1 - math.min(oy, y)) local cutBottom = math.max(0, (math.max(oy + oh, y + h) - 1) - monH) pcall(cb.copy, ox, oy + cutTop, w, h - (cutTop + cutBottom), x - ox, y - oy) if surface == surfaces[1] then local cacheControl = {} for i = 1 + cutTop, h - cutBottom 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 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](oldSurface[8], "focus", false) end if ns1 then setSurfacePalette(ns1, nil) ns1[6](ns1[8], "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 llid = lid lid = lid + 1 local surf = {math.min(#monitors, math.max(1, lIM)), 1, 2, w, h, sendSig, title, llid} if h >= monitors[surf[1]][4] then surf[3] = 1 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 checkArg(3, text, "string") 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 surfaces[1] ~= surf 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) checkArg(1, 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) checkArg(1, 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 = savingThrow.getSetting("sys-everest.launcher") or lApp 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, lin = screens.getMonitorByKeyboard(ku) for k, v in ipairs(monitors) do if ku and v[2] == ku then lin = k break end end if not lin then return end lIM = lin local focus = surfaces[1] if kc == 29 then isCtrDown = down elseif kc == 56 then isAltDown = down end if isAltDown and ka == 122 then if focus and down then local n = table.remove(surfaces, 1) table.insert(surfaces, n) changeFocus(n) end elseif isAltDown and kc == 200 then if focus and down then ofsSurface(focus, 0, -1) end elseif isAltDown and kc == 208 then if focus and down then ofsSurface(focus, 0, 1) end elseif isAltDown and kc == 203 then if focus and down then ofsSurface(focus, -1, 0) end elseif isAltDown and kc == 205 then if focus and down then ofsSurface(focus, 1, 0) end elseif isAltDown and ka == 120 then if focus and down then ofsMSurface(focus, 1) end elseif isAltDown and ka == 97 then if not down then isAltDown = false end elseif isAltDown and (ka == 3 or ka == 99) then if down then if isCtrDown then error("User-authorized Everest crash.") elseif focus then focus[6](focus[8], "close") end end elseif isAltDown and ka == 13 then if down then startLauncher() end elseif focus then if kc ~= 56 then lIM = focus[1] end focus[6](focus[8], "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](surfaces[1][8], "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) draggingWindowX, draggingWindowY = nil if ly == 1 then if s[5] == 1 then ns[6](ns[8], "close") else draggingWindowX, draggingWindowY = lx, ly end else ns[6](ns[8], "touch", lx, ly - 1, ix, iy, s[5]) end 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]) if s[1] == "h.drag" and draggingWindowX then local ofsX, ofsY = x - draggingWindowX, y - draggingWindowY if ofsX ~= 0 or ofsY ~= 0 then ofsSurface(focus, ofsX, ofsY) end else draggingWindowX, draggingWindowY = nil -- Ok, so let's see... focus[6](focus[8], s[1]:sub(3), x, y - 1, ix, iy, s[5]) end 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() 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