diff options
| author | Thijs Schreijer <thijs@thijsschreijer.nl> | 2025-04-13 22:22:46 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-04-13 22:22:46 +0200 |
| commit | 091672f57ba57b745a51202b38af61bd240d1084 (patch) | |
| tree | dcdd0b4e65516682b17390f049fd8b1599274232 | |
| parent | 06691764e87c2979f4a00ed386e237150f055d5a (diff) | |
| download | luasystem-091672f57ba57b745a51202b38af61bd240d1084.tar.gz luasystem-091672f57ba57b745a51202b38af61bd240d1084.tar.bz2 luasystem-091672f57ba57b745a51202b38af61bd240d1084.zip | |
fix(terminal): readansi now properly handles <alt>+key key-presses (#62)
Also; documents the internal buffer and retry behaviour of readansi
| -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 |
