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

794 lines
22 KiB
Lua

-- 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