770 lines · 17.0 KB · Lua
RawDownload
-- SPDX-License-Identifier: ISC---- A small VT100/xterm screen emulator, so a test can read the grid an-- ncurses application would have painted. It covers what ncurses emits-- under xterm-256color: cursor motion, erase, scroll region, insert and-- delete, SGR text attributes. Mouse reports, palette changes and titles-- are parsed and dropped. Colours are dropped too -- assertions are about-- layout and emphasis, and every cell carries a text-attribute mask.local vt = {} local BOLD = 1local ITALIC = 2local UNDERLINE = 4local BLINK = 8local REVERSE = 16local STRIKE = 32 vt.BOLD = BOLDvt.ITALIC = ITALICvt.UNDERLINE = UNDERLINEvt.BLINK = BLINKvt.REVERSE = REVERSEvt.STRIKE = STRIKE -- SGR parameter -> {bit, on}.local SGR_TEXT = {	[1] = { BOLD, true },	[3] = { ITALIC, true },	[4] = { UNDERLINE, true },	[5] = { BLINK, true },	[7] = { REVERSE, true },	[9] = { STRIKE, true },	[22] = { BOLD, false },	[23] = { ITALIC, false },	[24] = { UNDERLINE, false },	[25] = { BLINK, false },	[27] = { REVERSE, false },	[29] = { STRIKE, false },} -- Modes are tracked by number; private ones are shifted so DECAWM (private-- 7) never collides with the ANSI mode 7.local IRM = 4local DECOM = 6 * 32local DECAWM = 7 * 32 -- Cells are immutable and interned, so a screen of spaces costs one table.local cell_cache = {} local function cell(ch, attr)	local by_attr = cell_cache[attr]	if by_attr == nil then		by_attr = {}		cell_cache[attr] = by_attr	end	local c = by_attr[ch]	if c == nil then		c = { ch = ch, attr = attr }		by_attr[ch] = c	end	return cend local BLANK = cell(" ", 0) -- Character width. Everything outside the wide and zero-width ranges is one-- column, which is all a terminal needs to lay text out.local WIDE = {	{ 0x1100, 0x115f },	{ 0x2e80, 0x303e },	{ 0x3041, 0x33ff },	{ 0x3400, 0x4dbf },	{ 0x4e00, 0x9fff },	{ 0xa000, 0xa4cf },	{ 0xac00, 0xd7a3 },	{ 0xf900, 0xfaff },	{ 0xfe30, 0xfe6f },	{ 0xff00, 0xff60 },	{ 0xffe0, 0xffe6 },	{ 0x1f300, 0x1f64f },	{ 0x1f900, 0x1f9ff },	{ 0x20000, 0x3fffd },} local function char_width(cp)	if cp == 0 then		return 0	end	if cp < 32 or (cp >= 0x7f and cp < 0xa0) then		return -1	end	if (cp >= 0x300 and cp <= 0x36f) or (cp >= 0x200b and cp <= 0x200f) then		return 0	end	for i = 1, #WIDE do		if cp >= WIDE[i][1] and cp <= WIDE[i][2] then			return 2		end	end	return 1end local Screen = {}Screen.__index = Screen -- Rows and cells are sparse: an absent entry reads as a blank with default-- attributes, which is what "never written" means on a real terminal.local function row(self, y)	local r = self.buf[y]	if r == nil then		r = {}		self.buf[y] = r	end	return rend local function at(self, y, x)	local r = self.buf[y]	if r == nil then		return BLANK	end	return r[x] or BLANKend function vt.new(rows, cols)	local self = setmetatable({}, Screen)	self.rows = rows	self.cols = cols	self:reset()	return selfend function Screen:reset()	self.buf = {}	self.margins = nil	self.mode = { [DECAWM] = true }	self.cy, self.cx, self.attr = 0, 0, 0	self.savepoints = {}	self.tabstops = {}	local x = 8	while x < self.cols do		self.tabstops[x] = true		x = x + 8	end	self.pending = "" -- undecoded utf-8 tail	self.state = "ground"end function Screen:margin_top()	return self.margins and self.margins.top or 0end function Screen:margin_bottom()	return self.margins and self.margins.bottom or self.rows - 1end local function ensure_h(self)	if self.cx < 0 then		self.cx = 0	elseif self.cx > self.cols - 1 then		self.cx = self.cols - 1	endend local function ensure_v(self, use_margins)	local top, bottom = 0, self.rows - 1	if (use_margins or self.mode[DECOM]) and self.margins then		top, bottom = self.margins.top, self.margins.bottom	end	if self.cy < top then		self.cy = top	elseif self.cy > bottom then		self.cy = bottom	endend -- ---- rendering primitives ---- function Screen:draw_char(ch, cp)	local w = char_width(cp) 	if self.cx == self.cols then		if self.mode[DECAWM] then			self.cx = 0			self:index()		elseif w > 0 then			self.cx = self.cx - w		end	end	if self.mode[IRM] and w > 0 then		self:insert_characters(w)	end 	local r = row(self, self.cy)	if w == 1 then		r[self.cx] = cell(ch, self.attr)	elseif w == 2 then		r[self.cx] = cell(ch, self.attr)		if self.cx + 1 < self.cols then			r[self.cx + 1] = cell("", self.attr)		end	elseif w == 0 then		-- A combining mark joins the character before it.		if self.cx > 0 then			local prev = r[self.cx - 1] or BLANK			r[self.cx - 1] = cell(prev.ch .. ch, prev.attr)		end	else		return	end	if w > 0 then		self.cx = math.min(self.cx + w, self.cols)	endend function Screen:index()	local top, bottom = self:margin_top(), self:margin_bottom()	if self.cy == bottom then		for y = top, bottom - 1 do			self.buf[y] = self.buf[y + 1]		end		self.buf[bottom] = nil	else		self:cursor_down(1)	endend function Screen:reverse_index()	local top, bottom = self:margin_top(), self:margin_bottom()	if self.cy == top then		for y = bottom, top + 1, -1 do			self.buf[y] = self.buf[y - 1]		end		self.buf[top] = nil	else		self:cursor_up(1)	endend function Screen:scroll_up(n)	local top, bottom = self:margin_top(), self:margin_bottom()	for _ = 1, n do		for y = top, bottom - 1 do			self.buf[y] = self.buf[y + 1]		end		self.buf[bottom] = nil	endend function Screen:scroll_down(n)	local top, bottom = self:margin_top(), self:margin_bottom()	for _ = 1, n do		for y = bottom, top + 1, -1 do			self.buf[y] = self.buf[y - 1]		end		self.buf[top] = nil	endend function Screen:cursor_up(n)	self.cy = math.max(self.cy - (n or 1), self:margin_top())end function Screen:cursor_down(n)	self.cy = math.min(self.cy + (n or 1), self:margin_bottom())end function Screen:cursor_position(line, col)	col = (col or 1) - 1	line = (line or 1) - 1	if self.margins and self.mode[DECOM] then		line = line + self.margins.top		if line < self.margins.top or line > self.margins.bottom then			return		end	end	self.cx, self.cy = col, line	ensure_h(self)	ensure_v(self)end function Screen:erase_in_line(how)	local first, last	if how == 1 then		first, last = 0, self.cx	elseif how == 2 then		first, last = 0, self.cols - 1	else		first, last = self.cx, self.cols - 1	end	local r = row(self, self.cy)	for x = first, last do		r[x] = cell(" ", self.attr)	endend function Screen:erase_in_display(how)	local first, last	if how == 1 then		first, last = 0, self.cy - 1	elseif how == 2 or how == 3 then		first, last = 0, self.rows - 1	else		first, last = self.cy + 1, self.rows - 1	end	local blank = cell(" ", self.attr)	for y = first, last do		local r = self.buf[y]		if r ~= nil then			for x in pairs(r) do				r[x] = blank			end		end	end	if how == 0 or how == 1 then		self:erase_in_line(how)	endend function Screen:insert_characters(n)	n = n or 1	local r = row(self, self.cy)	for x = self.cols, self.cx, -1 do		if x + n <= self.cols then			r[x + n] = r[x]		end		r[x] = nil	endend function Screen:delete_characters(n)	n = n or 1	local r = row(self, self.cy)	for x = self.cx, self.cols - 1 do		if x + n <= self.cols then			r[x] = r[x + n]			r[x + n] = nil		else			r[x] = nil		end	endend function Screen:erase_characters(n)	n = n or 1	local r = row(self, self.cy)	local blank = cell(" ", self.attr)	for x = self.cx, math.min(self.cx + n, self.cols) - 1 do		r[x] = blank	endend function Screen:insert_lines(n)	n = n or 1	local top, bottom = self:margin_top(), self:margin_bottom()	if self.cy < top or self.cy > bottom then		return	end	for y = bottom, self.cy, -1 do		if y + n <= bottom then			self.buf[y + n] = self.buf[y]		end		self.buf[y] = nil	end	self.cx = 0end function Screen:delete_lines(n)	n = n or 1	local top, bottom = self:margin_top(), self:margin_bottom()	if self.cy < top or self.cy > bottom then		return	end	for y = self.cy, bottom do		if y + n <= bottom then			self.buf[y] = self.buf[y + n]			self.buf[y + n] = nil		else			self.buf[y] = nil		end	end	self.cx = 0end function Screen:set_margins(top, bottom)	if (top == nil or top == 0) and bottom == nil then		self.margins = nil		return	end	local cur_top = self:margin_top()	local cur_bottom = self:margin_bottom()	if top == nil then		top = cur_top	else		top = math.max(0, math.min(top - 1, self.rows - 1))	end	if bottom == nil then		bottom = cur_bottom	else		bottom = math.max(0, math.min(bottom - 1, self.rows - 1))	end	if bottom - top >= 1 then		self.margins = { top = top, bottom = bottom }		self:cursor_position()	endend function Screen:sgr(params)	if #params == 0 or (#params == 1 and params[1] == 0) then		self.attr = 0		return	end	local i = 1	while i <= #params do		local p = params[i]		local t = SGR_TEXT[p]		if p == 0 then			self.attr = 0		elseif t then			if t[2] then				self.attr = self.attr | t[1]			else				self.attr = self.attr & ~t[1]			end		elseif p == 38 or p == 48 then			-- Extended colour: skip its arguments.			local n = params[i + 1]			if n == 5 then				i = i + 2			elseif n == 2 then				i = i + 4			end		end		i = i + 1	endend function Screen:set_mode(private, params)	for _, p in ipairs(params) do		self.mode[private and p * 32 or p] = true	endend function Screen:reset_mode(private, params)	for _, p in ipairs(params) do		self.mode[private and p * 32 or p] = nil	endend function Screen:save_cursor()	self.savepoints[#self.savepoints + 1] =	    { cy = self.cy, cx = self.cx, attr = self.attr }end function Screen:restore_cursor()	local sp = table.remove(self.savepoints)	if sp == nil then		self.mode[DECOM] = nil		self:cursor_position()		return	end	self.cy, self.cx, self.attr = sp.cy, sp.cx, sp.attr	ensure_h(self)	ensure_v(self, true)end function Screen:tab()	local column = self.cols - 1	for x = self.cx + 1, self.cols - 1 do		if self.tabstops[x] then			column = x			break		end	end	self.cx = columnend -- ---- control dispatch ---- local function ctrl_char(self, b)	if b == 0x08 then -- BS		if self.cx == self.cols then			self.cx = self.cx - 1		end		self.cx = self.cx - 1		ensure_h(self)	elseif b == 0x09 then -- HT		self:tab()	elseif b == 0x0a or b == 0x0b or b == 0x0c then -- LF, VT, FF		self:index()	elseif b == 0x0d then -- CR		self.cx = 0	end	-- BEL, SO and SI have no effect on the grid.end local function is_basic(b)	return b == 0x07 or (b >= 0x08 and b <= 0x0d) or b == 0x0e or b == 0x0fend 

Lines 1–500 of 770.