-- SPDX-FileCopyrightText: © 2022—2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. -- Phase 2: term.read_key reads a key/sequence cooperatively from a -- lev.fd_reader (e.g. stdin) without blocking the event loop, and -- reassembles KKBP sequences split across reads. local testimony = require("testimony") local term = require("term") local lev = require("lev") local ps = require("std.ps") local testify = testimony.new("== term.read_key ==") -- Read one key from `chunks` (a list of byte fragments) fed through a -- pipe with a small delay between fragments, exercising the split-read -- reassembly path. local function read_key_from_chunks(chunks, opts) local p = assert(ps.pipe()) lev.set_nonblocking(p.out, true) local result lev.run(function() lev.spawn(function() for _, chunk in ipairs(chunks) do lev.sleep(0.008) p:write(chunk) end end) local reader = lev.fd_reader(p.out) result = { term.read_key(reader, opts) } end) p:close_inn() p:close_out() return result end -- ========================================================================= -- Whole-sequence reads -- ========================================================================= testify:that("read_key: ctrl-c (ESC[99;5u) in one chunk", function() local r = read_key_from_chunks({ "\27[99;5u" }) testimony.assert_equal("c", r[1]) testimony.assert_equal(5, r[2]) -- CTRL bit (4) + 1 end) testify:that("read_key: alt-h (ESC[104;3u) in one chunk", function() local r = read_key_from_chunks({ "\27[104;3u" }) testimony.assert_equal("h", r[1]) testimony.assert_equal(3, r[2]) -- ALT bit (2) + 1 end) testify:that("read_key: plain non-escape byte returned directly", function() local r = read_key_from_chunks({ "a" }) testimony.assert_equal("a", r[1]) end) -- ========================================================================= -- Split sequences (the critical correctness case) -- ========================================================================= testify:that("read_key: reassembles ctrl-c split across 3 chunks", function() local r = read_key_from_chunks({ "\27", "[99;", "5u" }) testimony.assert_equal("c", r[1]) testimony.assert_equal(5, r[2]) end) testify:that("read_key: reassembles a byte-at-a-time sequence", function() local chunks = {} for _, b in ipairs({ "\27", "[", "1", "0", "4", ";", "3", "u" }) do chunks[#chunks + 1] = b end local r = read_key_from_chunks(chunks) testimony.assert_equal("h", r[1]) testimony.assert_equal(3, r[2]) end) -- ========================================================================= -- Terminal replies interleaved on the input stream are skipped, not treated -- as a key (or, worse, as EOF). A real terminal answers the gfx loop's -- per-frame graphics commands with APC/OSC/DCS replies on stdin; read_key -- must consume them whole and return the next real key, otherwise a -- cooperative reader stops on the first reply. -- ========================================================================= testify:that("read_key: skips an APC graphics reply, returns the next key", function() local r = read_key_from_chunks({ "\27_Gi=1;OK\27\\", "\27[99;5u" }) testimony.assert_equal("c", r[1]) testimony.assert_equal(5, r[2]) end) testify:that("read_key: skips a BEL-terminated OSC reply, returns the next key", function() local r = read_key_from_chunks({ "\27]11;rgb:00/00/00\7", "a" }) testimony.assert_equal("a", r[1]) end) testify:that("read_key: skips a reply bundled with the key in one chunk", function() local r = read_key_from_chunks({ "\27_Gi=1;OK\27\\\27[104;3u" }) testimony.assert_equal("h", r[1]) testimony.assert_equal(3, r[2]) end) -- ========================================================================= -- Bracketed paste: ESC[200~ ... ESC[201~ is read whole and returned as a -- single string (no mods/event), reassembling content and the closing marker -- across split reads, and not mistaking embedded escapes for the end marker. -- ========================================================================= testify:that("read_key: bracketed paste in one chunk -> text + paste flag", function() local r = read_key_from_chunks({ "\27[200~hello world\27[201~" }) testimony.assert_equal("hello world", r[1]) testimony.assert_nil(r[2]) -- a paste carries no modifiers testimony.assert_equal(true, r[6]) -- is_paste flag, so callers insert literally end) testify:that("read_key: paste that looks like a key name still carries text", function() -- "LHS 3558" matches LEM's is_printable_key key-name rejection (^%u[%u%d]); -- the paste flag is what lets LEM insert it verbatim instead of dropping it. local r = read_key_from_chunks({ "\27[200~LHS 3558\27[201~" }) testimony.assert_equal("LHS 3558", r[1]) testimony.assert_equal(true, r[6]) end) testify:that("read_key: paste with closing marker split across chunks", function() local r = read_key_from_chunks({ "\27[200~hello", "\27", "[201", "~" }) testimony.assert_equal("hello", r[1]) end) testify:that("read_key: paste reassembled byte-at-a-time", function() local chunks = {} for _, b in ipairs({ "\27", "[", "2", "0", "0", "~", "h", "i", "\27", "[", "2", "0", "1", "~" }) do chunks[#chunks + 1] = b end local r = read_key_from_chunks(chunks) testimony.assert_equal("hi", r[1]) end) testify:that("read_key: embedded ESC[ that is not ESC[201~ is kept verbatim", function() -- A pasted arrow-key sequence (ESC[A) must survive as literal content, -- exercising the false-alarm branch of the end-marker check. local r = read_key_from_chunks({ "\27[200~a\27[Ab\27[201~" }) testimony.assert_equal("a\27[Ab", r[1]) end) -- ========================================================================= -- Watcher contract: key tuple -> shortcut string -- Mirrors shell.lua's in-run combo watcher derivation so the combo -- string the watcher dispatches matches what modes expect (e.g. -- Agent Smith's "CTRL+c"). Guards the host<->mode combo-format contract. -- ========================================================================= local function shortcut_from_chunks(chunks) local r = read_key_from_chunks(chunks) local key, mods, _event, _shifted, base = r[1], r[2], r[3], r[4], r[5] if base then key = base end local shortcut = key local mod_string = term.mods_to_string(mods - 1) if mod_string ~= "" then shortcut = mod_string .. "+" .. key end return shortcut end testify:that("watcher contract: ctrl-c derives CTRL+c", function() testimony.assert_equal("CTRL+c", shortcut_from_chunks({ "\27[99;5u" })) end) testify:that("watcher contract: alt-h derives ALT+h", function() testimony.assert_equal("ALT+h", shortcut_from_chunks({ "\27[104;3u" })) end) -- ========================================================================= -- Cancellation -- ========================================================================= testify:that("read_key: cancel token interrupts a blocked read -> nil", function() local p = assert(ps.pipe()) lev.set_nonblocking(p.out, true) local result lev.run(function() local token = lev.cancel_token() lev.spawn(function() lev.sleep(0.01) token:cancel() end) local reader = lev.fd_reader(p.out) result = { term.read_key(reader, { cancel = token }) } end) testimony.assert_nil(result[1]) p:close_inn() p:close_out() end) -- ========================================================================= -- Conclude -- ========================================================================= testify:conclude()