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 |