2025-07-02 22:38:42 +02:00

286 lines
7.8 KiB
Lua

local unicode = require("unicode")
local event = require("event")
local component = require("component")
local computer = require("computer")
local tty = {}
tty.window =
{
fullscreen = true,
blink = true,
dx = 0,
dy = 0,
x = 1,
y = 1,
output_buffer = "",
}
tty.stream = {}
local screen_cache = {}
local function screen_reset(gpu, addr)
screen_cache[addr or gpu.getScreen() or false] = nil
end
event.listen("screen_resized", screen_reset)
function tty.getViewport()
local window = tty.window
local screen = tty.screen()
if window.fullscreen and screen and not screen_cache[screen] then
screen_cache[screen] = true
window.width, window.height = window.gpu.getViewport()
end
return window.width, window.height, window.dx, window.dy, window.x, window.y
end
function tty.setViewport(width, height, dx, dy, x, y)
checkArg(1, width, "number")
checkArg(2, height, "number")
local window = tty.window
dx, dy, x, y = dx or 0, dy or 0, x or 1, y or 1
window.width, window.height, window.dx, window.dy, window.x, window.y = width, height, dx, dy, x, y
end
function tty.gpu()
return tty.window.gpu
end
function tty.clear()
tty.stream.scroll(math.huge)
tty.setCursor(1, 1)
end
function tty.isAvailable()
local gpu = tty.gpu()
return not not (gpu and gpu.getScreen())
end
-- PLEASE do not use this method directly, use io.read or term.read
function tty.stream.read()
local core = require("core/cursor")
local cursor = core.new(tty.window.cursor)
-- the window is given the cursor to allow sy updates [needed for wide char wrapping]
-- even if the user didn't set a cursor, we need one to read
tty.window.cursor = cursor
local ok, result, reason = xpcall(core.read, debug.traceback, cursor)
if not ok or not result then
pcall(cursor.update, cursor)
end
return select(2, assert(ok, result, reason))
end
-- PLEASE do not use this method directly, use io.write or term.write
function tty.stream:write(value)
local gpu = tty.gpu()
if not gpu then
return
end
local window = tty.window
local cursor = window.cursor or {}
cursor.sy = cursor.sy or 0
cursor.tails = cursor.tails or {}
local beeped
local uptime = computer.uptime
local last_sleep = uptime()
window.output_buffer = window.output_buffer .. value
while true do
if uptime() - last_sleep > 3 then
os.sleep(0)
last_sleep = uptime()
end
local ansi_print = require("vt100").parse(window)
-- scroll before parsing next line
-- the value may only have been a newline
cursor.sy = cursor.sy + self.scroll()
-- we may have needed to scroll one last time [nowrap adjustments]
-- or the vt100 parse is incomplete, print nothing else
if #window.output_buffer == 0 or not ansi_print then
break
end
local x, y = tty.getCursor()
local _, ei, delim = unicode.sub(window.output_buffer, 1, window.width):find("([\27\t\r\n\a\b\v\15])")
local segment = ansi_print .. (ei and window.output_buffer:sub(1, ei - 1) or window.output_buffer)
if segment ~= "" then
local gpu_x, gpu_y = x + window.dx, y + window.dy
local tail = ""
local wlen_needed = unicode.wlen(segment)
local wlen_remaining = window.width - x + 1
if wlen_remaining < wlen_needed then
segment = unicode.wtrunc(segment, wlen_remaining + 1)
wlen_needed = unicode.wlen(segment)
tail = wlen_needed < wlen_remaining and " " or ""
cursor.tails[gpu_y - cursor.sy] = tail
if not window.nowrap then
-- we have to reparse the delimeter
ei = #segment
-- fake a newline
delim = "\n"
end
end
gpu.set(gpu_x, gpu_y, segment..tail)
x = x + wlen_needed
end
window.output_buffer = ei and window.output_buffer:sub(ei + 1) or
unicode.sub(window.output_buffer, window.width + 1)
if delim == "\t" then
x = ((x-1) - ((x-1) % 8)) + 9
elseif delim == "\r" then
x = 1
elseif delim == "\n" then
x = 1
y = y + 1
elseif delim == "\b" then
x = x - 1
elseif delim == "\v" then
y = y + 1
elseif delim == "\a" and not beeped then
computer.beep()
beeped = true
elseif delim == "\27" then
window.output_buffer = delim .. window.output_buffer
end
tty.setCursor(x, y)
end
return cursor.sy
end
function tty.getCursor()
local window = tty.window
return window.x, window.y
end
function tty.setCursor(x, y)
checkArg(1, x, "number")
checkArg(2, y, "number")
local window = tty.window
window.x, window.y = x, y
end
local gpu_intercept = {}
function tty.bind(gpu)
checkArg(1, gpu, "table")
if not gpu_intercept[gpu] then
gpu_intercept[gpu] = true -- only override a gpu once
-- the gpu can change resolution before we get a chance to call events and handle screen_resized
-- unfortunately, we have to handle viewport changes by intercept
local setr, setv = gpu.setResolution, gpu.setViewport
gpu.setResolution = function(...)
screen_reset(gpu)
return setr(...)
end
gpu.setViewport = function(...)
screen_reset(gpu)
return setv(...)
end
end
local window = tty.window
if window.gpu ~= gpu then
window.gpu = gpu
window.keyboard = nil -- without a keyboard bound, always use the screen's main keyboard (1st)
tty.getViewport()
end
screen_reset(gpu)
end
function tty.keyboard()
-- this method needs to be safe even if there is no terminal window (e.g. no gpu)
local window = tty.window
if window.keyboard then
return window.keyboard
end
local system_keyboard = component.isAvailable("keyboard") and component.keyboard
system_keyboard = system_keyboard and system_keyboard.address or "no_system_keyboard"
local screen = tty.screen()
if not screen then
-- no screen, no known keyboard, use system primary keyboard if any
return system_keyboard
end
-- if we are using a gpu bound to the primary screen, then use the primary keyboard
if component.isAvailable("screen") and component.screen.address == screen then
window.keyboard = system_keyboard
else
-- calling getKeyboards() on the screen is costly (time)
-- changes to this design should avoid this on every key hit
-- this is expensive (slow!)
debugprint("screen", type(screen), string.format("%q", screen))
window.keyboard = assert(component.invoke(screen, "getKeyboards"))[1] or system_keyboard
end
return window.keyboard
end
function tty.screen()
local gpu = tty.gpu()
if not gpu then
return nil
end
return gpu.getScreen()
end
function tty.stream.scroll(lines)
local gpu = tty.gpu()
if not gpu then
return 0
end
local width, height, dx, dy, x, y = tty.getViewport()
-- nil lines indicates a request to auto scroll
-- auto scroll is when the cursor has gone below the bottom on the terminal
-- and the text is scroll up, pulling the cursor back into view
-- lines<0 scrolls up (text down)
-- lines>0 scrolls down (text up)
-- no lines count given, the user is asking to auto scroll y back into view
if not lines then
if y < 1 then
lines = y - 1 -- y==0 scrolls back -1
elseif y > height then
lines = y - height -- y==height+1 scroll forward 1
else
return 0 -- do nothing
end
end
lines = math.min(lines, height)
lines = math.max(lines,-height)
-- scroll request can be too large
local abs_lines = math.abs(lines)
local box_height = height - abs_lines
local fill_top = dy + 1 + (lines < 0 and 0 or box_height)
gpu.copy(dx + 1, dy + 1 + math.max(0, lines), width, box_height, 0, -lines)
gpu.fill(dx + 1, fill_top, width, abs_lines, ' ')
tty.setCursor(x, math.max(1, math.min(y, height)))
return lines
end
-- stream methods
local function bfd() return nil, "tty: invalid operation" end
tty.stream.close = bfd
tty.stream.seek = bfd
tty.stream.handle = "tty"
return tty