From 2981db32130b30c9b12e7347bfdbe2e7584e9274 Mon Sep 17 00:00:00 2001 From: Thijs Schreijer Date: Sat, 1 Mar 2025 19:17:17 +0100 Subject: feat(readkey): allow a sleep function to be passed There are cases where both a blocking and non-blocking sleep is needed. In those cases just poatching system.sleep isn't good enough. Hence now we can pass a sleep function. --- CHANGELOG.md | 1 + doc_topics/03-terminal.md | 8 ++++---- examples/readline.lua | 6 +++++- system/init.lua | 21 ++++++++++++--------- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d95d9f..af9a294 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ The scope of what is covered by the version number excludes: - Feat: when detecting character display width, also accept unicode codepoints (integers), since the Lua utf8 library returns codepoints, not strings +- Feat: allow passing in a sleep function to `readkey` and `readansi` - Fix: NetBSD fix compilation, undeclared directives - Refactor: random bytes; remove deprecated API usage on Windows, move to binary api instead of /dev/urandom file on linux and bsd diff --git a/doc_topics/03-terminal.md b/doc_topics/03-terminal.md index 9bad359..5bdf543 100644 --- a/doc_topics/03-terminal.md +++ b/doc_topics/03-terminal.md @@ -112,10 +112,10 @@ To use non-blocking input here's how to set it up: sys.setnonblock(io.stdin, true) -Both functions require a timeout to be provided which allows for proper asynchronous -code to be written. Since the underlying sleep method used is `system.sleep`, just patching -that function with a coroutine based yielding one should be all that is needed to make -the result work with asynchroneous coroutine schedulers. +Both `readkey` and `readansi` require a timeout to be provided which allows for proper asynchronous +code to be written. The underlying sleep method to use can be provided, and defaults to `system.sleep`. +Just passing a coroutine enabled sleep method should be all that is needed to make +the result work with asynchroneous coroutine schedulers. Alternatively just patch `system.sleep`. ### 3.3.2 Blocking input diff --git a/examples/readline.lua b/examples/readline.lua index ff215dd..98da267 100644 --- a/examples/readline.lua +++ b/examples/readline.lua @@ -144,10 +144,12 @@ readline.__index = readline -- @tparam[opt=""] string opts.value the default value -- @tparam[opt=`#value`] number opts.position of the cursor in the input -- @tparam[opt={"\10"/"\13"}] table opts.exit_keys an array of keys that will cause the readline to exit +-- @tparam[opt=system.sleep] function opts.fsleep the sleep function to use (see `system.readansi`) -- @treturn readline the new readline object function readline.new(opts) local value = utf8parse(opts.value or "") local prompt = utf8parse(opts.prompt or "") + local fsleep = opts.fsleep or sys.sleep local pos = math.floor(opts.position or (#value + 1)) pos = math.max(math.min(pos, (#value + 1)), 1) local len = math.floor(opts.max_length or 80) @@ -175,6 +177,7 @@ function readline.new(opts) position = pos, -- the current position in the input drawn_before = false, -- if the prompt has been drawn exit_keys = exit_keys, -- the keys that will cause the readline to exit + fsleep = fsleep, -- the sleep function to use } setmetatable(self, readline) @@ -413,7 +416,7 @@ function readline:__call(timeout, redraw) local timeout_end = sys.gettime() + timeout while true do - local key, keytype = sys.readansi(timeout_end - sys.gettime()) + local key, keytype = sys.readansi(timeout_end - sys.gettime(), self.fsleep) if not key then -- error or timeout return nil, keytype @@ -458,6 +461,7 @@ local rl = readline.new{ value = "Hello, 你-好 World 🚀!", -- position = 2, exit_keys = {key_sequences.enter, "\27", "\t", "\27[Z"}, -- enter, escape, tab, shift-tab + fsleep = sys.sleep, } diff --git a/system/init.lua b/system/init.lua index e99d0d4..a81978e 100644 --- a/system/init.lua +++ b/system/init.lua @@ -229,17 +229,18 @@ end do --- Reads a single byte from the console, with a timeout. - -- This function uses `system.sleep` to wait until either a byte is available or the timeout is reached. + -- This function uses `fsleep` to wait until either a byte is available or the timeout is reached. -- The sleep period is exponentially backing off, starting at 0.0125 seconds, with a maximum of 0.2 seconds. -- It returns immediately if a byte is available or if `timeout` is less than or equal to `0`. -- -- Using `system.readansi` is preferred over this function. Since this function can leave stray/invalid -- byte-sequences in the input buffer, while `system.readansi` reads full ANSI and UTF8 sequences. -- @tparam number timeout the timeout in seconds. + -- @tparam[opt=system.sleep] function fsleep the function to call for sleeping. -- @treturn[1] byte the byte value that was read. -- @treturn[2] nil if no key was read -- @treturn[2] string error message; `"timeout"` if the timeout was reached. - function system.readkey(timeout) + function system.readkey(timeout, fsleep) if type(timeout) ~= "number" then error("arg #1 to readkey, expected timeout in seconds, got " .. type(timeout), 2) end @@ -247,7 +248,7 @@ do local interval = 0.0125 local key = system._readkey() while key == nil and timeout > 0 do - system.sleep(math.min(interval, timeout)) + (fsleep or system.sleep)(math.min(interval, timeout)) timeout = timeout - interval interval = math.min(0.2, interval * 2) key = system._readkey() @@ -270,20 +271,22 @@ do --- Reads a single key, if it is the start of ansi escape sequence then it reads -- the full sequence. The key can be a multi-byte string in case of multibyte UTF-8 character. - -- This function uses `system.readkey`, and hence `system.sleep` to wait until either a key is + -- This function uses `system.readkey`, and hence `fsleep` 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. + -- @tparam[opt=system.sleep] function fsleep the function to call for sleeping. -- @treturn[1] string the character that was received (can be multi-byte), 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 system.readansi(timeout) + function system.readansi(timeout, fsleep) if type(timeout) ~= "number" then error("arg #1 to readansi, expected timeout in seconds, got " .. type(timeout), 2) end + fsleep = fsleep or system.sleep local key @@ -297,7 +300,7 @@ do else -- read a new key local err - key, err = system.readkey(timeout) + key, err = system.readkey(timeout, fsleep) if key == nil then -- timeout or error return nil, err end @@ -306,7 +309,7 @@ do if key == 27 then -- looks like an ansi escape sequence, immediately read next char -- as an heuristic against manually typing escape sequences - local key2 = system.readkey(0) + local key2 = system.readkey(0, fsleep) 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 @@ -335,7 +338,7 @@ do -- read remainder of UTF8 sequence local timeout_end = system.gettime() + timeout while true do - key, err = system.readkey(timeout_end - system.gettime()) + key, err = system.readkey(timeout_end - system.gettime(), fsleep) if err then break end @@ -354,7 +357,7 @@ do -- read remainder of ANSI sequence local timeout_end = system.gettime() + timeout while true do - key, err = system.readkey(timeout_end - system.gettime()) + key, err = system.readkey(timeout_end - system.gettime(), fsleep) if err then break end -- cgit v1.2.3-55-g6feb