diff options
-rw-r--r-- | CHANGELOG.md | 5 | ||||
-rw-r--r-- | spec/04-term_spec.lua | 41 | ||||
-rw-r--r-- | system/init.lua | 53 |
3 files changed, 78 insertions, 21 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e7272..2048c92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md | |||
@@ -27,6 +27,11 @@ The scope of what is covered by the version number excludes: | |||
27 | 27 | ||
28 | ## Version history | 28 | ## Version history |
29 | 29 | ||
30 | ### version xxx, unreleased | ||
31 | |||
32 | - Docs: document readansi internal buffer for incomplete sequences. | ||
33 | - Fix: ensure to properly parse `<alt>+key` key presses | ||
34 | |||
30 | ### version 0.6.0, released 10-Apr-2025 | 35 | ### version 0.6.0, released 10-Apr-2025 |
31 | 36 | ||
32 | - Fix: when sleep returns an error, pass that on in `readkey`. | 37 | - Fix: when sleep returns an error, pass that on in `readkey`. |
diff --git a/spec/04-term_spec.lua b/spec/04-term_spec.lua index b3de461..0ce0033 100644 --- a/spec/04-term_spec.lua +++ b/spec/04-term_spec.lua | |||
@@ -928,10 +928,31 @@ describe("Terminal:", function() | |||
928 | end) | 928 | end) |
929 | 929 | ||
930 | 930 | ||
931 | it("reads ANSI escape sequences, just an '<esc>O' (<alt><O> key press)", function() | ||
932 | setbuffer("\27O") | ||
933 | assert.are.same({"\27O", "ansi"}, {system.readansi(0)}) | ||
934 | end) | ||
935 | |||
936 | |||
937 | it("reads <alt>+key key presses; <esc>+<key>", function() | ||
938 | setbuffer("\27a\27b\27c\27d") | ||
939 | assert.are.same({"\27a", "ansi"}, {system.readansi(0)}) | ||
940 | assert.are.same({"\27b", "ansi"}, {system.readansi(0)}) | ||
941 | assert.are.same({"\27c", "ansi"}, {system.readansi(0)}) | ||
942 | assert.are.same({"\27d", "ansi"}, {system.readansi(0)}) | ||
943 | end) | ||
944 | |||
945 | |||
946 | it("reads <ctrl><alt>[ key press; <esc>+<esc>", function() | ||
947 | setbuffer("\27\27\27\27") | ||
948 | assert.are.same({"\27\27", "ansi"}, {system.readansi(0)}) | ||
949 | assert.are.same({"\27\27", "ansi"}, {system.readansi(0)}) | ||
950 | end) | ||
951 | |||
952 | |||
931 | it("returns a single <esc> character if no sequence is found", function() | 953 | it("returns a single <esc> character if no sequence is found", function() |
932 | setbuffer("\27\27[A") | 954 | setbuffer("\27") |
933 | assert.are.same({"\27", "char"}, {system.readansi(0)}) | 955 | assert.are.same({"\27", "char"}, {system.readansi(0)}) |
934 | assert.are.same({"\27[A", "ansi"}, {system.readansi(0)}) | ||
935 | end) | 956 | end) |
936 | 957 | ||
937 | 958 | ||
@@ -945,6 +966,22 @@ describe("Terminal:", function() | |||
945 | assert.is.near(1, timing, 0.5) -- this also works for MacOS in CI | 966 | assert.is.near(1, timing, 0.5) -- this also works for MacOS in CI |
946 | end) | 967 | end) |
947 | 968 | ||
969 | |||
970 | it("incomplete ANSI sequences will be completed on next call", function() | ||
971 | setbuffer("\27[") | ||
972 | assert.are.same({nil, "timeout", "\27["}, {system.readansi(0)}) | ||
973 | setbuffer("A") | ||
974 | assert.are.same({"\27[A", "ansi"}, {system.readansi(0)}) | ||
975 | end) | ||
976 | |||
977 | |||
978 | it("incomplete UTF8 sequences will be completed on next call", function() | ||
979 | setbuffer(string.char(240, 159)) | ||
980 | assert.are.same({nil, "timeout", string.char(240, 159)}, {system.readansi(0)}) | ||
981 | setbuffer(string.char(154, 128)) | ||
982 | assert.are.same({"🚀", "char"}, {system.readansi(0)}) | ||
983 | end) | ||
984 | |||
948 | end) | 985 | end) |
949 | 986 | ||
950 | end) | 987 | end) |
diff --git a/system/init.lua b/system/init.lua index 28fe65c..e0e5f21 100644 --- a/system/init.lua +++ b/system/init.lua | |||
@@ -267,7 +267,6 @@ end | |||
267 | 267 | ||
268 | 268 | ||
269 | do | 269 | do |
270 | local left_over_key | ||
271 | local sequence -- table to store the sequence in progress | 270 | local sequence -- table to store the sequence in progress |
272 | local utf8_length -- length of utf8 sequence currently being processed | 271 | local utf8_length -- length of utf8 sequence currently being processed |
273 | local unpack = unpack or table.unpack | 272 | local unpack = unpack or table.unpack |
@@ -285,6 +284,8 @@ do | |||
285 | -- @treturn[2] nil in case of an error | 284 | -- @treturn[2] nil in case of an error |
286 | -- @treturn[2] string error message; `"timeout"` if the timeout was reached. | 285 | -- @treturn[2] string error message; `"timeout"` if the timeout was reached. |
287 | -- @treturn[2] string partial result in case of an error while reading a sequence, the sequence so far. | 286 | -- @treturn[2] string partial result in case of an error while reading a sequence, the sequence so far. |
287 | -- The function retains its own internal buffer, so on the next call the incomplete buffer is used to | ||
288 | -- complete the sequence. | ||
288 | function system.readansi(timeout, fsleep) | 289 | function system.readansi(timeout, fsleep) |
289 | if type(timeout) ~= "number" then | 290 | if type(timeout) ~= "number" then |
290 | error("arg #1 to readansi, expected timeout in seconds, got " .. type(timeout), 2) | 291 | error("arg #1 to readansi, expected timeout in seconds, got " .. type(timeout), 2) |
@@ -295,33 +296,47 @@ do | |||
295 | 296 | ||
296 | if not sequence then | 297 | if not sequence then |
297 | -- no sequence in progress, read a key | 298 | -- no sequence in progress, read a key |
298 | 299 | local err | |
299 | if left_over_key then | 300 | key, err = system.readkey(timeout, fsleep) |
300 | -- we still have a cached key from the last call | 301 | if key == nil then -- timeout or error |
301 | key = left_over_key | 302 | return nil, err |
302 | left_over_key = nil | ||
303 | else | ||
304 | -- read a new key | ||
305 | local err | ||
306 | key, err = system.readkey(timeout, fsleep) | ||
307 | if key == nil then -- timeout or error | ||
308 | return nil, err | ||
309 | end | ||
310 | end | 303 | end |
311 | 304 | ||
312 | if key == 27 then | 305 | if key == 27 then |
313 | -- looks like an ansi escape sequence, immediately read next char | 306 | -- looks like an ansi escape sequence, immediately read next char |
314 | -- as an heuristic against manually typing escape sequences | 307 | -- as an heuristic against manually typing escape sequences |
315 | local key2 = system.readkey(0, fsleep) | 308 | local key2 = system.readkey(0, fsleep) |
316 | if key2 ~= 91 and key2 ~= 79 then -- we expect either "[" or "O" for an ANSI sequence | 309 | if key2 == nil then |
317 | -- not the expected [ or O character, so we return the key as is | 310 | -- no key available, return the escape key, on its own |
318 | -- and store the extra key read for the next call | 311 | sequence = nil |
319 | left_over_key = key2 | ||
320 | return string.char(key), "char" | 312 | return string.char(key), "char" |
313 | |||
314 | elseif key2 == 91 then | ||
315 | -- "[" means it is for sure an ANSI sequence | ||
316 | sequence = { key, key2 } | ||
317 | |||
318 | elseif key2 == 79 then | ||
319 | -- "O" means it is either an ANSI sequence or just an <alt>+O key stroke | ||
320 | -- check if there is yet another byte available | ||
321 | local key3 = system.readkey(0, fsleep) | ||
322 | if key3 == nil then | ||
323 | -- no key available, return the <alt>O key stroke, report as ANSI | ||
324 | sequence = nil | ||
325 | return string.char(key, key2), "ansi" | ||
326 | end | ||
327 | -- it's an ANSI sequence, marked with <ESC>O | ||
328 | if (key3 >= 65 and key3 <= 90) or (key3 >= 97 and key3 <= 126) then | ||
329 | -- end of sequence, return the full sequence | ||
330 | return string.char(key, key2, key3), "ansi" | ||
331 | end | ||
332 | sequence = { key, key2, key3 } | ||
333 | |||
334 | else | ||
335 | -- not an ANSI sequence, but an <alt>+<key2> key stroke, so report as ANSI | ||
336 | sequence = nil | ||
337 | return string.char(key, key2), "ansi" | ||
321 | end | 338 | end |
322 | 339 | ||
323 | -- escape sequence detected | ||
324 | sequence = { key, key2 } | ||
325 | else | 340 | else |
326 | -- check UTF8 length | 341 | -- check UTF8 length |
327 | utf8_length = key < 128 and 1 or key < 224 and 2 or key < 240 and 3 or key < 248 and 4 | 342 | utf8_length = key < 128 and 1 or key < 224 and 2 or key < 240 and 3 or key < 248 and 4 |