diff options
| author | Thijs Schreijer <thijs@thijsschreijer.nl> | 2024-05-07 19:42:13 +0200 |
|---|---|---|
| committer | Thijs Schreijer <thijs@thijsschreijer.nl> | 2024-05-07 19:56:24 +0200 |
| commit | c8a2d1d2dacde9fea43ebbacf924707d563cf99a (patch) | |
| tree | 2be118fc759855635c45ffe048b1d61a2f173248 | |
| parent | a22b5c8e14105b9617c8b2000f6353b011d1d0f9 (diff) | |
| download | luasystem-c8a2d1d2dacde9fea43ebbacf924707d563cf99a.tar.gz luasystem-c8a2d1d2dacde9fea43ebbacf924707d563cf99a.tar.bz2 luasystem-c8a2d1d2dacde9fea43ebbacf924707d563cf99a.zip | |
make readkey async, and add keyboard parser; ansi + utf8
| -rw-r--r-- | examples/read.lua | 65 | ||||
| -rw-r--r-- | examples/spinner.lua | 4 | ||||
| -rw-r--r-- | system/init.lua | 147 |
3 files changed, 157 insertions, 59 deletions
diff --git a/examples/read.lua b/examples/read.lua index 7a1c747..bd5cbff 100644 --- a/examples/read.lua +++ b/examples/read.lua | |||
| @@ -25,71 +25,19 @@ local get_cursor_pos = "\27[6n" | |||
| 25 | 25 | ||
| 26 | 26 | ||
| 27 | 27 | ||
| 28 | local read_input do | ||
| 29 | local left_over_key | ||
| 30 | |||
| 31 | -- Reads a single key, if it is a 27 (start of ansi escape sequence) then it reads | ||
| 32 | -- the rest of the sequence. | ||
| 33 | -- This function is non-blocking, and will return nil if no key is available. | ||
| 34 | -- In case of an ANSI sequence, it will return the full sequence as a string. | ||
| 35 | -- @return nil|string the key read, or nil if no key is available | ||
| 36 | function read_input() | ||
| 37 | if left_over_key then | ||
| 38 | -- we still have a cached key, return it | ||
| 39 | local key = left_over_key | ||
| 40 | left_over_key = nil | ||
| 41 | return string.char(key) | ||
| 42 | end | ||
| 43 | |||
| 44 | local key = sys.readkey() | ||
| 45 | if key == nil then | ||
| 46 | return nil | ||
| 47 | end | ||
| 48 | |||
| 49 | if key ~= 27 then | ||
| 50 | return string.char(key) | ||
| 51 | end | ||
| 52 | |||
| 53 | -- looks like an ansi escape sequence, immediately read next char | ||
| 54 | -- as an heuristic against manually typing escape sequences | ||
| 55 | local brack = sys.readkey() | ||
| 56 | if brack ~= 91 then | ||
| 57 | -- not the expected [ character, so we return the key as is | ||
| 58 | -- and store the extra key read for the next call | ||
| 59 | left_over_key = brack | ||
| 60 | return string.char(key) | ||
| 61 | end | ||
| 62 | |||
| 63 | -- escape sequence detected, read the rest of the sequence | ||
| 64 | local seq = { key, brack } | ||
| 65 | while true do | ||
| 66 | key = sys.readkey() | ||
| 67 | table.insert(seq, key) | ||
| 68 | if (key >= 65 and key <= 90) or (key >= 97 and key <= 126) then | ||
| 69 | -- end of sequence, return the full sequence | ||
| 70 | return string.char((unpack or table.unpack)(seq)) | ||
| 71 | end | ||
| 72 | end | ||
| 73 | -- unreachable | ||
| 74 | end | ||
| 75 | end | ||
| 76 | |||
| 77 | |||
| 78 | |||
| 79 | print("Press a key, or 'A' to get cursor position, 'ESC' to exit") | 28 | print("Press a key, or 'A' to get cursor position, 'ESC' to exit") |
| 80 | while true do | 29 | while true do |
| 81 | local key | 30 | local key, keytype |
| 82 | 31 | ||
| 83 | -- wait for a key, and sleep a bit to not do a busy-wait | 32 | -- wait for a key |
| 84 | while not key do | 33 | while not key do |
| 85 | key = read_input() | 34 | key, keytype = sys.readansi(math.huge) |
| 86 | if not key then sys.sleep(0.1) end | ||
| 87 | end | 35 | end |
| 88 | 36 | ||
| 89 | if key == "A" then io.write(get_cursor_pos); io.flush() end | 37 | if key == "A" then io.write(get_cursor_pos); io.flush() end |
| 90 | 38 | ||
| 91 | -- check if we got a key or ANSI sequence | 39 | -- check if we got a key or ANSI sequence |
| 92 | if #key == 1 then | 40 | if keytype == "key" then |
| 93 | -- just a key | 41 | -- just a key |
| 94 | local b = key:byte() | 42 | local b = key:byte() |
| 95 | if b < 32 then | 43 | if b < 32 then |
| @@ -102,10 +50,13 @@ while true do | |||
| 102 | break | 50 | break |
| 103 | end | 51 | end |
| 104 | 52 | ||
| 105 | else | 53 | elseif keytype == "ansi" then |
| 106 | -- we got an ANSI sequence | 54 | -- we got an ANSI sequence |
| 107 | local seq = { key:byte(1, #key) } | 55 | local seq = { key:byte(1, #key) } |
| 108 | print("ANSI sequence received: " .. key:sub(2,-1), "(bytes: " .. table.concat(seq, ", ")..")") | 56 | print("ANSI sequence received: " .. key:sub(2,-1), "(bytes: " .. table.concat(seq, ", ")..")") |
| 57 | |||
| 58 | else | ||
| 59 | print("unknown key type received: " .. tostring(keytype)) | ||
| 109 | end | 60 | end |
| 110 | end | 61 | end |
| 111 | 62 | ||
diff --git a/examples/spinner.lua b/examples/spinner.lua index 5526adc..e518e60 100644 --- a/examples/spinner.lua +++ b/examples/spinner.lua | |||
| @@ -44,8 +44,8 @@ local spinner do | |||
| 44 | i = i + 1 | 44 | i = i + 1 |
| 45 | if i > #spin then i = 1 end | 45 | if i > #spin then i = 1 end |
| 46 | 46 | ||
| 47 | if sys.keypressed() then | 47 | if sys.readkey(0) ~= nil then |
| 48 | sys.readkey() -- consume key pressed | 48 | while sys.readkey(0) ~= nil do end -- consume keys pressed |
| 49 | io.write(" "); | 49 | io.write(" "); |
| 50 | left() | 50 | left() |
| 51 | showCursor() | 51 | showCursor() |
diff --git a/system/init.lua b/system/init.lua index 93dd488..94c2f09 100644 --- a/system/init.lua +++ b/system/init.lua | |||
| @@ -207,4 +207,151 @@ end | |||
| 207 | 207 | ||
| 208 | 208 | ||
| 209 | 209 | ||
| 210 | do | ||
| 211 | local _readkey = sys.readkey | ||
| 212 | local interval = 0.1 | ||
| 213 | |||
| 214 | --- Reads a single byte from the console, with a timeout. | ||
| 215 | -- This function uses `system.sleep` to wait in increments of 0.1 seconds until either a byte is | ||
| 216 | -- available or the timeout is reached. | ||
| 217 | -- It returns immediately if a byte is available or if `timeout` is less than or equal to `0`. | ||
| 218 | -- @tparam number timeout the timeout in seconds. | ||
| 219 | -- @treturn[1] integer the key code of the key that was received | ||
| 220 | -- @treturn[2] nil if no key was read | ||
| 221 | -- @treturn[2] string error message; `"timeout"` if the timeout was reached. | ||
| 222 | function sys.readkey(timeout) | ||
| 223 | if type(timeout) ~= "number" then | ||
| 224 | error("arg #1 to readkey, expected timeout in seconds, got " .. type(timeout), 2) | ||
| 225 | end | ||
| 226 | |||
| 227 | local key = _readkey() | ||
| 228 | while key == nil and timeout > 0 do | ||
| 229 | sys.sleep(interval) | ||
| 230 | timeout = timeout - interval | ||
| 231 | key = _readkey() | ||
| 232 | end | ||
| 233 | |||
| 234 | if key then | ||
| 235 | return key | ||
| 236 | end | ||
| 237 | return nil, "timeout" | ||
| 238 | end | ||
| 239 | end | ||
| 240 | |||
| 241 | |||
| 242 | |||
| 243 | do | ||
| 244 | local left_over_key | ||
| 245 | local sequence -- table to store the sequence in progress | ||
| 246 | local utf8_length -- length of utf8 sequence currently being processed | ||
| 247 | |||
| 248 | -- Reads a single key, if it is the start of ansi escape sequence then it reads | ||
| 249 | -- the full sequence. | ||
| 250 | -- This function uses `system.readkey`, and hence `system.sleep` to wait until either a key is | ||
| 251 | -- available or the timeout is reached. | ||
| 252 | -- It returns immediately if a key is available or if `timeout` is less than or equal to `0`. | ||
| 253 | -- In case of an ANSI sequence, it will return the full sequence as a string. | ||
| 254 | -- @tparam number timeout the timeout in seconds. | ||
| 255 | -- @treturn[1] string the character that was received, or a complete ANSI sequence | ||
| 256 | -- @treturn[1] string the type of input: `"char"` for a single key, `"ansi"` for an ANSI sequence | ||
| 257 | -- @treturn[2] nil in case of an error | ||
| 258 | -- @treturn[2] string error message; `"timeout"` if the timeout was reached. | ||
| 259 | -- @treturn[2] string partial result in case of an error while reading a sequence, the sequence so far. | ||
| 260 | function sys.readansi(timeout) | ||
| 261 | if type(timeout) ~= "number" then | ||
| 262 | error("arg #1 to readansi, expected timeout in seconds, got " .. type(timeout), 2) | ||
| 263 | end | ||
| 264 | |||
| 265 | local key | ||
| 266 | |||
| 267 | if not sequence then | ||
| 268 | -- no sequence in progress, read a key | ||
| 269 | |||
| 270 | if left_over_key then | ||
| 271 | -- we still have a cached key from the last call | ||
| 272 | key = left_over_key | ||
| 273 | left_over_key = nil | ||
| 274 | else | ||
| 275 | -- read a new key | ||
| 276 | local err | ||
| 277 | key, err = sys.readkey(timeout) | ||
| 278 | if key == nil then -- timeout or error | ||
| 279 | return nil, err | ||
| 280 | end | ||
| 281 | end | ||
| 282 | |||
| 283 | if key == 27 then | ||
| 284 | -- looks like an ansi escape sequence, immediately read next char | ||
| 285 | -- as an heuristic against manually typing escape sequences | ||
| 286 | local key2 = sys.readkey(0) | ||
| 287 | if key2 ~= 91 and key2 ~= 79 then -- we expect either "[" or "O" for an ANSI sequence | ||
| 288 | -- not the expected [ or O character, so we return the key as is | ||
| 289 | -- and store the extra key read for the next call | ||
| 290 | left_over_key = key2 | ||
| 291 | return string.char(key), "char" | ||
| 292 | end | ||
| 293 | |||
| 294 | -- escape sequence detected | ||
| 295 | sequence = { key, key2 } | ||
| 296 | else | ||
| 297 | -- check UTF8 length | ||
| 298 | utf8_length = key < 128 and 1 or key < 224 and 2 or key < 240 and 3 or key < 248 and 4 | ||
| 299 | if utf8_length == 1 then | ||
| 300 | -- single byte character | ||
| 301 | utf8_length = nil | ||
| 302 | return string.char(key), "char" | ||
| 303 | else | ||
| 304 | -- UTF8 sequence detected | ||
| 305 | sequence = { key } | ||
| 306 | end | ||
| 307 | end | ||
| 308 | end | ||
| 309 | |||
| 310 | local err | ||
| 311 | if utf8_length then | ||
| 312 | -- read remainder of UTF8 sequence | ||
| 313 | local timeout_end = sys.gettime() + timeout | ||
| 314 | while true do | ||
| 315 | key, err = sys.readkey(timeout_end - sys.gettime()) | ||
| 316 | if err then | ||
| 317 | break | ||
| 318 | end | ||
| 319 | table.insert(sequence, key) | ||
| 320 | |||
| 321 | if #sequence == utf8_length then | ||
| 322 | -- end of sequence, return the full sequence | ||
| 323 | local result = string.char((unpack or table.unpack)(sequence)) | ||
| 324 | sequence = nil | ||
| 325 | utf8_length = nil | ||
| 326 | return result, "char" | ||
| 327 | end | ||
| 328 | end | ||
| 329 | |||
| 330 | else | ||
| 331 | -- read remainder of ANSI sequence | ||
| 332 | local timeout_end = sys.gettime() + timeout | ||
| 333 | while true do | ||
| 334 | key, err = sys.readkey(timeout_end - sys.gettime()) | ||
| 335 | if err then | ||
| 336 | break | ||
| 337 | end | ||
| 338 | table.insert(sequence, key) | ||
| 339 | |||
| 340 | if (key >= 65 and key <= 90) or (key >= 97 and key <= 126) then | ||
| 341 | -- end of sequence, return the full sequence | ||
| 342 | local result = string.char((unpack or table.unpack)(sequence)) | ||
| 343 | sequence = nil | ||
| 344 | return result, "ansi" | ||
| 345 | end | ||
| 346 | end | ||
| 347 | end | ||
| 348 | |||
| 349 | -- error, or timeout reached, return the sequence so far | ||
| 350 | local partial = string.char((unpack or table.unpack)(sequence)) | ||
| 351 | return nil, err, partial | ||
| 352 | end | ||
| 353 | end | ||
| 354 | |||
| 355 | |||
| 356 | |||
| 210 | return sys | 357 | return sys |
