summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThijs Schreijer <thijs@thijsschreijer.nl>2024-05-07 19:42:13 +0200
committerThijs Schreijer <thijs@thijsschreijer.nl>2024-05-07 19:56:24 +0200
commitc8a2d1d2dacde9fea43ebbacf924707d563cf99a (patch)
tree2be118fc759855635c45ffe048b1d61a2f173248
parenta22b5c8e14105b9617c8b2000f6353b011d1d0f9 (diff)
downloadluasystem-c8a2d1d2dacde9fea43ebbacf924707d563cf99a.zip
make readkey async, and add keyboard parser; ansi + utf8
-rw-r--r--examples/read.lua65
-rw-r--r--examples/spinner.lua4
-rw-r--r--system/init.lua147
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"
-local read_input do
- local left_over_key
-
- -- Reads a single key, if it is a 27 (start of ansi escape sequence) then it reads
- -- the rest of the sequence.
- -- This function is non-blocking, and will return nil if no key is available.
- -- In case of an ANSI sequence, it will return the full sequence as a string.
- -- @return nil|string the key read, or nil if no key is available
- function read_input()
- if left_over_key then
- -- we still have a cached key, return it
- local key = left_over_key
- left_over_key = nil
- return string.char(key)
- end
-
- local key = sys.readkey()
- if key == nil then
- return nil
- end
-
- if key ~= 27 then
- return string.char(key)
- end
-
- -- looks like an ansi escape sequence, immediately read next char
- -- as an heuristic against manually typing escape sequences
- local brack = sys.readkey()
- if brack ~= 91 then
- -- not the expected [ character, so we return the key as is
- -- and store the extra key read for the next call
- left_over_key = brack
- return string.char(key)
- end
-
- -- escape sequence detected, read the rest of the sequence
- local seq = { key, brack }
- while true do
- key = sys.readkey()
- table.insert(seq, key)
- if (key >= 65 and key <= 90) or (key >= 97 and key <= 126) then
- -- end of sequence, return the full sequence
- return string.char((unpack or table.unpack)(seq))
- end
- end
- -- unreachable
- end
-end
-
-
-
print("Press a key, or 'A' to get cursor position, 'ESC' to exit")
while true do
- local key
+ local key, keytype
- -- wait for a key, and sleep a bit to not do a busy-wait
+ -- wait for a key
while not key do
- key = read_input()
- if not key then sys.sleep(0.1) end
+ key, keytype = sys.readansi(math.huge)
end
if key == "A" then io.write(get_cursor_pos); io.flush() end
-- check if we got a key or ANSI sequence
- if #key == 1 then
+ if keytype == "key" then
-- just a key
local b = key:byte()
if b < 32 then
@@ -102,10 +50,13 @@ while true do
break
end
- else
+ elseif keytype == "ansi" then
-- we got an ANSI sequence
local seq = { key:byte(1, #key) }
print("ANSI sequence received: " .. key:sub(2,-1), "(bytes: " .. table.concat(seq, ", ")..")")
+
+ else
+ print("unknown key type received: " .. tostring(keytype))
end
end
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
i = i + 1
if i > #spin then i = 1 end
- if sys.keypressed() then
- sys.readkey() -- consume key pressed
+ if sys.readkey(0) ~= nil then
+ while sys.readkey(0) ~= nil do end -- consume keys pressed
io.write(" ");
left()
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
+do
+ local _readkey = sys.readkey
+ local interval = 0.1
+
+ --- Reads a single byte from the console, with a timeout.
+ -- This function uses `system.sleep` to wait in increments of 0.1 seconds until either a byte is
+ -- available or the timeout is reached.
+ -- It returns immediately if a byte is available or if `timeout` is less than or equal to `0`.
+ -- @tparam number timeout the timeout in seconds.
+ -- @treturn[1] integer the key code of the key that was received
+ -- @treturn[2] nil if no key was read
+ -- @treturn[2] string error message; `"timeout"` if the timeout was reached.
+ function sys.readkey(timeout)
+ if type(timeout) ~= "number" then
+ error("arg #1 to readkey, expected timeout in seconds, got " .. type(timeout), 2)
+ end
+
+ local key = _readkey()
+ while key == nil and timeout > 0 do
+ sys.sleep(interval)
+ timeout = timeout - interval
+ key = _readkey()
+ end
+
+ if key then
+ return key
+ end
+ return nil, "timeout"
+ end
+end
+
+
+
+do
+ local left_over_key
+ local sequence -- table to store the sequence in progress
+ local utf8_length -- length of utf8 sequence currently being processed
+
+ -- Reads a single key, if it is the start of ansi escape sequence then it reads
+ -- the full sequence.
+ -- This function uses `system.readkey`, and hence `system.sleep` to wait until either a key is
+ -- available or the timeout is reached.
+ -- It returns immediately if a key is available or if `timeout` is less than or equal to `0`.
+ -- In case of an ANSI sequence, it will return the full sequence as a string.
+ -- @tparam number timeout the timeout in seconds.
+ -- @treturn[1] string the character that was received, or a complete ANSI sequence
+ -- @treturn[1] string the type of input: `"char"` for a single key, `"ansi"` for an ANSI sequence
+ -- @treturn[2] nil in case of an error
+ -- @treturn[2] string error message; `"timeout"` if the timeout was reached.
+ -- @treturn[2] string partial result in case of an error while reading a sequence, the sequence so far.
+ function sys.readansi(timeout)
+ if type(timeout) ~= "number" then
+ error("arg #1 to readansi, expected timeout in seconds, got " .. type(timeout), 2)
+ end
+
+ local key
+
+ if not sequence then
+ -- no sequence in progress, read a key
+
+ if left_over_key then
+ -- we still have a cached key from the last call
+ key = left_over_key
+ left_over_key = nil
+ else
+ -- read a new key
+ local err
+ key, err = sys.readkey(timeout)
+ if key == nil then -- timeout or error
+ return nil, err
+ end
+ end
+
+ if key == 27 then
+ -- looks like an ansi escape sequence, immediately read next char
+ -- as an heuristic against manually typing escape sequences
+ local key2 = sys.readkey(0)
+ if key2 ~= 91 and key2 ~= 79 then -- we expect either "[" or "O" for an ANSI sequence
+ -- not the expected [ or O character, so we return the key as is
+ -- and store the extra key read for the next call
+ left_over_key = key2
+ return string.char(key), "char"
+ end
+
+ -- escape sequence detected
+ sequence = { key, key2 }
+ else
+ -- check UTF8 length
+ utf8_length = key < 128 and 1 or key < 224 and 2 or key < 240 and 3 or key < 248 and 4
+ if utf8_length == 1 then
+ -- single byte character
+ utf8_length = nil
+ return string.char(key), "char"
+ else
+ -- UTF8 sequence detected
+ sequence = { key }
+ end
+ end
+ end
+
+ local err
+ if utf8_length then
+ -- read remainder of UTF8 sequence
+ local timeout_end = sys.gettime() + timeout
+ while true do
+ key, err = sys.readkey(timeout_end - sys.gettime())
+ if err then
+ break
+ end
+ table.insert(sequence, key)
+
+ if #sequence == utf8_length then
+ -- end of sequence, return the full sequence
+ local result = string.char((unpack or table.unpack)(sequence))
+ sequence = nil
+ utf8_length = nil
+ return result, "char"
+ end
+ end
+
+ else
+ -- read remainder of ANSI sequence
+ local timeout_end = sys.gettime() + timeout
+ while true do
+ key, err = sys.readkey(timeout_end - sys.gettime())
+ if err then
+ break
+ end
+ table.insert(sequence, key)
+
+ if (key >= 65 and key <= 90) or (key >= 97 and key <= 126) then
+ -- end of sequence, return the full sequence
+ local result = string.char((unpack or table.unpack)(sequence))
+ sequence = nil
+ return result, "ansi"
+ end
+ end
+ end
+
+ -- error, or timeout reached, return the sequence so far
+ local partial = string.char((unpack or table.unpack)(sequence))
+ return nil, err, partial
+ end
+end
+
+
+
return sys