From 9b6bbf508b0e95b354115ed9077a639bedb45ae2 Mon Sep 17 00:00:00 2001 From: XeonSquared Date: Sun, 4 Jun 2023 16:07:42 +1000 Subject: [PATCH] add multi-terminal patch --- multiterm/lib/vtansi.lua | 268 ++++++++++++++++++++++++++++++++++++ multiterm/package.cfg | 4 + multiterm/service/getty.lua | 88 ++++++++++++ 3 files changed, 360 insertions(+) create mode 100644 multiterm/lib/vtansi.lua create mode 100644 multiterm/package.cfg create mode 100644 multiterm/service/getty.lua diff --git a/multiterm/lib/vtansi.lua b/multiterm/lib/vtansi.lua new file mode 100644 index 0000000..c7c4863 --- /dev/null +++ b/multiterm/lib/vtansi.lua @@ -0,0 +1,268 @@ +local vtansi = {} +local keyboardIgnore = {} +vtansi.activeBuffers = {} +vtansi.sequences = { + [28] = "\n", -- newline + [200] = "\27[A", -- up + [203] = "\27[D", -- left + [205] = "\27[C", -- right + [208] = "\27[B", -- down + [201] = "\27[5~", -- page up + [209] = "\27[6~" -- page down +} +vtansi.keys = {} +vtansi.keys[0x38] = "lalt" +vtansi.keys[0xB8] = "ralt" +function vtansi.saveToBuffer(gpu,idx) + gpu.bitblt(idx, nil, nil, nil, nil, 0) +end +function vtansi.loadFromBuffer(gpu,idx) + gpu.bitblt(0, nil, nil, nil, nil, idx) +end +function vtansi.switchToBuffer(gpu,idx) + -- copy screen to the active buffer + vtansi.saveToBuffer(gpu,vtansi.activeBuffers[gpu.address]) + -- copy the new buffer to the screen + vtansi.loadFromBuffer(gpu,idx) + vtansi.activeBuffers[gpu.address] = idx +end +function vtansi.vtemu(gpu,bn) -- table number -- function -- takes GPU component proxy *gpu* and returns a function to write to it in a manner like an ANSI terminal, either allocating a new buffer or using *bn*. + local colours = {0x0,0xFF0000,0x00FF00,0xFFFF00,0x0000FF,0xFF00FF,0x00B6FF,0xFFFFFF} + local mx, my = gpu.maxResolution() + local cx, cy = 1, 1 + local pc = " " + local lc = "" + local mode = 0 -- 0 normal, 1 escape, 2 command + local lw = true + local sx, sy = 1,1 + local cs = "" + local bg, fg = 0, 0xFFFFFF + + local buffer = bn or gpu.allocateBuffer(mx,my) + if not vtansi.activeBuffers[gpu.address] then + vtansi.activeBuffers[gpu.address] = buffer + end + local oldActiveBuffer = vtansi.activeBuffers[gpu.address] + gpu.setActiveBuffer(buffer) + -- setup + gpu.setResolution(mx,my) + gpu.fill(1,1,mx,my," ") + local function checkCursor() + if cx > mx and lw then + cx, cy = 1, cy+1 + end + if cy > my then + gpu.copy(1,2,mx,my-1,0,-1) + gpu.fill(1,my,mx,1," ") + cy=my + end + if cy < 1 then cy = 1 end + if cx < 1 then cx = 1 end + end + gpu.setActiveBuffer(oldActiveBuffer) + + local function termwrite(s) + if vtansi.activeBuffers[gpu.address] == buffer then + gpu.setActiveBuffer(0) + else + gpu.setActiveBuffer(buffer) + end + local wb = "" + local lb, ec = nil, nil + local function flushwb() + while wb:len() > 0 do + checkCursor() + local wl = wb:sub(1,mx-cx+1) + wb = wb:sub(wl:len()+1) + gpu.set(cx, cy, wl) + cx = cx + wl:len() + end + end + local rs = "" + s=s:gsub("\8","\27[D") + pc = gpu.get(cx,cy) + gpu.setForeground(fg) + gpu.setBackground(bg) + gpu.set(cx,cy,pc) + for cc in s:gmatch(".") do + if mode == 0 then + if cc == "\n" then + flushwb() + cx,cy = 1, cy+1 + checkCursor() + elseif cc == "\t" then + wb=wb..(" "):rep(8*((cx+9)//8)) + elseif cc == "\27" then + flushwb() + mode = 1 + else + wb = wb .. cc + end + elseif mode == 1 then + if cc == "[" then + mode = 2 + else + mode = 0 + end + elseif mode == 2 then + if cc:match("[%d;]") then + cs = cs .. cc + else + mode = 0 + local tA = {} + for s in cs:gmatch("%d+") do + tA[#tA+1] = tonumber(s) + end + if cc == "H" then + cx, cy = math.min(mx,tA[1] or 1), math.min(my,tA[2] or 1) + elseif cc == "A" then + for i = 1, (tA[1] or 1) do + cy = cy - 1 + checkCursor() + end + elseif cc == "B" then + for i = 1, (tA[1] or 1) do + cy = cy + 1 + checkCursor() + end + elseif cc == "C" then + for i = 1, (tA[1] or 1) do + cx = cx + 1 + checkCursor() + end + elseif cc == "D" then + for i = 1, (tA[1] or 1) do + cx = cx - 1 + checkCursor() + end + elseif cc == "s" then + sx, sy = cx, cy + elseif cc == "u" then + cx, cy = sx, sy + elseif cc == "n" and tA[1] == 6 then + rs = string.format("%s\27[%d;%dR",rs,cx,cy) + dprint(string.format("reporting %d;%d as current cursor position",cx,cy)) + elseif cc == "K" and tA[1] == 1 then + gpu.fill(1,cy,cx,1," ") + elseif cc == "K" and tA[1] == 2 then + gpu.fill(cx,cy,mx,1," ") + elseif cc == "K" then + gpu.fill(1,cy,mx,1," ") + elseif cc == "J" and tA[1] == 1 then + gpu.fill(1,1,mx,cy," ") + elseif cc == "J" and tA[1] == 2 then + gpu.fill(1,1,mx,my," ") + cx, cy = 1, 1 + elseif cc == "J" then + gpu.fill(1,cy,mx,my," ") + elseif cc == "m" then + for _,num in ipairs(tA) do + if num == 0 then + fg,bg,ec,lb = 0xFFFFFF,0,false,true + elseif num == 7 then + local nfg,nbg = bg, fg + fg, bg = nfg, nbg + elseif num > 29 and num < 38 then + fg = colours[num-29] + elseif num > 39 and num < 48 then + bg = colours[num-39] + elseif num == 100 or num == 8 then -- disable local echo + ec = false + elseif num == 101 then -- disable line mode + lb = false + end + end + gpu.setForeground(fg) + gpu.setBackground(bg) + end + cs = "" + checkCursor() + end + end + end + flushwb() + checkCursor() + pc = gpu.get(cx,cy) + gpu.setForeground(bg) + gpu.setBackground(fg) + gpu.set(cx,cy,pc) + gpu.setForeground(fg) + gpu.setBackground(bg) + return rs, lb, ec + end + + return termwrite, buffer +end + +function vtansi.vtsession(gpua,scra,bn) -- string string number -- function function function -- creates a process to handle the GPU and screen address combination *gpua*/*scra*, optionally using buffer number *bn* specifically. Returns read, write and "close" functions. + local modifiers = {} + local gpu = component.proxy(gpua) +-- gpu.bind(scra) + local write, bn = vtansi.vtemu(gpu,bn) + local kba = {} + for k,v in ipairs(component.invoke(scra,"getKeyboards")) do + kba[v]=true + end + local buf, lbuf, echo = "", false, false + os.spawn(function() + while true do + local ty,ka,ch,kc = coroutine.yield() + if keyboardIgnore[ka] == bn then + keyboardIgnore[ka] = nil + end + if kba[ka] and vtansi.keys[kc] then + modifiers[vtansi.keys[kc]] = ty == "key_down" + end + if ty == "key_down" and kba[ka] and vtansi.activeBuffers[gpua] == bn then + if false then + elseif ty == "key_down" and kba[ka] and ch == 46 and kc == 52 and (modifiers.lalt or modifiers.ralt) then + -- next buffer + local allBuffers = gpu.buffers() + for k,v in ipairs(allBuffers) do + if v == vtansi.activeBuffers[gpu.address] and allBuffers[k+1] and not keyboardIgnore[ka] then + keyboardIgnore[ka] = bn + vtansi.switchToBuffer(gpu,allBuffers[k+1]) + end + end + elseif ty == "key_down" and kba[ka] and ch == 44 and kc == 51 and (modifiers.lalt or modifiers.ralt) then + -- previous buffer + local allBuffers = gpu.buffers() + for k,v in ipairs(allBuffers) do + if v == vtansi.activeBuffers[gpu.address] and allBuffers[k-1] and not keyboardIgnore[ka] then + keyboardIgnore[ka] = bn + vtansi.switchToBuffer(gpu,allBuffers[k-1]) + end + end + else + local outs + if ch > 0 then + outs = string.char(ch) + end + outs = vtansi.sequences[kc] or outs + if outs then + if echo then write(outs) end + buf=buf..outs + end + end + end + end + end,string.format("ttyd[%s:%s/%i]",gpua:sub(1,8),scra:sub(1,8),bn)) + local function bread(n) + coroutine.yield() + local r = buf + buf = "" + return r + end + local function bwrite(d) + local ba, lb, ec = write(d) + buf = buf .. ba + if lb ~= nil then + lbuf = lb + end + if ec ~= nil then + echo = ec + end + end + return bread, bwrite, function() io.write("\27[2J\27[H") end +end +return vtansi diff --git a/multiterm/package.cfg b/multiterm/package.cfg new file mode 100644 index 0000000..ef59a70 --- /dev/null +++ b/multiterm/package.cfg @@ -0,0 +1,4 @@ +{["name"]="multiterm", + ["description"]="Patched vtansi and getty to support multiple virtual terminals per display, using video memory", + ["authors"]="Izaya", + ["system"]=true} diff --git a/multiterm/service/getty.lua b/multiterm/service/getty.lua new file mode 100644 index 0000000..ad621b9 --- /dev/null +++ b/multiterm/service/getty.lua @@ -0,0 +1,88 @@ +local gpus,screens,ttyn,pids = {}, {}, 0, {} +local basepid = nil +local shell = require "shell" +local vtansi = require "vtansi" +local function scan() + local w,di = pcall(computer.getDeviceInfo) + if w then + for a,t in pairs(component.list()) do + if t == "gpu" then + gpus[a] = gpus[a] or {false, tonumber(di[a].capacity)} + elseif t == "screen" then + screens[a] = screens[a] or {false, tonumber(di[a].capacity)} + end + end + else + dprint("no getDevInfo") + for a,t in pairs(component.list()) do + if t == "gpu" then + gpus[a] = gpus[a] or {false, 8000} + elseif t == "screen" then + screens[a] = screens[a] or {false, 8000} + end + end + end +end +local function nextScreen(n) + local rt = {} + for k,v in pairs(screens) do + if not v[1] then + rt[v[2]] = rt[v[2]] or k + end + end + return rt[n] or rt[24000] or rt[8000] or rt[4000] or rt[2000] or rt[800] or rt[640] or rt[600] +end + +local function spawnShell(fin,fout) + io.input(fin) + io.output(fout):setvbuf("no") + print(_OSVERSION.." - "..tostring(math.floor(computer.totalMemory()/1024)).."K RAM") + print((os.getenv("HOSTNAME") or "unknown") .. " on " .. fin) + return os.spawn(function() local w,r = pcall(shell.interactive) if not w then syslog(r) end end, "shell: "..tostring(fin)) +end + +local function allocate() + for k,v in pairs(gpus) do + dprint(k) + local sA = nextScreen(v[2]) + if v[1] == false and sA then + component.invoke(k,"freeAllBuffers") + component.invoke(k,"bind",sA) + local mw, mh = component.invoke(k,"maxResolution") + local bufferSize = mw * mh + for i = 1, math.floor(component.invoke(k,"totalMemory") / bufferSize) do + local r,w = vtansi.vtsession(k,sA,i == 0 and i) + devfs.register("tty"..tostring(ttyn), function() return r,w,function() end end) + pids["tty"..tostring(ttyn)] = {-1} + ttyn = ttyn + 1 + end + gpus[k][1] = true + screens[sA][1] = true + end + end +end + +function start() +basepid = os.spawn(function() +scan() +allocate() +dprint("screens ready") +while true do + coroutine.yield() + for k,v in pairs(pids) do + if not os.taskInfo(v[1]) then + dprint("Spawning new shell for "..k) + pids[k][1] = spawnShell(v[2] or "/dev/"..k, v[3] or "/dev/"..k) + pids[k][2], pids[k][3] = pids[k][2] or io.input(), pids[k][3] or io.output() + coroutine.yield() + end + end +end +end,"getty") +return basepid +end +function stop() + os.kill(basepid) + basepid = nil +end +return {start=start,stop=stop}